462 lines
16 KiB
Go
462 lines
16 KiB
Go
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)
|
|
}
|