diff --git a/packages/eslint-plugin/lib/rules/no-unnecessary-type-assertion.js b/packages/eslint-plugin/lib/rules/no-unnecessary-type-assertion.js index ed1747c0d95e..bcbe300a01b0 100644 --- a/packages/eslint-plugin/lib/rules/no-unnecessary-type-assertion.js +++ b/packages/eslint-plugin/lib/rules/no-unnecessary-type-assertion.js @@ -15,120 +15,6 @@ const util = require('../util'); // Rule Definition //------------------------------------------------------------------------------ -/** - * Sometimes tuple types don't have ObjectFlags.Tuple set, like when they're being matched against an inferred type. - * So, in addition, check if there are integer properties 0..n and no other numeric keys - * @param {ts.ObjectType} type type - * @returns {boolean} true if type could be a tuple type - */ -function couldBeTupleType(type) { - const properties = type.getProperties(); - - if (properties.length === 0) { - return false; - } - let i = 0; - - for (; i < properties.length; ++i) { - const name = properties[i].name; - - if (String(i) !== name) { - if (i === 0) { - // if there are no integer properties, this is not a tuple - return false; - } - break; - } - } - for (; i < properties.length; ++i) { - if (String(+properties[i].name) === properties[i].name) { - return false; // if there are any other numeric properties, this is not a tuple - } - } - return true; -} - -/** - * - * @param {Node} node node being linted - * @param {Context} context linting context - * @param {ts.TypeChecker} checker TypeScript typechecker - * @returns {void} - */ -function checkNonNullAssertion(node, context, checker) { - /** - * Corresponding TSNode is guaranteed to be in map - * @type {ts.NonNullExpression} - */ - const originalNode = context.parserServices.esTreeNodeToTSNodeMap.get(node); - const type = checker.getTypeAtLocation(originalNode.expression); - - if (type === checker.getNonNullableType(type)) { - context.report({ - node, - messageId: 'unnecessaryAssertion', - fix(fixer) { - return fixer.removeRange([ - originalNode.expression.end, - originalNode.end - ]); - } - }); - } -} - -/** - * @param {Node} node node being linted - * @param {Context} context linting context - * @param {ts.TypeChecker} checker TypeScript typechecker - * @returns {void} - */ -function verifyCast(node, context, checker) { - /** - * * Corresponding TSNode is guaranteed to be in map - * @type {ts.AssertionExpression} - */ - const originalNode = context.parserServices.esTreeNodeToTSNodeMap.get(node); - const options = context.options[0]; - - if ( - options && - options.typesToIgnore && - options.typesToIgnore.indexOf(originalNode.type.getText()) !== -1 - ) { - return; - } - const castType = checker.getTypeAtLocation(originalNode); - - if ( - tsutils.isTypeFlagSet(castType, ts.TypeFlags.Literal) || - (tsutils.isObjectType(castType) && - (tsutils.isObjectFlagSet(castType, ts.ObjectFlags.Tuple) || - couldBeTupleType(castType))) - ) { - // It's not always safe to remove a cast to a literal type or tuple - // type, as those types are sometimes widened without the cast. - return; - } - - const uncastType = checker.getTypeAtLocation(originalNode.expression); - - if (uncastType === castType) { - context.report({ - node, - messageId: 'unnecessaryAssertion', - fix(fixer) { - return originalNode.kind === ts.SyntaxKind.TypeAssertionExpression - ? fixer.removeRange([ - originalNode.getStart(), - originalNode.expression.getStart() - ]) - : fixer.removeRange([originalNode.expression.end, originalNode.end]); - } - }); - } -} - /** @type {import("eslint").Rule.RuleModule} */ module.exports = { meta: { @@ -162,17 +48,137 @@ module.exports = { }, create(context) { + const sourceCode = context.getSourceCode(); const checker = util.getParserServices(context).program.getTypeChecker(); + /** + * Sometimes tuple types don't have ObjectFlags.Tuple set, like when they're being matched against an inferred type. + * So, in addition, check if there are integer properties 0..n and no other numeric keys + * @param {ts.ObjectType} type type + * @returns {boolean} true if type could be a tuple type + */ + function couldBeTupleType(type) { + const properties = type.getProperties(); + + if (properties.length === 0) { + return false; + } + let i = 0; + + for (; i < properties.length; ++i) { + const name = properties[i].name; + + if (String(i) !== name) { + if (i === 0) { + // if there are no integer properties, this is not a tuple + return false; + } + break; + } + } + for (; i < properties.length; ++i) { + if (String(+properties[i].name) === properties[i].name) { + return false; // if there are any other numeric properties, this is not a tuple + } + } + return true; + } + + /** + * @param {Node} node node being linted + * @returns {void} + */ + function checkNonNullAssertion(node) { + /** + * Corresponding TSNode is guaranteed to be in map + * @type {ts.NonNullExpression} + */ + const originalNode = context.parserServices.esTreeNodeToTSNodeMap.get( + node + ); + const type = checker.getTypeAtLocation(originalNode.expression); + + if (type === checker.getNonNullableType(type)) { + context.report({ + node, + messageId: 'unnecessaryAssertion', + fix(fixer) { + return fixer.removeRange([ + originalNode.expression.end, + originalNode.end + ]); + } + }); + } + } + + /** + * @param {Node} node node being linted + * @returns {void} + */ + function verifyCast(node) { + const options = context.options[0]; + + if ( + options && + options.typesToIgnore && + options.typesToIgnore.indexOf( + sourceCode.getText(node.typeAnnotation) + ) !== -1 + ) { + return; + } + + /** + * Corresponding TSNode is guaranteed to be in map + * @type {ts.AssertionExpression} + */ + const originalNode = context.parserServices.esTreeNodeToTSNodeMap.get( + node + ); + const castType = checker.getTypeAtLocation(originalNode); + + if ( + tsutils.isTypeFlagSet(castType, ts.TypeFlags.Literal) || + (tsutils.isObjectType(castType) && + (tsutils.isObjectFlagSet(castType, ts.ObjectFlags.Tuple) || + couldBeTupleType(castType))) + ) { + // It's not always safe to remove a cast to a literal type or tuple + // type, as those types are sometimes widened without the cast. + return; + } + + const uncastType = checker.getTypeAtLocation(originalNode.expression); + + if (uncastType === castType) { + context.report({ + node, + messageId: 'unnecessaryAssertion', + fix(fixer) { + return originalNode.kind === ts.SyntaxKind.TypeAssertionExpression + ? fixer.removeRange([ + originalNode.getStart(), + originalNode.expression.getStart() + ]) + : fixer.removeRange([ + originalNode.expression.end, + originalNode.end + ]); + } + }); + } + } + return { TSNonNullExpression(node) { - checkNonNullAssertion(node, context, checker); + checkNonNullAssertion(node); }, TSTypeAssertion(node) { - verifyCast(node, context, checker); + verifyCast(node); }, TSAsExpression(node) { - verifyCast(node, context, checker); + verifyCast(node); } }; } diff --git a/packages/eslint-plugin/tests/lib/rules/no-unnecessary-type-assertion.js b/packages/eslint-plugin/tests/lib/rules/no-unnecessary-type-assertion.js index 633f1a334330..8795cff89c51 100644 --- a/packages/eslint-plugin/tests/lib/rules/no-unnecessary-type-assertion.js +++ b/packages/eslint-plugin/tests/lib/rules/no-unnecessary-type-assertion.js @@ -51,6 +51,18 @@ type Foo = number; const foo = (3 + 5) as Foo;`, options: [{ typesToIgnore: ['Foo'] }] }, + { + code: `const foo = (3 + 5) as any;`, + options: [{ typesToIgnore: ['any'] }] + }, + { + code: `((Syntax as any).ArrayExpression = 'foo')`, + options: [{ typesToIgnore: ['any'] }] + }, + { + code: `const foo = (3 + 5) as string;`, + options: [{ typesToIgnore: ['string'] }] + }, { code: ` type Foo = number; diff --git a/packages/typescript-estree/src/convert.ts b/packages/typescript-estree/src/convert.ts index bf121fa01bb0..9f14bbcc2489 100644 --- a/packages/typescript-estree/src/convert.ts +++ b/packages/typescript-estree/src/convert.ts @@ -119,7 +119,15 @@ export class Converter { if (result && this.options.shouldProvideParserServices) { this.tsNodeToESTreeNodeMap.set(node, result); - this.esTreeNodeToTSNodeMap.set(result, node); + if ( + node.kind !== SyntaxKind.ParenthesizedExpression && + node.kind !== SyntaxKind.ComputedPropertyName + ) { + // Parenthesized expressions and computed property names do not have individual nodes in ESTree. + // Therefore, result.type will never "match" node.kind if it is a ParenthesizedExpression + // or a ComputedPropertyName and, furthermore, will overwrite the "matching" node + this.esTreeNodeToTSNodeMap.set(result, node); + } } this.inTypeMode = typeMode; diff --git a/packages/typescript-estree/tests/fixtures/semanticInfo/non-existent-estree-nodes.src.ts b/packages/typescript-estree/tests/fixtures/semanticInfo/non-existent-estree-nodes.src.ts new file mode 100644 index 000000000000..4eb9dba432b4 --- /dev/null +++ b/packages/typescript-estree/tests/fixtures/semanticInfo/non-existent-estree-nodes.src.ts @@ -0,0 +1,5 @@ +const binExp = (3 + 5); + +class Bar { + ['test']: string; +} diff --git a/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.ts.snap index 5af1b41ba261..f5722861dd24 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.ts.snap @@ -1133,4 +1133,611 @@ Object { } `; +exports[`semanticInfo fixtures/non-existent-estree-nodes.src 1`] = ` +Object { + "body": Array [ + Object { + "declarations": Array [ + Object { + "id": Object { + "loc": Object { + "end": Object { + "column": 12, + "line": 1, + }, + "start": Object { + "column": 6, + "line": 1, + }, + }, + "name": "binExp", + "range": Array [ + 6, + 12, + ], + "type": "Identifier", + }, + "init": Object { + "left": Object { + "loc": Object { + "end": Object { + "column": 17, + "line": 1, + }, + "start": Object { + "column": 16, + "line": 1, + }, + }, + "range": Array [ + 16, + 17, + ], + "raw": "3", + "type": "Literal", + "value": 3, + }, + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 16, + "line": 1, + }, + }, + "operator": "+", + "range": Array [ + 16, + 21, + ], + "right": Object { + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 20, + "line": 1, + }, + }, + "range": Array [ + 20, + 21, + ], + "raw": "5", + "type": "Literal", + "value": 5, + }, + "type": "BinaryExpression", + }, + "loc": Object { + "end": Object { + "column": 22, + "line": 1, + }, + "start": Object { + "column": 6, + "line": 1, + }, + }, + "range": Array [ + 6, + 22, + ], + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "loc": Object { + "end": Object { + "column": 23, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 23, + ], + "type": "VariableDeclaration", + }, + Object { + "body": Object { + "body": Array [ + Object { + "computed": true, + "key": Object { + "loc": Object { + "end": Object { + "column": 9, + "line": 4, + }, + "start": Object { + "column": 3, + "line": 4, + }, + }, + "range": Array [ + 40, + 46, + ], + "raw": "'test'", + "type": "Literal", + "value": "test", + }, + "loc": Object { + "end": Object { + "column": 19, + "line": 4, + }, + "start": Object { + "column": 2, + "line": 4, + }, + }, + "range": Array [ + 39, + 56, + ], + "static": false, + "type": "ClassProperty", + "typeAnnotation": Object { + "loc": Object { + "end": Object { + "column": 18, + "line": 4, + }, + "start": Object { + "column": 10, + "line": 4, + }, + }, + "range": Array [ + 47, + 55, + ], + "type": "TSTypeAnnotation", + "typeAnnotation": Object { + "loc": Object { + "end": Object { + "column": 18, + "line": 4, + }, + "start": Object { + "column": 12, + "line": 4, + }, + }, + "range": Array [ + 49, + 55, + ], + "type": "TSStringKeyword", + }, + }, + "value": null, + }, + ], + "loc": Object { + "end": Object { + "column": 1, + "line": 5, + }, + "start": Object { + "column": 10, + "line": 3, + }, + }, + "range": Array [ + 35, + 58, + ], + "type": "ClassBody", + }, + "id": Object { + "loc": Object { + "end": Object { + "column": 9, + "line": 3, + }, + "start": Object { + "column": 6, + "line": 3, + }, + }, + "name": "Bar", + "range": Array [ + 31, + 34, + ], + "type": "Identifier", + }, + "loc": Object { + "end": Object { + "column": 1, + "line": 5, + }, + "start": Object { + "column": 0, + "line": 3, + }, + }, + "range": Array [ + 25, + 58, + ], + "superClass": null, + "type": "ClassDeclaration", + }, + ], + "comments": Array [], + "loc": Object { + "end": Object { + "column": 0, + "line": 6, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 59, + ], + "sourceType": "script", + "tokens": Array [ + Object { + "loc": Object { + "end": Object { + "column": 5, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "range": Array [ + 0, + 5, + ], + "type": "Keyword", + "value": "const", + }, + Object { + "loc": Object { + "end": Object { + "column": 12, + "line": 1, + }, + "start": Object { + "column": 6, + "line": 1, + }, + }, + "range": Array [ + 6, + 12, + ], + "type": "Identifier", + "value": "binExp", + }, + Object { + "loc": Object { + "end": Object { + "column": 14, + "line": 1, + }, + "start": Object { + "column": 13, + "line": 1, + }, + }, + "range": Array [ + 13, + 14, + ], + "type": "Punctuator", + "value": "=", + }, + Object { + "loc": Object { + "end": Object { + "column": 16, + "line": 1, + }, + "start": Object { + "column": 15, + "line": 1, + }, + }, + "range": Array [ + 15, + 16, + ], + "type": "Punctuator", + "value": "(", + }, + Object { + "loc": Object { + "end": Object { + "column": 17, + "line": 1, + }, + "start": Object { + "column": 16, + "line": 1, + }, + }, + "range": Array [ + 16, + 17, + ], + "type": "Numeric", + "value": "3", + }, + Object { + "loc": Object { + "end": Object { + "column": 19, + "line": 1, + }, + "start": Object { + "column": 18, + "line": 1, + }, + }, + "range": Array [ + 18, + 19, + ], + "type": "Punctuator", + "value": "+", + }, + Object { + "loc": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 20, + "line": 1, + }, + }, + "range": Array [ + 20, + 21, + ], + "type": "Numeric", + "value": "5", + }, + Object { + "loc": Object { + "end": Object { + "column": 22, + "line": 1, + }, + "start": Object { + "column": 21, + "line": 1, + }, + }, + "range": Array [ + 21, + 22, + ], + "type": "Punctuator", + "value": ")", + }, + Object { + "loc": Object { + "end": Object { + "column": 23, + "line": 1, + }, + "start": Object { + "column": 22, + "line": 1, + }, + }, + "range": Array [ + 22, + 23, + ], + "type": "Punctuator", + "value": ";", + }, + Object { + "loc": Object { + "end": Object { + "column": 5, + "line": 3, + }, + "start": Object { + "column": 0, + "line": 3, + }, + }, + "range": Array [ + 25, + 30, + ], + "type": "Keyword", + "value": "class", + }, + Object { + "loc": Object { + "end": Object { + "column": 9, + "line": 3, + }, + "start": Object { + "column": 6, + "line": 3, + }, + }, + "range": Array [ + 31, + 34, + ], + "type": "Identifier", + "value": "Bar", + }, + Object { + "loc": Object { + "end": Object { + "column": 11, + "line": 3, + }, + "start": Object { + "column": 10, + "line": 3, + }, + }, + "range": Array [ + 35, + 36, + ], + "type": "Punctuator", + "value": "{", + }, + Object { + "loc": Object { + "end": Object { + "column": 3, + "line": 4, + }, + "start": Object { + "column": 2, + "line": 4, + }, + }, + "range": Array [ + 39, + 40, + ], + "type": "Punctuator", + "value": "[", + }, + Object { + "loc": Object { + "end": Object { + "column": 9, + "line": 4, + }, + "start": Object { + "column": 3, + "line": 4, + }, + }, + "range": Array [ + 40, + 46, + ], + "type": "String", + "value": "'test'", + }, + Object { + "loc": Object { + "end": Object { + "column": 10, + "line": 4, + }, + "start": Object { + "column": 9, + "line": 4, + }, + }, + "range": Array [ + 46, + 47, + ], + "type": "Punctuator", + "value": "]", + }, + Object { + "loc": Object { + "end": Object { + "column": 11, + "line": 4, + }, + "start": Object { + "column": 10, + "line": 4, + }, + }, + "range": Array [ + 47, + 48, + ], + "type": "Punctuator", + "value": ":", + }, + Object { + "loc": Object { + "end": Object { + "column": 18, + "line": 4, + }, + "start": Object { + "column": 12, + "line": 4, + }, + }, + "range": Array [ + 49, + 55, + ], + "type": "Identifier", + "value": "string", + }, + Object { + "loc": Object { + "end": Object { + "column": 19, + "line": 4, + }, + "start": Object { + "column": 18, + "line": 4, + }, + }, + "range": Array [ + 55, + 56, + ], + "type": "Punctuator", + "value": ";", + }, + Object { + "loc": Object { + "end": Object { + "column": 1, + "line": 5, + }, + "start": Object { + "column": 0, + "line": 5, + }, + }, + "range": Array [ + 57, + 58, + ], + "type": "Punctuator", + "value": "}", + }, + ], + "type": "Program", +} +`; + exports[`semanticInfo malformed project file 1`] = `"Compiler option 'compileOnSave' requires a value of type boolean."`; diff --git a/packages/typescript-estree/tests/lib/semanticInfo.ts b/packages/typescript-estree/tests/lib/semanticInfo.ts index 531ab1a34d54..126ad35879b6 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.ts @@ -18,6 +18,11 @@ import { parseCodeAndGenerateServices } from '../../tools/test-utils'; import { parseAndGenerateServices } from '../../src/parser'; +import { + VariableDeclaration, + ClassDeclaration, + ClassProperty +} from '../../src/typedefs'; //------------------------------------------------------------------------------ // Setup @@ -101,6 +106,29 @@ describe('semanticInfo', () => { testIsolatedFile(parseResult); }); + it('non-existent-estree-nodes tests', () => { + const fileName = resolve(FIXTURES_DIR, 'non-existent-estree-nodes.src.ts'); + const parseResult = parseCodeAndGenerateServices( + readFileSync(fileName, 'utf8'), + createOptions(fileName) + ); + + expect(parseResult).toHaveProperty('services.esTreeNodeToTSNodeMap'); + const binaryExpression = (parseResult.ast.body[0] as VariableDeclaration) + .declarations[0].init!; + const tsBinaryExpression = parseResult.services.esTreeNodeToTSNodeMap!.get( + binaryExpression + ); + expect(tsBinaryExpression.kind).toEqual(ts.SyntaxKind.BinaryExpression); + + const computedPropertyString = ((parseResult.ast + .body[1] as ClassDeclaration).body.body[0] as ClassProperty).key; + const tsComputedPropertyString = parseResult.services.esTreeNodeToTSNodeMap!.get( + computedPropertyString + ); + expect(tsComputedPropertyString.kind).toEqual(ts.SyntaxKind.StringLiteral); + }); + it('imported-file tests', () => { const fileName = resolve(FIXTURES_DIR, 'import-file.src.ts'); const parseResult = parseCodeAndGenerateServices(