From 8c45bb6c231faad4a972dbd2631a73e0ac1ae982 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:54:37 +0000 Subject: [PATCH 1/2] deps(deps): bump microsoft/typescript-go from `487baf0` to `8a74937` Bumps [microsoft/typescript-go](https://github.com/microsoft/typescript-go) from `487baf0` to `8a74937`. - [Release notes](https://github.com/microsoft/typescript-go/releases) - [Commits](https://github.com/microsoft/typescript-go/compare/487baf0cc74a684977526c02a0e9c45dd7bb3823...8a749379d556f1bb4044218a1f94bc90fbfb6a03) --- updated-dependencies: - dependency-name: microsoft/typescript-go dependency-version: 8a749379d556f1bb4044218a1f94bc90fbfb6a03 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- microsoft/typescript-go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsoft/typescript-go b/microsoft/typescript-go index 487baf0cc..8a749379d 160000 --- a/microsoft/typescript-go +++ b/microsoft/typescript-go @@ -1 +1 @@ -Subproject commit 487baf0cc74a684977526c02a0e9c45dd7bb3823 +Subproject commit 8a749379d556f1bb4044218a1f94bc90fbfb6a03 From 525e3e40f359a97fb9a53fc745ffba448fce3a6f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 22:03:40 +0000 Subject: [PATCH 2/2] chore(sync): mirror internal packages into pkg/ (auto) --- go.mod | 8 +- go.sum | 16 +-- pkg/api/proto.go | 27 +++++ pkg/api/session.go | 110 ++++++++++++++++++ pkg/execute/tsctests/tsc_test.go | 38 ++++++ pkg/execute/tsctests/tscbuild_test.go | 37 ++++++ pkg/project/session.go | 38 ++++-- pkg/project/snapshot_test.go | 24 ++++ pkg/tsoptions/parsinghelpers.go | 33 ++++-- pkg/tsoptions/tsconfigparsing.go | 54 ++++++--- ...eports-invalid-project-reference-fields.js | 71 +++++++++++ ...-project-references-have-invalid-fields.js | 90 ++++++++++++++ 12 files changed, 504 insertions(+), 42 deletions(-) create mode 100644 testdata/baselines/reference/tsbuild/configFileErrors/reports-invalid-project-reference-fields.js create mode 100644 testdata/baselines/reference/tsc/projectReferences/when-project-references-have-invalid-fields.js diff --git a/go.mod b/go.mod index 5924f1011..bc9b7bc12 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 github.com/google/go-cmp v0.7.0 - github.com/mackerelio/go-osstat v0.2.8 + github.com/mackerelio/go-osstat v0.2.7 github.com/peter-evans/patience v0.3.0 github.com/zeebo/xxh3 v1.1.0 - golang.org/x/sync v0.22.0 - golang.org/x/sys v0.47.0 - golang.org/x/text v0.40.0 + golang.org/x/sync v0.21.0 + golang.org/x/sys v0.46.0 + golang.org/x/text v0.38.0 gotest.tools/v3 v3.5.2 ) diff --git a/go.sum b/go.sum index c14482c26..4b88773b8 100644 --- a/go.sum +++ b/go.sum @@ -6,19 +6,19 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/mackerelio/go-osstat v0.2.8 h1:I2duicTaCGWoM53XwAwA9OIe1inu0xnVs8/pqOWWVr4= -github.com/mackerelio/go-osstat v0.2.8/go.mod h1:SyS3XxKdoSKJnTGTkN5Yrh6VUQVuAURACfE6y+2DN4k= +github.com/mackerelio/go-osstat v0.2.7 h1:TCavZi10wF49bT6iQZ9eT2keGZQpC69MTDfdJej5e94= +github.com/mackerelio/go-osstat v0.2.7/go.mod h1:dwpYh5pIPmvk+IEwBKNIWRFMB92mrC08CmXOhDC7nQk= github.com/peter-evans/patience v0.3.0 h1:rX0JdJeepqdQl1Sk9c9uvorjYYzL2TfgLX1adqYm9cA= github.com/peter-evans/patience v0.3.0/go.mod h1:Kmxu5sY1NmBLFSStvXjX1wS9mIv7wMcP/ubucyMOAu0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/pkg/api/proto.go b/pkg/api/proto.go index b39765e83..72f1144b7 100644 --- a/pkg/api/proto.go +++ b/pkg/api/proto.go @@ -145,6 +145,7 @@ const ( MethodGetConstraintOfTypeParameter Method = "getConstraintOfTypeParameter" MethodGetBaseConstraintOfType Method = "getBaseConstraintOfType" MethodGetTypeArguments Method = "getTypeArguments" + MethodGetImportAdderEdits Method = "getImportAdderEdits" MethodGetTrueTypeOfConditionalType Method = "getTrueTypeOfConditionalType" MethodGetFalseTypeOfConditionalType Method = "getFalseTypeOfConditionalType" MethodGetConstantValue Method = "getConstantValue" @@ -438,6 +439,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetConstraintOfTypeParameter: unmarshallerFor[CheckerTypeParams], MethodGetBaseConstraintOfType: unmarshallerFor[CheckerTypeParams], MethodGetTypeArguments: unmarshallerFor[CheckerTypeParams], + MethodGetImportAdderEdits: unmarshallerFor[GetImportAdderEditsParams], MethodGetConstantValue: unmarshallerFor[CheckerNodeParams], MethodGetSignatureFromDeclaration: unmarshallerFor[CheckerNodeParams], MethodGetExportSpecifierLocalTarget: unmarshallerFor[CheckerNodeParams], @@ -1014,6 +1016,31 @@ type GetTypesAtPositionsParams struct { Positions []uint32 `json:"positions"` } +type ImportAdderActionKind string + +const ( + ImportAdderActionKindImportSymbol ImportAdderActionKind = "importSymbol" +) + +type ImportAdderAction struct { + Kind ImportAdderActionKind `json:"kind"` + Symbol SymbolID `json:"symbol,omitempty"` + IsValidTypeOnlyUseSite *bool `json:"isValidTypeOnlyUseSite,omitempty"` +} + +type GetImportAdderEditsParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + File DocumentIdentifier `json:"file"` + Actions []ImportAdderAction `json:"actions"` +} + +type TextEdit struct { + Pos int `json:"pos"` + End int `json:"end"` + NewText string `json:"newText"` +} + // TypeToTypeNodeParams are the parameters for the typeToTypeNode method. type TypeToTypeNodeParams struct { Snapshot SnapshotID `json:"snapshot"` diff --git a/pkg/api/session.go b/pkg/api/session.go index ea91e3e94..111a6d8fb 100644 --- a/pkg/api/session.go +++ b/pkg/api/session.go @@ -19,6 +19,8 @@ import ( "github.com/buke/typescript-go-internal/pkg/core" "github.com/buke/typescript-go-internal/pkg/json" "github.com/buke/typescript-go-internal/pkg/ls" + "github.com/buke/typescript-go-internal/pkg/ls/autoimport" + "github.com/buke/typescript-go-internal/pkg/ls/lsconv" "github.com/buke/typescript-go-internal/pkg/lsp/lsproto" "github.com/buke/typescript-go-internal/pkg/nodebuilder" "github.com/buke/typescript-go-internal/pkg/pprof" @@ -696,6 +698,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetBaseConstraintOfType(ctx, parsed.(*CheckerTypeParams)) case string(MethodGetTypeArguments): return s.handleGetTypeArguments(ctx, parsed.(*CheckerTypeParams)) + case string(MethodGetImportAdderEdits): + return s.handleGetImportAdderEdits(ctx, parsed.(*GetImportAdderEditsParams)) case string(MethodGetConstantValue): return s.handleGetConstantValue(ctx, parsed.(*CheckerNodeParams)) case string(MethodGetSignatureFromDeclaration): @@ -1559,6 +1563,112 @@ func (s *Session) handleGetTargetOfSignature(_ context.Context, params *GetSigna return s.resolveSignaturePropertyOfSignature(params, (*checker.Signature).Target) } +func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImportAdderEditsParams) ([]*TextEdit, error) { + sd, err := s.getSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + + projectPath := parseProjectHandle(params.Project) + workingSnapshot := sd.snapshot + program, err := sd.getProgram(params.Project) + if err != nil { + return nil, err + } + sourceFile := program.GetSourceFile(params.File.ToFileName()) + if sourceFile == nil { + return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) + } + + userPreferences := workingSnapshot.UserPreferences() + if registry := workingSnapshot.AutoImportRegistry(); registry == nil || + !registry.IsPreparedForImportingFile(sourceFile.FileName(), projectPath, userPreferences) { + preparedSnapshot := s.projectSession.GetSnapshotWithAutoImports(ctx, workingSnapshot, params.File.ToURI(s.projectSession.GetCurrentDirectory())) + defer preparedSnapshot.Deref(s.projectSession) + + workingSnapshot = preparedSnapshot + proj := workingSnapshot.ProjectCollection.GetProjectByPath(projectPath) + if proj == nil { + return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectPath) + } + program = proj.GetProgram() + if program == nil { + return nil, fmt.Errorf("%w: project has no program", ErrClientError) + } + sourceFile = program.GetSourceFile(params.File.ToFileName()) + if sourceFile == nil { + return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) + } + userPreferences = workingSnapshot.UserPreferences() + } + + registry := workingSnapshot.AutoImportRegistry() + if registry == nil { + return []*TextEdit{}, nil + } + + ch, done := program.GetTypeChecker(ctx) + defer done() + + view := autoimport.NewView( + registry, + sourceFile, + projectPath, + program, + userPreferences.ModuleSpecifierPreferences(), + ) + importAdder := autoimport.NewImportAdder( + ctx, + program, + ch, + sourceFile, + view, + workingSnapshot.GetPreferences(sourceFile.FileName()).FormatCodeSettings, + workingSnapshot.Converters(), + userPreferences, + ) + + for i, action := range params.Actions { + switch action.Kind { + case ImportAdderActionKindImportSymbol: + if action.Symbol == 0 { + return nil, fmt.Errorf("%w: import adder action %d missing symbol", ErrClientError, i) + } + symbol, err := sd.resolveSymbolHandle(action.Symbol) + if err != nil { + return nil, err + } + isValidTypeOnlyUseSite := true + if action.IsValidTypeOnlyUseSite != nil { + isValidTypeOnlyUseSite = *action.IsValidTypeOnlyUseSite + } + importAdder.AddImportFromExportedSymbol(symbol, isValidTypeOnlyUseSite) + default: + return nil, fmt.Errorf("%w: unknown import adder action kind %q", ErrClientError, action.Kind) + } + } + + if !importAdder.HasFixes() { + return []*TextEdit{}, nil + } + return toAPITextEdits(sourceFile, workingSnapshot.Converters(), importAdder.Edits()), nil +} + +func toAPITextEdits(sourceFile *ast.SourceFile, converters *lsconv.Converters, edits []*lsproto.TextEdit) []*TextEdit { + positionMap := sourceFile.GetPositionMap() + result := make([]*TextEdit, len(edits)) + for i, edit := range edits { + start := converters.LineAndCharacterToPosition(sourceFile, edit.Range.Start) + end := converters.LineAndCharacterToPosition(sourceFile, edit.Range.End) + result[i] = &TextEdit{ + Pos: positionMap.UTF8ToUTF16(int(start)), + End: positionMap.UTF8ToUTF16(int(end)), + NewText: edit.NewText, + } + } + return result +} + // resolveTypePropertyOfType resolves a type property of type `Type` and returns a type response. func (s *Session) resolveTypePropertyOfType(params *GetTypePropertyParams, getter func(*checker.Type) *checker.Type) (*TypeResponse, error) { sd, err := s.getSnapshotData(params.Snapshot) diff --git a/pkg/execute/tsctests/tsc_test.go b/pkg/execute/tsctests/tsc_test.go index acc81295c..17e7cf1de 100644 --- a/pkg/execute/tsctests/tsc_test.go +++ b/pkg/execute/tsctests/tsc_test.go @@ -4132,6 +4132,44 @@ func TestTscProjectReferences(t *testing.T) { cwd: "/home/src/workspaces/solution", commandLineArgs: []string{"--p", "project"}, }, + { + subScenario: "when project references have invalid fields", + files: FileMap{ + "/home/src/workspaces/solution/project/index.ts": `export const x = 10;`, + "/home/src/workspaces/solution/project/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "noEmit": true + }, + "files": ["index.ts"], + "references": [ + { "path": true }, + { "circular": true }, + { "path": "../utils", "circular": "yes" }, + { "path": "" }, + { "path": "../valid", "circular": true } + ] + }`), + "/home/src/workspaces/solution/utils/index.ts": "export const y = 10;", + "/home/src/workspaces/solution/utils/index.d.ts": "export declare const y = 10;", + "/home/src/workspaces/solution/utils/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "composite": true + } + }`), + "/home/src/workspaces/solution/valid/index.ts": "export const z = 10;", + "/home/src/workspaces/solution/valid/index.d.ts": "export declare const z = 10;", + "/home/src/workspaces/solution/valid/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "composite": true + } + }`), + }, + cwd: "/home/src/workspaces/solution", + commandLineArgs: []string{"--p", "project"}, + }, { subScenario: "default import interop uses referenced project settings", files: FileMap{ diff --git a/pkg/execute/tsctests/tscbuild_test.go b/pkg/execute/tsctests/tscbuild_test.go index e16f74ad3..5316ae41f 100644 --- a/pkg/execute/tsctests/tscbuild_test.go +++ b/pkg/execute/tsctests/tscbuild_test.go @@ -373,6 +373,43 @@ func TestBuildConfigFileErrors(t *testing.T) { }, commandLineArgs: []string{"--b"}, }, + { + subScenario: "reports invalid project reference fields", + files: FileMap{ + "/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "composite": true + }, + "files": ["index.ts"], + "references": [ + { "path": true }, + { "circular": true }, + { "path": "./utils", "circular": "yes" }, + { "path": "" }, + { "path": "./valid", "circular": true } + ] + }`), + "/home/src/workspaces/project/index.ts": "export const x = 10;", + "/home/src/workspaces/project/utils/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "composite": true + }, + "files": ["index.ts"] + }`), + "/home/src/workspaces/project/utils/index.ts": "export const y = 10;", + "/home/src/workspaces/project/valid/tsconfig.json": stringtestutil.Dedent(` + { + "compilerOptions": { + "composite": true + }, + "files": ["index.ts"] + }`), + "/home/src/workspaces/project/valid/index.ts": "export const z = 10;", + }, + commandLineArgs: []string{"--b", "--dry"}, + }, { subScenario: "reports syntax errors in config file", files: FileMap{ diff --git a/pkg/project/session.go b/pkg/project/session.go index e6450f138..567e7b3b6 100644 --- a/pkg/project/session.go +++ b/pkg/project/session.go @@ -1127,6 +1127,31 @@ func (s *Session) WithLanguageServiceAndSnapshot( // The cloned snapshot will be adopted as the session's current snapshot in the background // if other changes haven't been adopted in the meantime. func (s *Session) GetLanguageServiceWithAutoImports(ctx context.Context, baseSnapshot *Snapshot, uri lsproto.DocumentUri) (*ls.LanguageService, error) { + newSnapshot := s.cloneWithAutoImports(ctx, baseSnapshot, uri, false /*callerRef*/) + project := newSnapshot.GetDefaultProject(uri) + if project == nil { + // Clone's initial ref (1) is released since we won't use this snapshot. + newSnapshot.Deref(s) + return nil, fmt.Errorf("no project found for URI %s", uri) + } + + s.adoptSnapshotChangeInBackground(baseSnapshot, newSnapshot) + + return ls.NewLanguageService(project.configFilePath, project.GetProgram(), newSnapshot, uri.FileName()), nil +} + +// GetSnapshotWithAutoImports clones the given snapshot with auto-import +// preparation for the given URI, without flushing pending file changes. +// The returned snapshot is ref'd for the caller, which must call Deref when done. +// The cloned snapshot will also be adopted as the session's current snapshot in +// the background if other changes haven't been adopted in the meantime. +func (s *Session) GetSnapshotWithAutoImports(ctx context.Context, baseSnapshot *Snapshot, uri lsproto.DocumentUri) *Snapshot { + newSnapshot := s.cloneWithAutoImports(ctx, baseSnapshot, uri, true /*callerRef*/) + s.adoptSnapshotChangeInBackground(baseSnapshot, newSnapshot) + return newSnapshot +} + +func (s *Session) cloneWithAutoImports(ctx context.Context, baseSnapshot *Snapshot, uri lsproto.DocumentUri, callerRef bool) *Snapshot { change := SnapshotChange{ reason: UpdateReasonRequestedLanguageServiceWithAutoImports, ResourceRequest: ResourceRequest{ @@ -1135,22 +1160,19 @@ func (s *Session) GetLanguageServiceWithAutoImports(ctx context.Context, baseSna }, } newSnapshot := baseSnapshot.Clone(ctx, change, baseSnapshot.fs.overlays, s) - - project := newSnapshot.GetDefaultProject(uri) - if project == nil { - // Clone's initial ref (1) is released since we won't use this snapshot. - newSnapshot.Deref(s) - return nil, fmt.Errorf("no project found for URI %s", uri) + if callerRef { + newSnapshot.ref() } + return newSnapshot +} +func (s *Session) adoptSnapshotChangeInBackground(baseSnapshot, newSnapshot *Snapshot) { // The clone's initial ref (1) is transferred to adoptSnapshotChange, // which will either promote it as the session's current snapshot or // release it if the session has moved on. s.backgroundQueue.Enqueue(s.backgroundCtx, func(ctx context.Context) { s.adoptSnapshotChange(baseSnapshot, newSnapshot) }) - - return ls.NewLanguageService(project.configFilePath, project.GetProgram(), newSnapshot, uri.FileName()), nil } // adoptSnapshotChange promotes a cloned snapshot as the session's current diff --git a/pkg/project/snapshot_test.go b/pkg/project/snapshot_test.go index 873e5db6c..62febd126 100644 --- a/pkg/project/snapshot_test.go +++ b/pkg/project/snapshot_test.go @@ -210,6 +210,30 @@ func TestSnapshot(t *testing.T) { _, err = session.GetLanguageService(context.Background(), otherURI) assert.NilError(t, err) }) + + t.Run("auto-import snapshot is adopted when session snapshot is unchanged", func(t *testing.T) { + t.Parallel() + files := map[string]any{ + "/home/projects/TS/p1/tsconfig.json": "{}", + "/home/projects/TS/p1/index.ts": "const value = foo;", + "/home/projects/TS/p1/foo.ts": "export const foo = 1;", + } + session := setup(files) + t.Cleanup(session.Close) + ctx := context.Background() + uri := lsproto.DocumentUri("file:///home/projects/TS/p1/index.ts") + + session.DidOpenFile(ctx, uri, 1, files["/home/projects/TS/p1/index.ts"].(string), lsproto.LanguageKindTypeScript) + _, err := session.GetLanguageService(ctx, uri) + assert.NilError(t, err) + + baseSnapshot := session.Snapshot() + preparedSnapshot := session.GetSnapshotWithAutoImports(ctx, baseSnapshot, uri) + defer preparedSnapshot.Deref(session) + + session.WaitForBackgroundTasks() + assert.Equal(t, session.Snapshot(), preparedSnapshot) + }) } func BenchmarkSnapshotCloneRefCost(b *testing.B) { diff --git a/pkg/tsoptions/parsinghelpers.go b/pkg/tsoptions/parsinghelpers.go index 107780b5e..f65039c5d 100644 --- a/pkg/tsoptions/parsinghelpers.go +++ b/pkg/tsoptions/parsinghelpers.go @@ -70,19 +70,34 @@ func parseNumber(value any) *int { return nil } -func parseProjectReference(json any) []*core.ProjectReference { - var result []*core.ProjectReference +type projectReferenceParseResult struct { + reference core.ProjectReference + hasPath bool + pathValid bool + hasCircular bool + circularValid bool +} + +func parseProjectReference(json any) *projectReferenceParseResult { if v, ok := json.(*collections.OrderedMap[string, any]); ok { - var reference core.ProjectReference - if v, ok := v.Get("path"); ok { - reference.Path = v.(string) + result := &projectReferenceParseResult{} + if value, ok := v.Get("path"); ok { + result.hasPath = true + if path, ok := value.(string); ok { + result.reference.Path = path + result.pathValid = true + } } - if v, ok := v.Get("circular"); ok { - reference.Circular = v.(bool) + if value, ok := v.Get("circular"); ok { + result.hasCircular = true + if circular, ok := value.(bool); ok { + result.reference.Circular = circular + result.circularValid = true + } } - result = append(result, &reference) + return result } - return result + return nil } func parseJsonToStringKey(json any) *collections.OrderedMap[string, any] { diff --git a/pkg/tsoptions/tsconfigparsing.go b/pkg/tsoptions/tsconfigparsing.go index 9f37a3db6..f0291ec11 100644 --- a/pkg/tsoptions/tsconfigparsing.go +++ b/pkg/tsoptions/tsconfigparsing.go @@ -1348,20 +1348,27 @@ func parseJsonConfigFileContentWorker( newReferencesOfRaw := getPropFromRaw("references", func(element any) bool { return reflect.TypeOf(element) == orderedMapType }, "object") if newReferencesOfRaw.sliceValue != nil { projectReferences = []*core.ProjectReference{} - for _, reference := range newReferencesOfRaw.sliceValue { - for _, ref := range parseProjectReference(reference) { - if ref.Path == "" { - if sourceFile == nil { - errors = append(errors, ast.NewCompilerDiagnostic(diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string")) - } - } else { - projectReferences = append(projectReferences, &core.ProjectReference{ - Path: tspath.GetNormalizedAbsolutePath(ref.Path, basePath), - OriginalPath: ref.Path, - Circular: ref.Circular, - }) - } + for index, reference := range newReferencesOfRaw.sliceValue { + ref := parseProjectReference(reference) + if ref == nil { + continue + } + if !ref.hasPath || !ref.pathValid { + errors = append(errors, createDiagnosticAtProjectReferenceProperty(sourceFile, index, "path", diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string")) + continue } + if ref.reference.Path == "" { + errors = append(errors, createDiagnosticAtProjectReferenceProperty(sourceFile, index, "path", diagnostics.Compiler_option_0_cannot_be_given_an_empty_string, "reference.path")) + continue + } + if ref.hasCircular && !ref.circularValid { + errors = append(errors, createDiagnosticAtProjectReferenceProperty(sourceFile, index, "circular", diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.circular", "boolean")) + } + projectReferences = append(projectReferences, &core.ProjectReference{ + Path: tspath.GetNormalizedAbsolutePath(ref.reference.Path, basePath), + OriginalPath: ref.reference.Path, + Circular: ref.reference.Circular, + }) } } return projectReferences @@ -1494,6 +1501,27 @@ func CreateDiagnosticAtReferenceSyntax(config *ParsedCommandLine, index int, mes }) } +func createDiagnosticAtProjectReferenceProperty(sourceFile *TsConfigSourceFile, index int, propertyName string, message *diagnostics.Message, args ...any) *ast.Diagnostic { + var node *ast.Node + if sourceFile != nil { + node = ForEachTsConfigPropArray(sourceFile.SourceFile, "references", func(property *ast.PropertyAssignment) *ast.Node { + if ast.IsArrayLiteralExpression(property.Initializer) { + elements := property.Initializer.Elements() + if len(elements) > index && ast.IsObjectLiteralExpression(elements[index]) { + if propertyNode := ForEachPropertyAssignment(elements[index].AsObjectLiteralExpression(), propertyName, func(property *ast.PropertyAssignment) *ast.Node { + return property.Initializer + }); propertyNode != nil { + return propertyNode + } + return elements[index] + } + } + return nil + }) + } + return CreateDiagnosticForNodeInSourceFileOrCompilerDiagnostic(tsconfigToSourceFile(sourceFile), node, message, args...) +} + func GetCallbackForFindingPropertyAssignmentByValue(value string) func(property *ast.PropertyAssignment) *ast.Node { return func(property *ast.PropertyAssignment) *ast.Node { if ast.IsArrayLiteralExpression(property.Initializer) { diff --git a/testdata/baselines/reference/tsbuild/configFileErrors/reports-invalid-project-reference-fields.js b/testdata/baselines/reference/tsbuild/configFileErrors/reports-invalid-project-reference-fields.js new file mode 100644 index 000000000..d3e97b877 --- /dev/null +++ b/testdata/baselines/reference/tsbuild/configFileErrors/reports-invalid-project-reference-fields.js @@ -0,0 +1,71 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +export const x = 10; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true + }, + "files": ["index.ts"], + "references": [ + { "path": true }, + { "circular": true }, + { "path": "./utils", "circular": "yes" }, + { "path": "" }, + { "path": "./valid", "circular": true } + ] +} +//// [/home/src/workspaces/project/utils/index.ts] *new* +export const y = 10; +//// [/home/src/workspaces/project/utils/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true + }, + "files": ["index.ts"] +} +//// [/home/src/workspaces/project/valid/index.ts] *new* +export const z = 10; +//// [/home/src/workspaces/project/valid/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true + }, + "files": ["index.ts"] +} + +tsgo --b --dry +ExitStatus:: DiagnosticsPresent_OutputsSkipped +Output:: +[HH:MM:SS AM] A non-dry build would build project '/home/src/workspaces/project/utils/tsconfig.json' + +[HH:MM:SS AM] A non-dry build would build project '/home/src/workspaces/project/valid/tsconfig.json' + +[HH:MM:SS AM] A non-dry build would build project '/home/src/workspaces/project/tsconfig.json' + +tsconfig.json:7:19 - error TS5024: Compiler option 'reference.path' requires a value of type string. + +7 { "path": true }, +   ~~~~ + +tsconfig.json:8:9 - error TS5024: Compiler option 'reference.path' requires a value of type string. + +8 { "circular": true }, +   ~~~~~~~~~~~~~~~~~~~~ + +tsconfig.json:9:42 - error TS5024: Compiler option 'reference.circular' requires a value of type boolean. + +9 { "path": "./utils", "circular": "yes" }, +   ~~~~~ + +tsconfig.json:10:19 - error TS18051: Compiler option 'reference.path' cannot be given an empty string. + +10 { "path": "" }, +   ~~ + + +Found 4 errors in the same file, starting at: tsconfig.json:7 + + diff --git a/testdata/baselines/reference/tsc/projectReferences/when-project-references-have-invalid-fields.js b/testdata/baselines/reference/tsc/projectReferences/when-project-references-have-invalid-fields.js new file mode 100644 index 000000000..c88afc222 --- /dev/null +++ b/testdata/baselines/reference/tsc/projectReferences/when-project-references-have-invalid-fields.js @@ -0,0 +1,90 @@ +currentDirectory::/home/src/workspaces/solution +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/solution/project/index.ts] *new* +export const x = 10; +//// [/home/src/workspaces/solution/project/tsconfig.json] *new* +{ + "compilerOptions": { + "noEmit": true + }, + "files": ["index.ts"], + "references": [ + { "path": true }, + { "circular": true }, + { "path": "../utils", "circular": "yes" }, + { "path": "" }, + { "path": "../valid", "circular": true } + ] +} +//// [/home/src/workspaces/solution/utils/index.d.ts] *new* +export declare const y = 10; +//// [/home/src/workspaces/solution/utils/index.ts] *new* +export const y = 10; +//// [/home/src/workspaces/solution/utils/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true + } +} +//// [/home/src/workspaces/solution/valid/index.d.ts] *new* +export declare const z = 10; +//// [/home/src/workspaces/solution/valid/index.ts] *new* +export const z = 10; +//// [/home/src/workspaces/solution/valid/tsconfig.json] *new* +{ + "compilerOptions": { + "composite": true + } +} + +tsgo --p project +ExitStatus:: DiagnosticsPresent_OutputsSkipped +Output:: +project/tsconfig.json:7:19 - error TS5024: Compiler option 'reference.path' requires a value of type string. + +7 { "path": true }, +   ~~~~ + +project/tsconfig.json:8:9 - error TS5024: Compiler option 'reference.path' requires a value of type string. + +8 { "circular": true }, +   ~~~~~~~~~~~~~~~~~~~~ + +project/tsconfig.json:9:43 - error TS5024: Compiler option 'reference.circular' requires a value of type boolean. + +9 { "path": "../utils", "circular": "yes" }, +   ~~~~~ + +project/tsconfig.json:10:19 - error TS18051: Compiler option 'reference.path' cannot be given an empty string. + +10 { "path": "" }, +   ~~ + + +Found 4 errors in the same file, starting at: project/tsconfig.json:7 + +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +