Files
kjol/tools/tsgo/internal/checker/nodecopy.go
2026-07-09 16:50:43 -04:00

901 lines
36 KiB
Go

package checker
import (
"strings"
"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/nodebuilder"
"github.com/microsoft/typescript-go/internal/printer"
)
func (b *NodeBuilderImpl) reuseNode(node *ast.Node) *ast.Node {
if node == nil {
return node
}
return b.tryReuseExistingNodeHelper(node)
}
func (b *NodeBuilderImpl) tryJSTypeNodeToTypeNode(node *ast.Node) *ast.Node {
return b.reuseNode(node)
}
func (b *NodeBuilderImpl) reuseName(node *ast.Node, isMethod bool) *ast.Node {
res := b.reuseNode(node)
if res == nil {
return res
}
text, ok := ast.TryGetTextOfPropertyName(res)
if !ok {
return res
}
kind := classifyPropertyName(text, ast.IsStringLiteral(res), isMethod)
if ast.IsIdentifier(res) && kind == propertyNameNodeKindIdentifier {
return res
}
if ast.IsStringLiteral(res) && kind == propertyNameNodeKindStringLiteral {
return res
}
var renamed *ast.Node
switch kind {
case propertyNameNodeKindIdentifier:
renamed = b.newIdentifier(text, nil)
case propertyNameNodeKindStringLiteral:
renamed = b.f.NewStringLiteral(text, ast.TokenFlagsNone)
default:
return res
}
b.e.SetOriginal(renamed, res)
return b.setTextRange(renamed, res)
}
func (b *NodeBuilderImpl) reuseTypeNode(node *ast.Node) *ast.Node {
if node == nil {
return node
}
r := b.reuseNode(node)
if r != nil {
// After successful reuse during hover, probe the reused AST for expandable
// type references so canIncreaseExpansionDepth is set even though
// typeToTypeNode (and shouldExpandType) were never called.
if b.ctx.maxExpansionDepth >= 0 && !b.ctx.canIncreaseExpansionDepth {
b.walkNodeForExpandability(node)
}
return r
}
b.ctx.tracker.ReportInferenceFallback(node)
t := b.getTypeFromTypeNode(node, false)
return b.typeToTypeNode(t)
}
// walkNodeForExpandability walks a reused AST node tree, calling checkTypeExpandability
// on each type reference, type predicate, or import type node.
// Short-circuits once canIncreaseExpansionDepth is set.
func (b *NodeBuilderImpl) walkNodeForExpandability(node *ast.Node) {
if b.ctx.canIncreaseExpansionDepth || node == nil {
return
}
// Check these explicitly so we look into type arguments wehther or not they are in the tree or not.
if ast.IsTypeReferenceNode(node) || ast.IsExpressionWithTypeArguments(node) || ast.IsTypePredicateNode(node) || ast.IsImportTypeNode(node) {
t := b.getTypeFromTypeNode(node, false)
if t != nil {
b.checkTypeExpandability(t)
if b.ctx.canIncreaseExpansionDepth {
return
}
}
}
node.ForEachChild(func(child *ast.Node) bool {
b.walkNodeForExpandability(child)
return b.ctx.canIncreaseExpansionDepth
})
}
type recoveryBoundary struct {
ctx *NodeBuilderContext
hadError bool
deferredReports []func()
oldTracker nodebuilder.SymbolTracker
oldTrackedSymbols []*TrackedSymbolArgs
trackedSymbols []*TrackedSymbolArgs
oldEncounteredError bool
oldApproximateLength int
}
func (b *recoveryBoundary) markError(f func()) {
b.hadError = true
if f != nil {
b.deferredReports = append(b.deferredReports, f)
}
}
type originalRecoveryScopeState struct {
trackedSymbolsTop int
unreportedErrorsTop int
hadError bool
}
func (b *recoveryBoundary) startRecoveryScope() originalRecoveryScopeState {
trackedSymbolsTop := len(b.ctx.trackedSymbols)
unreportedErrorsTop := len(b.deferredReports)
return originalRecoveryScopeState{trackedSymbolsTop: trackedSymbolsTop, unreportedErrorsTop: unreportedErrorsTop, hadError: b.hadError}
}
func (b *recoveryBoundary) endRecoveryScope(state originalRecoveryScopeState) {
b.hadError = state.hadError
b.ctx.trackedSymbols = b.ctx.trackedSymbols[0:state.trackedSymbolsTop]
b.deferredReports = b.deferredReports[0:state.unreportedErrorsTop]
}
type wrappingTracker struct {
wrapped nodebuilder.SymbolTracker
bound *recoveryBoundary
}
func (w *wrappingTracker) PopErrorFallbackNode() {
w.wrapped.PopErrorFallbackNode()
}
func (w *wrappingTracker) PushErrorFallbackNode(node *ast.Node) {
w.wrapped.PushErrorFallbackNode(node)
}
func (w *wrappingTracker) ReportCyclicStructureError() {
w.bound.markError(w.wrapped.ReportCyclicStructureError)
}
func (w *wrappingTracker) ReportInaccessibleThisError() {
w.bound.markError(w.wrapped.ReportInaccessibleThisError)
}
func (w *wrappingTracker) ReportInaccessibleUniqueSymbolError() {
w.bound.markError(w.wrapped.ReportInaccessibleUniqueSymbolError)
}
func (w *wrappingTracker) ReportInferenceFallback(node *ast.Node) {
w.wrapped.ReportInferenceFallback(node) // Should this also be deferred?
}
func (w *wrappingTracker) ReportLikelyUnsafeImportRequiredError(specifier string, symbolName string) {
w.bound.markError(func() { w.wrapped.ReportLikelyUnsafeImportRequiredError(specifier, symbolName) })
}
func (w *wrappingTracker) ReportNonSerializableProperty(propertyName string) {
w.bound.markError(func() { w.wrapped.ReportNonSerializableProperty(propertyName) })
}
func (w *wrappingTracker) ReportNonlocalAugmentation(containingFile *ast.SourceFile, parentSymbol *ast.Symbol, augmentingSymbol *ast.Symbol) {
w.wrapped.ReportNonlocalAugmentation(containingFile, parentSymbol, augmentingSymbol) // Should this also be deferred?
}
func (w *wrappingTracker) ReportPrivateInBaseOfClassExpression(propertyName string) {
w.bound.markError(func() { w.wrapped.ReportPrivateInBaseOfClassExpression(propertyName) })
}
func (w *wrappingTracker) ReportTruncationError() {
w.wrapped.ReportTruncationError() // Should this also be deferred?
}
func (w *wrappingTracker) TrackSymbol(symbol *ast.Symbol, enclosingDeclaration *ast.Node, meaning ast.SymbolFlags) bool {
w.bound.trackedSymbols = append(w.bound.trackedSymbols, &TrackedSymbolArgs{symbol, enclosingDeclaration, meaning})
return false
}
func newWrappingTracker(inner nodebuilder.SymbolTracker, bound *recoveryBoundary) *wrappingTracker {
return &wrappingTracker{
wrapped: inner,
bound: bound,
}
}
func (b *NodeBuilderImpl) createRecoveryBoundary() *recoveryBoundary {
b.ch.checkNotCanceled()
bound := &recoveryBoundary{ctx: b.ctx, oldTracker: b.ctx.tracker, oldTrackedSymbols: b.ctx.trackedSymbols, oldEncounteredError: b.ctx.encounteredError, oldApproximateLength: b.ctx.approximateLength}
newTracker := NewSymbolTrackerImpl(b.ctx, newWrappingTracker(b.ctx.tracker, bound))
b.ctx.tracker = newTracker
b.ctx.trackedSymbols = nil
return bound
}
func (b *NodeBuilderImpl) finalizeBoundary(bound *recoveryBoundary) bool {
b.ctx.tracker = bound.oldTracker
b.ctx.trackedSymbols = bound.oldTrackedSymbols
b.ctx.encounteredError = bound.oldEncounteredError
b.ctx.approximateLength = bound.oldApproximateLength
for _, f := range bound.deferredReports {
f()
}
if bound.hadError {
return false
}
for _, a := range bound.trackedSymbols {
b.ctx.tracker.TrackSymbol(a.symbol, a.enclosingDeclaration, a.meaning)
}
return true
}
func (b *NodeBuilderImpl) tryReuseExistingNodeHelper(existing *ast.TypeNode) *ast.TypeNode {
bound := b.createRecoveryBoundary()
var transformed *ast.Node
v := getExistingNodeTreeVisitor(b, bound) // !!! TODO: Cache visitor and just reset bound+host builder? We try this for a *lot* of nodes.
transformed = v.VisitNode(existing)
if !b.finalizeBoundary(bound) {
return nil
}
b.ctx.approximateLength += existing.Loc.End() - existing.Loc.Pos()
return transformed
}
func (b *NodeBuilderImpl) getModuleSpecifierOverride(parent *ast.Node, lit *ast.Node) string {
if b.ctx.enclosingFile != ast.GetSourceFileOfNode(lit) {
mode := core.ResolutionModeNone
if parent.AsImportTypeNode().Attributes != nil {
mode = b.ch.getResolutionModeOverride(parent.AsImportTypeNode().Attributes.AsImportAttributes(), false)
}
name := lit.Text()
originalName := name
nodeSymbol := b.tryGetResolvedSymbolFromTypeNode(parent)
meaning := ast.SymbolFlagsType
if parent.AsImportTypeNode().IsTypeOf {
meaning = ast.SymbolFlagsValue
}
var parentSymbol *ast.Symbol
if nodeSymbol != nil && b.ch.IsSymbolAccessible(nodeSymbol, b.ctx.enclosingDeclaration, meaning, false).Accessibility == printer.SymbolAccessibilityAccessible {
parentSymbol = b.lookupSymbolChain(nodeSymbol, meaning, true)[0]
}
if parentSymbol != nil && IsExternalModuleSymbol(parentSymbol) {
name = b.getSpecifierForModuleSymbol(parentSymbol, mode)
} else {
targetFile := b.ch.getExternalModuleFileFromDeclaration(parent)
if targetFile != nil {
name = b.getSpecifierForModuleSymbol(targetFile.Symbol, mode)
}
}
if len(name) > 0 && strings.Contains(name, "/node_modules/") {
b.ctx.encounteredError = true
b.ctx.tracker.ReportLikelyUnsafeImportRequiredError(name, "")
}
if name != originalName {
return name
}
}
return ""
}
func (b *NodeBuilderImpl) rewriteModuleSpecifier(parent *ast.Node, lit *ast.Node) *ast.Node {
newName := b.getModuleSpecifierOverride(parent, lit)
if len(newName) == 0 {
return lit
}
res := b.f.NewStringLiteral(newName, ast.TokenFlagsNone)
b.e.SetOriginal(res, lit)
return res
}
func (b *NodeBuilderImpl) getEnclosingDeclarationIgnoringFakeScope() *ast.Node {
enc := b.ctx.enclosingDeclaration
for enc != nil && b.links.Get(enc).fakeScopeForSignatureDeclaration != nil {
enc = enc.Parent
}
return enc
}
func getExistingNodeTreeVisitor(b *NodeBuilderImpl, bound *recoveryBoundary) *ast.NodeVisitor {
// TODO: wrap all these closures into methods on an object so we can guarantee we reuse the same memory on each invocation by reusing/resetting the object
// instead of re-closing-over all of these each time we need a visitor. In theory the compiler could handle this, but in practice closure inlining hasn't been reliable
var visitor *ast.NodeVisitor
// note: also handles renaming type parameters renamed within the current context
attachSymbolToLeftmostIdentifier := func(leftmost *ast.Node, node *ast.Node, sym *ast.Symbol) *ast.Node {
var vis *ast.NodeVisitor
visitorFunc := func(node *ast.Node) *ast.Node {
if node == leftmost {
var type_ *Type
var name *ast.Node
if sym != nil {
type_ = b.ch.getDeclaredTypeOfSymbol(sym)
if sym.Flags&ast.SymbolFlagsTypeParameter != 0 {
name = b.typeParameterToName(type_).AsNode()
}
}
if name == nil {
name = b.newIdentifier(node.Text(), sym)
}
name = b.setTextRange(name, node)
b.e.AddEmitFlags(name, printer.EFNoAsciiEscaping)
return name
}
return b.setTextRange(node.VisitEachChild(vis), node)
}
vis = ast.NewNodeVisitor(visitorFunc, b.f, ast.NodeVisitorHooks{})
return visitorFunc(node)
}
trackExistingEntityName := func(node *ast.Node, overrideEnclosing *ast.Node) (bool, *ast.Node, *ast.Symbol) {
enclosingDeclaration := b.ctx.enclosingDeclaration
if overrideEnclosing != nil {
enclosingDeclaration = overrideEnclosing
}
introducesError := false
leftmost := ast.GetFirstIdentifier(node)
if ast.IsInJSFile(node) && (ast.IsExportsIdentifier(leftmost) || ast.IsModuleExportsAccessExpression(leftmost.Parent) || (ast.IsQualifiedName(leftmost.Parent) && ast.IsModuleIdentifier(leftmost.Parent.AsQualifiedName().Left) && ast.IsExportsIdentifier(leftmost.Parent.AsQualifiedName().Right))) {
introducesError = true
return introducesError, b.setTextRange(b.f.DeepCloneNode(node), node), nil
}
meaning := getMeaningOfEntityNameReference(node)
var sym *ast.Symbol
if ast.IsThisIdentifier(leftmost) {
// `this` isn't a bindable identifier - skip resolution, find a relevant `this` symbol directly and avoid exhaustive scope traversal
sym = b.ch.getSymbolOfDeclaration(b.ch.getThisContainer(leftmost, false, false))
if b.ch.IsSymbolAccessible(sym, leftmost, meaning, false).Accessibility != printer.SymbolAccessibilityAccessible {
introducesError = true
b.ctx.tracker.ReportInaccessibleThisError()
}
return introducesError, attachSymbolToLeftmostIdentifier(leftmost, node, sym), nil
}
sym = b.ch.resolveEntityName(leftmost, meaning, true, true, nil)
if b.ctx.enclosingDeclaration != nil && !(sym != nil && sym.Flags&ast.SymbolFlagsTypeParameter != 0) {
sym = b.ch.getExportSymbolOfValueSymbolIfExported(sym)
// Some declarations may be transplanted to a new location.
// When this happens we need to make sure that the name has the same meaning at both locations
// We also check for the unknownSymbol because when we create a fake scope some parameters may actually not be usable
// either because they are the expanded rest parameter,
// or because they are the newly added parameters from the tuple, which might have different meanings in the original context
symAtLocation := b.ch.resolveEntityName(leftmost, meaning, true, true, b.ctx.enclosingDeclaration)
if
// Check for unusable parameters symbols
symAtLocation == b.ch.unknownSymbol ||
// If the symbol is not found, but was not found in the original scope either we probably have an error, don't reuse the node
(symAtLocation == nil && sym != nil) ||
// If the symbol is found both in declaration scope and in current scope then it should point to the same reference
(symAtLocation != nil && sym != nil && b.ch.getSymbolIfSameReference(b.ch.getExportSymbolOfValueSymbolIfExported(symAtLocation), sym) == nil) {
// In isolated declaration we will not do rest parameter expansion so there is no need to report on these.
if symAtLocation != b.ch.unknownSymbol {
b.ctx.tracker.ReportInferenceFallback(node)
}
introducesError = true
return introducesError, b.setTextRange(b.f.DeepCloneNode(node), node), sym
} else {
sym = symAtLocation
}
}
if sym != nil {
// If a parameter is resolvable in the current context it is also visible, so no need to go to symbol accesibility
if sym.Flags&ast.SymbolFlagsFunctionScopedVariable != 0 && sym.ValueDeclaration != nil {
if ast.IsPartOfParameterDeclaration(sym.ValueDeclaration) || ast.IsJSDocParameterTag(sym.ValueDeclaration) {
return introducesError, attachSymbolToLeftmostIdentifier(leftmost, node, sym), nil
}
}
if sym.Flags&ast.SymbolFlagsTypeParameter == 0 /* Type parameters are visible in the current context if they are are resolvable */ && !ast.IsDeclarationName(node) &&
b.ch.IsSymbolAccessible(sym, enclosingDeclaration, meaning, false).Accessibility != printer.SymbolAccessibilityAccessible {
b.ctx.tracker.ReportInferenceFallback(node)
introducesError = true
} else {
b.ctx.tracker.TrackSymbol(sym, enclosingDeclaration, meaning)
}
return introducesError, attachSymbolToLeftmostIdentifier(leftmost, node, sym), nil
}
return introducesError, b.setTextRange(b.f.DeepCloneNode(node), node), nil
}
var tryVisitSimpleTypeNode func(node *ast.Node) *ast.Node
tryVisitIndexedAccess := func(node *ast.Node) *ast.Node {
resultObjectType := tryVisitSimpleTypeNode(node.AsIndexedAccessTypeNode().ObjectType)
if resultObjectType == nil {
return nil
}
return b.setTextRange(b.f.UpdateIndexedAccessTypeNode(node.AsIndexedAccessTypeNode(), resultObjectType, visitor.VisitNode(node.AsIndexedAccessTypeNode().IndexType)), node)
}
tryVisitKeyOf := func(node *ast.Node) *ast.Node {
to := node.AsTypeOperatorNode()
t := tryVisitSimpleTypeNode(to.Type)
if t == nil {
return nil
}
return b.setTextRange(b.f.UpdateTypeOperatorNode(to, to.Operator, t), node)
}
tryVisitTypeQuery := func(node *ast.Node) *ast.Node {
introducesError, exprName, _ := trackExistingEntityName(node.AsTypeQueryNode().ExprName, nil)
if !introducesError {
return b.setTextRange(b.f.UpdateTypeQueryNode(
node.AsTypeQueryNode(),
exprName,
visitor.VisitNodes(node.AsTypeQueryNode().TypeArguments),
), node)
}
serializedName := b.serializeTypeName(node.AsTypeQueryNode().ExprName, true, visitor.VisitNodes(node.AsTypeQueryNode().TypeArguments))
if serializedName != nil {
return b.setTextRange(serializedName, node.AsTypeQueryNode().ExprName)
}
return nil
}
tryVisitTypeReference := func(node *ast.Node) *ast.Node {
if ast.IsConstTypeReference(node) {
return nil
}
s := b.tryGetResolvedSymbolFromTypeNode(node)
if s == nil {
return nil // ???
}
if s.Flags&ast.SymbolFlagsTypeParameter != 0 {
declaredType := b.ch.getDeclaredTypeOfSymbol(s)
if b.ctx.mapper != nil && b.ctx.mapper.Map(declaredType) != declaredType {
return nil // refers to type parameter remapped by context (TODO improvement: just return the remapped param name?)
}
}
if !b.canReuseExistingJSTypeNode(node, b.getTypeFromTypeNode(node, false)) {
// fallback to serialization for jsdoc types that have insufficient or incomplete type args, or are remapped by the checker in only jsdoc contexts
// TODO: remappings like `promise` -> `Promise<any>` are static, we *could* statically remap the nodes, too. But that only matters for `isolatedDeclarations`
// in JS, should we enable that.
return nil
}
introducesError, newName, _ := trackExistingEntityName(node.AsTypeReferenceNode().TypeName, nil)
if !introducesError {
typeArguments := visitor.VisitNodes(node.AsTypeReferenceNode().TypeArguments)
return b.setTextRange(b.f.UpdateTypeReferenceNode(
node.AsTypeReferenceNode(),
newName,
typeArguments,
), node)
} else {
serializedName := b.serializeTypeName(node.AsTypeReferenceNode().TypeName, false, visitor.VisitNodes(node.AsTypeReferenceNode().TypeArguments))
if serializedName != nil {
return b.setTextRange(serializedName, node.AsTypeReferenceNode().TypeName)
}
return nil
}
}
tryVisitSimpleTypeNode = func(node *ast.Node) *ast.Node {
innerNode := ast.SkipParentheses(node)
switch innerNode.Kind {
case ast.KindTypeReference:
return tryVisitTypeReference(innerNode)
case ast.KindTypeQuery:
return tryVisitTypeQuery(innerNode)
case ast.KindIndexedAccessType:
return tryVisitIndexedAccess(innerNode)
case ast.KindTypeOperator:
if innerNode.AsTypeOperatorNode().Operator == ast.KindKeyOfKeyword {
return tryVisitKeyOf(innerNode)
}
}
return visitor.VisitNode(node)
}
visitExistingNodeTreeSymbolsWorker := func(node *ast.Node) *ast.Node {
factory := b.f
// !!! TODO: the reparser *should* make all the jsdoc remapping logic here redundant,
// assuming we only ever try to preserve reparsed nodes and never walk back to the jsdoc "originals"
// accidentally.
// Still, what can be ported of the logic is here, just in case.
// Begin JSDoc handling
if node.Kind == ast.KindJSDocTypeExpression {
// Unwrap JSDocTypeExpressions
return visitor.VisitNode(node.AsJSDocTypeExpression().Type)
}
// !!! TODO: We don't _actually_ support jsdoc namepath types, emit `any` instead; verify we handle as gracefully as strada
if node.Kind == ast.KindJSDocAllType /* || node.Kind == ast.JSDocNamepathType */ {
return factory.NewKeywordTypeNode(ast.KindAnyKeyword)
}
// !!! TODO: verify JSDocUnknwonType is hopefully just parsed into `unknown` upfront; the kind no longer exists
// if node.Kind == ast.KindJSDocUnknownType {
// return factory.NewKeywordTypeNode(ast.KindUnknownKeyword)
// }
if node.Kind == ast.KindJSDocNullableType {
unionMembers := []*ast.Node{
visitor.VisitNode(node.AsJSDocNullableType().Type),
factory.NewLiteralTypeNode(factory.NewKeywordExpression(ast.KindNullKeyword)),
}
return factory.NewUnionTypeNode(factory.NewNodeList(unionMembers))
}
if node.Kind == ast.KindJSDocOptionalType {
unionMembers := []*ast.Node{
visitor.VisitNode(node.AsJSDocOptionalType().Type),
factory.NewKeywordTypeNode(ast.KindUndefinedKeyword),
}
return factory.NewUnionTypeNode(factory.NewNodeList(unionMembers))
}
if node.Kind == ast.KindJSDocNonNullableType {
// Unwrap
return visitor.VisitNode(node.AsJSDocNonNullableType().Type)
}
if node.Kind == ast.KindJSDocVariadicType { // !!! TODO: verify this matches how jsdoc variadics are actually handled now?
return factory.NewArrayTypeNode(visitor.VisitNode(node.AsJSDocVariadicType().Type))
}
if node.Kind == ast.KindJSDocTypeLiteral {
var members []*ast.Node
for _, t := range node.AsJSDocTypeLiteral().JSDocPropertyTags {
if t.Kind != ast.KindJSDocPropertyTag && t.Kind != ast.KindJSDocParameterTag {
continue
}
n := t.Name()
var targetName *ast.Node
if ast.IsIdentifier(n) {
targetName = n
} else {
targetName = n.AsQualifiedName().Right // !!! TODO: without typesystem backup, doing this cast unguarded seems really suspect, even though it is what strada does
}
name := visitor.VisitNode(targetName)
shouldBeOptional := t.AsJSDocParameterOrPropertyTag().IsBracketed || (t.TypeExpression() != nil && t.TypeExpression().Kind == ast.KindJSDocOptionalType)
var question *ast.Node
if shouldBeOptional {
question = factory.NewToken(ast.KindQuestionToken)
}
ty := visitor.VisitNode(t.TypeExpression()) // !!! TODO: alternate lookup locations for the type? serialize on demand if it doesn't serialze? strada does something funky here.
members = append(members, factory.NewPropertySignatureDeclaration(nil, name, question, ty, nil))
}
return factory.NewTypeLiteralNode(factory.NewNodeList(members))
}
// if (ast.IsExpressionWithTypeArguments(node) || ast.IsTypeReferenceNode(node)) && ast.IsJSDocIndexSignature(node) { /// !!! TODO: JSDocIndexSignature handling hasn't been ported - readd if it's readded
// args := node.TypeArguments()
// if len(args) != 2 {
// return factory.NewKeywordTypeNode(ast.KindAnyKeyword) // shouldn't be flagged as a jsdoc index signature in the first place
// }
// return factory.NewTypeLiteralNode(factory.NewNodeList([]*ast.Node{
// factory.NewIndexSignatureDeclaration(nil, factory.NewNodeList([]*ast.Node{
// factory.NewParameterDeclaration(nil, nil, factory.NewIdentifier("x"), nil, visitor.VisitNode(args[0]), nil),
// }), visitor.VisitNode(args[1])),
// }))
// }
// if node.Kind == ast.KindJSDocFunctionType {} // !!! no longer exists
// End JSDoc handling
if ast.IsTypeReferenceNode(node) && ast.IsIdentifier(node.AsTypeReferenceNode().TypeName) && node.AsTypeReferenceNode().TypeName.AsIdentifier().Text == "" {
replacement := factory.NewKeywordTypeNode(ast.KindAnyKeyword)
b.e.SetOriginal(replacement, node)
return replacement
}
if ast.IsThisTypeNode(node) {
// TODO: strada never marks `this` type nodes as an error - it calls `canReuseTypeNode` on it, but that function always returns `true` for `this`
// type nodes, which in turn fails to verify that the `this` context is the same between the source and target locations. The conservative thing is to
// _never_ copy a `this`. We could improve this, but strada is *definitely* wrong and overbroad here. (note that we're inling uses of `canReuseTypeNode`
// in corsa because of the unfurled host structure meaning we don't need to defer to a host object for functionality it needs)
// bound.markError(nil) // conservative approach
return node
}
if ast.IsTypeParameterDeclaration(node) {
_, newName, _ := trackExistingEntityName(node.Name(), nil)
return factory.UpdateTypeParameterDeclaration(
node.AsTypeParameterDeclaration(),
visitor.VisitModifiers(node.Modifiers()),
newName,
visitor.VisitNode(node.AsTypeParameterDeclaration().Constraint),
visitor.VisitNode(node.AsTypeParameterDeclaration().Expression),
visitor.VisitNode(node.AsTypeParameterDeclaration().DefaultType),
)
}
if ast.IsIndexedAccessTypeNode(node) {
result := tryVisitIndexedAccess(node)
if result != nil {
return result
}
bound.markError(nil)
return node
}
if ast.IsTypeReferenceNode(node) {
result := tryVisitTypeReference(node)
if result != nil {
return result
}
bound.markError(nil)
return node
}
if ast.IsTypeQueryNode(node) {
result := tryVisitTypeQuery(node)
if result != nil {
return result
}
bound.markError(nil)
return node
}
if ast.IsTypeOperatorNode(node) {
if node.AsTypeOperatorNode().Operator == ast.KindUniqueKeyword && node.AsTypeOperatorNode().Type.Kind == ast.KindSymbolKeyword {
nonFakeEnclosing := b.getEnclosingDeclarationIgnoringFakeScope()
sameScope := ast.FindAncestor(node, func(a *ast.Node) bool {
return a == nonFakeEnclosing
})
if sameScope == nil {
bound.markError(nil)
return node
}
} else if node.AsTypeOperatorNode().Operator == ast.KindKeyOfKeyword {
result := tryVisitKeyOf(node)
if result != nil {
return result
}
bound.markError(nil)
return node
}
}
if ast.IsLiteralImportTypeNode(node) {
// assert keyword in imported attributes is deprecated, so we don't reuse types that contain it
// Ex: import("pkg", { assert: {} }
if node.AsImportTypeNode().Attributes != nil && node.AsImportTypeNode().Attributes.AsImportAttributes().Token == ast.KindAssertKeyword {
bound.markError(nil)
return node
}
t := b.getTypeFromTypeNode(node, true)
if t == nil {
bound.markError(nil)
return node
}
if ast.IsInJSFile(node) {
// !!! TODO: invalidate node reuse if js fallback logic used in type param list/typeof lookup (but isn't this logic gone?)
// s := b.ch.symbolNodeLinks.Get(node).resolvedSymbol
}
originalSpec := node.AsImportTypeNode().Argument.AsLiteralTypeNode().Literal
specifier := b.rewriteModuleSpecifier(node, originalSpec)
if originalSpec == specifier {
specifier = visitor.VisitNode(specifier) // visit node if not replaced
}
arg := node.AsImportTypeNode().Argument
if specifier != originalSpec {
arg = factory.NewLiteralTypeNode(specifier)
}
return factory.UpdateImportTypeNode(
node.AsImportTypeNode(),
node.AsImportTypeNode().IsTypeOf,
arg,
visitor.VisitNode(node.AsImportTypeNode().Attributes),
visitor.VisitNode(node.AsImportTypeNode().Qualifier),
visitor.VisitNodes(node.AsImportTypeNode().TypeArguments),
)
}
if node.Name() != nil && node.Name().Kind == ast.KindComputedPropertyName && !b.ch.hasLateBindableName(node) {
if !ast.HasDynamicName(node) {
// !!! TODO: This matches strada, but rather than recursing, this should probably fall down to later cases.
// Take a `["field"]` property declaration - it still needs a `: any` appended to it
return visitor.VisitEachChild(node)
}
// !!! TODO: this condition matches strada, but it just seems wrong? Or at the very least extraordinarily approximate, and doesn't flag a builder error...
shouldRemoveDeclaration := !((b.ctx.internalFlags&nodebuilder.InternalFlagsAllowUnresolvedNames != 0) && ast.IsEntityNameExpression(node.Name().AsComputedPropertyName().Expression) && (b.ch.checkComputedPropertyName(node.Name()).flags&TypeFlagsAny != 0))
if shouldRemoveDeclaration {
return nil
}
}
if (ast.IsFunctionLike(node) && node.Type() == nil) || (ast.IsPropertyDeclaration(node) && node.Type() == nil && node.Initializer() == nil) || (ast.IsPropertySignatureDeclaration(node) && node.Type() == nil && node.Initializer() == nil) || (ast.IsParameterDeclaration(node) && node.Type() == nil && node.Initializer() == nil) {
visited := visitor.VisitEachChild(node)
if visited == node {
visited = b.setTextRange(node.Clone(factory), node)
}
node = visited
newType := factory.NewKeywordTypeNode(ast.KindAnyKeyword)
switch node.Kind {
case ast.KindPropertyDeclaration:
return factory.UpdatePropertyDeclaration(
node.AsPropertyDeclaration(),
node.Modifiers(),
node.Name(),
node.PostfixToken(),
newType,
nil,
)
case ast.KindPropertySignature:
return factory.UpdatePropertySignatureDeclaration(
node.AsPropertySignatureDeclaration(),
node.Modifiers(),
node.Name(),
node.PostfixToken(),
newType,
nil,
)
case ast.KindParameter:
return factory.UpdateParameterDeclaration(
node.AsParameterDeclaration(),
nil,
node.AsParameterDeclaration().DotDotDotToken,
node.Name(),
node.AsParameterDeclaration().QuestionToken,
newType,
nil,
)
case ast.KindMethodSignature:
return factory.UpdateMethodSignatureDeclaration(
node.AsMethodSignatureDeclaration(),
node.Modifiers(),
node.Name(),
node.AsMethodSignatureDeclaration().PostfixToken,
node.AsMethodSignatureDeclaration().TypeParameters,
node.AsMethodSignatureDeclaration().Parameters,
newType,
)
case ast.KindCallSignature:
return factory.UpdateCallSignatureDeclaration(
node.AsCallSignatureDeclaration(),
node.AsCallSignatureDeclaration().TypeParameters,
node.AsCallSignatureDeclaration().Parameters,
newType,
)
case ast.KindJSDocSignature:
return factory.UpdateJSDocSignature(
node.AsJSDocSignature(),
node.AsJSDocSignature().TypeParameters,
node.AsJSDocSignature().Parameters,
newType,
)
case ast.KindConstructSignature:
return factory.UpdateConstructSignatureDeclaration(
node.AsConstructSignatureDeclaration(),
node.AsConstructSignatureDeclaration().TypeParameters,
node.AsConstructSignatureDeclaration().Parameters,
newType,
)
case ast.KindIndexSignature:
return factory.UpdateIndexSignatureDeclaration(
node.AsIndexSignatureDeclaration(),
node.Modifiers(),
node.AsIndexSignatureDeclaration().Parameters,
newType,
)
case ast.KindFunctionType:
return factory.UpdateFunctionTypeNode(
node.AsFunctionTypeNode(),
node.AsFunctionTypeNode().TypeParameters,
node.AsFunctionTypeNode().Parameters,
newType,
)
case ast.KindConstructorType:
return factory.UpdateConstructorTypeNode(
node.AsConstructorTypeNode(),
node.Modifiers(),
node.AsConstructorTypeNode().TypeParameters,
node.AsConstructorTypeNode().Parameters,
newType,
)
}
}
if ast.IsComputedPropertyName(node) && ast.IsEntityNameExpression(node.AsComputedPropertyName().Expression) {
introducesError, result, _ := trackExistingEntityName(node.AsComputedPropertyName().Expression, nil)
if !introducesError {
return factory.UpdateComputedPropertyName(node.AsComputedPropertyName(), result)
} else {
// !!! TODO: rewriting computed names based on evaluator/typecheck results?
// strada's behavior seems hard to justify vs marking an error and moving on
bound.markError(nil)
return visitor.VisitEachChild(node)
}
}
if ast.IsTypePredicateNode(node) {
var parameterName *ast.Node
if ast.IsIdentifier(node.AsTypePredicateNode().ParameterName) {
introducesError, result, _ := trackExistingEntityName(node.AsTypePredicateNode().ParameterName, nil)
// Should not usually happen the only case is when a type predicate comes from a JSDoc type annotation with it's own parameter symbol definition.
// /** @type {(v: unknown) => v is undefined} */
// const isUndef = v => v === undefined;
if introducesError {
bound.markError(nil)
}
parameterName = result
} else {
parameterName = node.AsTypePredicateNode().ParameterName.Clone(factory)
}
return factory.UpdateTypePredicateNode(
node.AsTypePredicateNode(),
visitor.VisitNode(node.AsTypePredicateNode().AssertsModifier),
parameterName,
visitor.VisitNode(node.AsTypePredicateNode().Type),
)
}
if ast.IsConditionalTypeNode(node) {
checkType := visitor.VisitNode(node.AsConditionalTypeNode().CheckType)
dispose := b.enterNewScope(node, nil, b.ch.getInferTypeParameters(node), nil, nil)
extendsType := visitor.VisitNode(node.AsConditionalTypeNode().ExtendsType)
trueType := visitor.VisitNode(node.AsConditionalTypeNode().TrueType)
dispose()
falseType := visitor.VisitNode(node.AsConditionalTypeNode().FalseType)
return factory.UpdateConditionalTypeNode(
node.AsConditionalTypeNode(),
checkType,
extendsType,
trueType,
falseType,
)
}
// style applications
if ast.IsTupleTypeNode(node) || (b.ctx.flags&nodebuilder.FlagsMultilineObjectLiterals == 0 && ast.IsTypeLiteralNode(node)) || ast.IsMappedTypeNode(node) {
// make tuples/types/mappedtypes single line
res := visitor.VisitEachChild(node)
if res == node {
res = res.Clone(factory)
res = b.setTextRange(res, node)
}
b.e.AddEmitFlags(res, printer.EFSingleLine)
return res
}
if ast.IsStringLiteralLike(node) {
// Preserve the original characters of the literal (e.g. emojis) in declaration emit
// rather than escaping them as ASCII Unicode escapes. Mirrors TypeScript's behavior
// for synthesized string literal types in the node builder (checker.ts:6853).
c := node.Clone(b.f)
if ast.IsStringLiteral(node) && b.ctx.flags&nodebuilder.FlagsUseSingleQuotesForStringLiteralType != 0 && node.AsStringLiteral().TokenFlags&ast.TokenFlagsSingleQuote == 0 {
// set single quote on string literals
c.AsStringLiteral().TokenFlags ^= ast.TokenFlagsSingleQuote
}
b.e.AddEmitFlags(c, printer.EFNoAsciiEscaping)
return c
}
return visitor.VisitEachChild(node)
}
nonLocalNode := true
visitor = ast.NewNodeVisitor(func(node *ast.Node) *ast.Node {
// If there was an error in a sibling node bail early, the result will be discarded anyway
if bound.hadError {
return node
}
recover_ := bound.startRecoveryScope()
introducesNewScope := ast.IsFunctionLike(node) || ast.IsMappedTypeNode(node)
var exit func()
if introducesNewScope {
var params []*ast.Symbol
var typeParams []*Type
if ast.IsFunctionLike(node) {
sig := b.ch.getSignatureFromDeclaration(node)
params = sig.parameters
typeParams = sig.typeParameters
} else if ast.IsConditionalTypeNode(node) { // !!! TODO: impossible in combination with the scope start check???
typeParams = b.ch.getInferTypeParameters(node)
} else if ast.IsMappedTypeNode(node) {
typeParams = []*Type{b.ch.getDeclaredTypeOfTypeParameter(b.ch.getSymbolOfDeclaration(node.AsMappedTypeNode().TypeParameter))}
}
exit = b.enterNewScope(node, params, typeParams, nil, nil)
}
result := visitExistingNodeTreeSymbolsWorker(node)
if exit != nil {
exit()
}
if result == node && !ast.NodeIsSynthesized(node) {
result = b.f.DeepCloneNode(node) // always clone a new node
}
// We want to clone the subtree, so when we mark it up with __pos and __end in quickfixes,
// we don't get odd behavior because of reused nodes. We also need to clone to _remove_
// the position information if the node comes from a different file than the one the node builder
// is set to build for (even though we are reusing the node structure, the position information
// would make the printer print invalid spans for literals and identifiers, and the formatter would
// choke on the mismatched positonal spans between a parent and an injected child from another file).
result = b.setTextRange(result, node)
if bound.hadError {
if ast.IsTypeNode(node) && !ast.IsTypePredicateNode(node) {
bound.endRecoveryScope(recover_)
// TODO: this fallback matches strada behavior, but it lacks any verification that the type from `node` actually matches
// the type we'd expect at this traversal position within the parent type.
t := b.getTypeFromTypeNode(node, false)
return b.typeToTypeNode(t)
}
return b.setTextRange(node.Clone(b.f), node)
}
return result
}, b.f, ast.NodeVisitorHooks{
VisitNodes: func(nodes *ast.NodeList, v *ast.NodeVisitor) *ast.NodeList {
res := v.VisitNodes(nodes)
if nonLocalNode && res != nil {
// Remove position data from node lists originating in other files
if res == nodes {
res = nodes.Clone(b.f)
}
res.Loc = core.NewTextRange(-1, -1)
}
return res
},
VisitNode: func(node *ast.Node, v *ast.NodeVisitor) *ast.Node {
// Capture if the current node is in the current file so node lists knoww if they can keep positions or not
oldNonLocalNode := nonLocalNode
nonLocalNode = b.ctx.enclosingFile == nil || b.ctx.enclosingFile != ast.GetSourceFileOfNode(b.e.MostOriginal(node))
res := v.VisitNode(node)
nonLocalNode = oldNonLocalNode
return res
},
})
return visitor
}