Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add allowedPrefixes prop to interface-name-prefix #1433

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 33 additions & 2 deletions packages/eslint-plugin/docs/rules/interface-name-prefix.md
Expand Up @@ -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
Expand Down Expand Up @@ -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" }`
Expand Down
33 changes: 21 additions & 12 deletions packages/eslint-plugin/src/rules/interface-name-prefix.ts
Expand Up @@ -3,6 +3,7 @@ import * as util from '../util';
type ParsedOptions =
| {
prefixWithI: 'never';
allowedPrefixes: string[];
}
| {
prefixWithI: 'always';
Expand All @@ -13,6 +14,7 @@ type Options = [
| 'always'
| {
prefixWithI?: 'never';
allowedPrefixes?: string[];
}
| {
prefixWithI: 'always';
Expand All @@ -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<Options, MessageIds>({
Expand All @@ -58,7 +64,7 @@ export default util.createRule<Options, MessageIds>({
oneOf: [
{
enum: [
// Deprecated, equivalent to: { prefixWithI: 'never' }
// Deprecated, equivalent to: { prefixWithI: 'never', allowedPrefixes: [] }
'never',
// Deprecated, equivalent to: { prefixWithI: 'always', allowUnderscorePrefix: false }
'always',
Expand All @@ -71,6 +77,12 @@ export default util.createRule<Options, MessageIds>({
type: 'string',
enum: ['never'],
},
allowedPrefixes: {
type: 'array',
items: {
type: 'string',
},
},
},
additionalProperties: false,
},
Expand All @@ -92,7 +104,7 @@ export default util.createRule<Options, MessageIds>({
},
],
},
defaultOptions: [{ prefixWithI: 'never' }],
defaultOptions: [{ prefixWithI: 'never', allowedPrefixes: [] }],
create(context, [options]) {
const parsedOptions = parseOptions([options]);

Expand All @@ -101,10 +113,6 @@ export default util.createRule<Options, MessageIds>({
* @param name The string to check
*/
function isPrefixedWithI(name: string): boolean {
if (typeof name !== 'string') {
return false;
}

return /^I[A-Z]/.test(name);
}

Expand All @@ -113,17 +121,18 @@ export default util.createRule<Options, MessageIds>({
* @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',
Expand Down
56 changes: 47 additions & 9 deletions 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 });
});
});

Expand Down Expand Up @@ -83,6 +98,14 @@ interface I18n {
`,
options: ['never'],
},
{
code: `
interface IAMUser {
name: string;
}
`,
options: [{ allowedPrefixes: ['IAM'] }],
},
],
invalid: [
{
Expand All @@ -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;
}
Expand Down