Skip to content

feat(eslint-plugin): add no-unnecessary-type-assertion rule #157

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 8 commits into from
Jan 29, 2019
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
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ See [@typescript-eslint/parser's README.md](../parser/README.md) for more inform
| [`@typescript-eslint/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` (`no-this-assignment` from TSLint) | | |
| [`@typescript-eslint/no-triple-slash-reference`](./docs/rules/no-triple-slash-reference.md) | Disallow `/// <reference path="" />` comments (`no-reference` from TSLint) | :heavy_check_mark: | |
| [`@typescript-eslint/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases (`interface-over-type-literal` from TSLint) | | |
| [`typescript/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression (`no-unnecessary-type-assertion` from TSLint) | | :wrench: |
| [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables (`no-unused-variable` from TSLint) | :heavy_check_mark: | |
| [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | |
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements (`no-var-requires` from TSLint) | :heavy_check_mark: | |
Expand Down
8 changes: 4 additions & 4 deletions packages/eslint-plugin/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Roadmap
# Roadmap

✅ (27) = done
✅ (28) = done
🌟 (79) = in ESLint core
🔌 (33) = in another plugin
🌓 (16) = implementations differ or ESLint version is missing functionality
🛑 (71) = unimplemented
🛑 (70) = unimplemented

## TSLint rules

Expand All @@ -26,7 +26,7 @@
| [`no-non-null-assertion`] | ✅ | [`@typescript-eslint/no-non-null-assertion`] |
| [`no-parameter-reassignment`] | ✅ | [`no-param-reassign`][no-param-reassign] |
| [`no-reference`] | ✅ | [`@typescript-eslint/no-triple-slash-reference`] |
| [`no-unnecessary-type-assertion`] | 🛑 | N/A |
| [`no-unnecessary-type-assertion`] | | [`@typescript-eslint/no-unnecessary-type-assertion`] |
| [`no-var-requires`] | ✅ | [`@typescript-eslint/no-var-requires`] |
| [`only-arrow-functions`] | 🔌 | [`prefer-arrow/prefer-arrow-functions`] |
| [`prefer-for-of`] | 🛑 | N/A |
Expand Down
57 changes: 57 additions & 0 deletions packages/eslint-plugin/docs/rules/no-unnecessary-type-assertion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Warns if a type assertion does not change the type of an expression (no-unnecessary-type-assertion)

This rule prohibits using a type assertion that does not change the type of an expression.

## Rule Details

This rule aims to prevent unnecessary type assertions.

Examples of **incorrect** code for this rule:

```ts
const foo = 3;
const bar = foo!;
```

```ts
const foo = <3>3;
```

```ts
type Foo = 3;
const foo = <Foo>3;
```

```ts
type Foo = 3;
const foo = 3 as Foo;
```

Examples of **correct** code for this rule:

```ts
const foo = <number>3;
```

```ts
const foo = 3 as number;
```

### Options

This rule optionally takes an object with a single property `typesToIgnore`, which can be set to a list of type names to ignore.

For example, with `@typescript-eslint/no-unnecessary-type-assertion: ["error", { typesToIgnore: ['Foo'] }]`, the following is **correct** code":

```ts
type Foo = 3;
const foo: Foo = 3;
```

## When Not To Use It

If you don't care about having no-op type assertions in your code, then you can turn off this rule.

## Related to

- TSLint: ['no-unnecessary-type-assertion`](https://palantir.github.io/tslint/rules/no-unnecessary-type-assertion/)
179 changes: 179 additions & 0 deletions packages/eslint-plugin/lib/rules/no-unnecessary-type-assertion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* @fileoverview Rule to warn if a type assertion does not change the type of an expression
* @author Benjamin Lichtman
*/

'use strict';
const tsutils = require('tsutils');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@uniqueiniquity @JamesHenry Upgraded to latest canary (1.1.2-alpha.4) to work around #161.

Which then triggered

$ yarn run lint:ts
$ eslint . --ext .ts,.tsx --max-warnings=0
Error: Cannot find module 'tsutils'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)
    at Function.Module._load (internal/modules/cjs/loader.js:507:25)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at Object.<anonymous> (C:\REDACTED\node_modules\@typescript-eslint\eslint-plugin\lib\rules\no-unnecessary-type-assertion.js:7:17)

Had to add tsutils to package.json. Is that expected ?

Maybe a slight addition to the README would be helpful regarding this. Or even better, add tsutils as a dependency of that plugin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nulltoken My mistake. I'm not sure how I missed adding it to the package.json and it still managed to pass the tests...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I get it; tslint is being installed as a dev dependency, and that happens to pull in tsutils so it works in a dev scenario. I'll get a fix up right now.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

const ts = require('typescript');
const util = require('../util');

/** @typedef {import("estree").Node} Node */
/** @typedef {import("eslint").Rule.RuleContext} Context */

//------------------------------------------------------------------------------
// 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: {
docs: {
description:
'Warns if a type assertion does not change the type of an expression',
category: 'TypeScript-specific',
recommended: false,
extraDescription: [util.tslintRule('no-unnecessary-type-assertion')],
url: util.metaDocsUrl('no-unnecessary-type-assertion')
},
fixable: 'code',
messages: {
unnecessaryAssertion:
'This assertion is unnecessary since it does not change the type of the expression.'
},
schema: [
{
type: 'object',
properties: {
typesToIgnore: {
type: 'array',
items: {
type: 'string'
}
}
}
}
],
type: 'suggestion'
},

create(context) {
const checker = util.getParserServices(context).program.getTypeChecker();

return {
TSNonNullExpression(node) {
checkNonNullAssertion(node, context, checker);
},
TSTypeAssertion(node) {
verifyCast(node, context, checker);
},
TSAsExpression(node) {
verifyCast(node, context, checker);
}
};
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @fileoverview Warns if a type assertion does not change the type of an expression.
* @author Benjamin Lichtman
*/
'use strict';

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require('../../../lib/rules/no-unnecessary-type-assertion'),
RuleTester = require('eslint').RuleTester,
path = require('path');

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const rootDir = path.join(process.cwd(), 'tests/fixtures');
const parserOptions = {
ecmaVersion: 2015,
tsconfigRootDir: rootDir,
project: './tsconfig.json'
};
const ruleTester = new RuleTester({
parserOptions,
parser: '@typescript-eslint/parser'
});

ruleTester.run('no-unnecessary-type-assertion', rule, {
valid: [
'const foo = 3 as number;',
'const foo = <number> 3;',
'const foo = <3>3;',
'const foo = 3 as 3;',
`
type Tuple = [3, "hi", "bye"];
const foo = ([3, "hi", "bye"]) as Tuple;`,
`
type PossibleTuple = {};
const foo = ({}) as PossibleTuple;`,
`
type PossibleTuple = { hello: "hello" };
const foo = ({ hello: "hello" }) as PossibleTuple;`,
`
type PossibleTuple = { 0: "hello", 5: "hello" };
const foo = ({ 0: "hello", 5: "hello" }) as PossibleTuple;`,
{
code: `
type Foo = number;
const foo = (3 + 5) as Foo;`,
options: [{ typesToIgnore: ['Foo'] }]
},
{
code: `
type Foo = number;
const foo = <Foo>(3 + 5);`,
options: [{ typesToIgnore: ['Foo'] }]
}
],

invalid: [
{
code: `
const foo = 3;
const bar = foo!;`,
errors: [
{
messageId: 'unnecessaryAssertion',
line: 3,
column: 13
}
]
},
{
code: `
const foo = (3 + 5) as number;`,
errors: [
{
messageId: 'unnecessaryAssertion',
line: 2,
column: 13
}
]
},
{
code: `
const foo = <number>(3 + 5);`,
errors: [
{
messageId: 'unnecessaryAssertion',
line: 2,
column: 13
}
]
},
{
code: `
type Foo = number;
const foo = (3 + 5) as Foo;`,
errors: [
{
messageId: 'unnecessaryAssertion',
line: 3,
column: 13
}
]
},
{
code: `
type Foo = number;
const foo = <Foo>(3 + 5);`,
errors: [
{
messageId: 'unnecessaryAssertion',
line: 3,
column: 13
}
]
}
]
});