Skip to content

Commit 3e7f08e

Browse files
committed
feat: handle unmatched jsdoc parameters
1 parent 0c36c62 commit 3e7f08e

20 files changed

+376
-239
lines changed

internal/checker/checker.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2554,6 +2554,7 @@ func (c *Checker) checkSignatureDeclaration(node *ast.Node) {
25542554
c.checkGrammarFunctionLikeDeclaration(node)
25552555
}
25562556
c.checkTypeParameters(node.TypeParameters())
2557+
c.checkUnmatchedJSDocParameters(node)
25572558
c.checkSourceElements(node.Parameters())
25582559
returnTypeNode := node.Type()
25592560
if returnTypeNode != nil {
@@ -30482,6 +30483,36 @@ func (c *Checker) getRegularTypeOfExpression(expr *ast.Node) *Type {
3048230483
return c.getRegularTypeOfLiteralType(c.getTypeOfExpression(expr))
3048330484
}
3048430485

30486+
func (c *Checker) containsArgumentsReference(node *ast.Node) bool {
30487+
links := c.nodeLinks.Get(node)
30488+
if links.containsArgumentsReference == core.TSUnknown {
30489+
var visit func(node *ast.Node) bool
30490+
visit = func(node *ast.Node) bool {
30491+
if node == nil {
30492+
return false
30493+
}
30494+
switch node.Kind {
30495+
case ast.KindIdentifier:
30496+
return node.Text() == c.argumentsSymbol.Name && c.IsArgumentsSymbol(c.getResolvedSymbol(node))
30497+
case ast.KindPropertyDeclaration, ast.KindMethodDeclaration, ast.KindGetAccessor, ast.KindSetAccessor:
30498+
if ast.IsComputedPropertyName(node.Name()) {
30499+
return visit(node.Name())
30500+
}
30501+
case ast.KindPropertyAccessExpression, ast.KindElementAccessExpression:
30502+
return visit(node.Expression())
30503+
case ast.KindPropertyAssignment:
30504+
return visit(node.AsPropertyAssignment().Initializer)
30505+
}
30506+
if nodeStartsNewLexicalEnvironment(node) || ast.IsPartOfTypeNode(node) {
30507+
return false
30508+
}
30509+
return node.ForEachChild(visit)
30510+
}
30511+
links.containsArgumentsReference = core.IfElse(visit(node.Body()), core.TSTrue, core.TSFalse)
30512+
}
30513+
return links.containsArgumentsReference == core.TSTrue
30514+
}
30515+
3048530516
func (c *Checker) GetTypeAtLocation(node *ast.Node) *Type {
3048630517
return c.getTypeOfNode(node)
3048730518
}

internal/checker/jsdoc.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package checker
2+
3+
import (
4+
"github.com/microsoft/typescript-go/internal/ast"
5+
"github.com/microsoft/typescript-go/internal/collections"
6+
"github.com/microsoft/typescript-go/internal/diagnostics"
7+
)
8+
9+
func (c *Checker) checkUnmatchedJSDocParameters(node *ast.Node) {
10+
var jsdocParameters []*ast.Node
11+
for _, tag := range getAllJSDocTags(node) {
12+
if tag.Kind == ast.KindJSDocParameterTag {
13+
name := tag.AsJSDocParameterOrPropertyTag().Name()
14+
if ast.IsIdentifier(name) && len(name.Text()) == 0 {
15+
continue
16+
}
17+
jsdocParameters = append(jsdocParameters, tag)
18+
}
19+
}
20+
21+
if len(jsdocParameters) == 0 {
22+
return
23+
}
24+
25+
isJs := ast.IsInJSFile(node)
26+
parameters := collections.Set[string]{}
27+
excludedParameters := collections.Set[int]{}
28+
29+
for i, param := range node.Parameters() {
30+
name := param.AsParameterDeclaration().Name()
31+
if ast.IsIdentifier(name) {
32+
parameters.Add(name.Text())
33+
}
34+
if ast.IsBindingPattern(name) {
35+
excludedParameters.Add(i)
36+
}
37+
}
38+
if c.containsArgumentsReference(node) {
39+
lastJSDocParamIndex := len(jsdocParameters) - 1
40+
lastJSDocParam := jsdocParameters[lastJSDocParamIndex].AsJSDocParameterOrPropertyTag()
41+
if isJs && lastJSDocParam != nil && ast.IsIdentifier(lastJSDocParam.Name()) && lastJSDocParam.TypeExpression != nil &&
42+
lastJSDocParam.TypeExpression.Type() != nil && !parameters.Has(lastJSDocParam.Name().Text()) && !excludedParameters.Has(lastJSDocParamIndex) {
43+
c.error(lastJSDocParam.Name(), diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name_It_would_match_arguments_if_it_had_an_array_type, lastJSDocParam.Name().Text())
44+
}
45+
} else {
46+
for index, tag := range jsdocParameters {
47+
name := tag.AsJSDocParameterOrPropertyTag().Name()
48+
isNameFirst := tag.AsJSDocParameterOrPropertyTag().IsNameFirst
49+
50+
if excludedParameters.Has(index) || (ast.IsIdentifier(name) && parameters.Has(name.Text())) {
51+
continue
52+
}
53+
54+
if ast.IsQualifiedName(name) {
55+
if isJs {
56+
c.error(name, diagnostics.Qualified_name_0_is_not_allowed_without_a_leading_param_object_1,
57+
entityNameToString(name),
58+
entityNameToString(name.AsQualifiedName().Left),
59+
)
60+
}
61+
} else {
62+
if !isNameFirst {
63+
c.errorOrSuggestion(isJs, name,
64+
diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name,
65+
name.Text(),
66+
)
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
func getAllJSDocTags(node *ast.Node) []*ast.Node {
74+
if node == nil {
75+
return nil
76+
}
77+
tags := []*ast.Node{}
78+
for _, jsdoc := range node.JSDoc(nil) {
79+
jsdocTags := jsdoc.AsJSDoc().Tags
80+
if jsdocTags == nil {
81+
continue
82+
}
83+
tags = append(tags, jsdocTags.Nodes...)
84+
}
85+
return tags
86+
}

internal/checker/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ type NodeLinks struct {
362362
flags NodeCheckFlags // Set of flags specific to Node
363363
declarationRequiresScopeChange core.Tristate // Set by `useOuterVariableScopeInParameter` in checker when downlevel emit would change the name resolution scope inside of a parameter.
364364
hasReportedStatementInAmbientContext bool // Cache boolean if we report statements in ambient context
365+
containsArgumentsReference core.Tristate // Whether a function-like declaration contains an 'arguments' reference
365366
}
366367

367368
type SymbolNodeLinks struct {

internal/checker/utilities.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,3 +1952,12 @@ func ValueToString(value any) string {
19521952
}
19531953
panic("unhandled value type in valueToString")
19541954
}
1955+
1956+
func nodeStartsNewLexicalEnvironment(node *ast.Node) bool {
1957+
switch node.Kind {
1958+
case ast.KindConstructor, ast.KindFunctionExpression, ast.KindFunctionDeclaration, ast.KindArrowFunction,
1959+
ast.KindMethodDeclaration, ast.KindGetAccessor, ast.KindSetAccessor, ast.KindModuleDeclaration, ast.KindSourceFile:
1960+
return true
1961+
}
1962+
return false
1963+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/a.js(1,21): error TS8024: JSDoc '@param' tag has name 'colour', but there is no parameter with that name.
2+
3+
4+
==== /a.js (1 errors) ====
5+
/** @param {string} colour */
6+
~~~~~~
7+
!!! error TS8024: JSDoc '@param' tag has name 'colour', but there is no parameter with that name.
8+
function f(color) {}
9+
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
0.js(70,20): error TS8024: JSDoc '@param' tag has name 'y', but there is no parameter with that name.
2+
3+
4+
==== 0.js (1 errors) ====
5+
// Object literal syntax
6+
/**
7+
* @param {{a: string, b: string}} obj
8+
* @param {string} x
9+
*/
10+
function good1({a, b}, x) {}
11+
/**
12+
* @param {{a: string, b: string}} obj
13+
* @param {{c: number, d: number}} OBJECTION
14+
*/
15+
function good2({a, b}, {c, d}) {}
16+
/**
17+
* @param {number} x
18+
* @param {{a: string, b: string}} obj
19+
* @param {string} y
20+
*/
21+
function good3(x, {a, b}, y) {}
22+
/**
23+
* @param {{a: string, b: string}} obj
24+
*/
25+
function good4({a, b}) {}
26+
27+
// nested object syntax
28+
/**
29+
* @param {Object} obj
30+
* @param {string} obj.a - this is like the saddest way to specify a type
31+
* @param {string} obj.b - but it sure does allow a lot of documentation
32+
* @param {string} x
33+
*/
34+
function good5({a, b}, x) {}
35+
/**
36+
* @param {Object} obj
37+
* @param {string} obj.a
38+
* @param {string} obj.b - but it sure does allow a lot of documentation
39+
* @param {Object} OBJECTION - documentation here too
40+
* @param {string} OBJECTION.c
41+
* @param {string} OBJECTION.d - meh
42+
*/
43+
function good6({a, b}, {c, d}) {}
44+
/**
45+
* @param {number} x
46+
* @param {Object} obj
47+
* @param {string} obj.a
48+
* @param {string} obj.b
49+
* @param {string} y
50+
*/
51+
function good7(x, {a, b}, y) {}
52+
/**
53+
* @param {Object} obj
54+
* @param {string} obj.a
55+
* @param {string} obj.b
56+
*/
57+
function good8({a, b}) {}
58+
59+
/**
60+
* @param {{ a: string }} argument
61+
*/
62+
function good9({ a }) {
63+
console.log(arguments, a);
64+
}
65+
66+
/**
67+
* @param {object} obj - this type gets ignored
68+
* @param {string} obj.a
69+
* @param {string} obj.b - and x's type gets used for both parameters
70+
* @param {string} x
71+
*/
72+
function bad1(x, {a, b}) {}
73+
/**
74+
* @param {string} y - here, y's type gets ignored but obj's is fine
75+
~
76+
!!! error TS8024: JSDoc '@param' tag has name 'y', but there is no parameter with that name.
77+
* @param {{a: string, b: string}} obj
78+
*/
79+
function bad2(x, {a, b}) {}
80+
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
0.js(3,20): error TS8024: JSDoc '@param' tag has name 'unrelated', but there is no parameter with that name.
2+
3+
4+
==== 0.js (1 errors) ====
5+
/**
6+
* @param {Object} notSpecial
7+
* @param {string} unrelated - not actually related because it's not notSpecial.unrelated
8+
~~~~~~~~~
9+
!!! error TS8024: JSDoc '@param' tag has name 'unrelated', but there is no parameter with that name.
10+
*/
11+
function normal(notSpecial) {
12+
notSpecial; // should just be 'Object'
13+
}
14+
normal(12);
15+
16+
/**
17+
* @param {Object} opts1 doc1
18+
* @param {string} opts1.x doc2
19+
* @param {string=} opts1.y doc3
20+
* @param {string} [opts1.z] doc4
21+
* @param {string} [opts1.w="hi"] doc5
22+
*/
23+
function foo1(opts1) {
24+
opts1.x;
25+
}
26+
27+
foo1({x: 'abc'});
28+
29+
/**
30+
* @param {Object[]} opts2
31+
* @param {string} opts2[].anotherX
32+
* @param {string=} opts2[].anotherY
33+
*/
34+
function foo2(/** @param opts2 bad idea theatre! */opts2) {
35+
opts2[0].anotherX;
36+
}
37+
38+
foo2([{anotherX: "world"}]);
39+
40+
/**
41+
* @param {object} opts3
42+
* @param {string} opts3.x
43+
*/
44+
function foo3(opts3) {
45+
opts3.x;
46+
}
47+
foo3({x: 'abc'});
48+
49+
/**
50+
* @param {object[]} opts4
51+
* @param {string} opts4[].x
52+
* @param {string=} opts4[].y
53+
* @param {string} [opts4[].z]
54+
* @param {string} [opts4[].w="hi"]
55+
*/
56+
function foo4(opts4) {
57+
opts4[0].x;
58+
}
59+
60+
foo4([{ x: 'hi' }]);
61+
62+
/**
63+
* @param {object[]} opts5 - Let's test out some multiple nesting levels
64+
* @param {string} opts5[].help - (This one is just normal)
65+
* @param {object} opts5[].what - Look at us go! Here's the first nest!
66+
* @param {string} opts5[].what.a - (Another normal one)
67+
* @param {Object[]} opts5[].what.bad - Now we're nesting inside a nested type
68+
* @param {string} opts5[].what.bad[].idea - I don't think you can get back out of this level...
69+
* @param {boolean} opts5[].what.bad[].oh - Oh ... that's how you do it.
70+
* @param {number} opts5[].unnest - Here we are almost all the way back at the beginning.
71+
*/
72+
function foo5(opts5) {
73+
opts5[0].what.bad[0].idea;
74+
opts5[0].unnest;
75+
}
76+
77+
foo5([{ help: "help", what: { a: 'a', bad: [{ idea: 'idea', oh: false }] }, unnest: 1 }]);
78+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
paramTagNestedWithoutTopLevelObject.js(2,20): error TS8032: Qualified name 'xyz.p' is not allowed without a leading '@param {object} xyz'.
2+
3+
4+
==== paramTagNestedWithoutTopLevelObject.js (1 errors) ====
5+
/**
6+
* @param {number} xyz.p
7+
~~~~~
8+
!!! error TS8032: Qualified name 'xyz.p' is not allowed without a leading '@param {object} xyz'.
9+
*/
10+
function g(xyz) {
11+
return xyz.p;
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
paramTagNestedWithoutTopLevelObject2.js(2,20): error TS8032: Qualified name 'xyz.bar' is not allowed without a leading '@param {object} xyz'.
2+
3+
4+
==== paramTagNestedWithoutTopLevelObject2.js (1 errors) ====
5+
/**
6+
* @param {object} xyz.bar
7+
~~~~~~~
8+
!!! error TS8032: Qualified name 'xyz.bar' is not allowed without a leading '@param {object} xyz'.
9+
* @param {number} xyz.bar.p
10+
*/
11+
function g(xyz) {
12+
return xyz.bar.p;
13+
}

testdata/baselines/reference/submodule/conformance/paramTagNestedWithoutTopLevelObject3.errors.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
paramTagNestedWithoutTopLevelObject3.js(3,20): error TS8032: Qualified name 'xyz.bar.p' is not allowed without a leading '@param {object} xyz.bar'.
12
paramTagNestedWithoutTopLevelObject3.js(6,16): error TS2339: Property 'bar' does not exist on type 'object'.
23

34

4-
==== paramTagNestedWithoutTopLevelObject3.js (1 errors) ====
5+
==== paramTagNestedWithoutTopLevelObject3.js (2 errors) ====
56
/**
67
* @param {object} xyz
78
* @param {number} xyz.bar.p
9+
~~~~~~~~~
10+
!!! error TS8032: Qualified name 'xyz.bar.p' is not allowed without a leading '@param {object} xyz.bar'.
811
*/
912
function g(xyz) {
1013
return xyz.bar.p;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
paramTagNestedWithoutTopLevelObject4.js(2,20): error TS8032: Qualified name 'xyz.bar.p' is not allowed without a leading '@param {object} xyz.bar'.
2+
3+
4+
==== paramTagNestedWithoutTopLevelObject4.js (1 errors) ====
5+
/**
6+
* @param {number} xyz.bar.p
7+
~~~~~~~~~
8+
!!! error TS8032: Qualified name 'xyz.bar.p' is not allowed without a leading '@param {object} xyz.bar'.
9+
*/
10+
function g(xyz) {
11+
return xyz.bar.p;
12+
}

0 commit comments

Comments
 (0)