Skip to content

Commit

Permalink
feat(eslint-plugin): add new rule prefer-as-const (#1431)
Browse files Browse the repository at this point in the history
  • Loading branch information
armano2 authored and bradzacher committed Jan 21, 2020
1 parent 7fabd97 commit 420db96
Show file tree
Hide file tree
Showing 14 changed files with 342 additions and 75 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Expand Up @@ -33,6 +33,7 @@ module.exports = {
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/prefer-as-const': 'error',

'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': [
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -133,6 +133,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-unnecessary-type-assertion`](./docs/rules/no-unnecessary-type-assertion.md) | Warns if a type assertion does not change the type of an expression | :heavy_check_mark: | :wrench: | :thought_balloon: |
| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | :thought_balloon: |
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | |
| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | | :wrench: | |
| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | |
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | |
| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | :heavy_check_mark: | :wrench: | :thought_balloon: |
Expand Down
28 changes: 28 additions & 0 deletions packages/eslint-plugin/docs/rules/prefer-as-const.md
@@ -0,0 +1,28 @@
# Prefer usage of `as const` over literal type (`prefer-as-const`)

This rule recommends usage of `const` assertion when type primitive value is equal to type.

## Rule Details

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

```ts
let bar: 2 = 2;
let foo = <'bar'>'bar';
let foo = { bar: 'baz' as 'baz' };
```

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

```ts
let foo = 'bar';
let foo = 'bar' as const;
let foo: 'bar' = 'bar' as const;
let bar = 'bar' as string;
let foo = <string>'bar';
let foo = { bar: 'baz' };
```

## When Not To Use It

If you are using typescript < 3.4
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -65,6 +65,7 @@
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-includes": "error",
Expand Down
4 changes: 2 additions & 2 deletions packages/eslint-plugin/src/rules/indent.ts
Expand Up @@ -260,7 +260,7 @@ export default util.createRule<Options, MessageIds>({

return rules.VariableDeclaration({
type: AST_NODE_TYPES.VariableDeclaration,
kind: 'const' as 'const',
kind: 'const' as const,
declarations: [
{
type: AST_NODE_TYPES.VariableDeclarator,
Expand Down Expand Up @@ -389,7 +389,7 @@ export default util.createRule<Options, MessageIds>({
? node.typeAnnotation.loc.end
: squareBracketStart.loc.end,
},
kind: 'init' as 'init',
kind: 'init' as const,
computed: false,
method: false,
shorthand: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -56,6 +56,7 @@ import noUnusedVarsExperimental from './no-unused-vars-experimental';
import noUseBeforeDefine from './no-use-before-define';
import noUselessConstructor from './no-useless-constructor';
import noVarRequires from './no-var-requires';
import preferAsConst from './prefer-as-const';
import preferForOf from './prefer-for-of';
import preferFunctionType from './prefer-function-type';
import preferIncludes from './prefer-includes';
Expand Down Expand Up @@ -140,6 +141,7 @@ export default {
'no-use-before-define': noUseBeforeDefine,
'no-useless-constructor': noUselessConstructor,
'no-var-requires': noVarRequires,
'prefer-as-const': preferAsConst,
'prefer-for-of': preferForOf,
'prefer-function-type': preferFunctionType,
'prefer-includes': preferIncludes,
Expand Down
78 changes: 78 additions & 0 deletions packages/eslint-plugin/src/rules/prefer-as-const.ts
@@ -0,0 +1,78 @@
import {
AST_NODE_TYPES,
TSESLint,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import * as util from '../util';

export default util.createRule({
name: 'prefer-as-const',
meta: {
type: 'suggestion',
docs: {
description: 'Prefer usage of `as const` over literal type',
category: 'Best Practices',
recommended: false,
},
fixable: 'code',
messages: {
preferConstAssertion:
'Expected a `const` instead of a literal type assertion',
variableConstAssertion:
'Expected a `const` assertion instead of a literal type annotation',
variableSuggest: 'You should use `as const` instead of type annotation.',
},
schema: [],
},
defaultOptions: [],
create(context) {
function compareTypes(
valueNode: TSESTree.Expression,
typeNode: TSESTree.TypeNode,
canFix: boolean,
): void {
if (
valueNode.type === AST_NODE_TYPES.Literal &&
typeNode.type === AST_NODE_TYPES.TSLiteralType &&
'raw' in typeNode.literal &&
valueNode.raw === typeNode.literal.raw
) {
if (canFix) {
context.report({
node: typeNode,
messageId: 'preferConstAssertion',
fix: fixer => fixer.replaceText(typeNode, 'const'),
});
} else {
context.report({
node: typeNode,
messageId: 'variableConstAssertion',
suggest: [
{
messageId: 'variableSuggest',
fix: (fixer): TSESLint.RuleFix[] => [
fixer.remove(typeNode.parent!),
fixer.insertTextAfter(valueNode, ' as const'),
],
},
],
});
}
}
}

return {
TSAsExpression(node): void {
compareTypes(node.expression, node.typeAnnotation, true);
},
TSTypeAssertion(node): void {
compareTypes(node.expression, node.typeAnnotation, true);
},
VariableDeclarator(node): void {
if (node.init && node.id.typeAnnotation) {
compareTypes(node.init, node.id.typeAnnotation.typeAnnotation, false);
}
},
};
},
});
Expand Up @@ -6,7 +6,7 @@ const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

const messageId = 'useLiteral' as 'useLiteral';
const messageId = 'useLiteral' as const;

ruleTester.run('no-array-constructor', rule, {
valid: [
Expand Down
Expand Up @@ -3,13 +3,13 @@ import rule from '../../src/rules/no-extraneous-class';
import { RuleTester } from '../RuleTester';

const empty = {
messageId: 'empty' as 'empty',
messageId: 'empty' as const,
};
const onlyStatic = {
messageId: 'onlyStatic' as 'onlyStatic',
messageId: 'onlyStatic' as const,
};
const onlyConstructor = {
messageId: 'onlyConstructor' as 'onlyConstructor',
messageId: 'onlyConstructor' as const,
};

const ruleTester = new RuleTester({
Expand Down
6 changes: 3 additions & 3 deletions packages/eslint-plugin/tests/rules/no-this-alias.test.ts
Expand Up @@ -3,15 +3,15 @@ import rule from '../../src/rules/no-this-alias';
import { RuleTester } from '../RuleTester';

const idError = {
messageId: 'thisAssignment' as 'thisAssignment',
messageId: 'thisAssignment' as const,
type: AST_NODE_TYPES.Identifier,
};
const destructureError = {
messageId: 'thisDestructure' as 'thisDestructure',
messageId: 'thisDestructure' as const,
type: AST_NODE_TYPES.ObjectPattern,
};
const arrayDestructureError = {
messageId: 'thisDestructure' as 'thisDestructure',
messageId: 'thisDestructure' as const,
type: AST_NODE_TYPES.ArrayPattern,
};

Expand Down
Expand Up @@ -6,7 +6,7 @@ const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

const parserOptions = { ecmaVersion: 6 as 6 };
const parserOptions = { ecmaVersion: 6 as const };

ruleTester.run('no-use-before-define', rule, {
valid: [
Expand Down
156 changes: 156 additions & 0 deletions packages/eslint-plugin/tests/rules/prefer-as-const.test.ts
@@ -0,0 +1,156 @@
import rule from '../../src/rules/prefer-as-const';
import { RuleTester } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('prefer-as-const', rule, {
valid: [
"let foo = 'baz' as const",
'let foo = 1 as const',
"let foo = { bar: 'baz' as const }",
'let foo = { bar: 1 as const }',
"let foo = { bar: 'baz' }",
'let foo = { bar: 2 }',
"let foo = <bar>'bar';",
"let foo = <string>'bar';",
"let foo = 'bar' as string;",
'let foo = `bar` as `bar`;',
'let foo = `bar` as `foo`;',
"let foo = `bar` as 'bar';",
"let foo: string = 'bar';",
'let foo: number = 1;',
"let foo: 'bar' = baz;",
"let foo = 'bar';",
'class foo { bar: "baz" = "baz" }',
'class foo { bar = "baz" }',
"let foo: 'bar'",
'let foo = { bar }',
"let foo: 'baz' = 'baz' as const",
],
invalid: [
{
code: "let foo = { bar: 'baz' as 'baz' }",
output: "let foo = { bar: 'baz' as const }",
errors: [
{
messageId: 'preferConstAssertion',
line: 1,
column: 27,
},
],
},
{
code: 'let foo = { bar: 1 as 1 }',
output: 'let foo = { bar: 1 as const }',
errors: [
{
messageId: 'preferConstAssertion',
line: 1,
column: 23,
},
],
},
{
code: "let []: 'bar' = 'bar';",
output: "let []: 'bar' = 'bar';",
errors: [
{
messageId: 'variableConstAssertion',
line: 1,
column: 9,
},
],
},
{
code: "let foo: 'bar' = 'bar';",
output: "let foo: 'bar' = 'bar';",
errors: [
{
messageId: 'variableConstAssertion',
line: 1,
column: 10,
suggestions: [
{
messageId: 'variableSuggest',
output: "let foo = 'bar' as const;",
},
],
},
],
},
{
code: 'let foo: 2 = 2;',
output: 'let foo: 2 = 2;',
errors: [
{
messageId: 'variableConstAssertion',
line: 1,
column: 10,
suggestions: [
{
messageId: 'variableSuggest',
output: 'let foo = 2 as const;',
},
],
},
],
},
{
code: "let foo: 'bar' = 'bar' as 'bar';",
output: "let foo: 'bar' = 'bar' as const;",
errors: [
{
messageId: 'preferConstAssertion',
line: 1,
column: 27,
},
],
},
{
code: "let foo = <'bar'>'bar';",
output: "let foo = <const>'bar';",
errors: [
{
messageId: 'preferConstAssertion',
line: 1,
column: 12,
},
],
},
{
code: 'let foo = <4>4;',
output: 'let foo = <const>4;',
errors: [
{
messageId: 'preferConstAssertion',
line: 1,
column: 12,
},
],
},
{
code: "let foo = 'bar' as 'bar';",
output: "let foo = 'bar' as const;",
errors: [
{
messageId: 'preferConstAssertion',
line: 1,
column: 20,
},
],
},
{
code: 'let foo = 5 as 5;',
output: 'let foo = 5 as const;',
errors: [
{
messageId: 'preferConstAssertion',
line: 1,
column: 16,
},
],
},
],
});
2 changes: 1 addition & 1 deletion packages/parser/tests/lib/parser.ts
Expand Up @@ -43,7 +43,7 @@ describe('parser', () => {
comment: false,
range: false,
tokens: false,
sourceType: 'module' as 'module',
sourceType: 'module' as const,
ecmaVersion: 2018,
ecmaFeatures: {
globalReturn: false,
Expand Down

0 comments on commit 420db96

Please sign in to comment.