Skip to content

Commit

Permalink
feat(eslint-plugin): Improve "interface-name-prefix" to accept a priv…
Browse files Browse the repository at this point in the history
…ate name such as "_IAnimal"
  • Loading branch information
octogonz committed Aug 3, 2019
1 parent 73f8c79 commit 44ef4a3
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 18 deletions.
37 changes: 32 additions & 5 deletions packages/eslint-plugin/docs/rules/interface-name-prefix.md
@@ -1,18 +1,21 @@
# Require that interface names be prefixed with `I` (interface-name-prefix)

It can be hard to differentiate between classes and interfaces.
Prefixing interfaces with "I" can help telling them apart at a glance.
Interfaces often represent important software contracts, so it can be helpful to prefix their names with `I`.
The unprefixed name is then available for a class that provides a standard implementation of the interface.

## Rule Details

This rule enforces consistency of interface naming prefix conventions.
This rule enforces whether or not the `I` prefix is required for interface names.

## Options

This rule has a string option.

- `"never"` (default) disallows all interfaces being prefixed with `"I"`
- `"always"` requires all interfaces be prefixed with `"I"`
- `"never"` (default) disallows all interfaces being prefixed with `"I"` (or `"_I"`)
- `"always"` requires all interfaces be prefixed with `"I"` (or `"_I"`)

The `_` prefix is sometimes used to designate a private declaration, in which case a private interface might be
named `_IAnimal` instead of `IAnimal`. The rule recognizes both forms.

### never

Expand All @@ -24,6 +27,14 @@ The following patterns are considered warnings:
interface IAnimal {
name: string;
}

interface _IAnimal {
name: string;
}

interface IIguana {
name: string;
}
```

The following patterns are not warnings:
Expand All @@ -32,6 +43,10 @@ The following patterns are not warnings:
interface Animal {
name: string;
}

interface Iguana {
name: string;
}
```

### always
Expand All @@ -42,6 +57,10 @@ The following patterns are considered warnings:
interface Animal {
name: string;
}

interface Iguana {
name: string;
}
```

The following patterns are not warnings:
Expand All @@ -50,6 +69,14 @@ The following patterns are not warnings:
interface IAnimal {
name: string;
}

interface _IAnimal {
name: string;
}

interface IIguana {
name: string;
}
```

## When Not To Use It
Expand Down
109 changes: 97 additions & 12 deletions packages/eslint-plugin/src/rules/interface-name-prefix.ts
@@ -1,8 +1,37 @@
import * as util from '../util';

type Options = ['never' | 'always'];
type ParsedOptions = {
prefixWithI: 'never';
} | {
prefixWithI: 'always';
allowUnderscorePrefix: boolean;
};
type Options = [
| 'never'
| 'always'
| {
prefixWithI?: 'never';
}
| {
prefixWithI: 'always';
allowUnderscorePrefix?: boolean;
},
];
type MessageIds = 'noPrefix' | 'alwaysPrefix';

/**
* Parses a given value as options.
*/
export function parseOptions([options]: Options): ParsedOptions {
if (options === 'always') {
return { prefixWithI: 'always', allowUnderscorePrefix: false };
}
if (options !== 'never' && options.prefixWithI === 'always') {
return { prefixWithI: 'always', allowUnderscorePrefix: !!options.allowUnderscorePrefix };
}
return { prefixWithI: 'never' };
}

export default util.createRule<Options, MessageIds>({
name: 'interface-name-prefix',
meta: {
Expand All @@ -18,13 +47,48 @@ export default util.createRule<Options, MessageIds>({
},
schema: [
{
enum: ['never', 'always'],
oneOf: [
{
enum: [
// Deprecated, equivalent to: { prefixWithI: 'never' }
'never',
// Deprecated, equivalent to: { prefixWithI: 'always', allowUnderscorePrefix: false }
'always'
],
},
{
type: 'object',
properties: {
prefixWithI: {
type: 'string',
enum: [ 'never' ]
},
},
additionalProperties: false,
},
{
type: 'object',
properties: {
prefixWithI: {
type: 'string',
enum: [ 'always' ]
},
allowUnderscorePrefix: {
type: 'boolean'
}
},
required: [ 'prefixWithI' ], // required to select this "oneOf" alternative
additionalProperties: false,
},
],
},
],
},
defaultOptions: ['never'],
create(context, [option]) {
const never = option !== 'always';
defaultOptions: [
{ prefixWithI: 'never' }
],
create(context, [options]) {
const parsedOptions = parseOptions([options]);

/**
* Checks if a string is prefixed with "I".
Expand All @@ -38,21 +102,42 @@ export default util.createRule<Options, MessageIds>({
return /^I[A-Z]/.test(name);
}

/**
* Checks if a string is prefixed with "I" or "_I".
* @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 (never) {
if (isPrefixedWithI(node.id.name)) {
if (parsedOptions.prefixWithI === 'never') {
if (isPrefixedWithIOrUnderscoreI(node.id.name)) {
context.report({
node: node.id,
messageId: 'noPrefix',
});
}
} else {
if (!isPrefixedWithI(node.id.name)) {
context.report({
node: node.id,
messageId: 'alwaysPrefix',
});
if (parsedOptions.allowUnderscorePrefix) {
if (!isPrefixedWithIOrUnderscoreI(node.id.name)) {
context.report({
node: node.id,
messageId: 'alwaysPrefix',
});
}
} else {
if (!isPrefixedWithI(node.id.name)) {
context.report({
node: node.id,
messageId: 'alwaysPrefix',
});
}
}
}
},
Expand Down
61 changes: 60 additions & 1 deletion packages/eslint-plugin/tests/rules/interface-name-prefix.test.ts
@@ -1,6 +1,42 @@
import rule from '../../src/rules/interface-name-prefix';
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']),
{ prefixWithI: 'always', allowUnderscorePrefix: false }
);
assert.deepEqual(
parseOptions([ { }]),
{ prefixWithI: 'never' }
);
assert.deepEqual(
parseOptions([
{ prefixWithI: 'never' }
]),
{ prefixWithI: 'never' }
);
assert.deepEqual(
parseOptions([
{ prefixWithI: 'always' }
]),
{ prefixWithI: 'always', allowUnderscorePrefix: false }
);
assert.deepEqual(
parseOptions([
{ prefixWithI: 'always', allowUnderscorePrefix: true }
]),
{ prefixWithI: 'always', allowUnderscorePrefix: true }
);
});
});

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});
Expand All @@ -22,6 +58,14 @@ interface IAnimal {
},
{
code: `
interface _IAnimal {
name: string;
}
`,
options: [ { prefixWithI: 'always', allowUnderscorePrefix: true }],
},
{
code: `
interface IIguana {
name: string;
}
Expand Down Expand Up @@ -117,6 +161,21 @@ interface IIguana {
code: `
interface IAnimal {
name: string;
}
`,
options: ['never'],
errors: [
{
messageId: 'noPrefix',
line: 2,
column: 11,
},
],
},
{
code: `
interface _IAnimal {
name: string;
}
`,
options: ['never'],
Expand Down

0 comments on commit 44ef4a3

Please sign in to comment.