Skip to content

fix(typescript-estree, eslint-plugin): stop adding ParenthesizedExpressions to node maps #226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 123 additions & 117 deletions packages/eslint-plugin/lib/rules/no-unnecessary-type-assertion.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion packages/typescript-estree/src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const binExp = (3 + 5);

class Bar {
['test']: string;
}
Loading