Skip to content

Type nodes for JSDoc #1013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 9, 2025
Merged
Changes from all commits
Commits
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
114 changes: 70 additions & 44 deletions internal/ast/ast.go

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions internal/ast/utilities.go
Original file line number Diff line number Diff line change
@@ -656,6 +656,7 @@ func isDeclarationStatementKind(kind Kind) bool {
KindExportDeclaration,
KindExportAssignment,
KindJSExportAssignment,
KindCommonJSExport,
KindNamespaceExportDeclaration:
return true
}
@@ -863,6 +864,15 @@ func WalkUpParenthesizedTypes(node *TypeNode) *Node {
return node
}

func GetEffectiveTypeParent(parent *Node) *Node {
if IsInJSFile(parent) && parent.Kind == KindJSDocTypeExpression {
if host := parent.AsJSDocTypeExpression().Host; host != nil {
parent = host
}
}
return parent
}

// Walks up the parents of a node to find the containing SourceFile
func GetSourceFileOfNode(node *Node) *SourceFile {
for node != nil {
@@ -3403,6 +3413,7 @@ func ReplaceModifiers(factory *NodeFactory, node *Node, modifierArray *ModifierL
return factory.UpdateExportAssignment(
node.AsExportAssignment(),
modifierArray,
node.Type(),
node.Expression(),
)
case KindExportDeclaration:
5 changes: 4 additions & 1 deletion internal/binder/binder.go
Original file line number Diff line number Diff line change
@@ -1653,7 +1653,7 @@ func (b *Binder) bindChildren(node *ast.Node) {
b.inAssignmentPattern = saveInAssignmentPattern
b.bindEachChild(node)
case ast.KindJSExportAssignment, ast.KindCommonJSExport:
return // Reparsed nodes do not double-bind children, which are not reparsed
// Reparsed nodes do not double-bind children, which are not reparsed
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an improvement--now setJSDocParents runs and inAssignmentPattern correctly follows dynamic scope

default:
b.bindEachChild(node)
}
@@ -2214,9 +2214,11 @@ func (b *Binder) bindDestructuringAssignmentFlow(node *ast.Node) {
b.bind(expr.Right)
b.inAssignmentPattern = true
b.bind(expr.Left)
b.bind(expr.Type)
} else {
b.inAssignmentPattern = true
b.bind(expr.Left)
b.bind(expr.Type)
b.inAssignmentPattern = false
b.bind(expr.OperatorToken)
b.bind(expr.Right)
@@ -2245,6 +2247,7 @@ func (b *Binder) bindBinaryExpressionFlow(node *ast.Node) {
}
} else {
b.bind(expr.Left)
b.bind(expr.Type)
if operator == ast.KindCommaToken {
b.maybeBindExpressionFlowIfCall(node)
}
94 changes: 67 additions & 27 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
@@ -2896,11 +2896,12 @@ func (c *Checker) checkTypePredicate(node *ast.Node) {
}

func (c *Checker) getTypePredicateParent(node *ast.Node) *ast.SignatureDeclaration {
switch node.Parent.Kind {
parent := ast.GetEffectiveTypeParent(node.Parent)
switch parent.Kind {
case ast.KindArrowFunction, ast.KindCallSignature, ast.KindFunctionDeclaration, ast.KindFunctionExpression, ast.KindFunctionType,
ast.KindMethodDeclaration, ast.KindMethodSignature:
if node == node.Parent.Type() {
return node.Parent
if node == parent.Type() {
return parent
}
}
return nil
@@ -5377,6 +5378,11 @@ func (c *Checker) checkExportAssignment(node *ast.Node) {
c.error(node, diagnostics.ESM_syntax_is_not_allowed_in_a_CommonJS_module_when_verbatimModuleSyntax_is_enabled)
}
c.checkExternalModuleExports(container)
if typeNode := node.Type(); typeNode != nil && node.Kind == ast.KindExportAssignment {
t := c.getTypeFromTypeNode(typeNode)
initializerType := c.checkExpressionCached(node.Expression())
c.checkTypeAssignableToAndOptionallyElaborate(initializerType, t, node.Expression(), node.Expression(), nil /*headMessage*/, nil)
}
if (node.Flags&ast.NodeFlagsAmbient != 0) && !ast.IsEntityNameExpression(node.Expression()) {
c.grammarErrorOnNode(node.Expression(), diagnostics.The_expression_of_an_export_assignment_must_be_an_identifier_or_qualified_name_in_an_ambient_context)
}
@@ -12579,18 +12585,11 @@ func (c *Checker) checkObjectLiteral(node *ast.Node, checkMode CheckMode) *Type
}
if ast.IsPropertyAssignment(memberDecl) || ast.IsShorthandPropertyAssignment(memberDecl) || ast.IsObjectLiteralMethod(memberDecl) {
var t *Type
switch {
case memberDecl.Kind == ast.KindPropertyAssignment:
switch memberDecl.Kind {
case ast.KindPropertyAssignment:
t = c.checkPropertyAssignment(memberDecl, checkMode)
case memberDecl.Kind == ast.KindShorthandPropertyAssignment:
var expr *ast.Node
if !inDestructuringPattern {
expr = memberDecl.AsShorthandPropertyAssignment().ObjectAssignmentInitializer
}
if expr == nil {
expr = memberDecl.Name()
}
t = c.checkExpressionForMutableLocation(expr, checkMode)
case ast.KindShorthandPropertyAssignment:
t = c.checkShorthandPropertyAssignment(memberDecl, inDestructuringPattern, checkMode)
default:
t = c.checkObjectLiteralMethod(memberDecl, checkMode)
}
@@ -13025,7 +13024,30 @@ func (c *Checker) checkPropertyAssignment(node *ast.Node, checkMode CheckMode) *
if ast.IsComputedPropertyName(node.Name()) {
c.checkComputedPropertyName(node.Name())
}
return c.checkExpressionForMutableLocation(node.Initializer(), checkMode)
initializerType := c.checkExpressionForMutableLocation(node.Initializer(), checkMode)
if node.Type() != nil {
t := c.getTypeFromTypeNode(node.Type())
c.checkTypeAssignableToAndOptionallyElaborate(initializerType, t, node, node.Initializer(), nil /*headMessage*/, nil)
return t
}
return initializerType
}

func (c *Checker) checkShorthandPropertyAssignment(node *ast.Node, inDestructuringPattern bool, checkMode CheckMode) *Type {
var expr *ast.Node
if !inDestructuringPattern {
expr = node.AsShorthandPropertyAssignment().ObjectAssignmentInitializer
}
if expr == nil {
expr = node.Name()
}
expressionType := c.checkExpressionForMutableLocation(expr, checkMode)
if node.Type() != nil {
t := c.getTypeFromTypeNode(node.Type())
c.checkTypeAssignableToAndOptionallyElaborate(expressionType, t, node, expr, nil /*headMessage*/, nil)
return t
}
return expressionType
}

func (c *Checker) isInPropertyInitializerOrClassStaticBlock(node *ast.Node) bool {
@@ -15683,11 +15705,15 @@ func (c *Checker) getTypeOfVariableOrParameterOrPropertyWorker(symbol *ast.Symbo
case ast.KindPropertyAssignment:
result = c.checkPropertyAssignment(declaration, CheckModeNormal)
case ast.KindShorthandPropertyAssignment:
result = c.checkExpressionForMutableLocation(declaration.Name(), CheckModeNormal)
result = c.checkShorthandPropertyAssignment(declaration, true /*inDestructuringPattern*/, CheckModeNormal)
case ast.KindMethodDeclaration:
result = c.checkObjectLiteralMethod(declaration, CheckModeNormal)
case ast.KindExportAssignment, ast.KindJSExportAssignment:
result = c.widenTypeForVariableLikeDeclaration(c.checkExpressionCached(declaration.AsExportAssignment().Expression), declaration, false /*reportErrors*/)
if declaration.Type() != nil {
result = c.getTypeFromTypeNode(declaration.Type())
} else {
result = c.widenTypeForVariableLikeDeclaration(c.checkExpressionCached(declaration.AsExportAssignment().Expression), declaration, false /*reportErrors*/)
}
case ast.KindBinaryExpression:
result = c.getWidenedTypeForAssignmentDeclaration(symbol)
case ast.KindJsxAttribute:
@@ -17114,23 +17140,28 @@ const (
func (c *Checker) getWidenedTypeForAssignmentDeclaration(symbol *ast.Symbol) *Type {
var t *Type
kind, location := c.isConstructorDeclaredThisProperty(symbol)
if kind == thisAssignmentDeclarationTyped {
switch kind {
case thisAssignmentDeclarationTyped:
if location == nil {
panic("location should not be nil when this assignment has a type.")
}
t = c.getTypeFromTypeNode(location)
} else if kind == thisAssignmentDeclarationConstructor {
case thisAssignmentDeclarationConstructor:
if location == nil {
panic("constructor should not be nil when this assignment is in a constructor.")
}
t = c.getFlowTypeInConstructor(symbol, location)
} else if kind == thisAssignmentDeclarationMethod {
case thisAssignmentDeclarationMethod:
t = c.getTypeOfPropertyInBaseClass(symbol)
}
if t == nil {
var types []*Type
for _, declaration := range symbol.Declarations {
if ast.IsBinaryExpression(declaration) {
if declaration.Type() != nil {
t = c.getTypeFromTypeNode(declaration.Type())
break
}
types = core.AppendIfUnique(types, c.checkExpressionForMutableLocation(declaration.AsBinaryExpression().Right, CheckModeNormal))
}
}
@@ -17139,7 +17170,9 @@ func (c *Checker) getWidenedTypeForAssignmentDeclaration(symbol *ast.Symbol) *Ty
types = core.AppendIfUnique(types, c.undefinedOrMissingType)
}
}
t = c.getWidenedType(c.getUnionType(types))
if t == nil {
t = c.getWidenedType(c.getUnionType(types))
}
}
// report an all-nullable or empty union as an implicit any in JS files
if symbol.ValueDeclaration != nil && ast.IsInJSFile(symbol.ValueDeclaration) &&
@@ -17160,7 +17193,7 @@ func (c *Checker) isConstructorDeclaredThisProperty(symbol *ast.Symbol) (thisAss
if kind, ok := c.thisExpandoKinds[symbol]; ok {
location, ok2 := c.thisExpandoLocations[symbol]
if !ok2 {
panic("ctor should be cached whenever this expando location is cached")
panic("location should be cached whenever this expando symbol is cached")
}
return kind, location
}
@@ -17174,9 +17207,8 @@ func (c *Checker) isConstructorDeclaredThisProperty(symbol *ast.Symbol) (thisAss
bin := declaration.AsBinaryExpression()
if ast.GetAssignmentDeclarationKind(bin) == ast.JSDeclarationKindThisProperty &&
(bin.Left.Kind != ast.KindElementAccessExpression || ast.IsStringOrNumericLiteralLike(bin.Left.AsElementAccessExpression().ArgumentExpression)) {
// TODO: if bin.Type() != nil, use bin.Type()
if bin.Right.Kind == ast.KindTypeAssertionExpression {
typeAnnotation = bin.Right.AsTypeAssertion().Type
if bin.Type != nil {
typeAnnotation = bin.Type
}
} else {
allThis = false
@@ -21796,6 +21828,7 @@ func (c *Checker) getTypeFromTypeOperatorNode(node *ast.Node) *Type {
}

func (c *Checker) getESSymbolLikeTypeForNode(node *ast.Node) *Type {
node = ast.GetEffectiveTypeParent(node)
if isValidESSymbolDeclaration(node) {
symbol := c.getSymbolOfNode(node)
if symbol != nil {
@@ -27665,7 +27698,7 @@ func (c *Checker) getContextualType(node *ast.Node, contextFlags ContextFlags) *
return c.getContextualType(parent, contextFlags)
case ast.KindSatisfiesExpression:
return c.getTypeFromTypeNode(parent.AsSatisfiesExpression().Type)
case ast.KindExportAssignment:
case ast.KindExportAssignment, ast.KindJSExportAssignment, ast.KindCommonJSExport:
return c.tryGetTypeFromTypeNode(parent)
case ast.KindJsxExpression:
return c.getContextualTypeForJsxExpression(parent, contextFlags)
@@ -28075,11 +28108,15 @@ func (c *Checker) getContextualTypeForDecorator(decorator *ast.Node) *Type {

func (c *Checker) getContextualTypeForBinaryOperand(node *ast.Node, contextFlags ContextFlags) *Type {
binary := node.Parent.AsBinaryExpression()
if t := binary.Type; t != nil {
return c.getTypeFromTypeNode(t)
}
switch binary.OperatorToken.Kind {
case ast.KindEqualsToken, ast.KindAmpersandAmpersandEqualsToken, ast.KindBarBarEqualsToken, ast.KindQuestionQuestionEqualsToken:
// In an assignment expression, the right operand is contextually typed by the type of the left operand
// unless it's an assignment declaration.
if node == binary.Right {
kind := ast.GetAssignmentDeclarationKind(binary)
if node == binary.Right && kind != ast.JSDeclarationKindModuleExports && kind != ast.JSDeclarationKindExportsProperty {
return c.getContextualTypeForAssignmentExpression(binary)
}
case ast.KindBarBarToken, ast.KindQuestionQuestionToken:
@@ -28171,6 +28208,9 @@ func (c *Checker) getContextualTypeForAssignmentExpression(binary *ast.BinaryExp
}

func (c *Checker) getContextualTypeForObjectLiteralElement(element *ast.Node, contextFlags ContextFlags) *Type {
if t := element.Type(); t != nil && !ast.IsObjectLiteralMethod(element) {
return c.getTypeFromTypeNode(t)
}
objectLiteral := element.Parent
t := c.getApparentTypeOfContextualType(objectLiteral, contextFlags)
if t != nil {
9 changes: 1 addition & 8 deletions internal/checker/grammarchecks.go
Original file line number Diff line number Diff line change
@@ -1377,14 +1377,7 @@ func (c *Checker) checkGrammarTypeOperatorNode(node *ast.TypeOperatorNode) bool
if innerType.Kind != ast.KindSymbolKeyword {
return c.grammarErrorOnNode(innerType, diagnostics.X_0_expected, scanner.TokenToString(ast.KindSymbolKeyword))
}
parent := ast.WalkUpParenthesizedTypes(node.Parent)
// !!!
// if ast.IsInJSFile(parent) && isJSDocTypeExpression(parent) {
// host := getJSDocHost(parent)
// if host != nil {
// parent = getSingleVariableOfVariableStatement(host) || host
// }
// }
parent := ast.GetEffectiveTypeParent(ast.WalkUpParenthesizedTypes(node.Parent))
switch parent.Kind {
case ast.KindVariableDeclaration:
decl := parent.AsVariableDeclaration()
6 changes: 4 additions & 2 deletions internal/parser/jsdoc.go
Original file line number Diff line number Diff line change
@@ -26,9 +26,9 @@ const (
propertyLikeParseCallbackParameter
)

func (p *Parser) withJSDoc(node *ast.Node, hasJSDoc bool) {
func (p *Parser) withJSDoc(node *ast.Node, hasJSDoc bool) []*ast.Node {
if !hasJSDoc {
return
return nil
}

if p.jsdocCache == nil {
@@ -60,7 +60,9 @@ func (p *Parser) withJSDoc(node *ast.Node, hasJSDoc bool) {
p.reparseTags(node, jsdoc)
}
p.jsdocCache[node] = jsdoc
return jsdoc
}
Copy link
Preview

Copilot AI Jun 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache branch always returns nil, dropping previously parsed JSDoc tags. You should return the cached jsdoc slice (e.g. return p.jsdocCache[node]) instead of nil when node is already in jsdocCache.

Suggested change
}
}
if cached, ok := p.jsdocCache[node]; ok {
return cached
}

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. jsdocCache is write-only, write-once here by the nature of the parser. The current bode's jsdoc will never be cached.

return nil
}

func (p *Parser) parseJSDocTypeExpression(mayOmitBraces bool) *ast.Node {
14 changes: 7 additions & 7 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
@@ -1396,8 +1396,8 @@ func (p *Parser) parseExpressionOrLabeledStatement() *ast.Statement {
}
result := p.factory.NewExpressionStatement(expression)
p.finishNode(result, pos)
p.withJSDoc(result, hasJSDoc && !hasParen)
p.reparseCommonJS(result)
jsdoc := p.withJSDoc(result, hasJSDoc && !hasParen)
p.reparseCommonJS(result, jsdoc)
return result
}

@@ -2378,7 +2378,7 @@ func (p *Parser) parseExportAssignment(pos int, hasJSDoc bool, modifiers *ast.Mo
p.parseSemicolon()
p.contextFlags = saveContextFlags
p.statementHasAwaitIdentifier = saveHasAwaitIdentifier
result := p.factory.NewExportAssignment(modifiers, isExportEquals, expression)
result := p.factory.NewExportAssignment(modifiers, isExportEquals, nil /*typeNode*/, expression)
p.finishNode(result, pos)
p.withJSDoc(result, hasJSDoc)
return result
@@ -4592,7 +4592,7 @@ func (p *Parser) makeAsExpression(left *ast.Expression, right *ast.TypeNode) *as
}

func (p *Parser) makeBinaryExpression(left *ast.Expression, operatorToken *ast.Node, right *ast.Expression, pos int) *ast.Node {
result := p.factory.NewBinaryExpression(left, operatorToken, right)
result := p.factory.NewBinaryExpression(nil /*modifiers*/, left, nil /*typeNode*/, operatorToken, right)
p.finishNode(result, pos)
return result
}
@@ -4734,7 +4734,7 @@ func (p *Parser) parseJsxElementOrSelfClosingElementOrFragment(inExpressionConte
operatorToken := p.factory.NewToken(ast.KindCommaToken)
operatorToken.Loc = core.NewTextRange(invalidElement.Pos(), invalidElement.Pos())
p.parseErrorAt(scanner.SkipTrivia(p.sourceText, topBadPos), invalidElement.End(), diagnostics.JSX_expressions_must_have_one_parent_element)
result = p.factory.NewBinaryExpression(result, operatorToken, invalidElement)
result = p.factory.NewBinaryExpression(nil /*modifiers*/, result, nil /*typeNode*/, operatorToken, invalidElement)
p.finishNode(result, pos)
}
return result
@@ -5644,11 +5644,11 @@ func (p *Parser) parseObjectLiteralElement() *ast.Node {
if equalsToken != nil {
initializer = doInContext(p, ast.NodeFlagsDisallowInContext, false, (*Parser).parseAssignmentExpressionOrHigher)
}
node = p.factory.NewShorthandPropertyAssignment(modifiers, name, postfixToken, equalsToken, initializer)
node = p.factory.NewShorthandPropertyAssignment(modifiers, name, postfixToken, nil /*typeNode*/, equalsToken, initializer)
} else {
p.parseExpected(ast.KindColonToken)
initializer := doInContext(p, ast.NodeFlagsDisallowInContext, false, (*Parser).parseAssignmentExpressionOrHigher)
node = p.factory.NewPropertyAssignment(modifiers, name, postfixToken, initializer)
node = p.factory.NewPropertyAssignment(modifiers, name, postfixToken, nil /*typeNode*/, initializer)
}
p.finishNode(node, pos)
p.withJSDoc(node, hasJSDoc)
Loading
Oops, something went wrong.