Skip to content

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

Closed

Conversation

andrewbranch
Copy link
Member

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 sets node.Parent, so I thought the trivial fix ought to be

+ if node.Parent == nil {
    node.Parent = b.parent
+ }

But 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 an ImportDeclaration or JSImportDeclaration ancestor, but syntactic code would expect a token inside JSDoc to have a JSDoc ancestor. We can’t satisfy both (reasonable) expectations without cloning everything.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Copy link
Contributor

@Copilot Copilot AI left a 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 the reparsedNodes map in SourceFile.
  • 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

@jakebailey
Copy link
Member

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.

Copy link
Member

@sandersn sandersn left a 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())
Copy link
Member

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)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, good catch.

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants