Skip to content

Commit db3ef67

Browse files
committed
Fix 'as const'-like behavior in JSDoc type cast
1 parent d50c91d commit db3ef67

File tree

11 files changed

+121
-23
lines changed

11 files changed

+121
-23
lines changed

src/compiler/checker.ts

+26-12
Original file line numberDiff line numberDiff line change
@@ -8388,12 +8388,12 @@ namespace ts {
83888388
}
83898389

83908390
function isNullOrUndefined(node: Expression) {
8391-
const expr = skipParentheses(node);
8391+
const expr = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true);
83928392
return expr.kind === SyntaxKind.NullKeyword || expr.kind === SyntaxKind.Identifier && getResolvedSymbol(expr as Identifier) === undefinedSymbol;
83938393
}
83948394

83958395
function isEmptyArrayLiteral(node: Expression) {
8396-
const expr = skipParentheses(node);
8396+
const expr = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true);
83978397
return expr.kind === SyntaxKind.ArrayLiteralExpression && (expr as ArrayLiteralExpression).elements.length === 0;
83988398
}
83998399

@@ -9022,13 +9022,15 @@ namespace ts {
90229022
}
90239023

90249024
function tryGetTypeFromEffectiveTypeNode(declaration: Declaration) {
9025+
if (isVariableDeclaration(declaration) && isIdentifier(declaration.name) && declaration.name.escapedText === "aaaaa") debugger;
90259026
const typeNode = getEffectiveTypeAnnotationNode(declaration);
90269027
if (typeNode) {
90279028
return getTypeFromTypeNode(typeNode);
90289029
}
90299030
}
90309031

90319032
function getTypeOfVariableOrParameterOrProperty(symbol: Symbol): Type {
9033+
if (symbol.escapedName === "aaaaa") debugger;
90329034
const links = getSymbolLinks(symbol);
90339035
if (!links.type) {
90349036
const type = getTypeOfVariableOrParameterOrPropertyWorker(symbol);
@@ -22956,7 +22958,7 @@ namespace ts {
2295622958
}
2295722959

2295822960
function isFalseExpression(expr: Expression): boolean {
22959-
const node = skipParentheses(expr);
22961+
const node = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true);
2296022962
return node.kind === SyntaxKind.FalseKeyword || node.kind === SyntaxKind.BinaryExpression && (
2296122963
(node as BinaryExpression).operatorToken.kind === SyntaxKind.AmpersandAmpersandToken && (isFalseExpression((node as BinaryExpression).left) || isFalseExpression((node as BinaryExpression).right)) ||
2296222964
(node as BinaryExpression).operatorToken.kind === SyntaxKind.BarBarToken && isFalseExpression((node as BinaryExpression).left) && isFalseExpression((node as BinaryExpression).right));
@@ -23278,7 +23280,7 @@ namespace ts {
2327823280
}
2327923281

2328023282
function narrowTypeByAssertion(type: Type, expr: Expression): Type {
23281-
const node = skipParentheses(expr);
23283+
const node = skipParentheses(expr, /*excludeJSDocTypeAssertions*/ true);
2328223284
if (node.kind === SyntaxKind.FalseKeyword) {
2328323285
return unreachableNeverType;
2328423286
}
@@ -25858,7 +25860,9 @@ namespace ts {
2585825860
case SyntaxKind.ParenthesizedExpression: {
2585925861
// Like in `checkParenthesizedExpression`, an `/** @type {xyz} */` comment before a parenthesized expression acts as a type cast.
2586025862
const tag = isInJSFile(parent) ? getJSDocTypeTag(parent) : undefined;
25861-
return tag ? getTypeFromTypeNode(tag.typeExpression.type) : getContextualType(parent as ParenthesizedExpression, contextFlags);
25863+
return !tag ? getContextualType(parent as ParenthesizedExpression, contextFlags) :
25864+
isJSDocTypeTag(tag) && isConstTypeReference(tag.typeExpression.type) ? tryFindWhenConstTypeReference(parent as ParenthesizedExpression) :
25865+
getTypeFromTypeNode(tag.typeExpression.type);
2586225866
}
2586325867
case SyntaxKind.NonNullExpression:
2586425868
return getContextualType(parent as NonNullExpression, contextFlags);
@@ -32768,8 +32772,10 @@ namespace ts {
3276832772
}
3276932773

3277032774
function isTypeAssertion(node: Expression) {
32771-
node = skipParentheses(node);
32772-
return node.kind === SyntaxKind.TypeAssertionExpression || node.kind === SyntaxKind.AsExpression;
32775+
node = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true);
32776+
return node.kind === SyntaxKind.TypeAssertionExpression ||
32777+
node.kind === SyntaxKind.AsExpression ||
32778+
isJSDocTypeAssertion(node);
3277332779
}
3277432780

3277532781
function checkDeclarationInitializer(declaration: HasExpressionInitializer, contextualType?: Type | undefined) {
@@ -32844,6 +32850,7 @@ namespace ts {
3284432850
function isConstContext(node: Expression): boolean {
3284532851
const parent = node.parent;
3284632852
return isAssertionExpression(parent) && isConstTypeReference(parent.type) ||
32853+
isJSDocTypeAssertion(parent) && isConstTypeReference(getJSDocTypeAssertionType(parent)) ||
3284732854
(isParenthesizedExpression(parent) || isArrayLiteralExpression(parent) || isSpreadElement(parent)) && isConstContext(parent) ||
3284832855
(isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent) || isTemplateSpan(parent)) && isConstContext(parent.parent);
3284932856
}
@@ -33056,7 +33063,14 @@ namespace ts {
3305633063
}
3305733064

3305833065
function getQuickTypeOfExpression(node: Expression) {
33059-
const expr = skipParentheses(node);
33066+
let expr = skipParentheses(node, /*excludeJSDocTypeAssertions*/ true);
33067+
if (isJSDocTypeAssertion(expr)) {
33068+
const type = getJSDocTypeAssertionType(expr);
33069+
if (!isConstTypeReference(type)) {
33070+
return getTypeFromTypeNode(type);
33071+
}
33072+
}
33073+
expr = skipParentheses(node);
3306033074
// Optimize for the common case of a call to a function with a single non-generic call
3306133075
// signature where we can just fetch the return type without checking the arguments.
3306233076
if (isCallExpression(expr) && expr.expression.kind !== SyntaxKind.SuperKeyword && !isRequireCall(expr, /*checkArgumentIsStringLiteralLike*/ true) && !isSymbolOrSymbolForCall(expr)) {
@@ -33143,9 +33157,9 @@ namespace ts {
3314333157
}
3314433158

3314533159
function checkParenthesizedExpression(node: ParenthesizedExpression, checkMode?: CheckMode): Type {
33146-
const tag = isInJSFile(node) ? getJSDocTypeTag(node) : undefined;
33147-
if (tag) {
33148-
return checkAssertionWorker(tag.typeExpression.type, tag.typeExpression.type, node.expression, checkMode);
33160+
if (isJSDocTypeAssertion(node)) {
33161+
const type = getJSDocTypeAssertionType(node);
33162+
return checkAssertionWorker(type, type, node.expression, checkMode);
3314933163
}
3315033164
return checkExpression(node.expression, checkMode);
3315133165
}
@@ -36093,7 +36107,7 @@ namespace ts {
3609336107
if (getFalsyFlags(type)) return;
3609436108

3609536109
const location = isBinaryExpression(condExpr) ? condExpr.right : condExpr;
36096-
if (isPropertyAccessExpression(location) && isAssertionExpression(skipParentheses(location.expression))) {
36110+
if (isPropertyAccessExpression(location) && isTypeAssertion(location.expression)) {
3609736111
return;
3609836112
}
3609936113

src/compiler/factory/utilities.ts

+15
Original file line numberDiff line numberDiff line change
@@ -416,9 +416,24 @@ namespace ts {
416416
node.kind === SyntaxKind.CommaListExpression;
417417
}
418418

419+
export function isJSDocTypeAssertion(node: Node): node is JSDocTypeAssertion {
420+
return isParenthesizedExpression(node)
421+
&& isInJSFile(node)
422+
&& !!getJSDocTypeTag(node);
423+
}
424+
425+
export function getJSDocTypeAssertionType(node: JSDocTypeAssertion) {
426+
const type = getJSDocType(node);
427+
Debug.assertIsDefined(type);
428+
return type;
429+
}
430+
419431
export function isOuterExpression(node: Node, kinds = OuterExpressionKinds.All): node is OuterExpression {
420432
switch (node.kind) {
421433
case SyntaxKind.ParenthesizedExpression:
434+
if (kinds & OuterExpressionKinds.ExcludeJSDocTypeAssertion && isJSDocTypeAssertion(node)) {
435+
return false;
436+
}
422437
return (kinds & OuterExpressionKinds.Parentheses) !== 0;
423438
case SyntaxKind.TypeAssertionExpression:
424439
case SyntaxKind.AsExpression:

src/compiler/types.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -2253,6 +2253,11 @@ namespace ts {
22532253
readonly expression: Expression;
22542254
}
22552255

2256+
/* @internal */
2257+
export interface JSDocTypeAssertion extends ParenthesizedExpression {
2258+
readonly _jsDocTypeAssertionBrand: never;
2259+
}
2260+
22562261
export interface ArrayLiteralExpression extends PrimaryExpression {
22572262
readonly kind: SyntaxKind.ArrayLiteralExpression;
22582263
readonly elements: NodeArray<Expression>;
@@ -6889,7 +6894,9 @@ namespace ts {
68896894
PartiallyEmittedExpressions = 1 << 3,
68906895

68916896
Assertions = TypeAssertions | NonNullAssertions,
6892-
All = Parentheses | Assertions | PartiallyEmittedExpressions
6897+
All = Parentheses | Assertions | PartiallyEmittedExpressions,
6898+
6899+
ExcludeJSDocTypeAssertion = 1 << 4,
68936900
}
68946901

68956902
/* @internal */

src/compiler/utilities.ts

+29-6
Original file line numberDiff line numberDiff line change
@@ -2634,13 +2634,13 @@ namespace ts {
26342634
let result: (JSDoc | JSDocTag)[] | undefined;
26352635
// Pull parameter comments from declaring function as well
26362636
if (isVariableLike(hostNode) && hasInitializer(hostNode) && hasJSDocNodes(hostNode.initializer!)) {
2637-
result = append(result, last((hostNode.initializer as HasJSDoc).jsDoc!));
2637+
result = addRange(result, filterOwnedJSDocTags(hostNode, last((hostNode.initializer as HasJSDoc).jsDoc!)));
26382638
}
26392639

26402640
let node: Node | undefined = hostNode;
26412641
while (node && node.parent) {
26422642
if (hasJSDocNodes(node)) {
2643-
result = append(result, last(node.jsDoc!));
2643+
result = addRange(result, filterOwnedJSDocTags(hostNode, last(node.jsDoc!)));
26442644
}
26452645

26462646
if (node.kind === SyntaxKind.Parameter) {
@@ -2656,6 +2656,26 @@ namespace ts {
26562656
return result || emptyArray;
26572657
}
26582658

2659+
function filterOwnedJSDocTags(hostNode: Node, jsDoc: JSDoc | JSDocTag) {
2660+
if (isJSDoc(jsDoc)) {
2661+
const ownedTags = filter(jsDoc.tags, tag => ownsJSDocTag(hostNode, tag));
2662+
return jsDoc.tags === ownedTags ? [jsDoc] : ownedTags;
2663+
}
2664+
return ownsJSDocTag(hostNode, jsDoc) ? [jsDoc] : undefined;
2665+
}
2666+
2667+
/**
2668+
* Determines whether a host node owns a jsDoc tag. A `@type` tag attached to a
2669+
* a ParenthesizedExpression belongs only to the ParenthesizedExpression.
2670+
*/
2671+
function ownsJSDocTag(hostNode: Node, tag: JSDocTag) {
2672+
return !isJSDocTypeTag(tag)
2673+
|| !tag.parent
2674+
|| !isJSDoc(tag.parent)
2675+
|| !isParenthesizedExpression(tag.parent.parent)
2676+
|| tag.parent.parent === hostNode;
2677+
}
2678+
26592679
export function getNextJSDocCommentLocation(node: Node) {
26602680
const parent = node.parent;
26612681
if (parent.kind === SyntaxKind.PropertyAssignment ||
@@ -2899,10 +2919,13 @@ namespace ts {
28992919
return [child, node];
29002920
}
29012921

2902-
export function skipParentheses(node: Expression): Expression;
2903-
export function skipParentheses(node: Node): Node;
2904-
export function skipParentheses(node: Node): Node {
2905-
return skipOuterExpressions(node, OuterExpressionKinds.Parentheses);
2922+
export function skipParentheses(node: Expression, excludeJSDocTypeAssertions?: boolean): Expression;
2923+
export function skipParentheses(node: Node, excludeJSDocTypeAssertions?: boolean): Node;
2924+
export function skipParentheses(node: Node, excludeJSDocTypeAssertions?: boolean): Node {
2925+
const flags = excludeJSDocTypeAssertions ?
2926+
OuterExpressionKinds.Parentheses | OuterExpressionKinds.ExcludeJSDocTypeAssertion :
2927+
OuterExpressionKinds.Parentheses;
2928+
return skipOuterExpressions(node, flags);
29062929
}
29072930

29082931
// a node is delete target iff. it is PropertyAccessExpression/ElementAccessExpression with parentheses skipped

tests/baselines/reference/api/tsserverlibrary.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3218,7 +3218,8 @@ declare namespace ts {
32183218
NonNullAssertions = 4,
32193219
PartiallyEmittedExpressions = 8,
32203220
Assertions = 6,
3221-
All = 15
3221+
All = 15,
3222+
ExcludeJSDocTypeAssertion = 16
32223223
}
32233224
export type TypeOfTag = "undefined" | "number" | "bigint" | "boolean" | "string" | "symbol" | "object" | "function";
32243225
export interface NodeFactory {

tests/baselines/reference/api/typescript.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3218,7 +3218,8 @@ declare namespace ts {
32183218
NonNullAssertions = 4,
32193219
PartiallyEmittedExpressions = 8,
32203220
Assertions = 6,
3221-
All = 15
3221+
All = 15,
3222+
ExcludeJSDocTypeAssertion = 16
32223223
}
32233224
export type TypeOfTag = "undefined" | "number" | "bigint" | "boolean" | "string" | "symbol" | "object" | "function";
32243225
export interface NodeFactory {

tests/baselines/reference/jsdocTypeTagCast.errors.txt

+4-1
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,7 @@ tests/cases/conformance/jsdoc/b.js(67,8): error TS2454: Variable 'numOrStr' is u
123123
}
124124

125125

126-
126+
var asConst1 = /** @type {const} */(1);
127+
var asConst2 = /** @type {const} */({
128+
x: 1
129+
});

tests/baselines/reference/jsdocTypeTagCast.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error
7474
}
7575

7676

77-
77+
var asConst1 = /** @type {const} */(1);
78+
var asConst2 = /** @type {const} */({
79+
x: 1
80+
});
7881

7982
//// [a.js]
8083
var W;
@@ -154,3 +157,7 @@ var str;
154157
if ( /** @type {numOrStr is string} */(numOrStr === undefined)) { // Error
155158
str = numOrStr; // Error, no narrowing occurred
156159
}
160+
var asConst1 = /** @type {const} */ (1);
161+
var asConst2 = /** @type {const} */ ({
162+
x: 1
163+
});

tests/baselines/reference/jsdocTypeTagCast.symbols

+9
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,13 @@ if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error
157157
}
158158

159159

160+
var asConst1 = /** @type {const} */(1);
161+
>asConst1 : Symbol(asConst1, Decl(b.js, 70, 3))
160162

163+
var asConst2 = /** @type {const} */({
164+
>asConst2 : Symbol(asConst2, Decl(b.js, 71, 3))
165+
166+
x: 1
167+
>x : Symbol(x, Decl(b.js, 71, 37))
168+
169+
});

tests/baselines/reference/jsdocTypeTagCast.types

+14
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,18 @@ if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error
209209
}
210210

211211

212+
var asConst1 = /** @type {const} */(1);
213+
>asConst1 : 1
214+
>(1) : 1
215+
>1 : 1
212216

217+
var asConst2 = /** @type {const} */({
218+
>asConst2 : { readonly x: 1; }
219+
>({ x: 1}) : { readonly x: 1; }
220+
>{ x: 1} : { readonly x: 1; }
221+
222+
x: 1
223+
>x : 1
224+
>1 : 1
225+
226+
});

tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts

+4
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,7 @@ if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error
7676
}
7777

7878

79+
var asConst1 = /** @type {const} */(1);
80+
var asConst2 = /** @type {const} */({
81+
x: 1
82+
});

0 commit comments

Comments
 (0)