Skip to content

Commit cfc9b72

Browse files
authored
Fix race when copying lazy computed values for updateProgram (#2702)
1 parent a311dd9 commit cfc9b72

1 file changed

Lines changed: 109 additions & 102 deletions

File tree

internal/compiler/program.go

Lines changed: 109 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,34 @@ func (p *ProgramOptions) canUseProjectReferenceSource() bool {
4646
return p.UseSourceOfProjectReference && !p.Config.CompilerOptions().DisableSourceOfProjectReferenceRedirect.IsTrue()
4747
}
4848

49+
type lazyValue[T any] struct {
50+
value *T
51+
once sync.Once
52+
initialized atomic.Bool
53+
}
54+
55+
func (l *lazyValue[T]) getValue(compute func() *T) *T {
56+
l.once.Do(func() {
57+
if l.value == nil {
58+
l.value = compute()
59+
}
60+
l.initialized.Store(true)
61+
})
62+
return l.value
63+
}
64+
65+
func (l *lazyValue[T]) tryReuse(from *lazyValue[T]) {
66+
if from.initialized.Load() {
67+
l.value = from.value
68+
l.initialized.Store(true)
69+
}
70+
}
71+
72+
type packageNamesInfo struct {
73+
resolved *collections.Set[string]
74+
unresolved *collections.Set[string]
75+
}
76+
4977
type Program struct {
5078
opts ProgramOptions
5179
checkerPool CheckerPool
@@ -68,15 +96,11 @@ type Program struct {
6896
sourceFilesToEmit []*ast.SourceFile
6997

7098
// Cached unresolved imports for ATA
71-
unresolvedImportsOnce sync.Once
72-
unresolvedImports *collections.Set[string]
73-
knownSymlinks *symlinks.KnownSymlinks
74-
knownSymlinksOnce sync.Once
99+
unresolvedImports lazyValue[collections.Set[string]]
100+
knownSymlinks lazyValue[symlinks.KnownSymlinks]
75101

76102
// Used by auto-imports
77-
packageNamesOnce sync.Once
78-
resolvedPackageNames *collections.Set[string]
79-
unresolvedPackageNames *collections.Set[string]
103+
packageNames lazyValue[packageNamesInfo]
80104

81105
// Used by workspace/symbol
82106
hasTSFileOnce sync.Once
@@ -264,22 +288,17 @@ func (p *Program) UpdateProgram(changedFilePath tspath.Path, newHost CompilerHos
264288
usesUriStyleNodeCoreModules: p.usesUriStyleNodeCoreModules,
265289
programDiagnostics: p.programDiagnostics,
266290
hasEmitBlockingDiagnostics: p.hasEmitBlockingDiagnostics,
267-
unresolvedImports: p.unresolvedImports,
268-
resolvedPackageNames: p.resolvedPackageNames,
269-
unresolvedPackageNames: p.unresolvedPackageNames,
270-
knownSymlinks: p.knownSymlinks,
271291
}
292+
result.unresolvedImports.tryReuse(&p.unresolvedImports)
293+
result.knownSymlinks.tryReuse(&p.knownSymlinks)
294+
result.packageNames.tryReuse(&p.packageNames)
272295
result.initCheckerPool()
273296
index := core.FindIndex(result.files, func(file *ast.SourceFile) bool { return file.Path() == newFile.Path() })
274297
result.files = slices.Clone(result.files)
275298
result.files[index] = newFile
276299
result.filesByPath = maps.Clone(result.filesByPath)
277300
result.filesByPath[newFile.Path()] = newFile
278301
updateFileIncludeProcessor(result)
279-
result.knownSymlinks = symlinks.NewKnownSymlink(result.GetCurrentDirectory(), result.UseCaseSensitiveFileNames())
280-
if len(result.resolvedModules) > 0 || len(result.typeResolutionsInFile) > 0 {
281-
result.knownSymlinks.SetSymlinksFromResolutions(result.ForEachResolvedModule, result.ForEachResolvedTypeReferenceDirective)
282-
}
283302
return result, true
284303
}
285304

@@ -335,13 +354,7 @@ func (p *Program) GetConfigFileParsingDiagnostics() []*ast.Diagnostic {
335354
// GetUnresolvedImports returns the unresolved imports for this program.
336355
// The result is cached and computed only once.
337356
func (p *Program) GetUnresolvedImports() *collections.Set[string] {
338-
p.unresolvedImportsOnce.Do(func() {
339-
if p.unresolvedImports == nil {
340-
p.unresolvedImports = p.extractUnresolvedImports()
341-
}
342-
})
343-
344-
return p.unresolvedImports
357+
return p.unresolvedImports.getValue(p.extractUnresolvedImports)
345358
}
346359

347360
func (p *Program) extractUnresolvedImports() *collections.Set[string] {
@@ -1689,62 +1702,58 @@ func (p *Program) SourceFileMayBeEmitted(sourceFile *ast.SourceFile, forceDtsEmi
16891702
}
16901703

16911704
func (p *Program) ResolvedPackageNames() *collections.Set[string] {
1692-
p.collectPackageNames()
1693-
return p.resolvedPackageNames
1705+
return p.collectPackageNames().resolved
16941706
}
16951707

16961708
func (p *Program) UnresolvedPackageNames() *collections.Set[string] {
1697-
p.collectPackageNames()
1698-
return p.unresolvedPackageNames
1699-
}
1700-
1701-
func (p *Program) collectPackageNames() {
1702-
p.packageNamesOnce.Do(func() {
1703-
if p.resolvedPackageNames == nil {
1704-
p.resolvedPackageNames = &collections.Set[string]{}
1705-
p.unresolvedPackageNames = &collections.Set[string]{}
1706-
for _, file := range p.files {
1707-
if p.IsSourceFileDefaultLibrary(file.Path()) || p.IsSourceFileFromExternalLibrary(file) || strings.Contains(file.FileName(), "/node_modules/") {
1708-
// Checking for /node_modules/ is a little imprecise, but ATA treats locally installed typings
1709-
// as root files, which would not pass IsSourceFileFromExternalLibrary.
1709+
return p.collectPackageNames().unresolved
1710+
}
1711+
1712+
func (p *Program) collectPackageNames() *packageNamesInfo {
1713+
return p.packageNames.getValue(func() *packageNamesInfo {
1714+
packageNames := &packageNamesInfo{&collections.Set[string]{}, &collections.Set[string]{}}
1715+
for _, file := range p.files {
1716+
if p.IsSourceFileDefaultLibrary(file.Path()) || p.IsSourceFileFromExternalLibrary(file) || strings.Contains(file.FileName(), "/node_modules/") {
1717+
// Checking for /node_modules/ is a little imprecise, but ATA treats locally installed typings
1718+
// as root files, which would not pass IsSourceFileFromExternalLibrary.
1719+
continue
1720+
}
1721+
for _, imp := range file.Imports() {
1722+
if tspath.IsExternalModuleNameRelative(imp.Text()) {
17101723
continue
17111724
}
1712-
for _, imp := range file.Imports() {
1713-
if tspath.IsExternalModuleNameRelative(imp.Text()) {
1714-
continue
1715-
}
1716-
if resolvedModules, ok := p.resolvedModules[file.Path()]; ok {
1717-
key := module.ModeAwareCacheKey{Name: imp.Text(), Mode: p.GetModeForUsageLocation(file, imp)}
1718-
if resolvedModule, ok := resolvedModules[key]; ok && resolvedModule.IsResolved() {
1719-
if !resolvedModule.IsExternalLibraryImport {
1720-
continue
1721-
}
1722-
// Priority order for getting package name:
1723-
// 1. PackageId.Name (requires both name and version in package.json)
1724-
name := resolvedModule.PackageId.Name
1725-
if name == "" {
1726-
// 2. GetPackageScopeForPath - get name from package.json in the package directory
1727-
if packageScope := p.resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName); packageScope != nil && packageScope.Exists() {
1728-
if scopeName, ok := packageScope.Contents.Name.GetValue(); ok {
1729-
name = scopeName
1730-
}
1725+
if resolvedModules, ok := p.resolvedModules[file.Path()]; ok {
1726+
key := module.ModeAwareCacheKey{Name: imp.Text(), Mode: p.GetModeForUsageLocation(file, imp)}
1727+
if resolvedModule, ok := resolvedModules[key]; ok && resolvedModule.IsResolved() {
1728+
if !resolvedModule.IsExternalLibraryImport {
1729+
continue
1730+
}
1731+
// Priority order for getting package name:
1732+
// 1. PackageId.Name (requires both name and version in package.json)
1733+
name := resolvedModule.PackageId.Name
1734+
if name == "" {
1735+
// 2. GetPackageScopeForPath - get name from package.json in the package directory
1736+
if packageScope := p.resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName); packageScope != nil && packageScope.Exists() {
1737+
if scopeName, ok := packageScope.Contents.Name.GetValue(); ok {
1738+
name = scopeName
17311739
}
17321740
}
1733-
if name == "" {
1734-
// 3. GetPackageNameFromDirectory - extract from node_modules path
1735-
name = modulespecifiers.GetPackageNameFromDirectory(resolvedModule.ResolvedFileName)
1736-
}
1737-
// 4. If all fail, don't add empty string
1738-
if name != "" {
1739-
p.resolvedPackageNames.Add(name)
1740-
}
1741-
continue
17421741
}
1742+
if name == "" {
1743+
// 3. GetPackageNameFromDirectory - extract from node_modules path
1744+
name = modulespecifiers.GetPackageNameFromDirectory(resolvedModule.ResolvedFileName)
1745+
}
1746+
// 4. If all fail, don't add empty string
1747+
if name != "" {
1748+
packageNames.resolved.Add(name)
1749+
}
1750+
continue
17431751
}
1744-
p.unresolvedPackageNames.Add(imp.Text())
17451752
}
1753+
packageNames.unresolved.Add(imp.Text())
17461754
}
17471755
}
1756+
return packageNames
17481757
})
17491758
}
17501759

@@ -1766,54 +1775,52 @@ func (p *Program) HasTSFile() bool {
17661775
}
17671776

17681777
func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks {
1769-
p.knownSymlinksOnce.Do(func() {
1770-
if p.knownSymlinks == nil {
1771-
p.knownSymlinks = symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
1778+
return p.knownSymlinks.getValue(func() *symlinks.KnownSymlinks {
1779+
knownSymlinks := symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames())
1780+
1781+
// Resolved modules store realpath information when they're resolved inside node_modules
1782+
if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 {
1783+
knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective)
1784+
}
17721785

1773-
// Resolved modules store realpath information when they're resolved inside node_modules
1774-
if len(p.resolvedModules) > 0 || len(p.typeResolutionsInFile) > 0 {
1775-
p.knownSymlinks.SetSymlinksFromResolutions(p.ForEachResolvedModule, p.ForEachResolvedTypeReferenceDirective)
1786+
// Check other dependencies for symlinks
1787+
var seenPackageJsons collections.Set[tspath.Path]
1788+
for filePath, meta := range p.sourceFileMetaDatas {
1789+
if meta.PackageJsonDirectory == "" ||
1790+
!p.SourceFileMayBeEmitted(p.GetSourceFileByPath(filePath), false) ||
1791+
!seenPackageJsons.AddIfAbsent(p.toPath(meta.PackageJsonDirectory)) {
1792+
continue
1793+
}
1794+
packageJsonName := tspath.CombinePaths(meta.PackageJsonDirectory, "package.json")
1795+
info := p.GetPackageJsonInfo(packageJsonName)
1796+
if info.GetContents() == nil {
1797+
continue
17761798
}
17771799

1778-
// Check other dependencies for symlinks
1779-
var seenPackageJsons collections.Set[tspath.Path]
1780-
for filePath, meta := range p.sourceFileMetaDatas {
1781-
if meta.PackageJsonDirectory == "" ||
1782-
!p.SourceFileMayBeEmitted(p.GetSourceFileByPath(filePath), false) ||
1783-
!seenPackageJsons.AddIfAbsent(p.toPath(meta.PackageJsonDirectory)) {
1800+
for dep := range info.GetContents().GetRuntimeDependencyNames().Keys() {
1801+
// Skip work in common case: we already saved a symlink for this package directory
1802+
// in the node_modules adjacent to this package.json
1803+
possibleDirectoryPath := p.toPath(tspath.CombinePaths(meta.PackageJsonDirectory, "node_modules", dep))
1804+
if knownSymlinks.HasDirectory(possibleDirectoryPath) {
17841805
continue
17851806
}
1786-
packageJsonName := tspath.CombinePaths(meta.PackageJsonDirectory, "package.json")
1787-
info := p.GetPackageJsonInfo(packageJsonName)
1788-
if info.GetContents() == nil {
1789-
continue
1790-
}
1791-
1792-
for dep := range info.GetContents().GetRuntimeDependencyNames().Keys() {
1793-
// Skip work in common case: we already saved a symlink for this package directory
1794-
// in the node_modules adjacent to this package.json
1795-
possibleDirectoryPath := p.toPath(tspath.CombinePaths(meta.PackageJsonDirectory, "node_modules", dep))
1796-
if p.knownSymlinks.HasDirectory(possibleDirectoryPath) {
1807+
if !strings.HasPrefix(dep, "@types") {
1808+
possibleTypesDirectoryPath := p.toPath(tspath.CombinePaths(meta.PackageJsonDirectory, "node_modules", module.GetTypesPackageName(dep)))
1809+
if knownSymlinks.HasDirectory(possibleTypesDirectoryPath) {
17971810
continue
17981811
}
1799-
if !strings.HasPrefix(dep, "@types") {
1800-
possibleTypesDirectoryPath := p.toPath(tspath.CombinePaths(meta.PackageJsonDirectory, "node_modules", module.GetTypesPackageName(dep)))
1801-
if p.knownSymlinks.HasDirectory(possibleTypesDirectoryPath) {
1802-
continue
1803-
}
1804-
}
1812+
}
18051813

1806-
if packageResolution := p.resolver.ResolvePackageDirectory(dep, packageJsonName, core.ResolutionModeCommonJS, nil); packageResolution.IsResolved() {
1807-
p.knownSymlinks.ProcessResolution(
1808-
tspath.CombinePaths(packageResolution.OriginalPath, "package.json"),
1809-
tspath.CombinePaths(packageResolution.ResolvedFileName, "package.json"),
1810-
)
1811-
}
1814+
if packageResolution := p.resolver.ResolvePackageDirectory(dep, packageJsonName, core.ResolutionModeCommonJS, nil); packageResolution.IsResolved() {
1815+
knownSymlinks.ProcessResolution(
1816+
tspath.CombinePaths(packageResolution.OriginalPath, "package.json"),
1817+
tspath.CombinePaths(packageResolution.ResolvedFileName, "package.json"),
1818+
)
18121819
}
18131820
}
18141821
}
1822+
return knownSymlinks
18151823
})
1816-
return p.knownSymlinks
18171824
}
18181825

18191826
func (p *Program) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule {

0 commit comments

Comments
 (0)