-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Template tag allows specification of constraints #24600
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
Conversation
A bunch of tests hit the asserts I added though.
After checking the user tests and other bunches of JS I have cloned from github, I think the right thing to do is to simplify the syntax to disallow curlies around the type parameter name. This is technically a breaking change, but Closure never allowed this so the number of actual breaks will be low: Of the 3400 |
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.
if (!name) { | ||
parseErrorAtPosition(scanner.getStartPos(), 0, Diagnostics.Identifier_expected); | ||
if (!tokenIsIdentifierOrKeyword(token())) { | ||
parseErrorAtCurrentToken(Diagnostics.Unexpected_token_A_type_parameter_name_was_expected_without_curly_braces); | ||
return undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was already this way, but it looks like parseTemplateTag
is the only tag-parsing method out of 9 that can return undefined. Might be better to call parseJSDocIdentifierName
with createIfMissing
set, and a new parameter to allow a custom diagnostic. Than parseTag
could return JSDocTag
(without | undefined
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
parseTag returns undefined near the beginning if it can't parseJSDocIdentifierName, too. As-is the change is not worth it in my opinion. It might be worthwhile to have parseJSDocIdentifierName always return a missing node. I tried that and it looks a bit better, but there's a good bit of churn. Would you like to review it as part of this PR or would you like to see it in a separate one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separate is good.
src/compiler/parser.ts
Outdated
@@ -7044,6 +7042,11 @@ namespace ts { | |||
} | |||
} | |||
|
|||
if (constraint) { | |||
Debug.assert(!!typeParameters.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
first(typeParameters)
will do this assertion for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, didn't think about using first(typeParameters).constraint
as an lhs
src/compiler/utilities.ts
Outdated
const tag = find(getJSDocTags(node), (tag): tag is JSDocTemplateTag => | ||
isJSDocTemplateTag(tag) && !(tag.parent.kind === SyntaxKind.JSDocComment && tag.parent.tags!.some(isJSDocTypeAlias))); | ||
return (tag && tag.typeParameters) || emptyArray; | ||
return flatMap(filter(getJSDocTags(node), isNonTypeAliasTemplate), tag => tag.typeParameters); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flatMap(getJSDocTags(node), tag => isNonTypeAliasTemplate(tag) ? tag.typeParameters : undefined);
@@ -17,7 +17,6 @@ verify.signatureHelp({ | |||
text: "find<T>(l: T[], x: T): T", | |||
docComment: "Find an item", | |||
tags: [ | |||
// TODO: GH#24130 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't look like #24130 is fixed here -- text still contains "\n ".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the comment back in, with a pointer to this PR.
src/compiler/checker.ts
Outdated
@@ -28417,8 +28417,8 @@ namespace ts { | |||
|
|||
function checkGrammarConstructorTypeParameters(node: ConstructorDeclaration) { | |||
const typeParameters = getEffectiveTypeParameterDeclarations(node); | |||
if (isNodeArray(typeParameters)) { | |||
const { pos, end } = typeParameters; | |||
if (isNodeArray(typeParameters) || typeParameters.length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when is it not a NodeArray?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getEffectiveTypeParameterDeclarations returns a ReadonlyArray now. Previously it returned templateTags[0].typeParameters
, which was a NodeArray. Now it returns flatMap(templateTags, t => t.typeParameters)
, which is a ReadonlyArray.
I don't think it's worth the trouble to create a NodeArray because the resulting span would stretch across all 3 template tags below, which is very ugly:
/** @template T
* @template U
* @template V
*/
constructor() { }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the past we stuck a pos
and an end
on the result to make it look like a NodeArray.. why can not we do the same here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually looking at the code, that is exactly what we do.. it looks like it will always be a NodeArray
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In getEffectiveTypeParameterDeclarations, the code path that calls getJSDocTypeParameterDeclarations ends up calling flatMap, which is what returns the non-NodeArray:
/**
* Gets the effective type parameters. If the node was parsed in a
* JavaScript file, gets the type parameters from the `@template` tag from JSDoc.
*/
export function getEffectiveTypeParameterDeclarations(node: DeclarationWithTypeParameters): ReadonlyArray<TypeParameterDeclaration> {
if (isJSDocSignature(node)) {
return emptyArray; /*** Not a NodeArray ***/
}
if (isJSDocTypeAlias(node)) {
Debug.assert(node.parent.kind === SyntaxKind.JSDocComment);
const templateTags = flatMap(filter(node.parent.tags, isJSDocTemplateTag), tag => tag.typeParameters) as ReadonlyArray<TypeParameterDeclaration>;
const templateTagNodes = templateTags as NodeArray<TypeParameterDeclaration>;
templateTagNodes.pos = templateTagNodes.length > 0 ? first(templateTagNodes).pos : node.pos;
templateTagNodes.end = templateTagNodes.length > 0 ? last(templateTagNodes).end : node.end;
templateTagNodes.hasTrailingComma = false;
return templateTagNodes;
}
return node.typeParameters || (isInJavaScriptFile(node) ? getJSDocTypeParameterDeclarations(node) : emptyArray);
}
export function getJSDocTypeParameterDeclarations(node: DeclarationWithTypeParameters): ReadonlyArray<TypeParameterDeclaration> {
/*** flatMap does not return a NodeArray ***/
return flatMap(getJSDocTags(node), tag => isNonTypeAliasTemplate(tag) ? tag.typeParameters : undefined);
}
The jsdoc signature case returns emptyArray, which is also not a NodeArray.
Make checkGrammarConstructorTypeParameters do a little more work
This change introduces new syntax to allow the
@template
tag to specify constraints on type parameters, which is used in JSDoc to declare type parameters.Fixes #24283
Basic syntax
The constraint is a jsdoc type expression surrounded by curly braces. The type parameter name follows, not surrounded by curlies:
Notable decisions
Which parameter is constrained when multiple parameters are specified with a single tag?
Only the first parameter is constrained.
I chose this way because:
char* c,d,b;
andint i,j,k = 0;
What non-standard syntax should be allowed, and what are the semantics?
Closure does not allow the type parameter names to be enclosed with curly braces, but, currently, Typescript does because of the similarity because the type declaration reminds people a lot of a type reference. However, the thing between the curlies is still just an Identifier, not a TypeNode.
This first line is now ambiguous, unfortunately. Before this PR, it was equivalent to the typescript
<T>
. Now it's equivalent to the typescript<OK extends T>
, which is clear if you drop the rest of the comment:/** @template {T} OK */
I am kind of stuck here because this is the only reasonable way to resolve the ambiguity; if you let the mistaken syntax remain intact, the only way to specify a constraint is to require a double-mistaken syntax:
/** @template {T} {OK} here is the actual comment */
(This works in the current PR, by the way, if you do type that.)
The other option is to disable the old mistaken syntax entirely and always take the curly braces to mean a constraint. I guess this would break too much, but I haven't checked with our user code tests yet.
Also