Skip to content

Commit

Permalink
feat(eslint-plugin): [naming-convention] allow specifying an array of…
Browse files Browse the repository at this point in the history
… selectors (#2335)

Co-authored-by: Gavin <gavin@vinescloud.com>
  • Loading branch information
GavinWu1991 and Gavin committed Aug 2, 2020
1 parent fa169e7 commit 3ef6bd5
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 30 deletions.
23 changes: 21 additions & 2 deletions packages/eslint-plugin/docs/rules/naming-convention.md
@@ -1,7 +1,7 @@
# Enforces naming conventions for everything across a codebase (`naming-convention`)

Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable.
Additionally, a well designed style guide can help communicate intent, such as by enforcing all private properties begin with an `_`, and all global-level constants are written in `UPPER_CASE`.
Additionally, a well-designed style guide can help communicate intent, such as by enforcing all private properties begin with an `_`, and all global-level constants are written in `UPPER_CASE`.

There are many different rules that have existed over time, but they have had the problem of not having enough granularity, meaning it was hard to have a well defined style guide, and most of the time you needed 3 or more rules at once to enforce different conventions, hoping they didn't conflict.

Expand Down Expand Up @@ -39,7 +39,7 @@ type Options = {
suffix?: string[];

// selector options
selector: Selector;
selector: Selector | Selector[];
filter?:
| string
| {
Expand Down Expand Up @@ -155,6 +155,8 @@ If these are provided, the identifier must start with one of the provided values
### Selector Options

- `selector` (see "Allowed Selectors, Modifiers and Types" below).
- Accepts one or array of selectors to define an option block that applies to one or multiple selectors.
- For example, if you provide `{ selector: ['variable', 'function'] }`, then it will apply the same option to variable and function nodes.
- `modifiers` allows you to specify which modifiers to granularly apply to, such as the accessibility (`private`/`public`/`protected`), or if the thing is `static`, etc.
- The name must match _all_ of the modifiers.
- For example, if you provide `{ modifiers: ['private', 'static', 'readonly'] }`, then it will only match something that is `private static readonly`, and something that is just `private` will not match.
Expand Down Expand Up @@ -361,6 +363,23 @@ This allows you to emulate the old `interface-name-prefix` rule.
}
```

### Enforce that variable and function names are in camelCase

This allows you to lint multiple type with same pattern.

```json
{
"@typescript-eslint/naming-convention": [
"error",
{
"selector": ["variable", "function"],
"format": ["camelCase"],
"leadingUnderscore": "allow"
}
]
}
```

### Ignore properties that require quotes

Sometimes you have to use a quoted name that breaks the convention (for example, HTTP headers).
Expand Down
83 changes: 73 additions & 10 deletions packages/eslint-plugin/src/rules/naming-convention.ts
@@ -1,8 +1,8 @@
import {
AST_NODE_TYPES,
JSONSchema,
TSESTree,
TSESLint,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import * as ts from 'typescript';
import * as util from '../util';
Expand Down Expand Up @@ -111,7 +111,9 @@ interface Selector {
prefix?: string[];
suffix?: string[];
// selector options
selector: IndividualAndMetaSelectorsString;
selector:
| IndividualAndMetaSelectorsString
| IndividualAndMetaSelectorsString[];
modifiers?: ModifiersString[];
types?: TypeModifiersString[];
filter?:
Expand Down Expand Up @@ -249,10 +251,60 @@ function selectorSchema(
},
];
}

function selectorsSchema(): JSONSchema.JSONSchema4 {
return {
type: 'object',
properties: {
...FORMAT_OPTIONS_PROPERTIES,
...{
filter: {
oneOf: [
{
type: 'string',
minLength: 1,
},
MATCH_REGEX_SCHEMA,
],
},
selector: {
type: 'array',
items: {
type: 'string',
enum: [
...util.getEnumNames(MetaSelectors),
...util.getEnumNames(Selectors),
],
},
additionalItems: false,
},
},
},
modifiers: {
type: 'array',
items: {
type: 'string',
enum: util.getEnumNames(Modifiers),
},
additionalItems: false,
},
types: {
type: 'array',
items: {
type: 'string',
enum: util.getEnumNames(TypeModifiers),
},
additionalItems: false,
},
required: ['selector', 'format'],
additionalProperties: false,
};
}
const SCHEMA: JSONSchema.JSONSchema4 = {
type: 'array',
items: {
oneOf: [
selectorsSchema(),
...selectorSchema('default', false, util.getEnumNames(Modifiers)),

...selectorSchema('variableLike', false),
Expand Down Expand Up @@ -765,15 +817,15 @@ type ValidatorFunction = (
) => void;
type ParsedOptions = Record<SelectorsString, null | ValidatorFunction>;
type Context = Readonly<TSESLint.RuleContext<MessageIds, Options>>;

function parseOptions(context: Context): ParsedOptions {
const normalizedOptions = context.options.map(opt => normalizeOption(opt));
const parsedOptions = util.getEnumNames(Selectors).reduce((acc, k) => {
return util.getEnumNames(Selectors).reduce((acc, k) => {
acc[k] = createValidator(k, context, normalizedOptions);
return acc;
}, {} as ParsedOptions);

return parsedOptions;
}

function createValidator(
type: SelectorsString,
context: Context,
Expand Down Expand Up @@ -1219,7 +1271,7 @@ function normalizeOption(option: Selector): NormalizedSelector {
weight |= 1 << 30;
}

return {
const normalizedOption = {
// format options
format: option.format ? option.format.map(f => PredefinedFormats[f]) : null,
custom: option.custom
Expand All @@ -1238,10 +1290,6 @@ function normalizeOption(option: Selector): NormalizedSelector {
: null,
prefix: option.prefix && option.prefix.length > 0 ? option.prefix : null,
suffix: option.suffix && option.suffix.length > 0 ? option.suffix : null,
// selector options
selector: isMetaSelector(option.selector)
? MetaSelectors[option.selector]
: Selectors[option.selector],
modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null,
types: option.types?.map(m => TypeModifiers[m]) ?? null,
filter:
Expand All @@ -1256,6 +1304,21 @@ function normalizeOption(option: Selector): NormalizedSelector {
// calculated ordering weight based on modifiers
modifierWeight: weight,
};

const selectors = Array.isArray(option.selector)
? option.selector
: [option.selector];

return {
selector: selectors
.map(selector =>
isMetaSelector(selector)
? MetaSelectors[selector]
: Selectors[selector],
)
.reduce((accumulator, selector) => accumulator | selector),
...normalizedOption,
};
}

function isCorrectType(
Expand Down
144 changes: 126 additions & 18 deletions packages/eslint-plugin/tests/rules/naming-convention.test.ts
Expand Up @@ -205,33 +205,46 @@ function createInvalidTestCases(
options: Selector,
messageId: MessageIds,
data: Record<string, unknown> = {},
): TSESLint.InvalidTestCase<MessageIds, Options> => ({
options: [
{
...options,
filter: IGNORED_FILTER,
},
],
code: `// ${JSON.stringify(options)}\n${test.code
.map(code => code.replace(REPLACE_REGEX, preparedName))
.join('\n')}`,
errors: test.code.map(() => ({
): TSESLint.InvalidTestCase<MessageIds, Options> => {
const selectors = Array.isArray(test.options.selector)
? test.options.selector
: [test.options.selector];
const errorsTemplate = selectors.map(selector => ({
messageId,
...(test.options.selector !== 'default' &&
test.options.selector !== 'variableLike' &&
test.options.selector !== 'memberLike' &&
test.options.selector !== 'typeLike'
...(selector !== 'default' &&
selector !== 'variableLike' &&
selector !== 'memberLike' &&
selector !== 'typeLike'
? {
data: {
type: selectorTypeToMessageString(test.options.selector),
type: selectorTypeToMessageString(selector),
name: preparedName,
...data,
},
}
: // meta-types will use the correct selector, so don't assert on data shape
{}),
})),
});
}));

const errors: {
data?: { type: string; name: string };
messageId: MessageIds;
}[] = [];
test.code.forEach(() => errors.push(...errorsTemplate));

return {
options: [
{
...options,
filter: IGNORED_FILTER,
},
],
code: `// ${JSON.stringify(options)}\n${test.code
.map(code => code.replace(REPLACE_REGEX, preparedName))
.join('\n')}`,
errors: errors,
};
};

const prefixSingle = ['MyPrefix'];
const prefixMulti = ['MyPrefix1', 'MyPrefix2'];
Expand Down Expand Up @@ -714,6 +727,27 @@ ruleTester.run('naming-convention', rule, {
},
],
},
{
code: `
let foo = 'a';
const _foo = 1;
interface foo {}
class bar {}
function fooFunctionBar() {}
function _fooFunctionBar() {}
`,
options: [
{
selector: ['default', 'typeLike', 'function'],
format: ['camelCase'],
custom: {
regex: /^unused_\w/.source,
match: false,
},
leadingUnderscore: 'allow',
},
],
},
{
code: `
const match = 'test'.match(/test/);
Expand Down Expand Up @@ -1029,6 +1063,80 @@ ruleTester.run('naming-convention', rule, {
},
],
},
{
code: `
let unused_foo = 'a';
const _unused_foo = 1;
function foo_bar() {}
interface IFoo {}
class IBar {}
`,
options: [
{
selector: ['variable', 'function'],
format: ['camelCase'],
leadingUnderscore: 'allow',
},
{
selector: ['class', 'interface'],
format: ['PascalCase'],
custom: {
regex: /^I[A-Z]/.source,
match: false,
},
},
],
errors: [
{
messageId: 'doesNotMatchFormat',
line: 2,
data: {
type: 'Variable',
name: 'unused_foo',
formats: 'camelCase',
},
},
{
messageId: 'doesNotMatchFormatTrimmed',
line: 3,
data: {
type: 'Variable',
name: '_unused_foo',
processedName: 'unused_foo',
formats: 'camelCase',
},
},
{
messageId: 'doesNotMatchFormat',
line: 4,
data: {
type: 'Function',
name: 'foo_bar',
formats: 'camelCase',
},
},
{
messageId: 'satisfyCustom',
line: 5,
data: {
type: 'Interface',
name: 'IFoo',
regex: '/^I[A-Z]/u',
regexMatch: 'not match',
},
},
{
messageId: 'satisfyCustom',
line: 6,
data: {
type: 'Class',
name: 'IBar',
regex: '/^I[A-Z]/u',
regexMatch: 'not match',
},
},
],
},
{
code: `
const foo = {
Expand Down

0 comments on commit 3ef6bd5

Please sign in to comment.