Skip to content
Merged
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
59 changes: 49 additions & 10 deletions internal/ast/diagnostic.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package ast

import (
"maps"
"slices"
"strings"
"sync"

"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
Expand Down Expand Up @@ -141,13 +141,20 @@ func NewCompilerDiagnostic(message *diagnostics.Message, args ...any) *Diagnosti
}

type DiagnosticsCollection struct {
mu sync.Mutex
count int
fileDiagnostics map[string][]*Diagnostic
fileDiagnosticsSorted collections.Set[string]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're here, I wonder if you're open to renaming this filesWithSortedDiagnostics or something

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind the existing name, personally...

nonFileDiagnostics []*Diagnostic
nonFileDiagnosticsSorted bool
}

func (c *DiagnosticsCollection) Add(diagnostic *Diagnostic) {
c.mu.Lock()
defer c.mu.Unlock()

c.count++

if diagnostic.File() != nil {
fileName := diagnostic.File().FileName()
if c.fileDiagnostics == nil {
Expand All @@ -162,11 +169,14 @@ func (c *DiagnosticsCollection) Add(diagnostic *Diagnostic) {
}

func (c *DiagnosticsCollection) Lookup(diagnostic *Diagnostic) *Diagnostic {
c.mu.Lock()
defer c.mu.Unlock()

var diagnostics []*Diagnostic
if diagnostic.File() != nil {
diagnostics = c.GetDiagnosticsForFile(diagnostic.File().FileName())
diagnostics = c.getDiagnosticsForFileLocked(diagnostic.File().FileName())
} else {
diagnostics = c.GetGlobalDiagnostics()
diagnostics = c.getGlobalDiagnosticsLocked()
}
if i, ok := slices.BinarySearchFunc(diagnostics, diagnostic, CompareDiagnostics); ok {
return diagnostics[i]
Expand All @@ -175,28 +185,45 @@ func (c *DiagnosticsCollection) Lookup(diagnostic *Diagnostic) *Diagnostic {
}

func (c *DiagnosticsCollection) GetGlobalDiagnostics() []*Diagnostic {
c.mu.Lock()
defer c.mu.Unlock()

return c.getGlobalDiagnosticsLocked()
}

func (c *DiagnosticsCollection) getGlobalDiagnosticsLocked() []*Diagnostic {
if !c.nonFileDiagnosticsSorted {
slices.SortStableFunc(c.nonFileDiagnostics, CompareDiagnostics)
c.nonFileDiagnosticsSorted = true
}
return c.nonFileDiagnostics
return slices.Clone(c.nonFileDiagnostics)
}

func (c *DiagnosticsCollection) GetDiagnosticsForFile(fileName string) []*Diagnostic {
c.mu.Lock()
defer c.mu.Unlock()

return c.getDiagnosticsForFileLocked(fileName)
}

func (c *DiagnosticsCollection) getDiagnosticsForFileLocked(fileName string) []*Diagnostic {
if !c.fileDiagnosticsSorted.Has(fileName) {
slices.SortStableFunc(c.fileDiagnostics[fileName], CompareDiagnostics)
c.fileDiagnosticsSorted.Add(fileName)
}
return c.fileDiagnostics[fileName]
return slices.Clone(c.fileDiagnostics[fileName])
}

func (c *DiagnosticsCollection) GetDiagnostics() []*Diagnostic {
fileNames := slices.Collect(maps.Keys(c.fileDiagnostics))
slices.Sort(fileNames)
diagnostics := slices.Clip(c.nonFileDiagnostics)
for _, fileName := range fileNames {
diagnostics = append(diagnostics, c.fileDiagnostics[fileName]...)
c.mu.Lock()
defer c.mu.Unlock()

diagnostics := make([]*Diagnostic, 0, c.count)
diagnostics = append(diagnostics, c.nonFileDiagnostics...)
for _, diags := range c.fileDiagnostics {
diagnostics = append(diagnostics, diags...)
}
slices.SortFunc(diagnostics, CompareDiagnostics)
return diagnostics
}

Expand All @@ -208,11 +235,17 @@ func getDiagnosticPath(d *Diagnostic) string {
}

func EqualDiagnostics(d1, d2 *Diagnostic) bool {
if d1 == d2 {
return true
}
return EqualDiagnosticsNoRelatedInfo(d1, d2) &&
slices.EqualFunc(d1.RelatedInformation(), d2.RelatedInformation(), EqualDiagnostics)
}

func EqualDiagnosticsNoRelatedInfo(d1, d2 *Diagnostic) bool {
if d1 == d2 {
return true
}
return getDiagnosticPath(d1) == getDiagnosticPath(d2) &&
d1.Loc() == d2.Loc() &&
d1.Code() == d2.Code() &&
Expand All @@ -221,6 +254,9 @@ func EqualDiagnosticsNoRelatedInfo(d1, d2 *Diagnostic) bool {
}

func equalMessageChain(c1, c2 *Diagnostic) bool {
if c1 == c2 {
return true
}
return c1.Code() == c2.Code() &&
slices.Equal(c1.MessageArgs(), c2.MessageArgs()) &&
slices.EqualFunc(c1.MessageChain(), c2.MessageChain(), equalMessageChain)
Expand Down Expand Up @@ -271,6 +307,9 @@ func compareRelatedInfo(r1, r2 []*Diagnostic) int {
}

func CompareDiagnostics(d1, d2 *Diagnostic) int {
if d1 == d2 {
return 0
}
c := strings.Compare(getDiagnosticPath(d1), getDiagnosticPath(d2))
if c != 0 {
return c
Expand Down
40 changes: 10 additions & 30 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,6 @@ type Program interface {
GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node)
GetImportHelpersImportSpecifier(path tspath.Path) *ast.Node
SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmit bool) bool
IsSourceFromProjectReference(path tspath.Path) bool
IsSourceFileDefaultLibrary(path tspath.Path) bool
GetProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference
GetRedirectForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine
Expand Down Expand Up @@ -930,8 +929,6 @@ func NewChecker(program Program) (*Checker, *sync.Mutex) {
c.unionTypes = make(map[string]*Type)
c.unionOfUnionTypes = make(map[UnionOfUnionKey]*Type)
c.intersectionTypes = make(map[string]*Type)
c.diagnostics = ast.DiagnosticsCollection{}
c.suggestionDiagnostics = ast.DiagnosticsCollection{}
c.mergedSymbols = make(map[*ast.Symbol]*ast.Symbol)
c.patternForType = make(map[*Type]*ast.Node)
c.contextFreeTypes = make(map[*ast.Node]*Type)
Expand Down Expand Up @@ -2094,13 +2091,6 @@ func (c *Checker) getSymbol(symbols ast.SymbolTable, name string, meaning ast.Sy
return nil
}

func (c *Checker) CheckSourceFile(ctx context.Context, sourceFile *ast.SourceFile) {
if SkipTypeChecking(sourceFile, c.compilerOptions, c.program, false) {
return
}
c.checkSourceFile(ctx, sourceFile)
}

func (c *Checker) checkSourceFile(ctx context.Context, sourceFile *ast.SourceFile) {
c.checkNotCanceled()
links := c.sourceFileLinks.Get(sourceFile)
Expand Down Expand Up @@ -13507,30 +13497,20 @@ func (c *Checker) getDiagnostics(ctx context.Context, sourceFile *ast.SourceFile
c.checkNotCanceled()
isSuggestionDiagnostics := collection == &c.suggestionDiagnostics

files := c.files
if sourceFile != nil {
files = []*ast.SourceFile{sourceFile}
c.checkSourceFile(ctx, sourceFile)
if c.wasCanceled {
return nil
}

for _, file := range files {
c.CheckSourceFile(ctx, file)
if c.wasCanceled {
return nil
}

// Check unused identifiers as suggestions if we're collecting suggestion diagnostics
// and they are not configured as errors
if isSuggestionDiagnostics && !file.IsDeclarationFile &&
!(c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue()) {
links := c.sourceFileLinks.Get(file)
c.checkUnusedIdentifiers(links.identifierCheckNodes)
}
// Check unused identifiers as suggestions if we're collecting suggestion diagnostics
// and they are not configured as errors
if isSuggestionDiagnostics && !sourceFile.IsDeclarationFile &&
!(c.compilerOptions.NoUnusedLocals.IsTrue() || c.compilerOptions.NoUnusedParameters.IsTrue()) {
links := c.sourceFileLinks.Get(sourceFile)
c.checkUnusedIdentifiers(links.identifierCheckNodes)
}

if sourceFile != nil {
return collection.GetDiagnosticsForFile(sourceFile.FileName())
}
return collection.GetDiagnostics()
return collection.GetDiagnosticsForFile(sourceFile.FileName())
}

func (c *Checker) GetGlobalDiagnostics() []*ast.Diagnostic {
Expand Down
19 changes: 0 additions & 19 deletions internal/checker/checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,6 @@ foo.bar;`
}
}

func TestCheckSrcCompiler(t *testing.T) {
t.Parallel()

repo.SkipIfNoTypeScriptSubmodule(t)
fs := osvfs.FS()
fs = bundled.WrapFS(fs)

rootPath := tspath.CombinePaths(tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath), "src", "compiler")

host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil)
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), &core.CompilerOptions{}, nil, host, nil)
assert.Equal(t, len(errors), 0, "Expected no errors in parsed command line")
p := compiler.NewProgram(compiler.ProgramOptions{
Config: parsed,
Host: host,
})
p.CheckSourceFiles(t.Context(), nil)
}

func BenchmarkNewChecker(b *testing.B) {
repo.SkipIfNoTypeScriptSubmodule(b)
fs := osvfs.FS()
Expand Down
28 changes: 0 additions & 28 deletions internal/checker/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -1283,34 +1283,6 @@ func forEachYieldExpression(body *ast.Node, visitor func(expr *ast.Node)) {
traverse(body)
}

func SkipTypeChecking(sourceFile *ast.SourceFile, options *core.CompilerOptions, host Program, ignoreNoCheck bool) bool {
return (!ignoreNoCheck && options.NoCheck.IsTrue()) ||
options.SkipLibCheck.IsTrue() && sourceFile.IsDeclarationFile ||
options.SkipDefaultLibCheck.IsTrue() && host.IsSourceFileDefaultLibrary(sourceFile.Path()) ||
host.IsSourceFromProjectReference(sourceFile.Path()) ||
!canIncludeBindAndCheckDiagnostics(sourceFile, options)
}

func canIncludeBindAndCheckDiagnostics(sourceFile *ast.SourceFile, options *core.CompilerOptions) bool {
if sourceFile.CheckJsDirective != nil && !sourceFile.CheckJsDirective.Enabled {
return false
}

if sourceFile.ScriptKind == core.ScriptKindTS || sourceFile.ScriptKind == core.ScriptKindTSX || sourceFile.ScriptKind == core.ScriptKindExternal {
return true
}

isJS := sourceFile.ScriptKind == core.ScriptKindJS || sourceFile.ScriptKind == core.ScriptKindJSX
isCheckJS := isJS && ast.IsCheckJSEnabledForFile(sourceFile, options)
isPlainJS := ast.IsPlainJSFile(sourceFile, options.CheckJs)

// By default, only type-check .ts, .tsx, Deferred, plain JS, checked JS and External
// - plain JS: .js files with no // ts-check and checkJs: undefined
// - check JS: .js files with either // ts-check or checkJs: true
// - external: files that are added by plugins
return isPlainJS || isCheckJS || sourceFile.ScriptKind == core.ScriptKindDeferred
}

func getEnclosingContainer(node *ast.Node) *ast.Node {
return ast.FindAncestor(node.Parent, func(n *ast.Node) bool {
return binder.GetContainerFlags(n)&binder.ContainerFlagsIsContainer != 0
Expand Down
47 changes: 12 additions & 35 deletions internal/compiler/checkerpool.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package compiler

import (
"context"
"iter"
"slices"
"sync"

Expand All @@ -12,17 +11,13 @@ import (
)

type CheckerPool interface {
Count() int
GetChecker(ctx context.Context) (*checker.Checker, func())
GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker))
Files(checker *checker.Checker) iter.Seq[*ast.SourceFile]
}

type checkerPool struct {
checkerCount int
program *Program
program *Program

createCheckersOnce sync.Once
checkers []*checker.Checker
Expand All @@ -43,32 +38,26 @@ func newCheckerPool(program *Program) *checkerPool {
checkerCount = max(min(checkerCount, len(program.files), 256), 1)

pool := &checkerPool{
program: program,
checkerCount: checkerCount,
checkers: make([]*checker.Checker, checkerCount),
locks: make([]*sync.Mutex, checkerCount),
program: program,
checkers: make([]*checker.Checker, checkerCount),
locks: make([]*sync.Mutex, checkerCount),
}

return pool
}

func (p *checkerPool) Count() int {
return p.checkerCount
}

func (p *checkerPool) GetCheckerForFile(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
p.createCheckers()
checker := p.fileAssociations[file]
return checker, noop
return p.fileAssociations[file], noop
}

func (p *checkerPool) GetCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
c, done := p.GetCheckerForFile(ctx, file)
p.createCheckers()
c := p.fileAssociations[file]
idx := slices.Index(p.checkers, c)
p.locks[idx].Lock()
return c, sync.OnceFunc(func() {
p.locks[idx].Unlock()
done()
})
}

Expand All @@ -80,8 +69,9 @@ func (p *checkerPool) GetChecker(ctx context.Context) (*checker.Checker, func())

func (p *checkerPool) createCheckers() {
p.createCheckersOnce.Do(func() {
checkerCount := len(p.checkers)
wg := core.NewWorkGroup(p.program.SingleThreaded())
for i := range p.checkerCount {
for i := range checkerCount {
wg.Queue(func() {
p.checkers[i], p.locks[i] = checker.NewChecker(p.program)
})
Expand All @@ -91,14 +81,14 @@ func (p *checkerPool) createCheckers() {

p.fileAssociations = make(map[*ast.SourceFile]*checker.Checker, len(p.program.files))
for i, file := range p.program.files {
p.fileAssociations[file] = p.checkers[i%p.checkerCount]
p.fileAssociations[file] = p.checkers[i%checkerCount]
}
})
}

// Runs `cb` for each checker in the pool concurrently, locking and unlocking checker mutexes as it goes,
// making it safe to call `ForEachCheckerParallel` from many threads simultaneously.
func (p *checkerPool) ForEachCheckerParallel(ctx context.Context, cb func(idx int, c *checker.Checker)) {
// making it safe to call `forEachCheckerParallel` from many threads simultaneously.
func (p *checkerPool) forEachCheckerParallel(cb func(idx int, c *checker.Checker)) {
p.createCheckers()
wg := core.NewWorkGroup(p.program.SingleThreaded())
for idx, checker := range p.checkers {
Expand All @@ -111,17 +101,4 @@ func (p *checkerPool) ForEachCheckerParallel(ctx context.Context, cb func(idx in
wg.RunAndWait()
}

func (p *checkerPool) Files(checker *checker.Checker) iter.Seq[*ast.SourceFile] {
checkerIndex := slices.Index(p.checkers, checker)
return func(yield func(*ast.SourceFile) bool) {
for i, file := range p.program.files {
if i%p.checkerCount == checkerIndex {
if !yield(file) {
return
}
}
}
}
}

func noop() {}
Loading