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): [naming-convention] allow specifying an array of selectors #2335

Merged
Show file tree
Hide file tree
Changes from 14 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
21 changes: 19 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 @@ -361,6 +361,23 @@ This allows you to emulate the old `interface-name-prefix` rule.
}
```

### Enforce that variable and function names are in camelCase
bradzacher marked this conversation as resolved.
Show resolved Hide resolved

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