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
7 changes: 6 additions & 1 deletion tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ type Checker struct {
ReverseMappedSymbolLinks core.LinkStore[*ast.Symbol, ReverseMappedSymbolLinks]
markedAssignmentSymbolLinks core.LinkStore[*ast.Symbol, MarkedAssignmentSymbolLinks]
symbolContainerLinks core.LinkStore[*ast.Symbol, ContainingSymbolLinks]
externalModuleContainers *externalModuleContainerIndex
sourceFileLinks core.LinkStore[*ast.SourceFile, SourceFileLinks]
regExpScanner *scanner.Scanner
patternForType map[*Type]*ast.Node
Expand Down Expand Up @@ -14599,8 +14600,12 @@ func (c *Checker) recordMergedSymbol(target *ast.Symbol, source *ast.Symbol) {
c.mergedSymbols[source] = target
}

func (c *Checker) getResolvedTarget(symbol *ast.Symbol) *ast.Symbol {
return c.getMergedSymbol(c.resolveSymbol(c.getMergedSymbol(symbol)))
}

func (c *Checker) getSymbolIfSameReference(s1 *ast.Symbol, s2 *ast.Symbol) *ast.Symbol {
if c.getMergedSymbol(c.resolveSymbol(c.getMergedSymbol(s1))) == c.getMergedSymbol(c.resolveSymbol(c.getMergedSymbol(s2))) {
if c.getResolvedTarget(s1) == c.getResolvedTarget(s2) {
return s1
}
return nil
Expand Down
112 changes: 93 additions & 19 deletions tsc/internal/checker/symbolaccessibility.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package checker

import (
"cmp"
"slices"

"github.com/microsoft/TypeScript/tsc/internal/ast"
Expand Down Expand Up @@ -206,21 +207,100 @@ func (c *Checker) getAlternativeContainingModules(symbol *ast.Symbol, enclosingD
if links.extendedContainers != nil {
return *links.extendedContainers
}
// No results from files already being imported by this file - expand search (expensive, but not location-specific, so cached)
otherFiles := c.program.SourceFiles()
for _, file := range otherFiles {
results = c.getExternalModuleContainers(symbol)
links.extendedContainers = &results
return results
}

type externalModuleContainerIndex struct {
complete bool
containersByTarget map[*ast.Symbol][]*ast.Symbol
moduleOrder map[*ast.Symbol]int
}

func (index *externalModuleContainerIndex) add(target *ast.Symbol, container *ast.Symbol) {
// Modules are indexed one at a time, so a repeat of this container is always the last entry.
if existing := index.containersByTarget[target]; len(existing) == 0 || existing[len(existing)-1] != container {
index.containersByTarget[target] = append(existing, container)
}
}

func (c *Checker) getExternalModuleContainers(symbol *ast.Symbol) []*ast.Symbol {
if c.externalModuleContainers == nil {
c.buildExternalModuleContainerIndex()
}
index := c.externalModuleContainers
if !index.complete {
// Re-entered from an alias resolved while building the index; answer this query without it.
return c.scanExternalModuleContainers(symbol)
}
containers := index.containersByTarget[c.getResolvedTarget(symbol)]
parent := c.getParentOfSymbol(symbol)
parentOrder, parentIsModule := index.moduleOrder[parent]
if !parentIsModule {
return containers
}
// The parent module contains the symbol even when the symbol is absent from its exports.
if at, found := slices.BinarySearchFunc(containers, parentOrder, func(container *ast.Symbol, order int) int {
return cmp.Compare(index.moduleOrder[container], order)
}); !found {
return slices.Insert(slices.Clone(containers), at, parent)
}
return containers
}

func (c *Checker) buildExternalModuleContainerIndex() {
index := &externalModuleContainerIndex{
containersByTarget: make(map[*ast.Symbol][]*ast.Symbol),
moduleOrder: make(map[*ast.Symbol]int, len(c.program.SourceFiles())),
}
c.externalModuleContainers = index
for _, file := range c.program.SourceFiles() {
if !ast.IsExternalModule(file) {
continue
}
sym := c.getSymbolOfDeclaration(file.AsNode())
ref := c.getAliasForSymbolInContainer(sym, symbol)
if ref == nil {
container := c.getSymbolOfDeclaration(file.AsNode())
index.moduleOrder[container] = len(index.moduleOrder)
for _, exported := range c.getExportsOfSymbol(container) {
index.add(c.getResolvedTarget(exported), container)
}
if exportEquals := container.Exports[ast.InternalSymbolNameExportEquals]; exportEquals != nil {
index.add(c.getResolvedTarget(exportEquals), container)
}
}
index.complete = true
}

func (c *Checker) scanExternalModuleContainers(symbol *ast.Symbol) []*ast.Symbol {
var containers []*ast.Symbol
for _, file := range c.program.SourceFiles() {
if !ast.IsExternalModule(file) {
continue
}
results = append(results, sym)
if container := c.getSymbolOfDeclaration(file.AsNode()); c.getAliasForSymbolInContainer(container, symbol) != nil {
containers = append(containers, container)
}
}
links.extendedContainers = &results
return results
return containers
}

func (c *Checker) getExportsByTarget(container *ast.Symbol) map[*ast.Symbol][]*ast.Symbol {
links := c.symbolContainerLinks.Get(container)
if links.exportsByTarget == nil {
exports := c.getExportsOfSymbol(container)
byTarget := make(map[*ast.Symbol][]*ast.Symbol, len(exports))
for _, exported := range exports {
target := c.getResolvedTarget(exported)
byTarget[target] = append(byTarget[target], exported)
}
for _, candidates := range byTarget {
if len(candidates) > 1 {
c.sortSymbols(candidates) // symbol tables are randomly iterated
}
}
links.exportsByTarget = byTarget
}
return links.exportsByTarget
}

func (c *Checker) getVariableDeclarationOfObjectLiteral(symbol *ast.Symbol, meaning ast.SymbolFlags) *ast.Symbol {
Expand Down Expand Up @@ -344,27 +424,21 @@ func (c *Checker) getAliasForSymbolInContainer(container *ast.Symbol, symbol *as
// fast path, `symbol` is either already the alias or isn't aliased
return symbol
}
target := c.getResolvedTarget(symbol)
// Check if container is a thing with an `export=` which points directly at `symbol`, and if so, return
// the container itself as the alias for the symbol
if container.Exports != nil {
exportEquals, ok := container.Exports[ast.InternalSymbolNameExportEquals]
if ok && exportEquals != nil && c.getSymbolIfSameReference(exportEquals, symbol) != nil {
if ok && exportEquals != nil && c.getResolvedTarget(exportEquals) == target {
return container
}
}
exports := c.getExportsOfSymbol(container)
quick, ok := exports[symbol.Name]
if ok && quick != nil && c.getSymbolIfSameReference(quick, symbol) != nil {
if ok && quick != nil && c.getResolvedTarget(quick) == target {
return quick
}
var candidates []*ast.Symbol
for _, exported := range exports {
if c.getSymbolIfSameReference(exported, symbol) != nil {
candidates = append(candidates, exported)
}
}
if len(candidates) > 0 {
c.sortSymbols(candidates) // _must_ sort exports for stable results - symbol table is randomly iterated
if candidates := c.getExportsByTarget(container)[target]; len(candidates) > 0 {
return candidates[0]
}
return nil
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ type ContainingSymbolLinks struct {
extendedContainersByFile map[ast.NodeId][]*ast.Symbol // Symbols of nodes which which logically contain this one, cached by file the request is made within
extendedContainers *[]*ast.Symbol // Containers (other than the parent) which this symbol is aliased in
accessibleChainCache map[accessibleChainCacheKey][]*ast.Symbol
exportsByTarget map[*ast.Symbol][]*ast.Symbol
}

type AccessFlags uint32
Expand Down
57 changes: 27 additions & 30 deletions tsc/internal/execute/incremental/affectedfileshandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package incremental

import (
"context"
"maps"
"slices"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -85,19 +84,17 @@ func (h *affectedFilesHandler) computeDtsSignature(file *ast.SourceFile) string
}

func (h *affectedFilesHandler) updateShapeSignature(file *ast.SourceFile, useFileVersionAsSignature bool) bool {
info, _ := h.program.snapshot.fileInfos.Load(file.Path())
update := &updatedSignature{}
update.mu.Lock()
defer update.mu.Unlock()
// If we have cached the result for this file, that means hence forth we should assume file shape is uptodate
if existing, ok := h.updatedSignatures.LoadOrStore(file.Path(), update); ok {
// Ensure calculations for existing ones are complete before using the value
existing.mu.Lock()
defer existing.mu.Unlock()
return false
return existing.signature != info.signature
}

info, _ := h.program.snapshot.fileInfos.Load(file.Path())
prevSignature := info.signature
// JSON files have no declaration output from which to compute a shape
// signature, so use the file version to conservatively invalidate dependents.
if !file.IsDeclarationFile && !ast.IsJsonSourceFile(file) && !useFileVersionAsSignature {
Expand All @@ -108,45 +105,47 @@ func (h *affectedFilesHandler) updateShapeSignature(file *ast.SourceFile, useFil
update.signature = info.version
update.kind = SignatureUpdateKindUsedVersion
}
return update.signature != prevSignature
return update.signature != info.signature
}

func (h *affectedFilesHandler) getFilesAffectedBy(path tspath.Path) []*ast.SourceFile {
func (h *affectedFilesHandler) collectFilesAffectedBy(path tspath.Path, wg core.WorkGroup, result *collections.SyncSet[*ast.SourceFile]) {
file := h.program.program.GetSourceFileByPath(path)
if file == nil {
return nil
return
}

result.Add(file)
if !h.updateShapeSignature(file, false) {
return []*ast.SourceFile{file}
return
}

if info, _ := h.program.snapshot.fileInfos.Load(file.Path()); info.affectsGlobalScope {
h.hasAllFilesExcludingDefaultLibraryFile.Store(true)
return h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, file)
for _, affectedFile := range h.program.snapshot.getAllFilesExcludingDefaultLibraryFile(h.program.program, file) {
result.Add(affectedFile)
}
return
}

if h.program.snapshot.options.IsolatedModules.IsTrue() {
return []*ast.SourceFile{file}
return
}

// Now we need to if each file in the referencedBy list has a shape change as well.
// Because if so, its own referencedBy files need to be saved as well to make the
// emitting result consistent with files on disk.
seenFileNamesMap := h.forEachFileReferencedBy(
file,
func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool) {
// If the current file is not nil and has a shape change, we need to queue it for processing
if currentFile != nil && h.updateShapeSignature(currentFile, false) {
return true, false
h.collectReferencingFiles(file, wg, result)
}

func (h *affectedFilesHandler) collectReferencingFiles(file *ast.SourceFile, wg core.WorkGroup, result *collections.SyncSet[*ast.SourceFile]) {
for path := range h.program.snapshot.referencedMap.getReferencedBy(file.Path()) {
currentFile := h.program.program.GetSourceFileByPath(path)
if currentFile == nil || !result.AddIfAbsent(currentFile) {
continue
}
wg.Queue(func() {
if h.updateShapeSignature(currentFile, false) {
h.collectReferencingFiles(currentFile, wg, result)
}
return false, false
},
)
// Return array of values that needs emit
return core.Filter(slices.Collect(maps.Values(seenFileNamesMap)), func(file *ast.SourceFile) bool {
return file != nil
})
})
}
}

func (h *affectedFilesHandler) forEachFileReferencedBy(file *ast.SourceFile, fn func(currentFile *ast.SourceFile, currentPath tspath.Path) (queueForFile bool, fastReturn bool)) map[tspath.Path]*ast.SourceFile {
Expand Down Expand Up @@ -366,9 +365,7 @@ func collectAllAffectedFiles(ctx context.Context, program *Program) {
var result collections.SyncSet[*ast.SourceFile]
program.snapshot.changedFilesSet.Range(func(file tspath.Path) bool {
wg.Queue(func() {
for _, affectedFile := range handler.getFilesAffectedBy(file) {
result.Add(affectedFile)
}
handler.collectFilesAffectedBy(file, wg, &result)
})
return true
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package incremental

import (
"testing"

"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/bundled"
"github.com/microsoft/TypeScript/tsc/internal/collections"
"github.com/microsoft/TypeScript/tsc/internal/compiler"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/tsoptions"
"github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest"
"gotest.tools/v3/assert"
)

func TestUpdateShapeSignatureCachedResult(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}
fs := bundled.WrapFS(vfstest.FromMap(map[string]string{
"/tsconfig.json": `{"compilerOptions":{"strict":true,"noEmit":true,"incremental":true,"skipLibCheck":true}}`,
"/a.ts": `export const a = 1;`,
"/b.ts": `import { a } from "./a"; export const b = a; declare global { interface Window { fromB: string; } }`,
"/c.ts": `export const c = window.fromB;`,
"/d.ts": `import { b } from "./b"; export const d = 2;`,
}, true))
build := func(old *Program) *Program {
host := compiler.NewCompilerHost("/", fs, bundled.LibPath(), nil, nil, nil)
config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/tsconfig.json", &core.CompilerOptions{}, nil, host, nil)
assert.Equal(t, len(diagnostics), 0)
if old == nil {
old = ReadBuildInfoProgram(config, NewBuildInfoReader(host), host)
}
return NewProgram(compiler.NewProgram(compiler.ProgramOptions{Config: config, Host: host}), old, CreateHost(host), nil, false)
}
check := func() {
program := build(nil)
assert.Equal(t, len(program.GetSemanticDiagnostics(t.Context(), nil)), 0)
assert.Equal(t, len(program.Emit(t.Context(), compiler.EmitOptions{}).Diagnostics), 0)
}
check()
// A rebuild replaces the version-based signatures of a, b and d with computed ones.
assert.NilError(t, fs.AppendFile("/a.ts", "\n// edit\n"))
check()

assert.NilError(t, fs.WriteFile("/a.ts", `export const a = "changed";`))
assert.NilError(t, fs.AppendFile("/b.ts", "\n// edit\n"))
program := build(nil)
h := affectedFilesHandler{ctx: t.Context(), program: program}
b := program.program.GetSourceFile("/b.ts")
c := program.program.GetSourceFile("/c.ts")
d := program.program.GetSourceFile("/d.ts")

assert.Assert(t, h.updateShapeSignature(b, false))
assert.Assert(t, h.updateShapeSignature(b, false), "cached result must still report the changed signature")
assert.Assert(t, !h.updateShapeSignature(d, false))
assert.Assert(t, !h.updateShapeSignature(d, false))

wg := core.NewWorkGroup(true)
var result collections.SyncSet[*ast.SourceFile]
wg.Queue(func() { h.collectFilesAffectedBy(b.Path(), wg, &result) })
wg.RunAndWait()
assert.Assert(t, h.hasAllFilesExcludingDefaultLibraryFile.Load(), "global-scope invalidation must not depend on which traversal computed the signature")
assert.Assert(t, result.Has(c))
}
Loading