Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(eslint-plugin): [interface-name-prefix, class-name-casing] Add a…
…llowUnderscorePrefix option to support private declarations (#790)
  • Loading branch information
octogonz authored and JamesHenry committed Aug 13, 2019
1 parent d3470c9 commit 0c4f474
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 25 deletions.
16 changes: 16 additions & 0 deletions packages/eslint-plugin/docs/rules/class-name-casing.md
Expand Up @@ -5,6 +5,17 @@ This rule enforces PascalCased names for classes and interfaces.
## Rule Details

This rule aims to make it easy to differentiate classes from regular variables at a glance.
The `_` prefix is sometimes used to designate a private declaration, so the rule also supports a name
that might be `_Example` instead of `Example`.

## Options

This rule has an object option:

- `"allowUnderscorePrefix": false`: (default) does not allow the name to have an underscore prefix
- `"allowUnderscorePrefix": true`: allows the name to optionally have an underscore prefix

## Examples

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

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

interface someInterface {}

class _InternalClass {}
```

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

interface SomeInterface {}

/* eslint @typescript-eslint/class-name-casing: { "allowUnderscorePrefix": true } */
class _InternalClass {}
```

## When Not To Use It
Expand Down
85 changes: 78 additions & 7 deletions packages/eslint-plugin/docs/rules/interface-name-prefix.md
@@ -1,29 +1,50 @@
# 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.
Alternatively, the contributor guidelines for the TypeScript repo suggest
[never prefixing](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#names) interfaces with `I`.

## 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.
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`.

## Options

This rule has a string option.
This rule has an object option:

- `"never"` (default) disallows all interfaces being prefixed with `"I"`
- `"always"` requires all interfaces be prefixed with `"I"`
- `{ "prefixWithI": "never" }`: (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" }`
- `"always"`: Equivalent to `{ "prefixWithI": "always" }`

## Examples

### never

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

The following patterns are considered warnings:

```ts
interface IAnimal {
name: string;
}

interface IIguana {
name: string;
}

interface _IAnimal {
name: string;
}
```

The following patterns are not warnings:
Expand All @@ -32,16 +53,30 @@ The following patterns are not warnings:
interface Animal {
name: string;
}

interface Iguana {
name: string;
}
```

### always

**Configuration:** `{ "prefixWithI": "always" }`

The following patterns are considered warnings:

```ts
interface Animal {
name: string;
}

interface Iguana {
name: string;
}

interface _IAnimal {
name: string;
}
```

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

interface IIguana {
name: string;
}
```

### always and allowing underscores

**Configuration:** `{ "prefixWithI": "always", "allowUnderscorePrefix": true }`

The following patterns are considered warnings:

```ts
interface Animal {
name: string;
}

interface Iguana {
name: string;
}
```

The following patterns are not warnings:

```ts
interface IAnimal {
name: string;
}

interface IIguana {
name: string;
}

interface _IAnimal {
name: string;
}
```

## When Not To Use It
Expand Down
32 changes: 27 additions & 5 deletions packages/eslint-plugin/src/rules/class-name-casing.ts
Expand Up @@ -4,7 +4,14 @@ import {
} from '@typescript-eslint/experimental-utils';
import * as util from '../util';

export default util.createRule({
type Options = [
{
allowUnderscorePrefix?: boolean;
},
];
type MessageIds = 'notPascalCased';

export default util.createRule<Options, MessageIds>({
name: 'class-name-casing',
meta: {
type: 'suggestion',
Expand All @@ -16,16 +23,31 @@ export default util.createRule({
messages: {
notPascalCased: "{{friendlyName}} '{{name}}' must be PascalCased.",
},
schema: [],
schema: [
{
type: 'object',
properties: {
allowUnderscorePrefix: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
},
],
},
defaultOptions: [],
create(context) {
defaultOptions: [{ allowUnderscorePrefix: false }],
create(context, [options]) {
/**
* Determine if the identifier name is PascalCased
* @param name The identifier name
*/
function isPascalCase(name: string): boolean {
return /^[A-Z][0-9A-Za-z]*$/.test(name);
if (options.allowUnderscorePrefix) {
return /^_?[A-Z][0-9A-Za-z]*$/.test(name);
} else {
return /^[A-Z][0-9A-Za-z]*$/.test(name);
}
}

/**
Expand Down
113 changes: 101 additions & 12 deletions packages/eslint-plugin/src/rules/interface-name-prefix.ts
@@ -1,8 +1,43 @@
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 @@ -21,13 +56,46 @@ 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 @@ -41,21 +109,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
18 changes: 18 additions & 0 deletions packages/eslint-plugin/tests/rules/class-name-casing.test.ts
Expand Up @@ -14,6 +14,10 @@ ruleTester.run('class-name-casing', rule, {
sourceType: 'module',
},
},
{
code: 'class _NameWithUnderscore {}',
options: [{ allowUnderscorePrefix: true }],
},
'var Foo = class {};',
'interface SomeInterface {}',
'class ClassNameWithDigit2 {}',
Expand Down Expand Up @@ -50,6 +54,20 @@ ruleTester.run('class-name-casing', rule, {
},
],
},
{
code: 'class _NameWithUnderscore {}',
errors: [
{
messageId: 'notPascalCased',
data: {
friendlyName: 'Class',
name: '_NameWithUnderscore',
},
line: 1,
column: 7,
},
],
},
{
code: 'var foo = class {};',
errors: [
Expand Down

0 comments on commit 0c4f474

Please sign in to comment.