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): support negative matches for filter #1517

Merged
merged 5 commits into from Jan 30, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 19 additions & 2 deletions packages/eslint-plugin/docs/rules/naming-convention.md
Expand Up @@ -40,7 +40,12 @@ type Options = {

// selector options
selector: Selector;
filter?: string;
filter?:
| string
| {
regex: string;
match: boolean;
};
// the allowed values for these are dependent on the selector - see below
modifiers?: Modifiers<Selector>[];
types?: Types<Selector>[];
Expand Down Expand Up @@ -118,6 +123,19 @@ Accepts an object with the following properties:
- `regex` - accepts a regular expression (anything accepted into `new RegExp(regex)`).
- `match` - true if the identifier _must_ match the `regex`, false if the identifier _must not_ match the `regex`.

### `filter`

The `filter` option operates similar to `custom`, accepting the same shaped object, except that it controls if the rest of the configuration should or should not be applied to an identifier.

You can use this to include or exclude specific identifiers from specific configurations.

Accepts an object with the following properties:

- `regex` - accepts a regular expression (anything accepted into `new RegExp(regex)`).
- `match` - true if the identifier _must_ match the `regex`, false if the identifier _must not_ match the `regex`.

Alternatively, `filter` accepts a regular expression (anything accepted into `new RegExp(filter)`). In this case, it's treated as if you had passed an object with the regex and `match: true`.

#### `leadingUnderscore` / `trailingUnderscore`

The `leadingUnderscore` / `trailingUnderscore` options control whether leading/trailing underscores are considered valid. Accepts one of the following values:
Expand All @@ -135,7 +153,6 @@ If these are provided, the identifier must start with one of the provided values
### Selector Options

- `selector` (see "Allowed Selectors, Modifiers and Types" below).
- `filter` accepts a regular expression (anything accepted into `new RegExp(filter)`). It allows you to limit the scope of this configuration to names that match this regex.
- `modifiers` allows you to specify which modifiers to granularly apply to, such as the accessibility (`private`/`public`/`protected`), or if the thing is `static`, etc.
- The name must match _all_ of the modifiers.
- For example, if you provide `{ modifiers: ['private', 'static', 'readonly'] }`, then it will only match something that is `private static readonly`, and something that is just `private` will not match.
Expand Down
54 changes: 36 additions & 18 deletions packages/eslint-plugin/src/rules/naming-convention.ts
Expand Up @@ -113,7 +113,12 @@ interface Selector {
selector: IndividualAndMetaSelectorsString;
modifiers?: ModifiersString[];
types?: TypeModifiersString[];
filter?: string;
filter?:
| string
| {
regex: string;
match: boolean;
};
}
interface NormalizedSelector {
// format options
Expand All @@ -130,7 +135,10 @@ interface NormalizedSelector {
selector: Selectors | MetaSelectors;
modifiers: Modifiers[] | null;
types: TypeModifiers[] | null;
filter: RegExp | null;
filter: {
regex: RegExp;
match: boolean;
} | null;
// calculated ordering weight based on modifiers
modifierWeight: number;
}
Expand All @@ -156,6 +164,14 @@ const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = {
},
additionalItems: false,
};
const MATCH_REGEX_SCHEMA: JSONSchema.JSONSchema4 = {
type: 'object',
properties: {
match: { type: 'boolean' },
regex: { type: 'string' },
},
required: ['match', 'regex'],
};
type JSONSchemaProperties = Record<string, JSONSchema.JSONSchema4>;
const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = {
format: {
Expand All @@ -173,18 +189,7 @@ const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = {
},
],
},
custom: {
type: 'object',
properties: {
regex: {
type: 'string',
},
match: {
type: 'boolean',
},
},
required: ['regex', 'match'],
},
custom: MATCH_REGEX_SCHEMA,
leadingUnderscore: UNDERSCORE_SCHEMA,
trailingUnderscore: UNDERSCORE_SCHEMA,
prefix: PREFIX_SUFFIX_SCHEMA,
Expand All @@ -197,8 +202,13 @@ function selectorSchema(
): JSONSchema.JSONSchema4[] {
const selector: JSONSchemaProperties = {
filter: {
type: 'string',
minLength: 1,
oneOf: [
{
type: 'string',
minLength: 1,
},
MATCH_REGEX_SCHEMA,
],
},
selector: {
type: 'string',
Expand Down Expand Up @@ -797,7 +807,7 @@ function createValidator(
// return will break the loop and stop checking configs
// it is only used when the name is known to have failed or succeeded a config.
for (const config of configs) {
if (config.filter?.test(originalName) === false) {
if (config.filter?.regex.test(originalName) !== config.filter?.match) {
// name does not match the filter
continue;
}
Expand Down Expand Up @@ -1216,7 +1226,15 @@ function normalizeOption(option: Selector): NormalizedSelector {
: Selectors[option.selector],
modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null,
types: option.types?.map(m => TypeModifiers[m]) ?? null,
filter: option.filter !== undefined ? new RegExp(option.filter) : null,
filter:
option.filter !== undefined
? typeof option.filter === 'string'
? { regex: new RegExp(option.filter), match: true }
: {
regex: new RegExp(option.filter.regex),
match: option.filter.match,
}
: null,
// calculated ordering weight based on modifiers
modifierWeight: weight,
};
Expand Down
43 changes: 40 additions & 3 deletions packages/eslint-plugin/tests/rules/naming-convention.test.ts
Expand Up @@ -80,7 +80,11 @@ const formatTestNames: Readonly<Record<
};

const REPLACE_REGEX = /%/g;
const IGNORED_REGEX = /^.(?!gnored)/; // negative lookahead to not match `[iI]gnored`
// filter to not match `[iI]gnored`
const IGNORED_FILTER = {
match: false,
regex: /.gnored/.source,
};

type Cases = {
code: string[];
Expand All @@ -100,7 +104,7 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase<Options>[] {
options: [
{
...options,
filter: IGNORED_REGEX.source,
filter: IGNORED_FILTER,
},
],
code: `// ${JSON.stringify(options)}\n${test.code
Expand Down Expand Up @@ -206,7 +210,7 @@ function createInvalidTestCases(
options: [
{
...options,
filter: IGNORED_REGEX.source,
filter: IGNORED_FILTER,
},
],
code: `// ${JSON.stringify(options)}\n${test.code
Expand Down Expand Up @@ -606,6 +610,22 @@ ruleTester.run('naming-convention', rule, {
valid: [
`const x = 1;`, // no options shouldn't crash
...createValidTestCases(cases),
{
code: `
const child_process = require('child_process');
`,
parserOptions,
options: [
{
selector: 'default',
format: ['camelCase'],
filter: {
regex: 'child_process',
match: false,
},
},
],
},
{
code: `
declare const string_camelCase: string;
Expand Down Expand Up @@ -742,6 +762,23 @@ ruleTester.run('naming-convention', rule, {
],
invalid: [
...createInvalidTestCases(cases),
{
code: `
const child_process = require('child_process');
`,
parserOptions,
options: [
{
selector: 'default',
format: ['camelCase'],
filter: {
regex: 'child_process',
match: true,
},
},
],
errors: [{ messageId: 'doesNotMatchFormat' }],
},
{
code: `
declare const string_camelCase01: string;
Expand Down