Skip to content

Commit

Permalink
Add support for "specialize" tag
Browse files Browse the repository at this point in the history
  • Loading branch information
apendua committed Aug 17, 2024
1 parent 41b993b commit 83a1bb2
Show file tree
Hide file tree
Showing 34 changed files with 1,909 additions and 34 deletions.
3 changes: 3 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ import {
JSDocParameterTag,
JSDocPropertyLikeTag,
JSDocSignature,
JSDocSpecializeTag,
JSDocTypedefTag,
JSDocTypeLiteral,
JsxAttribute,
Expand Down Expand Up @@ -3076,6 +3077,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 @@ -827,7 +828,6 @@ import {
JsxFlags,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningFragment,
JsxOpeningLikeElement,
JsxReferenceKind,
Expand Down Expand Up @@ -34636,15 +34636,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 @@ -35595,21 +35614,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 @@ -35781,7 +35794,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 @@ -35995,7 +36008,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 @@ -36095,14 +36108,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 @@ -36138,7 +36152,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 @@ -36183,11 +36197,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 @@ -36552,9 +36567,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 @@ -36810,7 +36826,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 @@ -37049,7 +37065,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 @@ -41595,15 +41613,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 @@ -51341,7 +51359,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 @@ -5547,6 +5550,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 @@ -7205,6 +7224,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

0 comments on commit 83a1bb2

Please sign in to comment.