From 8a1cd334515eafa96b1b864cbdcbc24161ea327b Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 17 Jul 2017 23:39:20 -0700 Subject: [PATCH] Use jsdoc casts (#17251) * Allow jsdoc casts of parenthesized expressions * Feedback from #17211 --- src/compiler/checker.ts | 26 ++- src/compiler/parser.ts | 2 +- src/compiler/utilities.ts | 3 +- .../reference/jsdocTypeTagCast.errors.txt | 129 +++++++++++++++ tests/baselines/reference/jsdocTypeTagCast.js | 151 ++++++++++++++++++ .../conformance/jsdoc/jsdocTypeTagCast.ts | 78 +++++++++ 6 files changed, 382 insertions(+), 7 deletions(-) create mode 100644 tests/baselines/reference/jsdocTypeTagCast.errors.txt create mode 100644 tests/baselines/reference/jsdocTypeTagCast.js create mode 100644 tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index dafa75372e1..be14b7d95c9 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -16256,15 +16256,19 @@ namespace ts { } function checkAssertion(node: AssertionExpression) { - const exprType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(checkExpression(node.expression))); + return checkAssertionWorker(node, node.type, node.expression); + } - checkSourceElement(node.type); - const targetType = getTypeFromTypeNode(node.type); + function checkAssertionWorker(errNode: Node, type: TypeNode, expression: UnaryExpression | Expression, checkMode?: CheckMode) { + const exprType = getRegularTypeOfObjectLiteral(getBaseTypeOfLiteralType(checkExpression(expression, checkMode))); + + checkSourceElement(type); + const targetType = getTypeFromTypeNode(type); if (produceDiagnostics && targetType !== unknownType) { const widenedType = getWidenedType(exprType); if (!isTypeComparableTo(targetType, widenedType)) { - checkTypeComparableTo(exprType, targetType, node, Diagnostics.Type_0_cannot_be_converted_to_type_1); + checkTypeComparableTo(exprType, targetType, errNode, Diagnostics.Type_0_cannot_be_converted_to_type_1); } } return targetType; @@ -17735,6 +17739,18 @@ namespace ts { return type; } + function checkParenthesizedExpression(node: ParenthesizedExpression, checkMode?: CheckMode): Type { + if (isInJavaScriptFile(node) && node.jsDoc) { + const typecasts = flatMap(node.jsDoc, doc => filter(doc.tags, tag => tag.kind === SyntaxKind.JSDocTypeTag)); + if (typecasts && typecasts.length) { + // We should have already issued an error if there were multiple type jsdocs + const cast = typecasts[0] as JSDocTypeTag; + return checkAssertionWorker(cast, cast.typeExpression.type, node.expression, checkMode); + } + } + return checkExpression(node.expression, checkMode); + } + function checkExpressionWorker(node: Expression, checkMode: CheckMode): Type { switch (node.kind) { case SyntaxKind.Identifier: @@ -17774,7 +17790,7 @@ namespace ts { case SyntaxKind.TaggedTemplateExpression: return checkTaggedTemplateExpression(node); case SyntaxKind.ParenthesizedExpression: - return checkExpression((node).expression, checkMode); + return checkParenthesizedExpression(node, checkMode); case SyntaxKind.ClassExpression: return checkClassExpression(node); case SyntaxKind.FunctionExpression: diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 20c9559a8f4..4613f49a749 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -4342,7 +4342,7 @@ namespace ts { parseExpected(SyntaxKind.OpenParenToken); node.expression = allowInAnd(parseExpression); parseExpected(SyntaxKind.CloseParenToken); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function parseSpreadElement(): Expression { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index b7fabdda381..ab0afb12820 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -640,7 +640,8 @@ namespace ts { const commentRanges = (node.kind === SyntaxKind.Parameter || node.kind === SyntaxKind.TypeParameter || node.kind === SyntaxKind.FunctionExpression || - node.kind === SyntaxKind.ArrowFunction) ? + node.kind === SyntaxKind.ArrowFunction || + node.kind === SyntaxKind.ParenthesizedExpression) ? concatenate(getTrailingCommentRanges(text, node.pos), getLeadingCommentRanges(text, node.pos)) : getLeadingCommentRangesOfNodeFromText(node, text); // True if the comment starts with '/**' but not if it is '/**/' diff --git a/tests/baselines/reference/jsdocTypeTagCast.errors.txt b/tests/baselines/reference/jsdocTypeTagCast.errors.txt new file mode 100644 index 00000000000..f1f53ec1ecb --- /dev/null +++ b/tests/baselines/reference/jsdocTypeTagCast.errors.txt @@ -0,0 +1,129 @@ +tests/cases/conformance/jsdoc/b.js(4,13): error TS2352: Type 'number' cannot be converted to type 'string'. +tests/cases/conformance/jsdoc/b.js(45,16): error TS2352: Type 'SomeOther' cannot be converted to type 'SomeBase'. + Property 'p' is missing in type 'SomeOther'. +tests/cases/conformance/jsdoc/b.js(49,19): error TS2352: Type 'SomeOther' cannot be converted to type 'SomeDerived'. + Property 'x' is missing in type 'SomeOther'. +tests/cases/conformance/jsdoc/b.js(51,17): error TS2352: Type 'SomeDerived' cannot be converted to type 'SomeOther'. + Property 'q' is missing in type 'SomeDerived'. +tests/cases/conformance/jsdoc/b.js(52,17): error TS2352: Type 'SomeBase' cannot be converted to type 'SomeOther'. + Property 'q' is missing in type 'SomeBase'. +tests/cases/conformance/jsdoc/b.js(58,1): error TS2322: Type '{ p: string | number | undefined; }' is not assignable to type 'SomeBase'. + Types of property 'p' are incompatible. + Type 'string | number | undefined' is not assignable to type 'number'. + Type 'undefined' is not assignable to type 'number'. +tests/cases/conformance/jsdoc/b.js(66,8): error TS2352: Type 'boolean' cannot be converted to type 'string | number'. +tests/cases/conformance/jsdoc/b.js(66,15): error TS2304: Cannot find name 'numOrStr'. +tests/cases/conformance/jsdoc/b.js(66,24): error TS1005: '}' expected. +tests/cases/conformance/jsdoc/b.js(66,38): error TS2454: Variable 'numOrStr' is used before being assigned. +tests/cases/conformance/jsdoc/b.js(67,2): error TS2322: Type 'string | number' is not assignable to type 'string'. + Type 'number' is not assignable to type 'string'. +tests/cases/conformance/jsdoc/b.js(67,8): error TS2454: Variable 'numOrStr' is used before being assigned. + + +==== tests/cases/conformance/jsdoc/a.ts (0 errors) ==== + var W: string; + +==== tests/cases/conformance/jsdoc/b.js (12 errors) ==== + // @ts-check + var W = /** @type {string} */(/** @type {*} */ (4)); + + var W = /** @type {string} */(4); // Error + ~~~~~~~~~~~~~~ +!!! error TS2352: Type 'number' cannot be converted to type 'string'. + + /** @type {*} */ + var a; + + /** @type {string} */ + var s; + + var a = /** @type {*} */("" + 4); + var s = "" + /** @type {*} */(4); + + class SomeBase { + constructor() { + this.p = 42; + } + } + class SomeDerived extends SomeBase { + constructor() { + super(); + this.x = 42; + } + } + class SomeOther { + constructor() { + this.q = 42; + } + } + + function SomeFakeClass() { + /** @type {string|number} */ + this.p = "bar"; + } + + // Type assertion should check for assignability in either direction + var someBase = new SomeBase(); + var someDerived = new SomeDerived(); + var someOther = new SomeOther(); + var someFakeClass = new SomeFakeClass(); + + someBase = /** @type {SomeBase} */(someDerived); + someBase = /** @type {SomeBase} */(someBase); + someBase = /** @type {SomeBase} */(someOther); // Error + ~~~~~~~~~~~~~~~~ +!!! error TS2352: Type 'SomeOther' cannot be converted to type 'SomeBase'. +!!! error TS2352: Property 'p' is missing in type 'SomeOther'. + + someDerived = /** @type {SomeDerived} */(someDerived); + someDerived = /** @type {SomeDerived} */(someBase); + someDerived = /** @type {SomeDerived} */(someOther); // Error + ~~~~~~~~~~~~~~~~~~~ +!!! error TS2352: Type 'SomeOther' cannot be converted to type 'SomeDerived'. +!!! error TS2352: Property 'x' is missing in type 'SomeOther'. + + someOther = /** @type {SomeOther} */(someDerived); // Error + ~~~~~~~~~~~~~~~~~ +!!! error TS2352: Type 'SomeDerived' cannot be converted to type 'SomeOther'. +!!! error TS2352: Property 'q' is missing in type 'SomeDerived'. + someOther = /** @type {SomeOther} */(someBase); // Error + ~~~~~~~~~~~~~~~~~ +!!! error TS2352: Type 'SomeBase' cannot be converted to type 'SomeOther'. +!!! error TS2352: Property 'q' is missing in type 'SomeBase'. + someOther = /** @type {SomeOther} */(someOther); + + someFakeClass = someBase; + someFakeClass = someDerived; + + someBase = someFakeClass; // Error + ~~~~~~~~ +!!! error TS2322: Type '{ p: string | number | undefined; }' is not assignable to type 'SomeBase'. +!!! error TS2322: Types of property 'p' are incompatible. +!!! error TS2322: Type 'string | number | undefined' is not assignable to type 'number'. +!!! error TS2322: Type 'undefined' is not assignable to type 'number'. + someBase = /** @type {SomeBase} */(someFakeClass); + + // Type assertion cannot be a type-predicate type + /** @type {number | string} */ + var numOrStr; + /** @type {string} */ + var str; + if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error + ~~~~~~~~~~~~~~~ +!!! error TS2352: Type 'boolean' cannot be converted to type 'string | number'. + ~~~~~~~~ +!!! error TS2304: Cannot find name 'numOrStr'. + ~~ +!!! error TS1005: '}' expected. + ~~~~~~~~ +!!! error TS2454: Variable 'numOrStr' is used before being assigned. + str = numOrStr; // Error, no narrowing occurred + ~~~ +!!! error TS2322: Type 'string | number' is not assignable to type 'string'. +!!! error TS2322: Type 'number' is not assignable to type 'string'. + ~~~~~~~~ +!!! error TS2454: Variable 'numOrStr' is used before being assigned. + } + + + \ No newline at end of file diff --git a/tests/baselines/reference/jsdocTypeTagCast.js b/tests/baselines/reference/jsdocTypeTagCast.js new file mode 100644 index 00000000000..c2df50cd6dd --- /dev/null +++ b/tests/baselines/reference/jsdocTypeTagCast.js @@ -0,0 +1,151 @@ +//// [tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts] //// + +//// [a.ts] +var W: string; + +//// [b.js] +// @ts-check +var W = /** @type {string} */(/** @type {*} */ (4)); + +var W = /** @type {string} */(4); // Error + +/** @type {*} */ +var a; + +/** @type {string} */ +var s; + +var a = /** @type {*} */("" + 4); +var s = "" + /** @type {*} */(4); + +class SomeBase { + constructor() { + this.p = 42; + } +} +class SomeDerived extends SomeBase { + constructor() { + super(); + this.x = 42; + } +} +class SomeOther { + constructor() { + this.q = 42; + } +} + +function SomeFakeClass() { + /** @type {string|number} */ + this.p = "bar"; +} + +// Type assertion should check for assignability in either direction +var someBase = new SomeBase(); +var someDerived = new SomeDerived(); +var someOther = new SomeOther(); +var someFakeClass = new SomeFakeClass(); + +someBase = /** @type {SomeBase} */(someDerived); +someBase = /** @type {SomeBase} */(someBase); +someBase = /** @type {SomeBase} */(someOther); // Error + +someDerived = /** @type {SomeDerived} */(someDerived); +someDerived = /** @type {SomeDerived} */(someBase); +someDerived = /** @type {SomeDerived} */(someOther); // Error + +someOther = /** @type {SomeOther} */(someDerived); // Error +someOther = /** @type {SomeOther} */(someBase); // Error +someOther = /** @type {SomeOther} */(someOther); + +someFakeClass = someBase; +someFakeClass = someDerived; + +someBase = someFakeClass; // Error +someBase = /** @type {SomeBase} */(someFakeClass); + +// Type assertion cannot be a type-predicate type +/** @type {number | string} */ +var numOrStr; +/** @type {string} */ +var str; +if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error + str = numOrStr; // Error, no narrowing occurred +} + + + + +//// [a.js] +var W; +//// [b.js] +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +// @ts-check +var W = ((4)); +var W = (4); // Error +/** @type {*} */ +var a; +/** @type {string} */ +var s; +var a = ("" + 4); +var s = "" + (4); +var SomeBase = (function () { + function SomeBase() { + this.p = 42; + } + return SomeBase; +}()); +var SomeDerived = (function (_super) { + __extends(SomeDerived, _super); + function SomeDerived() { + var _this = _super.call(this) || this; + _this.x = 42; + return _this; + } + return SomeDerived; +}(SomeBase)); +var SomeOther = (function () { + function SomeOther() { + this.q = 42; + } + return SomeOther; +}()); +function SomeFakeClass() { + /** @type {string|number} */ + this.p = "bar"; +} +// Type assertion should check for assignability in either direction +var someBase = new SomeBase(); +var someDerived = new SomeDerived(); +var someOther = new SomeOther(); +var someFakeClass = new SomeFakeClass(); +someBase = (someDerived); +someBase = (someBase); +someBase = (someOther); // Error +someDerived = (someDerived); +someDerived = (someBase); +someDerived = (someOther); // Error +someOther = (someDerived); // Error +someOther = (someBase); // Error +someOther = (someOther); +someFakeClass = someBase; +someFakeClass = someDerived; +someBase = someFakeClass; // Error +someBase = (someFakeClass); +// Type assertion cannot be a type-predicate type +/** @type {number | string} */ +var numOrStr; +/** @type {string} */ +var str; +if ((numOrStr === undefined)) { + str = numOrStr; // Error, no narrowing occurred +} diff --git a/tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts b/tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts new file mode 100644 index 00000000000..60ca43b0533 --- /dev/null +++ b/tests/cases/conformance/jsdoc/jsdocTypeTagCast.ts @@ -0,0 +1,78 @@ +// @allowJS: true +// @suppressOutputPathCheck: true +// @strictNullChecks: true + +// @filename: a.ts +var W: string; + +// @filename: b.js +// @ts-check +var W = /** @type {string} */(/** @type {*} */ (4)); + +var W = /** @type {string} */(4); // Error + +/** @type {*} */ +var a; + +/** @type {string} */ +var s; + +var a = /** @type {*} */("" + 4); +var s = "" + /** @type {*} */(4); + +class SomeBase { + constructor() { + this.p = 42; + } +} +class SomeDerived extends SomeBase { + constructor() { + super(); + this.x = 42; + } +} +class SomeOther { + constructor() { + this.q = 42; + } +} + +function SomeFakeClass() { + /** @type {string|number} */ + this.p = "bar"; +} + +// Type assertion should check for assignability in either direction +var someBase = new SomeBase(); +var someDerived = new SomeDerived(); +var someOther = new SomeOther(); +var someFakeClass = new SomeFakeClass(); + +someBase = /** @type {SomeBase} */(someDerived); +someBase = /** @type {SomeBase} */(someBase); +someBase = /** @type {SomeBase} */(someOther); // Error + +someDerived = /** @type {SomeDerived} */(someDerived); +someDerived = /** @type {SomeDerived} */(someBase); +someDerived = /** @type {SomeDerived} */(someOther); // Error + +someOther = /** @type {SomeOther} */(someDerived); // Error +someOther = /** @type {SomeOther} */(someBase); // Error +someOther = /** @type {SomeOther} */(someOther); + +someFakeClass = someBase; +someFakeClass = someDerived; + +someBase = someFakeClass; // Error +someBase = /** @type {SomeBase} */(someFakeClass); + +// Type assertion cannot be a type-predicate type +/** @type {number | string} */ +var numOrStr; +/** @type {string} */ +var str; +if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error + str = numOrStr; // Error, no narrowing occurred +} + +