Skip to content

Commit 0c4f474

Browse files
octogonzJamesHenry
authored andcommitted
feat(eslint-plugin): [interface-name-prefix, class-name-casing] Add allowUnderscorePrefix option to support private declarations (#790)
1 parent d3470c9 commit 0c4f474

File tree

6 files changed

+302
-25
lines changed

6 files changed

+302
-25
lines changed

packages/eslint-plugin/docs/rules/class-name-casing.md

+16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ This rule enforces PascalCased names for classes and interfaces.
55
## Rule Details
66

77
This rule aims to make it easy to differentiate classes from regular variables at a glance.
8+
The `_` prefix is sometimes used to designate a private declaration, so the rule also supports a name
9+
that might be `_Example` instead of `Example`.
10+
11+
## Options
12+
13+
This rule has an object option:
14+
15+
- `"allowUnderscorePrefix": false`: (default) does not allow the name to have an underscore prefix
16+
- `"allowUnderscorePrefix": true`: allows the name to optionally have an underscore prefix
17+
18+
## Examples
819

920
Examples of **incorrect** code for this rule:
1021

@@ -16,6 +27,8 @@ class Another_Invalid_Class_Name {}
1627
var bar = class invalidName {};
1728

1829
interface someInterface {}
30+
31+
class _InternalClass {}
1932
```
2033

2134
Examples of **correct** code for this rule:
@@ -28,6 +41,9 @@ export default class {}
2841
var foo = class {};
2942

3043
interface SomeInterface {}
44+
45+
/* eslint @typescript-eslint/class-name-casing: { "allowUnderscorePrefix": true } */
46+
class _InternalClass {}
3147
```
3248

3349
## When Not To Use It

packages/eslint-plugin/docs/rules/interface-name-prefix.md

+78-7
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,50 @@
11
# Require that interface names be prefixed with `I` (interface-name-prefix)
22

3-
It can be hard to differentiate between classes and interfaces.
4-
Prefixing interfaces with "I" can help telling them apart at a glance.
3+
Interfaces often represent important software contracts, so it can be helpful to prefix their names with `I`.
4+
The unprefixed name is then available for a class that provides a standard implementation of the interface.
5+
Alternatively, the contributor guidelines for the TypeScript repo suggest
6+
[never prefixing](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#names) interfaces with `I`.
57

68
## Rule Details
79

8-
This rule enforces consistency of interface naming prefix conventions.
10+
This rule enforces whether or not the `I` prefix is required for interface names.
11+
The `_` prefix is sometimes used to designate a private declaration, so the rule also supports a private interface
12+
that might be named `_IAnimal` instead of `IAnimal`.
913

1014
## Options
1115

12-
This rule has a string option.
16+
This rule has an object option:
1317

14-
- `"never"` (default) disallows all interfaces being prefixed with `"I"`
15-
- `"always"` requires all interfaces be prefixed with `"I"`
18+
- `{ "prefixWithI": "never" }`: (default) disallows all interfaces being prefixed with `"I"` or `"_I"`
19+
- `{ "prefixWithI": "always" }`: requires all interfaces be prefixed with `"I"` (but does not allow `"_I"`)
20+
- `{ "prefixWithI": "always", "allowUnderscorePrefix": true }`: requires all interfaces be prefixed with
21+
either `"I"` or `"_I"`
22+
23+
For backwards compatibility, this rule supports a string option instead:
24+
25+
- `"never"`: Equivalent to `{ "prefixWithI": "never" }`
26+
- `"always"`: Equivalent to `{ "prefixWithI": "always" }`
27+
28+
## Examples
1629

1730
### never
1831

19-
TypeScript suggests [never prefixing](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#names) interfaces with "I".
32+
**Configuration:** `{ "prefixWithI": "never" }`
2033

2134
The following patterns are considered warnings:
2235

2336
```ts
2437
interface IAnimal {
2538
name: string;
2639
}
40+
41+
interface IIguana {
42+
name: string;
43+
}
44+
45+
interface _IAnimal {
46+
name: string;
47+
}
2748
```
2849

2950
The following patterns are not warnings:
@@ -32,16 +53,30 @@ The following patterns are not warnings:
3253
interface Animal {
3354
name: string;
3455
}
56+
57+
interface Iguana {
58+
name: string;
59+
}
3560
```
3661

3762
### always
3863

64+
**Configuration:** `{ "prefixWithI": "always" }`
65+
3966
The following patterns are considered warnings:
4067

4168
```ts
4269
interface Animal {
4370
name: string;
4471
}
72+
73+
interface Iguana {
74+
name: string;
75+
}
76+
77+
interface _IAnimal {
78+
name: string;
79+
}
4580
```
4681

4782
The following patterns are not warnings:
@@ -50,6 +85,42 @@ The following patterns are not warnings:
5085
interface IAnimal {
5186
name: string;
5287
}
88+
89+
interface IIguana {
90+
name: string;
91+
}
92+
```
93+
94+
### always and allowing underscores
95+
96+
**Configuration:** `{ "prefixWithI": "always", "allowUnderscorePrefix": true }`
97+
98+
The following patterns are considered warnings:
99+
100+
```ts
101+
interface Animal {
102+
name: string;
103+
}
104+
105+
interface Iguana {
106+
name: string;
107+
}
108+
```
109+
110+
The following patterns are not warnings:
111+
112+
```ts
113+
interface IAnimal {
114+
name: string;
115+
}
116+
117+
interface IIguana {
118+
name: string;
119+
}
120+
121+
interface _IAnimal {
122+
name: string;
123+
}
53124
```
54125

55126
## When Not To Use It

packages/eslint-plugin/src/rules/class-name-casing.ts

+27-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import {
44
} from '@typescript-eslint/experimental-utils';
55
import * as util from '../util';
66

7-
export default util.createRule({
7+
type Options = [
8+
{
9+
allowUnderscorePrefix?: boolean;
10+
},
11+
];
12+
type MessageIds = 'notPascalCased';
13+
14+
export default util.createRule<Options, MessageIds>({
815
name: 'class-name-casing',
916
meta: {
1017
type: 'suggestion',
@@ -16,16 +23,31 @@ export default util.createRule({
1623
messages: {
1724
notPascalCased: "{{friendlyName}} '{{name}}' must be PascalCased.",
1825
},
19-
schema: [],
26+
schema: [
27+
{
28+
type: 'object',
29+
properties: {
30+
allowUnderscorePrefix: {
31+
type: 'boolean',
32+
default: false,
33+
},
34+
},
35+
additionalProperties: false,
36+
},
37+
],
2038
},
21-
defaultOptions: [],
22-
create(context) {
39+
defaultOptions: [{ allowUnderscorePrefix: false }],
40+
create(context, [options]) {
2341
/**
2442
* Determine if the identifier name is PascalCased
2543
* @param name The identifier name
2644
*/
2745
function isPascalCase(name: string): boolean {
28-
return /^[A-Z][0-9A-Za-z]*$/.test(name);
46+
if (options.allowUnderscorePrefix) {
47+
return /^_?[A-Z][0-9A-Za-z]*$/.test(name);
48+
} else {
49+
return /^[A-Z][0-9A-Za-z]*$/.test(name);
50+
}
2951
}
3052

3153
/**

packages/eslint-plugin/src/rules/interface-name-prefix.ts

+101-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
11
import * as util from '../util';
22

3-
type Options = ['never' | 'always'];
3+
type ParsedOptions =
4+
| {
5+
prefixWithI: 'never';
6+
}
7+
| {
8+
prefixWithI: 'always';
9+
allowUnderscorePrefix: boolean;
10+
};
11+
type Options = [
12+
13+
| 'never'
14+
| 'always'
15+
| {
16+
prefixWithI?: 'never';
17+
}
18+
| {
19+
prefixWithI: 'always';
20+
allowUnderscorePrefix?: boolean;
21+
},
22+
];
423
type MessageIds = 'noPrefix' | 'alwaysPrefix';
524

25+
/**
26+
* Parses a given value as options.
27+
*/
28+
export function parseOptions([options]: Options): ParsedOptions {
29+
if (options === 'always') {
30+
return { prefixWithI: 'always', allowUnderscorePrefix: false };
31+
}
32+
if (options !== 'never' && options.prefixWithI === 'always') {
33+
return {
34+
prefixWithI: 'always',
35+
allowUnderscorePrefix: !!options.allowUnderscorePrefix,
36+
};
37+
}
38+
return { prefixWithI: 'never' };
39+
}
40+
641
export default util.createRule<Options, MessageIds>({
742
name: 'interface-name-prefix',
843
meta: {
@@ -21,13 +56,46 @@ export default util.createRule<Options, MessageIds>({
2156
},
2257
schema: [
2358
{
24-
enum: ['never', 'always'],
59+
oneOf: [
60+
{
61+
enum: [
62+
// Deprecated, equivalent to: { prefixWithI: 'never' }
63+
'never',
64+
// Deprecated, equivalent to: { prefixWithI: 'always', allowUnderscorePrefix: false }
65+
'always',
66+
],
67+
},
68+
{
69+
type: 'object',
70+
properties: {
71+
prefixWithI: {
72+
type: 'string',
73+
enum: ['never'],
74+
},
75+
},
76+
additionalProperties: false,
77+
},
78+
{
79+
type: 'object',
80+
properties: {
81+
prefixWithI: {
82+
type: 'string',
83+
enum: ['always'],
84+
},
85+
allowUnderscorePrefix: {
86+
type: 'boolean',
87+
},
88+
},
89+
required: ['prefixWithI'], // required to select this "oneOf" alternative
90+
additionalProperties: false,
91+
},
92+
],
2593
},
2694
],
2795
},
28-
defaultOptions: ['never'],
29-
create(context, [option]) {
30-
const never = option !== 'always';
96+
defaultOptions: [{ prefixWithI: 'never' }],
97+
create(context, [options]) {
98+
const parsedOptions = parseOptions([options]);
3199

32100
/**
33101
* Checks if a string is prefixed with "I".
@@ -41,21 +109,42 @@ export default util.createRule<Options, MessageIds>({
41109
return /^I[A-Z]/.test(name);
42110
}
43111

112+
/**
113+
* Checks if a string is prefixed with "I" or "_I".
114+
* @param name The string to check
115+
*/
116+
function isPrefixedWithIOrUnderscoreI(name: string): boolean {
117+
if (typeof name !== 'string') {
118+
return false;
119+
}
120+
121+
return /^_?I[A-Z]/.test(name);
122+
}
123+
44124
return {
45125
TSInterfaceDeclaration(node): void {
46-
if (never) {
47-
if (isPrefixedWithI(node.id.name)) {
126+
if (parsedOptions.prefixWithI === 'never') {
127+
if (isPrefixedWithIOrUnderscoreI(node.id.name)) {
48128
context.report({
49129
node: node.id,
50130
messageId: 'noPrefix',
51131
});
52132
}
53133
} else {
54-
if (!isPrefixedWithI(node.id.name)) {
55-
context.report({
56-
node: node.id,
57-
messageId: 'alwaysPrefix',
58-
});
134+
if (parsedOptions.allowUnderscorePrefix) {
135+
if (!isPrefixedWithIOrUnderscoreI(node.id.name)) {
136+
context.report({
137+
node: node.id,
138+
messageId: 'alwaysPrefix',
139+
});
140+
}
141+
} else {
142+
if (!isPrefixedWithI(node.id.name)) {
143+
context.report({
144+
node: node.id,
145+
messageId: 'alwaysPrefix',
146+
});
147+
}
59148
}
60149
}
61150
},

packages/eslint-plugin/tests/rules/class-name-casing.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ ruleTester.run('class-name-casing', rule, {
1414
sourceType: 'module',
1515
},
1616
},
17+
{
18+
code: 'class _NameWithUnderscore {}',
19+
options: [{ allowUnderscorePrefix: true }],
20+
},
1721
'var Foo = class {};',
1822
'interface SomeInterface {}',
1923
'class ClassNameWithDigit2 {}',
@@ -50,6 +54,20 @@ ruleTester.run('class-name-casing', rule, {
5054
},
5155
],
5256
},
57+
{
58+
code: 'class _NameWithUnderscore {}',
59+
errors: [
60+
{
61+
messageId: 'notPascalCased',
62+
data: {
63+
friendlyName: 'Class',
64+
name: '_NameWithUnderscore',
65+
},
66+
line: 1,
67+
column: 7,
68+
},
69+
],
70+
},
5371
{
5472
code: 'var foo = class {};',
5573
errors: [

0 commit comments

Comments
 (0)