Skip to content

merge upstream #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,29 @@ Most of our development takes place in the `internal` directory, and most behavi

Most development on the codebase is in Go.
Standard Go commands and practices apply, but we primarily use a tool called `hereby` to build, run tests, and other tasks.
Feel free to install `hereby` globally (`npm install -g hereby`) if it is easier, and run `hereby --list` to see all available commands.
Run `npx hereby --list` to see all available commands.

```sh
hereby build # Build the project
hereby test # Run tests
hereby format # Format the code
hereby lint # Run linters
npx hereby build # Build the project
npx hereby test # Run tests
npx hereby format # Format the code
npx hereby lint # Run linters

# To run a specific compiler test:
go test -run='TestSubmodule/<test name>' ./internal/testrunner # For submodule tests in _submodules/TypeScript
go test -run='TestLocal/<test name>' ./internal/testrunner # For local tests in testdata/tests/cases
```

Always make sure code is formatted, linted, and tested before sending a pull request.
Always make sure code is formatted, linted, and tested before sending a pull request.

## Compiler Features, Fixes, and Tests

When fixing a bug or implementing a new feature, at least one minimal test case should always be added in advance to verify the fix.
This project primarily uses snapshot/baseline/golden tests rather than unit tests.
New compiler tests are written in `.ts`/`.tsx` files in the directory `testdata/tests/cases/compiler/`, and are written in the following format:

**Note:** Issues with editor features cannot be tested with compiler tests in `testdata/tests/cases/`. Editor functionality requires integration testing with the language server.

```ts
// @target: esnext
// @module: preserve
Expand Down Expand Up @@ -79,6 +85,12 @@ It is ideal to implement features and fixes in the following order, and commit c

It is fine to implement more and more of a feature across commits, but be sure to update baselines every time so that reviewers can measure progress.

## Code Porting Reference

The code in `internal` is ported from the code in `_submodules/TypeScript`.
When implementing features or fixing bugs, those files should be searched for similar functions when code is either missing or potentially wrong.
The TypeScript submodule serves as the reference implementation for behavior and functionality.

# Other Instructions

- Do not add or change existing dependencies unless asked to.
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/init@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1
with:
config-file: ./.github/codeql/codeql-configuration.yml
# Override language selection by uncommenting this and choosing your languages
Expand All @@ -58,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below).
- name: Autobuild
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/autobuild@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -72,4 +72,4 @@ jobs:
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
uses: github/codeql-action/analyze@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1
5 changes: 5 additions & 0 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ jobs:
- uses: dtolnay/rust-toolchain@fcf085fcb4b4b8f63f96906cd713eb52181b5ea4 # stable
- uses: ./.github/actions/setup-go
with:
# Updated to 1.25.0-rc.1 to improve compilation time
go-version: '>=1.25.0-rc.1'
cache-name: copilot-setup-steps
- run: npm i -g @playwright/mcp@0.0.28
- run: npm ci
# pull dprint caches before network access is blocked
- run: npx hereby check:format || true
# cache build and lint operations
- run: npx hereby build || true
- run: npx hereby lint || true
25 changes: 25 additions & 0 deletions internal/compiler/fileloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type processedFiles struct {
// List of present unsupported extensions
unsupportedExtensions []string
sourceFilesFoundSearchingNodeModules collections.Set[tspath.Path]
fileLoadDiagnostics *ast.DiagnosticsCollection
}

type jsxRuntimeImportSpecifier struct {
Expand Down Expand Up @@ -132,6 +133,7 @@ func processAllProgramFiles(
var unsupportedExtensions []string
var sourceFilesFoundSearchingNodeModules collections.Set[tspath.Path]
var libFileSet collections.Set[tspath.Path]
fileLoadDiagnostics := &ast.DiagnosticsCollection{}

loader.parseTasks.collect(&loader, loader.rootTasks, func(task *parseTask, _ []tspath.Path) {
if task.isRedirected {
Expand Down Expand Up @@ -159,6 +161,7 @@ func processAllProgramFiles(
resolvedModules[path] = task.resolutionsInFile
typeResolutionsInFile[path] = task.typeResolutionsInFile
sourceFileMetaDatas[path] = task.metadata

if task.jsxRuntimeImportSpecifier != nil {
if jsxRuntimeImportSpecifiers == nil {
jsxRuntimeImportSpecifiers = make(map[tspath.Path]*jsxRuntimeImportSpecifier, totalFileCount)
Expand All @@ -183,8 +186,28 @@ func processAllProgramFiles(

allFiles := append(libFiles, files...)

for _, resolutions := range resolvedModules {
for _, resolvedModule := range resolutions {
for _, diag := range resolvedModule.ResolutionDiagnostics {
fileLoadDiagnostics.Add(diag)
}
}
}
for _, typeResolutions := range typeResolutionsInFile {
for _, resolvedTypeRef := range typeResolutions {
for _, diag := range resolvedTypeRef.ResolutionDiagnostics {
fileLoadDiagnostics.Add(diag)
}
}
}

loader.pathForLibFileResolutions.Range(func(key tspath.Path, value module.ModeAwareCache[*module.ResolvedModule]) bool {
resolvedModules[key] = value
for _, resolvedModule := range value {
for _, diag := range resolvedModule.ResolutionDiagnostics {
fileLoadDiagnostics.Add(diag)
}
}
return true
})

Expand All @@ -201,6 +224,7 @@ func processAllProgramFiles(
unsupportedExtensions: unsupportedExtensions,
sourceFilesFoundSearchingNodeModules: sourceFilesFoundSearchingNodeModules,
libFiles: libFileSet,
fileLoadDiagnostics: fileLoadDiagnostics,
}
}

Expand Down Expand Up @@ -372,6 +396,7 @@ func (p *fileLoader) resolveTypeReferenceDirectives(t *parseTask) {
resolutionMode := getModeForTypeReferenceDirectiveInFile(ref, file, meta, module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect))
resolved := p.resolver.ResolveTypeReferenceDirective(ref.FileName, file.FileName(), resolutionMode, redirect)
typeResolutionsInFile[module.ModeAwareCacheKey{Name: ref.FileName, Mode: resolutionMode}] = resolved

if resolved.IsResolved() {
t.addSubTask(resolvedRef{
fileName: resolved.ResolvedFileName,
Expand Down
1 change: 1 addition & 0 deletions internal/compiler/parsetask.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type parseTask struct {
metadata ast.SourceFileMetaData
resolutionsInFile module.ModeAwareCache[*module.ResolvedModule]
typeResolutionsInFile module.ModeAwareCache[*module.ResolvedTypeReferenceDirective]
resolutionDiagnostics []*ast.Diagnostic
importHelpersImportSpecifier *ast.Node
jsxRuntimeImportSpecifier *jsxRuntimeImportSpecifier
increaseDepth bool
Expand Down
5 changes: 5 additions & 0 deletions internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ func (p *Program) GetSuggestionDiagnostics(ctx context.Context, sourceFile *ast.
return p.getDiagnosticsHelper(ctx, sourceFile, true /*ensureBound*/, true /*ensureChecked*/, p.getSuggestionDiagnosticsForFile)
}

func (p *Program) GetProgramDiagnostics() []*ast.Diagnostic {
// !!!
return SortAndDeduplicateDiagnostics(p.fileLoadDiagnostics.GetDiagnostics())
}

func (p *Program) GetGlobalDiagnostics(ctx context.Context) []*ast.Diagnostic {
var globalDiagnostics []*ast.Diagnostic
checkers, done := p.checkerPool.GetAllCheckers(ctx)
Expand Down
1 change: 1 addition & 0 deletions internal/execute/tsc.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ func emitFilesAndReportErrors(sys System, program *compiler.Program, reportDiagn
configFileParsingDiagnosticsLength := len(allDiagnostics)

allDiagnostics = append(allDiagnostics, program.GetSyntacticDiagnostics(ctx, nil)...)
allDiagnostics = append(allDiagnostics, program.GetProgramDiagnostics()...)

if len(allDiagnostics) == configFileParsingDiagnosticsLength {
// Options diagnostics include global diagnostics (even though we collect them separately),
Expand Down
122 changes: 97 additions & 25 deletions internal/fourslash/_scripts/convertFourslash.mts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ function parseFourslashStatement(statement: ts.Statement): Cmd[] | undefined {
if (namespace.text === "verify" && func.text === "completions") {
return parseVerifyCompletionsArgs(callExpression.arguments);
}
// `goTo.marker(...)`
if (namespace.text === "goTo" && func.text === "marker") {
return parseGoToMarkerArgs(callExpression.arguments);
// `goTo....`
if (namespace.text === "goTo") {
return parseGoToArgs(callExpression.arguments, func.text);
}
// `edit....`
if (namespace.text === "edit") {
Expand Down Expand Up @@ -208,20 +208,83 @@ function getGoStringLiteral(text: string): string {
return `${JSON.stringify(text)}`;
}

function parseGoToMarkerArgs(args: readonly ts.Expression[]): GoToMarkerCmd[] | undefined {
if (args.length !== 1) {
console.error(`Expected exactly one argument in goTo.marker, got ${args.length}`);
return undefined;
}
const arg = args[0];
if (!ts.isStringLiteral(arg)) {
console.error(`Unrecognized argument in goTo.marker: ${arg.getText()}`);
return undefined;
function parseGoToArgs(args: readonly ts.Expression[], funcName: string): GoToCmd[] | undefined {
switch (funcName) {
case "marker":
const arg = args[0];
if (arg === undefined) {
return [{
kind: "goTo",
funcName: "marker",
args: [`""`],
}];
}
if (!ts.isStringLiteral(arg)) {
console.error(`Unrecognized argument in goTo.marker: ${arg.getText()}`);
return undefined;
}
return [{
kind: "goTo",
funcName: "marker",
args: [getGoStringLiteral(arg.text)],
}];
case "file":
if (args.length !== 1) {
console.error(`Expected a single argument in goTo.file, got ${args.map(arg => arg.getText()).join(", ")}`);
return undefined;
}
if (ts.isStringLiteral(args[0])) {
return [{
kind: "goTo",
funcName: "file",
args: [getGoStringLiteral(args[0].text)],
}];
}
else if (ts.isNumericLiteral(args[0])) {
return [{
kind: "goTo",
funcName: "fileNumber",
args: [args[0].text],
}];
}
console.error(`Expected string or number literal argument in goTo.file, got ${args[0].getText()}`);
return undefined;
case "position":
if (args.length !== 1 || !ts.isNumericLiteral(args[0])) {
console.error(`Expected a single numeric literal argument in goTo.position, got ${args.map(arg => arg.getText()).join(", ")}`);
return undefined;
}
return [{
kind: "goTo",
funcName: "position",
args: [`${args[0].text}`],
}];
case "eof":
return [{
kind: "goTo",
funcName: "EOF",
args: [],
}];
case "bof":
return [{
kind: "goTo",
funcName: "BOF",
args: [],
}];
case "select":
if (args.length !== 2 || !ts.isStringLiteral(args[0]) || !ts.isStringLiteral(args[1])) {
console.error(`Expected two string literal arguments in goTo.select, got ${args.map(arg => arg.getText()).join(", ")}`);
return undefined;
}
return [{
kind: "goTo",
funcName: "select",
args: [getGoStringLiteral(args[0].text), getGoStringLiteral(args[1].text)],
}];
default:
console.error(`Unrecognized goTo function: ${funcName}`);
return undefined;
}
return [{
kind: "goToMarker",
marker: getGoStringLiteral(arg.text),
}];
}

function parseVerifyCompletionsArgs(args: readonly ts.Expression[]): VerifyCompletionsCmd[] | undefined {
Expand Down Expand Up @@ -304,6 +367,7 @@ function parseVerifyCompletionArg(arg: ts.Expression): VerifyCompletionsCmd | un
break;
case "exact":
case "includes":
case "unsorted":
if (init.getText() === "undefined") {
return {
kind: "verifyCompletions",
Expand Down Expand Up @@ -385,9 +449,12 @@ function parseVerifyCompletionArg(arg: ts.Expression): VerifyCompletionsCmd | un
if (propName === "includes") {
(goArgs ??= {}).includes = expected;
}
else {
else if (propName === "exact") {
(goArgs ??= {}).exact = expected;
}
else {
(goArgs ??= {}).unsorted = expected;
}
break;
case "excludes":
let excludes = "[]string{";
Expand Down Expand Up @@ -659,19 +726,22 @@ interface VerifyCompletionsArgs {
includes?: string;
excludes?: string;
exact?: string;
unsorted?: string;
}

interface GoToMarkerCmd {
kind: "goToMarker";
marker: string;
interface GoToCmd {
kind: "goTo";
// !!! `selectRange` and `rangeStart` require parsing variables and `test.ranges()[n]`
funcName: "marker" | "file" | "fileNumber" | "EOF" | "BOF" | "position" | "select";
args: string[];
}

interface EditCmd {
kind: "edit";
goStatement: string;
}

type Cmd = VerifyCompletionsCmd | GoToMarkerCmd | EditCmd;
type Cmd = VerifyCompletionsCmd | GoToCmd | EditCmd;

function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: VerifyCompletionsCmd): string {
let expectedList = "nil";
Expand All @@ -680,6 +750,7 @@ function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: Ve
if (args.includes) expected.push(`Includes: ${args.includes},`);
if (args.excludes) expected.push(`Excludes: ${args.excludes},`);
if (args.exact) expected.push(`Exact: ${args.exact},`);
if (args.unsorted) expected.push(`Unsorted: ${args.unsorted},`);
// !!! isIncomplete
const commitCharacters = isNewIdentifierLocation ? "[]string{}" : "defaultCommitCharacters";
expectedList = `&fourslash.CompletionsExpectedList{
Expand All @@ -696,16 +767,17 @@ function generateVerifyCompletions({ marker, args, isNewIdentifierLocation }: Ve
return `f.VerifyCompletions(t, ${marker}, ${expectedList})`;
}

function generateGoToMarker({ marker }: GoToMarkerCmd): string {
return `f.GoToMarker(t, ${marker})`;
function generateGoToCommand({ funcName, args }: GoToCmd): string {
const funcNameCapitalized = funcName.charAt(0).toUpperCase() + funcName.slice(1);
return `f.GoTo${funcNameCapitalized}(t, ${args.join(", ")})`;
}

function generateCmd(cmd: Cmd): string {
switch (cmd.kind) {
case "verifyCompletions":
return generateVerifyCompletions(cmd as VerifyCompletionsCmd);
case "goToMarker":
return generateGoToMarker(cmd as GoToMarkerCmd);
case "goTo":
return generateGoToCommand(cmd as GoToCmd);
case "edit":
return cmd.goStatement;
default:
Expand Down
Loading