Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6a0c97c
Don't crash reporting a too large tuple outside of checking
jakebailey Nov 3, 2026
edda674
Check assertion operands once, with their normal type
jakebailey Oct 19, 2026
cdda47f
Derive binding pattern parent types without peeking at caches
jakebailey Oct 19, 2026
a0a4b3c
Make contextual typing of functions independent of query order
jakebailey Oct 19, 2026
9359276
Resolve calls in source order and locate their errors at the call
jakebailey Oct 19, 2026
3dd440a
Do not cache results that depend on who asked first
jakebailey Oct 19, 2026
3a931ab
Report unresolved names after resolution completes
jakebailey Oct 19, 2026
e1cbdad
Make flow analysis of a reference independent of other references
jakebailey Oct 19, 2026
5370726
Track computed property name cycles on the resolution stack
jakebailey Oct 19, 2026
48de196
Order and intern types by content instead of by type id
jakebailey Oct 20, 2026
fa66634
Cache relation results only once their assumptions are resolved
jakebailey Oct 20, 2026
7e5b1eb
Look through instantiation when reusing a parameter name
jakebailey Oct 22, 2026
4e16571
Check computed enum initializers when checking the member
jakebailey Oct 22, 2026
d94bd82
Instantiate symbols whether or not their types are known
jakebailey Nov 7, 2026
1829839
Keep declaration emit's visibility marks apart from cached visibility
jakebailey Nov 7, 2026
7cbfb9c
Do not count names resolved to print a type as uses
jakebailey Nov 7, 2026
693e4bd
Do not let names in JSDoc keep imports
jakebailey Nov 7, 2026
01d179f
Resolve the type of an accessor before the return type of its getter
jakebailey Nov 7, 2026
01e2291
Show accessors in inlay hints for types
jakebailey Nov 8, 2026
47d9b42
Keep type arguments when printing a member of an instantiated local type
jakebailey Nov 8, 2026
c4eedea
Decide restrictive type parameters from their declarations
jakebailey Nov 8, 2026
08d0215
Find the contextual type of a node from the innermost check of it
jakebailey Nov 8, 2026
1738f23
Reuse cached comparisons the same way wherever they are reused
jakebailey Nov 8, 2026
95dcf3b
Keep the narrowing of a type variable in a conditional's true branch
jakebailey Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
719 changes: 611 additions & 108 deletions tsc/internal/checker/checker.go

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions tsc/internal/checker/emitresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ type JSXLinks struct {

// Links for declarations

// DeclarationLinks hold whether a declaration is visible. Declaration emit
// makes the declarations it depends on visible as it goes, so that is kept
// apart from what the rules of the declaration say, which is all that is
// cached: a declaration found invisible before emit made it or its container
// visible is still found visible afterwards.
type DeclarationLinks struct {
isVisible core.Tristate // if declaration is depended upon by exported declarations
madeVisible bool // Declaration emit depends on the declaration
ruleKnown bool // Whether ruleVisible and ruleInheritsFrom are known
ruleVisible bool // Whether the declaration is visible by its own rules
ruleInheritsFrom *ast.Node // A declaration it is as visible as, if any
}

type DeclarationFileLinks struct {
Expand Down Expand Up @@ -186,7 +194,7 @@ func (r *EmitResolver) markLinkedAliases(node *ast.Node) {

var nextSymbol *ast.Symbol
for _, declaration := range exportSymbol.Declarations() {
r.checker.emitResolverLinks.declarationLinks.Get(declaration).isVisible = core.TSTrue
r.checker.emitResolverLinks.declarationLinks.Get(declaration).madeVisible = true

if ast.IsInternalModuleImportEqualsDeclaration(declaration) {
// Add the referenced top container visible
Expand Down
64 changes: 38 additions & 26 deletions tsc/internal/checker/emitsupport.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"slices"

"github.com/microsoft/TypeScript/tsc/internal/ast"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/printer"
)

Expand All @@ -17,27 +16,40 @@ func (c *Checker) isDeclarationVisible(node *ast.Node) bool {
return false
}

links := c.emitResolverLinks.declarationLinks.Get(node)
if links.isVisible == core.TSUnknown {
if c.determineIfDeclarationIsVisible(node) {
links.isVisible = core.TSTrue
} else {
links.isVisible = core.TSFalse
for {
links := c.emitResolverLinks.declarationLinks.Get(node)
if links.madeVisible {
return true
}
if !links.ruleKnown {
links.ruleVisible, links.ruleInheritsFrom = c.determineIfDeclarationIsVisible(node)
links.ruleKnown = true
}
if links.ruleInheritsFrom == nil {
return links.ruleVisible
}
node = links.ruleInheritsFrom
if !ast.IsParseTreeNode(node) {
return false
}
}
return links.isVisible == core.TSTrue
}

func (c *Checker) determineIfDeclarationIsVisible(node *ast.Node) bool {
// determineIfDeclarationIsVisible returns whether node is visible by the rules
// of its declaration alone, or, if those make it as visible as another
// declaration, that declaration. Which declarations are visible depends on
// what declaration emit makes visible (see DeclarationLinks), so whether
// another declaration is visible is not part of the result.
func (c *Checker) determineIfDeclarationIsVisible(node *ast.Node) (visible bool, inheritsFrom *ast.Node) {
switch node.Kind {
case ast.KindJSDocCallbackTag,
// ast.KindJSDocEnumTag, // !!! TODO: JSDoc @enum support?
ast.KindJSDocTypedefTag:
// Top-level jsdoc type aliases are considered exported
// First parent is comment node, second is hosting declaration or token; we only care about those tokens or declarations whose parent is a source file
return node.Parent != nil && node.Parent.Parent != nil && node.Parent.Parent.Parent != nil && ast.IsSourceFile(node.Parent.Parent.Parent)
return node.Parent != nil && node.Parent.Parent != nil && node.Parent.Parent.Parent != nil && ast.IsSourceFile(node.Parent.Parent.Parent), nil
case ast.KindBindingElement:
return c.isDeclarationVisible(node.Parent.Parent)
return false, node.Parent.Parent
case ast.KindVariableDeclaration,
ast.KindModuleDeclaration,
ast.KindClassDeclaration,
Expand All @@ -51,23 +63,23 @@ func (c *Checker) determineIfDeclarationIsVisible(node *ast.Node) bool {
if ast.IsBindingPattern(node.Name()) &&
len(node.Name().Elements()) == 0 {
// If the binding pattern is empty, this variable declaration is not visible
return false
return false, nil
}
// falls through
}
// External module augmentation is always visible
// A @typedef at top-level in an external module is always visible
if ast.IsExternalModuleAugmentation(node) || ast.IsImplicitlyExportedJSDocDeclaration(node) {
return true
return true, nil
}
parent := ast.GetDeclarationContainer(node)
// If the node is not exported or it is not ambient module element (except import declaration)
if c.getCombinedModifierFlagsCached(node)&ast.ModifierFlagsExport == 0 &&
!(node.Kind != ast.KindImportEqualsDeclaration && parent.Kind != ast.KindSourceFile && parent.Flags&ast.NodeFlagsAmbient != 0) {
return ast.IsGlobalSourceFile(parent)
return ast.IsGlobalSourceFile(parent), nil
}
// Exported members/ambient module elements (exception import declaration) are visible if parent is visible
return c.isDeclarationVisible(parent)
return false, parent

case ast.KindPropertyDeclaration,
ast.KindPropertySignature,
Expand All @@ -77,10 +89,10 @@ func (c *Checker) determineIfDeclarationIsVisible(node *ast.Node) bool {
ast.KindMethodSignature:
if c.GetEffectiveDeclarationFlags(node, ast.ModifierFlagsPrivate|ast.ModifierFlagsProtected) != 0 {
// Private/protected properties/methods are not visible
return false
return false, nil
}
// Public properties/methods are visible if its parents are visible, so:
return c.isDeclarationVisible(node.Parent)
return false, node.Parent

case ast.KindConstructor,
ast.KindConstructSignature,
Expand All @@ -98,38 +110,38 @@ func (c *Checker) determineIfDeclarationIsVisible(node *ast.Node) bool {
ast.KindIntersectionType,
ast.KindParenthesizedType,
ast.KindNamedTupleMember:
return c.isDeclarationVisible(node.Parent)
return false, node.Parent

// Default binding, import specifier and namespace import is visible
// only on demand so by default it is not visible
case ast.KindImportClause,
ast.KindNamespaceImport,
ast.KindImportSpecifier:
return false
return false, nil

// Type parameters are always visible
case ast.KindTypeParameter:
return true
return true, nil
// Source file and namespace export are always visible
case ast.KindSourceFile,
ast.KindNamespaceExportDeclaration:
return true
return true, nil

// Export assignments do not create name bindings outside the module
case ast.KindExportAssignment:
return false
return false, nil

// An `export {X}` (without a module specifier) is itself a visible re-export of
// the named binding; it contributes to the symbol's external visibility.
case ast.KindExportSpecifier:
exportDecl := node.Parent.Parent
if ast.IsExportDeclaration(exportDecl) && exportDecl.AsExportDeclaration().ModuleSpecifier == nil {
return c.isDeclarationVisible(exportDecl.Parent)
return false, exportDecl.Parent
}
return false
return false, nil

default:
return false
return false, nil
}
}

Expand Down Expand Up @@ -205,7 +217,7 @@ func (c *Checker) hasVisibleDeclarations(symbol *ast.Symbol, shouldComputeAliasT
var addVisibleAlias func(declaration *ast.Node, aliasingStatement *ast.Node)
if shouldComputeAliasToMakeVisible {
addVisibleAlias = func(declaration *ast.Node, aliasingStatement *ast.Node) {
c.emitResolverLinks.declarationLinks.Get(declaration).isVisible = core.TSTrue
c.emitResolverLinks.declarationLinks.Get(declaration).madeVisible = true
if aliasesToMakeVisibleSet == nil {
aliasesToMakeVisibleSet = make(map[ast.NodeId]*ast.Node)
}
Expand Down
18 changes: 11 additions & 7 deletions tsc/internal/checker/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ func (c *Checker) getFlowTypeOfReference(reference *ast.Node, declaredType *Type
}

func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Type, initialType *Type, flowContainer *ast.Node, flowNode *ast.FlowNode) *Type {
if c.flowAnalysisDisabled {
return c.errorType
}
if flowNode == nil {
flowNode = getFlowNodeOfNode(reference)
if flowNode == nil {
Expand Down Expand Up @@ -117,11 +114,11 @@ func (c *Checker) getFlowTypeOfReferenceEx(reference *ast.Node, declaredType *Ty
func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType {
if f.depth == 2000 {
// We have made 2000 recursive invocations. To avoid overflowing the call stack we report an error
// and disable further control flow analysis in the containing function or module body.
// and give up on this reference. Each reference hits the limit on its own, so its type does not depend
// on which other references were analyzed first.
if tr := c.tracer; tr != nil {
tr.Instant(tracing.PhaseCheckTypes, "getTypeAtFlowNode_DepthLimit", map[string]any{"depth": f.depth})
}
c.flowAnalysisDisabled = true
c.reportFlowControlError(f.reference)
return FlowType{t: c.errorType}
}
Expand All @@ -134,9 +131,9 @@ func (c *Checker) getTypeAtFlowNode(f *FlowState, flow *ast.FlowNode) FlowType {
// the same getFlowTypeOfReference invocation. A node is considered shared when it is the
// antecedent of more than one node.
for i := f.sharedFlowStart; i < len(c.sharedFlows); i++ {
if c.sharedFlows[i].flow == flow {
if s := &c.sharedFlows[i]; s.flow == flow {
f.depth--
return c.sharedFlows[i].flowType
return s.flowType
}
}
sharedFlow = flow
Expand Down Expand Up @@ -1364,12 +1361,19 @@ func (c *Checker) getTypeAtFlowLoopLabel(f *FlowState, flow *ast.FlowNode) FlowT
} else {
// All but the first antecedent are the looping control flow paths that lead
// back to the loop junction. We track these on the flow loop stack.
sharedFlowStart := len(c.sharedFlows)
c.flowLoopStack = append(c.flowLoopStack, FlowLoopInfo{key: key, types: antecedentTypes})
saveFlowTypeCache := c.flowTypeCache
c.flowTypeCache = nil
flowType = c.getTypeAtFlowNode(f, list.Flow)
c.flowTypeCache = saveFlowTypeCache
c.flowLoopStack = c.flowLoopStack[:len(c.flowLoopStack)-1]
// An incomplete type computed under the back edge reflects the loop's
// partial types, so it is not valid once the back edge is exited.
// Reusing it would make the result depend on which reference was
// analyzed first.
kept := slices.DeleteFunc(c.sharedFlows[sharedFlowStart:], func(s SharedFlow) bool { return s.flowType.incomplete })
c.sharedFlows = c.sharedFlows[:sharedFlowStart+len(kept)]
// If we see a value appear in the cache it is a sign that control flow analysis
// was restarted and completed by checkExpressionCached. We can simply pick up
// the resulting type and bail out.
Expand Down
36 changes: 33 additions & 3 deletions tsc/internal/checker/nodebuilderimpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ func (b *NodeBuilderImpl) checkTypeExpandability(t *Type) {
}

func (b *NodeBuilderImpl) appendReferenceToType(root *ast.TypeNode, ref *ast.TypeNode) *ast.TypeNode {
if !ast.IsImportTypeNode(root) && (ast.IsExpressionWithTypeArguments(root) || ast.IsExpressionWithTypeArguments(ref)) {
// Type arguments precede the last name of root or ref, so the
// reference is an expression with type arguments rather than a
// qualified name.
return b.appendReferenceToExpression(b.referenceToExpression(root), b.referenceToExpression(ref))
}
if ast.IsImportTypeNode(root) {
// first shift type arguments

Expand Down Expand Up @@ -323,6 +329,30 @@ func (b *NodeBuilderImpl) appendReferenceToType(root *ast.TypeNode, ref *ast.Typ
return expr
}

// referenceToExpression returns a type reference, or a reference that is
// already an expression, as an expression.
func (b *NodeBuilderImpl) referenceToExpression(ref *ast.Node) *ast.Expression {
if ast.IsTypeReferenceNode(ref) {
return b.createExpressionWithTypeArguments(b.createAccessExpression(ref.AsTypeReferenceNode().TypeName), ref.TypeArgumentList())
}
return b.createAccessExpression(ref)
}

// appendReferenceToExpression returns ref, a reference expression, as a
// reference to a member of root.
func (b *NodeBuilderImpl) appendReferenceToExpression(root *ast.Expression, ref *ast.Expression) *ast.Expression {
switch {
case ast.IsIdentifier(ref):
return b.f.NewPropertyAccessExpression(root, nil /*questionDotToken*/, ref, ast.NodeFlagsNone)
case ast.IsPropertyAccessExpression(ref):
return b.f.NewPropertyAccessExpression(b.appendReferenceToExpression(root, ref.Expression()), nil /*questionDotToken*/, ref.Name(), ast.NodeFlagsNone)
case ast.IsExpressionWithTypeArguments(ref):
return b.f.NewExpressionWithTypeArguments(b.appendReferenceToExpression(root, ref.Expression()), ref.AsExpressionWithTypeArguments().TypeArguments)
default:
panic("unexpected reference kind: " + ref.Kind.String())
}
}

func getAccessStack(ref *ast.Node) []*ast.Node {
var state *ast.Node = ref.AsTypeReferenceNode().TypeName
ids := []*ast.Node{}
Expand Down Expand Up @@ -445,7 +475,7 @@ func (b *NodeBuilderImpl) serializeTypeName(node *ast.Node, isTypeOf bool, typeA
if isTypeOf {
meaning = ast.SymbolFlagsValue
}
symbol := b.ch.resolveEntityName(node, meaning, true, false, node)
symbol := b.ch.resolveEntityNameEx(node, meaning, true, false, node, false /*use*/)
if symbol == nil {
return nil
}
Expand Down Expand Up @@ -2449,12 +2479,12 @@ func (b *NodeBuilderImpl) shouldUsePlaceholderForProperty(propertySymbol *ast.Sy
func (b *NodeBuilderImpl) trackComputedName(accessExpression *ast.Node, enclosingDeclaration *ast.Node) {
// get symbol of the first identifier of the entityName
firstIdentifier := ast.GetFirstIdentifier(accessExpression)
name := b.ch.resolveName(enclosingDeclaration, firstIdentifier.Text(), ast.SymbolFlagsValue|ast.SymbolFlagsExportValue, nil /*nameNotFoundMessage*/, true /*isUse*/, false)
name := b.ch.resolveName(enclosingDeclaration, firstIdentifier.Text(), ast.SymbolFlagsValue|ast.SymbolFlagsExportValue, nil /*nameNotFoundMessage*/, false /*isUse*/, false)
if name != nil {
b.ctx.tracker.TrackSymbol(name, enclosingDeclaration, ast.SymbolFlagsValue)
} else {
// Name does not resolve at target location, track symbol at dest location (should be inaccessible)
fallback := b.ch.resolveName(firstIdentifier, firstIdentifier.Text(), ast.SymbolFlagsValue|ast.SymbolFlagsExportValue, nil /*nameNotFoundMessage*/, true /*isUse*/, false)
fallback := b.ch.resolveName(firstIdentifier, firstIdentifier.Text(), ast.SymbolFlagsValue|ast.SymbolFlagsExportValue, nil /*nameNotFoundMessage*/, false /*isUse*/, false)
if fallback != nil {
b.ctx.tracker.TrackSymbol(fallback, enclosingDeclaration, ast.SymbolFlagsValue)
}
Expand Down
10 changes: 6 additions & 4 deletions tsc/internal/checker/nodecopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,22 +340,24 @@ func getExistingNodeTreeVisitor(b *NodeBuilderImpl, bound *recoveryBoundary) *as
}
return introducesError, attachSymbolToLeftmostIdentifier(leftmost, node, sym), nil
}
sym = b.ch.resolveEntityName(leftmost, meaning, true, true, nil)
sym = b.ch.resolveEntityNameEx(leftmost, meaning, true, true, nil, false /*use*/)
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)
symAtLocation := b.ch.resolveEntityNameEx(leftmost, meaning, true, true, b.ctx.enclosingDeclaration, false /*use*/)
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) {
// If the symbol is found both in declaration scope and in current scope then it should point to the same reference.
// A parameter in scope may be an instantiation of the one in the declaration, depending on whether
// its type was resolved before its signature was instantiated, so compare what they instantiate.
(symAtLocation != nil && sym != nil && b.ch.getSymbolIfSameReference(b.ch.getTargetSymbol(b.ch.getExportSymbolOfValueSymbolIfExported(symAtLocation)), b.ch.getTargetSymbol(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)
Expand Down
8 changes: 8 additions & 0 deletions tsc/internal/checker/pseudotypenodebuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,14 @@ func (b *NodeBuilderImpl) pseudoTypeToType(t *pseudochecker.PseudoType) *Type {
return b.ch.getReturnTypeOfSignature(b.ch.getSignatureFromDeclaration(node))
}
ty := b.ch.getWidenedType(b.ch.getRegularTypeOfExpression(node))
if ast.IsPropertyAssignment(node.Parent) {
// pseudoTypeToNode serializes the type of the property's symbol, which is
// not widened in the context of its containing object literal.
declared := b.ch.getWidenedLiteralType(b.ch.getTypeOfSymbol(b.ch.getSymbolOfDeclaration(node.Parent)))
if declared != ty {
return nil
}
}
return ty
case pseudochecker.PseudoTypeKindNoResult:
return nil // TODO: extract type selection logic from `serializeTypeForDeclaration`, not needed for current usecases but needed if completeness becomes required
Expand Down
Loading
Loading