Skip to content
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

feat(eslint-plugin): add new rule prefer-as-const #1431

Merged
merged 12 commits into from Jan 21, 2020
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 @@ -55,6 +55,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 @@ -138,6 +139,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