Skip to content

Ensure nodes have real parents, fix parent race #970

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 4 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 4 additions & 3 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -9643,9 +9643,10 @@ func (node *JSDocThisTag) Clone(f NodeFactoryCoercible) *Node {
// JSDocImportTag
type JSDocImportTag struct {
JSDocTagBase
ImportClause *Declaration
ModuleSpecifier *Expression
Attributes *Node
JSImportDeclaration *ImportDeclaration
ImportClause *Declaration
ModuleSpecifier *Expression
Attributes *Node
}

func (f *NodeFactory) NewJSDocImportTag(tagName *IdentifierNode, importClause *Declaration, moduleSpecifier *Node, attributes *Node, comment *NodeList) *Node {
Expand Down
4 changes: 3 additions & 1 deletion internal/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,9 @@ func (b *Binder) bind(node *ast.Node) bool {
if node == nil {
return false
}
node.Parent = b.parent
if node.Parent == nil || node.Parent.Flags&ast.NodeFlagsReparsed != 0 {
node.Parent = b.parent
}
saveInStrictMode := b.inStrictMode
// Even though in the AST the jsdoc @typedef node belongs to the current node,
// its symbol might be in the same scope with the current node's symbol. Consider:
Expand Down
5 changes: 5 additions & 0 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -13848,6 +13848,9 @@ func (c *Checker) getTargetOfImportSpecifier(node *ast.Node, dontResolveAlias bo
}
}
root := node.Parent.Parent.Parent // ImportDeclaration
if root.Kind == ast.KindJSDocImportTag {
root = root.AsJSDocImportTag().JSImportDeclaration.AsNode()
}
if ast.IsBindingElement(node) {
root = ast.GetRootDeclaration(node)
}
Expand Down Expand Up @@ -14219,6 +14222,8 @@ func (c *Checker) getModuleSpecifierForImportOrExport(node *ast.Node) *ast.Node

func getModuleSpecifierFromNode(node *ast.Node) *ast.Node {
switch node.Kind {
case ast.KindJSDocImportTag:
return node.AsJSDocImportTag().JSImportDeclaration.ModuleSpecifier
case ast.KindImportDeclaration, ast.KindJSImportDeclaration:
return node.AsImportDeclaration().ModuleSpecifier
case ast.KindExportDeclaration:
Expand Down
2 changes: 1 addition & 1 deletion internal/checker/grammarchecks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2117,7 +2117,7 @@ func (c *Checker) checkGrammarBigIntLiteral(node *ast.BigIntLiteral) bool {
}

func (c *Checker) checkGrammarImportClause(node *ast.ImportClause) bool {
if node.IsTypeOnly && node.Name() != nil && node.NamedBindings != nil {
if node.Flags&ast.NodeFlagsJSDoc == 0 && node.IsTypeOnly && node.Name() != nil && node.NamedBindings != nil {
return c.grammarErrorOnNode(&node.Node, diagnostics.A_type_only_import_can_specify_a_default_import_or_named_bindings_but_not_both)
}
if node.IsTypeOnly && node.NamedBindings != nil && node.NamedBindings.Kind == ast.KindNamedImports {
Expand Down
1 change: 1 addition & 0 deletions internal/parser/reparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func (p *Parser) reparseTags(parent *ast.Node, jsDoc []*ast.Node) {
importDeclaration.Loc = core.NewTextRange(tag.Pos(), tag.End())
importDeclaration.Flags = p.contextFlags | ast.NodeFlagsReparsed
p.reparseList = append(p.reparseList, importDeclaration)
importTag.JSImportDeclaration = importDeclaration.AsImportDeclaration()
// !!! @overload and other unattached tags (@callback et al) support goes here
}
if !isLast {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
index.js(2,28): error TS2304: Cannot find name 'B'.
index.js(2,42): error TS2304: Cannot find name 'A'.
index.js(2,48): error TS2304: Cannot find name 'B'.
index.js(2,67): error TS2304: Cannot find name 'B'.


==== index.js (4 errors) ====
/**
* @typedef {{ [K in keyof B]: { fn: (a: A, b: B) => void; thing: B[K]; } }} Funcs
~
Copy link
Member

Choose a reason for hiding this comment

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

my best guess is that the something set the type expression parents to the synthetic type alias, inconsistent with other type expressions, and the binder change now makes it consistent with the others. I have a branch that sets Host on typedef children which should fix this.

Edit:actually it may be that all type aliases were working by mistake?

Copy link
Member

Choose a reason for hiding this comment

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

actually it may be that all typedefs were setting their type's parent to the synthetic node. Not in the reparser, though, so through the binder's existing code.

!!! error TS2304: Cannot find name 'B'.
~
!!! error TS2304: Cannot find name 'A'.
~
!!! error TS2304: Cannot find name 'B'.
~
!!! error TS2304: Cannot find name 'B'.
* @template A
* @template {Record<string, unknown>} B
*/

/**
* @template A
* @template {Record<string, unknown>} B
* @param {Funcs<A, B>} fns
* @returns {[A, B]}
*/
function foo(fns) {
return /** @type {any} */ (null);
}

const result = foo({
bar: {
fn:
/** @param {string} a */
(a) => {},
thing: "asd",
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@
* @returns {[A, B]}
*/
function foo(fns) {
>foo : <A, B extends Record<string, unknown>>(fns: Funcs<A, B>) => [A, B]
>fns : Funcs<A, B>
>foo : <A, B extends Record<string, unknown>>(fns: { [x: string]: { fn: (a: A, b: B) => void; thing: B; }; }) => [A, B]
>fns : { [x: string]: { fn: (a: A, b: B) => void; thing: B; }; }

return /** @type {any} */ (null);
>(null) : any
>null : any
}

const result = foo({
>result : [string, { bar: string; }]
>foo({ bar: { fn: /** @param {string} a */ (a) => {}, thing: "asd", },}) : [string, { bar: string; }]
>foo : <A, B extends Record<string, unknown>>(fns: Funcs<A, B>) => [A, B]
>result : [unknown, Record<string, unknown>]
>foo({ bar: { fn: /** @param {string} a */ (a) => {}, thing: "asd", },}) : [unknown, Record<string, unknown>]
>foo : <A, B extends Record<string, unknown>>(fns: { [x: string]: { fn: (a: A, b: B) => void; thing: B; }; }) => [A, B]
>{ bar: { fn: /** @param {string} a */ (a) => {}, thing: "asd", },} : { bar: { fn: (a: string) => void; thing: string; }; }

bar: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
1.js(1,30): error TS2857: Import attributes cannot be used with type-only imports or exports.
1.js(2,33): error TS2823: Import attributes are only supported when the '--module' option is set to 'esnext', 'nodenext', or 'preserve'.
1.js(2,33): error TS2857: Import attributes cannot be used with type-only imports or exports.
1.js(4,13): error TS4078: Parameter 'a' of exported function has or is using private name 'I'.


==== 0.ts (0 errors) ====
export interface I { }

==== 1.js (4 errors) ====
==== 1.js (5 errors) ====
/** @import { I } from './0' with { type: "json" } */
~~~~~~~~~~~~~~~~~~~~~
!!! error TS2823: Import attributes are only supported when the '--module' option is set to 'esnext', 'nodenext', or 'preserve'.
Expand All @@ -20,5 +21,7 @@
!!! error TS2857: Import attributes cannot be used with type-only imports or exports.

/** @param {I} a */
~
!!! error TS4078: Parameter 'a' of exported function has or is using private name 'I'.
function f(a) {}

Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
1.js(1,30): error TS2857: Import attributes cannot be used with type-only imports or exports.
1.js(2,33): error TS2857: Import attributes cannot be used with type-only imports or exports.
1.js(4,13): error TS4078: Parameter 'a' of exported function has or is using private name 'I'.


==== 0.ts (0 errors) ====
export interface I { }

==== 1.js (2 errors) ====
==== 1.js (3 errors) ====
/** @import { I } from './0' with { type: "json" } */
~~~~~~~~~~~~~~~~~~~~~
!!! error TS2857: Import attributes cannot be used with type-only imports or exports.
Expand All @@ -14,5 +15,7 @@
!!! error TS2857: Import attributes cannot be used with type-only imports or exports.

/** @param {I} a */
~
!!! error TS4078: Parameter 'a' of exported function has or is using private name 'I'.
function f(a) {}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
b.js(1,13): error TS1363: A type-only import can specify a default import or named bindings, but not both.
b.js(5,12): error TS4078: Parameter 'b' of exported function has or is using private name 'I'.


==== a.ts (0 errors) ====
Expand All @@ -7,12 +7,12 @@ b.js(1,13): error TS1363: A type-only import can specify a default import or nam

==== b.js (1 errors) ====
/** @import Foo, { I } from "./a" */
~~~~~~~~~~
!!! error TS1363: A type-only import can specify a default import or named bindings, but not both.

/**
* @param {Foo} a
* @param {I} b
~
!!! error TS4078: Parameter 'b' of exported function has or is using private name 'I'.
*/
export function foo(a, b) {}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
b.js(8,12): error TS4078: Parameter 'a' of exported function has or is using private name 'Foo'.


==== a.ts (0 errors) ====
export interface Foo {}

==== b.js (1 errors) ====
/**
* @import {
* Foo
* } from "./a"
*/

/**
* @param {Foo} a
~~~
!!! error TS4078: Parameter 'a' of exported function has or is using private name 'Foo'.
*/
export function foo(a) {}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
b.js(7,12): error TS4078: Parameter 'a' of exported function has or is using private name 'Foo'.


==== a.ts (0 errors) ====
export interface Foo {}

==== b.js (1 errors) ====
/**
* @import { Foo }
* from "./a"
*/

/**
* @param {Foo} a
~~~
!!! error TS4078: Parameter 'a' of exported function has or is using private name 'Foo'.
*/
export function foo(a) {}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
b.js(8,12): error TS4078: Parameter 'a' of exported function has or is using private name 'Foo'.


==== a.ts (0 errors) ====
export interface Foo {}

==== b.js (1 errors) ====
/**
* @import
* { Foo
* } from './a'
*/

/**
* @param {Foo} a
~~~
!!! error TS4078: Parameter 'a' of exported function has or is using private name 'Foo'.
*/
export function foo(a) {}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/foo.js(6,13): error TS4078: Parameter 'foo' of exported function has or is using private name 'Foo'.


==== /types.ts (0 errors) ====
export interface Foo {
a: number;
}

==== /foo.js (1 errors) ====
/**
* @import { Foo } from "./types"
*/

/**
* @param { Foo } foo
~~~
!!! error TS4078: Parameter 'foo' of exported function has or is using private name 'Foo'.
*/
function f(foo) {}

Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,22 @@ module.exports = Timer;
* @param {HookHandler} handle
*/
function Hook(handle) {
>Hook : (handle: HookHandler) => void
>handle : HookHandler
>Hook : (handle: (arg: any) => void) => void
Copy link
Member

Choose a reason for hiding this comment

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

the main different effect of this alternate-parent technique is that symbols no longer print as aliases. I'll need to figure out why when I start burning down the jsdoc diffs.

>handle : (arg: any) => void

this.handle = handle;
>this.handle = handle : HookHandler
>this.handle = handle : (arg: any) => void
>this.handle : any
>this : any
>handle : any
>handle : HookHandler
>handle : (arg: any) => void
}
module.exports = Hook;
>module.exports = Hook : (handle: HookHandler) => void
>module.exports : (handle: HookHandler) => void
>module : { Hook(handle: HookHandler): void; }
>exports : (handle: HookHandler) => void
>Hook : (handle: HookHandler) => void
>module.exports = Hook : (handle: (arg: any) => void) => void
>module.exports : (handle: (arg: any) => void) => void
>module : { Hook(handle: (arg: any) => void): void; }
>exports : (handle: (arg: any) => void) => void
>Hook : (handle: (arg: any) => void) => void

=== context.js ===
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export {}; // flag file as module
* @returns {SomeType}
*/
function doTheThing(x) {
>doTheThing : (x: number) => SomeType
>doTheThing : (x: number) => number | ExportedThing | LocalThing | { x: string; }
>x : number

return {x: ""+x};
Expand All @@ -56,14 +56,14 @@ class ExportedThing {
>"ok" : "ok"
}
module.exports = {
>module.exports = { doTheThing, ExportedThing,} : { doTheThing: (x: number) => SomeType; ExportedThing: typeof ExportedThing; }
>module.exports : { doTheThing: (x: number) => SomeType; ExportedThing: typeof ExportedThing; }
>module : { "export=": { doTheThing: (x: number) => SomeType; ExportedThing: typeof ExportedThing; }; }
>exports : { doTheThing: (x: number) => SomeType; ExportedThing: typeof ExportedThing; }
>{ doTheThing, ExportedThing,} : { doTheThing: (x: number) => SomeType; ExportedThing: typeof ExportedThing; }
>module.exports = { doTheThing, ExportedThing,} : { doTheThing: (x: number) => number | ExportedThing | LocalThing | { x: string; }; ExportedThing: typeof ExportedThing; }
>module.exports : { doTheThing: (x: number) => number | ExportedThing | LocalThing | { x: string; }; ExportedThing: typeof ExportedThing; }
>module : { "export=": { doTheThing: (x: number) => number | ExportedThing | LocalThing | { x: string; }; ExportedThing: typeof ExportedThing; }; }
>exports : { doTheThing: (x: number) => number | ExportedThing | LocalThing | { x: string; }; ExportedThing: typeof ExportedThing; }
>{ doTheThing, ExportedThing,} : { doTheThing: (x: number) => number | ExportedThing | LocalThing | { x: string; }; ExportedThing: typeof ExportedThing; }

doTheThing,
>doTheThing : (x: number) => SomeType
>doTheThing : (x: number) => number | ExportedThing | LocalThing | { x: string; }

ExportedThing,
>ExportedThing : typeof ExportedThing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
templateTagOnConstructorFunctions.js(3,18): error TS2304: Cannot find name 'U'.
templateTagOnConstructorFunctions.js(3,24): error TS2304: Cannot find name 'U'.


==== templateTagOnConstructorFunctions.js (2 errors) ====
/**
* @template U
* @typedef {(u: U) => U} Id
~
!!! error TS2304: Cannot find name 'U'.
~
!!! error TS2304: Cannot find name 'U'.
*/
/**
* @param {T} t
* @template T
*/
function Zet(t) {
/** @type {T} */
this.u
this.t = t
}
/**
* @param {T} v
* @param {Id<T>} id
*/
Zet.prototype.add = function(v, id) {
this.u = v || this.t
return id(this.u)
}
var z = new Zet(1)
z.t = 2
z.u = false

Loading