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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -652,6 +652,7 @@ func isDeclarationStatementKind(kind Kind) bool {
KindExportDeclaration,
KindExportAssignment,
KindJSExportAssignment,
KindCommonJSExport,
KindNamespaceExportDeclaration:
return true
}
Expand Down Expand Up @@ -859,6 +860,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 {
Expand Down Expand Up @@ -3300,6 +3310,7 @@ func ReplaceModifiers(factory *NodeFactory, node *Node, modifierArray *ModifierL
return factory.UpdateExportAssignment(
node.AsExportAssignment(),
modifierArray,
node.Type(),
node.Expression(),
)
case KindExportDeclaration:
Expand Down
5 changes: 4 additions & 1 deletion internal/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -1649,7 +1649,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)
}
Expand Down Expand Up @@ -2208,9 +2208,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)
Expand Down Expand Up @@ -2239,6 +2241,7 @@ func (b *Binder) bindBinaryExpressionFlow(node *ast.Node) {
}
} else {
b.bind(expr.Left)
b.bind(expr.Type)
if operator == ast.KindCommaToken {
b.maybeBindExpressionFlowIfCall(node)
}
Expand Down
94 changes: 67 additions & 27 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2887,11 +2887,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
Expand Down Expand Up @@ -5322,6 +5323,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)
}
Expand Down Expand Up @@ -12500,18 +12506,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)
}
Expand Down Expand Up @@ -12946,7 +12945,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 {
Expand Down Expand Up @@ -15605,11 +15627,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:
Expand Down Expand Up @@ -17036,23 +17062,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))
}
}
Expand All @@ -17061,7 +17092,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) &&
Expand All @@ -17082,7 +17115,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
}
Expand All @@ -17096,9 +17129,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
Expand Down Expand Up @@ -21714,6 +21746,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 {
Expand Down Expand Up @@ -27585,7 +27618,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)
Expand Down Expand Up @@ -27995,11 +28028,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:
Expand Down Expand Up @@ -28091,6 +28128,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 {
Expand Down
9 changes: 1 addition & 8 deletions internal/checker/grammarchecks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 4 additions & 2 deletions internal/parser/jsdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 7 additions & 7 deletions internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -4585,7 +4585,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
}
Expand Down Expand Up @@ -4727,7 +4727,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
Expand Down Expand Up @@ -5637,11 +5637,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)
Expand Down
Loading