From dd0576a66c34810bc60e0958948c9a8104a3f1a3 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 17:20:54 -0800 Subject: [PATCH] feat(eslint-plugin): [naming-convention] add `requireDouble`, `allowDouble`, `allowSingleOrDouble` options for underscores (#2812) --- .../docs/rules/naming-convention.md | 23 +++- .../src/rules/naming-convention.ts | 114 +++++++++++++---- .../tests/rules/naming-convention.test.ts | 116 +++++++++++++++++- 3 files changed, 223 insertions(+), 30 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 70722707b87..0d180bbf7ae 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -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[]; @@ -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` diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 2b5dac564e1..fe025389af9 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -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; @@ -483,7 +488,7 @@ export default util.createRule({ 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: @@ -1143,6 +1148,7 @@ function createValidator( processedName, position, custom, + count, }: { affixes?: string[]; formats?: PredefinedFormats[]; @@ -1150,12 +1156,14 @@ function createValidator( processedName?: string; position?: 'leading' | 'trailing' | 'prefix' | 'suffix'; custom?: NonNullable; + count?: 'one' | 'two'; }): Record { return { type: selectorTypeToMessageString(type), name: originalName, processedName, position, + count, affixes: affixes?.join(', '), formats: formats?.map(f => PredefinedFormats[f]).join(', '), regex: custom?.regex?.toString(), @@ -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(); + } + } } /** diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index 28fdc4c060b..87123331153 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -128,6 +128,11 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { format, leadingUnderscore: 'require', }), + createCase(`__${name}`, { + ...test.options, + format, + leadingUnderscore: 'requireDouble', + }), createCase(`_${name}`, { ...test.options, format, @@ -138,6 +143,36 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { 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, { @@ -150,6 +185,11 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { format, trailingUnderscore: 'require', }), + createCase(`${name}__`, { + ...test.options, + format, + trailingUnderscore: 'requireDouble', + }), createCase(`${name}_`, { ...test.options, format, @@ -160,6 +200,36 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { 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}`, { @@ -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 @@ -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 @@ -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