Skip to content

Commit

Permalink
feat(51086): satisfies support in JSDoc (#51753)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk authored Jan 17, 2023
1 parent 577cc1b commit 9c9d4b0
Show file tree
Hide file tree
Showing 77 changed files with 2,073 additions and 44 deletions.
68 changes: 54 additions & 14 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ import {
GetAccessorDeclaration,
getAliasDeclarationFromName,
getAllAccessorDeclarations,
getAllJSDocTags,
getAllowSyntheticDefaultImports,
getAncestor,
getAssignedExpandoInitializer,
Expand Down Expand Up @@ -284,6 +285,7 @@ import {
getJSDocHost,
getJSDocParameterTags,
getJSDocRoot,
getJSDocSatisfiesExpressionType,
getJSDocTags,
getJSDocThisTag,
getJSDocType,
Expand Down Expand Up @@ -557,6 +559,8 @@ import {
isJSDocPropertyLikeTag,
isJSDocPropertyTag,
isJSDocReturnTag,
isJSDocSatisfiesExpression,
isJSDocSatisfiesTag,
isJSDocSignature,
isJSDocTemplateTag,
isJSDocTypeAlias,
Expand Down Expand Up @@ -732,6 +736,7 @@ import {
JSDocPropertyTag,
JSDocProtectedTag,
JSDocPublicTag,
JSDocSatisfiesTag,
JSDocSignature,
JSDocTemplateTag,
JSDocTypedefTag,
Expand Down Expand Up @@ -972,6 +977,7 @@ import {
tryExtractTSExtension,
tryGetClassImplementingOrExtendingExpressionWithTypeArguments,
tryGetExtensionFromPath,
tryGetJSDocSatisfiesTypeNode,
tryGetModuleSpecifierFromDeclaration,
tryGetPropertyAccessOrIdentifierToString,
TryStatement,
Expand Down Expand Up @@ -28201,7 +28207,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function getContextualTypeForVariableLikeDeclaration(declaration: VariableLikeDeclaration, contextFlags: ContextFlags | undefined): Type | undefined {
const typeNode = getEffectiveTypeAnnotationNode(declaration);
const typeNode = getEffectiveTypeAnnotationNode(declaration) || (isInJSFile(declaration) ? tryGetJSDocSatisfiesTypeNode(declaration) : undefined);
if (typeNode) {
return getTypeFromTypeNode(typeNode);
}
Expand Down Expand Up @@ -28884,11 +28890,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
Debug.assert(parent.parent.kind === SyntaxKind.TemplateExpression);
return getContextualTypeForSubstitutionExpression(parent.parent as TemplateExpression, node);
case SyntaxKind.ParenthesizedExpression: {
// Like in `checkParenthesizedExpression`, an `/** @type {xyz} */` comment before a parenthesized expression acts as a type cast.
const tag = isInJSFile(parent) ? getJSDocTypeTag(parent) : undefined;
return !tag ? getContextualType(parent as ParenthesizedExpression, contextFlags) :
isJSDocTypeTag(tag) && isConstTypeReference(tag.typeExpression.type) ? getContextualType(parent as ParenthesizedExpression, contextFlags) :
getTypeFromTypeNode(tag.typeExpression.type);
if (isInJSFile(parent)) {
if (isJSDocSatisfiesExpression(parent)) {
return getTypeFromTypeNode(getJSDocSatisfiesExpressionType(parent));
}
// Like in `checkParenthesizedExpression`, an `/** @type {xyz} */` comment before a parenthesized expression acts as a type cast.
const typeTag = getJSDocTypeTag(parent);
if (typeTag && !isConstTypeReference(typeTag.typeExpression.type)) {
return getTypeFromTypeNode(typeTag.typeExpression.type);
}
}
return getContextualType(parent as ParenthesizedExpression, contextFlags);
}
case SyntaxKind.NonNullExpression:
return getContextualType(parent as NonNullExpression, contextFlags);
Expand Down Expand Up @@ -33908,15 +33920,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function checkSatisfiesExpression(node: SatisfiesExpression) {
checkSourceElement(node.type);
const exprType = checkExpression(node.expression);
return checkSatisfiesExpressionWorker(node.expression, node.type);
}

const targetType = getTypeFromTypeNode(node.type);
function checkSatisfiesExpressionWorker(expression: Expression, target: TypeNode, checkMode?: CheckMode) {
const exprType = checkExpression(expression, checkMode);
const targetType = getTypeFromTypeNode(target);
if (isErrorType(targetType)) {
return targetType;
}

checkTypeAssignableToAndOptionallyElaborate(exprType, targetType, node.type, node.expression, Diagnostics.Type_0_does_not_satisfy_the_expected_type_1);

checkTypeAssignableToAndOptionallyElaborate(exprType, targetType, target, expression, Diagnostics.Type_0_does_not_satisfy_the_expected_type_1);
return exprType;
}

Expand Down Expand Up @@ -36254,6 +36267,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
contextualType?: Type | undefined
) {
const initializer = getEffectiveInitializer(declaration)!;
if (isInJSFile(declaration)) {
const typeNode = tryGetJSDocSatisfiesTypeNode(declaration);
if (typeNode) {
return checkSatisfiesExpressionWorker(initializer, typeNode, checkMode);
}
}
const type = getQuickTypeOfExpression(initializer) ||
(contextualType ?
checkExpressionWithContextualType(initializer, contextualType, /*inferenceContext*/ undefined, checkMode || CheckMode.Normal)
Expand Down Expand Up @@ -36636,9 +36655,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function checkParenthesizedExpression(node: ParenthesizedExpression, checkMode?: CheckMode): Type {
if (hasJSDocNodes(node) && isJSDocTypeAssertion(node)) {
const type = getJSDocTypeAssertionType(node);
return checkAssertionWorker(type, type, node.expression, checkMode);
if (hasJSDocNodes(node)) {
if (isJSDocSatisfiesExpression(node)) {
return checkSatisfiesExpressionWorker(node.expression, getJSDocSatisfiesExpressionType(node), checkMode);
}
if (isJSDocTypeAssertion(node)) {
const type = getJSDocTypeAssertionType(node);
return checkAssertionWorker(type, type, node.expression, checkMode);
}
}
return checkExpression(node.expression, checkMode);
}
Expand Down Expand Up @@ -38873,6 +38897,20 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
checkSourceElement(node.typeExpression);
}

function checkJSDocSatisfiesTag(node: JSDocSatisfiesTag) {
checkSourceElement(node.typeExpression);
const host = getEffectiveJSDocHost(node);
if (host) {
const tags = getAllJSDocTags(host, isJSDocSatisfiesTag);
if (length(tags) > 1) {
for (let i = 1; i < length(tags); i++) {
const tagName = tags[i].tagName;
error(tagName, Diagnostics._0_tag_already_specified, idText(tagName));
}
}
}
}

function checkJSDocLinkLikeTag(node: JSDocLink | JSDocLinkCode | JSDocLinkPlain) {
if (node.name) {
resolveJSDocMemberName(node.name, /*ignoreErrors*/ true);
Expand Down Expand Up @@ -43386,6 +43424,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.JSDocProtectedTag:
case SyntaxKind.JSDocPrivateTag:
return checkJSDocAccessibilityModifiers(node as JSDocPublicTag | JSDocProtectedTag | JSDocPrivateTag);
case SyntaxKind.JSDocSatisfiesTag:
return checkJSDocSatisfiesTag(node as JSDocSatisfiesTag);
case SyntaxKind.IndexedAccessType:
return checkIndexedAccessType(node as IndexedAccessTypeNode);
case SyntaxKind.MappedType:
Expand Down
7 changes: 5 additions & 2 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,13 @@ import {
JSDocOverloadTag,
JSDocPropertyLikeTag,
JSDocReturnTag,
JSDocSatisfiesTag,
JSDocSeeTag,
JSDocSignature,
JSDocTag,
JSDocTemplateTag,
JSDocThisTag,
JSDocThrowsTag,
JSDocTypedefTag,
JSDocTypeExpression,
JSDocTypeLiteral,
Expand Down Expand Up @@ -2135,7 +2137,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
case SyntaxKind.JSDocThisTag:
case SyntaxKind.JSDocTypeTag:
case SyntaxKind.JSDocThrowsTag:
return emitJSDocSimpleTypedTag(node as JSDocTypeTag);
case SyntaxKind.JSDocSatisfiesTag:
return emitJSDocSimpleTypedTag(node as JSDocTypeTag | JSDocReturnTag | JSDocThisTag | JSDocTypeTag | JSDocThrowsTag | JSDocSatisfiesTag);
case SyntaxKind.JSDocTemplateTag:
return emitJSDocTemplateTag(node as JSDocTemplateTag);
case SyntaxKind.JSDocTypedefTag:
Expand Down Expand Up @@ -4336,7 +4339,7 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
write("*/");
}

function emitJSDocSimpleTypedTag(tag: JSDocTypeTag | JSDocThisTag | JSDocEnumTag | JSDocReturnTag) {
function emitJSDocSimpleTypedTag(tag: JSDocTypeTag | JSDocThisTag | JSDocEnumTag | JSDocReturnTag | JSDocThrowsTag | JSDocSatisfiesTag) {
emitJSDocTagName(tag.tagName);
emitJSDocTypeExpression(tag.typeExpression);
emitJSDocComment(tag.comment);
Expand Down
6 changes: 6 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 {
JSDocPublicTag,
JSDocReadonlyTag,
JSDocReturnTag,
JSDocSatisfiesTag,
JSDocSeeTag,
JSDocSignature,
JSDocTag,
Expand Down Expand Up @@ -876,6 +877,9 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
get updateJSDocDeprecatedTag() { return getJSDocSimpleTagUpdateFunction<JSDocDeprecatedTag>(SyntaxKind.JSDocDeprecatedTag); },
get createJSDocThrowsTag() { return getJSDocTypeLikeTagCreateFunction<JSDocThrowsTag>(SyntaxKind.JSDocThrowsTag); },
get updateJSDocThrowsTag() { return getJSDocTypeLikeTagUpdateFunction<JSDocThrowsTag>(SyntaxKind.JSDocThrowsTag); },
get createJSDocSatisfiesTag() { return getJSDocTypeLikeTagCreateFunction<JSDocSatisfiesTag>(SyntaxKind.JSDocSatisfiesTag); },
get updateJSDocSatisfiesTag() { return getJSDocTypeLikeTagUpdateFunction<JSDocSatisfiesTag>(SyntaxKind.JSDocSatisfiesTag); },

createJSDocEnumTag,
updateJSDocEnumTag,
createJSDocUnknownTag,
Expand Down Expand Up @@ -5417,6 +5421,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
// createJSDocReturnTag
// createJSDocThisTag
// createJSDocEnumTag
// createJSDocSatisfiesTag
function createJSDocTypeLikeTagWorker<T extends JSDocTag & { typeExpression?: JSDocTypeExpression }>(kind: T["kind"], tagName: Identifier | undefined, typeExpression?: JSDocTypeExpression, comment?: string | NodeArray<JSDocComment>) {
const node = createBaseJSDocTag<T>(kind, tagName ?? createIdentifier(getDefaultTagNameForKind(kind)), comment);
node.typeExpression = typeExpression;
Expand All @@ -5428,6 +5433,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
// updateJSDocReturnTag
// updateJSDocThisTag
// updateJSDocEnumTag
// updateJSDocSatisfiesTag
function updateJSDocTypeLikeTagWorker<T extends JSDocTag & { typeExpression?: JSDocTypeExpression }>(kind: T["kind"], node: T, tagName: Identifier = getDefaultTagName(node), typeExpression: JSDocTypeExpression | undefined, comment: string | NodeArray<JSDocComment> | undefined) {
return node.tagName !== tagName
|| node.typeExpression !== typeExpression
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 @@ -107,6 +107,7 @@ import {
JSDocPublicTag,
JSDocReadonlyTag,
JSDocReturnTag,
JSDocSatisfiesTag,
JSDocSeeTag,
JSDocSignature,
JSDocTemplateTag,
Expand Down Expand Up @@ -1188,6 +1189,10 @@ export function isJSDocImplementsTag(node: Node): node is JSDocImplementsTag {
return node.kind === SyntaxKind.JSDocImplementsTag;
}

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

export function isJSDocThrowsTag(node: Node): node is JSDocThrowsTag {
return node.kind === SyntaxKind.JSDocThrowsTag;
}
Expand Down
13 changes: 12 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ import {
JSDocPublicTag,
JSDocReadonlyTag,
JSDocReturnTag,
JSDocSatisfiesTag,
JSDocSeeTag,
JSDocSignature,
JSDocSyntaxKind,
Expand Down Expand Up @@ -1108,6 +1109,7 @@ const forEachChildTable: ForEachChildTable = {
[SyntaxKind.JSDocTypeTag]: forEachChildInJSDocTypeLikeTag,
[SyntaxKind.JSDocThisTag]: forEachChildInJSDocTypeLikeTag,
[SyntaxKind.JSDocEnumTag]: forEachChildInJSDocTypeLikeTag,
[SyntaxKind.JSDocSatisfiesTag]: forEachChildInJSDocTypeLikeTag,
[SyntaxKind.JSDocThrowsTag]: forEachChildInJSDocTypeLikeTag,
[SyntaxKind.JSDocOverloadTag]: forEachChildInJSDocTypeLikeTag,
[SyntaxKind.JSDocSignature]: function forEachChildInJSDocSignature<T>(node: JSDocSignature, cbNode: (node: Node) => T | undefined, _cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
Expand Down Expand Up @@ -1203,7 +1205,7 @@ function forEachChildInJSDocParameterOrPropertyTag<T>(node: JSDocParameterTag |
(typeof node.comment === "string" ? undefined : visitNodes(cbNode, cbNodes, node.comment));
}

function forEachChildInJSDocTypeLikeTag<T>(node: JSDocReturnTag | JSDocTypeTag | JSDocThisTag | JSDocEnumTag | JSDocThrowsTag | JSDocOverloadTag, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
function forEachChildInJSDocTypeLikeTag<T>(node: JSDocReturnTag | JSDocTypeTag | JSDocThisTag | JSDocEnumTag | JSDocThrowsTag | JSDocOverloadTag | JSDocSatisfiesTag, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.tagName) ||
visitNode(cbNode, node.typeExpression) ||
(typeof node.comment === "string" ? undefined : visitNodes(cbNode, cbNodes, node.comment));
Expand Down Expand Up @@ -8779,6 +8781,9 @@ namespace Parser {
case "overload":
tag = parseOverloadTag(start, tagName, margin, indentText);
break;
case "satisfies":
tag = parseSatisfiesTag(start, tagName, margin, indentText);
break;
case "see":
tag = parseSeeTag(start, tagName, margin, indentText);
break;
Expand Down Expand Up @@ -9144,6 +9149,12 @@ namespace Parser {
return finishNode(factory.createJSDocAugmentsTag(tagName, className, parseTrailingTagComments(start, getNodePos(), margin, indentText)), start);
}

function parseSatisfiesTag(start: number, tagName: Identifier, margin: number, indentText: string): JSDocSatisfiesTag {
const typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ false);
const comments = margin !== undefined && indentText !== undefined ? parseTrailingTagComments(start, getNodePos(), margin, indentText) : undefined;
return finishNode(factory.createJSDocSatisfiesTag(tagName, typeExpression, comments), start);
}

function parseExpressionWithTypeArgumentsForAugments(): ExpressionWithTypeArguments & { expression: Identifier | PropertyAccessEntityNameExpression } {
const usedBrace = parseOptional(SyntaxKind.OpenBraceToken);
const pos = getNodePos();
Expand Down
18 changes: 16 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ export const enum SyntaxKind {
JSDocSeeTag,
JSDocPropertyTag,
JSDocThrowsTag,
JSDocSatisfiesTag,

// Synthesized list
SyntaxList,
Expand Down Expand Up @@ -478,9 +479,9 @@ export const enum SyntaxKind {
LastStatement = DebuggerStatement,
FirstNode = QualifiedName,
FirstJSDocNode = JSDocTypeExpression,
LastJSDocNode = JSDocThrowsTag,
LastJSDocNode = JSDocSatisfiesTag,
FirstJSDocTagNode = JSDocTag,
LastJSDocTagNode = JSDocThrowsTag,
LastJSDocTagNode = JSDocSatisfiesTag,
/** @internal */ FirstContextualKeyword = AbstractKeyword,
/** @internal */ LastContextualKeyword = OfKeyword,
}
Expand Down Expand Up @@ -1010,6 +1011,7 @@ export type ForEachChildNodes =
| JSDocDeprecatedTag
| JSDocThrowsTag
| JSDocOverrideTag
| JSDocSatisfiesTag
| JSDocOverloadTag
;

Expand Down Expand Up @@ -4105,6 +4107,16 @@ export interface JSDocTypeLiteral extends JSDocType, Declaration {
readonly isArrayType: boolean;
}

export interface JSDocSatisfiesTag extends JSDocTag {
readonly kind: SyntaxKind.JSDocSatisfiesTag;
readonly typeExpression: JSDocTypeExpression;
}

/** @internal */
export interface JSDocSatisfiesExpression extends ParenthesizedExpression {
readonly _jsDocSatisfiesExpressionBrand: never;
}

// NOTE: Ensure this is up-to-date with src/debug/debug.ts
export const enum FlowFlags {
Unreachable = 1 << 0, // Unreachable code
Expand Down Expand Up @@ -8580,6 +8592,8 @@ export interface NodeFactory {
updateJSDocOverrideTag(node: JSDocOverrideTag, tagName: Identifier, comment?: string | NodeArray<JSDocComment>): JSDocOverrideTag;
createJSDocThrowsTag(tagName: Identifier, typeExpression: JSDocTypeExpression | undefined, comment?: string | NodeArray<JSDocComment>): JSDocThrowsTag;
updateJSDocThrowsTag(node: JSDocThrowsTag, tagName: Identifier | undefined, typeExpression: JSDocTypeExpression | undefined, comment?: string | NodeArray<JSDocComment> | undefined): JSDocThrowsTag;
createJSDocSatisfiesTag(tagName: Identifier | undefined, typeExpression: JSDocTypeExpression, comment?: string | NodeArray<JSDocComment>): JSDocSatisfiesTag;
updateJSDocSatisfiesTag(node: JSDocSatisfiesTag, tagName: Identifier | undefined, typeExpression: JSDocTypeExpression, comment: string | NodeArray<JSDocComment> | undefined): JSDocSatisfiesTag;
createJSDocText(text: string): JSDocText;
updateJSDocText(node: JSDocText, text: string): JSDocText;
createJSDocComment(comment?: string | NodeArray<JSDocComment> | undefined, tags?: readonly JSDocTag[] | undefined): JSDoc;
Expand Down
Loading

0 comments on commit 9c9d4b0

Please sign in to comment.