Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): [naming-convention] add requireDouble, `allowD…
…ouble`, `allowSingleOrDouble` options for underscores (#2812)
  • Loading branch information
bradzacher committed Nov 25, 2020
1 parent 6a06944 commit dd0576a
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 30 deletions.
23 changes: 19 additions & 4 deletions packages/eslint-plugin/docs/rules/naming-convention.md
Expand Up @@ -33,8 +33,20 @@ type Options = {
regex: string;
match: boolean;
};
leadingUnderscore?: 'forbid' | 'allow' | 'require';
trailingUnderscore?: 'forbid' | 'allow' | 'require';
leadingUnderscore?:
| 'forbid'
| 'require'
| 'requireDouble'
| 'allow'
| 'allowDouble'
| 'allowSingleOrDouble';
trailingUnderscore?:
| 'forbid'
| 'require'
| 'requireDouble'
| 'allow'
| 'allowDouble'
| 'allowSingleOrDouble';
prefix?: string[];
suffix?: string[];

Expand Down Expand Up @@ -141,8 +153,11 @@ Alternatively, `filter` accepts a regular expression (anything accepted into `ne
The `leadingUnderscore` / `trailingUnderscore` options control whether leading/trailing underscores are considered valid. Accepts one of the following values:

- `forbid` - a leading/trailing underscore is not allowed at all.
- `allow` - existence of a leading/trailing underscore is not explicitly enforced.
- `require` - a leading/trailing underscore must be included.
- `require` - a single leading/trailing underscore must be included.
- `requireDouble` - two leading/trailing underscores must be included.
- `allow` - existence of a single leading/trailing underscore is not explicitly enforced.
- `allowDouble` - existence of a double leading/trailing underscore is not explicitly enforced.
- `allowSingleOrDouble` - existence of a single or a double leading/trailing underscore is not explicitly enforced.

#### `prefix` / `suffix`

Expand Down
114 changes: 91 additions & 23 deletions packages/eslint-plugin/src/rules/naming-convention.ts
Expand Up @@ -19,19 +19,24 @@ type MessageIds =
// #region Options Type Config

enum PredefinedFormats {
camelCase = 1 << 0,
strictCamelCase = 1 << 1,
PascalCase = 1 << 2,
StrictPascalCase = 1 << 3,
snake_case = 1 << 4,
UPPER_CASE = 1 << 5,
camelCase = 1,
strictCamelCase,
PascalCase,
StrictPascalCase,
snake_case,
UPPER_CASE,
}
type PredefinedFormatsString = keyof typeof PredefinedFormats;

enum UnderscoreOptions {
forbid = 1 << 0,
allow = 1 << 1,
require = 1 << 2,
forbid = 1,
allow,
require,

// special cases as it's common practice to use double underscore
requireDouble,
allowDouble,
allowSingleOrDouble,
}
type UnderscoreOptionsString = keyof typeof UnderscoreOptions;

Expand Down Expand Up @@ -483,7 +488,7 @@ export default util.createRule<Options, MessageIds>({
unexpectedUnderscore:
'{{type}} name `{{name}}` must not have a {{position}} underscore.',
missingUnderscore:
'{{type}} name `{{name}}` must have a {{position}} underscore.',
'{{type}} name `{{name}}` must have {{count}} {{position}} underscore(s).',
missingAffix:
'{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}',
satisfyCustom:
Expand Down Expand Up @@ -1143,19 +1148,22 @@ function createValidator(
processedName,
position,
custom,
count,
}: {
affixes?: string[];
formats?: PredefinedFormats[];
originalName: string;
processedName?: string;
position?: 'leading' | 'trailing' | 'prefix' | 'suffix';
custom?: NonNullable<NormalizedSelector['custom']>;
count?: 'one' | 'two';
}): Record<string, unknown> {
return {
type: selectorTypeToMessageString(type),
name: originalName,
processedName,
position,
count,
affixes: affixes?.join(', '),
formats: formats?.map(f => PredefinedFormats[f]).join(', '),
regex: custom?.regex?.toString(),
Expand Down Expand Up @@ -1186,47 +1194,107 @@ function createValidator(
return name;
}

const hasUnderscore =
position === 'leading' ? name.startsWith('_') : name.endsWith('_');
const trimUnderscore =
const hasSingleUnderscore =
position === 'leading'
? (): boolean => name.startsWith('_')
: (): boolean => name.endsWith('_');
const trimSingleUnderscore =
position === 'leading'
? (): string => name.slice(1)
: (): string => name.slice(0, -1);

const hasDoubleUnderscore =
position === 'leading'
? (): boolean => name.startsWith('__')
: (): boolean => name.endsWith('__');
const trimDoubleUnderscore =
position === 'leading'
? (): string => name.slice(2)
: (): string => name.slice(0, -2);

switch (option) {
case UnderscoreOptions.allow:
// no check - the user doesn't care if it's there or not
break;
// ALLOW - no conditions as the user doesn't care if it's there or not
case UnderscoreOptions.allow: {
if (hasSingleUnderscore()) {
return trimSingleUnderscore();
}

return name;
}

case UnderscoreOptions.allowDouble: {
if (hasDoubleUnderscore()) {
return trimDoubleUnderscore();
}

case UnderscoreOptions.forbid:
if (hasUnderscore) {
return name;
}

case UnderscoreOptions.allowSingleOrDouble: {
if (hasDoubleUnderscore()) {
return trimDoubleUnderscore();
}

if (hasSingleUnderscore()) {
return trimSingleUnderscore();
}

return name;
}

// FORBID
case UnderscoreOptions.forbid: {
if (hasSingleUnderscore()) {
context.report({
node,
messageId: 'unexpectedUnderscore',
data: formatReportData({
originalName,
position,
count: 'one',
}),
});
return null;
}
break;

case UnderscoreOptions.require:
if (!hasUnderscore) {
return name;
}

// REQUIRE
case UnderscoreOptions.require: {
if (!hasSingleUnderscore()) {
context.report({
node,
messageId: 'missingUnderscore',
data: formatReportData({
originalName,
position,
count: 'one',
}),
});
return null;
}

return trimSingleUnderscore();
}

case UnderscoreOptions.requireDouble: {
if (!hasDoubleUnderscore()) {
context.report({
node,
messageId: 'missingUnderscore',
data: formatReportData({
originalName,
position,
count: 'two',
}),
});
return null;
}
}

return hasUnderscore ? trimUnderscore() : name;
return trimDoubleUnderscore();
}
}
}

/**
Expand Down
116 changes: 113 additions & 3 deletions packages/eslint-plugin/tests/rules/naming-convention.test.ts
Expand Up @@ -128,6 +128,11 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase<Options>[] {
format,
leadingUnderscore: 'require',
}),
createCase(`__${name}`, {
...test.options,
format,
leadingUnderscore: 'requireDouble',
}),
createCase(`_${name}`, {
...test.options,
format,
Expand All @@ -138,6 +143,36 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase<Options>[] {
format,
leadingUnderscore: 'allow',
}),
createCase(`__${name}`, {
...test.options,
format,
leadingUnderscore: 'allowDouble',
}),
createCase(name, {
...test.options,
format,
leadingUnderscore: 'allowDouble',
}),
createCase(`_${name}`, {
...test.options,
format,
leadingUnderscore: 'allowSingleOrDouble',
}),
createCase(name, {
...test.options,
format,
leadingUnderscore: 'allowSingleOrDouble',
}),
createCase(`__${name}`, {
...test.options,
format,
leadingUnderscore: 'allowSingleOrDouble',
}),
createCase(name, {
...test.options,
format,
leadingUnderscore: 'allowSingleOrDouble',
}),

// trailingUnderscore
createCase(name, {
Expand All @@ -150,6 +185,11 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase<Options>[] {
format,
trailingUnderscore: 'require',
}),
createCase(`${name}__`, {
...test.options,
format,
trailingUnderscore: 'requireDouble',
}),
createCase(`${name}_`, {
...test.options,
format,
Expand All @@ -160,6 +200,36 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase<Options>[] {
format,
trailingUnderscore: 'allow',
}),
createCase(`${name}__`, {
...test.options,
format,
trailingUnderscore: 'allowDouble',
}),
createCase(name, {
...test.options,
format,
trailingUnderscore: 'allowDouble',
}),
createCase(`${name}_`, {
...test.options,
format,
trailingUnderscore: 'allowSingleOrDouble',
}),
createCase(name, {
...test.options,
format,
trailingUnderscore: 'allowSingleOrDouble',
}),
createCase(`${name}__`, {
...test.options,
format,
trailingUnderscore: 'allowSingleOrDouble',
}),
createCase(name, {
...test.options,
format,
trailingUnderscore: 'allowSingleOrDouble',
}),

// prefix
createCase(`MyPrefix${name}`, {
Expand Down Expand Up @@ -283,7 +353,27 @@ function createInvalidTestCases(
leadingUnderscore: 'require',
},
'missingUnderscore',
{ position: 'leading' },
{ position: 'leading', count: 'one' },
),
createCase(
name,
{
...test.options,
format,
leadingUnderscore: 'requireDouble',
},
'missingUnderscore',
{ position: 'leading', count: 'two' },
),
createCase(
`_${name}`,
{
...test.options,
format,
leadingUnderscore: 'requireDouble',
},
'missingUnderscore',
{ position: 'leading', count: 'two' },
),

// trailingUnderscore
Expand All @@ -305,7 +395,27 @@ function createInvalidTestCases(
trailingUnderscore: 'require',
},
'missingUnderscore',
{ position: 'trailing' },
{ position: 'trailing', count: 'one' },
),
createCase(
name,
{
...test.options,
format,
trailingUnderscore: 'requireDouble',
},
'missingUnderscore',
{ position: 'trailing', count: 'two' },
),
createCase(
`${name}_`,
{
...test.options,
format,
trailingUnderscore: 'requireDouble',
},
'missingUnderscore',
{ position: 'trailing', count: 'two' },
),

// prefix
Expand Down Expand Up @@ -1188,7 +1298,7 @@ ruleTester.run('naming-convention', rule, {
// this line is intentionally broken out
UnusedTypeParam
> = {};
export const used_var = 1;
export function used_func(
// this line is intentionally broken out
Expand Down

0 comments on commit dd0576a

Please sign in to comment.