Skip to content
Permalink
Browse files

feat(eslint-plugin): add extension rule `comma-dangle` (#2416)

  • Loading branch information
yeonjuan committed Sep 21, 2020
1 parent c6f72fb commit f7babcf4e6da3e5cba8f2c75d57abf8089432d05
@@ -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: |
@@ -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>
@@ -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',
},
};
@@ -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],
};
},
});
@@ -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';
@@ -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,

0 comments on commit f7babcf

Please sign in to comment.