Skip to content

Commit 0962017

Browse files
bradzacherJamesHenry
authored andcommitted
feat(eslint-plugin): Add semi [extension] (#461)
1 parent 0205e3e commit 0962017

File tree

6 files changed

+243
-0
lines changed

6 files changed

+243
-0
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
153153
| [`@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: | |
154154
| [`@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: |
155155
| [`@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: |
156+
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
156157
| [`@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: | |
157158
| [`@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: |
158159
| [`@typescript-eslint/unified-signatures`](./docs/rules/unified-signatures.md) | Warns for any two overloads that could be unified into one. (`unified-signatures` from TSLint) | | | |

packages/eslint-plugin/docs/semi.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# require or disallow semicolons instead of ASI (semi)
2+
3+
This rule enforces consistent use of semicolons.
4+
5+
## Rule Details
6+
7+
This rule extends the base [eslint/semi](https://eslint.org/docs/rules/semi) rule.
8+
It supports all options and features of the base rule.
9+
This version adds support for numerous typescript features.
10+
11+
## How to use
12+
13+
```cjson
14+
{
15+
// note you must disable the base rule as it can report incorrect errors
16+
"semi": "off",
17+
"@typescript-eslint/semi": ["error"]
18+
}
19+
```
20+
21+
## Options
22+
23+
See [eslint/semi options](https://eslint.org/docs/rules/semi#options).
24+
25+
<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/semi.md)</sup>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
2+
import baseRule from 'eslint/lib/rules/semi';
3+
import { RuleListener, RuleFunction } from 'ts-eslint';
4+
import * as util from '../util';
5+
6+
export type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
7+
export type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;
8+
9+
export default util.createRule<Options, MessageIds>({
10+
name: 'semi',
11+
meta: {
12+
type: 'layout',
13+
docs: {
14+
description: 'Require or disallow semicolons instead of ASI',
15+
category: 'Stylistic Issues',
16+
recommended: false,
17+
},
18+
fixable: 'code',
19+
schema: baseRule.meta.schema,
20+
messages: baseRule.meta.messages,
21+
},
22+
defaultOptions: [
23+
'always',
24+
{
25+
omitLastInOneLineBlock: false,
26+
beforeStatementContinuationChars: 'any',
27+
},
28+
],
29+
create(context) {
30+
const rules = baseRule.create(context);
31+
const checkForSemicolon = rules.ExpressionStatement as RuleFunction<
32+
TSESTree.Node
33+
>;
34+
35+
/*
36+
The following nodes are handled by the member-delimiter-style rule
37+
AST_NODE_TYPES.TSCallSignatureDeclaration,
38+
AST_NODE_TYPES.TSConstructSignatureDeclaration,
39+
AST_NODE_TYPES.TSIndexSignature,
40+
AST_NODE_TYPES.TSMethodSignature,
41+
AST_NODE_TYPES.TSPropertySignature,
42+
*/
43+
const nodesToCheck = [
44+
AST_NODE_TYPES.ClassProperty,
45+
AST_NODE_TYPES.TSAbstractClassProperty,
46+
AST_NODE_TYPES.TSAbstractMethodDefinition,
47+
AST_NODE_TYPES.TSDeclareFunction,
48+
AST_NODE_TYPES.TSExportAssignment,
49+
AST_NODE_TYPES.TSImportEqualsDeclaration,
50+
AST_NODE_TYPES.TSTypeAliasDeclaration,
51+
].reduce<RuleListener>((acc, node) => {
52+
acc[node] = checkForSemicolon;
53+
return acc;
54+
}, {});
55+
56+
return {
57+
...rules,
58+
...nodesToCheck,
59+
ExportDefaultDeclaration(node) {
60+
if (node.declaration.type !== AST_NODE_TYPES.TSInterfaceDeclaration) {
61+
rules.ExportDefaultDeclaration(node);
62+
}
63+
},
64+
};
65+
},
66+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import rule, { MessageIds, Options } from '../../src/rules/semi';
2+
import { InvalidTestCase, RuleTester, ValidTestCase } from '../RuleTester';
3+
4+
const ruleTester = new RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
});
7+
8+
ruleTester.run('semi', rule, {
9+
valid: [
10+
`
11+
class Class {
12+
prop: string;
13+
}
14+
`,
15+
`
16+
abstract class AbsClass {
17+
abstract prop: string;
18+
abstract meth(): string;
19+
}
20+
`,
21+
`declare function declareFn(): string;`,
22+
`export default interface Foo {}`,
23+
'export = Foo;',
24+
'import f = require("f");',
25+
'type Foo = {};',
26+
].reduce<ValidTestCase<Options>[]>((acc, code) => {
27+
acc.push({
28+
code,
29+
options: ['always'],
30+
});
31+
acc.push({
32+
code: code.replace(/;/g, ''),
33+
options: ['never'],
34+
});
35+
36+
return acc;
37+
}, []),
38+
invalid: [
39+
{
40+
code: `
41+
class Class {
42+
prop: string;
43+
}
44+
`,
45+
errors: [
46+
{
47+
line: 3,
48+
},
49+
],
50+
},
51+
{
52+
code: `
53+
abstract class AbsClass {
54+
abstract prop: string;
55+
abstract meth(): string;
56+
}
57+
`,
58+
errors: [
59+
{
60+
line: 3,
61+
},
62+
{
63+
line: 4,
64+
},
65+
],
66+
},
67+
{
68+
code: `declare function declareFn(): string;`,
69+
errors: [
70+
{
71+
line: 1,
72+
},
73+
],
74+
},
75+
{
76+
code: 'export = Foo;',
77+
errors: [
78+
{
79+
line: 1,
80+
},
81+
],
82+
},
83+
{
84+
code: 'import f = require("f");',
85+
errors: [
86+
{
87+
line: 1,
88+
},
89+
],
90+
},
91+
{
92+
code: 'type Foo = {};',
93+
errors: [
94+
{
95+
line: 1,
96+
},
97+
],
98+
},
99+
].reduce<InvalidTestCase<MessageIds, Options>[]>((acc, test) => {
100+
acc.push({
101+
code: test.code.replace(/;/g, ''),
102+
options: ['always'],
103+
errors: test.errors.map(e => ({
104+
...e,
105+
message: 'Missing semicolon.',
106+
})) as any,
107+
});
108+
acc.push({
109+
code: test.code,
110+
options: ['never'],
111+
errors: test.errors.map(e => ({
112+
...e,
113+
message: 'Extra semicolon.',
114+
})) as any,
115+
});
116+
117+
return acc;
118+
}, []),
119+
});

packages/eslint-plugin/typings/eslint-rules.d.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,34 @@ declare module 'eslint/lib/rules/no-extra-parens' {
353353
>;
354354
export = rule;
355355
}
356+
357+
declare module 'eslint/lib/rules/semi' {
358+
import { TSESTree } from '@typescript-eslint/typescript-estree';
359+
import RuleModule from 'ts-eslint';
360+
361+
const rule: RuleModule<
362+
never,
363+
[
364+
'always' | 'never',
365+
{
366+
beforeStatementContinuationChars?: 'always' | 'any' | 'never';
367+
omitLastInOneLineBlock?: boolean;
368+
}?
369+
],
370+
{
371+
VariableDeclaration(node: TSESTree.VariableDeclaration): void;
372+
ExpressionStatement(node: TSESTree.ExpressionStatement): void;
373+
ReturnStatement(node: TSESTree.ReturnStatement): void;
374+
ThrowStatement(node: TSESTree.ThrowStatement): void;
375+
DoWhileStatement(node: TSESTree.DoWhileStatement): void;
376+
DebuggerStatement(node: TSESTree.DebuggerStatement): void;
377+
BreakStatement(node: TSESTree.BreakStatement): void;
378+
ContinueStatement(node: TSESTree.ContinueStatement): void;
379+
ImportDeclaration(node: TSESTree.ImportDeclaration): void;
380+
ExportAllDeclaration(node: TSESTree.ExportAllDeclaration): void;
381+
ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration): void;
382+
ExportDefaultDeclaration(node: TSESTree.ExportDefaultDeclaration): void;
383+
}
384+
>;
385+
export = rule;
386+
}

packages/eslint-plugin/typings/ts-eslint.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ declare module 'ts-eslint' {
419419
ClassBody?: RuleFunction<TSESTree.ClassBody>;
420420
ClassDeclaration?: RuleFunction<TSESTree.ClassDeclaration>;
421421
ClassExpression?: RuleFunction<TSESTree.ClassExpression>;
422+
ClassProperty?: RuleFunction<TSESTree.ClassProperty>;
422423
Comment?: RuleFunction<TSESTree.Comment>;
423424
ConditionalExpression?: RuleFunction<TSESTree.ConditionalExpression>;
424425
ContinueStatement?: RuleFunction<TSESTree.ContinueStatement>;

0 commit comments

Comments
 (0)