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

0 commit comments

Comments
 (0)