-
Notifications
You must be signed in to change notification settings - Fork 644
Deep clone descendants of reparsed nodes #966
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
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 ensures reparsed AST nodes are deep-cloned and tracked to avoid cross-tree parent-pointer races.
- Deep-clone nodes in the JSDoc reparser and record original→reparsed mappings.
- Remove eager parent-setting in
jsdoc.go
and surface thereparsedNodes
map inSourceFile
. - Prevent binder from overwriting existing
Parent
pointers and panic on unexpected mismatches.
Reviewed Changes
Copilot reviewed 77 out of 77 changed files in this pull request and generated 3 comments.
File | Description |
---|---|
internal/parser/reparser.go | Use DeepCloneNode for JSDoc nodes, add setReparsed mapping |
internal/parser/parser.go | Introduce reparsedNodes map and assign to SourceFile |
internal/binder/binder.go | Only set node.Parent if nil; panic on mismatches |
Does this actually fix any races? I would think that even if the data being written is the same, writing to the parent concurrently will still trigger a race failure. |
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 reparseCommonJS needs clones too. I'll review again after I look at the test diffs one more time.
@@ -63,7 +64,7 @@ func (p *Parser) reparseTags(parent *ast.Node, jsDoc []*ast.Node) { | |||
var t *ast.Node | |||
switch typeExpression.Kind { | |||
case ast.KindJSDocTypeExpression: | |||
t = typeExpression.Type() | |||
t = p.factory.DeepCloneNode(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.
do you need to clone every child added to a synthetic node? There are a few up in reparseCommonJS too (eg bin.Left)
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 catch.
This PR fixes the data races discovered in #851. We do this thing (in
references.go
) where we set parent pointers early on subtrees that contain imports of any kind so we can navigate from a module specifier node up to its import during program construction, before binding. But until #851, we weren't yet reading that parent pointer. Now that we are, we have a data race between parallel tests sharing the same AST: one test is doing program construction and reading.Parent
on module specifiers, while another test is binding all the files. Binding unconditionally setsnode.Parent
, so I thought the trivial fix ought to beBut JS reparsing complicates things. Some nodes get copied into synthesized, “reparsed” subtrees, without cloning, which means in practice they have two different parents. The binder visits these nodes twice, and the parent chain ends up being the one it visits last, which differs depending on the reparsing implementation for the specific node being reparsed. I believe this will also cause problems in syntactic language services:
/** @import { foo } from "./bar" */
Existing semantic/checker code assumes that an ImportSpecifier like
foo
has anImportDeclaration
orJSImportDeclaration
ancestor, but syntactic code would expect a token inside JSDoc to have aJSDoc
ancestor. We can’t satisfy both (reasonable) expectations without cloning everything.