Skip to content

Commit 34e68ef

Browse files
authored
Template tag allows specification of constraints (#24600)
* Parse (and mostly support) template tag constraints A bunch of tests hit the asserts I added though. * Messy version is finished. Need to add a few tests * Refactor to be smaller * Small refactor + Add one test * Another test * Minor cleanup * Fix error reporting on type parameters on ctors * Simplify syntax of `@template` tag This is a breaking change, but in my sample, nobody except webpack used the erroneous syntax. I need to improve the error message, so jsdocTemplateTag3 currently fails to remind me of that. * Better error message for template tag * Fix fourslash baselines * Another fourslash update * Address PR comments * Simplify getEffectiveTypeParameterDeclarations Make checkGrammarConstructorTypeParameters do a little more work
1 parent 2ce7e5f commit 34e68ef

28 files changed

+412
-57
lines changed

src/compiler/checker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28421,9 +28421,9 @@ namespace ts {
2842128421
}
2842228422

2842328423
function checkGrammarConstructorTypeParameters(node: ConstructorDeclaration) {
28424-
const typeParameters = getEffectiveTypeParameterDeclarations(node);
28425-
if (isNodeArray(typeParameters)) {
28426-
const { pos, end } = typeParameters;
28424+
const jsdocTypeParameters = isInJavaScriptFile(node) && getJSDocTypeParameterDeclarations(node);
28425+
if (node.typeParameters || jsdocTypeParameters && jsdocTypeParameters.length) {
28426+
const { pos, end } = node.typeParameters || jsdocTypeParameters && jsdocTypeParameters[0] || node;
2842728427
return grammarErrorAtPos(node, pos, end - pos, Diagnostics.Type_parameters_cannot_appear_on_a_constructor_declaration);
2842828428
}
2842928429
}

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@
219219
"category": "Error",
220220
"code": 1068
221221
},
222+
"Unexpected token. A type parameter name was expected without curly braces.": {
223+
"category": "Error",
224+
"code": 1069
225+
},
222226
"'{0}' modifier cannot appear on a type member.": {
223227
"category": "Error",
224228
"code": 1070

src/compiler/parser.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6993,29 +6993,27 @@ namespace ts {
69936993
}
69946994

69956995
function parseTemplateTag(atToken: AtToken, tagName: Identifier): JSDocTemplateTag | undefined {
6996-
if (some(tags, isJSDocTemplateTag)) {
6997-
parseErrorAt(tagName.pos, scanner.getTokenPos(), Diagnostics._0_tag_already_specified, tagName.escapedText);
6996+
// the template tag looks like '@template {Constraint} T,U,V'
6997+
let constraint: JSDocTypeExpression | undefined;
6998+
if (token() === SyntaxKind.OpenBraceToken) {
6999+
constraint = parseJSDocTypeExpression();
7000+
skipWhitespace();
69987001
}
69997002

7000-
// Type parameter list looks like '@template T,U,V'
70017003
const typeParameters = [];
70027004
const typeParametersPos = getNodePos();
7003-
70047005
while (true) {
70057006
const typeParameter = <TypeParameterDeclaration>createNode(SyntaxKind.TypeParameter);
7006-
const name = parseJSDocIdentifierNameWithOptionalBraces();
7007-
skipWhitespace();
7008-
if (!name) {
7009-
parseErrorAtPosition(scanner.getStartPos(), 0, Diagnostics.Identifier_expected);
7007+
if (!tokenIsIdentifierOrKeyword(token())) {
7008+
parseErrorAtCurrentToken(Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces);
70107009
return undefined;
70117010
}
7012-
7013-
typeParameter.name = name;
7011+
typeParameter.name = parseJSDocIdentifierName()!;
7012+
skipWhitespace();
70147013
finishNode(typeParameter);
7015-
70167014
typeParameters.push(typeParameter);
7017-
70187015
if (token() === SyntaxKind.CommaToken) {
7016+
// need to look for more type parameters
70197017
nextJSDocToken();
70207018
skipWhitespace();
70217019
}
@@ -7024,6 +7022,10 @@ namespace ts {
70247022
}
70257023
}
70267024

7025+
if (constraint) {
7026+
first(typeParameters).constraint = constraint.type;
7027+
}
7028+
70277029
const result = <JSDocTemplateTag>createNode(SyntaxKind.JSDocTemplateTag, atToken.pos);
70287030
result.atToken = atToken;
70297031
result.tagName = tagName;
@@ -7032,15 +7034,6 @@ namespace ts {
70327034
return result;
70337035
}
70347036

7035-
function parseJSDocIdentifierNameWithOptionalBraces(): Identifier | undefined {
7036-
const parsedBrace = parseOptional(SyntaxKind.OpenBraceToken);
7037-
const res = parseJSDocIdentifierName();
7038-
if (parsedBrace) {
7039-
parseExpected(SyntaxKind.CloseBraceToken);
7040-
}
7041-
return res;
7042-
}
7043-
70447037
function nextJSDocToken(): JsDocSyntaxKind {
70457038
return currentToken = scanner.scanJSDocToken();
70467039
}

src/compiler/utilities.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3164,21 +3164,18 @@ namespace ts {
31643164
}
31653165
if (isJSDocTypeAlias(node)) {
31663166
Debug.assert(node.parent.kind === SyntaxKind.JSDocComment);
3167-
const templateTags = flatMap(filter(node.parent.tags, isJSDocTemplateTag), tag => tag.typeParameters) as ReadonlyArray<TypeParameterDeclaration>;
3168-
const templateTagNodes = templateTags as NodeArray<TypeParameterDeclaration>;
3169-
templateTagNodes.pos = templateTagNodes.length > 0 ? first(templateTagNodes).pos : node.pos;
3170-
templateTagNodes.end = templateTagNodes.length > 0 ? last(templateTagNodes).end : node.end;
3171-
templateTagNodes.hasTrailingComma = false;
3172-
return templateTagNodes;
3167+
return flatMap(node.parent.tags, tag => isJSDocTemplateTag(tag) ? tag.typeParameters : undefined) as ReadonlyArray<TypeParameterDeclaration>;
31733168
}
31743169
return node.typeParameters || (isInJavaScriptFile(node) ? getJSDocTypeParameterDeclarations(node) : emptyArray);
31753170
}
31763171

31773172
export function getJSDocTypeParameterDeclarations(node: DeclarationWithTypeParameters): ReadonlyArray<TypeParameterDeclaration> {
3178-
// template tags are only available when a typedef isn't already using them
3179-
const tag = find(getJSDocTags(node), (tag): tag is JSDocTemplateTag =>
3180-
isJSDocTemplateTag(tag) && !(tag.parent.kind === SyntaxKind.JSDocComment && tag.parent.tags!.some(isJSDocTypeAlias)));
3181-
return (tag && tag.typeParameters) || emptyArray;
3173+
return flatMap(getJSDocTags(node), tag => isNonTypeAliasTemplate(tag) ? tag.typeParameters : undefined);
3174+
}
3175+
3176+
/** template tags are only available when a typedef isn't already using them */
3177+
function isNonTypeAliasTemplate(tag: JSDocTag): tag is JSDocTemplateTag {
3178+
return isJSDocTemplateTag(tag) && !(tag.parent.kind === SyntaxKind.JSDocComment && tag.parent.tags!.some(isJSDocTypeAlias));
31823179
}
31833180

31843181
/**

tests/baselines/reference/jsdocTemplateClass.errors.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ tests/cases/conformance/jsdoc/templateTagOnClasses.js(25,1): error TS2322: Type
33

44
==== tests/cases/conformance/jsdoc/templateTagOnClasses.js (1 errors) ====
55
/**
6-
* @template {T}
6+
* @template T
77
* @typedef {(t: T) => T} Id
88
*/
99
/** @template T */

tests/baselines/reference/jsdocTemplateClass.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
=== tests/cases/conformance/jsdoc/templateTagOnClasses.js ===
22
/**
3-
* @template {T}
3+
* @template T
44
* @typedef {(t: T) => T} Id
55
*/
66
/** @template T */

tests/baselines/reference/jsdocTemplateClass.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
=== tests/cases/conformance/jsdoc/templateTagOnClasses.js ===
22
/**
3-
* @template {T}
3+
* @template T
44
* @typedef {(t: T) => T} Id
55
*/
66
/** @template T */

tests/baselines/reference/jsdocTemplateConstructorFunction.errors.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ tests/cases/conformance/jsdoc/templateTagOnConstructorFunctions.js(24,1): error
33

44
==== tests/cases/conformance/jsdoc/templateTagOnConstructorFunctions.js (1 errors) ====
55
/**
6-
* @template {U}
6+
* @template U
77
* @typedef {(u: U) => U} Id
88
*/
99
/**
1010
* @param {T} t
11-
* @template {T}
11+
* @template T
1212
*/
1313
function Zet(t) {
1414
/** @type {T} */

tests/baselines/reference/jsdocTemplateConstructorFunction.symbols

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
=== tests/cases/conformance/jsdoc/templateTagOnConstructorFunctions.js ===
22
/**
3-
* @template {U}
3+
* @template U
44
* @typedef {(u: U) => U} Id
55
*/
66
/**
77
* @param {T} t
8-
* @template {T}
8+
* @template T
99
*/
1010
function Zet(t) {
1111
>Zet : Symbol(Zet, Decl(templateTagOnConstructorFunctions.js, 0, 0))

tests/baselines/reference/jsdocTemplateConstructorFunction.types

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
=== tests/cases/conformance/jsdoc/templateTagOnConstructorFunctions.js ===
22
/**
3-
* @template {U}
3+
* @template U
44
* @typedef {(u: U) => U} Id
55
*/
66
/**
77
* @param {T} t
8-
* @template {T}
8+
* @template T
99
*/
1010
function Zet(t) {
1111
>Zet : typeof Zet

tests/baselines/reference/jsdocTemplateConstructorFunction2.errors.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ tests/cases/conformance/jsdoc/templateTagWithNestedTypeLiteral.js(26,15): error
55
==== tests/cases/conformance/jsdoc/templateTagWithNestedTypeLiteral.js (2 errors) ====
66
/**
77
* @param {T} t
8-
* @template {T}
8+
* @template T
99
*/
1010
function Zet(t) {
1111
/** @type {T} */

tests/baselines/reference/jsdocTemplateConstructorFunction2.symbols

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
=== tests/cases/conformance/jsdoc/templateTagWithNestedTypeLiteral.js ===
22
/**
33
* @param {T} t
4-
* @template {T}
4+
* @template T
55
*/
66
function Zet(t) {
77
>Zet : Symbol(Zet, Decl(templateTagWithNestedTypeLiteral.js, 0, 0))

tests/baselines/reference/jsdocTemplateConstructorFunction2.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
=== tests/cases/conformance/jsdoc/templateTagWithNestedTypeLiteral.js ===
22
/**
33
* @param {T} t
4-
* @template {T}
4+
* @template T
55
*/
66
function Zet(t) {
77
>Zet : typeof Zet
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
tests/cases/conformance/jsdoc/a.js(14,29): error TS2339: Property 'a' does not exist on type 'U'.
2+
tests/cases/conformance/jsdoc/a.js(14,35): error TS2339: Property 'b' does not exist on type 'U'.
3+
tests/cases/conformance/jsdoc/a.js(21,3): error TS2345: Argument of type '{ a: number; }' is not assignable to parameter of type '{ a: number; b: string; }'.
4+
Property 'b' is missing in type '{ a: number; }'.
5+
tests/cases/conformance/jsdoc/a.js(25,2): error TS1069: Unexpected token. A type parameter name was expected without curly braces.
6+
7+
8+
==== tests/cases/conformance/jsdoc/a.js (4 errors) ====
9+
/**
10+
* @template {{ a: number, b: string }} T,U A Comment
11+
* @template {{ c: boolean }} V uh ... are comments even supported??
12+
* @template W
13+
* @template X That last one had no comment
14+
* @param {T} t
15+
* @param {U} u
16+
* @param {V} v
17+
* @param {W} w
18+
* @param {X} x
19+
* @return {W | X}
20+
*/
21+
function f(t, u, v, w, x) {
22+
if(t.a + t.b.length > u.a - u.b.length && v.c) {
23+
~
24+
!!! error TS2339: Property 'a' does not exist on type 'U'.
25+
~
26+
!!! error TS2339: Property 'b' does not exist on type 'U'.
27+
return w;
28+
}
29+
return x;
30+
}
31+
32+
f({ a: 12, b: 'hi', c: null }, undefined, { c: false, d: 12, b: undefined }, 101, 'nope');
33+
f({ a: 12 }, undefined, undefined, 101, 'nope');
34+
~~~~~~~~~~
35+
!!! error TS2345: Argument of type '{ a: number; }' is not assignable to parameter of type '{ a: number; b: string; }'.
36+
!!! error TS2345: Property 'b' is missing in type '{ a: number; }'.
37+
38+
/**
39+
* @template {NoLongerAllowed}
40+
* @template T preceding line's syntax is no longer allowed
41+
~
42+
!!! error TS1069: Unexpected token. A type parameter name was expected without curly braces.
43+
* @param {T} x
44+
*/
45+
function g(x) { }
46+
47+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
=== tests/cases/conformance/jsdoc/a.js ===
2+
/**
3+
* @template {{ a: number, b: string }} T,U A Comment
4+
* @template {{ c: boolean }} V uh ... are comments even supported??
5+
* @template W
6+
* @template X That last one had no comment
7+
* @param {T} t
8+
* @param {U} u
9+
* @param {V} v
10+
* @param {W} w
11+
* @param {X} x
12+
* @return {W | X}
13+
*/
14+
function f(t, u, v, w, x) {
15+
>f : Symbol(f, Decl(a.js, 0, 0))
16+
>t : Symbol(t, Decl(a.js, 12, 11))
17+
>u : Symbol(u, Decl(a.js, 12, 13))
18+
>v : Symbol(v, Decl(a.js, 12, 16))
19+
>w : Symbol(w, Decl(a.js, 12, 19))
20+
>x : Symbol(x, Decl(a.js, 12, 22))
21+
22+
if(t.a + t.b.length > u.a - u.b.length && v.c) {
23+
>t.a : Symbol(a, Decl(a.js, 1, 15))
24+
>t : Symbol(t, Decl(a.js, 12, 11))
25+
>a : Symbol(a, Decl(a.js, 1, 15))
26+
>t.b.length : Symbol(String.length, Decl(lib.d.ts, --, --))
27+
>t.b : Symbol(b, Decl(a.js, 1, 26))
28+
>t : Symbol(t, Decl(a.js, 12, 11))
29+
>b : Symbol(b, Decl(a.js, 1, 26))
30+
>length : Symbol(String.length, Decl(lib.d.ts, --, --))
31+
>u : Symbol(u, Decl(a.js, 12, 13))
32+
>u : Symbol(u, Decl(a.js, 12, 13))
33+
>v.c : Symbol(c, Decl(a.js, 2, 15))
34+
>v : Symbol(v, Decl(a.js, 12, 16))
35+
>c : Symbol(c, Decl(a.js, 2, 15))
36+
37+
return w;
38+
>w : Symbol(w, Decl(a.js, 12, 19))
39+
}
40+
return x;
41+
>x : Symbol(x, Decl(a.js, 12, 22))
42+
}
43+
44+
f({ a: 12, b: 'hi', c: null }, undefined, { c: false, d: 12, b: undefined }, 101, 'nope');
45+
>f : Symbol(f, Decl(a.js, 0, 0))
46+
>a : Symbol(a, Decl(a.js, 19, 3))
47+
>b : Symbol(b, Decl(a.js, 19, 10))
48+
>c : Symbol(c, Decl(a.js, 19, 19))
49+
>undefined : Symbol(undefined)
50+
>c : Symbol(c, Decl(a.js, 19, 43))
51+
>d : Symbol(d, Decl(a.js, 19, 53))
52+
>b : Symbol(b, Decl(a.js, 19, 60))
53+
>undefined : Symbol(undefined)
54+
55+
f({ a: 12 }, undefined, undefined, 101, 'nope');
56+
>f : Symbol(f, Decl(a.js, 0, 0))
57+
>a : Symbol(a, Decl(a.js, 20, 3))
58+
>undefined : Symbol(undefined)
59+
>undefined : Symbol(undefined)
60+
61+
/**
62+
* @template {NoLongerAllowed}
63+
* @template T preceding line's syntax is no longer allowed
64+
* @param {T} x
65+
*/
66+
function g(x) { }
67+
>g : Symbol(g, Decl(a.js, 20, 49))
68+
>x : Symbol(x, Decl(a.js, 27, 11))
69+
70+

0 commit comments

Comments
 (0)