package autoimport import ( "slices" "sync/atomic" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/binder" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls/lsutil" "github.com/microsoft/typescript-go/internal/module" "github.com/microsoft/typescript-go/internal/tspath" ) type symbolExtractor struct { packageName string stats *extractorStats localNameResolver *binder.NameResolver checker *checker.Checker toPath func(fileName string) tspath.Path // realpath, if set, is used to resolve symlinks for ModuleID generation. // This ensures that symlinked packages use their realpath as ModuleID, // deduplicating exports from files that appear via multiple symlink paths. realpath func(fileName string) string } type exportExtractor struct { *symbolExtractor moduleResolver *module.Resolver } type extractorStats struct { exports atomic.Int32 usedChecker atomic.Int32 } func (e *exportExtractor) Stats() *extractorStats { return e.stats } type checkerLease struct { used bool checker *checker.Checker } func (l *checkerLease) GetChecker() *checker.Checker { l.used = true return l.checker } func (l *checkerLease) TryChecker() *checker.Checker { if l.used { return l.checker } return nil } func newSymbolExtractor(packageName string, checker *checker.Checker, toPath func(string) tspath.Path, realpath func(string) string) *symbolExtractor { return &symbolExtractor{ packageName: packageName, checker: checker, localNameResolver: &binder.NameResolver{ CompilerOptions: core.EmptyCompilerOptions, }, stats: &extractorStats{}, toPath: toPath, realpath: realpath, } } func (b *registryBuilder) newExportExtractor(packageName string, checker *checker.Checker, moduleResolver *module.Resolver, realpath func(string) string) *exportExtractor { return &exportExtractor{ symbolExtractor: newSymbolExtractor(packageName, checker, b.base.toPath, realpath), moduleResolver: moduleResolver, } } // getModuleID returns the ModuleID for a file, using realpath if available. func (e *symbolExtractor) getModuleID(file *ast.SourceFile) ModuleID { if e.realpath != nil && e.toPath != nil { realpath := e.realpath(file.FileName()) return ModuleID(e.toPath(realpath)) } return ModuleID(file.Path()) } // getModuleIDForSymbol returns the ModuleID for a module symbol, using realpath // normalization when available for source files. func (e *symbolExtractor) getModuleIDForSymbol(symbol *ast.Symbol) (ModuleID, bool) { moduleID, fileName, ok := tryGetModuleIDAndFileNameOfModuleSymbol(symbol) if !ok { return "", false } // If fileName is set, this is a source file that may need realpath normalization if fileName != "" && e.realpath != nil { decl := ast.GetNonAugmentationDeclaration(symbol) if decl != nil && decl.Kind == ast.KindSourceFile { return e.getModuleID(decl.AsSourceFile()), true } } return moduleID, true } func (e *exportExtractor) extractFromFile(file *ast.SourceFile) []*Export { if file.Symbol != nil { return e.extractFromModule(file) } if len(file.AmbientModuleNames) > 0 { moduleDeclarations := core.Filter(file.Statements.Nodes, ast.IsModuleWithStringLiteralName) var exportCount int for _, decl := range moduleDeclarations { exportCount += len(decl.AsModuleDeclaration().Symbol.Exports) } exports := make([]*Export, 0, exportCount) for _, decl := range moduleDeclarations { e.extractFromModuleDeclaration(decl.AsModuleDeclaration(), file, ModuleID(decl.Name().Text()), "", &exports) } return exports } return nil } func (e *exportExtractor) extractFromModule(file *ast.SourceFile) []*Export { moduleAugmentations := core.MapNonNil(file.ModuleAugmentations, func(name *ast.ModuleName) *ast.ModuleDeclaration { decl := name.Parent if ast.IsGlobalScopeAugmentation(decl) { return nil } return decl.AsModuleDeclaration() }) var augmentationExportCount int for _, decl := range moduleAugmentations { augmentationExportCount += len(decl.Symbol.Exports) } moduleID := e.getModuleID(file) exports := make([]*Export, 0, len(file.Symbol.Exports)+augmentationExportCount) for name, symbol := range file.Symbol.Exports { e.extractFromSymbol(name, symbol, moduleID, file.FileName(), file, &exports) } for _, decl := range moduleAugmentations { name := decl.Name().AsStringLiteral().Text moduleID := ModuleID(name) var moduleFileName string if tspath.IsExternalModuleNameRelative(name) { if resolved, _ := e.moduleResolver.ResolveModuleName(name, file.FileName(), core.ModuleKindCommonJS, nil); resolved.IsResolved() { moduleFileName = resolved.ResolvedFileName moduleID = ModuleID(e.toPath(moduleFileName)) } else { // :shrug: moduleFileName = tspath.ResolvePath(tspath.GetDirectoryPath(file.FileName()), name) moduleID = ModuleID(e.toPath(moduleFileName)) } } e.extractFromModuleDeclaration(decl, file, moduleID, moduleFileName, &exports) } return exports } func (e *exportExtractor) extractFromModuleDeclaration(decl *ast.ModuleDeclaration, file *ast.SourceFile, moduleID ModuleID, moduleFileName string, exports *[]*Export) { for name, symbol := range decl.Symbol.Exports { e.extractFromSymbol(name, symbol, moduleID, moduleFileName, file, exports) } } func (e *symbolExtractor) extractFromSymbol(name string, symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, file *ast.SourceFile, exports *[]*Export) { if shouldIgnoreSymbol(symbol) { return } if name == ast.InternalSymbolNameExportStar { checkerLease := &checkerLease{checker: e.checker} allExports := e.checker.GetExportsOfModule(symbol.Parent) // allExports includes named exports from the file that will be processed separately; // we want to add only the ones that come from the star for name, namedExport := range symbol.Parent.Exports { if name != ast.InternalSymbolNameExportStar { idx := slices.Index(allExports, namedExport) if idx >= 0 || shouldIgnoreSymbol(namedExport) { allExports = slices.Delete(allExports, idx, idx+1) } } } *exports = slices.Grow(*exports, len(allExports)) for _, reexportedSymbol := range allExports { export, _ := e.createExport(reexportedSymbol, moduleID, moduleFileName, ExportSyntaxStar, file, checkerLease) if export != nil { parent := checkerLease.GetChecker().GetMergedSymbol(reexportedSymbol.Parent) if parent != nil && parent.IsExternalModule() { if targetModuleID, ok := e.getModuleIDForSymbol(parent); ok { export.Target = ExportID{ ExportName: reexportedSymbol.Name, ModuleID: targetModuleID, } } } export.through = ast.InternalSymbolNameExportStar *exports = append(*exports, export) } } return } syntax := getSyntax(symbol) checkerLease := &checkerLease{checker: e.checker} export, target := e.createExport(symbol, moduleID, moduleFileName, syntax, file, checkerLease) if export == nil { return } *exports = append(*exports, export) if target != nil { if syntax == ExportSyntaxEquals && target.Flags&ast.SymbolFlagsNamespace != 0 { *exports = slices.Grow(*exports, len(target.Exports)) for innerName, namedExport := range target.Exports { if innerName != ast.InternalSymbolNameExportStar { export, _ := e.createExport(namedExport, moduleID, moduleFileName, syntax, file, checkerLease) if export != nil { export.through = name *exports = append(*exports, export) } } } } } else if syntax == ExportSyntaxCommonJSModuleExports { expression := symbol.Declarations[0].AsBinaryExpression().Right if expression.Kind == ast.KindObjectLiteralExpression { // what is actually desirable here? I think it would be reasonable to only treat these as exports // if *every* property is a shorthand property or identifier: identifier // At least, it would be sketchy if there were any methods, computed properties... *exports = slices.Grow(*exports, len(expression.AsObjectLiteralExpression().Properties.Nodes)) for _, prop := range expression.AsObjectLiteralExpression().Properties.Nodes { if ast.IsShorthandPropertyAssignment(prop) || ast.IsPropertyAssignment(prop) && prop.AsPropertyAssignment().Name().Kind == ast.KindIdentifier { export, _ := e.createExport(expression.Symbol().Members[prop.Name().Text()], moduleID, moduleFileName, syntax, file, checkerLease) if export != nil { export.through = name *exports = append(*exports, export) } } } } } } // createExport creates an Export for the given symbol, returning the Export and the target symbol if the export is an alias. func (e *symbolExtractor) createExport(symbol *ast.Symbol, moduleID ModuleID, moduleFileName string, syntax ExportSyntax, file *ast.SourceFile, checkerLease *checkerLease) (*Export, *ast.Symbol) { if shouldIgnoreSymbol(symbol) { return nil, nil } export := &Export{ ExportID: ExportID{ ExportName: symbol.Name, ModuleID: moduleID, }, ModuleFileName: moduleFileName, Syntax: syntax, Flags: symbol.CombinedLocalAndExportSymbolFlags(), Path: file.Path(), PackageName: e.packageName, } if syntax == ExportSyntaxUMD { export.ExportName = ast.InternalSymbolNameExportEquals export.localName = symbol.Name } var targetSymbol *ast.Symbol if symbol.Flags&ast.SymbolFlagsAlias != 0 { targetSymbol = e.tryResolveSymbol(symbol, syntax, checkerLease) if targetSymbol != nil { var decl *ast.Node if len(targetSymbol.Declarations) > 0 { decl = targetSymbol.Declarations[0] } else if targetSymbol.CheckFlags&ast.CheckFlagsMapped != 0 { if mappedDecl := checkerLease.GetChecker().GetMappedTypeSymbolOfProperty(targetSymbol); mappedDecl != nil && len(mappedDecl.Declarations) > 0 { decl = mappedDecl.Declarations[0] } } if decl == nil { // !!! consider GetImmediateAliasedSymbol to go as far as we can decl = symbol.Declarations[0] } if decl == nil { panic("no declaration for aliased symbol") } parent := targetSymbol.Parent if checker := checkerLease.TryChecker(); checker != nil { export.Flags = checker.GetSymbolFlags(targetSymbol) export.IsTypeOnly = checker.GetTypeOnlyAliasDeclaration(symbol) != nil parent = checker.GetMergedSymbol(parent) } else { export.Flags = targetSymbol.Flags export.IsTypeOnly = core.Some(symbol.Declarations, ast.IsPartOfTypeOnlyImportOrExportDeclaration) } export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), targetSymbol, decl) export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), targetSymbol) targetModuleID := ModuleID(ast.GetSourceFileOfNode(decl).Path()) if parent != nil && parent.IsExternalModule() { if id, ok := e.getModuleIDForSymbol(parent); ok { targetModuleID = id } } export.Target = ExportID{ ExportName: targetSymbol.Name, ModuleID: targetModuleID, } } } else { export.ScriptElementKind = lsutil.GetSymbolKind(checkerLease.TryChecker(), symbol, symbol.Declarations[0]) export.ScriptElementKindModifiers = lsutil.GetSymbolModifiers(checkerLease.TryChecker(), symbol) } if symbol.Name == ast.InternalSymbolNameDefault || symbol.Name == ast.InternalSymbolNameExportEquals { namedSymbol := symbol if s := binder.GetLocalSymbolForExportDefault(symbol); s != nil { namedSymbol = s } export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol) if isUnusableName(export.localName) { export.localName = export.Target.ExportName } if isUnusableName(export.localName) { if targetSymbol != nil { namedSymbol = targetSymbol if s := binder.GetLocalSymbolForExportDefault(targetSymbol); s != nil { namedSymbol = s } export.localName = getDefaultLikeExportNameFromDeclaration(namedSymbol) } } if isUnusableName(export.localName) { // Last resort: derive identifier from the file name. Use FileName() (original // casing) rather than ModuleID/Path() which is lowercased on case-insensitive // file systems, losing PascalCase. export.localName = lsutil.ModuleSpecifierToValidIdentifier(fileNameForDefaultExportName(targetSymbol, moduleFileName, moduleID), false) } } if isUnusableName(export.Name()) { return nil, nil } e.stats.exports.Add(1) if checkerLease.TryChecker() != nil { e.stats.usedChecker.Add(1) } return export, targetSymbol } func (e *symbolExtractor) tryResolveSymbol(symbol *ast.Symbol, syntax ExportSyntax, checkerLease *checkerLease) *ast.Symbol { if !ast.IsNonLocalAlias(symbol, ast.SymbolFlagsNone) { return symbol } var loc *ast.Node var name string switch syntax { case ExportSyntaxNamed: decl := ast.GetDeclarationOfKind(symbol, ast.KindExportSpecifier) if decl.Parent.Parent.AsExportDeclaration().ModuleSpecifier == nil { if n := core.FirstNonZero(decl.Name(), decl.PropertyName()); n.Kind == ast.KindIdentifier { loc = n name = n.Text() } } // !!! check if module.exports = foo is marked as an alias case ExportSyntaxEquals: if symbol.Name != ast.InternalSymbolNameExportEquals { break } fallthrough case ExportSyntaxDefaultDeclaration: decl := ast.GetDeclarationOfKind(symbol, ast.KindExportAssignment) if decl.Expression().Kind == ast.KindIdentifier { loc = decl.Expression() name = loc.Text() } } if loc != nil { local := e.localNameResolver.Resolve(loc, name, ast.SymbolFlagsAll, nil, false, false) if local != nil && !ast.IsNonLocalAlias(local, ast.SymbolFlagsNone) { return local } } checker := checkerLease.GetChecker() if resolved := checker.GetAliasedSymbol(symbol); !checker.IsUnknownSymbol(resolved) { return resolved } return nil } func shouldIgnoreSymbol(symbol *ast.Symbol) bool { if symbol.Flags&ast.SymbolFlagsPrototype != 0 { return true } return false } func getSyntax(symbol *ast.Symbol) ExportSyntax { for _, decl := range symbol.Declarations { switch decl.Kind { case ast.KindExportSpecifier: return ExportSyntaxNamed case ast.KindExportAssignment: return core.IfElse( decl.AsExportAssignment().IsExportEquals, ExportSyntaxEquals, ExportSyntaxDefaultDeclaration, ) case ast.KindNamespaceExportDeclaration: return ExportSyntaxUMD case ast.KindBinaryExpression: switch ast.GetAssignmentDeclarationKind(decl) { case ast.JSDeclarationKindModuleExports: return ExportSyntaxCommonJSModuleExports case ast.JSDeclarationKindExportsProperty: return ExportSyntaxCommonJSExportsProperty } default: if ast.GetCombinedModifierFlags(decl)&ast.ModifierFlagsDefault != 0 { return ExportSyntaxDefaultModifier } else { return ExportSyntaxModifier } } } return ExportSyntaxNone } func isUnusableName(name string) bool { return name == "" || name == "_default" || name == ast.InternalSymbolNameExportStar || name == ast.InternalSymbolNameDefault || name == ast.InternalSymbolNameExportEquals } // fileNameForDefaultExportName returns the best file name to use when deriving // a fallback identifier for a default-like export. It prefers the target symbol's // source file (closest to the export origin), falls back to the module's original // file name, and uses the lowercased moduleID only for ambient modules where no // original file name is available. func fileNameForDefaultExportName(targetSymbol *ast.Symbol, moduleFileName string, moduleID ModuleID) string { if targetSymbol != nil && len(targetSymbol.Declarations) > 0 { if fn := ast.GetSourceFileOfNode(targetSymbol.Declarations[0]).FileName(); fn != "" { return fn } } if moduleFileName != "" { return moduleFileName } return string(moduleID) }