From 4396290043ac5f201ed1ab34acc48be554ab4f0c Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 9 Dec 2019 19:30:45 +1030 Subject: [PATCH 01/25] feat: variable, function, parameter, parameterProperty, property --- .../src/rules/naming-convention.ts | 858 ++++++++++++++++++ .../tests/rules/naming-convention.test.ts | 455 ++++++++++ 2 files changed, 1313 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/naming-convention.ts create mode 100644 packages/eslint-plugin/tests/rules/naming-convention.test.ts diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts new file mode 100644 index 000000000000..eed049d8d6ee --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -0,0 +1,858 @@ +import { + AST_NODE_TYPES, + JSONSchema, + TSESTree, + TSESLint, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +type MessageIds = + | 'unexpectedUnderscore' + | 'missingUnderscore' + | 'missingAffix' + | 'doesNotMatchFormat'; + +// #region Options Type Config + +type PredefinedFormats = + | 'camelCase' + | 'strictCamelCase' + | 'PascalCase' + | 'StrictPascalCase' + | 'UPPER_CASE' + | 'snake_case'; +type UnderscroreOptions = 'forbid' | 'allow' | 'require'; +interface FormatOptions { + leadingUnderscore?: UnderscroreOptions; + trailingUnderscore?: UnderscroreOptions; + prefix?: string[]; + suffix?: string[]; + format: PredefinedFormats[]; +} + +type NonDefaultSelectors = + | 'variable' + | 'function' + | 'parameter' + | 'property' + | 'parameterProperty' + | 'enumMember' + | 'method' + | 'accessor' + | 'class' + | 'interface' + | 'typeAlias' + | 'typeParameter' + | 'enum'; +type Selectors = 'default' | NonDefaultSelectors; +type Modifiers = + | 'readonly' + | 'static' + | 'public' + | 'protected' + | 'private' + | 'abstract'; +type TypeModifiers = 'boolean' | 'string' | 'number' | 'function' | 'array'; + +interface SelectorBase { + selector: TType; + modifiers?: Modifiers[]; + types?: TypeModifiers[]; +} +type Selector = FormatOptions & + SelectorBase & { + filter?: string; + }; +type NormalizedSelector = FormatOptions & + SelectorBase & { + filter: RegExp | null; + }; + +// Note that this intentionally does not strictly type the modifiers/types properties. +// This is because doing so creates a huge headache, as the rule's code doesn't need to care. +// The JSON Schema strictly types these properties, so we know the user won't input invalid config. +type Options = ( + | Selector<'default'> + | Selector<'variable'> + | Selector<'function'> + | Selector<'parameter'> + | Selector<'property'> + | Selector<'parameterProperty'> + | Selector<'enumMember'> + | Selector<'method'> + | Selector<'accessor'> + | Selector<'class'> + | Selector<'interface'> + | Selector<'typeAlias'> + | Selector<'typeParameter'> + | Selector<'enum'> +)[]; + +// #endregion Options Type Config + +// #region Schema Config + +const UNDERSCORE_SCHEMA: JSONSchema.JSONSchema4 = { + type: 'string', + enum: ['forbid', 'allow', 'require'], +}; +const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + minItems: 1, + additionalItems: false, +}; +type JSONSchemaProperties = Record; +const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { + leadingUnderscore: UNDERSCORE_SCHEMA, + trailingUnderscore: UNDERSCORE_SCHEMA, + prefix: PREFIX_SUFFIX_SCHEMA, + suffix: PREFIX_SUFFIX_SCHEMA, + format: { + type: 'array', + items: { + type: 'string', + enum: [ + 'camelCase', + 'strictCamelCase', + 'PascalCase', + 'StrictPascalCase', + 'UPPER_CASE', + 'snake_case', + ], + }, + minItems: 1, + additionalItems: false, + }, +}; +const SELECTOR_BASE: JSONSchemaProperties = { + filter: { + type: 'string', + minLength: 1, + }, +}; +const TYPE_MODIFIERS_SCHEMA: JSONSchema.JSONSchema4 = { + type: 'array', + items: { + type: 'string', + enum: ['boolean', 'string', 'number', 'function', 'array'], + }, + additionalItems: false, +}; +function selectorSchema( + type: Selectors, + types: boolean, + modifiers?: Modifiers[], +): JSONSchema.JSONSchema4[] { + const selector: JSONSchemaProperties = { + ...SELECTOR_BASE, + selector: { + type: 'string', + enum: [type], + }, + }; + if (modifiers && modifiers.length > 0) { + selector.modifiers = { + type: 'array', + items: { + type: 'string', + enum: modifiers, + }, + additionalItems: false, + }; + } + if (types) { + selector.types = TYPE_MODIFIERS_SCHEMA; + } + + return [ + { + type: 'object', + properties: { + ...FORMAT_OPTIONS_PROPERTIES, + ...selector, + }, + required: ['selector', 'format'], + additionalProperties: false, + }, + ]; +} +const SCHEMA: JSONSchema.JSONSchema4 = { + type: 'array', + minItems: 1, + items: { + oneOf: [ + ...selectorSchema('default', false), + ...selectorSchema('variable', true), + ...selectorSchema('function', false), + ...selectorSchema('parameter', true), + ...selectorSchema('property', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + ...selectorSchema('parameterProperty', true, [ + 'private', + 'protected', + 'public', + 'readonly', + ]), + ...selectorSchema('enumMember', false), + ...selectorSchema('method', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), + ...selectorSchema('accessor', true, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), + ...selectorSchema('class', false, ['abstract']), + ...selectorSchema('interface', false), + ...selectorSchema('typeAlias', false), + ...selectorSchema('typeParameter', false), + ...selectorSchema('enum', false), + ], + }, + additionalItems: false, +}; + +// #endregion Schema Config + +export default util.createRule({ + name: 'naming-convention', + meta: { + docs: { + category: 'Variables', + description: '', + recommended: false, + }, + type: 'suggestion', + messages: { + unexpectedUnderscore: + '{{type}} name {{name}} must not have a {{position}} underscore.', + missingUnderscore: + '{{type}} name {{name}} must have a {{position}} underscore', + missingAffix: + '{{type}} name {{name}} must have one of the following {{position}}es: {{affixes}}', + doesNotMatchFormat: + '{{type}} name {{name}} must match one of the following formats: {{formats}}', + }, + schema: SCHEMA, + }, + defaultOptions: [], + create(context) { + const validators = parseOptions(context); + + function handleProperty( + node: + | TSESTree.TSPropertySignature + | TSESTree.Property + | TSESTree.ClassProperty + | TSESTree.TSAbstractClassProperty, + modifiers: Set, + ): void { + const validator = validators.property; + if (!validator) { + return; + } + + const key = node.key; + /* istanbul ignore if */ if (!util.isLiteralOrIdentifier(key)) { + // shouldn't happen due to selector + return; + } + + validator(key, modifiers); + } + + return { + // variable + VariableDeclarator(node: TSESTree.VariableDeclarator): void { + const validator = validators.variable; + if (!validator) { + return; + } + + const identifiers: TSESTree.Identifier[] = []; + getIdentifiersFromPattern(node.id, identifiers); + + identifiers.forEach(i => { + validator(i); + }); + }, + + // function + 'FunctionDeclaration, TSDeclareFunction, FunctionExpression'( + node: + | TSESTree.FunctionDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.FunctionExpression, + ): void { + const validator = validators.function; + if (!validator || node.id === null) { + return; + } + + validator(node.id); + }, + + // parameter + 'FunctionDeclaration, TSDeclareFunction, FunctionExpression, ArrowFunctionExpression'( + node: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression, + ): void { + const validator = validators.parameter; + if (!validator) { + return; + } + + node.params.forEach(param => { + if (param.type === AST_NODE_TYPES.TSParameterProperty) { + return; + } + + const identifiers: TSESTree.Identifier[] = []; + getIdentifiersFromPattern(param, identifiers); + + identifiers.forEach(i => { + validator(i); + }); + }); + }, + + // parameterProperty + TSParameterProperty(node): void { + const validator = validators.parameterProperty; + if (!validator) { + return; + } + + const modifiers = new Set(); + if (node.accessibility !== undefined) { + modifiers.add(node.accessibility); + } else { + modifiers.add('public'); + } + if (node.readonly) { + modifiers.add('readonly'); + } + + const identifiers: TSESTree.Identifier[] = []; + getIdentifiersFromPattern(node.parameter, identifiers); + + identifiers.forEach(i => { + validator(i, modifiers); + }); + }, + + // property + 'Property[computed = false][method = false][kind = "init"][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"]'( + node: TSESTree.Property, + ): void { + const modifiers = new Set(['public']); + handleProperty(node, modifiers); + }, + 'ClassProperty[computed = false], TSAbstractClassProperty[computed = false]'( + node: TSESTree.ClassProperty | TSESTree.TSAbstractClassProperty, + ): void { + const modifiers = new Set(); + if (node.accessibility) { + modifiers.add(node.accessibility); + } + if (node.readonly) { + modifiers.add('readonly'); + } + if (node.static) { + modifiers.add('static'); + } + if (node.type === AST_NODE_TYPES.TSAbstractClassProperty) { + modifiers.add('abstract'); + } + + handleProperty(node, modifiers); + }, + 'TSPropertySignature[computed = false]'( + node: TSESTree.TSPropertySignature, + ): void { + const modifiers = new Set(); + if (node.readonly) { + modifiers.add('readonly'); + } + + handleProperty(node, modifiers); + }, + }; + }, +}); + +function getIdentifiersFromPattern( + pattern: TSESTree.DestructuringPattern, + identifiers: TSESTree.Identifier[], +): void { + switch (pattern.type) { + case AST_NODE_TYPES.Identifier: + identifiers.push(pattern); + break; + + case AST_NODE_TYPES.ArrayPattern: + pattern.elements.forEach(element => { + getIdentifiersFromPattern(element, identifiers); + }); + break; + + case AST_NODE_TYPES.ObjectPattern: + pattern.properties.forEach(property => { + if (property.type === AST_NODE_TYPES.RestElement) { + getIdentifiersFromPattern(property, identifiers); + } else { + // this is a bit weird, but it's because ESTree doesn't have a new node type + // for object destructuring properties - it just reuses Property... + // https://github.com/estree/estree/blob/9ae284b71130d53226e7153b42f01bf819e6e657/es2015.md#L206-L211 + // However, the parser guarantees this is safe (and there is error handling) + getIdentifiersFromPattern( + property.value as TSESTree.DestructuringPattern, + identifiers, + ); + } + }); + break; + + case AST_NODE_TYPES.RestElement: + getIdentifiersFromPattern(pattern.argument, identifiers); + break; + + case AST_NODE_TYPES.AssignmentPattern: + getIdentifiersFromPattern(pattern.left, identifiers); + break; + + case AST_NODE_TYPES.MemberExpression: + // ignore member expressions, as the everything must already be defined + break; + + default: + // https://github.com/typescript-eslint/typescript-eslint/issues/1282 + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + throw new Error(`Unexpected pattern type ${pattern!.type}`); + } +} + +type ParsedOptions = Record< + NonDefaultSelectors, + | null + | (( + node: TSESTree.Identifier | TSESTree.Literal, + modifiers?: Set, + ) => void) +>; +type Context = TSESLint.RuleContext; +type Config = NormalizedSelector; +function parseOptions(context: Context): ParsedOptions { + const groupedOptions = new Map(); + const defaultConfig: NormalizedSelector<'default'>[] = []; + context.options.forEach(option => { + const normalized = normalizeOption(option); + if (option.selector === 'default') { + defaultConfig.push(normalized as NormalizedSelector<'default'>); + } else { + const selectors = groupedOptions.get(option.selector) ?? []; + selectors.push(normalized); + groupedOptions.set(option.selector, selectors); + } + }); + + const parsedOptions: ParsedOptions = { + variable: null, + function: null, + parameter: null, + property: null, + parameterProperty: null, + enumMember: null, + method: null, + accessor: null, + class: null, + interface: null, + typeAlias: null, + typeParameter: null, + enum: null, + }; + const selectorTypes = Object.keys(parsedOptions) as NonDefaultSelectors[]; + selectorTypes.forEach(type => { + const validators = groupedOptions.get(type); + if (validators) { + parsedOptions[type] = createValidator(type, context, validators); + } else if (defaultConfig.length > 0) { + parsedOptions[type] = createValidator(type, context, defaultConfig); + } + }); + + return parsedOptions; +} +function createValidator( + type: Selectors, + context: Context, + configs: Config[], +): (node: TSESTree.Identifier | TSESTree.Literal) => void { + return ( + node: TSESTree.Identifier | TSESTree.Literal, + modifiers: Set = new Set(), + ): void => { + // return will break the loop and stop checking configs + // it is only used when the name is known to have failed a config. + const originalName = + node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; + + for (const config of configs) { + if (config.filter?.test(originalName)) { + // name does not match the filter + continue; + } + + if (config.modifiers?.some(modifier => !modifiers.has(modifier))) { + // does not have the required modifiers + continue; + } + + let name: string | null = originalName; + + name = validateUnderscore('leading', config, name, node, originalName); + if (name === null) { + return; + } + + name = validateUnderscore('trailing', config, name, node, originalName); + if (name === null) { + return; + } + + name = validateAffix('prefix', config, name, node, originalName); + if (name === null) { + return; + } + + name = validateAffix('suffix', config, name, node, originalName); + if (name === null) { + return; + } + + if (!validatePredefinedFormat(config, name, node, originalName)) { + return; + } + + // it's valid for this config! + } + }; + + /** + * @returns the name with the underscore removed, if it is valid according to the specified underscore option, null otherwise + */ + function validateUnderscore( + position: 'leading' | 'trailing', + config: Config, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): string | null { + const option = + position === 'leading' + ? config.leadingUnderscore + : config.trailingUnderscore; + if (!option) { + return name; + } + + const hasUnderscore = + position === 'leading' ? name.startsWith('_') : name.endsWith('_'); + const trimUnderscore = + position === 'leading' + ? (): string => name.slice(1) + : (): string => name.slice(0, -1); + + switch (option) { + case 'allow': + // no check - the user doesn't care if it's there or not + break; + + case 'forbid': + if (hasUnderscore) { + context.report({ + node, + messageId: 'unexpectedUnderscore', + data: { + type: selectorTypeToMessageString(type), + name: originalName, + position, + }, + }); + return null; + } + break; + + case 'require': + if (!hasUnderscore) { + context.report({ + node, + messageId: 'missingUnderscore', + data: { + type: selectorTypeToMessageString(type), + name: originalName, + position, + }, + }); + return null; + } + } + + return hasUnderscore ? trimUnderscore() : name; + } + + /** + * @returns the name with the affix removed, if it is valid according to the specified affix option, null otherwise + */ + function validateAffix( + position: 'prefix' | 'suffix', + config: Config, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): string | null { + const options = config[position]; + if (!options || options.length === 0) { + return name; + } + + for (const option of options) { + const hasAffix = + position === 'prefix' ? name.startsWith(option) : name.endsWith(option); + const trimAffix = + position === 'prefix' + ? (): string => name.slice(option.length) + : (): string => name.slice(0, -option.length); + + if (hasAffix) { + // matches, so trim it and return + return trimAffix(); + } + } + + context.report({ + node, + messageId: 'missingAffix', + data: { + type: selectorTypeToMessageString(type), + name: originalName, + position, + affixes: options.join(', '), + }, + }); + return null; + } + + /** + * @returns true if the name is valid according to the `format` option, false otherwise + */ + function validatePredefinedFormat( + config: Config, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): boolean { + if (config.format.length === 0) { + return true; + } + + for (const format of config.format) { + switch (format) { + case 'PascalCase': + if (isPascalCase(name)) { + return true; + } + break; + + case 'StrictPascalCase': + if (isStrictPascalCase(name)) { + return true; + } + break; + + case 'camelCase': + if (isCamelCase(name)) { + return true; + } + break; + + case 'strictCamelCase': + if (isStrictCamelCase(name)) { + return true; + } + break; + + case 'UPPER_CASE': + if (isUpperCase(name)) { + return true; + } + break; + + case 'snake_case': + if (isSnakeCase(name)) { + return true; + } + break; + } + } + + context.report({ + node, + messageId: 'doesNotMatchFormat', + data: { + type: selectorTypeToMessageString(type), + name: originalName, + formats: config.format.join(', '), + }, + }); + return false; + } +} + +// #region Predefined Format Functions + +/* +These format functions are taken from tslint-consistent-codestyle/naming-convention: +https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/rules/namingConventionRule.ts#L603-L645 + +The licence for the code can be viewed here: +https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/LICENSE +*/ + +/* +Why not regex here? Because it's actually really, really difficult to create a regex to handle +all of the unicode cases, and we have many non-english users that use non-english characters. +https://gist.github.com/mathiasbynens/6334847 +*/ + +function isPascalCase(name: string): boolean { + return ( + name.length === 0 || + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + (name[0] === name[0].toUpperCase() && !name.includes('_')) + ); +} +function isStrictPascalCase(name: string): boolean { + return ( + name.length === 0 || + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + (name[0] === name[0].toUpperCase() && hasStrictCamelHumps(name, true)) + ); +} + +function isCamelCase(name: string): boolean { + return ( + name.length === 0 || + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + (name[0] === name[0].toLowerCase() && !name.includes('_')) + ); +} +function isStrictCamelCase(name: string): boolean { + return ( + name.length === 0 || + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + (name[0] === name[0].toLowerCase() && hasStrictCamelHumps(name, false)) + ); +} + +function hasStrictCamelHumps(name: string, isUpper: boolean): boolean { + if (name.startsWith('_')) { + return false; + } + for (let i = 1; i < name.length; ++i) { + if (name[i] === '_') { + return false; + } + if (isUpper === isUppercaseChar(name[i])) { + if (isUpper) { + return false; + } + } else { + isUpper = !isUpper; + } + } + return true; +} + +function isUppercaseChar(char: string): boolean { + return char === char.toUpperCase() && char !== char.toLowerCase(); +} + +function isSnakeCase(name: string): boolean { + return ( + name.length === 0 || + (name === name.toLowerCase() && validateUnderscores(name)) + ); +} + +function isUpperCase(name: string): boolean { + return ( + name.length === 0 || + (name === name.toUpperCase() && validateUnderscores(name)) + ); +} + +/** Check for leading trailing and adjacent underscores */ +function validateUnderscores(name: string): boolean { + if (name.startsWith('_')) { + return false; + } + let wasUnderscore = false; + for (let i = 1; i < name.length; ++i) { + if (name[i] === '_') { + if (wasUnderscore) { + return false; + } + wasUnderscore = true; + } else { + wasUnderscore = false; + } + } + return !wasUnderscore; +} +// #endregion Predefined Format Functions + +function selectorTypeToMessageString(selectorType: Selectors): string { + const notCamelCase = selectorType.replace(/([A-Z])/g, ' $1'); + return notCamelCase.charAt(0).toUpperCase() + notCamelCase.slice(1); +} + +function normalizeOption( + option: Selector, +): NormalizedSelector { + return { + ...option, + filter: option.filter !== undefined ? new RegExp(option.filter) : null, + }; +} + +export { + MessageIds, + Modifiers, + NonDefaultSelectors, + NormalizedSelector, + Options, + PredefinedFormats, + Selector, + Selectors, + selectorTypeToMessageString, + TypeModifiers, +}; diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts new file mode 100644 index 000000000000..a119ea9ee7d7 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -0,0 +1,455 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import rule, { + MessageIds, + Options, + PredefinedFormats, + Selector, + Selectors, + selectorTypeToMessageString, +} from '../../src/rules/naming-convention'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +const formatTestNames: Readonly +>> = { + camelCase: { + valid: ['strictCamelCase', 'lower', 'camelCaseUNSTRICT'], + invalid: ['snake_case', 'UPPER_CASE', 'UPPER', 'StrictPascalCase'], + }, + strictCamelCase: { + valid: ['strictCamelCase', 'lower'], + invalid: [ + 'snake_case', + 'UPPER_CASE', + 'UPPER', + 'StrictPascalCase', + 'camelCaseUNSTRICT', + ], + }, + PascalCase: { + valid: [ + 'StrictPascalCase', + 'Pascal', + 'I18n', + 'PascalCaseUNSTRICT', + 'UPPER', + ], + invalid: ['snake_case', 'UPPER_CASE', 'strictCamelCase'], + }, + StrictPascalCase: { + valid: ['StrictPascalCase', 'Pascal', 'I18n'], + invalid: [ + 'snake_case', + 'UPPER_CASE', + 'UPPER', + 'strictCamelCase', + 'PascalCaseUNSTRICT', + ], + }, + UPPER_CASE: { + valid: ['UPPER_CASE', 'UPPER'], + invalid: [ + 'lower', + 'snake_case', + 'SNAKE_case_UNSTRICT', + 'strictCamelCase', + 'StrictPascalCase', + ], + }, + // eslint-disable-next-line @typescript-eslint/camelcase + snake_case: { + valid: ['snake_case', 'lower'], + invalid: [ + 'UPPER_CASE', + 'SNAKE_case_UNSTRICT', + 'strictCamelCase', + 'StrictPascalCase', + ], + }, +}; + +const REPLACE_REGEX = /%/g; + +type Cases = { + code: string[]; + options: Omit; +}[]; +function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { + const newCases: TSESLint.ValidTestCase[] = []; + + for (const test of cases) { + for (const [formatLoose, names] of Object.entries(formatTestNames)) { + const format = [formatLoose as PredefinedFormats]; + for (const name of names.valid) { + const createCase = ( + preparedName: string, + options: Selector, + ): TSESLint.ValidTestCase => ({ + options: [ + { + ...(options as Options[0]), + filter: '[iI]gnored', + }, + ], + code: `// ${JSON.stringify(options)}\n${test.code + .map(code => code.replace(REPLACE_REGEX, preparedName)) + .join('\n')}`, + }); + + newCases.push( + createCase(name, { + ...test.options, + format, + }), + + // leadingUnderscore + createCase(name, { + ...test.options, + format, + leadingUnderscore: 'forbid', + }), + createCase(`_${name}`, { + ...test.options, + format, + leadingUnderscore: 'require', + }), + createCase(`_${name}`, { + ...test.options, + format, + leadingUnderscore: 'allow', + }), + createCase(name, { + ...test.options, + format, + leadingUnderscore: 'allow', + }), + + // trailingUnderscore + createCase(name, { + ...test.options, + format, + trailingUnderscore: 'forbid', + }), + createCase(`${name}_`, { + ...test.options, + format, + trailingUnderscore: 'require', + }), + createCase(`${name}_`, { + ...test.options, + format, + trailingUnderscore: 'allow', + }), + createCase(name, { + ...test.options, + format, + trailingUnderscore: 'allow', + }), + + // prefix + createCase(`MyPrefix${name}`, { + ...test.options, + format, + prefix: ['MyPrefix'], + }), + createCase(`MyPrefix2${name}`, { + ...test.options, + format, + prefix: ['MyPrefix1', 'MyPrefix2'], + }), + + // suffix + createCase(`${name}MySuffix`, { + ...test.options, + format, + suffix: ['MySuffix'], + }), + createCase(`${name}MySuffix2`, { + ...test.options, + format, + suffix: ['MySuffix1', 'MySuffix2'], + }), + ); + } + } + } + + return newCases; +} +function createInvalidTestCases( + cases: Cases, +): TSESLint.InvalidTestCase[] { + const newCases: TSESLint.InvalidTestCase[] = []; + + for (const test of cases) { + for (const [formatLoose, names] of Object.entries(formatTestNames)) { + const format = [formatLoose as PredefinedFormats]; + for (const name of names.invalid) { + const createCase = ( + preparedName: string, + options: Selector, + messageId: MessageIds, + data: Record = {}, + ): TSESLint.InvalidTestCase => ({ + options: [ + { + ...(options as Options[0]), + filter: '[iI]gnored', + }, + ], + code: `// ${JSON.stringify(options)}\n${test.code + .map(code => code.replace(REPLACE_REGEX, preparedName)) + .join('\n')}`, + errors: test.code.map(() => ({ + messageId, + ...(test.options.selector !== 'default' + ? { + data: { + type: selectorTypeToMessageString(test.options.selector), + name: preparedName, + ...data, + }, + } + : // default will use the correct selector, so don't assert on data + {}), + })), + }); + + const prefixSingle = ['MyPrefix']; + const prefixMulti = ['MyPrefix1', 'MyPrefix2']; + const suffixSingle = ['MySuffix']; + const suffixMulti = ['MySuffix1', 'MySuffix2']; + + newCases.push( + createCase( + name, + { + ...test.options, + format, + }, + 'doesNotMatchFormat', + { formats: format.join(', ') }, + ), + + // leadingUnderscore + createCase( + `_${name}`, + { + ...test.options, + format, + leadingUnderscore: 'forbid', + }, + 'unexpectedUnderscore', + { position: 'leading' }, + ), + createCase( + name, + { + ...test.options, + format, + leadingUnderscore: 'require', + }, + 'missingUnderscore', + { position: 'leading' }, + ), + + // trailingUnderscore + createCase( + `${name}_`, + { + ...test.options, + format, + trailingUnderscore: 'forbid', + }, + 'unexpectedUnderscore', + { position: 'trailing' }, + ), + createCase( + name, + { + ...test.options, + format, + trailingUnderscore: 'require', + }, + 'missingUnderscore', + { position: 'trailing' }, + ), + + // prefix + createCase( + name, + { + ...test.options, + format, + prefix: prefixSingle, + }, + 'missingAffix', + { position: 'prefix', affixes: prefixSingle.join(', ') }, + ), + createCase( + name, + { + ...test.options, + format, + prefix: prefixMulti, + }, + 'missingAffix', + { + position: 'prefix', + affixes: prefixMulti.join(', '), + }, + ), + + // suffix + createCase( + name, + { + ...test.options, + format, + suffix: suffixSingle, + }, + 'missingAffix', + { position: 'suffix', affixes: suffixSingle.join(', ') }, + ), + createCase( + name, + { + ...test.options, + format, + suffix: suffixMulti, + }, + 'missingAffix', + { + position: 'suffix', + affixes: suffixMulti.join(', '), + }, + ), + ); + } + } + } + + return newCases; +} + +const variableCases = [ + 'const % = 1;', + 'let % = 1;', + 'var % = 1;', + 'const {%} = {ignored: 1};', + 'const {% = 2} = {ignored: 1};', + 'const {...%} = {ignored: 1};', + 'const [%] = [1];', + 'const [% = 1] = [1];', + 'const [...%] = [1];', +]; +const functionCases = [ + 'function % () {}', + '(function % () {});', + 'declare function % ();', +]; +const parameterCases = [ + 'function ignored(%) {}', + '(function (%) {});', + 'declare function ignored(%);', + 'function ignored({%}) {}', + 'function ignored(...%) {}', + 'function ignored({% = 1}) {}', + 'function ignored({...%}) {}', + 'function ignored([%]) {}', + 'function ignored([% = 1]) {}', + 'function ignored([...%]) {}', +]; +const propertyCases = [ + 'const ignored = { %: 1 };', + 'const ignored = { "%": 1 };', + 'interface Ignored { %: string }', + 'interface Ignored { "%": string }', + 'type Ignored = { %: string }', + 'type Ignored = { "%": string }', + 'class Ignored { private % = 1 }', + 'class Ignored { private "%" = 1 }', + 'class Ignored { private readonly % = 1 }', + 'class Ignored { private static % = 1 }', + 'class Ignored { private static readonly % = 1 }', +]; +const parameterPropertyCases = [ + 'class Ignored { constructor(private %) {} }', + 'class Ignored { constructor(readonly %) {} }', + 'class Ignored { constructor(private readonly %) {} }', +]; + +const cases: Cases = [ + { + code: [ + 'const % = 1;', + 'function % () {}', + '(function (%) {});', + 'class Ignored { constructor(private %) {} }', + 'const ignored = { % };', + 'interface Ignored { %: string }', + 'type Ignored = { %: string }', + 'class Ignored { private % = 1 }', + ], + options: { + selector: 'default', + filter: '[iI]gnored', + }, + }, + { + code: variableCases, + options: { + selector: 'variable', + }, + }, + { + code: functionCases, + options: { + selector: 'function', + }, + }, + { + code: parameterCases, + options: { + selector: 'parameter', + }, + }, + { + code: parameterPropertyCases, + options: { + selector: 'parameterProperty', + }, + }, + { + code: ['class Ignored { constructor(private readonly %) {} }'], + options: { + selector: 'parameterProperty', + modifiers: ['readonly'], + }, + }, + { + code: propertyCases, + options: { + selector: 'property', + }, + }, + { + code: [ + 'class Ignored { abstract private static readonly % = 1; ignoredDueToModifiers = 1; }', + ], + options: { + selector: 'property', + modifiers: ['static', 'readonly'], + }, + }, +]; + +ruleTester.run('naming-convention', rule, { + valid: createValidTestCases(cases), + invalid: createInvalidTestCases(cases), +}); From 73d895c89e0a64f8ab452763d8e2436723446eb9 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 9 Dec 2019 22:41:25 +1030 Subject: [PATCH 02/25] feat: method --- .../src/rules/naming-convention.ts | 119 ++++++++++++-- .../tests/rules/naming-convention.test.ts | 155 +++++++++++------- 2 files changed, 207 insertions(+), 67 deletions(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index eed049d8d6ee..990c4a5523bd 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -78,9 +78,9 @@ type Options = ( | Selector<'parameter'> | Selector<'property'> | Selector<'parameterProperty'> - | Selector<'enumMember'> | Selector<'method'> | Selector<'accessor'> + | Selector<'enumMember'> | Selector<'class'> | Selector<'interface'> | Selector<'typeAlias'> @@ -203,7 +203,6 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'readonly', ]), - ...selectorSchema('enumMember', false), ...selectorSchema('method', false, [ 'private', 'protected', @@ -218,6 +217,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'abstract', ]), + ...selectorSchema('enumMember', false), ...selectorSchema('class', false, ['abstract']), ...selectorSchema('interface', false), ...selectorSchema('typeAlias', false), @@ -270,7 +270,31 @@ export default util.createRule({ const key = node.key; /* istanbul ignore if */ if (!util.isLiteralOrIdentifier(key)) { - // shouldn't happen due to selector + // shouldn't happen due to the selectors that are used + return; + } + + validator(key, modifiers); + } + + function handleMethod( + node: + | TSESTree.Property + | TSESTree.ClassProperty + | TSESTree.TSAbstractClassProperty + | TSESTree.MethodDefinition + | TSESTree.TSAbstractMethodDefinition + | TSESTree.TSMethodSignature, + modifiers: Set, + ): void { + const validator = validators.method; + if (!validator) { + return; + } + + const key = node.key; + /* istanbul ignore if */ if (!util.isLiteralOrIdentifier(key)) { + // shouldn't happen due to the selectors that are used return; } @@ -278,7 +302,8 @@ export default util.createRule({ } return { - // variable + // #region variable + VariableDeclarator(node: TSESTree.VariableDeclarator): void { const validator = validators.variable; if (!validator) { @@ -293,7 +318,10 @@ export default util.createRule({ }); }, - // function + // #endregion + + // #region function + 'FunctionDeclaration, TSDeclareFunction, FunctionExpression'( node: | TSESTree.FunctionDeclaration @@ -308,7 +336,10 @@ export default util.createRule({ validator(node.id); }, - // parameter + // #endregion function + + // #region parameter + 'FunctionDeclaration, TSDeclareFunction, FunctionExpression, ArrowFunctionExpression'( node: | TSESTree.FunctionDeclaration @@ -334,7 +365,10 @@ export default util.createRule({ }); }, - // parameterProperty + // #endregion parameter + + // #region parameterProperty + TSParameterProperty(node): void { const validator = validators.parameterProperty; if (!validator) { @@ -359,19 +393,28 @@ export default util.createRule({ }); }, - // property - 'Property[computed = false][method = false][kind = "init"][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"]'( + // #endregion parameterProperty + + // #region property + + 'Property[computed = false][kind = "init"][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( node: TSESTree.Property, ): void { const modifiers = new Set(['public']); handleProperty(node, modifiers); }, - 'ClassProperty[computed = false], TSAbstractClassProperty[computed = false]'( + + [[ + 'ClassProperty[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]', + 'TSAbstractClassProperty[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]', + ].join(', ')]( node: TSESTree.ClassProperty | TSESTree.TSAbstractClassProperty, ): void { const modifiers = new Set(); if (node.accessibility) { modifiers.add(node.accessibility); + } else { + modifiers.add('public'); } if (node.readonly) { modifiers.add('readonly'); @@ -385,16 +428,70 @@ export default util.createRule({ handleProperty(node, modifiers); }, + 'TSPropertySignature[computed = false]'( node: TSESTree.TSPropertySignature, ): void { - const modifiers = new Set(); + const modifiers = new Set(['public']); if (node.readonly) { modifiers.add('readonly'); } handleProperty(node, modifiers); }, + + // #endregion property + + // #region method + + [[ + 'Property[computed = false][kind = "init"][value.type = "ArrowFunctionExpression"]', + 'Property[computed = false][kind = "init"][value.type = "FunctionExpression"]', + 'Property[computed = false][kind = "init"][value.type = "TSEmptyBodyFunctionExpression"]', + 'TSMethodSignature[computed = false]', + ].join(', ')]( + node: TSESTree.Property | TSESTree.TSMethodSignature, + ): void { + const modifiers = new Set(['public']); + handleMethod(node, modifiers); + }, + + [[ + 'ClassProperty[computed = false][value.type = "ArrowFunctionExpression"]', + 'ClassProperty[computed = false][value.type = "FunctionExpression"]', + 'ClassProperty[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', + 'TSAbstractClassProperty[computed = false][value.type = "ArrowFunctionExpression"]', + 'TSAbstractClassProperty[computed = false][value.type = "FunctionExpression"]', + 'TSAbstractClassProperty[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', + 'MethodDefinition[computed = false][kind = "method"]', + 'TSAbstractMethodDefinition[computed = false][kind = "method"]', + ].join(', ')]( + node: + | TSESTree.ClassProperty + | TSESTree.TSAbstractClassProperty + | TSESTree.MethodDefinition + | TSESTree.TSAbstractMethodDefinition, + ): void { + const modifiers = new Set(); + if (node.accessibility) { + modifiers.add(node.accessibility); + } else { + modifiers.add('public'); + } + if (node.static) { + modifiers.add('static'); + } + if ( + node.type === AST_NODE_TYPES.TSAbstractClassProperty || + node.type === AST_NODE_TYPES.TSAbstractMethodDefinition + ) { + modifiers.add('abstract'); + } + + handleMethod(node, modifiers); + }, + + // #endregion method }; }, }); diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index a119ea9ee7d7..d74e5472cad6 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -337,54 +337,8 @@ function createInvalidTestCases( return newCases; } -const variableCases = [ - 'const % = 1;', - 'let % = 1;', - 'var % = 1;', - 'const {%} = {ignored: 1};', - 'const {% = 2} = {ignored: 1};', - 'const {...%} = {ignored: 1};', - 'const [%] = [1];', - 'const [% = 1] = [1];', - 'const [...%] = [1];', -]; -const functionCases = [ - 'function % () {}', - '(function % () {});', - 'declare function % ();', -]; -const parameterCases = [ - 'function ignored(%) {}', - '(function (%) {});', - 'declare function ignored(%);', - 'function ignored({%}) {}', - 'function ignored(...%) {}', - 'function ignored({% = 1}) {}', - 'function ignored({...%}) {}', - 'function ignored([%]) {}', - 'function ignored([% = 1]) {}', - 'function ignored([...%]) {}', -]; -const propertyCases = [ - 'const ignored = { %: 1 };', - 'const ignored = { "%": 1 };', - 'interface Ignored { %: string }', - 'interface Ignored { "%": string }', - 'type Ignored = { %: string }', - 'type Ignored = { "%": string }', - 'class Ignored { private % = 1 }', - 'class Ignored { private "%" = 1 }', - 'class Ignored { private readonly % = 1 }', - 'class Ignored { private static % = 1 }', - 'class Ignored { private static readonly % = 1 }', -]; -const parameterPropertyCases = [ - 'class Ignored { constructor(private %) {} }', - 'class Ignored { constructor(readonly %) {} }', - 'class Ignored { constructor(private readonly %) {} }', -]; - const cases: Cases = [ + // #region default { code: [ 'const % = 1;', @@ -401,26 +355,95 @@ const cases: Cases = [ filter: '[iI]gnored', }, }, + // #endregion default + + // #region variable { - code: variableCases, + code: [ + 'const % = 1;', + 'let % = 1;', + 'var % = 1;', + 'const {%} = {ignored: 1};', + 'const {% = 2} = {ignored: 1};', + 'const {...%} = {ignored: 1};', + 'const [%] = [1];', + 'const [% = 1] = [1];', + 'const [...%] = [1];', + ], options: { selector: 'variable', }, }, + // #endregion variable + + // #region function { - code: functionCases, + code: ['function % () {}', '(function % () {});', 'declare function % ();'], options: { selector: 'function', }, }, + // #endregion function + + // #region parameter { - code: parameterCases, + code: [ + 'function ignored(%) {}', + '(function (%) {});', + 'declare function ignored(%);', + 'function ignored({%}) {}', + 'function ignored(...%) {}', + 'function ignored({% = 1}) {}', + 'function ignored({...%}) {}', + 'function ignored([%]) {}', + 'function ignored([% = 1]) {}', + 'function ignored([...%]) {}', + ], options: { selector: 'parameter', }, }, + // #endregion parameter + + // #region property + { + code: [ + 'const ignored = { % };', + 'const ignored = { "%": 1 };', + 'interface Ignored { % }', + 'interface Ignored { "%": string }', + 'type Ignored = { % }', + 'type Ignored = { "%": string }', + 'class Ignored { private % }', + 'class Ignored { private "%" = 1 }', + 'class Ignored { private readonly % = 1 }', + 'class Ignored { private static % }', + 'class Ignored { private static readonly % = 1 }', + 'class Ignored { abstract % = 1 }', + 'class Ignored { declare % }', + ], + options: { + selector: 'property', + }, + }, + { + code: [ + 'class Ignored { abstract private static readonly % = 1; ignoredDueToModifiers = 1; }', + ], + options: { + selector: 'property', + modifiers: ['static', 'readonly'], + }, + }, + // #endregion property + + // #region parameterProperty { - code: parameterPropertyCases, + code: [ + 'class Ignored { constructor(private %) {} }', + 'class Ignored { constructor(readonly %) {} }', + 'class Ignored { constructor(private readonly %) {} }', + ], options: { selector: 'parameterProperty', }, @@ -432,21 +455,41 @@ const cases: Cases = [ modifiers: ['readonly'], }, }, + // #endregion parameterProperty + + // #region method { - code: propertyCases, + code: [ + 'const ignored = { %() {} };', + 'const ignored = { "%"() {} };', + 'const ignored = { %: () => {} };', + 'interface Ignored { %(): string }', + 'interface Ignored { "%"(): string }', + 'type Ignored = { %(): string }', + 'type Ignored = { "%"(): string }', + 'class Ignored { private %() {} }', + 'class Ignored { private "%"() {} }', + 'class Ignored { private readonly %() {} }', + 'class Ignored { private static %() {} }', + 'class Ignored { private static readonly %() {} }', + 'class Ignored { private % = () => {} }', + 'class Ignored { abstract %() }', + 'class Ignored { declare %() }', + ], options: { - selector: 'property', + selector: 'method', }, }, { code: [ - 'class Ignored { abstract private static readonly % = 1; ignoredDueToModifiers = 1; }', + 'class Ignored { abstract private static %() {}; ignoredDueToModifiers() {}; }', ], options: { - selector: 'property', - modifiers: ['static', 'readonly'], + selector: 'method', + modifiers: ['abstract', 'static'], }, }, + // #endregion method ]; ruleTester.run('naming-convention', rule, { From e3a14b5501101df98601deefae1df98e9c1f2bf4 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 9 Dec 2019 22:59:09 +1030 Subject: [PATCH 03/25] feat: accessors --- .../src/rules/naming-convention.ts | 123 +++++++++--------- .../tests/rules/naming-convention.test.ts | 24 ++++ 2 files changed, 83 insertions(+), 64 deletions(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 990c4a5523bd..66524bc58295 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -255,15 +255,18 @@ export default util.createRule({ create(context) { const validators = parseOptions(context); - function handleProperty( + function handleMember( + validator: ValidatiorFunction | null, node: - | TSESTree.TSPropertySignature | TSESTree.Property | TSESTree.ClassProperty - | TSESTree.TSAbstractClassProperty, + | TSESTree.TSAbstractClassProperty + | TSESTree.TSPropertySignature + | TSESTree.MethodDefinition + | TSESTree.TSAbstractMethodDefinition + | TSESTree.TSMethodSignature, modifiers: Set, ): void { - const validator = validators.property; if (!validator) { return; } @@ -277,28 +280,33 @@ export default util.createRule({ validator(key, modifiers); } - function handleMethod( + function getMemberModifiers( node: - | TSESTree.Property | TSESTree.ClassProperty | TSESTree.TSAbstractClassProperty | TSESTree.MethodDefinition - | TSESTree.TSAbstractMethodDefinition - | TSESTree.TSMethodSignature, - modifiers: Set, - ): void { - const validator = validators.method; - if (!validator) { - return; + | TSESTree.TSAbstractMethodDefinition, + ): Set { + const modifiers = new Set(); + if (node.accessibility) { + modifiers.add(node.accessibility); + } else { + modifiers.add('public'); } - - const key = node.key; - /* istanbul ignore if */ if (!util.isLiteralOrIdentifier(key)) { - // shouldn't happen due to the selectors that are used - return; + if (node.static) { + modifiers.add('static'); + } + if ('readonly' in node && node.readonly) { + modifiers.add('readonly'); + } + if ( + node.type === AST_NODE_TYPES.TSAbstractClassProperty || + node.type === AST_NODE_TYPES.TSAbstractMethodDefinition + ) { + modifiers.add('abstract'); } - validator(key, modifiers); + return modifiers; } return { @@ -401,7 +409,7 @@ export default util.createRule({ node: TSESTree.Property, ): void { const modifiers = new Set(['public']); - handleProperty(node, modifiers); + handleMember(validators.property, node, modifiers); }, [[ @@ -410,23 +418,8 @@ export default util.createRule({ ].join(', ')]( node: TSESTree.ClassProperty | TSESTree.TSAbstractClassProperty, ): void { - const modifiers = new Set(); - if (node.accessibility) { - modifiers.add(node.accessibility); - } else { - modifiers.add('public'); - } - if (node.readonly) { - modifiers.add('readonly'); - } - if (node.static) { - modifiers.add('static'); - } - if (node.type === AST_NODE_TYPES.TSAbstractClassProperty) { - modifiers.add('abstract'); - } - - handleProperty(node, modifiers); + const modifiers = getMemberModifiers(node); + handleMember(validators.property, node, modifiers); }, 'TSPropertySignature[computed = false]'( @@ -437,7 +430,7 @@ export default util.createRule({ modifiers.add('readonly'); } - handleProperty(node, modifiers); + handleMember(validators.property, node, modifiers); }, // #endregion property @@ -453,7 +446,7 @@ export default util.createRule({ node: TSESTree.Property | TSESTree.TSMethodSignature, ): void { const modifiers = new Set(['public']); - handleMethod(node, modifiers); + handleMember(validators.method, node, modifiers); }, [[ @@ -472,26 +465,31 @@ export default util.createRule({ | TSESTree.MethodDefinition | TSESTree.TSAbstractMethodDefinition, ): void { - const modifiers = new Set(); - if (node.accessibility) { - modifiers.add(node.accessibility); - } else { - modifiers.add('public'); - } - if (node.static) { - modifiers.add('static'); - } - if ( - node.type === AST_NODE_TYPES.TSAbstractClassProperty || - node.type === AST_NODE_TYPES.TSAbstractMethodDefinition - ) { - modifiers.add('abstract'); - } - - handleMethod(node, modifiers); + const modifiers = getMemberModifiers(node); + handleMember(validators.method, node, modifiers); }, // #endregion method + + // #region accessor + + [[ + 'Property[computed = false][kind = "get"]', + 'Property[computed = false][kind = "set"]', + ].join(', ')](node: TSESTree.Property): void { + const modifiers = new Set(['public']); + handleMember(validators.accessor, node, modifiers); + }, + + [[ + 'MethodDefinition[computed = false][kind = "get"]', + 'MethodDefinition[computed = false][kind = "set"]', + ].join(', ')](node: TSESTree.MethodDefinition): void { + const modifiers = getMemberModifiers(node); + handleMember(validators.accessor, node, modifiers); + }, + + // #endregion accessor }; }, }); @@ -547,14 +545,11 @@ function getIdentifiersFromPattern( } } -type ParsedOptions = Record< - NonDefaultSelectors, - | null - | (( - node: TSESTree.Identifier | TSESTree.Literal, - modifiers?: Set, - ) => void) ->; +type ValidatiorFunction = ( + node: TSESTree.Identifier | TSESTree.Literal, + modifiers?: Set, +) => void; +type ParsedOptions = Record; type Context = TSESLint.RuleContext; type Config = NormalizedSelector; function parseOptions(context: Context): ParsedOptions { diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index d74e5472cad6..3548e0e9dc50 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -490,6 +490,30 @@ const cases: Cases = [ }, }, // #endregion method + + // #region accessor + { + code: [ + 'const ignored = { get %() {} };', + 'const ignored = { set "%"(ignored) {} };', + 'class Ignored { private get %() {} }', + 'class Ignored { private set "%"(ignored) {} }', + 'class Ignored { private static get %() {} }', + ], + options: { + selector: 'accessor', + }, + }, + { + code: [ + 'class Ignored { private static get %() {}; get ignoredDueToModifiers() {}; }', + ], + options: { + selector: 'accessor', + modifiers: ['private', 'static'], + }, + }, + // #endregion accessor ]; ruleTester.run('naming-convention', rule, { From d8967e1903b80deaa059d9c6b9d80c6905426ab9 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 9 Dec 2019 23:22:17 +1030 Subject: [PATCH 04/25] feat: enumMember --- .../src/rules/naming-convention.ts | 19 +++++++++++++++++++ .../tests/rules/naming-convention.test.ts | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 66524bc58295..69a5fd9d9ed0 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -490,6 +490,25 @@ export default util.createRule({ }, // #endregion accessor + + // #region enumMember + + TSEnumMember(node): void { + const validator = validators.enumMember; + if (!validator) { + return; + } + + const id = node.id; + /* istanbul ignore if */ if (!util.isLiteralOrIdentifier(id)) { + // shouldn't happen in reality because it's not semantically valid code + return; + } + + validator(id); + }, + + // #endregion enumMember }; }, }); diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index 3548e0e9dc50..bd3e08230922 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -514,6 +514,15 @@ const cases: Cases = [ }, }, // #endregion accessor + + // #region enumMember + { + code: ['enum Ignored { % }', 'enum Ignored { "%" }'], + options: { + selector: 'enumMember', + }, + }, + // #endregion enumMember ]; ruleTester.run('naming-convention', rule, { From 3a431a292fabfbbba6b124bdae89a0938b8b9052 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 9 Dec 2019 23:52:46 +1030 Subject: [PATCH 05/25] feat: config ordering --- .../src/rules/naming-convention.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 69a5fd9d9ed0..ce9285e608d7 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -66,6 +66,8 @@ type Selector = FormatOptions & type NormalizedSelector = FormatOptions & SelectorBase & { filter: RegExp | null; + // calculated ordering weight based on modifiers + weight: number; }; // Note that this intentionally does not strictly type the modifiers/types properties. @@ -617,6 +619,9 @@ function createValidator( context: Context, configs: Config[], ): (node: TSESTree.Identifier | TSESTree.Literal) => void { + // make sure the "highest priority" configs are checked first + configs = [...configs].sort((a, b) => b.weight - a.weight); + return ( node: TSESTree.Identifier | TSESTree.Literal, modifiers: Set = new Set(), @@ -663,7 +668,8 @@ function createValidator( return; } - // it's valid for this config! + // it's valid for this config, so we don't need to check any more configs + return; } }; @@ -946,12 +952,41 @@ function selectorTypeToMessageString(selectorType: Selectors): string { return notCamelCase.charAt(0).toUpperCase() + notCamelCase.slice(1); } +const ModifierWeight = ((): Readonly< + Record +> => { + let i = 0; + return { + // Modifiers + readonly: 1 << i++, + static: 1 << i++, + public: 1 << i++, + protected: 1 << i++, + private: 1 << i++, + abstract: 1 << i++, + // TypeModifiers + boolean: 1 << i++, + string: 1 << i++, + number: 1 << i++, + function: 1 << i++, + array: 1 << i++, + }; +})(); function normalizeOption( option: Selector, ): NormalizedSelector { + let weight = 0; + option.modifiers?.forEach(mod => { + weight |= ModifierWeight[mod]; + }); + option.types?.forEach(mod => { + weight |= ModifierWeight[mod]; + }); + return { ...option, filter: option.filter !== undefined ? new RegExp(option.filter) : null, + weight, }; } From 23566431aa65b0ae1b1a64f437e6e3b9eb1058d1 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 10 Dec 2019 16:48:20 +1030 Subject: [PATCH 06/25] feat: meta selectors --- .../src/rules/naming-convention.ts | 470 ++++++++++-------- packages/eslint-plugin/src/util/misc.ts | 5 + .../tests/rules/naming-convention.test.ts | 22 +- 3 files changed, 284 insertions(+), 213 deletions(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index ce9285e608d7..721d41cc1d58 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -14,67 +14,129 @@ type MessageIds = // #region Options Type Config -type PredefinedFormats = - | 'camelCase' - | 'strictCamelCase' - | 'PascalCase' - | 'StrictPascalCase' - | 'UPPER_CASE' - | 'snake_case'; -type UnderscroreOptions = 'forbid' | 'allow' | 'require'; -interface FormatOptions { - leadingUnderscore?: UnderscroreOptions; - trailingUnderscore?: UnderscroreOptions; - prefix?: string[]; - suffix?: string[]; - format: PredefinedFormats[]; +enum PredefinedFormats { + camelCase = 1 << 0, + strictCamelCase = 1 << 1, + PascalCase = 1 << 2, + StrictPascalCase = 1 << 3, + UPPER_CASE = 1 << 4, + // eslint-disable-next-line @typescript-eslint/camelcase + snake_case = 1 << 5, } +type PredefinedFormatsString = keyof typeof PredefinedFormats; -type NonDefaultSelectors = - | 'variable' - | 'function' - | 'parameter' - | 'property' - | 'parameterProperty' - | 'enumMember' - | 'method' - | 'accessor' - | 'class' - | 'interface' - | 'typeAlias' - | 'typeParameter' - | 'enum'; -type Selectors = 'default' | NonDefaultSelectors; -type Modifiers = - | 'readonly' - | 'static' - | 'public' - | 'protected' - | 'private' - | 'abstract'; -type TypeModifiers = 'boolean' | 'string' | 'number' | 'function' | 'array'; - -interface SelectorBase { +enum UnderscroreOptions { + forbid = 1 << 0, + allow = 1 << 1, + require = 1 << 2, +} +type UnderscroreOptionsString = keyof typeof UnderscroreOptions; + +enum Selectors { + // variableLike + variable = 1 << 0, + function = 1 << 1, + parameter = 1 << 2, + + // memberLike + property = 1 << 3, + parameterProperty = 1 << 4, + enumMember = 1 << 5, + method = 1 << 6, + accessor = 1 << 7, + + // typeLike + class = 1 << 8, + interface = 1 << 9, + typeAlias = 1 << 10, + enum = 1 << 11, + typeParameter = 1 << 12, +} +type SelectorsString = keyof typeof Selectors; + +enum MetaSelectors { + default = -1, + variableLike = 0 | + Selectors.variable | + Selectors.function | + Selectors.parameter, + memberLike = 0 | + Selectors.property | + Selectors.parameterProperty | + Selectors.enumMember | + Selectors.method | + Selectors.accessor, + typeLike = 0 | + Selectors.class | + Selectors.interface | + Selectors.typeAlias | + Selectors.enum | + Selectors.typeParameter, +} +type MetaSelectorsString = keyof typeof MetaSelectors; +type IndividualAndMetaSelectorsString = SelectorsString | MetaSelectorsString; + +enum Modifiers { + readonly = 1 << 0, + static = 1 << 1, + public = 1 << 2, + protected = 1 << 3, + private = 1 << 4, + abstract = 1 << 5, +} +type ModifiersString = keyof typeof Modifiers; + +enum TypeModifiers { + boolean = 1 << 10, + string = 1 << 11, + number = 1 << 12, + function = 1 << 13, + array = 1 << 14, +} +type TypeModifiersString = keyof typeof TypeModifiers; + +interface Selector< + TType extends IndividualAndMetaSelectorsString = IndividualAndMetaSelectorsString +> { + // format options + leadingUnderscore?: UnderscroreOptionsString; + trailingUnderscore?: UnderscroreOptionsString; + prefix?: string[]; + suffix?: string[]; + format: PredefinedFormatsString[]; + // selector options selector: TType; - modifiers?: Modifiers[]; - types?: TypeModifiers[]; + modifiers?: ModifiersString[]; + types?: TypeModifiersString[]; + filter?: string; +} +interface NormalizedSelector { + // format options + leadingUnderscore: UnderscroreOptions | null; + trailingUnderscore: UnderscroreOptions | null; + prefix: string[] | null; + suffix: string[] | null; + format: PredefinedFormats[]; + // selector options + selector: Selectors | MetaSelectors; + modifiers: Modifiers[] | null; + types: TypeModifiers[] | null; + filter: RegExp | null; + // calculated ordering weight based on modifiers + modifierWeight: number; } -type Selector = FormatOptions & - SelectorBase & { - filter?: string; - }; -type NormalizedSelector = FormatOptions & - SelectorBase & { - filter: RegExp | null; - // calculated ordering weight based on modifiers - weight: number; - }; // Note that this intentionally does not strictly type the modifiers/types properties. // This is because doing so creates a huge headache, as the rule's code doesn't need to care. // The JSON Schema strictly types these properties, so we know the user won't input invalid config. type Options = ( - | Selector<'default'> + | // meta selectors + Selector<'default'> + | Selector<'variableLike'> + | Selector<'memberLike'> + | Selector<'typeLike'> + + // individual selectors | Selector<'variable'> | Selector<'function'> | Selector<'parameter'> @@ -86,8 +148,8 @@ type Options = ( | Selector<'class'> | Selector<'interface'> | Selector<'typeAlias'> - | Selector<'typeParameter'> | Selector<'enum'> + | Selector<'typeParameter'> )[]; // #endregion Options Type Config @@ -96,7 +158,7 @@ type Options = ( const UNDERSCORE_SCHEMA: JSONSchema.JSONSchema4 = { type: 'string', - enum: ['forbid', 'allow', 'require'], + enum: util.getEnumNames(UnderscroreOptions), }; const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { type: 'array', @@ -117,40 +179,30 @@ const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { type: 'array', items: { type: 'string', - enum: [ - 'camelCase', - 'strictCamelCase', - 'PascalCase', - 'StrictPascalCase', - 'UPPER_CASE', - 'snake_case', - ], + enum: util.getEnumNames(PredefinedFormats), }, minItems: 1, additionalItems: false, }, }; -const SELECTOR_BASE: JSONSchemaProperties = { - filter: { - type: 'string', - minLength: 1, - }, -}; const TYPE_MODIFIERS_SCHEMA: JSONSchema.JSONSchema4 = { type: 'array', items: { type: 'string', - enum: ['boolean', 'string', 'number', 'function', 'array'], + enum: util.getEnumNames(TypeModifiers), }, additionalItems: false, }; function selectorSchema( - type: Selectors, + type: IndividualAndMetaSelectorsString, types: boolean, - modifiers?: Modifiers[], + modifiers?: ModifiersString[], ): JSONSchema.JSONSchema4[] { const selector: JSONSchemaProperties = { - ...SELECTOR_BASE, + filter: { + type: 'string', + minLength: 1, + }, selector: { type: 'string', enum: [type], @@ -187,10 +239,21 @@ const SCHEMA: JSONSchema.JSONSchema4 = { minItems: 1, items: { oneOf: [ - ...selectorSchema('default', false), + ...selectorSchema('default', false, util.getEnumNames(Modifiers)), + + ...selectorSchema('variableLike', false), ...selectorSchema('variable', true), ...selectorSchema('function', false), ...selectorSchema('parameter', true), + + ...selectorSchema('memberLike', false, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), ...selectorSchema('property', true, [ 'private', 'protected', @@ -220,11 +283,13 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'abstract', ]), ...selectorSchema('enumMember', false), + + ...selectorSchema('typeLike', false, ['abstract']), ...selectorSchema('class', false, ['abstract']), ...selectorSchema('interface', false), ...selectorSchema('typeAlias', false), - ...selectorSchema('typeParameter', false), ...selectorSchema('enum', false), + ...selectorSchema('typeParameter', false), ], }, additionalItems: false, @@ -291,21 +356,21 @@ export default util.createRule({ ): Set { const modifiers = new Set(); if (node.accessibility) { - modifiers.add(node.accessibility); + modifiers.add(Modifiers[node.accessibility]); } else { - modifiers.add('public'); + modifiers.add(Modifiers.public); } if (node.static) { - modifiers.add('static'); + modifiers.add(Modifiers.static); } if ('readonly' in node && node.readonly) { - modifiers.add('readonly'); + modifiers.add(Modifiers.readonly); } if ( node.type === AST_NODE_TYPES.TSAbstractClassProperty || node.type === AST_NODE_TYPES.TSAbstractMethodDefinition ) { - modifiers.add('abstract'); + modifiers.add(Modifiers.abstract); } return modifiers; @@ -387,12 +452,12 @@ export default util.createRule({ const modifiers = new Set(); if (node.accessibility !== undefined) { - modifiers.add(node.accessibility); + modifiers.add(Modifiers[node.accessibility]); } else { - modifiers.add('public'); + modifiers.add(Modifiers.public); } if (node.readonly) { - modifiers.add('readonly'); + modifiers.add(Modifiers.readonly); } const identifiers: TSESTree.Identifier[] = []; @@ -410,7 +475,7 @@ export default util.createRule({ 'Property[computed = false][kind = "init"][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( node: TSESTree.Property, ): void { - const modifiers = new Set(['public']); + const modifiers = new Set([Modifiers.public]); handleMember(validators.property, node, modifiers); }, @@ -427,9 +492,9 @@ export default util.createRule({ 'TSPropertySignature[computed = false]'( node: TSESTree.TSPropertySignature, ): void { - const modifiers = new Set(['public']); + const modifiers = new Set([Modifiers.public]); if (node.readonly) { - modifiers.add('readonly'); + modifiers.add(Modifiers.readonly); } handleMember(validators.property, node, modifiers); @@ -447,7 +512,7 @@ export default util.createRule({ ].join(', ')]( node: TSESTree.Property | TSESTree.TSMethodSignature, ): void { - const modifiers = new Set(['public']); + const modifiers = new Set([Modifiers.public]); handleMember(validators.method, node, modifiers); }, @@ -479,7 +544,7 @@ export default util.createRule({ 'Property[computed = false][kind = "get"]', 'Property[computed = false][kind = "set"]', ].join(', ')](node: TSESTree.Property): void { - const modifiers = new Set(['public']); + const modifiers = new Set([Modifiers.public]); handleMember(validators.accessor, node, modifiers); }, @@ -570,67 +635,50 @@ type ValidatiorFunction = ( node: TSESTree.Identifier | TSESTree.Literal, modifiers?: Set, ) => void; -type ParsedOptions = Record; +type ParsedOptions = Record; type Context = TSESLint.RuleContext; -type Config = NormalizedSelector; function parseOptions(context: Context): ParsedOptions { - const groupedOptions = new Map(); - const defaultConfig: NormalizedSelector<'default'>[] = []; - context.options.forEach(option => { - const normalized = normalizeOption(option); - if (option.selector === 'default') { - defaultConfig.push(normalized as NormalizedSelector<'default'>); - } else { - const selectors = groupedOptions.get(option.selector) ?? []; - selectors.push(normalized); - groupedOptions.set(option.selector, selectors); - } - }); - - const parsedOptions: ParsedOptions = { - variable: null, - function: null, - parameter: null, - property: null, - parameterProperty: null, - enumMember: null, - method: null, - accessor: null, - class: null, - interface: null, - typeAlias: null, - typeParameter: null, - enum: null, - }; - const selectorTypes = Object.keys(parsedOptions) as NonDefaultSelectors[]; - selectorTypes.forEach(type => { - const validators = groupedOptions.get(type); - if (validators) { - parsedOptions[type] = createValidator(type, context, validators); - } else if (defaultConfig.length > 0) { - parsedOptions[type] = createValidator(type, context, defaultConfig); - } - }); + const normalizedOptions = context.options.map(opt => normalizeOption(opt)); + const parsedOptions = util.getEnumNames(Selectors).reduce((acc, k) => { + acc[k] = createValidator(k, context, normalizedOptions); + return acc; + }, {} as ParsedOptions); return parsedOptions; } function createValidator( - type: Selectors, + type: SelectorsString, context: Context, - configs: Config[], + allConfigs: NormalizedSelector[], ): (node: TSESTree.Identifier | TSESTree.Literal) => void { // make sure the "highest priority" configs are checked first - configs = [...configs].sort((a, b) => b.weight - a.weight); + const selectorType = Selectors[type]; + const configs = allConfigs + // gather all of the applicable selectors + .filter( + c => + (c.selector & selectorType) !== 0 || + c.selector === MetaSelectors.default, + ) + .sort((a, b) => { + if (a.selector === b.selector) { + // in the event of the same selector, order by modifier collection + return b.modifierWeight - a.modifierWeight; + } + + // check the meta selectors last + return b.selector - a.selector; + }); return ( node: TSESTree.Identifier | TSESTree.Literal, modifiers: Set = new Set(), ): void => { - // return will break the loop and stop checking configs - // it is only used when the name is known to have failed a config. const originalName = node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; + // return will break the loop and stop checking configs + // it is only used when the name is known to have failed a config. for (const config of configs) { if (config.filter?.test(originalName)) { // name does not match the filter @@ -673,12 +721,33 @@ function createValidator( } }; + // centralises the logic for formatting the report data + function formatReportData({ + affixes, + formats, + originalName, + position, + }: { + affixes?: string[]; + formats?: PredefinedFormats[]; + originalName: string; + position?: 'leading' | 'trailing' | 'prefix' | 'suffix'; + }): Record { + return { + type: selectorTypeToMessageString(type), + name: originalName, + position, + affixes: affixes?.join(', '), + formats: formats?.map(f => PredefinedFormats[f]).join(', '), + }; + } + /** * @returns the name with the underscore removed, if it is valid according to the specified underscore option, null otherwise */ function validateUnderscore( position: 'leading' | 'trailing', - config: Config, + config: NormalizedSelector, name: string, node: TSESTree.Identifier | TSESTree.Literal, originalName: string, @@ -699,35 +768,33 @@ function createValidator( : (): string => name.slice(0, -1); switch (option) { - case 'allow': + case UnderscroreOptions.allow: // no check - the user doesn't care if it's there or not break; - case 'forbid': + case UnderscroreOptions.forbid: if (hasUnderscore) { context.report({ node, messageId: 'unexpectedUnderscore', - data: { - type: selectorTypeToMessageString(type), - name: originalName, + data: formatReportData({ + originalName, position, - }, + }), }); return null; } break; - case 'require': + case UnderscroreOptions.require: if (!hasUnderscore) { context.report({ node, messageId: 'missingUnderscore', - data: { - type: selectorTypeToMessageString(type), - name: originalName, + data: formatReportData({ + originalName, position, - }, + }), }); return null; } @@ -741,23 +808,23 @@ function createValidator( */ function validateAffix( position: 'prefix' | 'suffix', - config: Config, + config: NormalizedSelector, name: string, node: TSESTree.Identifier | TSESTree.Literal, originalName: string, ): string | null { - const options = config[position]; - if (!options || options.length === 0) { + const affixes = config[position]; + if (!affixes || affixes.length === 0) { return name; } - for (const option of options) { + for (const affix of affixes) { const hasAffix = - position === 'prefix' ? name.startsWith(option) : name.endsWith(option); + position === 'prefix' ? name.startsWith(affix) : name.endsWith(affix); const trimAffix = position === 'prefix' - ? (): string => name.slice(option.length) - : (): string => name.slice(0, -option.length); + ? (): string => name.slice(affix.length) + : (): string => name.slice(0, -affix.length); if (hasAffix) { // matches, so trim it and return @@ -768,12 +835,11 @@ function createValidator( context.report({ node, messageId: 'missingAffix', - data: { - type: selectorTypeToMessageString(type), - name: originalName, + data: formatReportData({ + originalName, position, - affixes: options.join(', '), - }, + affixes, + }), }); return null; } @@ -782,48 +848,49 @@ function createValidator( * @returns true if the name is valid according to the `format` option, false otherwise */ function validatePredefinedFormat( - config: Config, + config: NormalizedSelector, name: string, node: TSESTree.Identifier | TSESTree.Literal, originalName: string, ): boolean { - if (config.format.length === 0) { + const formats = config.format; + if (formats.length === 0) { return true; } - for (const format of config.format) { + for (const format of formats) { switch (format) { - case 'PascalCase': + case PredefinedFormats.PascalCase: if (isPascalCase(name)) { return true; } break; - case 'StrictPascalCase': + case PredefinedFormats.StrictPascalCase: if (isStrictPascalCase(name)) { return true; } break; - case 'camelCase': + case PredefinedFormats.camelCase: if (isCamelCase(name)) { return true; } break; - case 'strictCamelCase': + case PredefinedFormats.strictCamelCase: if (isStrictCamelCase(name)) { return true; } break; - case 'UPPER_CASE': + case PredefinedFormats.UPPER_CASE: if (isUpperCase(name)) { return true; } break; - case 'snake_case': + case PredefinedFormats.snake_case: if (isSnakeCase(name)) { return true; } @@ -834,11 +901,10 @@ function createValidator( context.report({ node, messageId: 'doesNotMatchFormat', - data: { - type: selectorTypeToMessageString(type), - name: originalName, - formats: config.format.join(', '), - }, + data: formatReportData({ + originalName, + formats, + }), }); return false; } @@ -947,58 +1013,56 @@ function validateUnderscores(name: string): boolean { } // #endregion Predefined Format Functions -function selectorTypeToMessageString(selectorType: Selectors): string { +function selectorTypeToMessageString(selectorType: SelectorsString): string { const notCamelCase = selectorType.replace(/([A-Z])/g, ' $1'); return notCamelCase.charAt(0).toUpperCase() + notCamelCase.slice(1); } -const ModifierWeight = ((): Readonly< - Record -> => { - let i = 0; - return { - // Modifiers - readonly: 1 << i++, - static: 1 << i++, - public: 1 << i++, - protected: 1 << i++, - private: 1 << i++, - abstract: 1 << i++, - // TypeModifiers - boolean: 1 << i++, - string: 1 << i++, - number: 1 << i++, - function: 1 << i++, - array: 1 << i++, - }; -})(); -function normalizeOption( - option: Selector, -): NormalizedSelector { +function isMetaSelector( + selector: IndividualAndMetaSelectorsString, +): selector is MetaSelectorsString { + return selector in MetaSelectors; +} +function normalizeOption( + option: Selector, +): NormalizedSelector { let weight = 0; option.modifiers?.forEach(mod => { - weight |= ModifierWeight[mod]; + weight |= Modifiers[mod]; }); option.types?.forEach(mod => { - weight |= ModifierWeight[mod]; + weight |= TypeModifiers[mod]; }); return { - ...option, + // format options + leadingUnderscore: + option.leadingUnderscore !== undefined + ? UnderscroreOptions[option.leadingUnderscore] + : null, + trailingUnderscore: + option.trailingUnderscore !== undefined + ? UnderscroreOptions[option.trailingUnderscore] + : null, + prefix: option.prefix ?? null, + suffix: option.suffix ?? null, + format: option.format.map(f => PredefinedFormats[f]), + // selector options + selector: isMetaSelector(option.selector) + ? MetaSelectors[option.selector] + : Selectors[option.selector], + modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null, + types: option.types?.map(t => TypeModifiers[t]) ?? null, filter: option.filter !== undefined ? new RegExp(option.filter) : null, - weight, + // calculated ordering weight based on modifiers + modifierWeight: weight, }; } export { MessageIds, - Modifiers, - NonDefaultSelectors, - NormalizedSelector, Options, - PredefinedFormats, + PredefinedFormatsString, Selector, - Selectors, selectorTypeToMessageString, - TypeModifiers, }; diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 74bc37fabbb6..cd328ce69d12 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -139,11 +139,16 @@ type RequireKeys< TKeys extends keyof TObj > = ExcludeKeys & { [k in TKeys]-?: Exclude }; +function getEnumNames(myEnum: Record): T[] { + return Object.keys(myEnum).filter(x => isNaN(parseInt(x))) as T[]; +} + export { arraysAreEqual, Equal, ExcludeKeys, findFirstResult, + getEnumNames, getNameFromMember, InferMessageIdsTypeFromRule, InferOptionsTypeFromRule, diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index bd3e08230922..c27b32a92801 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -2,9 +2,8 @@ import { TSESLint } from '@typescript-eslint/experimental-utils'; import rule, { MessageIds, Options, - PredefinedFormats, + PredefinedFormatsString, Selector, - Selectors, selectorTypeToMessageString, } from '../../src/rules/naming-convention'; import { RuleTester } from '../RuleTester'; @@ -14,7 +13,7 @@ const ruleTester = new RuleTester({ }); const formatTestNames: Readonly >> = { camelCase: { @@ -84,11 +83,11 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { for (const test of cases) { for (const [formatLoose, names] of Object.entries(formatTestNames)) { - const format = [formatLoose as PredefinedFormats]; + const format = [formatLoose as PredefinedFormatsString]; for (const name of names.valid) { const createCase = ( preparedName: string, - options: Selector, + options: Selector, ): TSESLint.ValidTestCase => ({ options: [ { @@ -188,17 +187,17 @@ function createInvalidTestCases( for (const test of cases) { for (const [formatLoose, names] of Object.entries(formatTestNames)) { - const format = [formatLoose as PredefinedFormats]; + const format = [formatLoose as PredefinedFormatsString]; for (const name of names.invalid) { const createCase = ( preparedName: string, - options: Selector, + options: Selector, messageId: MessageIds, data: Record = {}, ): TSESLint.InvalidTestCase => ({ options: [ { - ...(options as Options[0]), + ...options, filter: '[iI]gnored', }, ], @@ -207,7 +206,10 @@ function createInvalidTestCases( .join('\n')}`, errors: test.code.map(() => ({ messageId, - ...(test.options.selector !== 'default' + ...(test.options.selector !== 'default' && + test.options.selector !== 'variableLike' && + test.options.selector !== 'memberLike' && + test.options.selector !== 'typeLike' ? { data: { type: selectorTypeToMessageString(test.options.selector), @@ -215,7 +217,7 @@ function createInvalidTestCases( ...data, }, } - : // default will use the correct selector, so don't assert on data + : // meta-types will use the correct selector, so don't assert on data shape {}), })), }); From 284fe4cc1e282ea42725fac8502c7a7f83de7a5d Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 10 Dec 2019 19:07:24 +1030 Subject: [PATCH 07/25] docs: add docs --- packages/eslint-plugin/README.md | 7 +- .../docs/rules/naming-convention.md | 184 ++++++++++++++++++ packages/eslint-plugin/src/configs/all.json | 6 +- packages/eslint-plugin/src/rules/camelcase.ts | 2 + .../src/rules/class-name-casing.ts | 2 + .../src/rules/generic-type-naming.ts | 2 + packages/eslint-plugin/src/rules/index.ts | 2 + .../eslint-plugin/src/rules/member-naming.ts | 2 + .../src/rules/naming-convention.ts | 55 ++---- 9 files changed, 218 insertions(+), 44 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/naming-convention.md diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index e018b8a5a0e7..d374f000eac8 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -114,12 +114,13 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/member-naming`](./docs/rules/member-naming.md) | Enforces naming conventions for class members by visibility | | | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | +| [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Bans usage of the delete operator with computed key expressions | | :wrench: | | | [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | -| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | +| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | | | | +| [`@typescript-eslint/no-extra-non-null-assertion`](./docs/rules/no-extra-non-null-assertion.md) | Disallow extra non-null assertion | | | | | [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | | [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | | | [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately. | | | :thought_balloon: | @@ -160,7 +161,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: | | [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | | | :thought_balloon: | -| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Rules for awaiting returned promises | | | :thought_balloon: | +| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Rules for awaiting returned promises | | | :thought_balloon: | | [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | | [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | enforce consistent spacing before `function` definition opening parenthesis | | :wrench: | | | [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md new file mode 100644 index 000000000000..d2d98897f101 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -0,0 +1,184 @@ +# 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`. + +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. + +## Rule Details + +This rule allows you to enforce conventions for any identifier, using granular selectors to create a fine-grained style guide. +By default, it enforces nothing. + +### Note - this rule only needs type information in specfic cases, detailed below + +## Options + +This rule accepts an array of objects, with each object describing a different naming convention. +Each property will be described in detail below. Also see the examples section below for illustrated examples. + +```ts +type Options = { + // format options + format: ( + | 'camelCase' + | 'strictCamelCase' + | 'PascalCase' + | 'StrictPascalCase' + | 'snake_case' + | 'UPPER_CASE' + )[]; + leadingUnderscore?: 'forbid' | 'allow' | 'require'; + trailingUnderscore?: 'forbid' | 'allow' | 'require'; + prefix?: string[]; + suffix?: string[]; + + // selector options + selector: Selector; + modifiers?: Modifiers[]; + types?: Types[]; + filter?: string; +}[]; + +const defaultOptions: Options = []; +``` + +### Format Options + +Every single selector can have the same set of format options. +When the format of an identifier is checked, it is checked in the following order: + +1. validate leading underscore +1. validate trailing underscore +1. validate prefix +1. validate suffix +1. validate format + +At each step, if the identifier matches the options, it the matching part will be removed. +For example, if you provide the following formating option: `{ leadingUnderscore: 'allow', prefix: ['I'], format: ['StrictPascalCase'] }`, for the identifier `_IMyInterface`, then the following checks will occur: + +1. `name = _IMyInterface` +1. validate leading underscore - pass + - Trim leading underscore - `name = IMyInterface` +1. validate trailing underscore - no check +1. validate prefix - pass + - Trim prefix - `name = MyInterface` +1. validate suffix - no check +1. validate format - pass + +#### `format` + +The `format` option defines the allowed formats for the identifier. This option accepts an array of the following values, and the identifier can match any of them: + +- `camelCase` - standard camelCase format - no underscores are allowed between characters, and consecutive capitals are allowed (i.e. both `myID` and `myId` are valid). +- `strictCamelCase` - same as `camelCase`, but consecutive capitals are not allowed (i.e. `myId` is valid, but `myID` is not). +- `PascalCase` - same as `camelCase`, except the first character must be upper-case. +- `StrictPascalCase` - same as `strictCamelCase`, except the first character must be upper-case. +- `snake_case` - standard snake_case format - all characters must be lower-case, and underscores are allowed. +- `UPPER_CASE` - same as `snake_case`, except all characters must be upper-case. + +#### `leadingUnderscore` / `trailingUnderscore` + +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 underscores must be included. + +#### `prefix` / `suffix` + +The `prefix` / `suffix` options control which prefix/suffix strings must exist for the identifier. Accepts an array of strings. + +If these are provided, the identifier must start with one of the provided values. For example, if you provide `{ prefix: ['IFace', 'Class', 'Type'] }`, then the following names are valid: `IFaceFoo`, `ClassBar`, `TypeBaz`, but the name `Bang` is not valid, as it contains none of the prefixes. + +### Selector Options + +The selector options determine which names that the formatting options will apply to. +Each value for `selector` has a set of `types` and `modifiers` that are allowed to be used with it, which are explained below. + +`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. + +`types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `function`, `array`). This lets you do things like enforce that `boolean` variables are prefixed with a verb. +**_NOTE - Using this option will require that you lint with type information._** + +`filter` accepts a regular expression (anything accepted into `new RegExp(filter)`). It allows you to limit the scope of this configuration to names that match this regex. + +#### Allowed Selectors + +There are two types of selectors, individual selectors, and grouped selectors. + +Individual Selectors match specific, well-defined sets. There is no overlap between each of the individual selectors. + +- `variable` - matches any `var` / `let` / `const` variable name. + - Allowed `modifiers`: none. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `function` - matches any named function declaration or named function expression. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `parameter` - matches any function parameter. Does not match parameter properties. + - Allowed `modifiers`: none. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `property` - matches any object, class, or object type property. Does not match properties that have direct function expression or arrow function expression values. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `parameterProperty` - matches any parameter property. + - Allowed `modifiers`: `private`, `protected`, `public`, `readonly`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `method` - matches any object, class, or object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. +- `accessor` - matches any accessor. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `enumMember` - matches any enum member. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `class` - matches any class declaration. + - Allowed `modifiers`: `abstract`. + - Allowed `types`: none. +- `interface` - matches any interface declaration. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `typeAlias` - matches any type alias declaration. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `enum` - matches any enum declaration. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `typeParameter` - matches any generic type parameter declaration. + - Allowed `modifiers`: none. + - Allowed `types`: none. + +Group Selectors are provided for convenience, and essentially bundle up sets of individual selectors. + +- `default` - matches everything. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. +- `variableLike` - matches the same as `variable`, `function` and `parameter`. + - Allowed `modifiers`: none. + - Allowed `types`: none. +- `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. +- `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. + - Allowed `modifiers`: `abstract`. + - Allowed `types`: none. + +The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. + +For example, if you provide the following config: + +```ts +[ + /* 1 */ { selector: 'default', format: ['camelCase'] }, + /* 2 */ { selector: 'variable', format: ['snake_case'] }, + /* 3 */ { selector: 'variable', type: ['boolean'], format: ['UPPER_CASE'] }, + /* 4 */ { selector: 'variableLike', format: ['PascalCase'] }, +]; +``` + +Then the rule will validate the selectors in the following order: `3`, `2`, `4`, `1`. + +## When Not To Use It + +If you do not want to enforce naming conventions for anything. diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index c3ee46bc95a3..10c0767f5d37 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -8,22 +8,18 @@ "@typescript-eslint/ban-types": "error", "brace-style": "off", "@typescript-eslint/brace-style": "error", - "camelcase": "off", - "@typescript-eslint/camelcase": "error", - "@typescript-eslint/class-name-casing": "error", "@typescript-eslint/consistent-type-assertions": "error", "@typescript-eslint/consistent-type-definitions": "error", "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-member-accessibility": "error", "func-call-spacing": "off", "@typescript-eslint/func-call-spacing": "error", - "@typescript-eslint/generic-type-naming": "error", "indent": "off", "@typescript-eslint/indent": "error", "@typescript-eslint/interface-name-prefix": "error", "@typescript-eslint/member-delimiter-style": "error", - "@typescript-eslint/member-naming": "error", "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/naming-convention": "error", "no-array-constructor": "off", "@typescript-eslint/no-array-constructor": "error", "@typescript-eslint/no-dynamic-delete": "error", diff --git a/packages/eslint-plugin/src/rules/camelcase.ts b/packages/eslint-plugin/src/rules/camelcase.ts index 17b88d7e93d9..37047b9b4ff3 100644 --- a/packages/eslint-plugin/src/rules/camelcase.ts +++ b/packages/eslint-plugin/src/rules/camelcase.ts @@ -30,6 +30,8 @@ export default util.createRule({ category: 'Stylistic Issues', recommended: 'error', }, + deprecated: true, + replacedBy: ['naming-convention'], schema: [schema], messages: baseRule.meta.messages, }, diff --git a/packages/eslint-plugin/src/rules/class-name-casing.ts b/packages/eslint-plugin/src/rules/class-name-casing.ts index b27bf1ebcf14..16fe8e6bba4c 100644 --- a/packages/eslint-plugin/src/rules/class-name-casing.ts +++ b/packages/eslint-plugin/src/rules/class-name-casing.ts @@ -20,6 +20,8 @@ export default util.createRule({ category: 'Best Practices', recommended: 'error', }, + deprecated: true, + replacedBy: ['naming-convention'], messages: { notPascalCased: "{{friendlyName}} '{{name}}' must be PascalCased.", }, diff --git a/packages/eslint-plugin/src/rules/generic-type-naming.ts b/packages/eslint-plugin/src/rules/generic-type-naming.ts index 95516174448f..14a697f2e604 100644 --- a/packages/eslint-plugin/src/rules/generic-type-naming.ts +++ b/packages/eslint-plugin/src/rules/generic-type-naming.ts @@ -13,6 +13,8 @@ export default util.createRule({ // too opinionated to be recommended recommended: false, }, + deprecated: true, + replacedBy: ['naming-convention'], messages: { paramNotMatchRule: 'Type parameter {{name}} does not match rule {{rule}}.', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index a4dc193e9394..85c6d37f5781 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -17,6 +17,7 @@ import interfaceNamePrefix from './interface-name-prefix'; import memberDelimiterStyle from './member-delimiter-style'; import memberNaming from './member-naming'; import memberOrdering from './member-ordering'; +import namingConvention from './naming-convention'; import noArrayConstructor from './no-array-constructor'; import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; @@ -93,6 +94,7 @@ export default { 'member-delimiter-style': memberDelimiterStyle, 'member-naming': memberNaming, 'member-ordering': memberOrdering, + 'naming-convention': namingConvention, 'no-array-constructor': noArrayConstructor, 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, diff --git a/packages/eslint-plugin/src/rules/member-naming.ts b/packages/eslint-plugin/src/rules/member-naming.ts index f2e4567dcb40..4facce887e5f 100644 --- a/packages/eslint-plugin/src/rules/member-naming.ts +++ b/packages/eslint-plugin/src/rules/member-naming.ts @@ -23,6 +23,8 @@ export default util.createRule({ category: 'Stylistic Issues', recommended: false, }, + deprecated: true, + replacedBy: ['naming-convention'], messages: { incorrectName: '{{accessibility}} property {{name}} should match {{convention}}.', diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 721d41cc1d58..1af2b7e9ec3b 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -19,9 +19,9 @@ enum PredefinedFormats { strictCamelCase = 1 << 1, PascalCase = 1 << 2, StrictPascalCase = 1 << 3, - UPPER_CASE = 1 << 4, // eslint-disable-next-line @typescript-eslint/camelcase - snake_case = 1 << 5, + snake_case = 1 << 4, + UPPER_CASE = 1 << 5, } type PredefinedFormatsString = keyof typeof PredefinedFormats; @@ -95,17 +95,15 @@ enum TypeModifiers { } type TypeModifiersString = keyof typeof TypeModifiers; -interface Selector< - TType extends IndividualAndMetaSelectorsString = IndividualAndMetaSelectorsString -> { +interface Selector { // format options + format: PredefinedFormatsString[]; leadingUnderscore?: UnderscroreOptionsString; trailingUnderscore?: UnderscroreOptionsString; prefix?: string[]; suffix?: string[]; - format: PredefinedFormatsString[]; // selector options - selector: TType; + selector: IndividualAndMetaSelectorsString; modifiers?: ModifiersString[]; types?: TypeModifiersString[]; filter?: string; @@ -129,28 +127,7 @@ interface NormalizedSelector { // Note that this intentionally does not strictly type the modifiers/types properties. // This is because doing so creates a huge headache, as the rule's code doesn't need to care. // The JSON Schema strictly types these properties, so we know the user won't input invalid config. -type Options = ( - | // meta selectors - Selector<'default'> - | Selector<'variableLike'> - | Selector<'memberLike'> - | Selector<'typeLike'> - - // individual selectors - | Selector<'variable'> - | Selector<'function'> - | Selector<'parameter'> - | Selector<'property'> - | Selector<'parameterProperty'> - | Selector<'method'> - | Selector<'accessor'> - | Selector<'enumMember'> - | Selector<'class'> - | Selector<'interface'> - | Selector<'typeAlias'> - | Selector<'enum'> - | Selector<'typeParameter'> -)[]; +type Options = Selector[]; // #endregion Options Type Config @@ -302,8 +279,11 @@ export default util.createRule({ meta: { docs: { category: 'Variables', - description: '', + description: + 'Enforces naming conventions for everything across a codebase', recommended: false, + // technically only requires type checkin if the user uses "type" modifiers + requiresTypeChecking: true, }, type: 'suggestion', messages: { @@ -662,12 +642,13 @@ function createValidator( ) .sort((a, b) => { if (a.selector === b.selector) { - // in the event of the same selector, order by modifier collection + // in the event of the same selector, order by modifier weight + // sort ascending - the type modifiers are "more important" return b.modifierWeight - a.modifierWeight; } - // check the meta selectors last - return b.selector - a.selector; + // sort descending - the meta selectors are "least important" + return a.selector - b.selector; }); return ( @@ -1023,9 +1004,7 @@ function isMetaSelector( ): selector is MetaSelectorsString { return selector in MetaSelectors; } -function normalizeOption( - option: Selector, -): NormalizedSelector { +function normalizeOption(option: Selector): NormalizedSelector { let weight = 0; option.modifiers?.forEach(mod => { weight |= Modifiers[mod]; @@ -1033,6 +1012,10 @@ function normalizeOption( option.types?.forEach(mod => { weight |= TypeModifiers[mod]; }); + // give selectors with a filter the _highest_ priority + if (option.filter) { + weight |= 1 << 30; + } return { // format options From 910acec5e29469b4d2c99621c068a824cdbb9b9d Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 11 Dec 2019 10:58:08 +1030 Subject: [PATCH 08/25] docs: typos and restructure a bit --- .../docs/rules/naming-convention.md | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index d2d98897f101..ac5dec959bd2 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -10,7 +10,7 @@ There are many different rules that have existed over time, but they have had th This rule allows you to enforce conventions for any identifier, using granular selectors to create a fine-grained style guide. By default, it enforces nothing. -### Note - this rule only needs type information in specfic cases, detailed below +### Note - this rule only needs type information in specific cases, detailed below ## Options @@ -35,9 +35,10 @@ type Options = { // selector options selector: Selector; + filter?: string; + // the allowed values for these are dependent on the selector - see below modifiers?: Modifiers[]; types?: Types[]; - filter?: string; }[]; const defaultOptions: Options = []; @@ -54,7 +55,7 @@ When the format of an identifier is checked, it is checked in the following orde 1. validate suffix 1. validate format -At each step, if the identifier matches the options, it the matching part will be removed. +At each step, if the identifier matches the option, the matching part will be removed. For example, if you provide the following formating option: `{ leadingUnderscore: 'allow', prefix: ['I'], format: ['StrictPascalCase'] }`, for the identifier `_IMyInterface`, then the following checks will occur: 1. `name = _IMyInterface` @@ -66,6 +67,8 @@ For example, if you provide the following formating option: `{ leadingUnderscore 1. validate suffix - no check 1. validate format - pass +One final note is that if the name were to become empty via this trimming process, it is considered to match all `format`s. An example of where this might be useful is for generic type parameters, where you want all names to be prefixed with `T`, but also want to allow for the single character `T` name. + #### `format` The `format` option defines the allowed formats for the identifier. This option accepts an array of the following values, and the identifier can match any of them: @@ -83,7 +86,7 @@ The `leadingUnderscore` / `trailingUnderscore` options control whether leading/t - `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 underscores must be included. +- `require` - a leading/trailing underscore must be included. #### `prefix` / `suffix` @@ -93,20 +96,40 @@ If these are provided, the identifier must start with one of the provided values ### Selector Options -The selector options determine which names that the formatting options will apply to. -Each value for `selector` has a set of `types` and `modifiers` that are allowed to be used with it, which are explained below. +- `selector` (see "Allowed Selectors, Modifiers and Types" below). +- `filter` accepts a regular expression (anything accepted into `new RegExp(filter)`). It allows you to limit the scope of this configuration to names that match this regex. +- `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. +- `types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `array`, `function`). This lets you do things like enforce that `boolean` variables are prefixed with a verb. + - **_NOTE - Using this option will require that you lint with type information._** + - `boolean` matches any type assignable to `boolean | null | undefined` + - `string` matches any type assignable to `string | null | undefined` + - `number` matches any type assignable to `number | null | undefined` + - `array` matches any type assignable to `Array | null | undefined` + - `function` matches any type asignable to `Function | null | undefined` -`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. +The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. -`types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `function`, `array`). This lets you do things like enforce that `boolean` variables are prefixed with a verb. -**_NOTE - Using this option will require that you lint with type information._** +For example, if you provide the following config: -`filter` accepts a regular expression (anything accepted into `new RegExp(filter)`). It allows you to limit the scope of this configuration to names that match this regex. +```ts +[ + /* 1 */ { selector: 'default', format: ['camelCase'] }, + /* 2 */ { selector: 'variable', format: ['snake_case'] }, + /* 3 */ { selector: 'variable', type: ['boolean'], format: ['UPPER_CASE'] }, + /* 4 */ { selector: 'variableLike', format: ['PascalCase'] }, +]; +``` + +Then for the code `const x = 1`, the rule will validate the selectors in the following order: `3`, `2`, `4`, `1`. -#### Allowed Selectors +#### Allowed Selectors, Modifiers and Types There are two types of selectors, individual selectors, and grouped selectors. +##### Individual Selectors + Individual Selectors match specific, well-defined sets. There is no overlap between each of the individual selectors. - `variable` - matches any `var` / `let` / `const` variable name. @@ -149,6 +172,8 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - Allowed `modifiers`: none. - Allowed `types`: none. +##### Group Selectors + Group Selectors are provided for convenience, and essentially bundle up sets of individual selectors. - `default` - matches everything. @@ -164,21 +189,6 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - Allowed `modifiers`: `abstract`. - Allowed `types`: none. -The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. - -For example, if you provide the following config: - -```ts -[ - /* 1 */ { selector: 'default', format: ['camelCase'] }, - /* 2 */ { selector: 'variable', format: ['snake_case'] }, - /* 3 */ { selector: 'variable', type: ['boolean'], format: ['UPPER_CASE'] }, - /* 4 */ { selector: 'variableLike', format: ['PascalCase'] }, -]; -``` - -Then the rule will validate the selectors in the following order: `3`, `2`, `4`, `1`. - ## When Not To Use It If you do not want to enforce naming conventions for anything. From 1db8b8cc602a7e8976c4ecb3ad7ade17ffb9eb0d Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 11 Dec 2019 12:19:03 +1030 Subject: [PATCH 09/25] feat: support simple primitive types --- .../src/rules/naming-convention.ts | 80 ++++++++++--- .../tests/rules/naming-convention.test.ts | 106 +++++++++++++++++- 2 files changed, 168 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 1af2b7e9ec3b..fd7c38438700 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -162,17 +162,9 @@ const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { additionalItems: false, }, }; -const TYPE_MODIFIERS_SCHEMA: JSONSchema.JSONSchema4 = { - type: 'array', - items: { - type: 'string', - enum: util.getEnumNames(TypeModifiers), - }, - additionalItems: false, -}; function selectorSchema( - type: IndividualAndMetaSelectorsString, - types: boolean, + selectorString: IndividualAndMetaSelectorsString, + allowType: boolean, modifiers?: ModifiersString[], ): JSONSchema.JSONSchema4[] { const selector: JSONSchemaProperties = { @@ -182,7 +174,7 @@ function selectorSchema( }, selector: { type: 'string', - enum: [type], + enum: [selectorString], }, }; if (modifiers && modifiers.length > 0) { @@ -195,8 +187,15 @@ function selectorSchema( additionalItems: false, }; } - if (types) { - selector.types = TYPE_MODIFIERS_SCHEMA; + if (allowType) { + selector.types = { + type: 'array', + items: { + type: 'string', + enum: util.getEnumNames(TypeModifiers), + }, + additionalItems: false, + }; } return [ @@ -671,6 +670,11 @@ function createValidator( continue; } + if (!isCorrectType(node, config, context)) { + // is not the correct type + continue; + } + let name: string | null = originalName; name = validateUnderscore('leading', config, name, node, originalName); @@ -1012,6 +1016,7 @@ function normalizeOption(option: Selector): NormalizedSelector { option.types?.forEach(mod => { weight |= TypeModifiers[mod]; }); + // give selectors with a filter the _highest_ priority if (option.filter) { weight |= 1 << 30; @@ -1035,13 +1040,60 @@ function normalizeOption(option: Selector): NormalizedSelector { ? MetaSelectors[option.selector] : Selectors[option.selector], modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null, - types: option.types?.map(t => TypeModifiers[t]) ?? null, + types: option.types?.map(m => TypeModifiers[m]) ?? null, filter: option.filter !== undefined ? new RegExp(option.filter) : null, // calculated ordering weight based on modifiers modifierWeight: weight, }; } +function isCorrectType( + node: TSESTree.Node, + config: NormalizedSelector, + context: Context, +): boolean { + if (config.types === null) { + return true; + } + + const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context); + const checker = program.getTypeChecker(); + const tsNode = esTreeNodeToTSNodeMap.get(node); + const type = checker.getTypeAtLocation(tsNode); + const typeString = checker.typeToString( + // this will resolve things like true => boolean, 'a' => string and 1 => number + checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), + ); + + for (const allowedType of config.types) { + switch (allowedType) { + case TypeModifiers.array: + // TODO + break; + + case TypeModifiers.function: + // TODO + break; + + case TypeModifiers.boolean: + case TypeModifiers.number: + case TypeModifiers.string: { + const allowedTypeString = TypeModifiers[allowedType]; + if ( + typeString === `${allowedTypeString}` || + typeString === `${allowedTypeString} | null` || + typeString === `${allowedTypeString} | null | undefined` + ) { + return true; + } + break; + } + } + } + + return false; +} + export { MessageIds, Options, diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index c27b32a92801..8d1ee982d37e 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -6,12 +6,19 @@ import rule, { Selector, selectorTypeToMessageString, } from '../../src/rules/naming-convention'; -import { RuleTester } from '../RuleTester'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', }); +// only need parserOptions for the `type` option tests +const rootDir = getFixturesRootDir(); +const parserOptions = { + tsconfigRootDir: rootDir, + project: './tsconfig.json', +}; + const formatTestNames: Readonly @@ -91,7 +98,7 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { ): TSESLint.ValidTestCase => ({ options: [ { - ...(options as Options[0]), + ...options, filter: '[iI]gnored', }, ], @@ -528,6 +535,97 @@ const cases: Cases = [ ]; ruleTester.run('naming-convention', rule, { - valid: createValidTestCases(cases), - invalid: createInvalidTestCases(cases), + valid: [ + ...createValidTestCases(cases), + { + code: ` + declare const string_camelCase: string; + declare const string_camelCase: string | null; + declare const string_camelCase: string | null | undefined; + declare const string_camelCase: 'a' | null | undefined; + declare const string_camelCase: string | 'a' | null | undefined; + + declare const number_camelCase: number; + declare const number_camelCase: number | null; + declare const number_camelCase: number | null | undefined; + declare const number_camelCase: 1 | null | undefined; + declare const number_camelCase: number | 2 | null | undefined; + + declare const boolean_camelCase: boolean; + declare const boolean_camelCase: boolean | null; + declare const boolean_camelCase: boolean | null | undefined; + declare const boolean_camelCase: true | null | undefined; + declare const boolean_camelCase: false | null | undefined; + declare const boolean_camelCase: true | false | null | undefined; + `, + parserOptions, + options: [ + { + selector: 'variable', + types: ['string'], + format: ['camelCase'], + prefix: ['string_'], + }, + { + selector: 'variable', + types: ['number'], + format: ['camelCase'], + prefix: ['number_'], + }, + { + selector: 'variable', + types: ['boolean'], + format: ['camelCase'], + prefix: ['boolean_'], + }, + ], + }, + ], + invalid: [ + ...createInvalidTestCases(cases), + { + code: ` + declare const string_camelCase: string; + declare const string_camelCase: string | null; + declare const string_camelCase: string | null | undefined; + declare const string_camelCase: 'a' | null | undefined; + declare const string_camelCase: string | 'a' | null | undefined; + + declare const number_camelCase: number; + declare const number_camelCase: number | null; + declare const number_camelCase: number | null | undefined; + declare const number_camelCase: 1 | null | undefined; + declare const number_camelCase: number | 2 | null | undefined; + + declare const boolean_camelCase: boolean; + declare const boolean_camelCase: boolean | null; + declare const boolean_camelCase: boolean | null | undefined; + declare const boolean_camelCase: true | null | undefined; + declare const boolean_camelCase: false | null | undefined; + declare const boolean_camelCase: true | false | null | undefined; + `, + options: [ + { + selector: 'variable', + types: ['string'], + format: ['snake_case'], + prefix: ['string_'], + }, + { + selector: 'variable', + types: ['number'], + format: ['snake_case'], + prefix: ['number_'], + }, + { + selector: 'variable', + types: ['boolean'], + format: ['snake_case'], + prefix: ['boolean_'], + }, + ], + parserOptions, + errors: Array(16).fill({ messageId: 'doesNotMatchFormat' }), + }, + ], }); From aa949d602abb196a592bcf97411e76a00caff8d5 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 11 Dec 2019 12:48:09 +1030 Subject: [PATCH 10/25] feat: class, interface, typeAlias, enum, typeParameter --- .../src/rules/naming-convention.ts | 85 ++++++++++++++++++- .../tests/rules/naming-convention.test.ts | 67 +++++++++++++++ 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index fd7c38438700..107ba34f8ce6 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -41,9 +41,9 @@ enum Selectors { // memberLike property = 1 << 3, parameterProperty = 1 << 4, - enumMember = 1 << 5, - method = 1 << 6, - accessor = 1 << 7, + method = 1 << 5, + accessor = 1 << 6, + enumMember = 1 << 7, // typeLike class = 1 << 8, @@ -555,6 +555,85 @@ export default util.createRule({ }, // #endregion enumMember + + // #region class + + 'ClassDeclaration, ClassExpression'( + node: TSESTree.ClassDeclaration | TSESTree.ClassDeclaration, + ): void { + const validator = validators.class; + if (!validator) { + return; + } + + const id = node.id; + if (id === null) { + return; + } + + const modifiers = new Set(); + if (node.abstract) { + modifiers.add(Modifiers.abstract); + } + + validator(id, modifiers); + }, + + // #endregion class + + // #region interface + + TSInterfaceDeclaration(node): void { + const validator = validators.interface; + if (!validator) { + return; + } + + validator(node.id); + }, + + // #endregion interface + + // #region typeAlias + + TSTypeAliasDeclaration(node): void { + const validator = validators.typeAlias; + if (!validator) { + return; + } + + validator(node.id); + }, + + // #endregion typeAlias + + // #region enum + + TSEnumDeclaration(node): void { + const validator = validators.enum; + if (!validator) { + return; + } + + validator(node.id); + }, + + // #endregion enum + + // #region typeParameter + + 'TSTypeParameterDeclaration > TSTypeParameter'( + node: TSESTree.TSTypeParameter, + ): void { + const validator = validators.typeParameter; + if (!validator) { + return; + } + + validator(node.name); + }, + + // #endregion typeParameter }; }, }); diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index 8d1ee982d37e..e23a54c9f24b 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -358,6 +358,16 @@ const cases: Cases = [ 'interface Ignored { %: string }', 'type Ignored = { %: string }', 'class Ignored { private % = 1 }', + 'class Ignored { constructor(private %) {} }', + 'class Ignored { private %() {} }', + 'const ignored = { %() {} };', + 'class Ignored { private get %() {} }', + 'enum Ignored { % }', + 'abstract class % {}', + 'interface % { }', + 'type % = { };', + 'enum % {}', + 'interface Ignored<%> extends Ignored {}', ], options: { selector: 'default', @@ -532,6 +542,63 @@ const cases: Cases = [ }, }, // #endregion enumMember + + // #region class + { + code: ['class % {}', 'abstract class % {}', 'const ignored = class % {}'], + options: { + selector: 'class', + }, + }, + { + code: ['abstract class % {}; class ignoredDueToModifier {}'], + options: { + selector: 'class', + modifiers: ['abstract'], + }, + }, + // #endregion class + + // #region interface + { + code: ['interface % {}'], + options: { + selector: 'interface', + }, + }, + // #endregion interface + + // #region typeAlias + { + code: ['type % = {};', 'type % = 1;'], + options: { + selector: 'typeAlias', + }, + }, + // #endregion typeAlias + + // #region enum + { + code: ['enum % {}'], + options: { + selector: 'enum', + }, + }, + // #endregion enum + + // #region typeParameter + { + code: [ + 'class Ignored<%> {}', + 'function ignored<%>() {}', + 'type Ignored<%> = { ignored: % };', + 'interface Ignored<%> extends Ignored {}', + ], + options: { + selector: 'typeParameter', + }, + }, + // #endregion typeParameter ]; ruleTester.run('naming-convention', rule, { From 7b68c4729466ae5d669380a73ea7f96b773cca62 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 11 Dec 2019 13:20:32 +1030 Subject: [PATCH 11/25] docs: add examples --- .../docs/rules/naming-convention.md | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index ac5dec959bd2..0245f9fde944 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -101,8 +101,10 @@ If these are provided, the identifier must start with one of the provided values - `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. -- `types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `array`, `function`). This lets you do things like enforce that `boolean` variables are prefixed with a verb. +- `types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `array`, `function`). + - The name must match _one_ of the types. - **_NOTE - Using this option will require that you lint with type information._** + - For example, this lets you do things like enforce that `boolean` variables are prefixed with a verb. - `boolean` matches any type assignable to `boolean | null | undefined` - `string` matches any type assignable to `string | null | undefined` - `number` matches any type assignable to `number | null | undefined` @@ -189,6 +191,124 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - Allowed `modifiers`: `abstract`. - Allowed `types`: none. +## Examples + +### Enforce that all variables, functions and properties follow are camelCase + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { "selector": "variableLike", "format": ["camelCase"] } + ] +} +``` + +### Enforce that private members are prefixed with an underscore + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "memberLike", + "modifier": ["private"], + "format": ["camelCase"], + "leadingUnderscore": "require" + } + ] +} +``` + +### Enforce that boolean variables are prefixed with an allowed verb + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "variable", + "types": ["boolean"], + "format": ["PascalCase"], + "prefix": ["is", "should", "has", "can", "did", "will"] + } + ] +} +``` + +### Enforce that all variables are either in camelCase or UPPER_CASE + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "variable", + "format": ["camelCase", "UPPER_CASE"] + } + ] +} +``` + +### Enforce that type parameters (generics) are prefixed with `T` + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "typeParameter", + "format": ["PascalCase"], + "prefix": ["T"] + } + ] +} +``` + +### Enforce the codebase follows eslint's `camelcase` conventions + +```json +{ + "@typescript-eslint/naming-conventions": [ + "error", + { + "selector": "default", + "format": ["camelCase"] + }, + + { + "selector": "variableLike", + "format": ["camelCase"] + }, + { + "selector": "variable", + "format": ["camelCase", "UPPER_CASE"] + }, + { + "selector": "parameter", + "format": ["camelCase"], + "leadingUnderscore": "allow" + }, + + { + "selector": "memberLike", + "format": ["camelCase"] + }, + { + "selector": "memberLike", + "modifiers": ["private"], + "format": ["camelCase"], + "leadingUnderscore": "require" + }, + + { + "selector": "typeLike", + "format": ["PascalCase"] + } + ] +} +``` + ## When Not To Use It If you do not want to enforce naming conventions for anything. From 19ece7f9956fdbf7d801fb6f83a3cff8e16a4ad5 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 11 Dec 2019 14:23:10 +1030 Subject: [PATCH 12/25] feat: support array and function types --- .../src/rules/naming-convention.ts | 47 ++++++++--- .../tests/rules/naming-convention.test.ts | 77 ++++++++++++++----- .../eslint-plugin/typings/typescript.d.ts | 21 +++++ 3 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 packages/eslint-plugin/typings/typescript.d.ts diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 107ba34f8ce6..63ca9a83feda 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -4,6 +4,7 @@ import { TSESTree, TSESLint, } from '@typescript-eslint/experimental-utils'; +import ts from 'typescript'; import * as util from '../util'; type MessageIds = @@ -1138,31 +1139,39 @@ function isCorrectType( const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context); const checker = program.getTypeChecker(); const tsNode = esTreeNodeToTSNodeMap.get(node); - const type = checker.getTypeAtLocation(tsNode); - const typeString = checker.typeToString( - // this will resolve things like true => boolean, 'a' => string and 1 => number - checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), - ); + const type = checker + .getTypeAtLocation(tsNode) + // remove null and undefined from the type, as we don't care about it here + .getNonNullableType(); for (const allowedType of config.types) { switch (allowedType) { case TypeModifiers.array: - // TODO + if ( + isAllTypesMatch( + type, + t => checker.isArrayType(t) || checker.isTupleType(t), + ) + ) { + return true; + } break; case TypeModifiers.function: - // TODO + if (isAllTypesMatch(type, t => t.getCallSignatures().length > 0)) { + return true; + } break; case TypeModifiers.boolean: case TypeModifiers.number: case TypeModifiers.string: { + const typeString = checker.typeToString( + // this will resolve things like true => boolean, 'a' => string and 1 => number + checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), + ); const allowedTypeString = TypeModifiers[allowedType]; - if ( - typeString === `${allowedTypeString}` || - typeString === `${allowedTypeString} | null` || - typeString === `${allowedTypeString} | null | undefined` - ) { + if (typeString === allowedTypeString) { return true; } break; @@ -1173,6 +1182,20 @@ function isCorrectType( return false; } +/** + * @returns `true` if the type (or all union types) in the given type return true for the callback + */ +function isAllTypesMatch( + type: ts.Type, + cb: (type: ts.Type) => boolean, +): boolean { + if (type.isUnion()) { + return type.types.every(t => cb(t)); + } + + return cb(type); +} + export { MessageIds, Options, diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index e23a54c9f24b..014149be7b35 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -652,24 +652,24 @@ ruleTester.run('naming-convention', rule, { ...createInvalidTestCases(cases), { code: ` - declare const string_camelCase: string; - declare const string_camelCase: string | null; - declare const string_camelCase: string | null | undefined; - declare const string_camelCase: 'a' | null | undefined; - declare const string_camelCase: string | 'a' | null | undefined; - - declare const number_camelCase: number; - declare const number_camelCase: number | null; - declare const number_camelCase: number | null | undefined; - declare const number_camelCase: 1 | null | undefined; - declare const number_camelCase: number | 2 | null | undefined; - - declare const boolean_camelCase: boolean; - declare const boolean_camelCase: boolean | null; - declare const boolean_camelCase: boolean | null | undefined; - declare const boolean_camelCase: true | null | undefined; - declare const boolean_camelCase: false | null | undefined; - declare const boolean_camelCase: true | false | null | undefined; + declare const string_camelCase01: string; + declare const string_camelCase02: string | null; + declare const string_camelCase03: string | null | undefined; + declare const string_camelCase04: 'a' | null | undefined; + declare const string_camelCase05: string | 'a' | null | undefined; + + declare const number_camelCase06: number; + declare const number_camelCase07: number | null; + declare const number_camelCase08: number | null | undefined; + declare const number_camelCase09: 1 | null | undefined; + declare const number_camelCase10: number | 2 | null | undefined; + + declare const boolean_camelCase11: boolean; + declare const boolean_camelCase12: boolean | null; + declare const boolean_camelCase13: boolean | null | undefined; + declare const boolean_camelCase14: true | null | undefined; + declare const boolean_camelCase15: false | null | undefined; + declare const boolean_camelCase16: true | false | null | undefined; `, options: [ { @@ -694,5 +694,46 @@ ruleTester.run('naming-convention', rule, { parserOptions, errors: Array(16).fill({ messageId: 'doesNotMatchFormat' }), }, + { + code: ` + declare const function_camelCase1: (() => void); + declare const function_camelCase2: (() => void) | null; + declare const function_camelCase3: (() => void) | null | undefined; + declare const function_camelCase4: (() => void) | (() => string) | null | undefined; + `, + options: [ + { + selector: 'variable', + types: ['function'], + format: ['snake_case'], + prefix: ['function_'], + }, + ], + parserOptions, + errors: Array(4).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + declare const array_camelCase1: Array; + declare const array_camelCase2: ReadonlyArray | null; + declare const array_camelCase3: number[] | null | undefined; + declare const array_camelCase4: readonly number[] | null | undefined; + declare const array_camelCase5: number[] | (number | string)[] | null | undefined; + declare const array_camelCase6: [] | null | undefined; + declare const array_camelCase7: [number] | null | undefined; + + declare const array_camelCase8: readonly number[] | Array | [boolean] | null | undefined; + `, + options: [ + { + selector: 'variable', + types: ['array'], + format: ['snake_case'], + prefix: ['array_'], + }, + ], + parserOptions, + errors: Array(8).fill({ messageId: 'doesNotMatchFormat' }), + }, ], }); diff --git a/packages/eslint-plugin/typings/typescript.d.ts b/packages/eslint-plugin/typings/typescript.d.ts new file mode 100644 index 000000000000..6d9a098b538b --- /dev/null +++ b/packages/eslint-plugin/typings/typescript.d.ts @@ -0,0 +1,21 @@ +import { TypeChecker, Type } from 'typescript'; + +declare module 'typescript' { + interface TypeChecker { + // internal TS APIs + + /** + * @returns `true` if the given type is an array type: + * - Array + * - ReadonlyArray + * - foo[] + * - readonly foo[] + */ + isArrayType(type: Type): boolean; + /** + * @returns `true` if the given type is a tuple type: + * - [foo] + */ + isTupleType(type: Type): boolean; + } +} From f28058d6a55c8409f05f6210075489d6183f6355 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 12 Dec 2019 11:06:29 +1030 Subject: [PATCH 13/25] docs: remove deprecated rules from readme --- packages/eslint-plugin/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index d374f000eac8..9b2eef3d7ec0 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -101,18 +101,14 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/ban-ts-ignore`](./docs/rules/ban-ts-ignore.md) | Bans “// @ts-ignore” comments from being used | :heavy_check_mark: | | | | [`@typescript-eslint/ban-types`](./docs/rules/ban-types.md) | Bans specific types from being used | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | -| [`@typescript-eslint/camelcase`](./docs/rules/camelcase.md) | Enforce camelCase naming convention | :heavy_check_mark: | | | -| [`@typescript-eslint/class-name-casing`](./docs/rules/class-name-casing.md) | Require PascalCased class and interface names | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-assertions`](./docs/rules/consistent-type-assertions.md) | Enforces consistent usage of type assertions. | :heavy_check_mark: | | | | [`@typescript-eslint/consistent-type-definitions`](./docs/rules/consistent-type-definitions.md) | Consistent with type definition either `interface` or `type` | | :wrench: | | | [`@typescript-eslint/explicit-function-return-type`](./docs/rules/explicit-function-return-type.md) | Require explicit return types on functions and class methods | :heavy_check_mark: | | | | [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | | [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | -| [`@typescript-eslint/generic-type-naming`](./docs/rules/generic-type-naming.md) | Enforces naming of generic type variables | | | | | [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | | [`@typescript-eslint/interface-name-prefix`](./docs/rules/interface-name-prefix.md) | Require that interface names should or should not prefixed with `I` | :heavy_check_mark: | | | | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/member-naming`](./docs/rules/member-naming.md) | Enforces naming conventions for class members by visibility | | | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | | [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | | [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | From 503a7b000fcfa3120a599972094a9e92704e0d9d Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 18 Dec 2019 19:34:14 +1030 Subject: [PATCH 14/25] docs: fix spelling --- .spelling | 8 ++++++-- packages/eslint-plugin/docs/rules/naming-convention.md | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.spelling b/.spelling index 8acf2e674fe3..bb310bb14242 100644 --- a/.spelling +++ b/.spelling @@ -7,6 +7,8 @@ 1pm 30s A11y +accessor +accessors Airbnb Airbnb's ands @@ -48,6 +50,7 @@ ESTree fallthrough falsy filenames +granularly heavy_check_mark i.e. IIFE @@ -72,9 +75,9 @@ necroing NodeJS npm nullish +onboard OOM OOMs -onboard OSS package.json Palantir @@ -97,6 +100,7 @@ ruleset rulesets runtime searchable +snake_case String#match substring substrings @@ -110,7 +114,6 @@ TODO transpile truthy tsconfig -tsconfig.json tsconfigs TSLint typecheck @@ -121,6 +124,7 @@ typings unfixable unprefixed untyped +UPPER_CASE VSCode Vue whitespace diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 0245f9fde944..9071451e5920 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -56,7 +56,7 @@ When the format of an identifier is checked, it is checked in the following orde 1. validate format At each step, if the identifier matches the option, the matching part will be removed. -For example, if you provide the following formating option: `{ leadingUnderscore: 'allow', prefix: ['I'], format: ['StrictPascalCase'] }`, for the identifier `_IMyInterface`, then the following checks will occur: +For example, if you provide the following formatting option: `{ leadingUnderscore: 'allow', prefix: ['I'], format: ['StrictPascalCase'] }`, for the identifier `_IMyInterface`, then the following checks will occur: 1. `name = _IMyInterface` 1. validate leading underscore - pass @@ -109,7 +109,7 @@ If these are provided, the identifier must start with one of the provided values - `string` matches any type assignable to `string | null | undefined` - `number` matches any type assignable to `number | null | undefined` - `array` matches any type assignable to `Array | null | undefined` - - `function` matches any type asignable to `Function | null | undefined` + - `function` matches any type assignable to `Function | null | undefined` The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. @@ -265,7 +265,7 @@ Group Selectors are provided for convenience, and essentially bundle up sets of } ``` -### Enforce the codebase follows eslint's `camelcase` conventions +### Enforce the codebase follows ESLint's `camelcase` conventions ```json { From e21d472f2f7213dc4a7a156bd48eb59534d026c6 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 18 Dec 2019 20:53:58 +1030 Subject: [PATCH 15/25] feat: nits --- .../docs/rules/naming-convention.md | 39 ++++-- .../src/rules/naming-convention.ts | 120 ++++++++++-------- 2 files changed, 96 insertions(+), 63 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 9071451e5920..e9851b5ed484 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -8,7 +8,6 @@ There are many different rules that have existed over time, but they have had th ## Rule Details This rule allows you to enforce conventions for any identifier, using granular selectors to create a fine-grained style guide. -By default, it enforces nothing. ### Note - this rule only needs type information in specific cases, detailed below @@ -41,7 +40,35 @@ type Options = { types?: Types[]; }[]; -const defaultOptions: Options = []; +// the default config essentially does the same thing as ESLint's camelcase rule +const defaultOptions: Options = [ + { + selector: 'default', + format: ['camelCase'], + }, + + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE'], + }, + { + selector: 'parameter', + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + + { + selector: 'memberLike', + modifiers: ['private'], + format: ['camelCase'], + leadingUnderscore: 'require', + }, + + { + selector: 'typeLike', + format: ['PascalCase'], + }, +]; ``` ### Format Options @@ -276,10 +303,6 @@ Group Selectors are provided for convenience, and essentially bundle up sets of "format": ["camelCase"] }, - { - "selector": "variableLike", - "format": ["camelCase"] - }, { "selector": "variable", "format": ["camelCase", "UPPER_CASE"] @@ -290,10 +313,6 @@ Group Selectors are provided for convenience, and essentially bundle up sets of "leadingUnderscore": "allow" }, - { - "selector": "memberLike", - "format": ["camelCase"] - }, { "selector": "memberLike", "modifiers": ["private"], diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 63ca9a83feda..157d7ae48045 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -274,6 +274,33 @@ const SCHEMA: JSONSchema.JSONSchema4 = { // #endregion Schema Config +const defaultCamelCaseAllTheThingsConfig: Options = [ + { + selector: 'default', + format: ['camelCase'], + }, + + { + selector: 'variable', + format: ['camelCase', 'UPPER_CASE'], + }, + { + selector: 'parameter', + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + + { + selector: 'memberLike', + format: ['camelCase'], + }, + + { + selector: 'typeLike', + format: ['PascalCase'], + }, +]; + export default util.createRule({ name: 'naming-convention', meta: { @@ -298,8 +325,18 @@ export default util.createRule({ }, schema: SCHEMA, }, - defaultOptions: [], - create(context) { + defaultOptions: defaultCamelCaseAllTheThingsConfig, + create(contextWithoutDefaults) { + const context: Context = contextWithoutDefaults.options + ? contextWithoutDefaults + : // only apply the defaults when the user provides no config + Object.setPrototypeOf( + { + options: defaultCamelCaseAllTheThingsConfig, + }, + contextWithoutDefaults, + ); + const validators = parseOptions(context); function handleMember( @@ -332,7 +369,8 @@ export default util.createRule({ | TSESTree.ClassProperty | TSESTree.TSAbstractClassProperty | TSESTree.MethodDefinition - | TSESTree.TSAbstractMethodDefinition, + | TSESTree.TSAbstractMethodDefinition + | TSESTree.TSParameterProperty, ): Set { const modifiers = new Set(); if (node.accessibility) { @@ -430,15 +468,7 @@ export default util.createRule({ return; } - const modifiers = new Set(); - if (node.accessibility !== undefined) { - modifiers.add(Modifiers[node.accessibility]); - } else { - modifiers.add(Modifiers.public); - } - if (node.readonly) { - modifiers.add(Modifiers.readonly); - } + const modifiers = getMemberModifiers(node); const identifiers: TSESTree.Identifier[] = []; getIdentifiersFromPattern(node.parameter, identifiers); @@ -738,7 +768,7 @@ function createValidator( node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; // return will break the loop and stop checking configs - // it is only used when the name is known to have failed a config. + // it is only used when the name is known to have failed or succeeded a config. for (const config of configs) { if (config.filter?.test(originalName)) { // name does not match the filter @@ -759,21 +789,25 @@ function createValidator( name = validateUnderscore('leading', config, name, node, originalName); if (name === null) { + // fail return; } name = validateUnderscore('trailing', config, name, node, originalName); if (name === null) { + // fail return; } name = validateAffix('prefix', config, name, node, originalName); if (name === null) { + // fail return; } name = validateAffix('suffix', config, name, node, originalName); if (name === null) { + // fail return; } @@ -924,42 +958,9 @@ function createValidator( } for (const format of formats) { - switch (format) { - case PredefinedFormats.PascalCase: - if (isPascalCase(name)) { - return true; - } - break; - - case PredefinedFormats.StrictPascalCase: - if (isStrictPascalCase(name)) { - return true; - } - break; - - case PredefinedFormats.camelCase: - if (isCamelCase(name)) { - return true; - } - break; - - case PredefinedFormats.strictCamelCase: - if (isStrictCamelCase(name)) { - return true; - } - break; - - case PredefinedFormats.UPPER_CASE: - if (isUpperCase(name)) { - return true; - } - break; - - case PredefinedFormats.snake_case: - if (isSnakeCase(name)) { - return true; - } - break; + const checker = PredefinedFormatToCheckFunction[format]; + if (checker(name)) { + return true; } } @@ -1022,6 +1023,10 @@ function isStrictCamelCase(name: string): boolean { } function hasStrictCamelHumps(name: string, isUpper: boolean): boolean { + function isUppercaseChar(char: string): boolean { + return char === char.toUpperCase() && char !== char.toLowerCase(); + } + if (name.startsWith('_')) { return false; } @@ -1040,10 +1045,6 @@ function hasStrictCamelHumps(name: string, isUpper: boolean): boolean { return true; } -function isUppercaseChar(char: string): boolean { - return char === char.toUpperCase() && char !== char.toLowerCase(); -} - function isSnakeCase(name: string): boolean { return ( name.length === 0 || @@ -1076,6 +1077,19 @@ function validateUnderscores(name: string): boolean { } return !wasUnderscore; } + +const PredefinedFormatToCheckFunction: Readonly boolean +>> = { + [PredefinedFormats.PascalCase]: isPascalCase, + [PredefinedFormats.StrictPascalCase]: isStrictPascalCase, + [PredefinedFormats.camelCase]: isCamelCase, + [PredefinedFormats.strictCamelCase]: isStrictCamelCase, + [PredefinedFormats.UPPER_CASE]: isUpperCase, + [PredefinedFormats.snake_case]: isSnakeCase, +}; + // #endregion Predefined Format Functions function selectorTypeToMessageString(selectorType: SelectorsString): string { From 409a6167838c5da2b8e3ad938cc7368b5ce857a4 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 19 Dec 2019 11:35:13 +1030 Subject: [PATCH 16/25] fix: correct default config to closely match eslint's camelcase --- .../eslint-plugin/docs/rules/naming-convention.md | 14 +++----------- .../eslint-plugin/src/rules/naming-convention.ts | 14 +++++--------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index e9851b5ed484..93bfd24d85cb 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -45,23 +45,15 @@ const defaultOptions: Options = [ { selector: 'default', format: ['camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', }, { selector: 'variable', format: ['camelCase', 'UPPER_CASE'], - }, - { - selector: 'parameter', - format: ['camelCase'], leadingUnderscore: 'allow', - }, - - { - selector: 'memberLike', - modifiers: ['private'], - format: ['camelCase'], - leadingUnderscore: 'require', + trailingUnderscore: 'allow', }, { diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 157d7ae48045..f54cfaea8167 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -274,25 +274,21 @@ const SCHEMA: JSONSchema.JSONSchema4 = { // #endregion Schema Config +// This essentially mirrors eslint's camelcase rule +// note that that rule ignores leading and trailing underscores and only checks those in the middle of a variable name const defaultCamelCaseAllTheThingsConfig: Options = [ { selector: 'default', format: ['camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', }, { selector: 'variable', format: ['camelCase', 'UPPER_CASE'], - }, - { - selector: 'parameter', - format: ['camelCase'], leadingUnderscore: 'allow', - }, - - { - selector: 'memberLike', - format: ['camelCase'], + trailingUnderscore: 'allow', }, { From bb709497cf92a87d76332e02c42b72bf9d8c7468 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 23 Dec 2019 10:06:11 +1030 Subject: [PATCH 17/25] fix: typos, use NonComputedName types --- .../src/rules/naming-convention.ts | 85 +++++++++---------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index f54cfaea8167..c5808c0e22e2 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -26,12 +26,12 @@ enum PredefinedFormats { } type PredefinedFormatsString = keyof typeof PredefinedFormats; -enum UnderscroreOptions { +enum UnderscoreOptions { forbid = 1 << 0, allow = 1 << 1, require = 1 << 2, } -type UnderscroreOptionsString = keyof typeof UnderscroreOptions; +type UnderscoreOptionsString = keyof typeof UnderscoreOptions; enum Selectors { // variableLike @@ -99,8 +99,8 @@ type TypeModifiersString = keyof typeof TypeModifiers; interface Selector { // format options format: PredefinedFormatsString[]; - leadingUnderscore?: UnderscroreOptionsString; - trailingUnderscore?: UnderscroreOptionsString; + leadingUnderscore?: UnderscoreOptionsString; + trailingUnderscore?: UnderscoreOptionsString; prefix?: string[]; suffix?: string[]; // selector options @@ -111,8 +111,8 @@ interface Selector { } interface NormalizedSelector { // format options - leadingUnderscore: UnderscroreOptions | null; - trailingUnderscore: UnderscroreOptions | null; + leadingUnderscore: UnderscoreOptions | null; + trailingUnderscore: UnderscoreOptions | null; prefix: string[] | null; suffix: string[] | null; format: PredefinedFormats[]; @@ -136,7 +136,7 @@ type Options = Selector[]; const UNDERSCORE_SCHEMA: JSONSchema.JSONSchema4 = { type: 'string', - enum: util.getEnumNames(UnderscroreOptions), + enum: util.getEnumNames(UnderscoreOptions), }; const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { type: 'array', @@ -336,15 +336,15 @@ export default util.createRule({ const validators = parseOptions(context); function handleMember( - validator: ValidatiorFunction | null, + validator: ValidatorFunction | null, node: - | TSESTree.Property - | TSESTree.ClassProperty - | TSESTree.TSAbstractClassProperty - | TSESTree.TSPropertySignature - | TSESTree.MethodDefinition - | TSESTree.TSAbstractMethodDefinition - | TSESTree.TSMethodSignature, + | TSESTree.PropertyNonComputedName + | TSESTree.ClassPropertyNonComputedName + | TSESTree.TSAbstractClassPropertyNonComputedName + | TSESTree.TSPropertySignatureNonComputedName + | TSESTree.MethodDefinitionNonComputedName + | TSESTree.TSAbstractMethodDefinitionNonComputedName + | TSESTree.TSMethodSignatureNonComputedName, modifiers: Set, ): void { if (!validator) { @@ -352,11 +352,6 @@ export default util.createRule({ } const key = node.key; - /* istanbul ignore if */ if (!util.isLiteralOrIdentifier(key)) { - // shouldn't happen due to the selectors that are used - return; - } - validator(key, modifiers); } @@ -479,7 +474,7 @@ export default util.createRule({ // #region property 'Property[computed = false][kind = "init"][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( - node: TSESTree.Property, + node: TSESTree.PropertyNonComputedName, ): void { const modifiers = new Set([Modifiers.public]); handleMember(validators.property, node, modifiers); @@ -489,14 +484,16 @@ export default util.createRule({ 'ClassProperty[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]', 'TSAbstractClassProperty[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]', ].join(', ')]( - node: TSESTree.ClassProperty | TSESTree.TSAbstractClassProperty, + node: + | TSESTree.ClassPropertyNonComputedName + | TSESTree.TSAbstractClassPropertyNonComputedName, ): void { const modifiers = getMemberModifiers(node); handleMember(validators.property, node, modifiers); }, 'TSPropertySignature[computed = false]'( - node: TSESTree.TSPropertySignature, + node: TSESTree.TSPropertySignatureNonComputedName, ): void { const modifiers = new Set([Modifiers.public]); if (node.readonly) { @@ -516,7 +513,9 @@ export default util.createRule({ 'Property[computed = false][kind = "init"][value.type = "TSEmptyBodyFunctionExpression"]', 'TSMethodSignature[computed = false]', ].join(', ')]( - node: TSESTree.Property | TSESTree.TSMethodSignature, + node: + | TSESTree.PropertyNonComputedName + | TSESTree.TSMethodSignatureNonComputedName, ): void { const modifiers = new Set([Modifiers.public]); handleMember(validators.method, node, modifiers); @@ -533,10 +532,10 @@ export default util.createRule({ 'TSAbstractMethodDefinition[computed = false][kind = "method"]', ].join(', ')]( node: - | TSESTree.ClassProperty - | TSESTree.TSAbstractClassProperty - | TSESTree.MethodDefinition - | TSESTree.TSAbstractMethodDefinition, + | TSESTree.ClassPropertyNonComputedName + | TSESTree.TSAbstractClassPropertyNonComputedName + | TSESTree.MethodDefinitionNonComputedName + | TSESTree.TSAbstractMethodDefinitionNonComputedName, ): void { const modifiers = getMemberModifiers(node); handleMember(validators.method, node, modifiers); @@ -549,7 +548,7 @@ export default util.createRule({ [[ 'Property[computed = false][kind = "get"]', 'Property[computed = false][kind = "set"]', - ].join(', ')](node: TSESTree.Property): void { + ].join(', ')](node: TSESTree.PropertyNonComputedName): void { const modifiers = new Set([Modifiers.public]); handleMember(validators.accessor, node, modifiers); }, @@ -557,7 +556,7 @@ export default util.createRule({ [[ 'MethodDefinition[computed = false][kind = "get"]', 'MethodDefinition[computed = false][kind = "set"]', - ].join(', ')](node: TSESTree.MethodDefinition): void { + ].join(', ')](node: TSESTree.MethodDefinitionNonComputedName): void { const modifiers = getMemberModifiers(node); handleMember(validators.accessor, node, modifiers); }, @@ -566,18 +565,16 @@ export default util.createRule({ // #region enumMember - TSEnumMember(node): void { + // computed is optional, so can't do [computed = false] + 'TSEnumMember[computed != true]'( + node: TSESTree.TSEnumMemberNonComputedName, + ): void { const validator = validators.enumMember; if (!validator) { return; } const id = node.id; - /* istanbul ignore if */ if (!util.isLiteralOrIdentifier(id)) { - // shouldn't happen in reality because it's not semantically valid code - return; - } - validator(id); }, @@ -586,7 +583,7 @@ export default util.createRule({ // #region class 'ClassDeclaration, ClassExpression'( - node: TSESTree.ClassDeclaration | TSESTree.ClassDeclaration, + node: TSESTree.ClassDeclaration | TSESTree.ClassExpression, ): void { const validator = validators.class; if (!validator) { @@ -716,11 +713,11 @@ function getIdentifiersFromPattern( } } -type ValidatiorFunction = ( +type ValidatorFunction = ( node: TSESTree.Identifier | TSESTree.Literal, modifiers?: Set, ) => void; -type ParsedOptions = Record; +type ParsedOptions = Record; type Context = TSESLint.RuleContext; function parseOptions(context: Context): ParsedOptions { const normalizedOptions = context.options.map(opt => normalizeOption(opt)); @@ -863,11 +860,11 @@ function createValidator( : (): string => name.slice(0, -1); switch (option) { - case UnderscroreOptions.allow: + case UnderscoreOptions.allow: // no check - the user doesn't care if it's there or not break; - case UnderscroreOptions.forbid: + case UnderscoreOptions.forbid: if (hasUnderscore) { context.report({ node, @@ -881,7 +878,7 @@ function createValidator( } break; - case UnderscroreOptions.require: + case UnderscoreOptions.require: if (!hasUnderscore) { context.report({ node, @@ -1116,11 +1113,11 @@ function normalizeOption(option: Selector): NormalizedSelector { // format options leadingUnderscore: option.leadingUnderscore !== undefined - ? UnderscroreOptions[option.leadingUnderscore] + ? UnderscoreOptions[option.leadingUnderscore] : null, trailingUnderscore: option.trailingUnderscore !== undefined - ? UnderscroreOptions[option.trailingUnderscore] + ? UnderscoreOptions[option.trailingUnderscore] : null, prefix: option.prefix ?? null, suffix: option.suffix ?? null, From 1a85712201d90737b2254091971b2041aff512ad Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sun, 29 Dec 2019 18:45:39 +1030 Subject: [PATCH 18/25] fix: lint error --- packages/eslint-plugin/src/rules/naming-convention.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index c5808c0e22e2..e25241eb606b 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -4,7 +4,7 @@ import { TSESTree, TSESLint, } from '@typescript-eslint/experimental-utils'; -import ts from 'typescript'; +import * as ts from 'typescript'; import * as util from '../util'; type MessageIds = From 39ea8cc98247a94c6718ad8e78c843b5cd1c0eb6 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 30 Dec 2019 16:49:33 +1100 Subject: [PATCH 19/25] fix: spelling --- .cspell.json | 5 ++++- package.json | 2 +- packages/eslint-plugin/src/rules/naming-convention.ts | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.cspell.json b/.cspell.json index b3e10009914b..eb85de6763c1 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,7 +9,8 @@ "**/**/CHANGELOG.md", "**/**/CONTRIBUTORS.md", "**/**/ROADMAP.md", - "**/*.json" + "**/*.json", + ".cspell.json" ], "dictionaries": [ "typescript", @@ -54,6 +55,8 @@ "destructure", "destructured", "erroring", + "ESLint", + "ESLint's", "espree", "estree", "linebreaks", diff --git a/package.json b/package.json index 8255719940d2..76fed22655ac 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "cz": "git-cz", "check:docs": "lerna run check:docs", "check:configs": "lerna run check:configs", - "check:spelling": "cspell --config=.cspell.json **/*.{md,ts,js}", + "check:spelling": "cspell --config=.cspell.json \"**/*.{md,ts,js}\"", "generate-contributors": "yarn ts-node --transpile-only ./tools/generate-contributors.ts && yarn all-contributors generate", "format": "prettier --write \"./**/*.{ts,js,json,md}\"", "format-check": "prettier --list-different \"./**/*.{ts,js,json,md}\"", diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index e25241eb606b..b5a1fb1e4182 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -274,7 +274,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { // #endregion Schema Config -// This essentially mirrors eslint's camelcase rule +// This essentially mirrors ESLint's `camelcase` rule // note that that rule ignores leading and trailing underscores and only checks those in the middle of a variable name const defaultCamelCaseAllTheThingsConfig: Options = [ { @@ -305,7 +305,7 @@ export default util.createRule({ description: 'Enforces naming conventions for everything across a codebase', recommended: false, - // technically only requires type checkin if the user uses "type" modifiers + // technically only requires type checking if the user uses "type" modifiers requiresTypeChecking: true, }, type: 'suggestion', @@ -813,7 +813,7 @@ function createValidator( } }; - // centralises the logic for formatting the report data + // centralizes the logic for formatting the report data function formatReportData({ affixes, formats, @@ -972,7 +972,7 @@ function createValidator( // #region Predefined Format Functions /* -These format functions are taken from tslint-consistent-codestyle/naming-convention: +These format functions are taken from `tslint-consistent-codestyle/naming-convention`: https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/rules/namingConventionRule.ts#L603-L645 The licence for the code can be viewed here: From c2b8ce9134efcaa9daa591620a8522adb288856a Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 30 Dec 2019 16:54:51 +1100 Subject: [PATCH 20/25] feat: matches for some selectors --- .../src/rules/naming-convention.ts | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index b5a1fb1e4182..654fcd5d616d 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -480,10 +480,7 @@ export default util.createRule({ handleMember(validators.property, node, modifiers); }, - [[ - 'ClassProperty[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]', - 'TSAbstractClassProperty[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]', - ].join(', ')]( + ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( node: | TSESTree.ClassPropertyNonComputedName | TSESTree.TSAbstractClassPropertyNonComputedName, @@ -522,14 +519,10 @@ export default util.createRule({ }, [[ - 'ClassProperty[computed = false][value.type = "ArrowFunctionExpression"]', - 'ClassProperty[computed = false][value.type = "FunctionExpression"]', - 'ClassProperty[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', - 'TSAbstractClassProperty[computed = false][value.type = "ArrowFunctionExpression"]', - 'TSAbstractClassProperty[computed = false][value.type = "FunctionExpression"]', - 'TSAbstractClassProperty[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', - 'MethodDefinition[computed = false][kind = "method"]', - 'TSAbstractMethodDefinition[computed = false][kind = "method"]', + ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "ArrowFunctionExpression"]', + ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "FunctionExpression"]', + ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type = "TSEmptyBodyFunctionExpression"]', + ':matches(MethodDefinition, TSAbstractMethodDefinition)[computed = false][kind = "method"]', ].join(', ')]( node: | TSESTree.ClassPropertyNonComputedName @@ -545,18 +538,16 @@ export default util.createRule({ // #region accessor - [[ - 'Property[computed = false][kind = "get"]', - 'Property[computed = false][kind = "set"]', - ].join(', ')](node: TSESTree.PropertyNonComputedName): void { + 'Property[computed = false]:matches([kind = "get"], [kind = "set"])'( + node: TSESTree.PropertyNonComputedName, + ): void { const modifiers = new Set([Modifiers.public]); handleMember(validators.accessor, node, modifiers); }, - [[ - 'MethodDefinition[computed = false][kind = "get"]', - 'MethodDefinition[computed = false][kind = "set"]', - ].join(', ')](node: TSESTree.MethodDefinitionNonComputedName): void { + 'MethodDefinition[computed = false]:matches([kind = "get"], [kind = "set"])'( + node: TSESTree.MethodDefinitionNonComputedName, + ): void { const modifiers = getMemberModifiers(node); handleMember(validators.accessor, node, modifiers); }, From 0154b401d64152fc1baa30c18c48391b008947f6 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sun, 5 Jan 2020 13:19:43 -0800 Subject: [PATCH 21/25] docs: new heading format --- packages/eslint-plugin/docs/rules/naming-convention.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 93bfd24d85cb..f1d367511dcd 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -1,4 +1,4 @@ -# Enforces naming conventions for everything across a codebase (naming-convention) +# 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`. From a16e6f482d2b53909fca3f483ad23c302b3c7b85 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 9 Jan 2020 11:42:02 -0800 Subject: [PATCH 22/25] feat: add custom regex option --- .../docs/rules/naming-convention.md | 13 ++ .../src/rules/naming-convention.ts | 109 +++++++++++++-- .../tests/rules/naming-convention.test.ts | 126 ++++++++++++++++++ 3 files changed, 239 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index f1d367511dcd..5186c98eb6db 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -27,6 +27,10 @@ type Options = { | 'snake_case' | 'UPPER_CASE' )[]; + custom?: { + regex: string; + match: boolean; + }; leadingUnderscore?: 'forbid' | 'allow' | 'require'; trailingUnderscore?: 'forbid' | 'allow' | 'require'; prefix?: string[]; @@ -72,6 +76,7 @@ When the format of an identifier is checked, it is checked in the following orde 1. validate trailing underscore 1. validate prefix 1. validate suffix +1. validate custom 1. validate format At each step, if the identifier matches the option, the matching part will be removed. @@ -99,6 +104,14 @@ The `format` option defines the allowed formats for the identifier. This option - `snake_case` - standard snake_case format - all characters must be lower-case, and underscores are allowed. - `UPPER_CASE` - same as `snake_case`, except all characters must be upper-case. +### `custom` + +The `custom` option defines a custom regex that the identifier must (or must not) match. This option allows you to have a bit more finer-grained control over identifiers, letting you ban (or force) certain patterns and substrings. +Accepts an object with the following properties: + +- `regex` - accepts a regular expression (anything accepted into `new RegExp(filter)`). +- `match` - true if the identifier _must_ match the `regex`, false if the identifier _must not_ match the `regex`. + #### `leadingUnderscore` / `trailingUnderscore` The `leadingUnderscore` / `trailingUnderscore` options control whether leading/trailing underscores are considered valid. Accepts one of the following values: diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 654fcd5d616d..50acf190d046 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -11,6 +11,7 @@ type MessageIds = | 'unexpectedUnderscore' | 'missingUnderscore' | 'missingAffix' + | 'satisfyCustom' | 'doesNotMatchFormat'; // #region Options Type Config @@ -54,6 +55,7 @@ enum Selectors { typeParameter = 1 << 12, } type SelectorsString = keyof typeof Selectors; +const SELECTOR_COUNT = util.getEnumNames(Selectors).length; enum MetaSelectors { default = -1, @@ -99,6 +101,10 @@ type TypeModifiersString = keyof typeof TypeModifiers; interface Selector { // format options format: PredefinedFormatsString[]; + custom?: { + regex: string; + match: boolean; + }; leadingUnderscore?: UnderscoreOptionsString; trailingUnderscore?: UnderscoreOptionsString; prefix?: string[]; @@ -111,11 +117,15 @@ interface Selector { } interface NormalizedSelector { // format options + format: PredefinedFormats[]; + custom: { + regex: RegExp; + match: boolean; + } | null; leadingUnderscore: UnderscoreOptions | null; trailingUnderscore: UnderscoreOptions | null; prefix: string[] | null; suffix: string[] | null; - format: PredefinedFormats[]; // selector options selector: Selectors | MetaSelectors; modifiers: Modifiers[] | null; @@ -149,10 +159,6 @@ const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { }; type JSONSchemaProperties = Record; const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { - leadingUnderscore: UNDERSCORE_SCHEMA, - trailingUnderscore: UNDERSCORE_SCHEMA, - prefix: PREFIX_SUFFIX_SCHEMA, - suffix: PREFIX_SUFFIX_SCHEMA, format: { type: 'array', items: { @@ -162,6 +168,22 @@ const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { minItems: 1, additionalItems: false, }, + custom: { + type: 'object', + properties: { + regex: { + type: 'string', + }, + match: { + type: 'boolean', + }, + }, + required: ['regex', 'match'], + }, + leadingUnderscore: UNDERSCORE_SCHEMA, + trailingUnderscore: UNDERSCORE_SCHEMA, + prefix: PREFIX_SUFFIX_SCHEMA, + suffix: PREFIX_SUFFIX_SCHEMA, }; function selectorSchema( selectorString: IndividualAndMetaSelectorsString, @@ -316,6 +338,8 @@ export default util.createRule({ '{{type}} name {{name}} must have a {{position}} underscore', missingAffix: '{{type}} name {{name}} must have one of the following {{position}}es: {{affixes}}', + satisfyCustom: + '{{type}} name {{name}} must {{regexMatch}} the RegExp: {{regex}}', doesNotMatchFormat: '{{type}} name {{name}} must match one of the following formats: {{formats}}', }, @@ -737,11 +761,24 @@ function createValidator( if (a.selector === b.selector) { // in the event of the same selector, order by modifier weight // sort ascending - the type modifiers are "more important" - return b.modifierWeight - a.modifierWeight; + return a.modifierWeight - b.modifierWeight; } + /* + meta selectors will always be larger numbers than the normal selectors they contain, as they are the sum of all + of the selectors that they contain. + to give normal selectors a higher priority, shift them all SELECTOR_COUNT bits to the left before comparison, so + they are instead always guaranteed to be larger than the meta selectors. + */ + const aSelector = isMetaSelector(a.selector) + ? a.selector + : a.selector << SELECTOR_COUNT; + const bSelector = isMetaSelector(b.selector) + ? b.selector + : b.selector << SELECTOR_COUNT; + // sort descending - the meta selectors are "least important" - return a.selector - b.selector; + return bSelector - aSelector; }); return ( @@ -795,7 +832,13 @@ function createValidator( return; } + if (!validateCustom(config, name, node, originalName)) { + // fail + return; + } + if (!validatePredefinedFormat(config, name, node, originalName)) { + // fail return; } @@ -810,11 +853,13 @@ function createValidator( formats, originalName, position, + custom, }: { affixes?: string[]; formats?: PredefinedFormats[]; originalName: string; position?: 'leading' | 'trailing' | 'prefix' | 'suffix'; + custom?: NonNullable; }): Record { return { type: selectorTypeToMessageString(type), @@ -822,6 +867,13 @@ function createValidator( position, affixes: affixes?.join(', '), formats: formats?.map(f => PredefinedFormats[f]).join(', '), + regex: custom?.regex?.toString(), + regexMatch: + custom?.match === true + ? 'match' + : custom?.match === false + ? 'not match' + : null, }; } @@ -927,6 +979,39 @@ function createValidator( return null; } + /** + * @returns true if the name is valid according to the `regex` option, false otherwise + */ + function validateCustom( + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): boolean { + const custom = config.custom; + if (!custom) { + return true; + } + + const result = custom.regex.test(name); + if (custom.match && result) { + return true; + } + if (!custom.match && !result) { + return true; + } + + context.report({ + node, + messageId: 'satisfyCustom', + data: formatReportData({ + originalName, + custom, + }), + }); + return false; + } + /** * @returns true if the name is valid according to the `format` option, false otherwise */ @@ -1082,7 +1167,7 @@ function selectorTypeToMessageString(selectorType: SelectorsString): string { } function isMetaSelector( - selector: IndividualAndMetaSelectorsString, + selector: IndividualAndMetaSelectorsString | Selectors | MetaSelectors, ): selector is MetaSelectorsString { return selector in MetaSelectors; } @@ -1102,6 +1187,13 @@ function normalizeOption(option: Selector): NormalizedSelector { return { // format options + format: option.format.map(f => PredefinedFormats[f]), + custom: option.custom + ? { + regex: new RegExp(option.custom.regex), + match: option.custom.match, + } + : null, leadingUnderscore: option.leadingUnderscore !== undefined ? UnderscoreOptions[option.leadingUnderscore] @@ -1112,7 +1204,6 @@ function normalizeOption(option: Selector): NormalizedSelector { : null, prefix: option.prefix ?? null, suffix: option.suffix ?? null, - format: option.format.map(f => PredefinedFormats[f]), // selector options selector: isMetaSelector(option.selector) ? MetaSelectors[option.selector] diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index 014149be7b35..de4625fbeb20 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -647,6 +647,43 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + let foo = 'a'; + const _foo = 1; + interface Foo {} + class Bar {} + function foo_function_bar() {} + `, + options: [ + { + selector: 'default', + format: ['camelCase'], + custom: { + regex: /^unused_\w/.source, + match: false, + }, + leadingUnderscore: 'allow', + }, + { + selector: 'typeLike', + format: ['PascalCase'], + custom: { + regex: /^I[A-Z]/.source, + match: false, + }, + }, + { + selector: 'function', + format: ['snake_case'], + custom: { + regex: /_function_/.source, + match: true, + }, + leadingUnderscore: 'allow', + }, + ], + }, ], invalid: [ ...createInvalidTestCases(cases), @@ -735,5 +772,94 @@ ruleTester.run('naming-convention', rule, { parserOptions, errors: Array(8).fill({ messageId: 'doesNotMatchFormat' }), }, + { + code: ` + let unused_foo = 'a'; + const _unused_foo = 1; + interface IFoo {} + class IBar {} + function fooBar() {} + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + custom: { + regex: /^unused_\w/.source, + match: false, + }, + leadingUnderscore: 'allow', + }, + { + selector: 'typeLike', + format: ['PascalCase'], + custom: { + regex: /^I[A-Z]/.source, + match: false, + }, + }, + { + selector: 'function', + format: ['camelCase'], + custom: { + regex: /function/.source, + match: true, + }, + leadingUnderscore: 'allow', + }, + ], + errors: [ + { + messageId: 'satisfyCustom', + line: 2, + data: { + type: 'Variable', + name: 'unused_foo', + regex: '/^unused_\\w/', + regexMatch: 'not match', + }, + }, + { + messageId: 'satisfyCustom', + line: 3, + data: { + type: 'Variable', + name: '_unused_foo', + regex: '/^unused_\\w/', + regexMatch: 'not match', + }, + }, + { + messageId: 'satisfyCustom', + line: 4, + data: { + type: 'Interface', + name: 'IFoo', + regex: '/^I[A-Z]/', + regexMatch: 'not match', + }, + }, + { + messageId: 'satisfyCustom', + line: 5, + data: { + type: 'Class', + name: 'IBar', + regex: '/^I[A-Z]/', + regexMatch: 'not match', + }, + }, + { + messageId: 'satisfyCustom', + line: 6, + data: { + type: 'Function', + name: 'fooBar', + regex: '/function/', + regexMatch: 'match', + }, + }, + ], + }, ], }); From 557e8279f13f8d0c8411631bee252cdaa9e84912 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 9 Jan 2020 11:48:06 -0800 Subject: [PATCH 23/25] feat: deprecate interface-name-prefix --- packages/eslint-plugin/src/rules/interface-name-prefix.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin/src/rules/interface-name-prefix.ts b/packages/eslint-plugin/src/rules/interface-name-prefix.ts index 6f2c57dc3159..039c0d7d67c2 100644 --- a/packages/eslint-plugin/src/rules/interface-name-prefix.ts +++ b/packages/eslint-plugin/src/rules/interface-name-prefix.ts @@ -49,6 +49,8 @@ export default util.createRule({ // https://github.com/typescript-eslint/typescript-eslint/issues/374 recommended: 'error', }, + deprecated: true, + replacedBy: ['naming-convention'], messages: { noPrefix: 'Interface name must not be prefixed with "I".', alwaysPrefix: 'Interface name must be prefixed with "I".', From c7ed80eb11bb06075793ddcabcd3c853e6fd36f9 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 9 Jan 2020 11:49:39 -0800 Subject: [PATCH 24/25] docs: regen docs for deprecation --- packages/eslint-plugin/README.md | 1 - packages/eslint-plugin/src/configs/all.json | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index b0388e03cd45..bdedc634ce56 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -107,7 +107,6 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/explicit-member-accessibility`](./docs/rules/explicit-member-accessibility.md) | Require explicit accessibility modifiers on class properties and methods | | | | | [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | | [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | -| [`@typescript-eslint/interface-name-prefix`](./docs/rules/interface-name-prefix.md) | Require that interface names should or should not prefixed with `I` | :heavy_check_mark: | | | | [`@typescript-eslint/member-delimiter-style`](./docs/rules/member-delimiter-style.md) | Require a specific member delimiter style for interfaces and type literals | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/member-ordering`](./docs/rules/member-ordering.md) | Require a consistent member declaration order | | | | | [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index ff8edf834c6e..7a254536d28f 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -16,7 +16,6 @@ "@typescript-eslint/func-call-spacing": "error", "indent": "off", "@typescript-eslint/indent": "error", - "@typescript-eslint/interface-name-prefix": "error", "@typescript-eslint/member-delimiter-style": "error", "@typescript-eslint/member-ordering": "error", "@typescript-eslint/naming-convention": "error", From 6596a02b7d037feed301de9e6d0096cefad33e3f Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sun, 12 Jan 2020 19:52:40 -0800 Subject: [PATCH 25/25] fix: config checker tests --- packages/eslint-plugin/tests/configs.test.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/tests/configs.test.ts b/packages/eslint-plugin/tests/configs.test.ts index 3ec22f349e8c..eea9079f8ba3 100644 --- a/packages/eslint-plugin/tests/configs.test.ts +++ b/packages/eslint-plugin/tests/configs.test.ts @@ -8,10 +8,6 @@ function entriesToObject(value: [string, T][]): Record { }, {}); } -const notDeprecatedRules = Object.entries(rules).filter( - ([, rule]) => !rule.meta.deprecated, -); - function filterRules(values: Record): [string, string][] { return Object.entries(values).filter(([name]) => name.startsWith(RULE_NAME_PREFIX), @@ -22,10 +18,10 @@ const RULE_NAME_PREFIX = '@typescript-eslint/'; describe('all.json config', () => { const configRules = filterRules(plugin.configs.all.rules); - const ruleConfigs = notDeprecatedRules.map<[string, string]>(([name]) => [ - `${RULE_NAME_PREFIX}${name}`, - 'error', - ]); + // note: exclude deprecated rules, this config is allowed to change between minor versions + const ruleConfigs = Object.entries(rules) + .filter(([, rule]) => !rule.meta.deprecated) + .map<[string, string]>(([name]) => [`${RULE_NAME_PREFIX}${name}`, 'error']); it('contains all of the rules, excluding the deprecated ones', () => { expect(entriesToObject(ruleConfigs)).toEqual(entriesToObject(configRules)); @@ -34,7 +30,8 @@ describe('all.json config', () => { describe('recommended.json config', () => { const configRules = filterRules(plugin.configs.recommended.rules); - const ruleConfigs = notDeprecatedRules + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = Object.entries(rules) .filter( ([, rule]) => rule.meta.docs.recommended !== false && @@ -54,7 +51,8 @@ describe('recommended-requiring-type-checking.json config', () => { const configRules = filterRules( plugin.configs['recommended-requiring-type-checking'].rules, ); - const ruleConfigs = notDeprecatedRules + // note: include deprecated rules so that the config doesn't change between major bumps + const ruleConfigs = Object.entries(rules) .filter( ([, rule]) => rule.meta.docs.recommended !== false && pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy