Skip to content
Draft
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"
}
55 changes: 53 additions & 2 deletions tsc/internal/compiler/checkerpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,45 @@ type CheckerPool interface {
GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
}

// CheckingPool is the pool the compiler checks a program with: one checker per the program's
// `checkers` option, with its files partitioned across them, and the global diagnostics they find
// collected from all of them. Which checker sees a file is part of how a program is checked, so
// anything wanting to check one the way the command line does has to check it through this.
type CheckingPool interface {
CheckerPool
// ForEachCheckerGroupDo runs one task per checker rather than one per file, so each checker
// is taken once for the whole group of files assigned to it.
ForEachCheckerGroupDo(ctx context.Context, files []*ast.SourceFile, singleThreaded bool, cb func(c *checker.Checker, fileIndex int, file *ast.SourceFile))
GetGlobalDiagnostics() []*ast.Diagnostic
// CheckerStats reports what each checker built so far holds, for logging.
CheckerStats() []CheckerStats
}

// CheckerStats is what one checker has built up. Go cannot be asked how much memory an object graph
// holds, and what grows with the work a checker has done is its types and symbols, so those stand in
// for it: a checker that has seen every file in a program holds far more than one that has seen a
// handful.
type CheckerStats struct {
Types uint32
Symbols uint32
Instantiations uint32
}

// CheckerStatsOf reads what a checker has accumulated.
func CheckerStatsOf(c *checker.Checker) CheckerStats {
if c == nil {
return CheckerStats{}
}
return CheckerStats{Types: c.TypeCount, Symbols: c.SymbolCount, Instantiations: c.TotalInstantiationCount}
}

// NewCheckingPool returns the pool the compiler would check this program with. A Program builds its
// own, so this is for callers that supply a CheckerPool of their own and need the compiler's for
// checking.
func NewCheckingPool(program *Program) CheckingPool {
return newCheckerPool(program)
}

type checkerPool struct {
program *Program
tracing *tracing.Tracing
Expand Down Expand Up @@ -457,6 +496,18 @@ func (p *checkerPool) forEachCheckerParallel(cb func(idx int, c *checker.Checker
wg.RunAndWait()
}

// CheckerStats implements CheckingPool. Checkers that have not been created are not counted, and
// none are created to answer this.
func (p *checkerPool) CheckerStats() []CheckerStats {
stats := make([]CheckerStats, 0, len(p.checkers))
for _, c := range p.checkers {
if c != nil {
stats = append(stats, CheckerStatsOf(c))
}
}
return stats
}

func (p *checkerPool) GetGlobalDiagnostics() []*ast.Diagnostic {
p.createCheckers()
globalDiagnostics := make([][]*ast.Diagnostic, len(p.checkers))
Expand All @@ -466,10 +517,10 @@ func (p *checkerPool) GetGlobalDiagnostics() []*ast.Diagnostic {
return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...))
}

// forEachCheckerGroupDo runs one task per checker in parallel. Each task iterates
// ForEachCheckerGroupDo runs one task per checker in parallel. Each task iterates
// the provided files, processing only those assigned to its checker. Within each
// checker's set, files are visited in their original order.
func (p *checkerPool) forEachCheckerGroupDo(ctx context.Context, files []*ast.SourceFile, singleThreaded bool, cb func(c *checker.Checker, fileIndex int, file *ast.SourceFile)) {
func (p *checkerPool) ForEachCheckerGroupDo(ctx context.Context, files []*ast.SourceFile, singleThreaded bool, cb func(c *checker.Checker, fileIndex int, file *ast.SourceFile)) {
p.createCheckers()

checkerCount := len(p.checkers)
Expand Down
6 changes: 4 additions & 2 deletions tsc/internal/compiler/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,8 +698,10 @@ 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 {
diagnostics := make([][]*ast.Diagnostic, len(sourceFiles))
if p.compilerCheckerPool != nil {
p.compilerCheckerPool.forEachCheckerGroupDo(ctx, sourceFiles, p.SingleThreaded(), func(c *checker.Checker, fileIndex int, file *ast.SourceFile) {
// Any pool that can iterate its checkers as groups is used that way, so a file is checked by
// the checker that pool assigned it and each checker is taken once rather than per file.
if grouped, ok := p.checkerPool.(CheckingPool); ok {
grouped.ForEachCheckerGroupDo(ctx, sourceFiles, p.SingleThreaded(), func(c *checker.Checker, fileIndex int, file *ast.SourceFile) {
diagnostics[fileIndex] = collect(ctx, c, file)
})
} else {
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/core/compileroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type CompilerOptions struct {
ForceConsistentCasingInFileNames Tristate `json:"forceConsistentCasingInFileNames,omitzero"`
IsolatedModules Tristate `json:"isolatedModules,omitzero"`
IsolatedDeclarations Tristate `json:"isolatedDeclarations,omitzero"`
ExperimentalWorkspaceDiagnosticsExclude []string `json:"experimentalWorkspaceDiagnosticsExclude,omitzero"`
IgnoreConfig Tristate `json:"ignoreConfig,omitzero"`
IgnoreDeprecations string `json:"ignoreDeprecations,omitzero"`
ImportHelpers Tristate `json:"importHelpers,omitzero"`
Expand Down
8 changes: 8 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.

8 changes: 8 additions & 0 deletions tsc/internal/diagnostics/extraDiagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,5 +338,13 @@
"Diagnostic directive {0} returned by the content mapper has an invalid 'unusedExpectDirectiveIndex'.": {
"category": "Message",
"code": 100068
},
"Checking workspace": {
"category": "Message",
"code": 100069
},
"Paths that workspace-wide diagnostics in the editor should not report on.": {
"category": "Message",
"code": 100070
}
}
10 changes: 10 additions & 0 deletions tsc/internal/execute/incremental/program.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ type TestingData struct {
UpdatedSignatureKinds map[tspath.Path]SignatureUpdateKind
}

// WithoutProgram returns what a later program needs in order to work out what a change reached,
// and nothing else. A caller holding a whole Program for that would hold its program, and every
// type reachable from it, until the next one is built.
func (p *Program) WithoutProgram() *Program {
if p == nil {
return nil
}
return &Program{snapshot: p.snapshot, host: p.host}
}

func (p *Program) GetTestingData() *TestingData {
return p.testingData
}
Expand Down
112 changes: 99 additions & 13 deletions tsc/internal/ls/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/ls/lsconv"
"github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto"
"github.com/microsoft/TypeScript/tsc/internal/spanmap"
"github.com/microsoft/TypeScript/tsc/internal/vfs/vfsmatch"
)

// getAllDiagnostics collects all diagnostics for a file: syntactic, semantic,
Expand All @@ -30,24 +31,66 @@ func getAllDiagnostics(ctx context.Context, program *compiler.Program, file *ast
}

func (l *LanguageService) ProvideDiagnostics(ctx context.Context, uri lsproto.DocumentUri) (lsproto.DocumentDiagnosticResponse, error) {
program, file := l.getProgramAndFile(uri)
_, file := l.getProgramAndFile(uri)
return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
Items: l.ProvideDiagnosticsForFile(ctx, file),
},
}, nil
}

// ProvideDiagnosticsForFile computes diagnostics for a file of this project's program, for callers
// that already hold it and need not re-resolve it by URI.
func (l *LanguageService) ProvideDiagnosticsForFile(ctx context.Context, file *ast.SourceFile) []*lsproto.Diagnostic {
if l.UserPreferences().EnableValidation.IsFalse() {
diagnostics := []*lsproto.Diagnostic{}
return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
Items: diagnostics,
},
}, nil
return []*lsproto.Diagnostic{}
}
return l.toLSPDiagnostics(ctx, getAllDiagnostics(ctx, l.program, file))
}

diagnostics := getAllDiagnostics(ctx, program, file)
// defaultWorkspaceDiagnosticsExclude applies when a project does not set
// experimentalWorkspaceDiagnosticsExclude. Dependencies reached by resolution are filtered out separately; this
// catches locally installed typings, which are program roots and so not external library imports.
var defaultWorkspaceDiagnosticsExclude = []string{"**/node_modules/**"}

return lsproto.RelatedFullDocumentDiagnosticReportOrUnchangedDocumentDiagnosticReport{
FullDocumentDiagnosticReport: &lsproto.RelatedFullDocumentDiagnosticReport{
Items: l.toLSPDiagnostics(ctx, diagnostics),
},
}, nil
// workspaceDiagnosticsExcludeMatcher compiles the project's exclusion patterns relative to its
// tsconfig, falling back to the default when the option is unset.
func (l *LanguageService) workspaceDiagnosticsExcludeMatcher() *vfsmatch.SpecMatcher {
specs := l.program.Options().ExperimentalWorkspaceDiagnosticsExclude
if specs == nil {
specs = defaultWorkspaceDiagnosticsExclude
}
return vfsmatch.NewSpecMatcher(specs, l.program.CommandLine().GetCurrentDirectory(), vfsmatch.UsageExclude, l.UseCaseSensitiveFileNames())
}

// WorkspaceDiagnosticFiles returns the files a workspace pull should report, in program order.
func (l *LanguageService) WorkspaceDiagnosticFiles() []*ast.SourceFile {
program := l.program
excluded := l.workspaceDiagnosticsExcludeMatcher()
files := make([]*ast.SourceFile, 0, len(program.SourceFiles()))
for _, file := range program.SourceFiles() {
// Dependencies are not the user's code to fix.
if program.IsSourceFileDefaultLibrary(file.Path()) || program.IsSourceFileFromExternalLibrary(file) {
continue
}
if excluded != nil && excluded.MatchString(file.FileName()) {
continue
}
// A referenced project's source, reached through the redirect; it reports its own.
if program.IsSourceFromProjectReference(file.Path()) {
continue
}
// A referenced project's emitted declarations, consumed when the redirect is disabled.
if program.GetProjectReferenceFromOutputDts(file.Path()) != nil {
continue
}
// A projection of a content-mapped file; its canonical file reports it under the same URI.
if file.CanonicalSourceFile() != nil {
continue
}
files = append(files, file)
}
return files
}

func (l *LanguageService) toLSPDiagnostics(ctx context.Context, diagnostics ...[]*ast.Diagnostic) []*lsproto.Diagnostic {
Expand Down Expand Up @@ -118,3 +161,46 @@ func worstCategory(diags []*ast.Diagnostic) diagnostics.Category {
}
return worst
}

// WorkspaceDiagnosticsForProject checks a project in one call and returns what each of its files
// should report, keyed by file. Checking everything in one call lets the program split the work
// across the checkers a build would use and keeps the pool's own coordination rather than repeating
// it per file.
//
// The program is passed in rather than taken from the language service because a sweep hands over
// the incremental view of it, which re-checks only the files a change reached and serves the rest
// from what it cached last time. Suggestions are left out: nothing caches them, so asking would
// re-check every file and undo that.
func (l *LanguageService) WorkspaceDiagnosticsForProject(ctx context.Context, program compiler.ProgramLike, files []*ast.SourceFile) map[*ast.SourceFile][]*lsproto.Diagnostic {
reports := make(map[*ast.SourceFile][]*lsproto.Diagnostic, len(files))
if l.UserPreferences().EnableValidation.IsFalse() {
for _, file := range files {
reports[file] = []*lsproto.Diagnostic{}
}
return reports
}

byFile := make(map[*ast.SourceFile][]*ast.Diagnostic, len(files))
collect := func(diagnostics []*ast.Diagnostic) {
for _, diagnostic := range diagnostics {
if file := diagnostic.File(); file != nil {
byFile[file] = append(byFile[file], diagnostic)
}
}
}
collect(program.GetSyntacticDiagnostics(ctx, nil))
collect(program.GetSemanticDiagnostics(ctx, nil))
if program.Options().GetEmitDeclarations() {
collect(program.GetDeclarationDiagnostics(ctx, nil))
}

for _, file := range files {
// A file's supplemental sources report under the file itself, as they do for a pull on it.
diagnostics := byFile[file]
for _, supplemental := range file.SupplementalSourceFiles() {
diagnostics = append(diagnostics, byFile[supplemental]...)
}
reports[file] = l.toLSPDiagnostics(ctx, diagnostics)
}
return reports
}
Loading