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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [interface-name-prefix, class-name-casing] Add allowUnderscorePrefix option to support private declarations #790

Merged
Show file tree
Hide file tree
Changes from 2 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
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 @@ -18,13 +53,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 @@ -38,21 +106,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