Skip to content

Add callback tag, fix jsdoc type parameter resolution #1187

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 8 commits into from
Jun 13, 2025

Conversation

sandersn
Copy link
Member

This PR adds support for the @callback tag. More importantly, it fixes type parameter resolution and number of other bugs in @typedef and @template. It does some of this by simplifying the rules for how @template attaches its type parameter to its host.

Specifically, all @template tags would previously apply to all @overloads, @typedef, function declarations, etc, that they shared a jsdoc with. They didn't even have to come before their usage:

/**
 * @template T
 * @typedef {T} Alias
 * @template U
 * @overload
 * @param {[T,U]} tu
 * @overload
 * @param {number} n
 * @return {string}
 * @param {any} tun
 * @return {string}
 */
function f(tun) {}

// is equivalent to TS
type Alias<T,U> = T
function f<T,U>(tu: [T,U])
function f<T,U>(n: number): string
function f<T,U>(tun: any): string {}

Not only is this nonsensical, it's really hard to implement, and I don't think people write their types in one single jsdoc like this in the first place. Corsa, as implemented in this PR, requires @template to precede its usage and for it to apply in only one place:

type Alias<T> = T
function f<U>(tu: [T,U])
function f(n: number): string
function f(tun: any): string

This will break any intentional sharing, but those will have to be rewritten. Sharing reparsed nodes to more than one other place in the tree would require Host to be a slice, plus some way to choose the correct host when resolving names.

Speaking of Host, JSDocTemplateTag now has a host, and the grossly-named GetEffectiveTypeParent now has two cases. It's also used consistently in the 5 places it makes sense. I do think we're nearing the total number of places it'll be needed, but there are 512 uses of Node.Parent in the checker alone, so I could be wrong.

Despite the improvements in typing, there is little code churn outside reparser.go. Inside, I refactored callback/typedef/overload quite a bit to share code, so there are almost 200 lines changed. Best reviewed ignoring whitespace, as always.

@Copilot Copilot AI review requested due to automatic review settings June 13, 2025 21:35
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 enhances JSDoc support by adding @callback tag handling, tightening how @template tags attach to their specific hosts, and simplifying type parameter resolution across @typedef/@overload/callback declarations.

  • Added support for parsing and emitting @callback tags as type aliases.
  • Refactored gatherTypeParameters to only apply templates preceding their host tag.
  • Introduced a Host field on JSDocTemplateTag and updated parent traversal (GetEffectiveTypeParent).

Reviewed Changes

Copilot reviewed 105 out of 105 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/printer/printer.go Added nil-check in emitReturnType to guard against missing nodes.
internal/parser/reparser.go Major reparser overhaul for @typedef, @callback, and @overload with host assignment.
internal/checker/checker.go Updated parent traversal to use GetEffectiveTypeParent.
internal/binder/nameresolver.go Integrated effective parent logic in name-resolution loop.
internal/ast/utilities.go Extended GetEffectiveTypeParent to handle @template hosts.
internal/ast/ast.go Added Host to JSDocTemplateTag and updated JSDocSignature.
internal/api/encoder/encoder.go Adjusted encoding mask to reflect renamed TypeParameters field.
Comments suppressed due to low confidence (1)

internal/parser/reparser.go:82

  • [nitpick] The indentation of typeAlias.Loc, typeAlias.Flags, and the append call is inconsistent with the surrounding block; aligning these lines will improve readability.
typeAlias.Loc = tag.Loc

typeAlias.Loc = tag.Loc
typeAlias.Flags = p.contextFlags | ast.NodeFlagsReparsed
p.reparseList = append(p.reparseList, typeAlias)
case ast.KindJSDocImportTag:
importTag := tag.AsJSDocImportTag()
importClause := importTag.ImportClause.Clone(&p.factory)
importClause := importTag.ImportClause
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

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

Cloning of importTag.ImportClause was removed, so setting Flags will mutate the shared AST node. Please clone the clause (e.g., importTag.ImportClause.Clone(&p.factory)) before updating its flags to avoid side-effects.

Suggested change
importClause := importTag.ImportClause
importClause := importTag.ImportClause.Clone(&p.factory)

Copilot uses AI. Check for mistakes.

Comment on lines +291 to +292
lastLocation = location
location = ast.GetEffectiveTypeParent(location.Parent)
Copy link
Preview

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

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

Assigning lastLocation before normalizing location means lastLocation may reference raw JSDoc nodes instead of their hosts, which could break self-reference detection. Consider updating lastLocation after computing the effective parent.

Copilot uses AI. Check for mistakes.

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

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

This seems like it regresses generic typedefs?

@@ -17,7 +17,7 @@
*/
transform(fn) {
->transform : { <U>(fn: (y: T) => U): U; <U>(): T; }
+>transform : { <U>(fn: (y: T) => U): U; <U_1>(): T; }
+>transform : { <U>(fn: (y: T) => U): U; (): T; }
Copy link
Member

Choose a reason for hiding this comment

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

This is spooky; now there's a func without type params.

Copy link
Member Author

Choose a reason for hiding this comment

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

intentional change that's an improvement in this case (T comes from the class not the function). see my comment.

Copy link
Member

Choose a reason for hiding this comment

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

I guess it's that sharing thing again. I feel like it can't be good to break it...

Copy link
Member Author

@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.

explanation of some test diffs

@@ -27,6 +28,8 @@
+ * @template A
+ * @template {Record<string, unknown>} B
+ * @param {Funcs<A, B>} fns
+ ~~~~~~~~~~~
Copy link
Member Author

Choose a reason for hiding this comment

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

template tags have to come before their usage now. If this restriction is lifted, then this test passes. But I really don't want to because it makes so little sense and complicates the code for choosing between possible hosts so much.

Copy link
Member

Choose a reason for hiding this comment

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

It's better than nothing to have it work, but I do wonder how breaky this would be.

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 will try to support this specific case (only one possible host in the comment, template after typedef) if somebody complains about it. But I really don't think anybody expects this to work.

@@ -6,27 +6,22 @@
const MyComponent = () => /* @type {any} */(null);
->MyComponent : StatelessComponent<MyComponentProps>
->() => /* @type {any} */(null) : { (): any; defaultProps: Partial<MyComponentProps>; }
+>MyComponent : { (): any; defaultProps?: P; }
+>() => /* @type {any} */(null) : { (): any; defaultProps: { color: string; }; }
+>MyComponent : { (): any; defaultProps?: Partial<{ color: "blue" | "red"; }>; }
Copy link
Member Author

Choose a reason for hiding this comment

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

type printing still de-aliases typedef when it shouldn't. It's on my list of things to investigate.

*/
function flatMap(array, iterable = identity) {
->flatMap : { <T, U>(array: T[], iterable: (x: T) => U[]): U[]; <T, U>(array: T[][]): T[]; }
+>flatMap : { <T, U>(array: T[], iterable: (x: T) => U[]): U[]; (array: T[][]): T[]; }
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 type parameter sharing means that the second overload would need to add a @template T in order to be correct.

Copy link
Member

Choose a reason for hiding this comment

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

This seems like it's going to end up being even more breaky...

Copy link
Member Author

Choose a reason for hiding this comment

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

@overload is usually written with separated jsdoc comments to avoid the ambiguity that arises when looking for the end of a JSDocSignature, so most code will already be written in a way that requires the non-shared template tags (template tags are not shared between jsdoc comments)

+>flatMap : { <T, U>(array: T[], iterable: (x: T) => U[]): U[]; (array: T[][]): T[]; }
>array : unknown[]
>iterable : (x: unknown) => unknown
->identity : <T_1>(x: T_1) => T_1
Copy link
Member Author

Choose a reason for hiding this comment

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

pretty sure T_1 was because the implementation signature had its own T in scope, but that's no longer shared.

* @typedef {import('./file1').Foo<T>} Bar
- /**
- * @template T
- * @typedef {import('./file1').Foo<T>} Bar
Copy link
Member Author

Choose a reason for hiding this comment

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

still not checking tags on EOF

+ export const x = () => 1
+ var res = x('a', 'b')
+ ~~~
Copy link
Member Author

Choose a reason for hiding this comment

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

...T no longer makes an argument with its type into a rest type

- }
- var z = new Zet(1)
- z.t = 2
- z.u = false
Copy link
Member Author

Choose a reason for hiding this comment

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

still no constructor functions

+ /**
+ * @template const T
+ ~~~~~
+!!! error TS1277: 'const' modifier can only appear on a type parameter of a function, method or class
Copy link
Member Author

Choose a reason for hiding this comment

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

oh nooo, I completely forgot about this feature. Not sure what is broken here exactly.

>flatMap : { <U>(): any; (): any; }
function flatMap(array, iterable = identity) {
->flatMap : { <U>(): any; (): any; }
+>flatMap : { (): any; (): any; }
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 more sharing (unintended in this case, so it's an improvement)

>dibbity : T1
>dibbity : T1

test(1) // ok, T=1
>test(1) : 1
->test : Test<number>
+>test : <T1 extends T>(data: T1) => T1
+>test : <T1 extends number>(data: T1) => T1
Copy link
Member Author

Choose a reason for hiding this comment

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

type printing still following aliases incorrectly

Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

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

Going to approve since it seems like it's net improving things, but the behavior changes do seem unfortunate and likely to bite us later

@@ -27,6 +28,8 @@
+ * @template A
+ * @template {Record<string, unknown>} B
+ * @param {Funcs<A, B>} fns
+ ~~~~~~~~~~~
Copy link
Member

Choose a reason for hiding this comment

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

It's better than nothing to have it work, but I do wonder how breaky this would be.

*/
function flatMap(array, iterable = identity) {
->flatMap : { <T, U>(array: T[], iterable: (x: T) => U[]): U[]; <T, U>(array: T[][]): T[]; }
+>flatMap : { <T, U>(array: T[], iterable: (x: T) => U[]): U[]; (array: T[][]): T[]; }
Copy link
Member

Choose a reason for hiding this comment

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

This seems like it's going to end up being even more breaky...

@@ -17,7 +17,7 @@
*/
transform(fn) {
->transform : { <U>(fn: (y: T) => U): U; <U>(): T; }
+>transform : { <U>(fn: (y: T) => U): U; <U_1>(): T; }
+>transform : { <U>(fn: (y: T) => U): U; (): T; }
Copy link
Member

Choose a reason for hiding this comment

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

I guess it's that sharing thing again. I feel like it can't be good to break it...

const f2 = async str => {
+ ~~
Copy link
Member

Choose a reason for hiding this comment

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

complex and very hard to support

this will not stop me from asking for the feature back 🫠

@sandersn sandersn enabled auto-merge June 13, 2025 22:40
@sandersn sandersn added this pull request to the merge queue Jun 13, 2025
Merged via the queue into microsoft:main with commit 2668cdf Jun 13, 2025
22 checks passed
@sandersn sandersn deleted the add-callback-tag branch June 13, 2025 23:08
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.

2 participants