diff --git a/packages/eslint-plugin/docs/rules/interface-name-prefix.md b/packages/eslint-plugin/docs/rules/interface-name-prefix.md index f6439b490fd..96ebecd63c7 100644 --- a/packages/eslint-plugin/docs/rules/interface-name-prefix.md +++ b/packages/eslint-plugin/docs/rules/interface-name-prefix.md @@ -11,18 +11,21 @@ This rule enforces whether or not the `I` prefix is required for interface names The `_` prefix is sometimes used to designate a private declaration, so the rule also supports a private interface that might be named `_IAnimal` instead of `IAnimal`. +Finally, there are a few rare names that look like an `I` prefix, such as Identity and Access Management, more commonly refereed to as "IAM". +For these rare names, the rule can be provided an array of names to allow, via the `allowedPrefixes` option. + ## Options This rule has an object option: -- `{ "prefixWithI": "never" }`: (default) disallows all interfaces being prefixed with `"I"` or `"_I"` +- `{ "prefixWithI": "never", "allowedPrefixes": [] }`: (default) disallows all interfaces being prefixed with `"I"` or `"_I"` - `{ "prefixWithI": "always" }`: requires all interfaces be prefixed with `"I"` (but does not allow `"_I"`) - `{ "prefixWithI": "always", "allowUnderscorePrefix": true }`: requires all interfaces be prefixed with either `"I"` or `"_I"` For backwards compatibility, this rule supports a string option instead: -- `"never"`: Equivalent to `{ "prefixWithI": "never" }` +- `"never"`: Equivalent to `{ "prefixWithI": "never", "allowedPrefixes": [] }` - `"always"`: Equivalent to `{ "prefixWithI": "always" }` ## Examples @@ -59,6 +62,34 @@ interface Iguana { } ``` +### never and allowing names + +**Configuration:** `{ "prefixWithI": "never", "allowedPrefixes": ["IAM"] }` + +The following patterns are considered warnings: + +```ts +interface IAnimal { + name: string; +} + +interface IPMUser { + name: string; +} +``` + +The following patterns are not warnings: + +```ts +interface Animal { + name: string; +} + +interface IAMUser { + name: string; +} +``` + ### always **Configuration:** `{ "prefixWithI": "always" }` diff --git a/packages/eslint-plugin/src/rules/interface-name-prefix.ts b/packages/eslint-plugin/src/rules/interface-name-prefix.ts index 6f2c57dc315..1884878d8e8 100644 --- a/packages/eslint-plugin/src/rules/interface-name-prefix.ts +++ b/packages/eslint-plugin/src/rules/interface-name-prefix.ts @@ -3,6 +3,7 @@ import * as util from '../util'; type ParsedOptions = | { prefixWithI: 'never'; + allowedPrefixes: string[]; } | { prefixWithI: 'always'; @@ -13,6 +14,7 @@ type Options = [ | 'always' | { prefixWithI?: 'never'; + allowedPrefixes?: string[]; } | { prefixWithI: 'always'; @@ -34,7 +36,11 @@ export function parseOptions([options]: Options): ParsedOptions { allowUnderscorePrefix: !!options.allowUnderscorePrefix, }; } - return { prefixWithI: 'never' }; + return { + prefixWithI: 'never', + allowedPrefixes: + (typeof options === 'object' && options.allowedPrefixes) || [], + }; } export default util.createRule({ @@ -58,7 +64,7 @@ export default util.createRule({ oneOf: [ { enum: [ - // Deprecated, equivalent to: { prefixWithI: 'never' } + // Deprecated, equivalent to: { prefixWithI: 'never', allowedPrefixes: [] } 'never', // Deprecated, equivalent to: { prefixWithI: 'always', allowUnderscorePrefix: false } 'always', @@ -71,6 +77,12 @@ export default util.createRule({ type: 'string', enum: ['never'], }, + allowedPrefixes: { + type: 'array', + items: { + type: 'string', + }, + }, }, additionalProperties: false, }, @@ -92,7 +104,7 @@ export default util.createRule({ }, ], }, - defaultOptions: [{ prefixWithI: 'never' }], + defaultOptions: [{ prefixWithI: 'never', allowedPrefixes: [] }], create(context, [options]) { const parsedOptions = parseOptions([options]); @@ -101,10 +113,6 @@ export default util.createRule({ * @param name The string to check */ function isPrefixedWithI(name: string): boolean { - if (typeof name !== 'string') { - return false; - } - return /^I[A-Z]/.test(name); } @@ -113,17 +121,18 @@ export default util.createRule({ * @param name The string to check */ function isPrefixedWithIOrUnderscoreI(name: string): boolean { - if (typeof name !== 'string') { - return false; - } - return /^_?I[A-Z]/.test(name); } return { TSInterfaceDeclaration(node): void { if (parsedOptions.prefixWithI === 'never') { - if (isPrefixedWithIOrUnderscoreI(node.id.name)) { + if ( + isPrefixedWithIOrUnderscoreI(node.id.name) && + !parsedOptions.allowedPrefixes.some(allowedPrefix => + node.id.name.startsWith(allowedPrefix), + ) + ) { context.report({ node: node.id, messageId: 'noPrefix', diff --git a/packages/eslint-plugin/tests/rules/interface-name-prefix.test.ts b/packages/eslint-plugin/tests/rules/interface-name-prefix.test.ts index 337a96a368f..4c1776f158b 100644 --- a/packages/eslint-plugin/tests/rules/interface-name-prefix.test.ts +++ b/packages/eslint-plugin/tests/rules/interface-name-prefix.test.ts @@ -1,26 +1,41 @@ -import assert from 'assert'; import rule, { parseOptions } from '../../src/rules/interface-name-prefix'; import { RuleTester } from '../RuleTester'; describe('interface-name-prefix', () => { it('parseOptions', () => { - assert.deepEqual(parseOptions(['never']), { prefixWithI: 'never' }); - assert.deepEqual(parseOptions(['always']), { + expect(parseOptions(['never'])).toStrictEqual({ + prefixWithI: 'never', + allowedPrefixes: [], + }); + expect(parseOptions(['always'])).toStrictEqual({ prefixWithI: 'always', allowUnderscorePrefix: false, }); - assert.deepEqual(parseOptions([{}]), { prefixWithI: 'never' }); - assert.deepEqual(parseOptions([{ prefixWithI: 'never' }]), { + expect(parseOptions([{}])).toStrictEqual({ + prefixWithI: 'never', + allowedPrefixes: [], + }); + expect(parseOptions([{ prefixWithI: 'never' }])).toStrictEqual({ + prefixWithI: 'never', + allowedPrefixes: [], + }); + expect(parseOptions([{ allowedPrefixes: ['IAM'] }])).toStrictEqual({ + prefixWithI: 'never', + allowedPrefixes: ['IAM'], + }); + expect( + parseOptions([{ prefixWithI: 'never', allowedPrefixes: [] }]), + ).toStrictEqual({ prefixWithI: 'never', + allowedPrefixes: [], }); - assert.deepEqual(parseOptions([{ prefixWithI: 'always' }]), { + expect(parseOptions([{ prefixWithI: 'always' }])).toStrictEqual({ prefixWithI: 'always', allowUnderscorePrefix: false, }); - assert.deepEqual( + expect( parseOptions([{ prefixWithI: 'always', allowUnderscorePrefix: true }]), - { prefixWithI: 'always', allowUnderscorePrefix: true }, - ); + ).toStrictEqual({ prefixWithI: 'always', allowUnderscorePrefix: true }); }); }); @@ -83,6 +98,14 @@ interface I18n { `, options: ['never'], }, + { + code: ` +interface IAMUser { + name: string; +} + `, + options: [{ allowedPrefixes: ['IAM'] }], + }, ], invalid: [ { @@ -101,6 +124,21 @@ interface IAnimal { }, { code: ` +interface IAMUser { + name: string; +} + `, + options: [{ allowedPrefixes: ['IPM'] }], + errors: [ + { + messageId: 'noPrefix', + line: 2, + column: 11, + }, + ], + }, + { + code: ` interface Animal { name: string; }