From 2545a54d3853f0257ca232dfa4184f1bfd3c2733 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:44:16 -0700 Subject: [PATCH] Skip non-scope ancestors quickly in NameResolver.Resolve Resolve walks every ancestor of the reference location, and for each one it fetches locals, evaluates getIsDeferredContext, runs a large switch on the kind, and checks isSelfReferenceLocation. Most ancestors (expressions, statements, type nodes, and so on) are not scopes, so all of that work does nothing for them. Classify node kinds up front: kinds that the loop body never acts on are climbed past directly, and simple block-like scopes (blocks, loops, case blocks, catch clauses, mapped types) only need a plain lookup in their locals. Everything else, including any locals container not explicitly listed, still runs the full loop body, so resolution results are unchanged. A `const` name keeps using the full path since every ancestor must be checked for a const assertion. Co-authored-by: Steven Chong <25894545+teamchong@users.noreply.github.com> --- tsc/internal/ast/ast.go | 4 ++ tsc/internal/binder/nameresolver.go | 71 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/tsc/internal/ast/ast.go b/tsc/internal/ast/ast.go index 48ff1a8d63d56..5f87aba82ec8e 100644 --- a/tsc/internal/ast/ast.go +++ b/tsc/internal/ast/ast.go @@ -1458,6 +1458,10 @@ func IsLocalsContainer(node *Node) bool { return node.LocalsContainerData() != nil } +func IsLocalsContainerKind(kind Kind) bool { + return nodeLocalsContainerDataDispatch[kind] != 0 +} + // FunctionLikeBase func (node *FunctionLikeBase) LocalsContainerData() *LocalsContainerBase { diff --git a/tsc/internal/binder/nameresolver.go b/tsc/internal/binder/nameresolver.go index 4d0db524946cb..82386d35178c9 100644 --- a/tsc/internal/binder/nameresolver.go +++ b/tsc/internal/binder/nameresolver.go @@ -22,6 +22,55 @@ type NameResolver struct { OnSuccessfullyResolvedSymbol func(location *ast.Node, result *ast.Symbol, meaning ast.SymbolFlags, lastLocation *ast.Node, associatedDeclarationForContainingInitializerOrBindingName *ast.Node, withinDeferredContext bool) } +type resolveStep uint8 + +const ( + // Nothing in the body of the Resolve loop applies to this kind; just move to the parent. + resolveStepSkip resolveStep = iota + // The only applicable work is an unrestricted lookup in the node's locals. + resolveStepLocalsOnly + // The full body of the Resolve loop must run. + resolveStepFull +) + +// resolveSteps classifies each node kind by the work Resolve must do when it reaches a node of that kind. +// Every kind that is a locals container, matched by the switch in Resolve, checked by getIsDeferredContext, +// or matched by isSelfReferenceLocation must not be resolveStepSkip. +var resolveSteps = func() (steps [ast.KindCount]resolveStep) { + for kind := range ast.KindCount { + if ast.IsLocalsContainerKind(kind) { + steps[kind] = resolveStepFull + } + } + for _, kind := range []ast.Kind{ + ast.KindBlock, + ast.KindForStatement, + ast.KindForInStatement, + ast.KindForOfStatement, + ast.KindCaseBlock, + ast.KindCatchClause, + ast.KindMappedType, + } { + steps[kind] = resolveStepLocalsOnly + } + for _, kind := range []ast.Kind{ + ast.KindEnumDeclaration, + ast.KindPropertyDeclaration, + ast.KindInterfaceDeclaration, + ast.KindExpressionWithTypeArguments, + ast.KindComputedPropertyName, + ast.KindDecorator, + ast.KindParameter, + ast.KindBindingElement, + ast.KindInferType, + ast.KindExportSpecifier, + ast.KindTypeQuery, + } { + steps[kind] = resolveStepFull + } + return steps +}() + func (r *NameResolver) Resolve(location *ast.Node, name string, meaning ast.SymbolFlags, nameNotFoundMessage *diagnostics.Message, isUse bool, excludeGlobals bool) *ast.Symbol { var result *ast.Symbol var lastLocation *ast.Node @@ -34,6 +83,28 @@ func (r *NameResolver) Resolve(location *ast.Node, name string, meaning ast.Symb nameIsConst := name == "const" loop: for location != nil { + // Most ancestors are not scopes and need no work, so climb past them without running the full loop body. + // A `const` name must check every ancestor for a const assertion, so it always takes the full path. + if !nameIsConst { + for { + step := resolveSteps[location.Kind] + if step == resolveStepFull { + break + } + if step == resolveStepLocalsOnly { + if locals := location.Locals(); locals != nil { + if result = r.lookup(locals, name, meaning); result != nil { + break loop + } + } + } + lastLocation = location + location = location.Parent + if location == nil { + break loop + } + } + } if nameIsConst && ast.IsConstAssertion(location) { // `const` in an `as const` has no symbol, but issues no error because there is no *actual* lookup of the type // (it refers to the constant type of the expression instead)