From 6504a34ec80a2a11d8979e8f348f166413cac72f Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Sun, 15 Aug 2021 19:27:22 -0700 Subject: [PATCH] Fix 'as const'-like behavior in JSDoc type cast --- src/compiler/checker.ts | 36 ++++++++++++------- src/compiler/factory/utilities.ts | 15 ++++++++ src/compiler/types.ts | 9 ++++- src/compiler/utilities.ts | 35 ++++++++++++++---- .../reference/api/tsserverlibrary.d.ts | 3 +- tests/baselines/reference/api/typescript.d.ts | 3 +- .../reference/jsdocTypeTagCast.errors.txt | 5 ++- tests/baselines/reference/jsdocTypeTagCast.js | 9 ++++- .../reference/jsdocTypeTagCast.symbols | 9 +++++ .../reference/jsdocTypeTagCast.types | 14 ++++++++ .../conformance/jsdoc/jsdocTypeTagCast.ts | 4 +++ 11 files changed, 119 insertions(+), 23 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 38888e5e582ed..4c9ece69365ff 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8388,12 +8388,12 @@ namespace ts { } function isNullOrUndefined(node: Expression) { - const expr = skipParentheses(node); + const expr = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); return expr.kind === SyntaxKind.NullKeyword || expr.kind === SyntaxKind.Identifier && getResolvedSymbol(expr as Identifier) === undefinedSymbol; } function isEmptyArrayLiteral(node: Expression) { - const expr = skipParentheses(node); + const expr = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); return expr.kind === SyntaxKind.ArrayLiteralExpression && (expr as ArrayLiteralExpression).elements.length === 0; } @@ -22956,7 +22956,7 @@ namespace ts { } function isFalseExpression(expr: Expression): boolean { - const node = skipParentheses(expr); + const node = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); return node.kind === SyntaxKind.FalseKeyword || node.kind === SyntaxKind.BinaryExpression && ( (node as BinaryExpression).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && (isFalseExpression((node as BinaryExpression).left) || isFalseExpression((node as BinaryExpression).right)) || (node as BinaryExpression).operatorToken.kind === SyntaxKind.BarBarToken && isFalseExpression((node as BinaryExpression).left) && isFalseExpression((node as BinaryExpression).right)); @@ -23278,7 +23278,7 @@ namespace ts { } function narrowTypeByAssertion(type: Type, expr: Expression): Type { - const node = skipParentheses(expr); + const node = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true); if (node.kind === SyntaxKind.FalseKeyword) { return unreachableNeverType; } @@ -25858,7 +25858,9 @@ namespace ts { 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 ? getTypeFromTypeNode(tag.typeExpression.type) : getContextualType(parent as ParenthesizedExpression, contextFlags); + return !tag ? getContextualType(parent as ParenthesizedExpression, contextFlags) : + isJSDocTypeTag(tag) && isConstTypeReference(tag.typeExpression.type) ? tryFindWhenConstTypeReference(parent as ParenthesizedExpression) : + getTypeFromTypeNode(tag.typeExpression.type); } case SyntaxKind.NonNullExpression: return getContextualType(parent as NonNullExpression, contextFlags); @@ -32768,8 +32770,10 @@ namespace ts { } function isTypeAssertion(node: Expression) { - node = skipParentheses(node); - return node.kind === SyntaxKind.TypeAssertionExpression || node.kind === SyntaxKind.AsExpression; + node = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); + return node.kind === SyntaxKind.TypeAssertionExpression || + node.kind === SyntaxKind.AsExpression || + isJSDocTypeAssertion(node); } function checkDeclarationInitializer(declaration: HasExpressionInitializer, contextualType?: Type | undefined) { @@ -32844,6 +32848,7 @@ namespace ts { function isConstContext(node: Expression): boolean { const parent = node.parent; return isAssertionExpression(parent) && isConstTypeReference(parent.type) || + isJSDocTypeAssertion(parent) && isConstTypeReference(getJSDocTypeAssertionType(parent)) || (isParenthesizedExpression(parent) || isArrayLiteralExpression(parent) || isSpreadElement(parent)) && isConstContext(parent) || (isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent) || isTemplateSpan(parent)) && isConstContext(parent.parent); } @@ -33056,7 +33061,14 @@ namespace ts { } function getQuickTypeOfExpression(node: Expression) { - const expr = skipParentheses(node); + let expr = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true); + if (isJSDocTypeAssertion(expr)) { + const type = getJSDocTypeAssertionType(expr); + if (!isConstTypeReference(type)) { + return getTypeFromTypeNode(type); + } + } + expr = skipParentheses(node); // Optimize for the common case of a call to a function with a single non-generic call // signature where we can just fetch the return type without checking the arguments. if (isCallExpression(expr) && expr.expression.kind !== SyntaxKind.SuperKeyword && !isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) { @@ -33143,9 +33155,9 @@ namespace ts { } function checkParenthesizedExpression(node: ParenthesizedExpression, checkMode?: CheckMode): Type { - const tag = isInJSFile(node) ? getJSDocTypeTag(node) : undefined; - if (tag) { - return checkAssertionWorker(tag.typeExpression.type, tag.typeExpression.type, node.expression, checkMode); + if (isJSDocTypeAssertion(node)) { + const type = getJSDocTypeAssertionType(node); + return checkAssertionWorker(type, type, node.expression, checkMode); } return checkExpression(node.expression, checkMode); } @@ -36093,7 +36105,7 @@ namespace ts { if (getFalsyFlags(type)) return; const location = isBinaryExpression(condExpr) ? condExpr.right : condExpr; - if (isPropertyAccessExpression(location) && isAssertionExpression(skipParentheses(location.expression))) { + if (isPropertyAccessExpression(location) && isTypeAssertion(location.expression)) { return; } diff --git a/src/compiler/factory/utilities.ts b/src/compiler/factory/utilities.ts index 6d40ae277bd4e..fdd3200d572aa 100644 --- a/src/compiler/factory/utilities.ts +++ b/src/compiler/factory/utilities.ts @@ -416,9 +416,24 @@ namespace ts { node.kind === SyntaxKind.CommaListExpression; } + export function isJSDocTypeAssertion(node: Node): node is JSDocTypeAssertion { + return isParenthesizedExpression(node) + && isInJSFile(node) + && !!getJSDocTypeTag(node); + } + + export function getJSDocTypeAssertionType(node: JSDocTypeAssertion) { + const type = getJSDocType(node); + Debug.assertIsDefined(type); + return type; + } + export function isOuterExpression(node: Node, kinds = OuterExpressionKinds.All): node is OuterExpression { switch (node.kind) { case SyntaxKind.ParenthesizedExpression: + if (kinds & OuterExpressionKinds.ExcludeJSDocTypeAssertion && isJSDocTypeAssertion(node)) { + return false; + } return (kinds & OuterExpressionKinds.Parentheses) !== 0; case SyntaxKind.TypeAssertionExpression: case SyntaxKind.AsExpression: diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6194f41a20d22..5ae68145122a2 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2253,6 +2253,11 @@ namespace ts { readonly expression: Expression; } + /* @internal */ + export interface JSDocTypeAssertion extends ParenthesizedExpression { + readonly _jsDocTypeAssertionBrand: never; + } + export interface ArrayLiteralExpression extends PrimaryExpression { readonly kind: SyntaxKind.ArrayLiteralExpression; readonly elements: NodeArray; @@ -6889,7 +6894,9 @@ namespace ts { PartiallyEmittedExpressions = 1 << 3, Assertions = TypeAssertions | NonNullAssertions, - All = Parentheses | Assertions | PartiallyEmittedExpressions + All = Parentheses | Assertions | PartiallyEmittedExpressions, + + ExcludeJSDocTypeAssertion = 1 << 4, } /* @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 00390b144b590..e77f0dd4673cb 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2634,13 +2634,13 @@ namespace ts { let result: (JSDoc | JSDocTag)[] | undefined; // Pull parameter comments from declaring function as well if (isVariableLike(hostNode) && hasInitializer(hostNode) && hasJSDocNodes(hostNode.initializer!)) { - result = append(result, last((hostNode.initializer as HasJSDoc).jsDoc!)); + result = addRange(result, filterOwnedJSDocTags(hostNode, last((hostNode.initializer as HasJSDoc).jsDoc!))); } let node: Node | undefined = hostNode; while (node && node.parent) { if (hasJSDocNodes(node)) { - result = append(result, last(node.jsDoc!)); + result = addRange(result, filterOwnedJSDocTags(hostNode, last(node.jsDoc!))); } if (node.kind === SyntaxKind.Parameter) { @@ -2656,6 +2656,26 @@ namespace ts { return result || emptyArray; } + function filterOwnedJSDocTags(hostNode: Node, jsDoc: JSDoc | JSDocTag) { + if (isJSDoc(jsDoc)) { + const ownedTags = filter(jsDoc.tags, tag => ownsJSDocTag(hostNode, tag)); + return jsDoc.tags === ownedTags ? [jsDoc] : ownedTags; + } + return ownsJSDocTag(hostNode, jsDoc) ? [jsDoc] : undefined; + } + + /** + * Determines whether a host node owns a jsDoc tag. A `@type` tag attached to a + * a ParenthesizedExpression belongs only to the ParenthesizedExpression. + */ + function ownsJSDocTag(hostNode: Node, tag: JSDocTag) { + return !isJSDocTypeTag(tag) + || !tag.parent + || !isJSDoc(tag.parent) + || !isParenthesizedExpression(tag.parent.parent) + || tag.parent.parent === hostNode; + } + export function getNextJSDocCommentLocation(node: Node) { const parent = node.parent; if (parent.kind === SyntaxKind.PropertyAssignment || @@ -2899,10 +2919,13 @@ namespace ts { return [child, node]; } - export function skipParentheses(node: Expression): Expression; - export function skipParentheses(node: Node): Node; - export function skipParentheses(node: Node): Node { - return skipOuterExpressions(node, OuterExpressionKinds.Parentheses); + export function skipParentheses(node: Expression, excludeJSDocTypeAssertions?: boolean): Expression; + export function skipParentheses(node: Node, excludeJSDocTypeAssertions?: boolean): Node; + export function skipParentheses(node: Node, excludeJSDocTypeAssertions?: boolean): Node { + const flags = excludeJSDocTypeAssertions ? + OuterExpressionKinds.Parentheses | OuterExpressionKinds.ExcludeJSDocTypeAssertion : + OuterExpressionKinds.Parentheses; + return skipOuterExpressions(node, flags); } // a node is delete target iff. it is PropertyAccessExpression/ElementAccessExpression with parentheses skipped diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 803d3163ef209..cf96ec31f9188 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3218,7 +3218,8 @@ declare namespace ts { NonNullAssertions = 4, PartiallyEmittedExpressions = 8, Assertions = 6, - All = 15 + All = 15, + ExcludeJSDocTypeAssertion = 16 } export type TypeOfTag = "undefined" | "number" | "bigint" | "boolean" | "string" | "symbol" | "object" | "function"; export interface NodeFactory { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 94d77a0c42dd1..72f11c4611fc9 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3218,7 +3218,8 @@ declare namespace ts { NonNullAssertions = 4, PartiallyEmittedExpressions = 8, Assertions = 6, - All = 15 + All = 15, + ExcludeJSDocTypeAssertion = 16 } export type TypeOfTag = "undefined" | "number" | "bigint" | "boolean" | "string" | "symbol" | "object" | "function"; export interface NodeFactory { diff --git a/tests/baselines/reference/jsdocTypeTagCast.errors.txt b/tests/baselines/reference/jsdocTypeTagCast.errors.txt index 41544f603485c..f85dd92939fbe 100644 --- a/tests/baselines/reference/jsdocTypeTagCast.errors.txt +++ b/tests/baselines/reference/jsdocTypeTagCast.errors.txt @@ -123,4 +123,7 @@ tests/cases/conformance/jsdoc/b.js(67,8): error TS2454: Variable 'numOrStr' is u } - \ No newline at end of file + var asConst1 = /** @type {const} */(1); + var asConst2 = /** @type {const} */({ + x: 1 + }); \ No newline at end of file diff --git a/tests/baselines/reference/jsdocTypeTagCast.js b/tests/baselines/reference/jsdocTypeTagCast.js index fce2dca5dc293..d829ef238addd 100644 --- a/tests/baselines/reference/jsdocTypeTagCast.js +++ b/tests/baselines/reference/jsdocTypeTagCast.js @@ -74,7 +74,10 @@ if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error } - +var asConst1 = /** @type {const} */(1); +var asConst2 = /** @type {const} */({ + x: 1 +}); //// [a.js] var W; @@ -154,3 +157,7 @@ var str; if ( /** @type {numOrStr is string} */(numOrStr === undefined)) { // Error str = numOrStr; // Error, no narrowing occurred } +var asConst1 = /** @type {const} */ (1); +var asConst2 = /** @type {const} */ ({ + x: 1 +}); diff --git a/tests/baselines/reference/jsdocTypeTagCast.symbols b/tests/baselines/reference/jsdocTypeTagCast.symbols index 5c4fbefb2c707..f719cfd9739a3 100644 --- a/tests/baselines/reference/jsdocTypeTagCast.symbols +++ b/tests/baselines/reference/jsdocTypeTagCast.symbols @@ -157,4 +157,13 @@ if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error } +var asConst1 = /** @type {const} */(1); +>asConst1 : Symbol(asConst1, Decl(b.js, 70, 3)) +var asConst2 = /** @type {const} */({ +>asConst2 : Symbol(asConst2, Decl(b.js, 71, 3)) + + x: 1 +>x : Symbol(x, Decl(b.js, 71, 37)) + +}); diff --git a/tests/baselines/reference/jsdocTypeTagCast.types b/tests/baselines/reference/jsdocTypeTagCast.types index 5a330825aea61..8ed961dcce52e 100644 --- a/tests/baselines/reference/jsdocTypeTagCast.types +++ b/tests/baselines/reference/jsdocTypeTagCast.types @@ -209,4 +209,18 @@ if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error } +var asConst1 = /** @type {const} */(1); +>asConst1 : 1 +>(1) : 1 +>1 : 1 +var asConst2 = /** @type {const} */({ +>asConst2 : { readonly x: 1; } +>({ x: 1}) : { readonly x: 1; } +>{ x: 1} : { readonly x: 1; } + + x: 1 +>x : 1 +>1 : 1 + +}); diff --git a/tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts b/tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts index 60ca43b053346..b7b140b33b9bd 100644 --- a/tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts +++ b/tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts @@ -76,3 +76,7 @@ if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error } +var asConst1 = /** @type {const} */(1); +var asConst2 = /** @type {const} */({ + x: 1 +}); \ No newline at end of file