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 extension rule comma-dangle #2416

Merged
merged 9 commits into from Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -188,6 +188,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------ | -------- | ----------------- |
| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | |
| [`@typescript-eslint/comma-dangle`](./docs/rules/comma-dangle.md) | Require or disallow trailing comma | | :wrench: | |
| [`@typescript-eslint/comma-spacing`](./docs/rules/comma-spacing.md) | Enforces consistent spacing before and after commas | | :wrench: | |
| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | |
| [`@typescript-eslint/dot-notation`](./docs/rules/dot-notation.md) | enforce dot notation whenever possible | | :wrench: | :thought_balloon: |
Expand Down
34 changes: 34 additions & 0 deletions packages/eslint-plugin/docs/rules/comma-dangle.md
@@ -0,0 +1,34 @@
# Require or disallow trailing comma (`comma-dangle`)

## Rule Details

This rule extends the base [`eslint/comma-dangle`](https://eslint.org/docs/rules/comma-dangle) rule.
It adds support for TypeScript syntax.

See the [ESLint documentation](https://eslint.org/docs/rules/comma-dangle) for more details on the `comma-dangle` rule.

## Rule Changes

```cjson
{
// note you must disable the base rule as it can report incorrect errors
"comma-dangle": "off",
"@typescript-eslint/comma-dangle": ["error"]
}
```

In addition to the options supported by the `comma-dangle` rule in ESLint core, the rule adds the following options:

## Options

This rule has a string option and an object option.

- Object option:

- `"enums"` is for trailing comma in enum. (e.g. `enum Foo = {Bar,}`)
- `"generics"` is for trailing comma in generic. (e.g. `function foo<T,>() {}`)
- `"tuples"` is for trailing comma in tuple. (e.g. `type Foo = [string,]`)

- [See the other options allowed](https://github.com/eslint/eslint/blob/master/docs/rules/comma-dangle.md#options)

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/comma-dangle.md)</sup>
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -139,5 +139,7 @@ export = {
'@typescript-eslint/typedef': 'error',
'@typescript-eslint/unbound-method': 'error',
'@typescript-eslint/unified-signatures': 'error',
'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': 'error',
},
};
179 changes: 179 additions & 0 deletions packages/eslint-plugin/src/rules/comma-dangle.ts
@@ -0,0 +1,179 @@
import * as util from '../util';
import baseRule from 'eslint/lib/rules/comma-dangle';
import {
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/experimental-utils';

export type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
export type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;

type Option = Options[0];
type NormalizedOptions = Required<
Pick<Exclude<Option, string>, 'enums' | 'generics' | 'tuples'>
>;

const OPTION_VALUE_SCHEME = [
'always-multiline',
'always',
'never',
'only-multiline',
];

const DEFAULT_OPTION_VALUE = 'never';

function normalizeOptions(options: Option): NormalizedOptions {
if (typeof options === 'string') {
return {
enums: options,
generics: options,
tuples: options,
};
}
return {
enums: options.enums ?? DEFAULT_OPTION_VALUE,
generics: options.generics ?? DEFAULT_OPTION_VALUE,
tuples: options.tuples ?? DEFAULT_OPTION_VALUE,
};
}

export default util.createRule<Options, MessageIds>({
name: 'comma-dangle',
meta: {
type: 'layout',
docs: {
description: 'Require or disallow trailing comma',
category: 'Stylistic Issues',
recommended: false,
extendsBaseRule: true,
},
schema: {
definitions: {
value: {
enum: OPTION_VALUE_SCHEME,
},
valueWithIgnore: {
enum: [...OPTION_VALUE_SCHEME, 'ignore'],
},
},
type: 'array',
items: [
{
oneOf: [
{
$ref: '#/definitions/value',
},
{
type: 'object',
properties: {
arrays: { $ref: '#/definitions/valueWithIgnore' },
objects: { $ref: '#/definitions/valueWithIgnore' },
imports: { $ref: '#/definitions/valueWithIgnore' },
exports: { $ref: '#/definitions/valueWithIgnore' },
functions: { $ref: '#/definitions/valueWithIgnore' },
enums: { $ref: '#/definitions/valueWithIgnore' },
generics: { $ref: '#/definitions/valueWithIgnore' },
tuples: { $ref: '#/definitions/valueWithIgnore' },
},
additionalProperties: false,
},
],
},
],
},
fixable: 'code',
messages: baseRule.meta.messages,
},
defaultOptions: ['never'],
create(context, [options]) {
const rules = baseRule.create(context);
const sourceCode = context.getSourceCode();
const normalizedOptions = normalizeOptions(options);

const predicate = {
always: forceComma,
'always-multiline': forceCommaIfMultiline,
'only-multiline': allowCommaIfMultiline,
never: forbidComma,
ignore: (): void => {},
};

function last(nodes: TSESTree.Node[]): TSESTree.Node | null {
return nodes[nodes.length - 1] ?? null;
}

function getLastItem(node: TSESTree.Node): TSESTree.Node | null {
switch (node.type) {
case AST_NODE_TYPES.TSEnumDeclaration:
return last(node.members);
case AST_NODE_TYPES.TSTypeParameterDeclaration:
return last(node.params);
case AST_NODE_TYPES.TSTupleType:
return last(node.elementTypes);
default:
return null;
}
}

function getTrailingToken(node: TSESTree.Node): TSESTree.Token | null {
const last = getLastItem(node);
const trailing = last && sourceCode.getTokenAfter(last);
return trailing;
}

function isMultiline(node: TSESTree.Node): boolean {
const last = getLastItem(node);
const lastToken = sourceCode.getLastToken(node);
return last?.loc.end.line !== lastToken?.loc.end.line;
}

function forbidComma(node: TSESTree.Node): void {
const last = getLastItem(node);
const trailing = getTrailingToken(node);
if (last && trailing && util.isCommaToken(trailing)) {
context.report({
node,
messageId: 'unexpected',
fix(fixer) {
return fixer.remove(trailing);
},
});
}
}

function forceComma(node: TSESTree.Node): void {
const last = getLastItem(node);
const trailing = getTrailingToken(node);
if (last && trailing && !util.isCommaToken(trailing)) {
context.report({
node,
messageId: 'missing',
fix(fixer) {
return fixer.insertTextAfter(last, ',');
},
});
}
}

function allowCommaIfMultiline(node: TSESTree.Node): void {
if (!isMultiline(node)) {
forbidComma(node);
}
}

function forceCommaIfMultiline(node: TSESTree.Node): void {
if (isMultiline(node)) {
forceComma(node);
} else {
forbidComma(node);
}
}

return {
...rules,
TSEnumDeclaration: predicate[normalizedOptions.enums],
TSTypeParameterDeclaration: predicate[normalizedOptions.generics],
TSTupleType: predicate[normalizedOptions.tuples],
};
},
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -6,6 +6,7 @@ import banTslintComment from './ban-tslint-comment';
import banTypes from './ban-types';
import braceStyle from './brace-style';
import classLiteralPropertyStyle from './class-literal-property-style';
import commaDangle from './comma-dangle';
import commaSpacing from './comma-spacing';
import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion';
import consistentTypeAssertions from './consistent-type-assertions';
Expand Down Expand Up @@ -114,6 +115,7 @@ export default {
'ban-types': banTypes,
'brace-style': braceStyle,
'class-literal-property-style': classLiteralPropertyStyle,
'comma-dangle': commaDangle,
'comma-spacing': commaSpacing,
'consistent-type-assertions': consistentTypeAssertions,
'consistent-type-definitions': consistentTypeDefinitions,
Expand Down