Skip to content

Commit

Permalink
chore(eslint-plugin): [naming-convention] refactor rule to split it up (
Browse files Browse the repository at this point in the history
#2816)

The rule file was up to 1700 LOC.
It was a pain in the butt to scroll around it and find pieces.
I'm pretty sure ESLint / TypeScript gets a bit choked up on a file that large as well (I've been running into all sorts of slowness with it).

So simply isolate each part into a module to better separate things and reduce the total LOC per file.

Also moved some stuff around in the docs to try and focus each section, and add an FAQ section for the misc stuff
  • Loading branch information
bradzacher committed Nov 25, 2020
1 parent 878dd4a commit 14758d2
Show file tree
Hide file tree
Showing 11 changed files with 1,389 additions and 1,151 deletions.
213 changes: 176 additions & 37 deletions packages/eslint-plugin/docs/rules/naming-convention.md
Expand Up @@ -89,29 +89,7 @@ const defaultOptions: Options = [
### Format Options

Every single selector can have the same set of format options.
When the format of an identifier is checked, it is checked in the following order:

1. validate leading underscore
1. validate trailing underscore
1. validate prefix
1. validate suffix
1. validate custom
1. validate format

For steps 1-4, if the identifier matches the option, the matching part will be removed.
For example, if you provide the following formatting option: `{ leadingUnderscore: 'allow', prefix: ['I'], format: ['StrictPascalCase'] }`, for the identifier `_IMyInterface`, then the following checks will occur:

1. `name = _IMyInterface`
1. validate leading underscore - pass
- Trim leading underscore - `name = IMyInterface`
1. validate trailing underscore - no check
1. validate prefix - pass
- Trim prefix - `name = MyInterface`
1. validate suffix - no check
1. validate custom - no check
1. validate format - pass

One final note is that if the name were to become empty via this trimming process, it is considered to match all `format`s. An example of where this might be useful is for generic type parameters, where you want all names to be prefixed with `T`, but also want to allow for the single character `T` name.
For information about how each selector is applied, see ["How does the rule evaluate a name's format?"](#how-does-the-rule-evaluate-a-names-format).

#### `format`

Expand Down Expand Up @@ -197,20 +175,7 @@ If these are provided, the identifier must start with one of the provided values
- `array` matches any type assignable to `Array<unknown> | null | undefined`
- `function` matches any type assignable to `Function | null | undefined`

The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name.

For example, if you provide the following config:

```ts
[
/* 1 */ { selector: 'default', format: ['camelCase'] },
/* 2 */ { selector: 'variable', format: ['snake_case'] },
/* 3 */ { selector: 'variable', types: ['boolean'], format: ['UPPER_CASE'] },
/* 4 */ { selector: 'variableLike', format: ['PascalCase'] },
];
```

Then for the code `const x = 1`, the rule will validate the selectors in the following order: `3`, `2`, `4`, `1`.
The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. See ["How does the rule automatically order selectors?"](#how-does-the-rule-automatically-order-selectors)

#### Allowed Selectors, Modifiers and Types

Expand Down Expand Up @@ -295,6 +260,180 @@ Group Selectors are provided for convenience, and essentially bundle up sets of
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
- Allowed `types`: none.

## FAQ

This is a big rule, and there's a lot of docs. Here are a few clarifications that people often ask about or figure out via trial-and-error.

### How does the rule evaluate a selector?

Each selector is checked in the following way:

1. check the `selector`
1. if `selector` is one individual selector → the name's type must be of that type.
1. if `selector` is a group selector → the name's type must be one of the grouped types.
1. if `selector` is an array of selectors → apply the above for each selector in the array.
1. check the `filter`
1. if `filter` is omitted → skip this step.
1. if the name matches the `filter` → continue evaluating this selector.
1. if the name does not match the `filter` → skip this selector and continue to the next selector.
1. check the `types`
1. if `types` is omitted → skip this step.
1. if the name has a type in `types` → continue evaluating this selector.
1. if the name does not have a type in `types` → skip this selector and continue to the next selector.

A name is considered to pass the config if it:

1. Matches one selector and passes all of that selector's format checks.
2. Matches no selectors.

A name is considered to fail the config if it matches one selector and fails one that selector's format checks.

### How does the rule automatically order selectors?

Each identifier should match exactly one selector. It may match multiple group selectors - but only ever one selector.
With that in mind - the base sort order works out to be:

1. Individual Selectors
2. Grouped Selectors
3. Default Selector

Within each of these categories, some further sorting occurs based on what selector options are supplied:

1. `filter` is given the highest priority above all else.
2. `types`
3. `modifiers`
4. everything else

For example, if you provide the following config:

```ts
[
/* 1 */ { selector: 'default', format: ['camelCase'] },
/* 2 */ { selector: 'variable', format: ['snake_case'] },
/* 3 */ { selector: 'variable', types: ['boolean'], format: ['UPPER_CASE'] },
/* 4 */ { selector: 'variableLike', format: ['PascalCase'] },
];
```

Then for the code `const x = 1`, the rule will validate the selectors in the following order: `3`, `2`, `4`, `1`.
To clearly spell it out:

- (3) is tested first because it has `types` and is an individual selector.
- (2) is tested next because it is an individual selector.
- (1) is tested next as it is a grouped selector.
- (4) is tested last as it is the base default selector.

Its worth noting that whilst this order is applied, all selectors may not run on a name.
This is explained in ["How does the rule evaluate a name's format?"](#how-does-the-rule-evaluate-a-names-format)

### How does the rule evaluate a name's format?

When the format of an identifier is checked, it is checked in the following order:

1. validate leading underscore
1. validate trailing underscore
1. validate prefix
1. validate suffix
1. validate custom
1. validate format

For steps 1-4, if the identifier matches the option, the matching part will be removed.
This is done so that you can apply formats like PascalCase without worrying about prefixes or underscores causing it to not match.

One final note is that if the name were to become empty via this trimming process, it is considered to match all `format`s. An example of where this might be useful is for generic type parameters, where you want all names to be prefixed with `T`, but also want to allow for the single character `T` name.

Here are some examples to help illustrate

Name: `_IMyInterface`
Selector:

```json
{
"leadingUnderscore": "require",
"prefix": ["I"],
"format": ["UPPER_CASE", "StrictPascalCase"]
}
```

1. `name = _IMyInterface`
1. validate leading underscore
1. config is provided
1. check name → pass
1. Trim underscore → `name = IMyInterface`
1. validate trailing underscore
1. config is not provided → skip
1. validate prefix
1. config is provided
1. check name → pass
1. Trim prefix → `name = MyInterface`
1. validate suffix
1. config is not provided → skip
1. validate custom
1. config is not provided → skip
1. validate format
1. for each format...
1. `format = 'UPPER_CASE'`
1. check format → fail.
- Important to note that if you supply multiple formats - the name only needs to match _one_ of them!
1. `format = 'StrictPascalCase'`
1. check format → success.
1. **_success_**

Name: `IMyInterface`
Selector:

```json
{
"format": ["StrictPascalCase"],
"trailingUnderscore": "allow",
"custom": {
"regex": "^I[A-Z]",
"match": false
}
}
```

1. `name = IMyInterface`
1. validate leading underscore
1. config is not provided → skip
1. validate trailing underscore
1. config is provided
1. check name → pass
1. Trim underscore → `name = IMyInterface`
1. validate prefix
1. config is not provided → skip
1. validate suffix
1. config is not provided → skip
1. validate custom
1. config is provided
1. `regex = new RegExp("^I[A-Z]")`
1. `regex.test(name) === custom.match`
1. **_fail_** → report and exit

### What happens if I provide a `modifiers` to a Group Selector?

Some group selectors accept `modifiers`. For the most part these will work exactly the same as with individual selectors.
There is one exception to this in that a modifier might not apply to all individual selectors covered by a group selector.

For example - `memberLike` includes the `enumMember` selector, and it allows the `protected` modifier.
An `enumMember` can never ever be `protected`, which means that the following config will never match any `enumMember`:

```json
{
"selector": "memberLike",
"modifiers": ["protected"]
}
```

To help with matching, members that cannot specify an accessibility will always have the `public` modifier. This means that the following config will always match any `enumMember`:

```json
{
"selector": "memberLike",
"modifiers": ["public"]
}
```

## Examples

### Enforce that all variables, functions and properties follow are camelCase
Expand Down
133 changes: 133 additions & 0 deletions packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts
@@ -0,0 +1,133 @@
enum PredefinedFormats {
camelCase = 1,
strictCamelCase,
PascalCase,
StrictPascalCase,
snake_case,
UPPER_CASE,
}
type PredefinedFormatsString = keyof typeof PredefinedFormats;

enum UnderscoreOptions {
forbid = 1,
allow,
require,

// special cases as it's common practice to use double underscore
requireDouble,
allowDouble,
allowSingleOrDouble,
}
type UnderscoreOptionsString = keyof typeof UnderscoreOptions;

enum Selectors {
// variableLike
variable = 1 << 0,
function = 1 << 1,
parameter = 1 << 2,

// memberLike
parameterProperty = 1 << 3,
accessor = 1 << 4,
enumMember = 1 << 5,
classMethod = 1 << 6,
objectLiteralMethod = 1 << 7,
typeMethod = 1 << 8,
classProperty = 1 << 9,
objectLiteralProperty = 1 << 10,
typeProperty = 1 << 11,

// typeLike
class = 1 << 12,
interface = 1 << 13,
typeAlias = 1 << 14,
enum = 1 << 15,
typeParameter = 1 << 17,
}
type SelectorsString = keyof typeof Selectors;

enum MetaSelectors {
default = -1,
variableLike = 0 |
Selectors.variable |
Selectors.function |
Selectors.parameter,
memberLike = 0 |
Selectors.classProperty |
Selectors.objectLiteralProperty |
Selectors.typeProperty |
Selectors.parameterProperty |
Selectors.enumMember |
Selectors.classMethod |
Selectors.objectLiteralMethod |
Selectors.typeMethod |
Selectors.accessor,
typeLike = 0 |
Selectors.class |
Selectors.interface |
Selectors.typeAlias |
Selectors.enum |
Selectors.typeParameter,
method = 0 |
Selectors.classMethod |
Selectors.objectLiteralMethod |
Selectors.typeProperty,
property = 0 |
Selectors.classProperty |
Selectors.objectLiteralProperty |
Selectors.typeMethod,
}
type MetaSelectorsString = keyof typeof MetaSelectors;
type IndividualAndMetaSelectorsString = SelectorsString | MetaSelectorsString;

enum Modifiers {
// const variable
const = 1 << 0,
// readonly members
readonly = 1 << 1,
// static members
static = 1 << 2,
// member accessibility
public = 1 << 3,
protected = 1 << 4,
private = 1 << 5,
abstract = 1 << 6,
// destructured variable
destructured = 1 << 7,
// variables declared in the top-level scope
global = 1 << 8,
// things that are exported
exported = 1 << 9,
// things that are unused
unused = 1 << 10,
// properties that require quoting
requiresQuotes = 1 << 11,

// make sure TypeModifiers starts at Modifiers + 1 or else sorting won't work
}
type ModifiersString = keyof typeof Modifiers;

enum TypeModifiers {
boolean = 1 << 12,
string = 1 << 13,
number = 1 << 14,
function = 1 << 15,
array = 1 << 16,
}
type TypeModifiersString = keyof typeof TypeModifiers;

export {
IndividualAndMetaSelectorsString,
MetaSelectors,
MetaSelectorsString,
Modifiers,
ModifiersString,
PredefinedFormats,
PredefinedFormatsString,
Selectors,
SelectorsString,
TypeModifiers,
TypeModifiersString,
UnderscoreOptions,
UnderscoreOptionsString,
};

0 comments on commit 14758d2

Please sign in to comment.