901 lines
36 KiB
Go
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
|
|
}
|