Skip to content
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
30 changes: 30 additions & 0 deletions packages/vscode-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,36 @@
],
"description": "%native-preview.trackFlakyDiagnostics.description%",
"scope": "window"
},
"js/ts.experimental.workspaceDiagnostics.scope": {
"type": "string",
"enum": [
"off",
"openProjects",
"openProjectsAndDependents",
"allProjects"
],
"enumDescriptions": [
"%native-preview.workspaceDiagnostics.off%",
"%native-preview.workspaceDiagnostics.openProjects%",
"%native-preview.workspaceDiagnostics.openProjectsAndDependents%",
"%native-preview.workspaceDiagnostics.allProjects%"
],
"default": "off",
"tags": [
"experimental"
],
"description": "%native-preview.workspaceDiagnostics.description%",
"scope": "window"
},
"js/ts.experimental.workspaceDiagnostics.serverDiagnosticsDeDuplication": {
"type": "boolean",
"default": true,
"tags": [
"experimental"
],
"description": "%native-preview.workspaceDiagnostics.serverDiagnosticsDeDuplication.description%",
"scope": "window"
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/vscode-typescript/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,11 @@
"native-preview.trackFlakyDiagnostics.log": "Log an error when a flaky diagnostic is detected.",
"native-preview.trackFlakyDiagnostics.never": "Never perform flaky diagnostic checking and logging.",
"native-preview.trackFlakyDiagnostics.auto": "Perform flaky diagnostic logging only on VS Code Insiders.",
"native-preview.workspaceDiagnostics.description": "Controls how much of the workspace is checked for errors, including files that are not open. Checking whole projects is expensive.",
"native-preview.workspaceDiagnostics.serverDiagnosticsDeDuplication.description": "Leave a file out of workspace diagnostics while it is open, because the editor reports open files separately and would otherwise show every problem in them twice. Turn this off only for a client that does not request diagnostics per document.",
"native-preview.workspaceDiagnostics.off": "Only report errors in open files.",
"native-preview.workspaceDiagnostics.openProjects": "Report errors in every file of the projects that contain an open file.",
"native-preview.workspaceDiagnostics.openProjectsAndDependents": "Also report errors in the projects that reference those projects.",
"native-preview.workspaceDiagnostics.allProjects": "Report errors in every project in the workspace.",
"developer": "Developer"
}
53 changes: 44 additions & 9 deletions tsc/internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,8 @@ func (p *Program) collectCheckerDiagnostics(ctx context.Context, sourceFile *ast
done()
return filterAndSortDiagnostics(result)
}
return filterAndSortDiagnostics(slices.Concat(p.collectCheckerDiagnosticsFromFiles(ctx, p.files, collect)...))
diagnostics, _ := p.collectCheckerDiagnosticsFromFiles(ctx, p.files, collect)
return filterAndSortDiagnostics(slices.Concat(diagnostics...))
}

func filterAndSortDiagnostics(diags []*ast.Diagnostic) []*ast.Diagnostic {
Expand All @@ -716,28 +717,58 @@ func filterAndSortDiagnostics(diags []*ast.Diagnostic) []*ast.Diagnostic {
}))
}

// collectCheckerDiagnosticsFromFiles collects checker diagnostics for a list of files.
func (p *Program) collectCheckerDiagnosticsFromFiles(ctx context.Context, sourceFiles []*ast.SourceFile, collect func(context.Context, *checker.Checker, *ast.SourceFile) []*ast.Diagnostic) [][]*ast.Diagnostic {
// wholeProgramCheckerPool is a CheckerPool that runs a check of many files itself, so that the pool,
// rather than the program, decides which checker takes each file.
type wholeProgramCheckerPool interface {
ForEachCheckerGroupDo(ctx context.Context, files []*ast.SourceFile, singleThreaded bool, cb func(c *checker.Checker, fileIndex int, file *ast.SourceFile))
}

// collectCheckerDiagnosticsFromFiles collects checker diagnostics for a list of files, and reports
// which of them it got through: a file a cancelled caller never reached is left nil, the same as
// one checked and found clean.
func (p *Program) collectCheckerDiagnosticsFromFiles(ctx context.Context, sourceFiles []*ast.SourceFile, collect func(context.Context, *checker.Checker, *ast.SourceFile) []*ast.Diagnostic) ([][]*ast.Diagnostic, []bool) {
diagnostics := make([][]*ast.Diagnostic, len(sourceFiles))
checked := make([]bool, len(sourceFiles))
check := func(c *checker.Checker, fileIndex int, file *ast.SourceFile) {
result := collect(ctx, c, file)
// Cancellation can land part way through a file, so only an uninterrupted check counts.
if ctx.Err() == nil {
diagnostics[fileIndex] = result
checked[fileIndex] = true
}
}
if p.compilerCheckerPool != nil {
p.compilerCheckerPool.forEachCheckerGroupDo(ctx, sourceFiles, p.SingleThreaded(), func(c *checker.Checker, fileIndex int, file *ast.SourceFile) {
diagnostics[fileIndex] = collect(ctx, c, file)
p.compilerCheckerPool.forEachCheckerGroupDo(ctx, sourceFiles, p.SingleThreaded(), check)
} else if pool, ok := p.checkerPool.(wholeProgramCheckerPool); ok {
files := make([]*ast.SourceFile, 0, len(sourceFiles))
indices := make([]int, 0, len(sourceFiles))
for i, file := range sourceFiles {
if p.SkipTypeChecking(file, false) {
checked[i] = true
continue
}
files = append(files, file)
indices = append(indices, i)
}
pool.ForEachCheckerGroupDo(ctx, files, p.SingleThreaded(), func(c *checker.Checker, fileIndex int, file *ast.SourceFile) {
check(c, indices[fileIndex], file)
})
} else {
wg := core.NewWorkGroup(p.SingleThreaded())
for i, file := range sourceFiles {
if p.SkipTypeChecking(file, false) {
checked[i] = true
continue
}
wg.Queue(func() {
c, done := p.checkerPool.GetChecker(ctx, file)
diagnostics[i] = collect(ctx, c, file)
check(c, i, file)
done()
})
}
wg.RunAndWait()
}
return diagnostics
return diagnostics, checked
}

func (p *Program) GetSyntacticDiagnostics(ctx context.Context, sourceFile *ast.SourceFile) []*ast.Diagnostic {
Expand Down Expand Up @@ -802,12 +833,16 @@ func (p *Program) GetSemanticDiagnostics(ctx context.Context, sourceFile *ast.So
// GetSemanticDiagnosticsForIncremental includes newly discovered globals in each
// file's cached diagnostics and leaves noEmit filtering to the builder.
func (p *Program) GetSemanticDiagnosticsForIncremental(ctx context.Context, sourceFiles []*ast.SourceFile) map[*ast.SourceFile][]*ast.Diagnostic {
allDiags := p.collectCheckerDiagnosticsFromFiles(ctx, sourceFiles, func(ctx context.Context, c *checker.Checker, file *ast.SourceFile) []*ast.Diagnostic {
allDiags, checked := p.collectCheckerDiagnosticsFromFiles(ctx, sourceFiles, func(ctx context.Context, c *checker.Checker, file *ast.SourceFile) []*ast.Diagnostic {
return p.getBindAndCheckDiagnosticsWithChecker(ctx, c, file, true /*includeDeferredGlobals*/)
})
result := make(map[*ast.SourceFile][]*ast.Diagnostic, len(sourceFiles))
for i, diags := range allDiags {
result[sourceFiles[i]] = filterAndSortDiagnostics(diags)
// Only the files this got through. A cancelled caller that kept the rest would be caching
// "no errors" for files nothing looked at.
if checked[i] {
result[sourceFiles[i]] = filterAndSortDiagnostics(diags)
}
}
return result
}
Expand Down
12 changes: 12 additions & 0 deletions tsc/internal/core/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type key int
const (
requestIDKey key = iota
checkerLifetimeKey
interactiveRequestKey
)

func WithRequestID(ctx context.Context, id string) context.Context {
Expand All @@ -22,6 +23,17 @@ func GetRequestID(ctx context.Context) string {
return ""
}

// WithInteractiveRequest marks work a user is waiting on directly, as against work done ahead of
// being asked for it. Whole-workspace passes stand aside while any of it is outstanding.
func WithInteractiveRequest(ctx context.Context) context.Context {
return context.WithValue(ctx, interactiveRequestKey, true)
}

func IsInteractiveRequest(ctx context.Context) bool {
interactive, _ := ctx.Value(interactiveRequestKey).(bool)
return interactive
}

type CheckerLifetime int

const (
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/diagnostics/diagnosticMessages.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -1942,6 +1942,7 @@
"The_content_mapper_returned_diagnostic_directives_with_overlapping_virtual_ranges_18108": "The content mapper returned diagnostic directives with overlapping virtual ranges.",
"The_invalid_diagnostic_directive_is_in_supplemental_output_0_returned_by_the_content_mapper_18109": "The invalid diagnostic directive is in supplemental output {0} returned by the content mapper.",
"Diagnostic_directive_0_returned_by_the_content_mapper_has_an_invalid_unusedExpectDirectiveIndex_18110": "Diagnostic directive {0} returned by the content mapper has an invalid 'unusedExpectDirectiveIndex'.",
"Checking_workspace_18111": "Checking workspace",
"nodenext_if_module_is_nodenext_node16_if_module_is_node16_or_node18_otherwise_bundler_69010": "`nodenext` if `module` is `nodenext`; `node16` if `module` is `node16` or `node18`; otherwise, `bundler`.",
"File_is_a_CommonJS_module_it_may_be_converted_to_an_ES_module_80001": "File is a CommonJS module; it may be converted to an ES module.",
"This_constructor_function_may_be_converted_to_a_class_declaration_80002": "This constructor function may be converted to a class declaration.",
Expand Down
4 changes: 4 additions & 0 deletions tsc/internal/diagnostics/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -8887,5 +8887,9 @@
"Diagnostic directive {0} returned by the content mapper has an invalid 'unusedExpectDirectiveIndex'.": {
"category": "Message",
"code": 18110
},
"Checking workspace": {
"category": "Message",
"code": 18111
}
}
3 changes: 3 additions & 0 deletions tsc/internal/diagnostics/diagnostics_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 41 additions & 4 deletions tsc/internal/execute/incremental/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,43 @@ type TestingData struct {
UpdatedSignatureKinds map[tspath.Path]SignatureUpdateKind
}

// PriorState is what one program leaves for the next to work out what a change reached: the file
// hashes, references and cached diagnostics it built, and none of the program they came from. A
// caller that keeps a whole Program for this keeps its program too, and every type reachable from
// it, for as long as it holds on.
type PriorState struct {
snapshot *snapshot
}

// PriorState returns what this program has worked out, without the program itself.
func (p *Program) PriorState() *PriorState {
if p == nil {
return nil
}
return &PriorState{snapshot: p.snapshot}
}

// NewProgramFromPriorState is NewProgram for a caller that kept only what the previous program
// worked out, rather than the program itself.
//
// reuseReferences says the program is a clone of the one prior came from. Working out what a file
// references means resolving each of its imports through a type checker, for every file in the
// program, which is the most expensive thing building this state does. A clone is only made when
// the replaced file's imports, module augmentations, ambient module names and reference directives
// are all unchanged, and no other file moves, so every file resolves to what it did before and the
// whole map can be carried over instead.
func NewProgramFromPriorState(program *compiler.Program, prior *PriorState, host Host, reuseReferences bool) *Program {
var oldSnapshot *snapshot
if prior != nil {
oldSnapshot = prior.snapshot
}
return &Program{
snapshot: buildSnapshot(program, oldSnapshot, false /*hashWithText*/, reuseReferences && oldSnapshot != nil),
program: program,
host: host,
}
}

func (p *Program) GetTestingData() *TestingData {
return p.testingData
}
Expand Down Expand Up @@ -303,11 +340,11 @@ func (p *Program) collectSemanticDiagnosticsOfAffectedFiles(ctx context.Context,
}

// Get their diagnostics and cache them
// Only the files it got through come back, so a cancelled check keeps what it finished
// rather than starting again from nothing the next time it is asked. On a project big enough
// that a check outlasts the gap between two edits, throwing the work away meant it could
// never finish at all.
diagnosticsPerFile := p.program.GetSemanticDiagnosticsForIncremental(ctx, affectedFiles)
// commit changes if no err
if ctx.Err() != nil {
return
}

// Commit changes to snapshot
for file, diagnostics := range diagnosticsPerFile {
Expand Down
Loading