Skip to content

Commit

Permalink
feat(eslint-plugin): Add semi [extension] (#461)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher authored and JamesHenry committed Apr 24, 2019
1 parent 0205e3e commit 0962017
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -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) | | | |
Expand Down
25 changes: 25 additions & 0 deletions 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).

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/semi.md)</sup>
66 changes: 66 additions & 0 deletions 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<typeof baseRule>;
export type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;

export default util.createRule<Options, MessageIds>({
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<RuleListener>((acc, node) => {
acc[node] = checkForSemicolon;
return acc;
}, {});

return {
...rules,
...nodesToCheck,
ExportDefaultDeclaration(node) {
if (node.declaration.type !== AST_NODE_TYPES.TSInterfaceDeclaration) {
rules.ExportDefaultDeclaration(node);
}
},
};
},
});
119 changes: 119 additions & 0 deletions 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<ValidTestCase<Options>[]>((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<InvalidTestCase<MessageIds, Options>[]>((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;
}, []),
});
31 changes: 31 additions & 0 deletions packages/eslint-plugin/typings/eslint-rules.d.ts
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions packages/eslint-plugin/typings/ts-eslint.d.ts
Expand Up @@ -419,6 +419,7 @@ declare module 'ts-eslint' {
ClassBody?: RuleFunction<TSESTree.ClassBody>;
ClassDeclaration?: RuleFunction<TSESTree.ClassDeclaration>;
ClassExpression?: RuleFunction<TSESTree.ClassExpression>;
ClassProperty?: RuleFunction<TSESTree.ClassProperty>;
Comment?: RuleFunction<TSESTree.Comment>;
ConditionalExpression?: RuleFunction<TSESTree.ConditionalExpression>;
ContinueStatement?: RuleFunction<TSESTree.ContinueStatement>;
Expand Down

0 comments on commit 0962017

Please sign in to comment.