Skip to content
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

Add support for "specialize" tag #59666

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ import {
JSDocParameterTag,
JSDocPropertyLikeTag,
JSDocSignature,
JSDocSpecializeTag,
JSDocTypedefTag,
JSDocTypeLiteral,
JsxAttribute,
Expand Down Expand Up @@ -3078,6 +3079,8 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
return bind((node as JSDocOverloadTag).typeExpression);
case SyntaxKind.JSDocImportTag:
return (jsDocImports || (jsDocImports = [])).push(node as JSDocImportTag);
case SyntaxKind.JSDocSpecializeTag:
return bindEach((node as JSDocSpecializeTag).typeArguments);
}
}

Expand Down
68 changes: 43 additions & 25 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ import {
getJSDocParameterTags,
getJSDocRoot,
getJSDocSatisfiesExpressionType,
getJSDocSpecializeTag,
getJSDocTags,
getJSDocThisTag,
getJSDocType,
Expand Down Expand Up @@ -828,7 +829,6 @@ import {
JsxFlags,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningFragment,
JsxOpeningLikeElement,
JsxReferenceKind,
Expand Down Expand Up @@ -34743,15 +34743,34 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return checkIndexedAccessIndexType(getFlowTypeOfAccessExpression(node, getNodeLinks(node).resolvedSymbol, indexedAccessType, indexExpression, checkMode), node);
}

function callLikeExpressionMayHaveTypeArguments(node: CallLikeExpression): node is CallExpression | NewExpression | TaggedTemplateExpression | JsxOpeningElement {
function callLikeExpressionMayHaveTypeArguments(node: CallLikeExpression): node is CallExpression | NewExpression | TaggedTemplateExpression | JsxOpeningLikeElement {
return isCallOrNewExpression(node) || isTaggedTemplateExpression(node) || isJsxOpeningLikeElement(node);
}

function getTypeArgumentsForCallLikeExpression(node: CallExpression | NewExpression | TaggedTemplateExpression | JsxOpeningLikeElement) {
if (isSuperCall(node)) {
return undefined;
}
if (isInJSFile(node)) {
let { parent } = node;
if (isJsxElement(parent)) {
parent = parent.parent;
}
if (canHaveJSDoc(parent)) {
const specializeTag = getJSDocSpecializeTag(parent);
if (specializeTag) {
return specializeTag.typeArguments;
}
}
}
return node.typeArguments;
}

function resolveUntypedCall(node: CallLikeExpression): Signature {
if (callLikeExpressionMayHaveTypeArguments(node)) {
// Check type arguments even though we will give an error that untyped calls may not accept type arguments.
// This gets us diagnostics for the type arguments and marks them as referenced.
forEach(node.typeArguments, checkSourceElement);
forEach(getTypeArgumentsForCallLikeExpression(node), checkSourceElement);
}

if (node.kind === SyntaxKind.TaggedTemplateExpression) {
Expand Down Expand Up @@ -35702,21 +35721,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function resolveCall(node: CallLikeExpression, signatures: readonly Signature[], candidatesOutArray: Signature[] | undefined, checkMode: CheckMode, callChainFlags: SignatureFlags, headMessage?: DiagnosticMessage): Signature {
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
const isDecorator = node.kind === SyntaxKind.Decorator;
const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node);
const isInstanceof = node.kind === SyntaxKind.BinaryExpression;
const reportErrors = !isInferencePartiallyBlocked && !candidatesOutArray;

let typeArguments: NodeArray<TypeNode> | undefined;

if (!isDecorator && !isInstanceof && !isSuperCall(node)) {
typeArguments = (node as CallExpression).typeArguments;

// We already perform checking on the type arguments on the class declaration itself.
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (node as CallExpression).expression.kind !== SyntaxKind.SuperKeyword) {
forEach(typeArguments, checkSourceElement);
}
if (callLikeExpressionMayHaveTypeArguments(node)) {
typeArguments = getTypeArgumentsForCallLikeExpression(node);
forEach(typeArguments, checkSourceElement);
}

const candidates = candidatesOutArray || [];
Expand Down Expand Up @@ -35888,7 +35901,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args, headMessage));
}
else if (candidateForTypeArgumentError) {
checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, headMessage);
checkTypeArguments(candidateForTypeArgumentError, typeArguments!, /*reportErrors*/ true, headMessage);
}
else {
const signaturesWithCorrectTypeArgumentArity = filter(signatures, s => hasCorrectTypeArgumentArity(s, typeArguments));
Expand Down Expand Up @@ -36102,7 +36115,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return candidate;
}

const typeArgumentNodes: readonly TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? node.typeArguments : undefined;
const typeArgumentNodes: readonly TypeNode[] | undefined = callLikeExpressionMayHaveTypeArguments(node) ? getTypeArgumentsForCallLikeExpression(node) : undefined;
const instantiated = typeArgumentNodes
? createSignatureInstantiation(candidate, getTypeArgumentsFromNodes(typeArgumentNodes, typeParameters, isInJSFile(node)))
: inferSignatureInstantiationForOverloadFailure(node, typeParameters, candidate, args, checkMode);
Expand Down Expand Up @@ -36202,14 +36215,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// that the user will not add any.
const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call);
const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length;
const typeArguments = getTypeArgumentsForCallLikeExpression(node);

// TS 1.0 Spec: 4.12
// In an untyped function call no TypeArgs are permitted, Args can be any argument list, no contextual
// types are provided for the argument expressions, and the result is always of type Any.
if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) {
// The unknownType indicates that an error already occurred (and was reported). No
// need to report another error in this case.
if (!isErrorType(funcType) && node.typeArguments) {
if (!isErrorType(funcType) && typeArguments) {
error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments);
}
return resolveUntypedCall(node);
Expand Down Expand Up @@ -36245,7 +36259,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// use the resolvingSignature singleton to indicate that we deferred processing. This result will be
// propagated out and eventually turned into silentNeverType (a type that is assignable to anything and
// from which we never make inferences).
if (checkMode & CheckMode.SkipGenericFunctions && !node.typeArguments && callSignatures.some(isGenericFunctionReturningFunction)) {
if (checkMode & CheckMode.SkipGenericFunctions && !typeArguments && callSignatures.some(isGenericFunctionReturningFunction)) {
skippedGenericFunction(node, checkMode);
return resolvingSignature;
}
Expand Down Expand Up @@ -36290,11 +36304,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return resolveErrorCall(node);
}

const typeArguments = getTypeArgumentsForCallLikeExpression(node);
// TS 1.0 spec: 4.11
// If expressionType is of type Any, Args can be any argument
// list and the result of the operation is of type Any.
if (isTypeAny(expressionType)) {
if (node.typeArguments) {
if (typeArguments) {
error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments);
}
return resolveUntypedCall(node);
Expand Down Expand Up @@ -36659,9 +36674,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node);
const fakeSignature = createSignatureForJSXIntrinsic(node, result);
checkTypeAssignableToAndOptionallyElaborate(checkExpressionWithContextualType(node.attributes, getEffectiveFirstArgumentForJsxSignature(fakeSignature, node), /*inferenceContext*/ undefined, CheckMode.Normal), result, node.tagName, node.attributes);
if (length(node.typeArguments)) {
forEach(node.typeArguments, checkSourceElement);
diagnostics.add(createDiagnosticForNodeArray(getSourceFileOfNode(node), node.typeArguments!, Diagnostics.Expected_0_type_arguments_but_got_1, 0, length(node.typeArguments)));
const typeArguments = getTypeArgumentsForCallLikeExpression(node);
if (length(typeArguments)) {
forEach(typeArguments, checkSourceElement);
diagnostics.add(createDiagnosticForNodeArray(getSourceFileOfNode(node), typeArguments!, Diagnostics.Expected_0_type_arguments_but_got_1, 0, length(typeArguments)));
}
return fakeSignature;
}
Expand Down Expand Up @@ -36917,7 +36933,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* @returns On success, the expression's signature's return type. On failure, anyType.
*/
function checkCallExpression(node: CallExpression | NewExpression, checkMode?: CheckMode): Type {
checkGrammarTypeArguments(node, node.typeArguments);
checkGrammarTypeArguments(node, getTypeArgumentsForCallLikeExpression(node));

const signature = getResolvedSignature(node, /*candidatesOutArray*/ undefined, checkMode);
if (signature === resolvingSignature) {
Expand Down Expand Up @@ -37156,7 +37172,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function checkTaggedTemplateExpression(node: TaggedTemplateExpression): Type {
if (!checkGrammarTaggedTemplateChain(node)) checkGrammarTypeArguments(node, node.typeArguments);
if (!checkGrammarTaggedTemplateChain(node)) {
checkGrammarTypeArguments(node, getTypeArgumentsForCallLikeExpression(node));
}
if (languageVersion < LanguageFeatureMinimumTarget.TaggedTemplates) {
checkExternalEmitHelpers(node, ExternalEmitHelpers.MakeTemplateObject);
}
Expand Down Expand Up @@ -41719,15 +41737,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
checkDecorators(node);
}

function getEffectiveTypeArgumentAtIndex(node: TypeReferenceNode | ExpressionWithTypeArguments, typeParameters: readonly TypeParameter[], index: number): Type {
function getEffectiveTypeArgumentAtIndex(node: TypeReferenceNode, typeParameters: readonly TypeParameter[], index: number): Type {
if (node.typeArguments && index < node.typeArguments.length) {
return getTypeFromTypeNode(node.typeArguments[index]);
}
return getEffectiveTypeArguments(node, typeParameters)[index];
}

function getEffectiveTypeArguments(node: TypeReferenceNode | ExpressionWithTypeArguments | NodeWithTypeArguments, typeParameters: readonly TypeParameter[]): Type[] {
return fillMissingTypeArguments(map(node.typeArguments!, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isInJSFile(node));
return fillMissingTypeArguments(map(node.typeArguments || [], getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isInJSFile(node));
}

function checkTypeArgumentConstraints(node: TypeReferenceNode | ExpressionWithTypeArguments | NodeWithTypeArguments, typeParameters: readonly TypeParameter[]): boolean {
Expand Down Expand Up @@ -51499,7 +51517,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function checkGrammarJsxElement(node: JsxOpeningLikeElement) {
checkGrammarJsxName(node.tagName);
checkGrammarTypeArguments(node, node.typeArguments);
checkGrammarTypeArguments(node, getTypeArgumentsForCallLikeExpression(node));
const seen = new Map<__String, boolean>();

for (const attr of node.attributes.properties) {
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ import {
JSDocSatisfiesTag,
JSDocSeeTag,
JSDocSignature,
JSDocSpecializeTag,
JSDocTag,
JSDocTemplateTag,
JSDocThisTag,
Expand Down Expand Up @@ -1876,6 +1877,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
return emitJSDocSeeTag(node as JSDocSeeTag);
case SyntaxKind.JSDocImportTag:
return emitJSDocImportTag(node as JSDocImportTag);
case SyntaxKind.JSDocSpecializeTag:
return emitJSDocSpecializeTag(node as JSDocSpecializeTag);
// SyntaxKind.JSDocPropertyTag (see JSDocParameterTag, above)

// Transformation nodes
Expand Down Expand Up @@ -4059,6 +4062,15 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
emitJSDocComment(tag.comment);
}

function emitJSDocSpecializeTag(tag: JSDocSpecializeTag) {
emitJSDocTagName(tag.tagName);
writeSpace();
writePunctuation("<");
emitList(tag, tag.typeArguments, ListFormat.CommaListElements);
writePunctuation(">");
emitJSDocComment(tag.comment);
}

function emitJSDocNameReference(node: JSDocNameReference) {
writeSpace();
writePunctuation("{");
Expand Down
21 changes: 21 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ import {
JSDocSatisfiesTag,
JSDocSeeTag,
JSDocSignature,
JSDocSpecializeTag,
JSDocTag,
JSDocTemplateTag,
JSDocText,
Expand Down Expand Up @@ -861,6 +862,8 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
updateJSDocSeeTag,
createJSDocImportTag,
updateJSDocImportTag,
createJSDocSpecializeTag,
updateJSDocSpecializeTag,
createJSDocNameReference,
updateJSDocNameReference,
createJSDocMemberName,
Expand Down Expand Up @@ -5552,6 +5555,22 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
: node;
}

// @api
function createJSDocSpecializeTag(tagName: Identifier | undefined, typeArguments: readonly TypeNode[], comment: string | NodeArray<JSDocComment> | undefined): JSDocSpecializeTag {
const node = createBaseJSDocTag<JSDocSpecializeTag>(SyntaxKind.JSDocSpecializeTag, tagName ?? createIdentifier("specialize"), /*comment*/ undefined);
node.typeArguments = asNodeArray(typeArguments);
node.comment = comment;
return node;
}

function updateJSDocSpecializeTag(node: JSDocSpecializeTag, tagName: Identifier | undefined, typeArguments: readonly TypeNode[], comment: string | NodeArray<JSDocComment> | undefined): JSDocSpecializeTag {
return node.tagName !== tagName
|| node.typeArguments !== typeArguments
|| node.comment !== comment
? update(createJSDocSpecializeTag(tagName, typeArguments, comment), node)
: node;
}

// @api
function createJSDocText(text: string): JSDocText {
const node = createBaseNode<JSDocText>(SyntaxKind.JSDocText);
Expand Down Expand Up @@ -7212,6 +7231,8 @@ function getDefaultTagNameForKind(kind: JSDocTag["kind"]): string {
return "implements";
case SyntaxKind.JSDocImportTag:
return "import";
case SyntaxKind.JSDocSpecializeTag:
return "specialize";
default:
return Debug.fail(`Unsupported kind: ${Debug.formatSyntaxKind(kind)}`);
}
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/factory/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import {
JSDocSatisfiesTag,
JSDocSeeTag,
JSDocSignature,
JSDocSpecializeTag,
JSDocTemplateTag,
JSDocThisTag,
JSDocThrowsTag,
Expand Down Expand Up @@ -1193,6 +1194,10 @@ export function isJSDocImportTag(node: Node): node is JSDocImportTag {
return node.kind === SyntaxKind.JSDocImportTag;
}

export function isJSDocSpecializeTag(node: Node): node is JSDocSpecializeTag {
return node.kind === SyntaxKind.JSDocSpecializeTag;
}

// Synthesized list

/** @internal */
Expand Down
Loading