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 (typescript-eslint#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

Lines changed: 16 additions & 0 deletions
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

Lines changed: 78 additions & 7 deletions
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

Lines changed: 27 additions & 5 deletions
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

Lines changed: 101 additions & 12 deletions
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

Lines changed: 18 additions & 0 deletions
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)
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