Skip to content

Add @implements/@extends tags #1111

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 3 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
68 changes: 68 additions & 0 deletions internal/parser/reparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,55 @@ func (p *Parser) reparseHosted(tag *ast.Node, parent *ast.Node, jsDoc *ast.Node)
fun.FunctionLikeData().Type = p.makeNewType(tag.AsJSDocReturnTag().TypeExpression, fun)
}
}
case ast.KindJSDocImplementsTag:
if class := getClassLikeData(parent); class != nil {
implementsTag := tag.AsJSDocImplementsTag()

if class.HeritageClauses != nil {
if implementsClause := core.Find(class.HeritageClauses.Nodes, func(node *ast.Node) bool {
return node.AsHeritageClause().Token == ast.KindImplementsKeyword
}); implementsClause != nil {
implementsClause.AsHeritageClause().Types.Nodes = append(implementsClause.AsHeritageClause().Types.Nodes, implementsTag.ClassName)
return
}
}
types := p.nodeSlicePool.NewSlice(1)
Copy link
Member

Choose a reason for hiding this comment

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

At some point it'd be good to pull this "new slice size 1 then assign" pattern into a helper, given we do it a lot. I definitely had this on an old branch but I guess never sent it.

Copy link
Member Author

Choose a reason for hiding this comment

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

where should it go? I have a followup PR for this one that I can add it to. Maybe core?

Copy link
Member

Choose a reason for hiding this comment

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

Followup, it'd just be a method like NewSingleElementSlice(v T) or something

types[0] = implementsTag.ClassName
implementsTag.ClassName.Flags |= ast.NodeFlagsReparsed
typesList := p.newNodeList(implementsTag.ClassName.Loc, types)

heritageClause := p.factory.NewHeritageClause(ast.KindImplementsKeyword, typesList)
heritageClause.Loc = implementsTag.ClassName.Loc
heritageClause.Flags = p.contextFlags | ast.NodeFlagsReparsed

if class.HeritageClauses == nil {
heritageClauses := p.newNodeList(implementsTag.ClassName.Loc, p.nodeSlicePool.NewSlice(1))
heritageClauses.Nodes[0] = heritageClause
class.HeritageClauses = heritageClauses
} else {
class.HeritageClauses.Nodes = append(class.HeritageClauses.Nodes, heritageClause)
}
}
case ast.KindJSDocAugmentsTag:
if class := getClassLikeData(parent); class != nil && class.HeritageClauses != nil {
if extendsClause := core.Find(class.HeritageClauses.Nodes, func(node *ast.Node) bool {
return node.AsHeritageClause().Token == ast.KindExtendsKeyword
}); extendsClause != nil && len(extendsClause.AsHeritageClause().Types.Nodes) == 1 {
target := extendsClause.AsHeritageClause().Types.Nodes[0].AsExpressionWithTypeArguments()
source := tag.AsJSDocAugmentsTag().ClassName.AsExpressionWithTypeArguments()
if hasSamePropertyAccessName(target.Expression, source.Expression) {
if target.TypeArguments == nil && source.TypeArguments != nil {
target.TypeArguments = source.TypeArguments
for _, typeArg := range source.TypeArguments.Nodes {
typeArg.Flags |= ast.NodeFlagsReparsed
}
}
return
}
}
}
}
// !!! other attached tags (@this, @satisfies) support goes here
}

func (p *Parser) makeQuestionIfOptional(parameter *ast.JSDocParameterTag) *ast.Node {
Expand Down Expand Up @@ -338,3 +386,23 @@ func (p *Parser) makeNewType(typeExpression *ast.TypeNode, host *ast.Node) *ast.
t.Flags |= ast.NodeFlagsReparsed
return t
}

func hasSamePropertyAccessName(node1, node2 *ast.Node) bool {
if node1.Kind == ast.KindIdentifier && node2.Kind == ast.KindIdentifier {
return node1.Text() == node2.Text()
} else if node1.Kind == ast.KindPropertyAccessExpression && node2.Kind == ast.KindPropertyAccessExpression {
return node1.AsPropertyAccessExpression().Name().Text() == node2.AsPropertyAccessExpression().Name().Text() &&
hasSamePropertyAccessName(node1.AsPropertyAccessExpression().Expression, node2.AsPropertyAccessExpression().Expression)
}
return false
}

func getClassLikeData(parent *ast.Node) *ast.ClassLikeBase {
var class *ast.ClassLikeBase
if parent.Kind == ast.KindClassDeclaration {
class = parent.AsClassDeclaration().ClassLikeData()
} else if parent.Kind == ast.KindClassExpression {
class = parent.AsClassExpression().ClassLikeData()
}
return class
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
*/
class My extends Set {}
>My : My<T>
>Set : Set<any>
>Set : Set<T>

Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/a.js(26,17): error TS2314: Generic type 'A<T>' requires 1 type argument(s).
/a.js(34,17): error TS2314: Generic type 'A<T>' requires 1 type argument(s).
/a.js(39,17): error TS2314: Generic type 'A<T>' requires 1 type argument(s).
/a.js(44,17): error TS2314: Generic type 'A<T>' requires 1 type argument(s).
/a.js(29,16): error TS2344: Type '{ a: string; b: string; }' does not satisfy the constraint '{ a: string | number; b: boolean | string[]; }'.
Types of property 'b' are incompatible.
Type 'string' is not assignable to type 'boolean | string[]'.
/a.js(42,16): error TS2344: Type '{ a: string; b: string; }' does not satisfy the constraint '{ a: string | number; b: boolean | string[]; }'.
Types of property 'b' are incompatible.
Type 'string' is not assignable to type 'boolean | string[]'.


==== /a.js (4 errors) ====
==== /a.js (2 errors) ====
/**
* @typedef {{
* a: number | string;
Expand All @@ -31,30 +33,33 @@
* }>}
*/
class B extends A {}
~
!!! error TS2314: Generic type 'A<T>' requires 1 type argument(s).

/**
* @extends {A<{
~
* a: string,
~~~~~~~~~~~~~~~~~
* b: string
~~~~~~~~~~~~~~~~
* }>}
~~~~
!!! error TS2344: Type '{ a: string; b: string; }' does not satisfy the constraint '{ a: string | number; b: boolean | string[]; }'.
!!! error TS2344: Types of property 'b' are incompatible.
!!! error TS2344: Type 'string' is not assignable to type 'boolean | string[]'.
*/
class C extends A {}
~
!!! error TS2314: Generic type 'A<T>' requires 1 type argument(s).

/**
* @extends {A<{a: string, b: string[]}>}
*/
class D extends A {}
~
!!! error TS2314: Generic type 'A<T>' requires 1 type argument(s).

/**
* @extends {A<{a: string, b: string}>}
~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2344: Type '{ a: string; b: string; }' does not satisfy the constraint '{ a: string | number; b: boolean | string[]; }'.
!!! error TS2344: Types of property 'b' are incompatible.
!!! error TS2344: Type 'string' is not assignable to type 'boolean | string[]'.
*/
class E extends A {}
~
!!! error TS2314: Generic type 'A<T>' requires 1 type argument(s).

Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class A {
*/
class B extends A {}
>B : B
>A : typeof A
>A : A<{ a: string; b: string[]; }>

/**
* @extends {A<{
Expand All @@ -43,19 +43,19 @@ class B extends A {}
*/
class C extends A {}
>C : C
>A : typeof A
>A : A<{ a: string; b: string; }>

/**
* @extends {A<{a: string, b: string[]}>}
*/
class D extends A {}
>D : D
>A : typeof A
>A : A<{ a: string; b: string[]; }>

/**
* @extends {A<{a: string, b: string}>}
*/
class E extends A {}
>E : E
>A : typeof A
>A : A<{ a: string; b: string; }>

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/b.js(5,18): error TS1361: 'NS' cannot be used as a value because it was imported using 'import type'.
/b.js(6,14): error TS2420: Class 'C' incorrectly implements interface 'I'.
Property 'foo' is missing in type 'C' but required in type 'I'.


==== /a.ts (0 errors) ====
export interface I {
foo(): void;
}

==== /b.js (2 errors) ====
/**
* @import * as NS from './a'
*/

/** @implements {NS.I} */
~~
!!! error TS1361: 'NS' cannot be used as a value because it was imported using 'import type'.
!!! related TS1376 /b.js:2:17: 'NS' was imported here.
export class C {}
~
!!! error TS2420: Class 'C' incorrectly implements interface 'I'.
!!! error TS2420: Property 'foo' is missing in type 'C' but required in type 'I'.
!!! related TS2728 /a.ts:2:5: 'foo' is declared here.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
lib.js(3,17): error TS2552: Cannot find name 'IEncoder'. Did you mean 'Encoder'?


==== interface.ts (0 errors) ====
export interface Encoder<T> {
encode(value: T): Uint8Array
}
==== lib.js (1 errors) ====
/**
* @template T
* @implements {IEncoder<T>}
~~~~~~~~
!!! error TS2552: Cannot find name 'IEncoder'. Did you mean 'Encoder'?
!!! related TS2728 lib.js:5:14: 'Encoder' is declared here.
*/
export class Encoder {
/**
* @param {T} value
*/
encode(value) {
return new Uint8Array(0)
}
}


/**
* @template T
* @typedef {import('./interface').Encoder<T>} IEncoder
*/
Loading