-
Notifications
You must be signed in to change notification settings - Fork 640
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
base: main
Are you sure you want to change the base?
Type nodes for JSDoc #1013
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds explicit TypeNode
fields to various AST nodes that previously only got types via JSDoc hacks, refactors the reparser to remove cloning of type assertions, and updates factory and transformer calls to pass the new typeNode
argument.
- Introduce
TypeNode
onPropertyAssignment
,ShorthandPropertyAssignment
,ExportAssignment
,CommonJSExport
, andBinaryExpression
- Refactor
reparser.go
to share parsing logic for JSDoc tags and remove clone-based reparsing - Update all factory, transformer, binding, and checker code to thread through the new
typeNode
parameter
Reviewed Changes
Copilot reviewed 207 out of 207 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor3_Js.types | Baseline updated to reflect new type inference on super.arguments |
internal/ast/ast.go | Added new Type() cases and panic message enhancements |
internal/ast/utilities.go | Introduced GetEffectiveTypeParent helper |
internal/parser/jsdoc.go | Changed withJSDoc to return parsed tags array |
internal/parser/reparser.go | Refactored reparseCommonJS , added reparseTags , gatherTypeParameters , and reparseHosted |
internal/parser/parser.go | Updated calls to withJSDoc , reparseCommonJS , and many factory invocations |
internal/transformers/* | Passed nil /*typeNode*/ to all New* and Update* calls |
internal/checker/checker.go | Added type-node–based checks for exports and property assignments |
internal/binder/binder.go | Extended binding to include expr.Type in destructor flows |
internal/printer/factory.go | Updated helper methods to pass nil /*typeNode*/ on binary ops |
Comments suppressed due to low confidence (1)
testdata/baselines/reference/submodule/compiler/argumentsReferenceInConstructor3_Js.types:48
- The baseline now shows
any
instead of the expectedobject
type forsuper.arguments.foo
. Verify the new double-attach logic isn't overriding the correct contextual type.
>this.bar = super.arguments.foo : any
@@ -60,7 +60,9 @@ func (p *Parser) withJSDoc(node *ast.Node, hasJSDoc bool) { | |||
p.reparseTags(node, jsdoc) | |||
} | |||
p.jsdocCache[node] = jsdoc | |||
return jsdoc | |||
} |
There was a problem hiding this comment.
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
.
} | |
} | |
if cached, ok := p.jsdocCache[node]; ok { | |
return cached | |
} |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
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.
panic("JSDoc type expression already has a host: " + typeExpression.AsJSDocTypeExpression().Host.Kind.String()) | ||
} | ||
t := typeExpression.Type().Clone(&p.factory) | ||
t := typeExpression.Type() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the clone here means the same TypeNode
instance may be reused across multiple hosts, leading to shared mutations. Consider cloning (Clone(&p.factory)
) or otherwise ensuring each host gets its own TypeNode
.
t := typeExpression.Type() | |
t := typeExpression.Type().Clone(&p.factory) |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what the panic is for! The JSExportAssignment/CommonJSExport exemption avoids a Host pointer on BinaryExpression, at the cost of special handling later.
internal/ast/ast.go
Outdated
@@ -366,7 +366,7 @@ func (n *Node) Expression() *Node { | |||
case KindJsxSpreadAttribute: | |||
return n.AsJsxSpreadAttribute().Expression | |||
} | |||
panic("Unhandled case in Node.Expression") | |||
panic("Unhandled case in Node.Expression" + n.Kind.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The panic message concatenates the kind string without a separator. For clarity, add a space or colon (e.g. "Unhandled case in Node.Expression: " + n.Kind.String()
).
panic("Unhandled case in Node.Expression" + n.Kind.String()) | |
panic("Unhandled case in Node.Expression: " + n.Kind.String()) |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good idea.
@@ -30,13 +29,12 @@ | |||
this.bar = super.arguments.foo; | |||
->this.bar = super.arguments.foo : error | |||
->this.bar : any | |||
+>this.bar = super.arguments.foo : object | |||
+>this.bar = super.arguments.foo : any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any
is better than object
because it shows that the sythetic cast is gone. There's an error, so error
would be even better though.
- | ||
a.js(8,18): error TS2353: Object literal may only specify known properties, and 'c' does not exist in type 'Foo'. | ||
|
||
|
||
-==== checkJsdocTypeTagOnExportAssignment1.js (0 errors) ==== |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all that's left is a diff in the test framework I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#1014 will fix this.
|
||
a; | ||
->a : import("a").Foo | ||
+>a : import("./a").Foo | ||
+>a : { c: number; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same type printing diff as for all reparsed imports. Needs later investigation.
@@ -4,120 +4,39 @@ | |||
index.js(2,19): error TS2315: Type 'Boolean' is not generic. | |||
-index2.js(2,19): error TS2315: Type 'Void' is not generic. | |||
-index3.js(2,19): error TS2315: Type 'Undefined' is not generic. | |||
+index.js(2,27): error TS2304: Cannot find name 'T'. | |||
+index2.js(2,19): error TS2304: Cannot find name 'Void'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this name resolution improvement is a result of not cloning the type anymore
import * as debug from './mod' | ||
|
||
debug.formatters.j | ||
+ ~ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is an improvement in imports, it's just that multi-file expandos are not supported in Corsa. I'll send a later PR adding to the list of accepted diffs.
+ /** @type {string} */ | ||
+ var text3 = seq(text1)(text2); | ||
+ ~~~~~ | ||
+!!! error TS2322: Type 'T' is not assignable to type 'string'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@template
still needs a fix to type instantation -- I believe it's inspecting parents at some point and failing.
+ module.exports.a = function a() {} | ||
+ | ||
+ module.exports.b = function b() {} | ||
+ module.exports.b.cat = "cat"; | ||
+ ~~~ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same typing improvement, but nested expandos aren't supported yet
module.exports = 0; | ||
>module.exports = 0 : string | ||
->module.exports = 0 : string | ||
+>module.exports = 0 : 0 |
There was a problem hiding this comment.
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, in which the binary expression source of the export ignores the annotation, but usages of it have the annotated type.
@@ -6,7 +6,7 @@ | |||
>this.b : Symbol(b, Decl(bug25926.js, 0, 23)) | |||
->this : Symbol(__type, Decl(bug25926.js, 0, 11)) | |||
->b : Symbol(b, Decl(bug25926.js, 2, 9)) | |||
+>this : Symbol(o1, Decl(bug25926.js, 0, 11)) | |||
+>this : Symbol(�type, Decl(bug25926.js, 0, 11)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is getting some sort of goofy symbol name, it seems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the usual string for the intenral name of types. Does it not normally print?
Edit: It prints in Strada, whereit's __type
not th'type
.
And functions 😭 |
I know, I know. But the function annotation code is so complicated. It was a bug farm for years in Strada. |
internal/ast/ast.go
Outdated
@@ -366,7 +366,7 @@ func (n *Node) Expression() *Node { | |||
case KindJsxSpreadAttribute: | |||
return n.AsJsxSpreadAttribute().Expression | |||
} | |||
panic("Unhandled case in Node.Expression") | |||
panic("Unhandled case in Node.Expression" + n.Kind.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good idea.
@@ -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 |
There was a problem hiding this comment.
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
internal/checker/checker.go
Outdated
@@ -15352,11 +15374,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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this isn't necessarily in a destructuring pattern, but true=never use the object assigmebt initialiser. Sometimes this shows up outside a destructuring though! I've passed true here to keep the behaviour the same but we should check this out in more detail.
@@ -60,7 +60,9 @@ func (p *Parser) withJSDoc(node *ast.Node, hasJSDoc bool) { | |||
p.reparseTags(node, jsdoc) | |||
} | |||
p.jsdocCache[node] = jsdoc | |||
return jsdoc | |||
} |
There was a problem hiding this comment.
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.
panic("JSDoc type expression already has a host: " + typeExpression.AsJSDocTypeExpression().Host.Kind.String()) | ||
} | ||
t := typeExpression.Type().Clone(&p.factory) | ||
t := typeExpression.Type() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what the panic is for! The JSExportAssignment/CommonJSExport exemption avoids a Host pointer on BinaryExpression, at the cost of special handling later.
In Javascript, you can give a type and modifiers (for classes) to expandos, exports and object literal properties:
Alternate PR name: Types where they shouldn't be.
This PR adds Type nodes to nodes that can only get types from jsdoc. Then it checks those types, replacing the previous hack of reparsing a type assertion. In order to make this work on commonjs exports it starts calling
withJSDoc
on synthetic commonjs nodes. It also (sorry), refactors reparser.go into separate functions, and stops cloning type nodes since we're moving to a no-clone reparsing. I'll remove the other 3 clones later, as part of the PR that corrects synthetic-parent walks for@template
.This is the first infrastructure/fix PR of the ones I'll spend most of June on. I hope they'll get more coherent but this one sprawls a bit.
Nodes with new Type nodes
Modifiers
to supportprivate
onthis.p
assignments.These additions are going to cost memory, which we agreed on at the design meeting.
Returns do not add type annotations because I think this is a low-value feature, and TS type checking has no equivalent whatsoever.
Notes
To fix contextual typing on commonjs exports, which of course walks up the parent chain, I should also have to introduce a Host property on BinaryExpression, because CommonJSExport is reparsed from BinaryExpression. This was a bridge too far for me, so I cheated: both CommonJSExport and BinaryExpression have the same type annotation, so I let the
@type
double-attach and have special-case code only ingetContextualTypeForBinaryOperand
to recognise the BinaryExpression syntax formodule.exports
.ShorthandPropertyAssignment type checking was inconsistent in the two places it existed. When I refactored it to a single function, I left the (unannotated) behaviour the same, but it means that the
inDestructuringPattern
argument seems backwards. We should revisit this because I think the consistent behaviour may be right.I started removing Clones in reparsing, starting with type nodes. This required me to add a synthetic-parent lookup in 2 more places (the existing one is in nameresolver.go). I expect there are a few more, so I created a utility. In order to nudge us toward making
Parent
a method, I gave it the namegetEffectiveTypeParent
. But I'm not sure how hard/how important aParent
method is.The reparser refactor obscures a few changes (sorry!), but not many. The cases in
reparseHosted
for PropertyAssignment, etc just change frommakeNewTypeAssertion
tomakeNewType
.