From 096201765e175853935c8d72310435a84669db75 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 24 Apr 2019 05:38:41 -0700 Subject: [PATCH] feat(eslint-plugin): Add semi [extension] (#461) --- packages/eslint-plugin/README.md | 1 + packages/eslint-plugin/docs/semi.md | 25 ++++ packages/eslint-plugin/src/rules/semi.ts | 66 ++++++++++ .../eslint-plugin/tests/rules/semi.test.ts | 119 ++++++++++++++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 31 +++++ packages/eslint-plugin/typings/ts-eslint.d.ts | 1 + 6 files changed, 243 insertions(+) create mode 100644 packages/eslint-plugin/docs/semi.md create mode 100644 packages/eslint-plugin/src/rules/semi.ts create mode 100644 packages/eslint-plugin/tests/rules/semi.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 00d11b52834..2d10621b7a6 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -153,6 +153,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules. (`no-internal-module` from TSLint) | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | | | :thought_balloon: | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string. (`restrict-plus-operands` from TSLint) | | | :thought_balloon: | +| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | | [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/unbound-method`](./docs/rules/unbound-method.md) | Enforces unbound methods are called with their expected scope. (`no-unbound-method` from TSLint) | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one. (`unified-signatures` from TSLint) | | | | diff --git a/packages/eslint-plugin/docs/semi.md b/packages/eslint-plugin/docs/semi.md new file mode 100644 index 00000000000..69fd5c57be8 --- /dev/null +++ b/packages/eslint-plugin/docs/semi.md @@ -0,0 +1,25 @@ +# require or disallow semicolons instead of ASI (semi) + +This rule enforces consistent use of semicolons. + +## Rule Details + +This rule extends the base [eslint/semi](https://eslint.org/docs/rules/semi) rule. +It supports all options and features of the base rule. +This version adds support for numerous typescript features. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "semi": "off", + "@typescript-eslint/semi": ["error"] +} +``` + +## Options + +See [eslint/semi options](https://eslint.org/docs/rules/semi#options). + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/semi.md) diff --git a/packages/eslint-plugin/src/rules/semi.ts b/packages/eslint-plugin/src/rules/semi.ts new file mode 100644 index 00000000000..89fe6180333 --- /dev/null +++ b/packages/eslint-plugin/src/rules/semi.ts @@ -0,0 +1,66 @@ +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'; +import baseRule from 'eslint/lib/rules/semi'; +import { RuleListener, RuleFunction } from 'ts-eslint'; +import * as util from '../util'; + +export type Options = util.InferOptionsTypeFromRule; +export type MessageIds = util.InferMessageIdsTypeFromRule; + +export default util.createRule({ + name: 'semi', + meta: { + type: 'layout', + docs: { + description: 'Require or disallow semicolons instead of ASI', + category: 'Stylistic Issues', + recommended: false, + }, + fixable: 'code', + schema: baseRule.meta.schema, + messages: baseRule.meta.messages, + }, + defaultOptions: [ + 'always', + { + omitLastInOneLineBlock: false, + beforeStatementContinuationChars: 'any', + }, + ], + create(context) { + const rules = baseRule.create(context); + const checkForSemicolon = rules.ExpressionStatement as RuleFunction< + TSESTree.Node + >; + + /* + The following nodes are handled by the member-delimiter-style rule + AST_NODE_TYPES.TSCallSignatureDeclaration, + AST_NODE_TYPES.TSConstructSignatureDeclaration, + AST_NODE_TYPES.TSIndexSignature, + AST_NODE_TYPES.TSMethodSignature, + AST_NODE_TYPES.TSPropertySignature, + */ + const nodesToCheck = [ + AST_NODE_TYPES.ClassProperty, + AST_NODE_TYPES.TSAbstractClassProperty, + AST_NODE_TYPES.TSAbstractMethodDefinition, + AST_NODE_TYPES.TSDeclareFunction, + AST_NODE_TYPES.TSExportAssignment, + AST_NODE_TYPES.TSImportEqualsDeclaration, + AST_NODE_TYPES.TSTypeAliasDeclaration, + ].reduce((acc, node) => { + acc[node] = checkForSemicolon; + return acc; + }, {}); + + return { + ...rules, + ...nodesToCheck, + ExportDefaultDeclaration(node) { + if (node.declaration.type !== AST_NODE_TYPES.TSInterfaceDeclaration) { + rules.ExportDefaultDeclaration(node); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/semi.test.ts b/packages/eslint-plugin/tests/rules/semi.test.ts new file mode 100644 index 00000000000..65ee4991733 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/semi.test.ts @@ -0,0 +1,119 @@ +import rule, { MessageIds, Options } from '../../src/rules/semi'; +import { InvalidTestCase, RuleTester, ValidTestCase } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('semi', rule, { + valid: [ + ` +class Class { + prop: string; +} + `, + ` +abstract class AbsClass { + abstract prop: string; + abstract meth(): string; +} + `, + `declare function declareFn(): string;`, + `export default interface Foo {}`, + 'export = Foo;', + 'import f = require("f");', + 'type Foo = {};', + ].reduce[]>((acc, code) => { + acc.push({ + code, + options: ['always'], + }); + acc.push({ + code: code.replace(/;/g, ''), + options: ['never'], + }); + + return acc; + }, []), + invalid: [ + { + code: ` +class Class { + prop: string; +} + `, + errors: [ + { + line: 3, + }, + ], + }, + { + code: ` +abstract class AbsClass { + abstract prop: string; + abstract meth(): string; +} + `, + errors: [ + { + line: 3, + }, + { + line: 4, + }, + ], + }, + { + code: `declare function declareFn(): string;`, + errors: [ + { + line: 1, + }, + ], + }, + { + code: 'export = Foo;', + errors: [ + { + line: 1, + }, + ], + }, + { + code: 'import f = require("f");', + errors: [ + { + line: 1, + }, + ], + }, + { + code: 'type Foo = {};', + errors: [ + { + line: 1, + }, + ], + }, + ].reduce[]>((acc, test) => { + acc.push({ + code: test.code.replace(/;/g, ''), + options: ['always'], + errors: test.errors.map(e => ({ + ...e, + message: 'Missing semicolon.', + })) as any, + }); + acc.push({ + code: test.code, + options: ['never'], + errors: test.errors.map(e => ({ + ...e, + message: 'Extra semicolon.', + })) as any, + }); + + return acc; + }, []), +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index aca4c8d8638..aac61ca5848 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -353,3 +353,34 @@ declare module 'eslint/lib/rules/no-extra-parens' { >; export = rule; } + +declare module 'eslint/lib/rules/semi' { + import { TSESTree } from '@typescript-eslint/typescript-estree'; + import RuleModule from 'ts-eslint'; + + const rule: RuleModule< + never, + [ + 'always' | 'never', + { + beforeStatementContinuationChars?: 'always' | 'any' | 'never'; + omitLastInOneLineBlock?: boolean; + }? + ], + { + VariableDeclaration(node: TSESTree.VariableDeclaration): void; + ExpressionStatement(node: TSESTree.ExpressionStatement): void; + ReturnStatement(node: TSESTree.ReturnStatement): void; + ThrowStatement(node: TSESTree.ThrowStatement): void; + DoWhileStatement(node: TSESTree.DoWhileStatement): void; + DebuggerStatement(node: TSESTree.DebuggerStatement): void; + BreakStatement(node: TSESTree.BreakStatement): void; + ContinueStatement(node: TSESTree.ContinueStatement): void; + ImportDeclaration(node: TSESTree.ImportDeclaration): void; + ExportAllDeclaration(node: TSESTree.ExportAllDeclaration): void; + ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration): void; + ExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration): void; + } + >; + export = rule; +} diff --git a/packages/eslint-plugin/typings/ts-eslint.d.ts b/packages/eslint-plugin/typings/ts-eslint.d.ts index ce6e6c577c2..3053c988d41 100644 --- a/packages/eslint-plugin/typings/ts-eslint.d.ts +++ b/packages/eslint-plugin/typings/ts-eslint.d.ts @@ -419,6 +419,7 @@ declare module 'ts-eslint' { ClassBody?: RuleFunction; ClassDeclaration?: RuleFunction; ClassExpression?: RuleFunction; + ClassProperty?: RuleFunction; Comment?: RuleFunction; ConditionalExpression?: RuleFunction; ContinueStatement?: RuleFunction;