vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,167 @@
package compiler
import (
"context"
"slices"
"sync"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tracing"
)
// CheckerPool is implemented by the project system to provide checkers with
// request-scoped lifetime and reclamation. It returns a checker and a release
// function that must be called when the caller is done with the checker.
// The returned checker must not be accessed concurrently; each acquisition is exclusive.
// If file is non-nil, the pool may use it as an affinity hint to return the same
// checker for the same file across calls.
type CheckerPool interface {
GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func())
}
type checkerPool struct {
program *Program
tracing *tracing.Tracing
createCheckersOnce sync.Once
checkers []*checker.Checker
locks []*sync.Mutex
fileAssociations map[*ast.SourceFile]*checker.Checker
}
var _ CheckerPool = (*checkerPool)(nil)
func newCheckerPool(program *Program) *checkerPool {
return newCheckerPoolWithTracing(program, nil)
}
func newCheckerPoolWithTracing(program *Program, tr *tracing.Tracing) *checkerPool {
checkerCount := 4
if program.SingleThreaded() {
checkerCount = 1
} else if c := program.Options().Checkers; c != nil {
checkerCount = *c
}
checkerCount = max(min(checkerCount, len(program.files), 256), 1)
pool := &checkerPool{
program: program,
checkers: make([]*checker.Checker, checkerCount),
locks: make([]*sync.Mutex, checkerCount),
tracing: tr,
}
return pool
}
// GetChecker implements CheckerPool. When file is non-nil, returns the checker
// associated with that file; otherwise returns the first checker.
func (p *checkerPool) GetChecker(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
if file != nil {
return p.getCheckerForFileExclusive(ctx, file)
}
p.createCheckers()
c := p.checkers[0]
p.locks[0].Lock()
return c, sync.OnceFunc(func() {
p.locks[0].Unlock()
})
}
// getCheckerForFileNonExclusive returns the checker for the given file without locking.
// This is only safe when the caller guarantees no concurrent access to the same checker,
// e.g. for read-only operations like obtaining an emit resolver.
func (p *checkerPool) getCheckerForFileNonExclusive(file *ast.SourceFile) (*checker.Checker, func()) {
p.createCheckers()
return p.fileAssociations[file], noop
}
func (p *checkerPool) getCheckerForFileExclusive(ctx context.Context, file *ast.SourceFile) (*checker.Checker, func()) {
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()
})
}
// getCheckerNonExclusive returns the first checker without locking.
func (p *checkerPool) getCheckerNonExclusive() (*checker.Checker, func()) {
p.createCheckers()
return p.checkers[0], noop
}
func (p *checkerPool) createCheckers() {
p.createCheckersOnce.Do(func() {
checkerCount := len(p.checkers)
wg := core.NewWorkGroup(p.program.SingleThreaded())
for i := range checkerCount {
wg.Queue(func() {
var tracer *checker.Tracer
if p.tracing != nil {
tracer = checker.NewTracer(p.tracing, i)
}
p.checkers[i], p.locks[i] = checker.NewChecker(p.program, tracer)
})
}
wg.RunAndWait()
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%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(cb func(idx int, c *checker.Checker)) {
p.createCheckers()
wg := core.NewWorkGroup(p.program.SingleThreaded())
for idx, checker := range p.checkers {
wg.Queue(func() {
p.locks[idx].Lock()
defer p.locks[idx].Unlock()
cb(idx, checker)
})
}
wg.RunAndWait()
}
func (p *checkerPool) GetGlobalDiagnostics() []*ast.Diagnostic {
p.createCheckers()
globalDiagnostics := make([][]*ast.Diagnostic, len(p.checkers))
p.forEachCheckerParallel(func(idx int, checker *checker.Checker) {
globalDiagnostics[idx] = checker.GetGlobalDiagnostics()
})
return SortAndDeduplicateDiagnostics(slices.Concat(globalDiagnostics...))
}
// 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)) {
p.createCheckers()
checkerCount := len(p.checkers)
wg := core.NewWorkGroup(singleThreaded)
for checkerIdx := range checkerCount {
wg.Queue(func() {
p.locks[checkerIdx].Lock()
defer p.locks[checkerIdx].Unlock()
for i, file := range files {
if checker := p.checkers[checkerIdx]; checker == p.fileAssociations[file] {
cb(checker, i, file)
}
}
})
}
wg.RunAndWait()
}
func noop() {}

View File

@@ -0,0 +1,139 @@
package compiler
import (
"context"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/outputpaths"
"github.com/microsoft/typescript-go/internal/packagejson"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/symlinks"
"github.com/microsoft/typescript-go/internal/transformers/declarations"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
// NOTE: EmitHost operations must be thread-safe
type EmitHost interface {
printer.EmitHost
declarations.DeclarationEmitHost
Options() *core.CompilerOptions
SourceFiles() []*ast.SourceFile
UseCaseSensitiveFileNames() bool
GetCurrentDirectory() string
CommonSourceDirectory() string
IsEmitBlocked(file string) bool
}
var _ EmitHost = (*emitHost)(nil)
// NOTE: emitHost operations must be thread-safe
type emitHost struct {
program *Program
emitResolver printer.EmitResolver
}
func newEmitHost(ctx context.Context, program *Program, file *ast.SourceFile) (*emitHost, func()) {
checker, done := program.GetTypeCheckerForFile(ctx, file)
return &emitHost{
program: program,
emitResolver: checker.GetEmitResolver(),
}, done
}
func (host *emitHost) GetModeForUsageLocation(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) core.ResolutionMode {
return host.program.GetModeForUsageLocation(file, moduleSpecifier)
}
func (host *emitHost) GetResolvedModuleFromModuleSpecifier(file ast.HasFileName, moduleSpecifier *ast.StringLiteralLike) *module.ResolvedModule {
return host.program.GetResolvedModuleFromModuleSpecifier(file, moduleSpecifier)
}
func (host *emitHost) GetDefaultResolutionModeForFile(file ast.HasFileName) core.ResolutionMode {
return host.program.GetDefaultResolutionModeForFile(file)
}
func (host *emitHost) GetEmitModuleFormatOfFile(file ast.HasFileName) core.ModuleKind {
return host.program.GetEmitModuleFormatOfFile(file)
}
func (host *emitHost) FileExists(path string) bool {
return host.program.FileExists(path)
}
func (host *emitHost) GetGlobalTypingsCacheLocation() string {
return host.program.GetGlobalTypingsCacheLocation()
}
func (host *emitHost) GetNearestAncestorDirectoryWithPackageJson(dirname string) string {
return host.program.GetNearestAncestorDirectoryWithPackageJson(dirname)
}
func (host *emitHost) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry {
return host.program.GetPackageJsonInfo(pkgJsonPath)
}
func (host *emitHost) GetSourceOfProjectReferenceIfOutputIncluded(file ast.HasFileName) string {
return host.program.GetSourceOfProjectReferenceIfOutputIncluded(file)
}
func (host *emitHost) GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
return host.program.GetProjectReferenceFromSource(path)
}
func (host *emitHost) GetRedirectTargets(path tspath.Path) []string {
return host.program.GetRedirectTargets(path)
}
func (host *emitHost) GetEffectiveDeclarationFlags(node *ast.Node, flags ast.ModifierFlags) ast.ModifierFlags {
return host.GetEmitResolver().GetEffectiveDeclarationFlags(node, flags)
}
func (host *emitHost) GetOutputPathsFor(file *ast.SourceFile, forceDtsPaths bool) declarations.OutputPaths {
// TODO: cache
return outputpaths.GetOutputPathsFor(file, host.Options(), host, forceDtsPaths)
}
func (host *emitHost) GetResolutionModeOverride(node *ast.Node) core.ResolutionMode {
return host.GetEmitResolver().GetResolutionModeOverride(node)
}
func (host *emitHost) GetSourceFileFromReference(origin *ast.SourceFile, ref *ast.FileReference) *ast.SourceFile {
return host.program.GetSourceFileFromReference(origin, ref)
}
func (host *emitHost) Options() *core.CompilerOptions { return host.program.Options() }
func (host *emitHost) SourceFiles() []*ast.SourceFile { return host.program.SourceFiles() }
func (host *emitHost) GetCurrentDirectory() string { return host.program.GetCurrentDirectory() }
func (host *emitHost) CommonSourceDirectory() string { return host.program.CommonSourceDirectory() }
func (host *emitHost) UseCaseSensitiveFileNames() bool {
return host.program.UseCaseSensitiveFileNames()
}
func (host *emitHost) IsEmitBlocked(file string) bool {
return host.program.IsEmitBlocked(file)
}
func (host *emitHost) WriteFile(fileName string, text string) error {
return host.program.Host().FS().WriteFile(fileName, text)
}
func (host *emitHost) GetEmitResolver() printer.EmitResolver {
return host.emitResolver
}
func (host *emitHost) IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool {
return host.program.IsSourceFileFromExternalLibrary(file)
}
func (host *emitHost) GetSymlinkCache() *symlinks.KnownSymlinks {
return host.program.GetSymlinkCache()
}
func (host *emitHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule {
resolved, _ := host.program.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil)
return resolved
}

View File

@@ -0,0 +1,191 @@
package compiler_test
import (
"context"
"fmt"
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
)
// generateLongLineTS generates TypeScript source code that produces a single very long line.
// This simulates generated code (e.g., from code generators) that has no line breaks,
// which triggers O(n²) behavior in source map generation due to
// GetECMALineAndUTF16CharacterOfPosition scanning from line start for each position.
func generateLongLineTS(numProperties int) string {
// Build a large object literal all on one line, with no line breaks.
var b strings.Builder
b.WriteString("export const data: Record<string, number> = {")
for i := range numProperties {
if i > 0 {
b.WriteString(", ")
}
fmt.Fprintf(&b, "prop_%d: %d", i, i)
}
b.WriteString("};")
return b.String()
}
func BenchmarkEmitLongLines(b *testing.B) {
if !bundled.Embedded {
b.Skip("bundled files are not embedded")
}
for _, numProps := range []int{1000, 5000, 10000} {
b.Run(fmt.Sprintf("props_%d", numProps), func(b *testing.B) {
source := generateLongLineTS(numProps)
fs := vfstest.FromMap(map[string]string{
"/dev/src/index.ts": source,
}, true /*useCaseSensitiveFileNames*/)
fs = bundled.WrapFS(fs)
opts := core.CompilerOptions{
Target: core.ScriptTargetES2015,
SourceMap: core.TSTrue,
OutDir: "/dev/out",
}
host := compiler.NewCompilerHost("/dev/src", fs, bundled.LibPath(), nil, nil)
p := compiler.NewProgram(compiler.ProgramOptions{
Config: &tsoptions.ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
FileNames: []string{"/dev/src/index.ts"},
CompilerOptions: &opts,
},
},
Host: host,
})
// Discard written files — we only care about emit performance.
nopWriteFile := func(fileName string, text string, data *compiler.WriteFileData) error {
return nil
}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
p.Emit(context.Background(), compiler.EmitOptions{
WriteFile: nopWriteFile,
})
}
})
}
}
func BenchmarkEmitManyFiles(b *testing.B) {
if !bundled.Embedded {
b.Skip("bundled files are not embedded")
}
// Simulate many files with moderately long single-line content.
numFiles := 200
numPropsPerFile := 500
files := make(map[string]string, numFiles)
fileNames := make([]string, 0, numFiles)
for i := range numFiles {
name := fmt.Sprintf("/dev/src/file_%d.ts", i)
files[name] = generateLongLineTS(numPropsPerFile)
fileNames = append(fileNames, name)
}
fs := vfstest.FromMap(files, true)
fs = bundled.WrapFS(fs)
opts := core.CompilerOptions{
Target: core.ScriptTargetES2015,
SourceMap: core.TSTrue,
OutDir: "/dev/out",
}
host := compiler.NewCompilerHost("/dev/src", fs, bundled.LibPath(), nil, nil)
p := compiler.NewProgram(compiler.ProgramOptions{
Config: &tsoptions.ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
FileNames: fileNames,
CompilerOptions: &opts,
},
},
Host: host,
})
nopWriteFile := func(fileName string, text string, data *compiler.WriteFileData) error {
return nil
}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
p.Emit(context.Background(), compiler.EmitOptions{
WriteFile: nopWriteFile,
})
}
}
// BenchmarkEmitLongLinesWithLineBreaks is a control benchmark that emits the same amount
// of code but WITH line breaks, showing that the issue is specific to long lines.
func BenchmarkEmitLongLinesWithLineBreaks(b *testing.B) {
if !bundled.Embedded {
b.Skip("bundled files are not embedded")
}
numProperties := 10000
// Same content but with newlines between each property.
var sb strings.Builder
sb.WriteString("export const data: Record<string, number> = {\n")
for i := range numProperties {
if i > 0 {
sb.WriteString(",\n")
}
fmt.Fprintf(&sb, " prop_%d: %d", i, i)
}
sb.WriteString("\n};\n")
source := sb.String()
fs := vfstest.FromMap(map[string]string{
"/dev/src/index.ts": source,
}, true)
fs = bundled.WrapFS(fs)
opts := core.CompilerOptions{
Target: core.ScriptTargetES2015,
SourceMap: core.TSTrue,
OutDir: "/dev/out",
}
host := compiler.NewCompilerHost("/dev/src", fs, bundled.LibPath(), nil, nil)
p := compiler.NewProgram(compiler.ProgramOptions{
Config: &tsoptions.ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
FileNames: []string{"/dev/src/index.ts"},
CompilerOptions: &opts,
},
},
Host: host,
})
nopWriteFile := func(fileName string, text string, data *compiler.WriteFileData) error {
return nil
}
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
p.Emit(context.Background(), compiler.EmitOptions{
WriteFile: nopWriteFile,
})
}
}

View File

@@ -0,0 +1,531 @@
package compiler
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/binder"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/outputpaths"
"github.com/microsoft/typescript-go/internal/printer"
"github.com/microsoft/typescript-go/internal/sourcemap"
"github.com/microsoft/typescript-go/internal/stringutil"
"github.com/microsoft/typescript-go/internal/tracing"
"github.com/microsoft/typescript-go/internal/transformers"
"github.com/microsoft/typescript-go/internal/transformers/declarations"
"github.com/microsoft/typescript-go/internal/transformers/estransforms"
"github.com/microsoft/typescript-go/internal/transformers/inliners"
"github.com/microsoft/typescript-go/internal/transformers/jsxtransforms"
"github.com/microsoft/typescript-go/internal/transformers/moduletransforms"
"github.com/microsoft/typescript-go/internal/transformers/tstransforms"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type EmitOnly byte
const (
EmitAll EmitOnly = iota
EmitOnlyJs
EmitOnlyDts
EmitOnlyForcedDts
)
type emitter struct {
host EmitHost
emitOnly EmitOnly
emitterDiagnostics ast.DiagnosticsCollection
writer printer.EmitTextWriter
paths *outputpaths.OutputPaths
sourceFile *ast.SourceFile
emitResult EmitResult
writeFile func(fileName string, text string, data *WriteFileData) error
tr *tracing.Tracing
}
func (e *emitter) emit() {
if e.tr != nil {
defer e.tr.Push(tracing.PhaseEmit, "emit", map[string]any{"path": string(e.sourceFile.Path())}, true)()
}
e.emitJSFile(e.sourceFile, e.paths.JsFilePath(), e.paths.SourceMapFilePath())
e.emitDeclarationFile(e.sourceFile, e.paths.DeclarationFilePath(), e.paths.DeclarationMapPath())
e.emitResult.Diagnostics = e.emitterDiagnostics.GetDiagnostics()
}
func (e *emitter) getDeclarationTransformers(emitContext *printer.EmitContext, declarationFilePath string, declarationMapPath string) []*declarations.DeclarationTransformer {
transform := declarations.NewDeclarationTransformer(e.host, emitContext, e.host.Options(), declarationFilePath, declarationMapPath)
return []*declarations.DeclarationTransformer{transform}
}
func (e *emitter) runScriptTransformers(emitContext *printer.EmitContext, sourceFile *ast.SourceFile) *ast.SourceFile {
if e.tr != nil {
defer e.tr.Push(tracing.PhaseEmit, "transformNodes", map[string]any{"path": string(sourceFile.Path())}, false)()
}
for _, transformer := range getScriptTransformers(emitContext, e.host, sourceFile) {
sourceFile = transformer.TransformSourceFile(sourceFile)
}
return sourceFile
}
func (e *emitter) runDeclarationTransformers(emitContext *printer.EmitContext, sourceFile *ast.SourceFile, declarationFilePath, declarationMapPath string) (*ast.SourceFile, []*ast.Diagnostic) {
if e.tr != nil {
defer e.tr.Push(tracing.PhaseEmit, "transformNodes", map[string]any{"path": string(sourceFile.Path())}, false)()
}
var diags []*ast.Diagnostic
for _, transformer := range e.getDeclarationTransformers(emitContext, declarationFilePath, declarationMapPath) {
sourceFile = transformer.TransformSourceFile(sourceFile)
diags = append(diags, transformer.GetDiagnostics()...)
}
return sourceFile, diags
}
func getModuleTransformer(opts *transformers.TransformOptions) *transformers.Transformer {
switch opts.CompilerOptions.GetEmitModuleKind() {
case core.ModuleKindPreserve:
// `ESModuleTransformer` contains logic for preserving CJS input syntax in `--module preserve`
return moduletransforms.NewESModuleTransformer(opts)
case core.ModuleKindESNext,
core.ModuleKindES2022,
core.ModuleKindES2020,
core.ModuleKindES2015,
core.ModuleKindNode20,
core.ModuleKindNode18,
core.ModuleKindNode16,
core.ModuleKindNodeNext,
core.ModuleKindCommonJS:
return moduletransforms.NewImpliedModuleTransformer(opts)
default:
return moduletransforms.NewCommonJSModuleTransformer(opts)
}
}
func getScriptTransformers(emitContext *printer.EmitContext, host printer.EmitHost, sourceFile *ast.SourceFile) []*transformers.Transformer {
var tx []*transformers.Transformer
options := host.Options()
// JS files don't use reference calculations as they don't do import elision, no need to calculate it
importElisionEnabled := !options.VerbatimModuleSyntax.IsTrue() && !ast.IsInJSFile(sourceFile.AsNode())
jsxTransformEnabled := options.GetJSXTransformEnabled() && sourceFile.LanguageVariant == core.LanguageVariantJSX
emitResolver := host.GetEmitResolver()
var referenceResolver binder.ReferenceResolver
if importElisionEnabled || jsxTransformEnabled || !options.GetIsolatedModules() || options.EmitDecoratorMetadata.IsTrue() {
referenceResolver = emitResolver
} else {
referenceResolver = binder.NewReferenceResolver(options, binder.ReferenceResolverHooks{})
}
opts := transformers.TransformOptions{
Context: emitContext,
CompilerOptions: options,
Resolver: referenceResolver,
EmitResolver: emitResolver,
GetEmitModuleFormatOfFile: host.GetEmitModuleFormatOfFile,
}
// transform TypeScript syntax
{
// use type nodes to add metadata decorators
if options.EmitDecoratorMetadata.IsTrue() {
tx = append(tx, tstransforms.NewMetadataTransformer(&opts))
}
// erase types
tx = append(tx, tstransforms.NewTypeEraserTransformer(&opts))
// elide imports
if importElisionEnabled {
tx = append(tx, tstransforms.NewImportElisionTransformer(&opts))
}
// transform `enum`, `namespace`, and parameter properties
tx = append(tx, tstransforms.NewRuntimeSyntaxTransformer(&opts))
if options.ExperimentalDecorators.IsTrue() {
tx = append(tx, tstransforms.NewLegacyDecoratorsTransformer(&opts))
}
}
if jsxTransformEnabled {
tx = append(tx, jsxtransforms.NewJSXTransformer(&opts))
}
downleveler := estransforms.GetESTransformer(&opts)
if downleveler != nil {
tx = append(tx, downleveler)
}
tx = append(tx, estransforms.NewUseStrictTransformer(&opts))
// transform module syntax
tx = append(tx, getModuleTransformer(&opts))
// inlining (formerly done via substitutions)
if !options.GetIsolatedModules() {
tx = append(tx, inliners.NewConstEnumInliningTransformer(&opts))
}
return tx
}
func (e *emitter) emitJSFile(sourceFile *ast.SourceFile, jsFilePath string, sourceMapFilePath string) {
options := e.host.Options()
if sourceFile == nil || e.emitOnly != EmitAll && e.emitOnly != EmitOnlyJs || len(jsFilePath) == 0 {
return
}
if options.NoEmit == core.TSTrue || e.host.IsEmitBlocked(jsFilePath) {
e.emitResult.EmitSkipped = true
return
}
if e.tr != nil {
defer e.tr.Push(tracing.PhaseEmit, "emitJsFileOrBundle", map[string]any{"jsFilePath": jsFilePath}, true)()
}
emitContext, putEmitContext := printer.GetEmitContext()
defer putEmitContext()
sourceFile = e.runScriptTransformers(emitContext, sourceFile)
printerOptions := printer.PrinterOptions{
RemoveComments: options.RemoveComments.IsTrue(),
NewLine: options.NewLine,
NoEmitHelpers: options.NoEmitHelpers.IsTrue(),
SourceMap: options.SourceMap.IsTrue(),
InlineSourceMap: options.InlineSourceMap.IsTrue(),
InlineSources: options.InlineSources.IsTrue(),
Target: options.Target,
// !!!
}
// create a printer to print the nodes
printer := printer.NewPrinter(printerOptions, printer.PrintHandlers{
// !!!
}, emitContext)
e.printSourceFile(jsFilePath, sourceMapFilePath, sourceFile, printer, options, shouldEmitSourceMaps(options, sourceFile))
}
func (e *emitter) emitDeclarationFile(sourceFile *ast.SourceFile, declarationFilePath string, declarationMapPath string) {
options := e.host.Options()
if sourceFile == nil || e.emitOnly == EmitOnlyJs || len(declarationFilePath) == 0 {
return
}
if e.tr != nil {
defer e.tr.Push(tracing.PhaseEmit, "emitDeclarationFileOrBundle", map[string]any{"declarationFilePath": declarationFilePath}, true)()
}
emitContext, putEmitContext := printer.GetEmitContext()
defer putEmitContext()
sourceFile, diags := e.runDeclarationTransformers(emitContext, sourceFile, declarationFilePath, declarationMapPath)
for _, elem := range diags {
// Add declaration transform diagnostics to emit diagnostics
e.emitterDiagnostics.Add(elem)
}
if e.emitOnly != EmitOnlyForcedDts && (options.NoEmit == core.TSTrue || e.host.IsEmitBlocked(declarationFilePath)) {
e.emitResult.EmitSkipped = true
return
}
declBlocked := len(diags) > 0 && e.emitOnly != EmitOnlyForcedDts
if declBlocked {
e.emitResult.EmitSkipped = true
return
}
printerOptions := printer.PrinterOptions{
RemoveComments: options.RemoveComments.IsTrue(),
NewLine: options.NewLine,
NoEmitHelpers: true,
// Module: options.Module, // NYI
// ModuleResolution: options.ModuleResolution, // NYI
Target: options.GetEmitScriptTarget(),
SourceMap: e.emitOnly != EmitOnlyForcedDts && options.DeclarationMap.IsTrue(),
InlineSourceMap: options.InlineSourceMap.IsTrue(),
// InlineSources: options.InlineSources.IsTrue(), // ignored, per strada
// ExtendedDiagnostics: options.ExtendedDiagnostics.IsTrue(), // NYI
OnlyPrintJSDocStyle: true,
OmitBraceSourceMapPositions: true,
}
// create a printer to print the nodes
printer := printer.NewPrinter(printerOptions, printer.PrintHandlers{
// !!!
}, emitContext)
declarationMapOptions := &core.CompilerOptions{
SourceMap: core.IfElse(e.emitOnly != EmitOnlyForcedDts && options.DeclarationMap.IsTrue(), core.TSTrue, core.TSFalse),
SourceRoot: options.SourceRoot,
MapRoot: options.MapRoot,
// Explicitly do not pass through either inline option.
}
e.printSourceFile(declarationFilePath, declarationMapPath, sourceFile, printer, declarationMapOptions, shouldEmitSourceMaps(declarationMapOptions, sourceFile))
}
func (e *emitter) printSourceFile(jsFilePath string, sourceMapFilePath string, sourceFile *ast.SourceFile, printer_ *printer.Printer, mapOptions *core.CompilerOptions, shouldEmitSourceMaps bool) {
// !!! sourceMapGenerator
options := e.host.Options()
var sourceMapGenerator *sourcemap.Generator
if shouldEmitSourceMaps {
sourceMapGenerator = sourcemap.NewGenerator(
tspath.GetBaseFileName(tspath.NormalizeSlashes(jsFilePath)),
getSourceRoot(mapOptions),
e.getSourceMapDirectory(mapOptions, jsFilePath, sourceFile),
tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: e.host.UseCaseSensitiveFileNames(),
CurrentDirectory: e.host.GetCurrentDirectory(),
},
)
}
printer_.Write(sourceFile.AsNode(), sourceFile, e.writer, sourceMapGenerator)
sourceMapUrlPos := -1
if sourceMapGenerator != nil {
if mapOptions.SourceMap.IsTrue() || mapOptions.InlineSourceMap.IsTrue() {
e.emitResult.SourceMaps = append(e.emitResult.SourceMaps, &SourceMapEmitResult{
InputSourceFileNames: sourceMapGenerator.Sources(),
SourceMap: sourceMapGenerator.RawSourceMap(),
GeneratedFile: jsFilePath,
})
}
sourceMappingURL := e.getSourceMappingURL(
mapOptions,
sourceMapGenerator,
jsFilePath,
sourceMapFilePath,
sourceFile,
)
if len(sourceMappingURL) > 0 {
if !e.writer.IsAtStartOfLine() {
e.writer.RawWrite(core.IfElse(options.NewLine == core.NewLineKindCRLF, "\r\n", "\n"))
}
sourceMapUrlPos = e.writer.GetTextPos()
e.writer.WriteComment("//# sourceMappingURL=")
e.writer.WriteComment(sourceMappingURL)
}
// Write the source map
if len(sourceMapFilePath) > 0 {
sourceMap := sourceMapGenerator.String()
err := e.writeText(sourceMapFilePath, sourceMap, nil)
if err != nil {
e.emitterDiagnostics.Add(ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, jsFilePath, err.Error()))
} else {
e.emitResult.EmittedFiles = append(e.emitResult.EmittedFiles, sourceMapFilePath)
}
}
} else {
e.writer.WriteLine()
}
// Write the output file
text := e.writer.String()
if options.EmitBOM.IsTrue() {
text = stringutil.AddUTF8ByteOrderMark(text)
}
data := &WriteFileData{
SourceMapUrlPos: sourceMapUrlPos,
Diagnostics: e.emitterDiagnostics.GetDiagnostics(),
}
err := e.writeText(jsFilePath, text, data)
skippedDtsWrite := data.SkippedDtsWrite
if err != nil {
e.emitterDiagnostics.Add(ast.NewCompilerDiagnostic(diagnostics.Could_not_write_file_0_Colon_1, jsFilePath, err.Error()))
} else if !skippedDtsWrite {
e.emitResult.EmittedFiles = append(e.emitResult.EmittedFiles, jsFilePath)
}
// Reset state
e.writer.Clear()
}
func (e *emitter) writeText(fileName string, text string, data *WriteFileData) error {
if e.writeFile != nil {
return e.writeFile(fileName, text, data)
}
return e.host.WriteFile(fileName, text)
}
func shouldEmitSourceMaps(mapOptions *core.CompilerOptions, sourceFile *ast.SourceFile) bool {
return (mapOptions.SourceMap.IsTrue() || mapOptions.InlineSourceMap.IsTrue()) &&
!tspath.FileExtensionIs(sourceFile.FileName(), tspath.ExtensionJson)
}
func getSourceRoot(mapOptions *core.CompilerOptions) string {
// Normalize source root and make sure it has trailing "/" so that it can be used to combine paths with the
// relative paths of the sources list in the sourcemap
sourceRoot := tspath.NormalizeSlashes(mapOptions.SourceRoot)
if len(sourceRoot) > 0 {
sourceRoot = tspath.EnsureTrailingDirectorySeparator(sourceRoot)
}
return sourceRoot
}
func (e *emitter) getSourceMapDirectory(mapOptions *core.CompilerOptions, filePath string, sourceFile *ast.SourceFile) string {
if len(mapOptions.SourceRoot) > 0 {
return e.host.CommonSourceDirectory()
}
if len(mapOptions.MapRoot) > 0 {
sourceMapDir := tspath.NormalizeSlashes(mapOptions.MapRoot)
if sourceFile != nil {
// For modules or multiple emit files the mapRoot will have directory structure like the sources
// So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map
sourceMapDir = tspath.GetDirectoryPath(outputpaths.GetSourceFilePathInNewDir(
sourceFile.FileName(),
sourceMapDir,
e.host.GetCurrentDirectory(),
e.host.CommonSourceDirectory(),
e.host.UseCaseSensitiveFileNames(),
))
}
if tspath.GetRootLength(sourceMapDir) == 0 {
// The relative paths are relative to the common directory
sourceMapDir = tspath.CombinePaths(e.host.CommonSourceDirectory(), sourceMapDir)
}
return sourceMapDir
}
return tspath.GetDirectoryPath(tspath.NormalizePath(filePath))
}
func (e *emitter) getSourceMappingURL(mapOptions *core.CompilerOptions, sourceMapGenerator *sourcemap.Generator, filePath string, sourceMapFilePath string, sourceFile *ast.SourceFile) string {
if mapOptions.InlineSourceMap.IsTrue() {
// Encode the sourceMap into the sourceMap url
return sourceMapGenerator.Base64DataURL()
}
sourceMapFile := tspath.GetBaseFileName(tspath.NormalizeSlashes(sourceMapFilePath))
if len(mapOptions.MapRoot) > 0 {
sourceMapDir := tspath.NormalizeSlashes(mapOptions.MapRoot)
if sourceFile != nil {
// For modules or multiple emit files the mapRoot will have directory structure like the sources
// So if src\a.ts and src\lib\b.ts are compiled together user would be moving the maps into mapRoot\a.js.map and mapRoot\lib\b.js.map
sourceMapDir = tspath.GetDirectoryPath(outputpaths.GetSourceFilePathInNewDir(
sourceFile.FileName(),
sourceMapDir,
e.host.GetCurrentDirectory(),
e.host.CommonSourceDirectory(),
e.host.UseCaseSensitiveFileNames(),
))
}
if tspath.GetRootLength(sourceMapDir) == 0 {
// The relative paths are relative to the common directory
sourceMapDir = tspath.CombinePaths(e.host.CommonSourceDirectory(), sourceMapDir)
return stringutil.EncodeURI(
tspath.GetRelativePathToDirectoryOrUrl(
tspath.GetDirectoryPath(tspath.NormalizePath(filePath)), // get the relative sourceMapDir path based on jsFilePath
tspath.CombinePaths(sourceMapDir, sourceMapFile), // this is where user expects to see sourceMap
/*isAbsolutePathAnUrl*/ true,
tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: e.host.UseCaseSensitiveFileNames(),
CurrentDirectory: e.host.GetCurrentDirectory(),
},
),
)
} else {
return stringutil.EncodeURI(tspath.CombinePaths(sourceMapDir, sourceMapFile))
}
}
return stringutil.EncodeURI(sourceMapFile)
}
type SourceFileMayBeEmittedHost interface {
Options() *core.CompilerOptions
GetProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference
IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool
GetCurrentDirectory() string
UseCaseSensitiveFileNames() bool
SourceFiles() []*ast.SourceFile
}
func sourceFileMayBeEmitted(sourceFile *ast.SourceFile, host SourceFileMayBeEmittedHost, forceDtsEmit bool) bool {
// TODO: move this to outputpaths?
options := host.Options()
// Js files are emitted only if option is enabled
if options.NoEmitForJsFiles.IsTrue() && ast.IsSourceFileJS(sourceFile) {
return false
}
// Declaration files are not emitted
if sourceFile.IsDeclarationFile {
return false
}
// Source file from node_modules are not emitted
if host.IsSourceFileFromExternalLibrary(sourceFile) {
return false
}
// forcing dts emit => file needs to be emitted
if forceDtsEmit {
return true
}
// Check other conditions for file emit
// Source files from referenced projects are not emitted
if host.GetProjectReferenceFromSource(sourceFile.Path()) != nil {
return false
}
// Any non json file should be emitted
if !ast.IsJsonSourceFile(sourceFile) {
return true
}
// Json file is not emitted if outDir is not specified
if options.OutDir == "" {
return false
}
// Otherwise, if rootDir is specified or a config file exists, we know the common source directory and can check if the file would be emitted in the same location
if options.RootDir != "" || options.ConfigFilePath != "" {
commonDir := tspath.GetNormalizedAbsolutePath(outputpaths.GetCommonSourceDirectory(options, func() []string { return nil }, host.GetCurrentDirectory(), host.UseCaseSensitiveFileNames(), nil), host.GetCurrentDirectory())
outputPath := outputpaths.GetSourceFilePathInNewDirWorker(sourceFile.FileName(), options.OutDir, host.GetCurrentDirectory(), commonDir, host.UseCaseSensitiveFileNames())
if tspath.ComparePaths(sourceFile.FileName(), outputPath, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
}) == 0 {
return false
}
}
return true
}
func getSourceFilesToEmit(host SourceFileMayBeEmittedHost, targetSourceFile *ast.SourceFile, forceDtsEmit bool) []*ast.SourceFile {
var sourceFiles []*ast.SourceFile
if targetSourceFile != nil {
sourceFiles = []*ast.SourceFile{targetSourceFile}
} else {
sourceFiles = host.SourceFiles()
}
return core.Filter(sourceFiles, func(sourceFile *ast.SourceFile) bool {
return sourceFileMayBeEmitted(sourceFile, host, forceDtsEmit)
})
}
func isSourceFileNotJson(file *ast.SourceFile) bool {
return !ast.IsJsonSourceFile(file)
}
func getDeclarationDiagnostics(host EmitHost, file *ast.SourceFile) []*ast.Diagnostic {
// TODO: use p.getSourceFilesToEmit cache
fullFiles := core.Filter(getSourceFilesToEmit(host, file, false), isSourceFileNotJson)
if !core.Some(fullFiles, func(f *ast.SourceFile) bool { return f == file }) {
return []*ast.Diagnostic{}
}
options := host.Options()
transform := declarations.NewDeclarationTransformer(host, nil, options, "", "")
transform.TransformSourceFile(file)
return transform.GetDiagnostics()
}

View File

@@ -0,0 +1,319 @@
package compiler
import (
"fmt"
"sync"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/scanner"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type fileIncludeKind int
const (
// References from file
fileIncludeKindImport = iota
fileIncludeKindReferenceFile
fileIncludeKindTypeReferenceDirective
fileIncludeKindLibReferenceDirective
fileIncludeKindRootFile
fileIncludeKindLibFile
fileIncludeKindAutomaticTypeDirectiveFile
)
type FileIncludeReason struct {
kind fileIncludeKind
data any
// Uses relative file name
relativeFileNameDiag *ast.Diagnostic
relativeFileNameDiagOnce sync.Once
// Uses file name as is
diag *ast.Diagnostic
diagOnce sync.Once
}
type referencedFileData struct {
file tspath.Path
index int
synthetic *ast.Node
}
type referenceFileLocation struct {
file *ast.SourceFile
node *ast.Node
ref *ast.FileReference
packageId module.PackageId
isSynthetic bool
}
func (r *referenceFileLocation) text() string {
if r.node != nil {
if !ast.NodeIsSynthesized(r.node) {
return r.file.Text()[scanner.SkipTrivia(r.file.Text(), r.node.Loc.Pos()):r.node.End()]
} else {
return fmt.Sprintf(`"%s"`, r.node.Text())
}
} else {
return r.file.Text()[r.ref.Pos():r.ref.End()]
}
}
func (r *referenceFileLocation) diagnosticAt(message *diagnostics.Message, args ...any) *ast.Diagnostic {
if r.node != nil {
return tsoptions.CreateDiagnosticForNodeInSourceFile(r.file, r.node, message, args...)
} else {
return ast.NewDiagnostic(r.file, r.ref.TextRange, message, args...)
}
}
type automaticTypeDirectiveFileData struct {
typeReference string
packageId module.PackageId
}
func (r *FileIncludeReason) asIndex() int {
return r.data.(int)
}
func (r *FileIncludeReason) asLibFileIndex() (int, bool) {
index, ok := r.data.(int)
return index, ok
}
func (r *FileIncludeReason) isReferencedFile() bool {
return r != nil && r.kind <= fileIncludeKindLibReferenceDirective
}
func (r *FileIncludeReason) asReferencedFileData() *referencedFileData {
return r.data.(*referencedFileData)
}
func (r *FileIncludeReason) asAutomaticTypeDirectiveFileData() *automaticTypeDirectiveFileData {
return r.data.(*automaticTypeDirectiveFileData)
}
func (r *FileIncludeReason) getReferencedLocation(program *Program) *referenceFileLocation {
ref := r.asReferencedFileData()
file := program.GetSourceFileByPath(ref.file)
switch r.kind {
case fileIncludeKindImport:
var specifier *ast.Node
var isSynthetic bool
if ref.synthetic != nil {
specifier = ref.synthetic
isSynthetic = true
} else if ref.index < len(file.Imports()) {
specifier = file.Imports()[ref.index]
} else {
augIndex := len(file.Imports())
for _, imp := range file.ModuleAugmentations {
if imp.Kind == ast.KindStringLiteral {
if augIndex == ref.index {
specifier = imp
break
}
augIndex++
}
}
}
resolution := program.GetResolvedModuleFromModuleSpecifier(file, specifier)
return &referenceFileLocation{
file: file,
node: specifier,
packageId: resolution.PackageId,
isSynthetic: isSynthetic,
}
case fileIncludeKindReferenceFile:
return &referenceFileLocation{
file: file,
ref: file.ReferencedFiles[ref.index],
}
case fileIncludeKindTypeReferenceDirective:
return &referenceFileLocation{
file: file,
ref: file.TypeReferenceDirectives[ref.index],
}
case fileIncludeKindLibReferenceDirective:
return &referenceFileLocation{
file: file,
ref: file.LibReferenceDirectives[ref.index],
}
default:
panic(fmt.Sprintf("unknown reason: %v", r.kind))
}
}
func (r *FileIncludeReason) toDiagnostic(program *Program, relativeFileName bool) *ast.Diagnostic {
if relativeFileName {
r.relativeFileNameDiagOnce.Do(func() {
r.relativeFileNameDiag = r.computeDiagnostic(program, func(fileName string) string {
return tspath.GetRelativePathFromDirectory(program.GetCurrentDirectory(), fileName, program.comparePathsOptions)
})
})
return r.relativeFileNameDiag
} else {
r.diagOnce.Do(func() {
r.diag = r.computeDiagnostic(program, func(fileName string) string { return fileName })
})
return r.diag
}
}
func (r *FileIncludeReason) computeDiagnostic(program *Program, toFileName func(string) string) *ast.Diagnostic {
if r.isReferencedFile() {
return r.computeReferenceFileDiagnostic(program, toFileName)
}
switch r.kind {
case fileIncludeKindRootFile:
if program.opts.Config.ConfigFile != nil {
config := program.opts.Config
fileName := tspath.GetNormalizedAbsolutePath(config.FileNames()[r.asIndex()], program.GetCurrentDirectory())
if matchedFileSpec := config.GetMatchedFileSpec(fileName); matchedFileSpec != "" {
return ast.NewCompilerDiagnostic(diagnostics.Part_of_files_list_in_tsconfig_json, matchedFileSpec, toFileName(fileName))
} else if matchedIncludeSpec, isDefaultIncludeSpec := config.GetMatchedIncludeSpec(fileName); matchedIncludeSpec != "" {
if isDefaultIncludeSpec {
return ast.NewCompilerDiagnostic(diagnostics.Matched_by_default_include_pattern_Asterisk_Asterisk_Slash_Asterisk)
} else {
return ast.NewCompilerDiagnostic(diagnostics.Matched_by_include_pattern_0_in_1, matchedIncludeSpec, toFileName(config.ConfigName()))
}
} else {
return ast.NewCompilerDiagnostic(diagnostics.Root_file_specified_for_compilation)
}
} else {
return ast.NewCompilerDiagnostic(diagnostics.Root_file_specified_for_compilation)
}
case fileIncludeKindAutomaticTypeDirectiveFile:
data := r.asAutomaticTypeDirectiveFileData()
if !program.Options().UsesWildcardTypes() {
if data.packageId.Name != "" {
return ast.NewCompilerDiagnostic(diagnostics.Entry_point_of_type_library_0_specified_in_compilerOptions_with_packageId_1, data.typeReference, data.packageId.String())
} else {
return ast.NewCompilerDiagnostic(diagnostics.Entry_point_of_type_library_0_specified_in_compilerOptions, data.typeReference)
}
} else {
if data.packageId.Name != "" {
return ast.NewCompilerDiagnostic(diagnostics.Entry_point_for_implicit_type_library_0_with_packageId_1, data.typeReference, data.packageId.String())
} else {
return ast.NewCompilerDiagnostic(diagnostics.Entry_point_for_implicit_type_library_0, data.typeReference)
}
}
case fileIncludeKindLibFile:
if index, ok := r.asLibFileIndex(); ok {
return ast.NewCompilerDiagnostic(diagnostics.Library_0_specified_in_compilerOptions, program.Options().Lib[index])
} else if target := program.Options().GetEmitScriptTarget().String(); target != "" {
return ast.NewCompilerDiagnostic(diagnostics.Default_library_for_target_0, target)
} else {
return ast.NewCompilerDiagnostic(diagnostics.Default_library)
}
default:
panic(fmt.Sprintf("unknown reason: %v", r.kind))
}
}
func (r *FileIncludeReason) computeReferenceFileDiagnostic(program *Program, toFileName func(string) string) *ast.Diagnostic {
referenceLocation := program.includeProcessor.getReferenceLocation(r, program)
referenceText := referenceLocation.text()
switch r.kind {
case fileIncludeKindImport:
if !referenceLocation.isSynthetic {
if referenceLocation.packageId.Name != "" {
return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_with_packageId_2, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String())
} else {
return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName()))
}
} else if specifier, ok := program.importHelpersImportSpecifiers[referenceLocation.file.Path()]; ok && specifier == referenceLocation.node {
if referenceLocation.packageId.Name != "" {
return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_with_packageId_2_to_import_importHelpers_as_specified_in_compilerOptions, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String())
} else {
return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_to_import_importHelpers_as_specified_in_compilerOptions, referenceText, toFileName(referenceLocation.file.FileName()))
}
} else {
if referenceLocation.packageId.Name != "" {
return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_with_packageId_2_to_import_jsx_and_jsxs_factory_functions, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String())
} else {
return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_to_import_jsx_and_jsxs_factory_functions, referenceText, toFileName(referenceLocation.file.FileName()))
}
}
case fileIncludeKindReferenceFile:
return ast.NewCompilerDiagnostic(diagnostics.Referenced_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName()))
case fileIncludeKindTypeReferenceDirective:
if referenceLocation.packageId.Name != "" {
return ast.NewCompilerDiagnostic(diagnostics.Type_library_referenced_via_0_from_file_1_with_packageId_2, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String())
} else {
return ast.NewCompilerDiagnostic(diagnostics.Type_library_referenced_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName()))
}
case fileIncludeKindLibReferenceDirective:
return ast.NewCompilerDiagnostic(diagnostics.Library_referenced_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName()))
default:
panic(fmt.Sprintf("unknown reason: %v", r.kind))
}
}
func (r *FileIncludeReason) toRelatedInfo(program *Program) *ast.Diagnostic {
if r.isReferencedFile() {
return r.computeReferenceFileRelatedInfo(program)
}
if program.opts.Config.ConfigFile == nil {
return nil
}
config := program.opts.Config
switch r.kind {
case fileIncludeKindRootFile:
fileName := tspath.GetNormalizedAbsolutePath(config.FileNames()[r.asIndex()], program.GetCurrentDirectory())
if matchedFileSpec := config.GetMatchedFileSpec(fileName); matchedFileSpec != "" {
if filesNode := tsoptions.GetTsConfigPropArrayElementValue(config.ConfigFile.SourceFile, "files", matchedFileSpec); filesNode != nil {
return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, filesNode.AsNode(), diagnostics.File_is_matched_by_files_list_specified_here)
}
} else if matchedIncludeSpec, isDefaultIncludeSpec := config.GetMatchedIncludeSpec(fileName); matchedIncludeSpec != "" && !isDefaultIncludeSpec {
if includeNode := tsoptions.GetTsConfigPropArrayElementValue(config.ConfigFile.SourceFile, "include", matchedIncludeSpec); includeNode != nil {
return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, includeNode.AsNode(), diagnostics.File_is_matched_by_include_pattern_specified_here)
}
}
case fileIncludeKindAutomaticTypeDirectiveFile:
if !program.Options().UsesWildcardTypes() {
data := r.asAutomaticTypeDirectiveFileData()
if typesSyntax := tsoptions.GetOptionsSyntaxByArrayElementValue(program.includeProcessor.getCompilerOptionsObjectLiteralSyntax(program), "types", data.typeReference); typesSyntax != nil {
return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, typesSyntax.AsNode(), diagnostics.File_is_entry_point_of_type_library_specified_here)
}
}
case fileIncludeKindLibFile:
if index, ok := r.asLibFileIndex(); ok {
if libSyntax := tsoptions.GetOptionsSyntaxByArrayElementValue(program.includeProcessor.getCompilerOptionsObjectLiteralSyntax(program), "lib", program.Options().Lib[index]); libSyntax != nil {
return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, libSyntax.AsNode(), diagnostics.File_is_library_specified_here)
}
} else if target := program.Options().GetEmitScriptTarget().String(); target != "" {
if targetValueSyntax := tsoptions.ForEachPropertyAssignment(program.includeProcessor.getCompilerOptionsObjectLiteralSyntax(program), "target", tsoptions.GetCallbackForFindingPropertyAssignmentByValue(target)); targetValueSyntax != nil {
return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, targetValueSyntax.AsNode(), diagnostics.File_is_default_library_for_target_specified_here)
}
}
default:
panic(fmt.Sprintf("unknown reason: %v", r.kind))
}
return nil
}
func (r *FileIncludeReason) computeReferenceFileRelatedInfo(program *Program) *ast.Diagnostic {
referenceLocation := program.includeProcessor.getReferenceLocation(r, program)
if referenceLocation.isSynthetic {
return nil
}
switch r.kind {
case fileIncludeKindImport:
return referenceLocation.diagnosticAt(diagnostics.File_is_included_via_import_here)
case fileIncludeKindReferenceFile:
return referenceLocation.diagnosticAt(diagnostics.File_is_included_via_reference_here)
case fileIncludeKindTypeReferenceDirective:
return referenceLocation.diagnosticAt(diagnostics.File_is_included_via_type_library_reference_here)
case fileIncludeKindLibReferenceDirective:
return referenceLocation.diagnosticAt(diagnostics.File_is_included_via_library_reference_here)
default:
panic(fmt.Sprintf("unknown reason: %v", r.kind))
}
}

View File

@@ -0,0 +1,791 @@
package compiler
import (
"cmp"
"slices"
"strings"
"sync"
"sync/atomic"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/tracing"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/zeebo/xxh3"
)
type libResolution struct {
libraryName string
resolution *module.ResolvedModule
trace []module.DiagAndArgs
}
type LibFile struct {
Name string
path string
Replaced bool
}
type sourceFileFromReferenceDiagnostic struct {
message *diagnostics.Message
args []any
}
type fileLoader struct {
opts ProgramOptions
resolver *module.Resolver
defaultLibraryPath string
comparePathsOptions tspath.ComparePathsOptions
supportedExtensions [][]string
supportedExtensionsWithJsonIfResolveJsonModule [][]string
filesParser *filesParser
rootTasks []*parseTask
totalFileCount atomic.Int32
libFileCount atomic.Int32
factoryMu sync.Mutex
factory ast.NodeFactory
projectReferenceFileMapper *projectReferenceFileMapper
dtsDirectories collections.Set[tspath.Path]
pathForLibFileCache collections.SyncMap[string, *LibFile]
pathForLibFileResolutions collections.SyncMap[tspath.Path, *libResolution]
}
type redirectsFile struct {
// Index of file at which this redirect file needs to be iterated
index int
fileName string
path tspath.Path
target tspath.Path
}
type DuplicateSourceFile struct {
ParseOptions ast.SourceFileParseOptions
Hash xxh3.Uint128
ScriptKind core.ScriptKind
}
var _ ast.HasFileName = (*redirectsFile)(nil)
func (r *redirectsFile) FileName() string {
return r.fileName
}
func (r *redirectsFile) Path() tspath.Path {
return r.path
}
type processedFiles struct {
resolver *module.Resolver
files []*ast.SourceFile
// duplicateSourceFiles tracks parsed files loaded during program construction
// that were later dropped from the final program, such as losing filename
// casing variants for the same path or files hidden behind package redirect
// deduplication. Their parse-cache acquires still need to be balanced when
// the program is disposed.
duplicateSourceFiles []*DuplicateSourceFile
filesByPath map[tspath.Path]*ast.SourceFile
projectReferenceFileMapper *projectReferenceFileMapper
missingFiles []string
resolvedModules map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule]
typeResolutionsInFile map[tspath.Path]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective]
sourceFileMetaDatas map[tspath.Path]ast.SourceFileMetaData
jsxRuntimeImportSpecifiers map[tspath.Path]*jsxRuntimeImportSpecifier
importHelpersImportSpecifiers map[tspath.Path]*ast.StringLiteralNode
libFiles map[tspath.Path]*LibFile
// List of present unsupported extensions
sourceFilesFoundSearchingNodeModules collections.Set[tspath.Path]
includeProcessor *includeProcessor
// if file was included using source file and its output is actually part of program
// this contains mapping from output to source file
outputFileToProjectReferenceSource map[tspath.Path]string
// Key is a file path. Value is the list of files that redirect to it (same package, different install location)
redirectTargetsMap map[tspath.Path][]string
// filesByPath for redirect files
redirectFilesByPath map[tspath.Path]*redirectsFile
finishedProcessing bool
}
type jsxRuntimeImportSpecifier struct {
moduleReference string
specifier *ast.StringLiteralNode
}
func processAllProgramFiles(
opts ProgramOptions,
singleThreaded bool,
) processedFiles {
compilerOptions := opts.Config.CompilerOptions()
rootFiles := opts.Config.FileNames()
supportedExtensions := tsoptions.GetSupportedExtensions(compilerOptions, nil /*extraFileExtensions*/)
supportedExtensionsWithJsonIfResolveJsonModule := tsoptions.GetSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, supportedExtensions)
var maxNodeModuleJsDepth int
if p := opts.Config.CompilerOptions().MaxNodeModuleJsDepth; p != nil {
maxNodeModuleJsDepth = *p
}
loader := fileLoader{
opts: opts,
defaultLibraryPath: tspath.GetNormalizedAbsolutePath(opts.Host.DefaultLibraryPath(), opts.Host.GetCurrentDirectory()),
comparePathsOptions: tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: opts.Host.FS().UseCaseSensitiveFileNames(),
CurrentDirectory: opts.Host.GetCurrentDirectory(),
},
filesParser: &filesParser{
wg: core.NewWorkGroup(singleThreaded),
maxDepth: maxNodeModuleJsDepth,
},
rootTasks: make([]*parseTask, 0, len(rootFiles)+len(compilerOptions.Lib)),
supportedExtensions: supportedExtensions,
supportedExtensionsWithJsonIfResolveJsonModule: supportedExtensionsWithJsonIfResolveJsonModule,
}
loader.addProjectReferenceTasks(singleThreaded)
loader.resolver = module.NewResolver(loader.projectReferenceFileMapper.host, compilerOptions, opts.TypingsLocation, opts.ProjectName)
if opts.Tracing != nil {
defer opts.Tracing.Push(tracing.PhaseProgram, "processRootFiles", map[string]any{"count": len(rootFiles)}, false)()
}
for index, rootFile := range rootFiles {
loader.addRootFileTask(rootFile, nil, &FileIncludeReason{kind: fileIncludeKindRootFile, data: index})
}
if len(rootFiles) > 0 && compilerOptions.NoLib.IsFalseOrUnknown() {
if compilerOptions.Lib == nil {
name := tsoptions.GetDefaultLibFileName(compilerOptions)
libFile := loader.pathForLibFile(name)
loader.addRootTask(libFile.path, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile})
} else {
for index, lib := range compilerOptions.Lib {
if name, ok := tsoptions.GetLibFileName(lib); ok {
libFile := loader.pathForLibFile(name)
loader.addRootTask(libFile.path, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile, data: index})
}
// !!! error on unknown name
}
}
}
if len(rootFiles) > 0 {
loader.addAutomaticTypeDirectiveTasks()
}
loader.filesParser.parse(&loader, loader.rootTasks)
// Clear out loader and host to ensure its not used post program creation
loader.projectReferenceFileMapper.loader = nil
loader.projectReferenceFileMapper.host = nil
return loader.filesParser.getProcessedFiles(&loader)
}
func (p *fileLoader) toPath(file string) tspath.Path {
return tspath.ToPath(file, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames())
}
func (p *fileLoader) addRootTask(fileName string, libFile *LibFile, includeReason *FileIncludeReason) {
absPath := tspath.GetNormalizedAbsolutePath(fileName, p.opts.Host.GetCurrentDirectory())
if p.opts.Config.CompilerOptions().AllowNonTsExtensions.IsTrue() || tspath.HasExtension(absPath) {
p.rootTasks = append(p.rootTasks, &parseTask{
normalizedFilePath: absPath,
libFile: libFile,
includeReason: includeReason,
})
}
}
func (p *fileLoader) addRootFileTask(fileName string, libFile *LibFile, includeReason *FileIncludeReason) {
currDir := p.opts.Host.GetCurrentDirectory()
absPath := tspath.GetNormalizedAbsolutePath(fileName, currDir)
containingFile := currDir
if p.opts.Config.ConfigFile != nil {
containingFile = tspath.GetNormalizedAbsolutePath(p.opts.Config.ConfigFile.SourceFile.FileName(), currDir)
}
resolvedFile, diagnostic := p.getSourceFileFromReference(absPath, fileName, containingFile, includeReason)
rootTask := &parseTask{
normalizedFilePath: resolvedFile,
libFile: libFile,
includeReason: includeReason,
}
if diagnostic != nil {
rootTask.normalizedFilePath = absPath
rootTask.processingDiagnostics = []*processingDiagnostic{{
kind: processingDiagnosticKindExplainingFileInclude,
data: &includeExplainingDiagnostic{
diagnosticReason: includeReason,
message: diagnostic.message,
args: diagnostic.args,
},
}}
}
p.rootTasks = append(p.rootTasks, rootTask)
}
func (p *fileLoader) addAutomaticTypeDirectiveTasks() {
var containingDirectory string
compilerOptions := p.opts.Config.CompilerOptions()
if compilerOptions.ConfigFilePath != "" {
containingDirectory = tspath.GetDirectoryPath(compilerOptions.ConfigFilePath)
} else {
containingDirectory = p.opts.Host.GetCurrentDirectory()
}
containingFileName := tspath.CombinePaths(containingDirectory, module.InferredTypesContainingFile)
p.rootTasks = append(p.rootTasks, &parseTask{
normalizedFilePath: containingFileName,
isForAutomaticTypeDirective: true,
})
}
func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName string) (
toParse []resolvedRef,
typeResolutionsInFile module.ModeAwareCache[*module.ResolvedTypeReferenceDirective],
typeResolutionsTrace []module.DiagAndArgs,
pDiagnostics []*processingDiagnostic,
) {
automaticTypeDirectiveNames := module.GetAutomaticTypeDirectiveNames(p.opts.Config.CompilerOptions(), p.opts.Host)
if len(automaticTypeDirectiveNames) != 0 {
toParse = make([]resolvedRef, 0, len(automaticTypeDirectiveNames))
typeResolutionsInFile = make(module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], len(automaticTypeDirectiveNames))
for _, name := range automaticTypeDirectiveNames {
// Under node16/nodenext module resolution, load `types`/ata include names as cjs resolution results by passing an `undefined` mode.
// Under bundler module resolution, this also triggers the "import" condition to be used.
resolutionMode := core.ResolutionModeNone
resolved, trace := p.resolver.ResolveTypeReferenceDirective(name, containingFileName, resolutionMode, nil)
var traceDone func()
if p.opts.Tracing != nil {
traceDone = p.opts.Tracing.Push(tracing.PhaseProgram, "processTypeReferenceDirective", map[string]any{"directive": name, "hasResolved": resolved.IsResolved(), "refKind": int(fileIncludeKindAutomaticTypeDirectiveFile)}, false)
}
typeResolutionsInFile[module.ModeAwareCacheKey{Name: name, Mode: resolutionMode}] = resolved
typeResolutionsTrace = append(typeResolutionsTrace, trace...)
if resolved.IsResolved() {
toParse = append(toParse, resolvedRef{
fileName: resolved.ResolvedFileName,
increaseDepth: resolved.IsExternalLibraryImport,
elideOnDepth: false,
includeReason: &FileIncludeReason{
kind: fileIncludeKindAutomaticTypeDirectiveFile,
data: &automaticTypeDirectiveFileData{name, resolved.PackageId},
},
packageId: resolved.PackageId,
})
} else {
pDiagnostics = append(pDiagnostics, &processingDiagnostic{
kind: processingDiagnosticKindExplainingFileInclude,
data: &includeExplainingDiagnostic{
diagnosticReason: &FileIncludeReason{
kind: fileIncludeKindAutomaticTypeDirectiveFile,
data: &automaticTypeDirectiveFileData{typeReference: name},
},
message: diagnostics.Cannot_find_type_definition_file_for_0,
args: []any{name},
},
})
}
if traceDone != nil {
traceDone()
}
}
}
return toParse, typeResolutionsInFile, typeResolutionsTrace, pDiagnostics
}
func (p *fileLoader) addProjectReferenceTasks(singleThreaded bool) {
p.projectReferenceFileMapper = &projectReferenceFileMapper{
opts: p.opts,
host: p.opts.Host,
}
projectReferences := p.opts.Config.ResolvedProjectReferencePaths()
if len(projectReferences) == 0 {
return
}
parser := &projectReferenceParser{
loader: p,
wg: core.NewWorkGroup(singleThreaded),
}
rootTasks := createProjectReferenceParseTasks(projectReferences)
parser.parse(rootTasks)
}
func (p *fileLoader) sortLibs(libFiles []*ast.SourceFile) {
slices.SortFunc(libFiles, func(f1 *ast.SourceFile, f2 *ast.SourceFile) int {
return cmp.Compare(p.getDefaultLibFilePriority(f1), p.getDefaultLibFilePriority(f2))
})
}
func (p *fileLoader) getDefaultLibFilePriority(a *ast.SourceFile) int {
// defaultLibraryPath and a.FileName() are absolute and normalized; a prefix check should suffice.
defaultLibraryPath := tspath.RemoveTrailingDirectorySeparator(p.defaultLibraryPath)
aFileName := a.FileName()
if strings.HasPrefix(aFileName, defaultLibraryPath) && len(aFileName) > len(defaultLibraryPath) && aFileName[len(defaultLibraryPath)] == tspath.DirectorySeparator {
// avoid tspath.GetBaseFileName; we know these paths are already absolute and normalized.
basename := aFileName[strings.LastIndexByte(aFileName, tspath.DirectorySeparator)+1:]
if basename == "lib.d.ts" || basename == "lib.es6.d.ts" {
return 0
}
name := strings.TrimSuffix(strings.TrimPrefix(basename, "lib."), ".d.ts")
index := slices.Index(tsoptions.Libs, name)
if index != -1 {
return index + 1
}
}
return len(tsoptions.Libs) + 2
}
func (p *fileLoader) loadSourceFileMetaData(fileName string) ast.SourceFileMetaData {
packageJsonScope := p.resolver.GetPackageScopeForPath(tspath.GetDirectoryPath(fileName))
moduleResolutionKind := p.opts.Config.CompilerOptions().GetModuleResolutionKind()
var packageJsonType, packageJsonDirectory string
if packageJsonScope.Exists() {
packageJsonDirectory = packageJsonScope.PackageDirectory
if value, ok := packageJsonScope.Contents.Type.GetValue(); ok {
if !tspath.FileExtensionIsOneOf(fileName, []string{tspath.ExtensionMts, tspath.ExtensionCts, tspath.ExtensionMjs, tspath.ExtensionCjs}) &&
core.ModuleResolutionKindNode16 <= moduleResolutionKind && moduleResolutionKind <= core.ModuleResolutionKindNodeNext || strings.Contains(fileName, "/node_modules/") {
packageJsonType = value
}
}
}
impliedNodeFormat := ast.GetImpliedNodeFormatForFile(fileName, packageJsonType)
return ast.SourceFileMetaData{
PackageJsonType: packageJsonType,
PackageJsonDirectory: packageJsonDirectory,
ImpliedNodeFormat: impliedNodeFormat,
}
}
func (p *fileLoader) parseSourceFile(t *parseTask) *ast.SourceFile {
if p.opts.Tracing != nil {
defer p.opts.Tracing.Push(tracing.PhaseParse, "createSourceFile", map[string]any{"path": t.normalizedFilePath}, true)()
}
path := p.toPath(t.normalizedFilePath)
options := p.projectReferenceFileMapper.getCompilerOptionsForFile(t)
sourceFile := p.opts.Host.GetSourceFile(ast.SourceFileParseOptions{
FileName: t.normalizedFilePath,
Path: path,
ExternalModuleIndicatorOptions: ast.GetExternalModuleIndicatorOptions(t.normalizedFilePath, options, t.metadata),
})
return sourceFile
}
func (p *fileLoader) isSupportedExtension(canonicalFileName string) bool {
for _, group := range p.supportedExtensionsWithJsonIfResolveJsonModule {
if tspath.FileExtensionIsOneOf(canonicalFileName, group) {
return true
}
}
return false
}
func (p *fileLoader) getSourceFileFromReference(
fileName string,
referenceText string,
containingFile string,
includeReason *FileIncludeReason,
) (string, *sourceFileFromReferenceDiagnostic) {
options := p.opts.Config.CompilerOptions()
allowNonTsExtensions := options.AllowNonTsExtensions.IsTrue()
diagnosticFileName := tspath.NormalizeSlashes(referenceText)
if tspath.HasExtension(fileName) {
canonicalFileName := tspath.GetCanonicalFileName(fileName, p.opts.Host.FS().UseCaseSensitiveFileNames())
if !allowNonTsExtensions && !p.isSupportedExtension(canonicalFileName) {
if tspath.HasJSFileExtension(canonicalFileName) {
return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, args: []any{diagnosticFileName}}
}
return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, args: []any{diagnosticFileName, "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}}
}
if !p.opts.Host.FS().FileExists(fileName) {
return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []any{diagnosticFileName}}
}
if includeReason.isReferencedFile() && tspath.GetCanonicalFileName(containingFile, p.opts.Host.FS().UseCaseSensitiveFileNames()) == canonicalFileName {
return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.A_file_cannot_have_a_reference_to_itself}
}
return fileName, nil
}
if allowNonTsExtensions && p.opts.Host.FS().FileExists(fileName) {
return fileName, nil
}
if allowNonTsExtensions {
return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []any{diagnosticFileName}}
}
for _, ext := range p.supportedExtensions[0] {
candidate := fileName + ext
if p.opts.Host.FS().FileExists(candidate) {
return candidate, nil
}
}
return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.Could_not_resolve_the_path_0_with_the_extensions_Colon_1, args: []any{diagnosticFileName, "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}}
}
func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containingFile string, index int) (*resolvedRef, *processingDiagnostic) {
basePath := tspath.GetDirectoryPath(containingFile)
referencedFileName := moduleName
if !tspath.IsRootedDiskPath(moduleName) {
referencedFileName = tspath.CombinePaths(basePath, moduleName)
}
normalizedFileName := tspath.NormalizePath(referencedFileName)
includeReason := &FileIncludeReason{
kind: fileIncludeKindReferenceFile,
data: &referencedFileData{
file: p.toPath(containingFile),
index: index,
},
}
resolvedFileName, diagnostic := p.getSourceFileFromReference(
normalizedFileName,
moduleName,
containingFile,
includeReason,
)
if diagnostic != nil {
return nil, &processingDiagnostic{
kind: processingDiagnosticKindExplainingFileInclude,
data: &includeExplainingDiagnostic{
diagnosticReason: includeReason,
message: diagnostic.message,
args: diagnostic.args,
},
}
}
return &resolvedRef{
fileName: resolvedFileName,
includeReason: includeReason,
}, nil
}
func (p *fileLoader) resolveTypeReferenceDirectives(t *parseTask) {
file := t.file
if len(file.TypeReferenceDirectives) == 0 {
return
}
if p.opts.Tracing != nil {
defer p.opts.Tracing.Push(tracing.PhaseProgram, "resolveTypeReferenceDirectiveNamesWorker", map[string]any{"containingFileName": file.FileName()}, false)()
}
meta := t.metadata
typeResolutionsInFile := make(module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], len(file.TypeReferenceDirectives))
var typeResolutionsTrace []module.DiagAndArgs
for index, ref := range file.TypeReferenceDirectives {
redirect, fileName := p.projectReferenceFileMapper.getRedirectForResolution(file)
resolutionMode := getModeForTypeReferenceDirectiveInFile(ref, file, meta, module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect))
resolved, trace := p.resolver.ResolveTypeReferenceDirective(ref.FileName, fileName, resolutionMode, redirect)
var traceDone func()
if p.opts.Tracing != nil {
traceDone = p.opts.Tracing.Push(tracing.PhaseProgram, "processTypeReferenceDirective", map[string]any{"directive": ref.FileName, "hasResolved": resolved.IsResolved(), "refKind": int(fileIncludeKindTypeReferenceDirective), "refPath": string(t.path)}, false)
}
typeResolutionsInFile[module.ModeAwareCacheKey{Name: ref.FileName, Mode: resolutionMode}] = resolved
includeReason := &FileIncludeReason{
kind: fileIncludeKindTypeReferenceDirective,
data: &referencedFileData{
file: t.path,
index: index,
},
}
typeResolutionsTrace = append(typeResolutionsTrace, trace...)
if resolved.IsResolved() {
t.addSubTask(resolvedRef{
fileName: resolved.ResolvedFileName,
increaseDepth: resolved.IsExternalLibraryImport,
elideOnDepth: false,
includeReason: includeReason,
packageId: resolved.PackageId,
}, nil)
} else {
t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{
kind: processingDiagnosticKindUnknownReference,
data: includeReason,
})
}
if traceDone != nil {
traceDone()
}
}
t.typeResolutionsInFile = typeResolutionsInFile
t.typeResolutionsTrace = typeResolutionsTrace
}
const externalHelpersModuleNameText = "tslib" // TODO(jakebailey): dedupe
func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) {
if p.opts.Tracing != nil {
defer p.opts.Tracing.Push(tracing.PhaseProgram, "resolveModuleNamesWorker", map[string]any{"containingFileName": t.file.FileName()}, false)()
}
file := t.file
meta := t.metadata
moduleNames := make([]*ast.Node, 0, len(file.Imports())+len(file.ModuleAugmentations)+2)
isJavaScriptFile := ast.IsSourceFileJS(file)
isExternalModuleFile := ast.IsExternalModule(file)
redirect, fileName := p.projectReferenceFileMapper.getRedirectForResolution(file)
optionsForFile := module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect)
if isJavaScriptFile || (!file.IsDeclarationFile && (optionsForFile.GetIsolatedModules() || isExternalModuleFile)) {
if optionsForFile.ImportHelpers.IsTrue() {
specifier := p.createSyntheticImport(externalHelpersModuleNameText, file)
moduleNames = append(moduleNames, specifier)
t.importHelpersImportSpecifier = specifier
}
}
if file.ScriptKind == core.ScriptKindJSX || file.ScriptKind == core.ScriptKindTSX {
jsxImport := ast.GetJSXRuntimeImport(ast.GetJSXImplicitImportBase(optionsForFile, file), optionsForFile)
if jsxImport != "" {
specifier := p.createSyntheticImport(jsxImport, file)
moduleNames = append(moduleNames, specifier)
t.jsxRuntimeImportSpecifier = &jsxRuntimeImportSpecifier{
moduleReference: jsxImport,
specifier: specifier,
}
}
}
importsStart := len(moduleNames)
moduleNames = append(moduleNames, file.Imports()...)
for _, imp := range file.ModuleAugmentations {
if imp.Kind == ast.KindStringLiteral {
moduleNames = append(moduleNames, imp)
}
// Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`.
}
if len(moduleNames) != 0 {
resolutionsInFile := make(module.ModeAwareCache[*module.ResolvedModule], len(moduleNames))
var resolutionsTrace []module.DiagAndArgs
for index, entry := range moduleNames {
moduleName := entry.Text()
if moduleName == "" {
continue
}
mode := getModeForUsageLocation(file.FileName(), meta, entry, optionsForFile)
resolvedModule, trace := p.resolver.ResolveModuleName(moduleName, fileName, mode, redirect)
resolutionsInFile[module.ModeAwareCacheKey{Name: moduleName, Mode: mode}] = resolvedModule
resolutionsTrace = append(resolutionsTrace, trace...)
if !resolvedModule.IsResolved() {
continue
}
resolvedFileName := resolvedModule.ResolvedFileName
isFromNodeModulesSearch := resolvedModule.IsExternalLibraryImport
// Don't treat redirected files as JS files.
isJsFile := !tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && p.projectReferenceFileMapper.getRedirectParsedCommandLineForResolution(ast.NewHasFileName(resolvedFileName, p.toPath(resolvedFileName))) == nil
isJsFileFromNodeModules := isFromNodeModulesSearch && isJsFile && strings.Contains(resolvedFileName, "/node_modules/")
// add file to program only if:
// - resolution was successful
// - noResolve is falsy
// - module name comes from the list of imports
// - it's not a top level JavaScript module that exceeded the search max
importIndex := index - importsStart
shouldAddFile := moduleName != "" &&
module.GetResolutionDiagnostic(optionsForFile, resolvedModule, file) == nil &&
!optionsForFile.NoResolve.IsTrue() &&
!(isJsFile && !optionsForFile.GetAllowJS()) &&
(importIndex < 0 || (importIndex < len(file.Imports()) && (ast.IsInJSFile(file.Imports()[importIndex]) || file.Imports()[importIndex].Flags&ast.NodeFlagsJSDoc == 0)))
if shouldAddFile {
t.addSubTask(resolvedRef{
fileName: resolvedFileName,
increaseDepth: resolvedModule.IsExternalLibraryImport,
elideOnDepth: isJsFileFromNodeModules,
includeReason: &FileIncludeReason{
kind: fileIncludeKindImport,
data: &referencedFileData{
file: t.path,
index: importIndex,
synthetic: core.IfElse(importIndex < 0, entry, nil),
},
},
packageId: resolvedModule.PackageId,
}, nil)
}
}
t.resolutionsInFile = resolutionsInFile
t.resolutionsTrace = resolutionsTrace
}
}
func (p *fileLoader) createSyntheticImport(text string, file *ast.SourceFile) *ast.StringLiteralNode {
p.factoryMu.Lock()
defer p.factoryMu.Unlock()
externalHelpersModuleReference := p.factory.NewStringLiteral(text, ast.TokenFlagsNone)
importDecl := p.factory.NewImportDeclaration(nil, nil, externalHelpersModuleReference, nil)
externalHelpersModuleReference.Parent = importDecl
importDecl.Parent = file.AsNode()
return externalHelpersModuleReference
}
func (p *fileLoader) pathForLibFile(name string) *LibFile {
if cached, ok := p.pathForLibFileCache.Load(name); ok {
return cached
}
path := tspath.CombinePaths(p.defaultLibraryPath, name)
replaced := false
if p.opts.Config.CompilerOptions().LibReplacement.IsTrue() && name != "lib.d.ts" {
libraryName := getLibraryNameFromLibFileName(name)
resolveFrom := getInferredLibraryNameResolveFrom(p.opts.Config.CompilerOptions(), p.opts.Host.GetCurrentDirectory(), name)
resolution, trace := p.resolveLibrary(libraryName, resolveFrom)
if resolution.IsResolved() {
path = resolution.ResolvedFileName
replaced = true
}
p.pathForLibFileResolutions.LoadOrStore(p.toPath(resolveFrom), &libResolution{
libraryName: libraryName,
resolution: resolution,
trace: trace,
})
}
libPath, _ := p.pathForLibFileCache.LoadOrStore(name, &LibFile{name, path, replaced})
return libPath
}
func (p *fileLoader) resolveLibrary(libraryName, resolveFrom string) (*module.ResolvedModule, []module.DiagAndArgs) {
if tr := p.opts.Tracing; tr != nil {
defer tr.Push(tracing.PhaseProgram, "resolveLibrary", map[string]any{"resolveFrom": resolveFrom}, false)()
}
return p.resolver.ResolveModuleName(libraryName, resolveFrom, core.ModuleKindCommonJS, nil)
}
func getLibraryNameFromLibFileName(libFileName string) string {
// Support resolving to lib.dom.d.ts -> @typescript/lib-dom, and
// lib.dom.iterable.d.ts -> @typescript/lib-dom/iterable
// lib.es2015.symbol.wellknown.d.ts -> @typescript/lib-es2015/symbol-wellknown
components := strings.Split(libFileName, ".")
var path strings.Builder
path.WriteString("@typescript/lib-")
if len(components) > 1 {
path.WriteString(components[1])
}
i := 2
for i < len(components) && components[i] != "" && components[i] != "d" {
if i == 2 {
path.WriteByte('/')
} else {
path.WriteByte('-')
}
path.WriteString(components[i])
i++
}
return path.String()
}
func getInferredLibraryNameResolveFrom(options *core.CompilerOptions, currentDirectory string, libFileName string) string {
var containingDirectory string
if options.ConfigFilePath != "" {
containingDirectory = tspath.GetDirectoryPath(options.ConfigFilePath)
} else {
containingDirectory = currentDirectory
}
return tspath.CombinePaths(containingDirectory, "__lib_node_modules_lookup_"+libFileName+"__.ts")
}
func getModeForTypeReferenceDirectiveInFile(ref *ast.FileReference, file *ast.SourceFile, meta ast.SourceFileMetaData, options *core.CompilerOptions) core.ResolutionMode {
if ref.ResolutionMode != core.ResolutionModeNone {
return ref.ResolutionMode
} else {
return getDefaultResolutionModeForFile(file.FileName(), meta, options)
}
}
func getDefaultResolutionModeForFile(fileName string, meta ast.SourceFileMetaData, options *core.CompilerOptions) core.ResolutionMode {
if importSyntaxAffectsModuleResolution(options) {
return ast.GetImpliedNodeFormatForEmitWorker(fileName, options.GetEmitModuleKind(), meta)
} else {
return core.ResolutionModeNone
}
}
func getModeForUsageLocation(fileName string, meta ast.SourceFileMetaData, usage *ast.StringLiteralLike, options *core.CompilerOptions) core.ResolutionMode {
if ast.IsImportDeclaration(usage.Parent) || usage.Parent.Kind == ast.KindJSImportDeclaration || ast.IsExportDeclaration(usage.Parent) || ast.IsJSDocImportTag(usage.Parent) {
isTypeOnly := ast.IsExclusivelyTypeOnlyImportOrExport(usage.Parent)
if isTypeOnly {
var override core.ResolutionMode
var ok bool
switch usage.Parent.Kind {
case ast.KindImportDeclaration, ast.KindJSImportDeclaration:
override, ok = usage.Parent.AsImportDeclaration().Attributes.GetResolutionModeOverride()
case ast.KindExportDeclaration:
override, ok = usage.Parent.AsExportDeclaration().Attributes.GetResolutionModeOverride()
case ast.KindJSDocImportTag:
override, ok = usage.Parent.AsJSDocImportTag().Attributes.GetResolutionModeOverride()
}
if ok {
return override
}
}
}
if ast.IsLiteralTypeNode(usage.Parent) && ast.IsImportTypeNode(usage.Parent.Parent) {
if override, ok := usage.Parent.Parent.AsImportTypeNode().Attributes.GetResolutionModeOverride(); ok {
return override
}
}
if options != nil && importSyntaxAffectsModuleResolution(options) {
return getEmitSyntaxForUsageLocationWorker(fileName, meta, usage, options)
}
return core.ResolutionModeNone
}
func importSyntaxAffectsModuleResolution(options *core.CompilerOptions) bool {
moduleResolution := options.GetModuleResolutionKind()
return core.ModuleResolutionKindNode16 <= moduleResolution && moduleResolution <= core.ModuleResolutionKindNodeNext ||
options.GetResolvePackageJsonExports() || options.GetResolvePackageJsonImports()
}
func getEmitSyntaxForUsageLocationWorker(fileName string, meta ast.SourceFileMetaData, usage *ast.Node, options *core.CompilerOptions) core.ResolutionMode {
if ast.IsRequireCall(usage.Parent, false /*requireStringLiteralLikeArgument*/) || ast.IsExternalModuleReference(usage.Parent) && ast.IsImportEqualsDeclaration(usage.Parent.Parent) {
return core.ModuleKindCommonJS
}
fileEmitMode := ast.GetEmitModuleFormatOfFileWorker(fileName, options, meta)
if ast.IsImportCall(ast.WalkUpParenthesizedExpressions(usage.Parent)) {
if ast.ShouldTransformImportCall(fileName, options, fileEmitMode) {
return core.ModuleKindCommonJS
} else {
return core.ModuleKindESNext
}
}
// If we're in --module preserve on an input file, we know that an import
// is an import. But if this is a declaration file, we'd prefer to use the
// impliedNodeFormat. Since we want things to be consistent between the two,
// we need to issue errors when the user writes ESM syntax in a definitely-CJS
// file, until/unless declaration emit can indicate a true ESM import. On the
// other hand, writing CJS syntax in a definitely-ESM file is fine, since declaration
// emit preserves the CJS syntax.
if fileEmitMode == core.ModuleKindCommonJS {
return core.ModuleKindCommonJS
} else {
if fileEmitMode.IsNonNodeESM() || fileEmitMode == core.ModuleKindPreserve {
return core.ModuleKindESNext
}
}
return core.ModuleKindNone
}

View File

@@ -0,0 +1,567 @@
package compiler
import (
"math"
"slices"
"strings"
"sync"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/tracing"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type parseTask struct {
normalizedFilePath string
path tspath.Path
file *ast.SourceFile
libFile *LibFile
redirectedParseTask *parseTask
subTasks []*parseTask
loaded bool
startedSubTasks bool
isForAutomaticTypeDirective bool
includeReason *FileIncludeReason
packageId module.PackageId
metadata ast.SourceFileMetaData
resolutionsInFile module.ModeAwareCache[*module.ResolvedModule]
resolutionsTrace []module.DiagAndArgs
typeResolutionsInFile module.ModeAwareCache[*module.ResolvedTypeReferenceDirective]
typeResolutionsTrace []module.DiagAndArgs
resolutionDiagnostics []*ast.Diagnostic
processingDiagnostics []*processingDiagnostic
importHelpersImportSpecifier *ast.StringLiteralNode
jsxRuntimeImportSpecifier *jsxRuntimeImportSpecifier
increaseDepth bool
elideOnDepth bool
loadedTask *parseTask
allIncludeReasons []*FileIncludeReason
}
func (t *parseTask) FileName() string {
return t.normalizedFilePath
}
func (t *parseTask) Path() tspath.Path {
return t.path
}
func (t *parseTask) load(loader *fileLoader) {
t.loaded = true
if t.isForAutomaticTypeDirective {
t.loadAutomaticTypeDirectives(loader)
return
}
if loader.opts.Tracing != nil {
defer loader.opts.Tracing.Push(tracing.PhaseProgram, "findSourceFile", map[string]any{"fileName": t.normalizedFilePath}, false)()
}
redirect := loader.projectReferenceFileMapper.getParseFileRedirect(t)
if redirect != "" {
t.redirect(loader, redirect)
return
}
if tspath.HasExtension(t.normalizedFilePath) {
compilerOptions := loader.opts.Config.CompilerOptions()
allowNonTsExtensions := compilerOptions.AllowNonTsExtensions.IsTrue()
if !allowNonTsExtensions {
canonicalFileName := tspath.GetCanonicalFileName(t.normalizedFilePath, loader.opts.Host.FS().UseCaseSensitiveFileNames())
if !loader.isSupportedExtension(canonicalFileName) {
if tspath.HasJSFileExtension(canonicalFileName) {
t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{
kind: processingDiagnosticKindExplainingFileInclude,
data: &includeExplainingDiagnostic{
diagnosticReason: t.includeReason,
message: diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option,
args: []any{t.normalizedFilePath},
},
})
} else {
t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{
kind: processingDiagnosticKindExplainingFileInclude,
data: &includeExplainingDiagnostic{
diagnosticReason: t.includeReason,
message: diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1,
args: []any{t.normalizedFilePath, "'" + strings.Join(core.Flatten(loader.supportedExtensions), "', '") + "'"},
},
})
}
return
}
}
}
loader.totalFileCount.Add(1)
if t.libFile != nil {
loader.libFileCount.Add(1)
// Default lib files are all scripts; we can safely skip looking up their package.json
// to avoid adding spurious lookups to file watcher tracking.
t.metadata = ast.SourceFileMetaData{ImpliedNodeFormat: core.ResolutionModeCommonJS}
} else {
t.metadata = loader.loadSourceFileMetaData(t.normalizedFilePath)
}
file := loader.parseSourceFile(t)
if file == nil {
return
}
t.file = file
t.subTasks = make([]*parseTask, 0, len(file.ReferencedFiles)+len(file.Imports())+len(file.ModuleAugmentations))
compilerOptions := loader.opts.Config.CompilerOptions()
if !compilerOptions.NoResolve.IsTrue() {
for index, ref := range file.ReferencedFiles {
resolvedRef, processingDiagnostic := loader.resolveTripleslashPathReference(ref.FileName, file.FileName(), index)
if processingDiagnostic != nil {
t.processingDiagnostics = append(t.processingDiagnostics, processingDiagnostic)
continue
}
t.addSubTask(*resolvedRef, nil)
}
loader.resolveTypeReferenceDirectives(t)
}
if compilerOptions.NoLib != core.TSTrue {
for index, lib := range file.LibReferenceDirectives {
includeReason := &FileIncludeReason{
kind: fileIncludeKindLibReferenceDirective,
data: &referencedFileData{
file: t.path,
index: index,
},
}
if name, ok := tsoptions.GetLibFileName(lib.FileName); ok {
libFile := loader.pathForLibFile(name)
t.addSubTask(resolvedRef{
fileName: libFile.path,
includeReason: includeReason,
}, libFile)
} else {
t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{
kind: processingDiagnosticKindUnknownReference,
data: includeReason,
})
}
}
}
loader.resolveImportsAndModuleAugmentations(t)
}
func (t *parseTask) redirect(loader *fileLoader, fileName string) {
t.redirectedParseTask = &parseTask{
normalizedFilePath: tspath.NormalizePath(fileName),
libFile: t.libFile,
includeReason: t.includeReason,
}
// increaseDepth and elideOnDepth are not copied to redirects, otherwise their depth would be double counted.
t.subTasks = []*parseTask{t.redirectedParseTask}
}
func (t *parseTask) loadAutomaticTypeDirectives(loader *fileLoader) {
if loader.opts.Tracing != nil {
defer loader.opts.Tracing.Push(tracing.PhaseProgram, "processTypeReferences", nil, false)()
}
toParseTypeRefs, typeResolutionsInFile, typeResolutionsTrace, pDiagnostics := loader.resolveAutomaticTypeDirectives(t.normalizedFilePath)
t.typeResolutionsInFile = typeResolutionsInFile
t.typeResolutionsTrace = typeResolutionsTrace
t.processingDiagnostics = append(t.processingDiagnostics, pDiagnostics...)
for _, typeResolution := range toParseTypeRefs {
t.addSubTask(typeResolution, nil)
}
}
type resolvedRef struct {
fileName string
increaseDepth bool
elideOnDepth bool
includeReason *FileIncludeReason
packageId module.PackageId
}
func (t *parseTask) addSubTask(ref resolvedRef, libFile *LibFile) {
normalizedFilePath := tspath.NormalizePath(ref.fileName)
subTask := &parseTask{
normalizedFilePath: normalizedFilePath,
libFile: libFile,
increaseDepth: ref.increaseDepth,
elideOnDepth: ref.elideOnDepth,
includeReason: ref.includeReason,
packageId: ref.packageId,
}
t.subTasks = append(t.subTasks, subTask)
}
type filesParser struct {
wg core.WorkGroup
taskDataByPath collections.SyncMap[tspath.Path, *parseTaskData]
maxDepth int
}
var parseTaskDataPool = sync.Pool{
New: func() any {
return &parseTaskData{
tasks: make(map[string]*parseTask, 1),
}
},
}
func getParseTaskData(task *parseTask) *parseTaskData {
td := parseTaskDataPool.Get().(*parseTaskData)
td.tasks[task.normalizedFilePath] = task
td.lowestDepth = math.MaxInt
return td
}
func putParseTaskData(td *parseTaskData) {
clear(td.tasks)
parseTaskDataPool.Put(td)
}
type parseTaskData struct {
// map of tasks by file casing
tasks map[string]*parseTask
mu sync.Mutex
lowestDepth int
startedSubTasks bool
packageId module.PackageId
}
func (w *filesParser) parse(loader *fileLoader, tasks []*parseTask) {
w.start(loader, tasks, 0)
w.wg.RunAndWait()
}
func (w *filesParser) start(loader *fileLoader, tasks []*parseTask, depth int) {
for i, task := range tasks {
task.path = loader.toPath(task.normalizedFilePath)
candidate := getParseTaskData(task)
data, loaded := w.taskDataByPath.LoadOrStore(task.path, candidate)
if loaded {
putParseTaskData(candidate)
}
w.wg.Queue(func() {
data.mu.Lock()
defer data.mu.Unlock()
startSubtasks := false
if loaded {
if existingTask, ok := data.tasks[task.normalizedFilePath]; ok {
tasks[i].loadedTask = existingTask
} else {
data.tasks[task.normalizedFilePath] = task
// This is new task for file name - so load subtasks if there was loading for any other casing
startSubtasks = data.startedSubTasks
}
}
// Propagate packageId to data if we have one and data doesn't yet
if data.packageId.Name == "" && task.packageId.Name != "" {
data.packageId = task.packageId
}
currentDepth := core.IfElse(task.increaseDepth, depth+1, depth)
if currentDepth < data.lowestDepth {
// If we're seeing this task at a lower depth than before,
// reprocess its subtasks to ensure they are loaded.
data.lowestDepth = currentDepth
startSubtasks = true
data.startedSubTasks = true
}
if task.elideOnDepth && currentDepth > w.maxDepth {
return
}
for _, taskByFileName := range data.tasks {
loadSubTasks := startSubtasks
if !taskByFileName.loaded {
taskByFileName.load(loader)
if taskByFileName.redirectedParseTask != nil {
// Always load redirected task
loadSubTasks = true
data.startedSubTasks = true
}
}
if !taskByFileName.startedSubTasks && loadSubTasks {
taskByFileName.startedSubTasks = true
w.start(loader, taskByFileName.subTasks, data.lowestDepth)
}
}
})
}
}
func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles {
totalFileCount := int(loader.totalFileCount.Load())
libFileCount := int(loader.libFileCount.Load())
var missingFiles []string
var duplicateSourceFiles []*DuplicateSourceFile
files := make([]*ast.SourceFile, 0, totalFileCount-libFileCount)
libFiles := make([]*ast.SourceFile, 0, totalFileCount) // totalFileCount here since we append files to it later to construct the final list
filesByPath := make(map[tspath.Path]*ast.SourceFile, totalFileCount)
// stores 'filename -> file association' ignoring case
// used to track cases when two file names differ only in casing
var tasksSeenByNameIgnoreCase map[string]*parseTask
if loader.comparePathsOptions.UseCaseSensitiveFileNames {
tasksSeenByNameIgnoreCase = make(map[string]*parseTask, totalFileCount)
}
includeProcessor := &includeProcessor{
fileIncludeReasons: make(map[tspath.Path][]*FileIncludeReason, totalFileCount),
}
var outputFileToProjectReferenceSource map[tspath.Path]string
if !loader.opts.canUseProjectReferenceSource() {
outputFileToProjectReferenceSource = make(map[tspath.Path]string, totalFileCount)
}
resolvedModules := make(map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule], totalFileCount+1)
typeResolutionsInFile := make(map[tspath.Path]module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], totalFileCount)
sourceFileMetaDatas := make(map[tspath.Path]ast.SourceFileMetaData, totalFileCount)
var jsxRuntimeImportSpecifiers map[tspath.Path]*jsxRuntimeImportSpecifier
var importHelpersImportSpecifiers map[tspath.Path]*ast.StringLiteralNode
var sourceFilesFoundSearchingNodeModules collections.Set[tspath.Path]
libFilesMap := make(map[tspath.Path]*LibFile, libFileCount)
var redirectTargetsMap map[tspath.Path][]string
var redirectFilesByPath map[tspath.Path]*redirectsFile
var packageIdToSourceFile map[module.PackageId]*ast.SourceFile
if !loader.opts.Config.CompilerOptions().DeduplicatePackages.IsFalse() {
redirectTargetsMap = make(map[tspath.Path][]string)
packageIdToSourceFile = make(map[module.PackageId]*ast.SourceFile)
}
var collectFiles func(tasks []*parseTask, seen map[*parseTaskData]string)
// recordedDuplicates tracks, per task data, the set of file-name casings that
// have already been recorded in duplicateSourceFiles. A file that is reached
// from multiple import sites is walked once per site, but each distinct casing
// is only parsed and acquired in the parse cache once. Recording the same casing
// as a duplicate more than once would cause it to be released more times than it
// was acquired when the snapshot is disposed, leaving a dangling cache entry that
// panics the next time it is referenced.
var recordedDuplicates map[*parseTaskData]*collections.Set[string]
collectFiles = func(tasks []*parseTask, seen map[*parseTaskData]string) {
for _, task := range tasks {
includeReason := task.includeReason
// Exclude automatic type directive tasks from include reason processing,
// as these are internal implementation details and should not contribute
// to the reasons for including files.
if task.redirectedParseTask == nil && !task.isForAutomaticTypeDirective {
if task.loadedTask != nil {
task = task.loadedTask
}
w.addIncludeReason(includeProcessor, task, includeReason)
}
data, _ := w.taskDataByPath.Load(task.path)
if !task.loaded {
continue
}
// ensure we only walk each task once
if checkedName, ok := seen[data]; ok {
if task.file != nil && checkedName != task.normalizedFilePath {
if recordedDuplicates == nil {
recordedDuplicates = make(map[*parseTaskData]*collections.Set[string])
}
dups := recordedDuplicates[data]
if dups == nil {
dups = &collections.Set[string]{}
recordedDuplicates[data] = dups
}
if dups.AddIfAbsent(task.normalizedFilePath) {
duplicateSourceFiles = append(duplicateSourceFiles, &DuplicateSourceFile{
ParseOptions: task.file.ParseOptions(),
Hash: task.file.Hash,
ScriptKind: task.file.ScriptKind,
})
}
}
if !loader.opts.Config.CompilerOptions().ForceConsistentCasingInFileNames.IsFalse() {
// Check if it differs only in drive letters its ok to ignore that error:
checkedAbsolutePath := tspath.GetNormalizedAbsolutePathWithoutRoot(checkedName, loader.comparePathsOptions.CurrentDirectory)
inputAbsolutePath := tspath.GetNormalizedAbsolutePathWithoutRoot(task.normalizedFilePath, loader.comparePathsOptions.CurrentDirectory)
if checkedAbsolutePath != inputAbsolutePath {
includeProcessor.addProcessingDiagnosticsForFileCasing(task.path, checkedName, task.normalizedFilePath, includeReason)
}
}
continue
} else {
seen[data] = task.normalizedFilePath
}
if tasksSeenByNameIgnoreCase != nil {
pathLowerCase := tspath.ToFileNameLowerCase(string(task.path))
if taskByIgnoreCase, ok := tasksSeenByNameIgnoreCase[pathLowerCase]; ok {
includeProcessor.addProcessingDiagnosticsForFileCasing(taskByIgnoreCase.path, taskByIgnoreCase.normalizedFilePath, task.normalizedFilePath, includeReason)
} else {
tasksSeenByNameIgnoreCase[pathLowerCase] = task
}
}
for _, trace := range task.typeResolutionsTrace {
loader.opts.Host.Trace(trace.Message, trace.Args...)
}
for _, trace := range task.resolutionsTrace {
loader.opts.Host.Trace(trace.Message, trace.Args...)
}
file := task.file
if packageIdToSourceFile != nil && data.packageId.Name != "" {
if packageIdFile, exists := packageIdToSourceFile[data.packageId]; exists {
if file != nil {
// Package deduplication keeps the first package instance in the
// program, but we still parsed this file and acquired it through
// the host, so snapshot disposal must release that extra owner.
duplicateSourceFiles = append(duplicateSourceFiles, &DuplicateSourceFile{
ParseOptions: file.ParseOptions(),
Hash: file.Hash,
ScriptKind: file.ScriptKind,
})
}
redirectTargetsMap[packageIdFile.Path()] = append(redirectTargetsMap[packageIdFile.Path()], task.normalizedFilePath)
if redirectFilesByPath == nil {
redirectFilesByPath = make(map[tspath.Path]*redirectsFile, totalFileCount)
}
redirectFilesByPath[task.path] = &redirectsFile{
index: len(files) + len(redirectFilesByPath),
fileName: task.normalizedFilePath,
path: task.path,
target: packageIdFile.Path(),
}
filesByPath[task.path] = packageIdFile
if data.lowestDepth > 0 {
sourceFilesFoundSearchingNodeModules.Add(task.path)
}
continue
} else if file != nil {
packageIdToSourceFile[data.packageId] = file
}
}
if subTasks := task.subTasks; len(subTasks) > 0 {
collectFiles(subTasks, seen)
}
// Exclude automatic type directive tasks from include reason processing,
// as these are internal implementation details and should not contribute
// to the reasons for including files.
if task.redirectedParseTask != nil {
if !loader.opts.canUseProjectReferenceSource() {
outputFileToProjectReferenceSource[task.redirectedParseTask.path] = task.FileName()
}
continue
}
if task.isForAutomaticTypeDirective {
typeResolutionsInFile[task.path] = task.typeResolutionsInFile
if len(task.processingDiagnostics) > 0 {
includeProcessor.processingDiagnostics = append(includeProcessor.processingDiagnostics, task.processingDiagnostics...)
}
continue
}
path := task.path
if len(task.processingDiagnostics) > 0 {
includeProcessor.processingDiagnostics = append(includeProcessor.processingDiagnostics, task.processingDiagnostics...)
}
if file == nil {
missingFiles = append(missingFiles, task.normalizedFilePath)
continue
}
if task.libFile != nil {
libFiles = append(libFiles, file)
libFilesMap[path] = task.libFile
} else {
files = append(files, file)
}
filesByPath[path] = file
resolvedModules[path] = task.resolutionsInFile
typeResolutionsInFile[path] = task.typeResolutionsInFile
sourceFileMetaDatas[path] = task.metadata
if task.jsxRuntimeImportSpecifier != nil {
if jsxRuntimeImportSpecifiers == nil {
jsxRuntimeImportSpecifiers = make(map[tspath.Path]*jsxRuntimeImportSpecifier, totalFileCount)
}
jsxRuntimeImportSpecifiers[path] = task.jsxRuntimeImportSpecifier
}
if task.importHelpersImportSpecifier != nil {
if importHelpersImportSpecifiers == nil {
importHelpersImportSpecifiers = make(map[tspath.Path]*ast.StringLiteralNode, totalFileCount)
}
importHelpersImportSpecifiers[path] = task.importHelpersImportSpecifier
}
if data.lowestDepth > 0 {
sourceFilesFoundSearchingNodeModules.Add(path)
}
}
}
collectFiles(loader.rootTasks, make(map[*parseTaskData]string, totalFileCount))
loader.sortLibs(libFiles)
allFiles := append(libFiles, files...)
for _, redirectFile := range redirectFilesByPath {
redirectFile.index += len(libFiles)
}
keys := slices.Collect(loader.pathForLibFileResolutions.Keys())
slices.Sort(keys)
for _, key := range keys {
value, _ := loader.pathForLibFileResolutions.Load(key)
resolvedModules[key] = module.ModeAwareCache[*module.ResolvedModule]{
module.ModeAwareCacheKey{Name: value.libraryName, Mode: core.ModuleKindCommonJS}: value.resolution,
}
for _, trace := range value.trace {
loader.opts.Host.Trace(trace.Message, trace.Args...)
}
}
return processedFiles{
finishedProcessing: true,
resolver: loader.resolver,
files: allFiles,
duplicateSourceFiles: duplicateSourceFiles,
filesByPath: filesByPath,
projectReferenceFileMapper: loader.projectReferenceFileMapper,
resolvedModules: resolvedModules,
typeResolutionsInFile: typeResolutionsInFile,
sourceFileMetaDatas: sourceFileMetaDatas,
jsxRuntimeImportSpecifiers: jsxRuntimeImportSpecifiers,
importHelpersImportSpecifiers: importHelpersImportSpecifiers,
sourceFilesFoundSearchingNodeModules: sourceFilesFoundSearchingNodeModules,
libFiles: libFilesMap,
missingFiles: missingFiles,
includeProcessor: includeProcessor,
outputFileToProjectReferenceSource: outputFileToProjectReferenceSource,
redirectTargetsMap: redirectTargetsMap,
redirectFilesByPath: redirectFilesByPath,
}
}
func (w *filesParser) addIncludeReason(includeProcessor *includeProcessor, task *parseTask, reason *FileIncludeReason) {
if task.redirectedParseTask != nil {
w.addIncludeReason(includeProcessor, task.redirectedParseTask, reason)
} else if task.loaded {
if existing, ok := includeProcessor.fileIncludeReasons[task.path]; ok {
includeProcessor.fileIncludeReasons[task.path] = append(existing, reason)
} else {
includeProcessor.fileIncludeReasons[task.path] = []*FileIncludeReason{reason}
}
}
}

View File

@@ -0,0 +1,89 @@
package compiler
import (
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/parser"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
)
type CompilerHost interface {
FS() vfs.FS
DefaultLibraryPath() string
GetCurrentDirectory() string
Trace(msg *diagnostics.Message, args ...any)
GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile
GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine
}
var _ CompilerHost = (*compilerHost)(nil)
type compilerHost struct {
currentDirectory string
fs vfs.FS
defaultLibraryPath string
extendedConfigCache tsoptions.ExtendedConfigCache
trace func(msg *diagnostics.Message, args ...any)
}
func NewCachedFSCompilerHost(
currentDirectory string,
fs vfs.FS,
defaultLibraryPath string,
extendedConfigCache tsoptions.ExtendedConfigCache,
trace func(msg *diagnostics.Message, args ...any),
) CompilerHost {
return NewCompilerHost(currentDirectory, cachedvfs.From(fs), defaultLibraryPath, extendedConfigCache, trace)
}
func NewCompilerHost(
currentDirectory string,
fs vfs.FS,
defaultLibraryPath string,
extendedConfigCache tsoptions.ExtendedConfigCache,
trace func(msg *diagnostics.Message, args ...any),
) CompilerHost {
if trace == nil {
trace = func(msg *diagnostics.Message, args ...any) {}
}
return &compilerHost{
currentDirectory: currentDirectory,
fs: fs,
defaultLibraryPath: defaultLibraryPath,
extendedConfigCache: extendedConfigCache,
trace: trace,
}
}
func (h *compilerHost) FS() vfs.FS {
return h.fs
}
func (h *compilerHost) DefaultLibraryPath() string {
return h.defaultLibraryPath
}
func (h *compilerHost) GetCurrentDirectory() string {
return h.currentDirectory
}
func (h *compilerHost) Trace(msg *diagnostics.Message, args ...any) {
h.trace(msg, args...)
}
func (h *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile {
text, ok := h.FS().ReadFile(opts.FileName)
if !ok {
return nil
}
return parser.ParseSourceFile(opts, text, core.GetScriptKindFromFileName(opts.FileName))
}
func (h *compilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine {
commandLine, _ := tsoptions.GetParsedCommandLineOfConfigFilePath(fileName, path, nil, nil /*optionsRaw*/, h, h.extendedConfigCache)
return commandLine
}

View File

@@ -0,0 +1,184 @@
package compiler
import (
"slices"
"sync"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type includeProcessor struct {
fileIncludeReasons map[tspath.Path][]*FileIncludeReason
processingDiagnostics []*processingDiagnostic
reasonToReferenceLocation collections.SyncMap[*FileIncludeReason, *referenceFileLocation]
includeReasonToRelatedInfo collections.SyncMap[*FileIncludeReason, *ast.Diagnostic]
redirectAndFileFormat collections.SyncMap[tspath.Path, []*ast.Diagnostic]
computedDiagnostics *ast.DiagnosticsCollection
computedDiagnosticsOnce sync.Once
compilerOptionsSyntax *ast.ObjectLiteralExpression
compilerOptionsSyntaxOnce sync.Once
}
func updateFileIncludeProcessor(p *Program) {
p.includeProcessor = &includeProcessor{
fileIncludeReasons: p.includeProcessor.fileIncludeReasons,
processingDiagnostics: p.includeProcessor.processingDiagnostics,
}
}
func (i *includeProcessor) getDiagnostics(p *Program) *ast.DiagnosticsCollection {
i.computedDiagnosticsOnce.Do(func() {
i.computedDiagnostics = &ast.DiagnosticsCollection{}
for _, d := range i.processingDiagnostics {
i.computedDiagnostics.Add(d.toDiagnostic(p))
}
for _, resolutions := range p.resolvedModules {
for _, resolvedModule := range resolutions {
for _, diag := range resolvedModule.ResolutionDiagnostics {
i.computedDiagnostics.Add(diag)
}
}
}
for _, typeResolutions := range p.typeResolutionsInFile {
for _, resolvedTypeRef := range typeResolutions {
for _, diag := range resolvedTypeRef.ResolutionDiagnostics {
i.computedDiagnostics.Add(diag)
}
}
}
})
return i.computedDiagnostics
}
func (i *includeProcessor) addProcessingDiagnostic(d ...*processingDiagnostic) {
i.processingDiagnostics = append(i.processingDiagnostics, d...)
}
func (i *includeProcessor) addProcessingDiagnosticsForFileCasing(file tspath.Path, existingCasing string, currentCasing string, reason *FileIncludeReason) {
if !reason.isReferencedFile() && slices.ContainsFunc(i.fileIncludeReasons[file], func(r *FileIncludeReason) bool {
return r.isReferencedFile()
}) {
i.addProcessingDiagnostic(&processingDiagnostic{
kind: processingDiagnosticKindExplainingFileInclude,
data: &includeExplainingDiagnostic{
file: file,
diagnosticReason: reason,
message: diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing,
args: []any{existingCasing, currentCasing},
},
})
} else {
i.addProcessingDiagnostic(&processingDiagnostic{
kind: processingDiagnosticKindExplainingFileInclude,
data: &includeExplainingDiagnostic{
file: file,
diagnosticReason: reason,
message: diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing,
args: []any{currentCasing, existingCasing},
},
})
}
}
func (i *includeProcessor) getReferenceLocation(r *FileIncludeReason, program *Program) *referenceFileLocation {
if existing, ok := i.reasonToReferenceLocation.Load(r); ok {
return existing
}
loc, _ := i.reasonToReferenceLocation.LoadOrStore(r, r.getReferencedLocation(program))
return loc
}
func (i *includeProcessor) getCompilerOptionsObjectLiteralSyntax(program *Program) *ast.ObjectLiteralExpression {
i.compilerOptionsSyntaxOnce.Do(func() {
configFile := program.opts.Config.ConfigFile
if configFile != nil {
if compilerOptionsProperty := tsoptions.ForEachTsConfigPropArray(configFile.SourceFile, "compilerOptions", core.Identity); compilerOptionsProperty != nil &&
compilerOptionsProperty.Initializer != nil &&
ast.IsObjectLiteralExpression(compilerOptionsProperty.Initializer) {
i.compilerOptionsSyntax = compilerOptionsProperty.Initializer.AsObjectLiteralExpression()
}
} else {
i.compilerOptionsSyntax = nil
}
})
return i.compilerOptionsSyntax
}
func (i *includeProcessor) getRelatedInfo(r *FileIncludeReason, program *Program) *ast.Diagnostic {
if existing, ok := i.includeReasonToRelatedInfo.Load(r); ok {
return existing
}
relatedInfo, _ := i.includeReasonToRelatedInfo.LoadOrStore(r, r.toRelatedInfo(program))
return relatedInfo
}
func (i *includeProcessor) explainRedirectAndImpliedFormat(
program *Program,
filePath tspath.Path,
toFileName func(fileName string) string,
) []*ast.Diagnostic {
if existing, ok := i.redirectAndFileFormat.Load(filePath); ok {
return existing
}
var file ast.HasFileName
var sourceFile *ast.SourceFile
redirectsFile := program.redirectFilesByPath[filePath]
if redirectsFile != nil {
file = redirectsFile
} else {
sourceFile = program.GetSourceFileByPath(filePath)
if sourceFile == nil {
return nil
}
file = sourceFile
}
var result []*ast.Diagnostic
if source := program.GetSourceOfProjectReferenceIfOutputIncluded(file); source != file.FileName() {
result = append(result, ast.NewCompilerDiagnostic(
diagnostics.File_is_output_of_project_reference_source_0,
toFileName(source),
))
}
if redirectsFile != nil {
targetFile := program.GetSourceFileByPath(redirectsFile.target)
result = append(result, ast.NewCompilerDiagnostic(
diagnostics.File_redirects_to_file_0,
toFileName(targetFile.FileName()),
))
}
if sourceFile != nil && ast.IsExternalOrCommonJSModule(sourceFile) {
metaData := program.GetSourceFileMetaData(file.Path())
switch program.GetImpliedNodeFormatForEmit(file) {
case core.ModuleKindESNext:
if metaData.PackageJsonType == "module" {
result = append(result, ast.NewCompilerDiagnostic(
diagnostics.File_is_ECMAScript_module_because_0_has_field_type_with_value_module,
toFileName(metaData.PackageJsonDirectory+"/package.json"),
))
}
case core.ModuleKindCommonJS:
if metaData.PackageJsonType != "" {
result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_0_has_field_type_whose_value_is_not_module, toFileName(metaData.PackageJsonDirectory+"/package.json")))
} else if metaData.PackageJsonDirectory != "" {
if metaData.PackageJsonType == "" {
result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_0_does_not_have_field_type, toFileName(metaData.PackageJsonDirectory+"/package.json")))
}
} else {
result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_package_json_was_not_found))
}
}
}
result, _ = i.redirectAndFileFormat.LoadOrStore(filePath, result)
return result
}

View File

@@ -0,0 +1,2 @@
// Package compiler implements the TypeScript compiler.
package compiler

View File

@@ -0,0 +1,135 @@
package compiler
import (
"slices"
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type processingDiagnosticKind int
const (
processingDiagnosticKindUnknownReference processingDiagnosticKind = iota
processingDiagnosticKindExplainingFileInclude
)
type processingDiagnostic struct {
kind processingDiagnosticKind
data any
}
func (d *processingDiagnostic) asFileIncludeReason() *FileIncludeReason {
return d.data.(*FileIncludeReason)
}
type includeExplainingDiagnostic struct {
file tspath.Path
diagnosticReason *FileIncludeReason
message *diagnostics.Message
args []any
}
func (d *processingDiagnostic) asIncludeExplainingDiagnostic() *includeExplainingDiagnostic {
return d.data.(*includeExplainingDiagnostic)
}
func (d *processingDiagnostic) toDiagnostic(program *Program) *ast.Diagnostic {
switch d.kind {
case processingDiagnosticKindUnknownReference:
ref := d.asFileIncludeReason()
loc := ref.getReferencedLocation(program)
switch ref.kind {
case fileIncludeKindTypeReferenceDirective:
return loc.diagnosticAt(diagnostics.Cannot_find_type_definition_file_for_0, loc.ref.FileName)
case fileIncludeKindLibReferenceDirective:
libName := tspath.ToFileNameLowerCase(loc.ref.FileName)
unqualifiedLibName := strings.TrimSuffix(strings.TrimPrefix(libName, "lib."), ".d.ts")
suggestion := core.GetSpellingSuggestionForStrings(unqualifiedLibName, slices.Values(tsoptions.Libs))
return loc.diagnosticAt(core.IfElse(
suggestion != "",
diagnostics.Cannot_find_lib_definition_for_0_Did_you_mean_1,
diagnostics.Cannot_find_lib_definition_for_0,
), libName, suggestion)
default:
panic("unknown include kind")
}
case processingDiagnosticKindExplainingFileInclude:
return d.createDiagnosticExplainingFile(program)
default:
panic("unknown processingDiagnosticKind")
}
}
func (d *processingDiagnostic) createDiagnosticExplainingFile(program *Program) *ast.Diagnostic {
diag := d.asIncludeExplainingDiagnostic()
var includeDetails []*ast.Diagnostic
var relatedInfo []*ast.Diagnostic
var redirectInfo []*ast.Diagnostic
var preferredLocation *FileIncludeReason
var seenReasons collections.Set[*FileIncludeReason]
if diag.diagnosticReason.isReferencedFile() && !program.includeProcessor.getReferenceLocation(diag.diagnosticReason, program).isSynthetic {
preferredLocation = diag.diagnosticReason
}
processRelatedInfo := func(includeReason *FileIncludeReason) {
if preferredLocation == nil && includeReason.isReferencedFile() && !program.includeProcessor.getReferenceLocation(includeReason, program).isSynthetic {
preferredLocation = includeReason
} else if preferredLocation != includeReason {
info := program.includeProcessor.getRelatedInfo(includeReason, program)
if info != nil {
relatedInfo = append(relatedInfo, info)
}
}
}
processInclude := func(includeReason *FileIncludeReason) {
if !seenReasons.AddIfAbsent(includeReason) {
return
}
includeDetails = append(includeDetails, includeReason.toDiagnostic(program, false))
processRelatedInfo(includeReason)
}
// !!! todo sheetal caching
if diag.file != "" {
reasons := program.includeProcessor.fileIncludeReasons[diag.file]
includeDetails = make([]*ast.Diagnostic, 0, len(reasons))
for _, reason := range reasons {
processInclude(reason)
}
redirectInfo = program.includeProcessor.explainRedirectAndImpliedFormat(program, diag.file, func(fileName string) string { return fileName })
}
if diag.diagnosticReason != nil {
processInclude(diag.diagnosticReason)
}
var chain []*ast.Diagnostic
if includeDetails != nil && (preferredLocation == nil || seenReasons.Len() != 1) {
fileReason := ast.NewCompilerDiagnostic(diagnostics.The_file_is_in_the_program_because_Colon)
fileReason.SetMessageChain(includeDetails)
chain = []*ast.Diagnostic{fileReason}
}
if redirectInfo != nil {
chain = append(chain, redirectInfo...)
}
var result *ast.Diagnostic
if preferredLocation != nil {
result = program.includeProcessor.getReferenceLocation(preferredLocation, program).diagnosticAt(diag.message, diag.args...)
}
if result == nil {
result = ast.NewCompilerDiagnostic(diag.message, diag.args...)
}
if chain != nil {
result.SetMessageChain(chain)
}
if relatedInfo != nil {
result.SetRelatedInfo(relatedInfo)
}
return result
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,367 @@
package compiler_test
import (
"fmt"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/repo"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
"gotest.tools/v3/assert"
)
type testFile struct {
fileName string
contents string
}
type programTest struct {
testName string
files []testFile
expectedFiles []string
target core.ScriptTarget
}
var esnextLibs = []string{
"lib.es5.d.ts",
"lib.es2015.d.ts",
"lib.es2016.d.ts",
"lib.es2017.d.ts",
"lib.es2018.d.ts",
"lib.es2019.d.ts",
"lib.es2020.d.ts",
"lib.es2021.d.ts",
"lib.es2022.d.ts",
"lib.es2023.d.ts",
"lib.es2024.d.ts",
"lib.es2025.d.ts",
"lib.esnext.d.ts",
"lib.dom.d.ts",
"lib.dom.iterable.d.ts",
"lib.dom.asynciterable.d.ts",
"lib.webworker.importscripts.d.ts",
"lib.scripthost.d.ts",
"lib.es2015.core.d.ts",
"lib.es2015.collection.d.ts",
"lib.es2015.generator.d.ts",
"lib.es2015.iterable.d.ts",
"lib.es2015.promise.d.ts",
"lib.es2015.proxy.d.ts",
"lib.es2015.reflect.d.ts",
"lib.es2015.symbol.d.ts",
"lib.es2015.symbol.wellknown.d.ts",
"lib.es2016.array.include.d.ts",
"lib.es2016.intl.d.ts",
"lib.es2017.arraybuffer.d.ts",
"lib.es2017.date.d.ts",
"lib.es2017.object.d.ts",
"lib.es2017.sharedmemory.d.ts",
"lib.es2017.string.d.ts",
"lib.es2017.intl.d.ts",
"lib.es2017.typedarrays.d.ts",
"lib.es2018.asyncgenerator.d.ts",
"lib.es2018.asynciterable.d.ts",
"lib.es2018.intl.d.ts",
"lib.es2018.promise.d.ts",
"lib.es2018.regexp.d.ts",
"lib.es2019.array.d.ts",
"lib.es2019.object.d.ts",
"lib.es2019.string.d.ts",
"lib.es2019.symbol.d.ts",
"lib.es2019.intl.d.ts",
"lib.es2020.bigint.d.ts",
"lib.es2020.date.d.ts",
"lib.es2020.promise.d.ts",
"lib.es2020.sharedmemory.d.ts",
"lib.es2020.string.d.ts",
"lib.es2020.symbol.wellknown.d.ts",
"lib.es2020.intl.d.ts",
"lib.es2020.number.d.ts",
"lib.es2021.promise.d.ts",
"lib.es2021.string.d.ts",
"lib.es2021.weakref.d.ts",
"lib.es2021.intl.d.ts",
"lib.es2022.array.d.ts",
"lib.es2022.error.d.ts",
"lib.es2022.intl.d.ts",
"lib.es2022.object.d.ts",
"lib.es2022.string.d.ts",
"lib.es2022.regexp.d.ts",
"lib.es2023.array.d.ts",
"lib.es2023.collection.d.ts",
"lib.es2023.intl.d.ts",
"lib.es2024.arraybuffer.d.ts",
"lib.es2024.collection.d.ts",
"lib.es2024.object.d.ts",
"lib.es2024.promise.d.ts",
"lib.es2024.regexp.d.ts",
"lib.es2024.sharedmemory.d.ts",
"lib.es2024.string.d.ts",
"lib.es2025.collection.d.ts",
"lib.es2025.float16.d.ts",
"lib.es2025.intl.d.ts",
"lib.es2025.iterator.d.ts",
"lib.es2025.promise.d.ts",
"lib.es2025.regexp.d.ts",
"lib.esnext.array.d.ts",
"lib.esnext.collection.d.ts",
"lib.esnext.date.d.ts",
"lib.esnext.decorators.d.ts",
"lib.esnext.disposable.d.ts",
"lib.esnext.error.d.ts",
"lib.esnext.intl.d.ts",
"lib.esnext.sharedmemory.d.ts",
"lib.esnext.temporal.d.ts",
"lib.esnext.typedarrays.d.ts",
"lib.decorators.d.ts",
"lib.decorators.legacy.d.ts",
"lib.esnext.full.d.ts",
}
var programTestCases = []programTest{
{
testName: "BasicFileOrdering",
files: []testFile{
{fileName: "c:/dev/src/index.ts", contents: "/// <reference path='c:/dev/src2/a/5.ts' />\n/// <reference path='c:/dev/src2/a/10.ts' />"},
{fileName: "c:/dev/src2/a/5.ts", contents: "/// <reference path='4.ts' />"},
{fileName: "c:/dev/src2/a/4.ts", contents: "/// <reference path='b/3.ts' />"},
{fileName: "c:/dev/src2/a/b/3.ts", contents: "/// <reference path='2.ts' />"},
{fileName: "c:/dev/src2/a/b/2.ts", contents: "/// <reference path='c/1.ts' />"},
{fileName: "c:/dev/src2/a/b/c/1.ts", contents: "console.log('hello');"},
{fileName: "c:/dev/src2/a/10.ts", contents: "/// <reference path='b/c/d/9.ts' />"},
{fileName: "c:/dev/src2/a/b/c/d/9.ts", contents: "/// <reference path='e/8.ts' />"},
{fileName: "c:/dev/src2/a/b/c/d/e/8.ts", contents: "/// <reference path='7.ts' />"},
{fileName: "c:/dev/src2/a/b/c/d/e/7.ts", contents: "/// <reference path='f/6.ts' />"},
{fileName: "c:/dev/src2/a/b/c/d/e/f/6.ts", contents: "console.log('world!');"},
},
expectedFiles: slices.Concat(esnextLibs,
[]string{
"c:/dev/src2/a/b/c/1.ts",
"c:/dev/src2/a/b/2.ts",
"c:/dev/src2/a/b/3.ts",
"c:/dev/src2/a/4.ts",
"c:/dev/src2/a/5.ts",
"c:/dev/src2/a/b/c/d/e/f/6.ts",
"c:/dev/src2/a/b/c/d/e/7.ts",
"c:/dev/src2/a/b/c/d/e/8.ts",
"c:/dev/src2/a/b/c/d/9.ts",
"c:/dev/src2/a/10.ts",
"c:/dev/src/index.ts",
}),
target: core.ScriptTargetESNext,
},
{
testName: "FileOrderingImports",
files: []testFile{
{fileName: "c:/dev/src/index.ts", contents: "import * as five from '../src2/a/5.ts';\nimport * as ten from '../src2/a/10.ts';"},
{fileName: "c:/dev/src2/a/5.ts", contents: "import * as four from './4.ts';"},
{fileName: "c:/dev/src2/a/4.ts", contents: "import * as three from './b/3.ts';"},
{fileName: "c:/dev/src2/a/b/3.ts", contents: "import * as two from './2.ts';"},
{fileName: "c:/dev/src2/a/b/2.ts", contents: "import * as one from './c/1.ts';"},
{fileName: "c:/dev/src2/a/b/c/1.ts", contents: "console.log('hello');"},
{fileName: "c:/dev/src2/a/10.ts", contents: "import * as nine from './b/c/d/9.ts';"},
{fileName: "c:/dev/src2/a/b/c/d/9.ts", contents: "import * as eight from './e/8.ts';"},
{fileName: "c:/dev/src2/a/b/c/d/e/8.ts", contents: "import * as seven from './7.ts';"},
{fileName: "c:/dev/src2/a/b/c/d/e/7.ts", contents: "import * as six from './f/6.ts';"},
{fileName: "c:/dev/src2/a/b/c/d/e/f/6.ts", contents: "console.log('world!');"},
},
expectedFiles: slices.Concat(esnextLibs,
[]string{
"c:/dev/src2/a/b/c/1.ts",
"c:/dev/src2/a/b/2.ts",
"c:/dev/src2/a/b/3.ts",
"c:/dev/src2/a/4.ts",
"c:/dev/src2/a/5.ts",
"c:/dev/src2/a/b/c/d/e/f/6.ts",
"c:/dev/src2/a/b/c/d/e/7.ts",
"c:/dev/src2/a/b/c/d/e/8.ts",
"c:/dev/src2/a/b/c/d/9.ts",
"c:/dev/src2/a/10.ts",
"c:/dev/src/index.ts",
}),
target: core.ScriptTargetESNext,
},
{
testName: "FileOrderingCycles",
files: []testFile{
{fileName: "c:/dev/src/index.ts", contents: "import * as five from '../src2/a/5.ts';\nimport * as ten from '../src2/a/10.ts';"},
{fileName: "c:/dev/src2/a/5.ts", contents: "import * as four from './4.ts';"},
{fileName: "c:/dev/src2/a/4.ts", contents: "import * as three from './b/3.ts';"},
{fileName: "c:/dev/src2/a/b/3.ts", contents: "import * as two from './2.ts';\nimport * as cycle from 'c:/dev/src/index.ts'; "},
{fileName: "c:/dev/src2/a/b/2.ts", contents: "import * as one from './c/1.ts';"},
{fileName: "c:/dev/src2/a/b/c/1.ts", contents: "console.log('hello');"},
{fileName: "c:/dev/src2/a/10.ts", contents: "import * as nine from './b/c/d/9.ts';"},
{fileName: "c:/dev/src2/a/b/c/d/9.ts", contents: "import * as eight from './e/8.ts';\nimport * as cycle from 'c:/dev/src/index.ts';"},
{fileName: "c:/dev/src2/a/b/c/d/e/8.ts", contents: "import * as seven from './7.ts';"},
{fileName: "c:/dev/src2/a/b/c/d/e/7.ts", contents: "import * as six from './f/6.ts';"},
{fileName: "c:/dev/src2/a/b/c/d/e/f/6.ts", contents: "console.log('world!');"},
},
expectedFiles: slices.Concat(esnextLibs,
[]string{
"c:/dev/src2/a/b/c/1.ts",
"c:/dev/src2/a/b/2.ts",
"c:/dev/src2/a/b/3.ts",
"c:/dev/src2/a/4.ts",
"c:/dev/src2/a/5.ts",
"c:/dev/src2/a/b/c/d/e/f/6.ts",
"c:/dev/src2/a/b/c/d/e/7.ts",
"c:/dev/src2/a/b/c/d/e/8.ts",
"c:/dev/src2/a/b/c/d/9.ts",
"c:/dev/src2/a/10.ts",
"c:/dev/src/index.ts",
}),
target: core.ScriptTargetESNext,
},
}
func TestProgram(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
// Without embedding, we'd need to read all of the lib files out from disk into the MapFS.
// Just skip this for now.
t.Skip("bundled files are not embedded")
}
for _, testCase := range programTestCases {
t.Run(testCase.testName, func(t *testing.T) {
t.Parallel()
libPrefix := bundled.LibPath() + "/"
fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/)
fs = bundled.WrapFS(fs)
for _, testFile := range testCase.files {
_ = fs.WriteFile(testFile.fileName, testFile.contents)
}
opts := core.CompilerOptions{Target: testCase.target}
program := compiler.NewProgram(compiler.ProgramOptions{
Config: &tsoptions.ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
FileNames: []string{"c:/dev/src/index.ts"},
CompilerOptions: &opts,
},
},
Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil),
})
actualFiles := []string{}
for _, file := range program.GetSourceFiles() {
actualFiles = append(actualFiles, strings.TrimPrefix(file.FileName(), libPrefix))
}
assert.DeepEqual(t, testCase.expectedFiles, actualFiles)
})
}
}
func TestIncludeProcessorDiagnosticsWithMissingFileCasing(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}
// Use case-sensitive file names so that /src/MyFile.ts and /src/myFile.ts
// have different canonical paths but the same lower-case path, triggering
// file casing diagnostics in the include processor.
fs := vfstest.FromMap[any](nil, true /*useCaseSensitiveFileNames*/)
fs = bundled.WrapFS(fs)
// Only create the lowercase version; /src/MyFile.ts does not exist.
_ = fs.WriteFile("/src/myFile.ts", `export const y = 2;`)
opts := core.CompilerOptions{SkipDefaultLibCheck: core.TSTrue}
// List both casings as root files. The first one (/src/MyFile.ts) will fail
// to load because it does not exist on the case-sensitive filesystem.
program := compiler.NewProgram(compiler.ProgramOptions{
Config: &tsoptions.ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
FileNames: []string{"/src/MyFile.ts", "/src/myFile.ts"},
CompilerOptions: &opts,
},
},
Host: compiler.NewCompilerHost("/", fs, bundled.LibPath(), nil, nil),
})
// GetProgramDiagnostics triggers getDiagnostics which processes all
// include processor diagnostics including the casing diagnostic whose
// file path points to the missing /src/MyFile.ts. Before the fix this
// panicked with a nil pointer dereference.
assert.NilError(t, func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
program.GetProgramDiagnostics()
return nil
}())
}
func BenchmarkNewProgram(b *testing.B) {
if !bundled.Embedded {
// Without embedding, we'd need to read all of the lib files out from disk into the MapFS.
// Just skip this for now.
b.Skip("bundled files are not embedded")
}
for _, testCase := range programTestCases {
b.Run(testCase.testName, func(b *testing.B) {
fs := vfstest.FromMap[any](nil, false /*useCaseSensitiveFileNames*/)
fs = bundled.WrapFS(fs)
for _, testFile := range testCase.files {
_ = fs.WriteFile(testFile.fileName, testFile.contents)
}
opts := core.CompilerOptions{Target: testCase.target}
programOpts := compiler.ProgramOptions{
Config: &tsoptions.ParsedCommandLine{
ParsedConfig: &core.ParsedOptions{
FileNames: []string{"c:/dev/src/index.ts"},
CompilerOptions: &opts,
},
},
Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil),
}
for b.Loop() {
compiler.NewProgram(programOpts)
}
})
}
b.Run("compiler", func(b *testing.B) {
repo.SkipIfNoTypeScriptSubmodule(b)
rootPath := tspath.NormalizeSlashes(filepath.Join(repo.TypeScriptSubmodulePath(), "src", "compiler"))
fs := osvfs.FS()
fs = bundled.WrapFS(fs)
host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil)
parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), nil, nil, host, nil)
assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line")
opts := compiler.ProgramOptions{
Config: parsed,
Host: host,
}
for b.Loop() {
compiler.NewProgram(opts)
}
})
}

View File

@@ -0,0 +1,237 @@
package compiler
import (
"strings"
"time"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/symlinks"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
"github.com/microsoft/typescript-go/internal/vfs/cachedvfs"
)
type projectReferenceDtsFakingHost struct {
host CompilerHost
fs *cachedvfs.FS
}
var _ module.ResolutionHost = (*projectReferenceDtsFakingHost)(nil)
func newProjectReferenceDtsFakingHost(loader *fileLoader) module.ResolutionHost {
// Create a new host that will fake the dts files
host := &projectReferenceDtsFakingHost{
host: loader.opts.Host,
fs: cachedvfs.From(&projectReferenceDtsFakingVfs{
projectReferenceFileMapper: loader.projectReferenceFileMapper,
dtsDirectories: loader.dtsDirectories,
knownSymlinks: symlinks.KnownSymlinks{},
}),
}
return host
}
// FS implements module.ResolutionHost.
func (h *projectReferenceDtsFakingHost) FS() vfs.FS {
return h.fs
}
// GetCurrentDirectory implements module.ResolutionHost.
func (h *projectReferenceDtsFakingHost) GetCurrentDirectory() string {
return h.host.GetCurrentDirectory()
}
type projectReferenceDtsFakingVfs struct {
projectReferenceFileMapper *projectReferenceFileMapper
dtsDirectories collections.Set[tspath.Path]
knownSymlinks symlinks.KnownSymlinks
}
var _ vfs.FS = (*projectReferenceDtsFakingVfs)(nil)
// UseCaseSensitiveFileNames implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) UseCaseSensitiveFileNames() bool {
return fs.projectReferenceFileMapper.opts.Host.FS().UseCaseSensitiveFileNames()
}
// FileExists implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) FileExists(path string) bool {
if fs.projectReferenceFileMapper.opts.Host.FS().FileExists(path) {
return true
}
if !tspath.IsDeclarationFileName(path) {
return false
}
// Project references go to source file instead of .d.ts file
return fs.fileOrDirectoryExistsUsingSource(path /*isFile*/, true)
}
// ReadFile implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) ReadFile(path string) (contents string, ok bool) {
// Dont need to override as we cannot mimick read file
return fs.projectReferenceFileMapper.opts.Host.FS().ReadFile(path)
}
// WriteFile implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) WriteFile(path string, data string) error {
panic("should not be called by resolver")
}
// AppendFile implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) AppendFile(path string, data string) error {
panic("should not be called by resolver")
}
// Remove implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) Remove(path string) error {
panic("should not be called by resolver")
}
// Chtimes implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) Chtimes(path string, aTime time.Time, mTime time.Time) error {
panic("should not be called by resolver")
}
// DirectoryExists implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) DirectoryExists(path string) bool {
if fs.projectReferenceFileMapper.opts.Host.FS().DirectoryExists(path) {
fs.handleDirectoryCouldBeSymlink(path)
return true
}
return fs.fileOrDirectoryExistsUsingSource(path /*isFile*/, false)
}
// GetAccessibleEntries implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) GetAccessibleEntries(path string) vfs.Entries {
panic("should not be called by resolver")
}
// Stat implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) Stat(path string) vfs.FileInfo {
panic("should not be called by resolver")
}
// WalkDir implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) WalkDir(root string, walkFn vfs.WalkDirFunc) error {
panic("should not be called by resolver")
}
// Realpath implements vfs.FS.
func (fs *projectReferenceDtsFakingVfs) Realpath(path string) string {
result, ok := fs.knownSymlinks.Files().Load(fs.toPath(path))
if ok {
return result
}
return fs.projectReferenceFileMapper.opts.Host.FS().Realpath(path)
}
func (fs *projectReferenceDtsFakingVfs) toPath(path string) tspath.Path {
return tspath.ToPath(path, fs.projectReferenceFileMapper.opts.Host.GetCurrentDirectory(), fs.UseCaseSensitiveFileNames())
}
func (fs *projectReferenceDtsFakingVfs) handleDirectoryCouldBeSymlink(directory string) {
if tspath.ContainsIgnoredPath(directory) {
return
}
// Because we already watch node_modules, handle symlinks in there
if !strings.Contains(directory, "/node_modules/") {
return
}
directoryPath := tspath.Path(tspath.EnsureTrailingDirectorySeparator(string(fs.toPath(directory))))
if _, ok := fs.knownSymlinks.Directories().Load(directoryPath); ok {
return
}
realDirectory := fs.Realpath(directory)
var realPath tspath.Path
if realDirectory == directory {
// not symlinked
return
}
if realPath = tspath.Path(tspath.EnsureTrailingDirectorySeparator(string(fs.toPath(realDirectory)))); realPath == directoryPath {
// not symlinked
return
}
fs.knownSymlinks.SetDirectory(directory, directoryPath, &symlinks.KnownDirectoryLink{
Real: tspath.EnsureTrailingDirectorySeparator(realDirectory),
RealPath: realPath,
})
}
func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrDirectory string, isFile bool) bool {
fileOrDirectoryExistsUsingSource := core.IfElse(isFile, fs.fileExistsIfProjectReferenceDts, fs.directoryExistsIfProjectReferenceDeclDir)
// Check current directory or file
result := fileOrDirectoryExistsUsingSource(fileOrDirectory)
if result != core.TSUnknown {
return result == core.TSTrue
}
fileOrDirectoryPath := fs.toPath(fileOrDirectory)
if !strings.Contains(string(fileOrDirectoryPath), "/node_modules/") {
return false
}
// Check if the directory or file is a symlinked package
if packageRoot := module.ParseNodeModuleFromPath(fileOrDirectory, true /*isFolder*/); packageRoot != "" {
fs.handleDirectoryCouldBeSymlink(packageRoot)
}
knownDirectoryLinks := fs.knownSymlinks.Directories()
if knownDirectoryLinks.Size() == 0 {
return false
}
if isFile {
_, ok := fs.knownSymlinks.Files().Load(fileOrDirectoryPath)
if ok {
return true
}
}
// If it contains node_modules check if its one of the symlinked path we know of
var exists bool
knownDirectoryLinks.Range(func(directoryPath tspath.Path, knownDirectoryLink *symlinks.KnownDirectoryLink) bool {
relative, hasPrefix := strings.CutPrefix(string(fileOrDirectoryPath), string(directoryPath))
if !hasPrefix {
return true
}
if exists = fileOrDirectoryExistsUsingSource(string(knownDirectoryLink.RealPath) + relative).IsTrue(); exists {
if isFile {
// Store the real path for the file
absolutePath := tspath.GetNormalizedAbsolutePath(fileOrDirectory, fs.projectReferenceFileMapper.opts.Host.GetCurrentDirectory())
fs.knownSymlinks.SetFile(
absolutePath,
fileOrDirectoryPath,
knownDirectoryLink.Real+absolutePath[len(directoryPath):],
)
}
return false
}
return true
})
return exists
}
func (fs *projectReferenceDtsFakingVfs) fileExistsIfProjectReferenceDts(file string) core.Tristate {
source := fs.projectReferenceFileMapper.getProjectReferenceFromOutputDts(fs.toPath(file))
if source != nil {
return core.IfElse(fs.projectReferenceFileMapper.opts.Host.FS().FileExists(source.Source), core.TSTrue, core.TSFalse)
}
return core.TSUnknown
}
func (fs *projectReferenceDtsFakingVfs) directoryExistsIfProjectReferenceDeclDir(dir string) core.Tristate {
dirPath := fs.toPath(dir)
dirPathWithTrailingDirectorySeparator := dirPath + "/"
for declDirPath := range fs.dtsDirectories.Keys() {
if dirPath == declDirPath ||
// Any parent directory of declaration dir
strings.HasPrefix(string(declDirPath), string(dirPathWithTrailingDirectorySeparator)) ||
// Any directory inside declaration dir
strings.HasPrefix(string(dirPath), string(declDirPath)+"/") {
return core.TSTrue
}
}
return core.TSUnknown
}

View File

@@ -0,0 +1,187 @@
package compiler
import (
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/module"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type projectReferenceFileMapper struct {
opts ProgramOptions
host module.ResolutionHost
loader *fileLoader // Only present during populating the mapper and parsing, released after that
configToProjectReference map[tspath.Path]*tsoptions.ParsedCommandLine // All the resolved references needed
referencesInConfigFile map[tspath.Path][]tspath.Path // Map of config file to its references
sourceToProjectReference map[tspath.Path]*tsoptions.SourceOutputAndProjectReference
outputDtsToProjectReference map[tspath.Path]*tsoptions.SourceOutputAndProjectReference
// Store all the realpath from dts in node_modules to source file from project reference needed during parsing so it can be used later
realpathDtsToSource collections.SyncMap[tspath.Path, *tsoptions.SourceOutputAndProjectReference]
}
func (mapper *projectReferenceFileMapper) getParseFileRedirect(file ast.HasFileName) string {
if mapper.opts.canUseProjectReferenceSource() {
// Map to source file from project reference
source := mapper.getProjectReferenceFromOutputDts(file.Path())
if source == nil {
source = mapper.getSourceToDtsIfSymlink(file)
}
if source != nil {
return source.Source
}
} else {
// Map to dts file from project reference
output := mapper.getProjectReferenceFromSource(file.Path())
if output != nil && output.OutputDts != "" {
return output.OutputDts
}
}
return ""
}
func (mapper *projectReferenceFileMapper) getResolvedProjectReferences() []*tsoptions.ParsedCommandLine {
if mapper.opts.Config.ConfigFile == nil {
return nil
}
refs, ok := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()]
var result []*tsoptions.ParsedCommandLine
if ok {
result = make([]*tsoptions.ParsedCommandLine, 0, len(refs))
for _, refPath := range refs {
refConfig, _ := mapper.configToProjectReference[refPath]
result = append(result, refConfig)
}
}
return result
}
func (mapper *projectReferenceFileMapper) getProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
return mapper.sourceToProjectReference[path]
}
func (mapper *projectReferenceFileMapper) getProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
return mapper.outputDtsToProjectReference[path]
}
func (mapper *projectReferenceFileMapper) isSourceFromProjectReference(path tspath.Path) bool {
return mapper.opts.canUseProjectReferenceSource() && mapper.getProjectReferenceFromSource(path) != nil
}
func (mapper *projectReferenceFileMapper) getCompilerOptionsForFile(file ast.HasFileName) *core.CompilerOptions {
redirect := mapper.getRedirectParsedCommandLineForResolution(file)
return module.GetCompilerOptionsWithRedirect(mapper.opts.Config.CompilerOptions(), redirect)
}
func (mapper *projectReferenceFileMapper) getRedirectParsedCommandLineForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine {
redirect, _ := mapper.getRedirectForResolution(file)
return redirect
}
func (mapper *projectReferenceFileMapper) getRedirectForResolution(file ast.HasFileName) (*tsoptions.ParsedCommandLine, string) {
path := file.Path()
// Check if outputdts of source file from project reference
output := mapper.getProjectReferenceFromSource(path)
if output != nil {
return output.Resolved, output.Source
}
// Source file from project reference
resultFromDts := mapper.getProjectReferenceFromOutputDts(path)
if resultFromDts != nil {
return resultFromDts.Resolved, resultFromDts.Source
}
realpathDtsToSource := mapper.getSourceToDtsIfSymlink(file)
if realpathDtsToSource != nil {
return realpathDtsToSource.Resolved, realpathDtsToSource.Source
}
return nil, file.FileName()
}
func (mapper *projectReferenceFileMapper) getResolvedReferenceFor(path tspath.Path) (*tsoptions.ParsedCommandLine, bool) {
config, ok := mapper.configToProjectReference[path]
return config, ok
}
func (mapper *projectReferenceFileMapper) rangeResolvedProjectReference(
f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
) bool {
if mapper.opts.Config.ConfigFile == nil {
return false
}
seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile))
seenRef.Add(mapper.opts.Config.ConfigFile.SourceFile.Path())
refs := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()]
return mapper.rangeResolvedReferenceWorker(refs, f, mapper.opts.Config, seenRef)
}
func (mapper *projectReferenceFileMapper) rangeResolvedReferenceWorker(
references []tspath.Path,
f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
parent *tsoptions.ParsedCommandLine,
seenRef *collections.Set[tspath.Path],
) bool {
for index, path := range references {
if !seenRef.AddIfAbsent(path) {
continue
}
config, _ := mapper.configToProjectReference[path]
if !f(path, config, parent, index) {
return false
}
if !mapper.rangeResolvedReferenceWorker(mapper.referencesInConfigFile[path], f, config, seenRef) {
return false
}
}
return true
}
func (mapper *projectReferenceFileMapper) rangeResolvedProjectReferenceInChildConfig(
childConfig *tsoptions.ParsedCommandLine,
f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
) bool {
if childConfig == nil || childConfig.ConfigFile == nil {
return false
}
seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile))
seenRef.Add(childConfig.ConfigFile.SourceFile.Path())
refs := mapper.referencesInConfigFile[childConfig.ConfigFile.SourceFile.Path()]
return mapper.rangeResolvedReferenceWorker(refs, f, mapper.opts.Config, seenRef)
}
func (mapper *projectReferenceFileMapper) getSourceToDtsIfSymlink(file ast.HasFileName) *tsoptions.SourceOutputAndProjectReference {
// If preserveSymlinks is true, module resolution wont jump the symlink
// but the resolved real path may be the .d.ts from project reference
// Note:: Currently we try the real path only if the
// file is from node_modules to avoid having to run real path on all file paths
path := file.Path()
realpathDtsToSource, ok := mapper.realpathDtsToSource.Load(path)
if ok {
return realpathDtsToSource
}
if mapper.loader != nil && mapper.opts.Config.CompilerOptions().PreserveSymlinks == core.TSTrue {
fileName := file.FileName()
if !strings.Contains(fileName, "/node_modules/") {
mapper.realpathDtsToSource.Store(path, nil)
} else {
realDeclarationPath := mapper.loader.toPath(mapper.host.FS().Realpath(fileName))
if realDeclarationPath == path {
mapper.realpathDtsToSource.Store(path, nil)
} else {
realpathDtsToSource := mapper.getProjectReferenceFromOutputDts(realDeclarationPath)
if realpathDtsToSource != nil {
mapper.realpathDtsToSource.Store(path, realpathDtsToSource)
return realpathDtsToSource
}
mapper.realpathDtsToSource.Store(path, nil)
}
}
}
return nil
}

View File

@@ -0,0 +1,115 @@
package compiler
import (
"maps"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tracing"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
)
type projectReferenceParseTask struct {
configName string
resolved *tsoptions.ParsedCommandLine
subTasks []*projectReferenceParseTask
}
func (t *projectReferenceParseTask) parse(projectReferenceParser *projectReferenceParser) {
loader := projectReferenceParser.loader
if tr := loader.opts.Tracing; tr != nil {
defer tr.Push(tracing.PhaseParse, "parseJsonSourceFileConfigFileContent", map[string]any{"path": t.configName}, false)()
}
t.resolved = loader.opts.Host.GetResolvedProjectReference(t.configName, loader.toPath(t.configName))
if t.resolved == nil {
return
}
t.resolved.ParseInputOutputNames()
if subReferences := t.resolved.ResolvedProjectReferencePaths(); len(subReferences) > 0 {
t.subTasks = createProjectReferenceParseTasks(subReferences)
}
}
func createProjectReferenceParseTasks(projectReferences []string) []*projectReferenceParseTask {
return core.Map(projectReferences, func(configName string) *projectReferenceParseTask {
return &projectReferenceParseTask{
configName: configName,
}
})
}
type projectReferenceParser struct {
loader *fileLoader
wg core.WorkGroup
tasksByFileName collections.SyncMap[tspath.Path, *projectReferenceParseTask]
}
func (p *projectReferenceParser) parse(tasks []*projectReferenceParseTask) {
p.loader.projectReferenceFileMapper.loader = p.loader
p.start(tasks)
p.wg.RunAndWait()
p.initMapper(tasks)
}
func (p *projectReferenceParser) start(tasks []*projectReferenceParseTask) {
for i, task := range tasks {
path := p.loader.toPath(task.configName)
if loadedTask, loaded := p.tasksByFileName.LoadOrStore(path, task); loaded {
// dedup tasks to ensure correct file order, regardless of which task would be started first
tasks[i] = loadedTask
} else {
p.wg.Queue(func() {
task.parse(p)
p.start(task.subTasks)
})
}
}
}
func (p *projectReferenceParser) initMapper(tasks []*projectReferenceParseTask) {
totalReferences := p.tasksByFileName.Size() + 1
p.loader.projectReferenceFileMapper.configToProjectReference = make(map[tspath.Path]*tsoptions.ParsedCommandLine, totalReferences)
p.loader.projectReferenceFileMapper.referencesInConfigFile = make(map[tspath.Path][]tspath.Path, totalReferences)
p.loader.projectReferenceFileMapper.sourceToProjectReference = make(map[tspath.Path]*tsoptions.SourceOutputAndProjectReference)
p.loader.projectReferenceFileMapper.outputDtsToProjectReference = make(map[tspath.Path]*tsoptions.SourceOutputAndProjectReference)
p.loader.projectReferenceFileMapper.referencesInConfigFile[p.loader.opts.Config.ConfigFile.SourceFile.Path()] = p.initMapperWorker(tasks, &collections.Set[*projectReferenceParseTask]{})
if p.loader.projectReferenceFileMapper.opts.canUseProjectReferenceSource() && len(p.loader.projectReferenceFileMapper.outputDtsToProjectReference) != 0 {
p.loader.projectReferenceFileMapper.host = newProjectReferenceDtsFakingHost(p.loader)
}
}
func (p *projectReferenceParser) initMapperWorker(tasks []*projectReferenceParseTask, seen *collections.Set[*projectReferenceParseTask]) []tspath.Path {
if len(tasks) == 0 {
return nil
}
results := make([]tspath.Path, 0, len(tasks))
for _, task := range tasks {
path := p.loader.toPath(task.configName)
results = append(results, path)
// ensure we only walk each task once
if !seen.AddIfAbsent(task) {
continue
}
p.loader.projectReferenceFileMapper.configToProjectReference[path] = task.resolved
if task.resolved != nil && p.loader.projectReferenceFileMapper.opts.Config.ConfigFile != task.resolved.ConfigFile {
// Map current task's files first, before recursing into subtasks.
// This matches TypeScript's behavior where child project references
// overwrite parent entries when a file belongs to multiple projects.
maps.Copy(p.loader.projectReferenceFileMapper.sourceToProjectReference, task.resolved.SourceToProjectReference())
maps.Copy(p.loader.projectReferenceFileMapper.outputDtsToProjectReference, task.resolved.OutputDtsToProjectReference())
if p.loader.projectReferenceFileMapper.opts.canUseProjectReferenceSource() {
declDir := task.resolved.CompilerOptions().DeclarationDir
if declDir == "" {
declDir = task.resolved.CompilerOptions().OutDir
}
if declDir != "" {
p.loader.dtsDirectories.Add(p.loader.toPath(declDir))
}
}
}
referencesInConfig := p.initMapperWorker(task.subTasks, seen)
p.loader.projectReferenceFileMapper.referencesInConfigFile[path] = referencesInConfig
}
return results
}