├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── new_rule.md
│ └── other.md
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── MIGRATION.md
├── README.md
├── docs
└── rules
│ ├── confusing-quantifier.md
│ ├── consistent-match-all-characters.md
│ ├── disjoint-alternatives.md
│ ├── identity-escape.md
│ ├── no-constant-capturing-group.md
│ ├── no-empty-alternative.md
│ ├── no-empty-backreference.md
│ ├── no-empty-lookaround.md
│ ├── no-lazy-ends.md
│ ├── no-obscure-range.md
│ ├── no-octal-escape.md
│ ├── no-optional-assertion.md
│ ├── no-potentially-empty-backreference.md
│ ├── no-trivially-nested-lookaround.md
│ ├── no-trivially-nested-quantifier.md
│ ├── no-unnecessary-assertions.md
│ ├── no-unnecessary-character-class.md
│ ├── no-unnecessary-flag.md
│ ├── no-unnecessary-group.md
│ ├── no-unnecessary-lazy.md
│ ├── no-unnecessary-quantifier.md
│ ├── no-zero-quantifier.md
│ ├── optimal-concatenation-quantifier.md
│ ├── optimal-lookaround-quantifier.md
│ ├── optimized-character-class.md
│ ├── prefer-character-class.md
│ ├── prefer-predefined-assertion.md
│ ├── prefer-predefined-character-set.md
│ ├── prefer-predefined-quantifiers.md
│ ├── simple-constant-quantifier.md
│ └── sort-flags.md
├── gulpfile.js
├── lib
├── ast-util.ts
├── char-util.ts
├── configs.ts
├── fa-util.ts
├── format.ts
├── index.ts
├── rules-util.ts
├── rules
│ ├── confusing-quantifier.ts
│ ├── consistent-match-all-characters.ts
│ ├── disjoint-alternatives.ts
│ ├── identity-escape.ts
│ ├── no-constant-capturing-group.ts
│ ├── no-empty-alternative.ts
│ ├── no-empty-backreference.ts
│ ├── no-empty-lookaround.ts
│ ├── no-lazy-ends.ts
│ ├── no-obscure-range.ts
│ ├── no-octal-escape.ts
│ ├── no-optional-assertion.ts
│ ├── no-potentially-empty-backreference.ts
│ ├── no-trivially-nested-lookaround.ts
│ ├── no-trivially-nested-quantifier.ts
│ ├── no-unnecessary-assertions.ts
│ ├── no-unnecessary-character-class.ts
│ ├── no-unnecessary-flag.ts
│ ├── no-unnecessary-group.ts
│ ├── no-unnecessary-lazy.ts
│ ├── no-unnecessary-quantifier.ts
│ ├── no-zero-quantifier.ts
│ ├── optimal-concatenation-quantifier.ts
│ ├── optimal-lookaround-quantifier.ts
│ ├── optimized-character-class.ts
│ ├── prefer-character-class.ts
│ ├── prefer-predefined-assertion.ts
│ ├── prefer-predefined-character-set.ts
│ ├── prefer-predefined-quantifiers.ts
│ ├── simple-constant-quantifier.ts
│ └── sort-flags.ts
└── util.ts
├── package-lock.json
├── package.json
├── tests
├── lib
│ ├── config.ts
│ ├── fixable.ts
│ ├── rules
│ │ ├── confusing-quantifier.ts
│ │ ├── consistent-match-all-characters.ts
│ │ ├── disjoint-alternatives.ts
│ │ ├── identity-escape.ts
│ │ ├── no-constant-capturing-group.ts
│ │ ├── no-empty-alternative.ts
│ │ ├── no-empty-backreference.ts
│ │ ├── no-empty-lookaround.ts
│ │ ├── no-lazy-ends.ts
│ │ ├── no-obscure-range.ts
│ │ ├── no-octal-escape.ts
│ │ ├── no-optional-assertion.ts
│ │ ├── no-potentially-empty-backreference.ts
│ │ ├── no-trivially-nested-lookaround.ts
│ │ ├── no-trivially-nested-quantifier.ts
│ │ ├── no-unnecessary-assertions.ts
│ │ ├── no-unnecessary-character-class.ts
│ │ ├── no-unnecessary-flag.ts
│ │ ├── no-unnecessary-group.ts
│ │ ├── no-unnecessary-lazy.ts
│ │ ├── no-unnecessary-quantifier.ts
│ │ ├── no-zero-quantifier.ts
│ │ ├── optimal-concatenation-quantifier.ts
│ │ ├── optimal-lookaround-quantifier.ts
│ │ ├── optimized-character-class.ts
│ │ ├── prefer-character-class.ts
│ │ ├── prefer-predefined-assertion.ts
│ │ ├── prefer-predefined-character-set.ts
│ │ ├── prefer-predefined-quantifiers.ts
│ │ ├── simple-constant-quantifier.ts
│ │ └── sort-flags.ts
│ └── util.ts
├── project.ts
└── test-util.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | insert_final_newline = true
5 | charset = utf-8
6 | indent_style = tab
7 | indent_size = 4
8 |
9 | [{.prettierrc,package.json,tsconfig.json}]
10 | indent_style = space
11 | indent_size = 2
12 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | gulpfile.js
2 | dist/
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es6": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": 2018,
8 | "sourceType": "module",
9 | "ecmaFeatures": {
10 | "node": true,
11 | "spread": true
12 | }
13 | },
14 | "extends": [
15 | "eslint:recommended",
16 | "plugin:@typescript-eslint/eslint-recommended",
17 | "plugin:@typescript-eslint/recommended",
18 | "plugin:prettier/recommended"
19 | ],
20 | "parser": "@typescript-eslint/parser",
21 | "plugins": [
22 | "@typescript-eslint",
23 | "prettier"
24 | ],
25 | "rules": {
26 | "semi": [
27 | "error",
28 | "always"
29 | ],
30 | "quotes": [
31 | "error",
32 | "double"
33 | ],
34 | "no-mixed-spaces-and-tabs": "off",
35 | "curly": ["error", "all"],
36 | // this plugin is literally all about regular expressions, so a few ESLint's rules get in the way
37 | "no-useless-escape": "off",
38 | "no-empty-character-class": "off", // this rule has massive problems with the new s flag
39 | "no-control-regex": "off"
40 | },
41 | "overrides": [
42 | {
43 | "files": [
44 | "tests/**"
45 | ],
46 | "env": {
47 | "mocha": true
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report.
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Description
11 |
12 |
13 | ## Example
14 |
15 |
16 | ```js
17 | // eslint configuration
18 | ```
19 |
20 | ```js
21 | // code to reproduce the issue
22 | ```
23 |
24 | ### Actual output
25 |
26 |
27 | ### Expected output
28 |
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/new_rule.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: New rule
3 | about: Suggest a new rule.
4 | title: ''
5 | labels: rule, new rule
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Motivation
11 |
12 |
13 | ## Description
14 |
15 |
16 | ## Example
17 |
18 |
19 | ```js
20 | // one or multiple regular expressions
21 | ```
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/other.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Other
3 | about: An issue that doesn't fall into the other categories.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Description
11 |
12 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | check:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version: [12.x, 14.x]
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v1
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 | - run: npm ci
25 | - run: npm run check
26 | env:
27 | CI: true
28 |
29 | test:
30 | runs-on: ubuntu-latest
31 | strategy:
32 | matrix:
33 | node-version: [12.x, 14.x]
34 | steps:
35 | - uses: actions/checkout@v2
36 | - name: Use Node.js ${{ matrix.node-version }}
37 | uses: actions/setup-node@v1
38 | with:
39 | node-version: ${{ matrix.node-version }}
40 | - run: npm ci
41 | - run: npm test
42 | env:
43 | CI: true
44 |
45 | no-changes:
46 | runs-on: ubuntu-latest
47 | strategy:
48 | matrix:
49 | node-version: [12.x]
50 | steps:
51 | - uses: actions/checkout@v2
52 | - name: Use Node.js ${{ matrix.node-version }}
53 | uses: actions/setup-node@v1
54 | with:
55 | node-version: ${{ matrix.node-version }}
56 | - run: npm ci
57 | - run: npm run build
58 | - run: git diff HEAD --exit-code # detect changes
59 | env:
60 | CI: true
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .vscode/
3 | .eslintcache
4 | debug.log
5 | dist/
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | tests/lib/rules
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 4,
4 | "useTabs": true,
5 | "arrowParens": "avoid",
6 | "quoteProps": "consistent",
7 | "semi": true
8 | }
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 | ## 0.5.2 (2021-10-11)
5 |
6 | The project is now officially deprecated and will not be worked on anymore.
7 |
8 | Please use [eslint-plugin-regexp](https://github.com/ota-meshi/eslint-plugin-regexp) instead. You can find a [migration guide here](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/MIGRATION.md).
9 |
10 |
11 | ## 0.5.1 (2021-03-25)
12 |
13 | Lots of minor fixes and improvements.
14 |
15 | ### Added
16 |
17 | - Contributing guide and issue templates
18 |
19 | ### Changed
20 |
21 | - `no-trivially-nested-quantifier`: Improved the condition that decides whether quantifiers are trivially nested. The rule will now be able to detect more cases.
22 | - Improved many functions that analyse the RegExp AST.
23 | - Updated to latest refa version.
24 |
25 | ### Fixed
26 |
27 | - Some typos.
28 |
29 |
30 | ## 0.5.0 (2020-10-26)
31 |
32 | I ❤️ Typescript + Prettier!
33 | The whole project is now implemented in Typescript and uses Prettier for formatting.
34 |
35 | ### Added
36 |
37 | - `no-obscure-range`: A new rule to report most likely unintended ranges in character classes due to unescaped `-`s.
38 | - `no-empty-alternative`: A new rule to detect empty alternatives in groups and patterns that are likely a mistake.
39 | - `prefer-predefined-assertion`: This will suggest predefined assertions (`\b\B^$`) if a lookaround behaves like one.
40 | - `disjoint-alternatives`: A new rule to report disjoint alternatives to prevent mistakes and exponential backtracking.
41 | - `no-trivially-nested-quantifier`: A new rule to fix trivially quantifiers.
42 |
43 | ### Changed
44 |
45 | - `no-unnecessary-group`: New option to allow non-capturing groups that wrap the whole regex.
46 | - `prefer-character-class`: The rule is now also active inside lookarounds, will reorder alternatives, and more aggressively merge into existing character classes.
47 | - `no-unnecessary-lazy`: It can now analyse single-character quantifiers.
48 | - `prefer-predefined-character-set`: It will now leave `0-9` as is.
49 | - `optimal-concatenation-quantifier`: It can now merge characters and quantifiers.
50 | - New rules table in README.
51 | - Better error messages for a lot of rules.
52 |
53 | ### Fixed
54 |
55 | - `no-empty-lookaround` didn't handle backreferences correctly.
56 | - Many, many typos and other mistakes in the docs.
57 |
58 |
59 | ## 0.4.0 (2020-05-05)
60 |
61 | Clean regex now uses [refa](https://github.com/RunDevelopment/refa), a library for NFA and DFA operations and converting JS RegExp to NFA.
62 | Right now, this plugin only really uses the `CharSet` API but implementing more complex rules is now possible!
63 |
64 | ### Added
65 |
66 | - `no-unnecessary-assertions`: A new rule to report trivially accepting/rejecting assertions.
67 | - `simple-constant-quantifier`: A new rule to simplify constant quantifiers with use the range syntax (e.g. `a{2,2}`).
68 | - Added changelog.
69 |
70 | ### Changed
71 |
72 | - `optimized-character-class`: Improved reporting and fixing thanks to a new implementation based on refa's character sets.
73 |
74 |
75 | ## 0.3.0 (2020-04-05)
76 |
77 | ### Added
78 |
79 | - `no-lazy-ends`: A new rule to report lazy quantifiers at the end of the expression tree.
80 | - `no-unnecessary-lazy`: A new rule to report and fix unnecessary lazy modifiers.
81 | - `no-empty-backreference`: A new rule to report backreferences that will always be replaced with the empty string.
82 | - `no-potentially-empty-backreference`: A new rule to report backreferences that might be replaced with the empty string only sometimes.
83 |
84 | ### Fixed
85 |
86 | - `no-empty-backreference` didn't handle lookbehinds correctly ([#17](https://github.com/RunDevelopment/eslint-plugin-clean-regex/issues/17))
87 |
88 | ### Removed
89 |
90 | - `no-early-backreference`: Use `no-empty-backreference` instead.
91 |
92 |
93 | ## 0.2.2 (2020-04-01)
94 |
95 | ### Fixed
96 |
97 | - Fixed examples in `README.md`.
98 |
99 |
100 | ## 0.2.1 (2020-04-01)
101 |
102 | ### Fixed
103 |
104 | - Fixed examples in `README.md`.
105 |
106 |
107 | ## 0.2.0 (2020-04-01)
108 |
109 | ### Changed
110 |
111 | - `no-constant-capturing-group` will now ignore non-empty constant capturing groups by default.
112 | - Added more documentation to almost every rule. All rules are now documented.
113 | - `README.md` now includes a small "Highlights" section.
114 |
115 |
116 | ## 0.1.0 (2020-03-22)
117 |
118 | Initial release
119 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you so much for contributing!
4 |
5 | You can contribute to the project in a number of ways:
6 |
7 | - Open bug reports.
8 |
9 | This might be a rule over-reporting, under-reporting, crashing, or providing
10 | incorrect fixes, suggestions, or messages.
11 |
12 | - Suggest new rules and rule options.
13 |
14 | If you have a good idea for a new rule, by all means, please share! This
15 | might be anything from stylistic suggestions to reporting potential bugs in
16 | regular expressions.
17 |
18 | You can also suggest new options to further customize existing rules.
19 |
20 | - Open pull requests.
21 |
22 | This might be anything from fixing typos to implementing new rules.
23 |
24 | Before you make a PR for a new rule, consider opening an issue first and say
25 | that you consider to or are already implementing it. Maybe there's someone
26 | already working on something similar or the project owner might be able to
27 | give some advice.
28 |
29 | # Naming conventions
30 |
31 | The names of rules and rule options are part of the API of the ESLint plugin and
32 | have to be stable. Once the name of a rule has been chosen, it can't be (easily)
33 | changed anymore. Choose names carefully.
34 |
35 | ## Rules
36 |
37 | - Use lower snake-case (e.g. `identity-escape`).
38 | - Use singular (e.g. `no-unnecessary-flag`).
39 | - Only use plural if the rule is explicitly about the relationship of at least
40 | two parts of the pattern (e.g. `disjoint-alternatives`).
41 | - Don't use abbreviations or contractions (e.g. "don't").
42 | - Keep it short (< 5 words).
43 |
44 | ## Rule options
45 |
46 | - Use lower camel case (e.g. `noTop`).
47 | - Don't use abbreviations or contractions (e.g. "don't").
48 | - Keep it short (< 5 words).
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Michael Schmidt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/rules/confusing-quantifier.md:
--------------------------------------------------------------------------------
1 | # `confusing-quantifier`
2 |
3 | > Warn about confusing quantifiers.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/confusing-quantifier.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/confusing-quantifier.ts)
9 |
10 | ## Description
11 |
12 | Confusing quantifiers are ones which imply one thing but don't deliver on that.
13 |
14 | An example of this is `(?:a?b*|c+){4}`. The group is quantified with `{4}` which
15 | implies that at least 4 characters will be matched but this is not the case. The
16 | whole pattern will match the empty string. It does that because in the `a?b*`
17 | alternative, it's possible to choose 0 many `a` and `b`. So rather than `{4}`,
18 | `{0,4}` should be used to reflect the fact that the empty string can be matched.
19 |
20 | ### Examples
21 |
22 | Examples of **valid** code for this rule:
23 |
24 |
25 | ```js
26 | /a*/
27 | /(a|b|c)+/
28 | /a?/
29 | ```
30 |
31 | Examples of **invalid** code for this rule:
32 |
33 |
34 | ```js
35 | /(a?){4}/ // warns about `{4}`
36 | /(a?b*)+/ // warns about `+`
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/rules/consistent-match-all-characters.md:
--------------------------------------------------------------------------------
1 | # `consistent-match-all-characters` :wrench:
2 |
3 | > Use one character class consistently whenever all characters have to be
4 | > matched.
5 |
6 | configuration in `plugin:clean-regex/recommended`: `"warn"`
7 |
8 |
9 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/consistent-match-all-characters.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/consistent-match-all-characters.ts)
10 |
11 | ## Description
12 |
13 | There are multiple ways to create a character class which matches all
14 | characters. This rule can be used to enforce one consistent way to do that.
15 |
16 | #### Example
17 |
18 |
19 | ```js
20 | /[\s\S]/ -> /[\s\S]/
21 | /[\d\D]/ -> /[\s\S]/
22 | /[\D\w]/ -> /[\s\S]/
23 | /[^]/ -> /[\s\S]/
24 | /[\0-\uFFFF]/ -> /[\s\S]/
25 | ```
26 |
27 | ### `charClass: string`
28 |
29 | By default all match-all character classes will be replaced with `[\s\S]`. To
30 | change that you can set the `charClass` option to the replacement string.
31 |
32 | #### `charClass: "[^]"`
33 |
34 |
35 | ```js
36 | /[\s\S]/ -> /[^]/
37 | /[\d\D]/ -> /[^]/
38 | /[\D\w]/ -> /[^]/
39 | /[^]/ -> /[^]/
40 | /[\0-\uFFFF]/ -> /[^]/
41 | ```
42 |
43 | ### Replacement modes
44 |
45 | The replacement mode will determine how this rule will replace match-all
46 | character classes and sets.
47 |
48 | #### `mode: "dot-if-dotAll"`
49 |
50 | This is the default mode. It will replace all match-all character classes with
51 | `charClass` if the `s` flag is not present. If the `s` flag is present, all
52 | match-all character classes will be replaced with a dot.
53 |
54 |
55 | ```js
56 | // (with charClass: "[^]")
57 | /[\s\S]/ -> /[^]/
58 | /[\s\S]/s -> /./s
59 | /./s -> /./s
60 | ```
61 |
62 | #### `mode: "char-class"`
63 |
64 | In this mode, all match-all character classes and sets will be replaced with
65 | `charClass`.
66 |
67 | If the `s` flag is present, it will be removed.
68 |
69 |
70 | ```js
71 | // (with charClass: "[^]")
72 | /[\s\S]/ -> /[^]/
73 | /[\s\S]/s -> /[^]/
74 | /./s -> /[^]/
75 | ```
76 |
77 | #### `mode: "dot"`
78 |
79 | In this mode, all match-all character classes and sets will be replaced with a
80 | dot. (The `charClass` options won't be used.)
81 |
82 | If the `s` flag is not present, it will be added.
83 |
84 |
85 | ```js
86 | /[\s\S]/ -> /./s
87 | /[\s\S]/s -> /./s
88 | /./s -> /./s
89 | ```
90 |
--------------------------------------------------------------------------------
/docs/rules/identity-escape.md:
--------------------------------------------------------------------------------
1 | # `identity-escape` :wrench:
2 |
3 | > How to handle identity escapes.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/identity-escape.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/identity-escape.ts)
9 |
10 | ## Description
11 |
12 | This rule allows you to configure where identity escapes are allowed,
13 | disallowed, or even required.
14 |
15 | Identity escapes are escape sequences where the escaped character is equal to
16 | the escape sequence. Examples: `\(`, `\+`, `\\`, `\/`, `\m` (not in unicode
17 | mode)
18 |
19 | ## Rules
20 |
21 | ### How to define rules
22 |
23 | The `identity-escape` rule is completely configurable via its own ruleset. To
24 | define a rule, add this to the `identity-escape` rule options:
25 |
26 |
27 | ```json
28 | "clean-regex/identity-escape": ["warn", {
29 | "rules": [
30 | // rules
31 | ]
32 | }]
33 | ```
34 |
35 | Each rule is an object like this:
36 |
37 |
38 | ```json
39 | {
40 | "name": "",
41 | "escape": "allow",
42 | "context": "inside",
43 | "characters": "/"
44 | }
45 | ```
46 |
47 | #### `characters`
48 |
49 | The `characters` property determines for which characters this rule applies. The
50 | string is regex pattern that is either a single character (e.g. `"a"`), a single
51 | character set (e.g. `"\w"`), or a single character class (e.g. `"[^a-zA-Z]"`).
52 | This pattern will be interpreted in unicode-mode (`u` flag enabled), so
53 | `"[\0-\u{10FFFF}]"` can be used to create a rule that applies to all characters.
54 |
55 | #### `escape`
56 |
57 | The `escape` property determines how the characters the rule applies to have to
58 | be escaped. `escape` has to be set one of the following three values:
59 |
60 | - `"require"`: All characters the rule applies to have to be escaped.
61 | - `"disallow"`: All characters the rule applies to must not be escaped.
62 | - `"allow"`: All characters the rule applies to can be escaped or unescaped.
63 | (This is the "don't care"-option.)
64 |
65 | #### `context`
66 |
67 | The `context` property is an additional requirement that has to be met for the
68 | rule to apply. It determines whether the rule applies to characters inside
69 | character classes, outside or both. Possible values are:
70 |
71 | - `"inside"`: The rule only applies to characters inside character classes.
72 | - `"outside"`: The rule only applies to characters outside character classes.
73 | - `"all"`: The rule applies to all characters.
74 |
75 | This property is set to `"all"` by default.
76 |
77 | #### `name`
78 |
79 | This optional property can be used to give a rule a name. The name of a rule
80 | will be included in the message for characters/escapes it changes.
81 |
82 | ### Evaluation
83 |
84 | If a character or identity escape can be turned into the other without changing
85 | the meaning of the pattern, `identity-escape` will check all rules. The escape
86 | option of the first rule that applies to the character or identity escape will
87 | be enforced. Rules are checked in the order in which they are defined with
88 | standard rules being checked last.
89 |
90 | This means that it is possible for rules to overshadow each other. This can be
91 | used to overwrite the standard behavior of `identity-escape`.
92 |
93 | ### Standard rules
94 |
95 | There are a few standard rules that are always active but may be overshadowed by
96 | custom rules. They are defined as follows:
97 |
98 |
99 | ```json
100 | [
101 | {
102 | "name": "standard:opening-square-bracket",
103 | "escape": "require",
104 | "context": "inside",
105 | "characters": "\\["
106 | },
107 | {
108 | "name": "standard:closing-square-bracket",
109 | "escape": "require",
110 | "context": "outside",
111 | "characters": "\\]"
112 | },
113 | {
114 | "name": "standard:curly-braces",
115 | "escape": "require",
116 | "context": "outside",
117 | "characters": "[{}]"
118 | },
119 | {
120 | "name": "standard:all",
121 | "escape": "disallow",
122 | "context": "all",
123 | "characters": "[^]"
124 | }
125 | ]
126 | ```
127 |
--------------------------------------------------------------------------------
/docs/rules/no-constant-capturing-group.md:
--------------------------------------------------------------------------------
1 | # `no-constant-capturing-group`
2 |
3 | > Disallow capturing groups that can match only one word.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-constant-capturing-group.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-constant-capturing-group.ts)
9 |
10 | ## Description
11 |
12 | Constant capturing groups can only match one word.
13 |
14 | Because they can only match one word, they should be replaced with the constant
15 | string they capture for better performance. This is especially the case if the
16 | capturing groups only matches the empty word.
17 |
18 | E.g. `/(foo)/`, `a()b`, `a(\b)`
19 |
20 | ### Examples
21 |
22 | Examples of **valid** code for this rule:
23 |
24 |
25 | ```js
26 | /(a)/i
27 | /(a|b)/
28 | /(a*)/
29 | /(a)/ // constant but it doesn't match the empty word
30 | ```
31 |
32 | Examples of **invalid** code for this rule:
33 |
34 |
35 | ```js
36 | /()/ // warn about `()`
37 | /(\b)/ // warn about `(\b)`
38 | ```
39 |
40 | ### `ignoreNonEmpty: boolean`
41 |
42 | If this option is set to `true`, the rule will ignore capturing groups that can
43 | match non-empty words. This option is `true` by default.
44 |
45 | #### `ignoreNonEmpty: false`
46 |
47 | Examples of **valid** code for this rule with `ignoreNonEmpty: false`:
48 |
49 |
50 | ```js
51 | /(a)/i
52 | /(a|b)/
53 | /(a*)/
54 | ```
55 |
56 | Examples of **invalid** code for this rule with `ignoreNonEmpty: false`:
57 |
58 |
59 | ```js
60 | /(a)/ // warn about `(a)`
61 | /()/ // warn about `()`
62 | /(\b)/ // warn about `(\b)`
63 | ```
64 |
--------------------------------------------------------------------------------
/docs/rules/no-empty-alternative.md:
--------------------------------------------------------------------------------
1 | # `no-empty-alternative`
2 |
3 | > Disallow alternatives without elements.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-empty-alternative.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-empty-alternative.ts)
9 |
10 | ## Description
11 |
12 | While (re-)writing long regular expressions, it can happen that one forgets to
13 | remove the `|` character of a former alternative. This rule tries to point out
14 | these potential mistakes by reporting all empty alternatives.
15 |
16 | ### Examples
17 |
18 | Examples of **valid** code for this rule:
19 |
20 |
21 | ```js
22 | /(?:)/
23 | /a+|b*/
24 | ```
25 |
26 | Examples of **invalid** code for this rule:
27 |
28 |
29 | ```js
30 | /a+|b+|/
31 | /\|\||\|||\|\|\|/
32 | /a(?:a|bc|def|h||ij|k)/
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/rules/no-empty-backreference.md:
--------------------------------------------------------------------------------
1 | # `no-empty-backreference`
2 |
3 | > Disallow backreferences that will always be replaced with the empty string.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"error"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-empty-backreference.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-empty-backreference.ts)
9 |
10 | ## Description
11 |
12 | Backreferences that will always be replaced with the empty string serve no
13 | function and can be removed.
14 |
15 | ### Empty capturing groups
16 |
17 | The easiest case of this is if the references capturing group does not consume
18 | any characters (e.g. `(\b)a\1`). Since the capturing group can only capture the
19 | empty string, the backreference is essentially useless.
20 |
21 | #### Examples
22 |
23 | Examples of **valid** code for this rule:
24 |
25 |
26 | ```js
27 | /(a?)b\1/
28 | /(\b|a)+b\1/
29 | ```
30 |
31 | Examples of **invalid** code for this rule:
32 |
33 |
34 | ```js
35 | /(|)a\1/
36 | /(\b)a\1/
37 | /(^|(?=.))a\1/
38 | ```
39 |
40 | ### Unreachable backreferences
41 |
42 | If a backreference cannot be reached from the position of the referenced
43 | capturing group without resetting the captured text, then the backreference will
44 | always be replaced with the empty string.
45 |
46 | If a group (capturing or non-capturing) is entered by the Javascript regex
47 | engine, the captured text of all capturing groups inside the group is reset. So
48 | even though a backreference might be reachable for the position of its
49 | referenced capturing group, the captured text might have been reset. An example
50 | of this is `(?:\1(a)){2}`. The `\1` is reachable after `(a)` in the second
51 | iteration but the captured text of `(a)` is reset by their parent non-capturing
52 | group before `\1` can matched (in the second iteration). This means that the
53 | Javascript regex `/^(?:\1(a)){2}$/` only accepts the string `aa` but not `aaa`.
54 | (Note: The regex engines of other programming languages may behave differently.
55 | I.e. with Python's re module, the regex `^(?:\1(a)){2}$` will accept the string
56 | `aaa` but not `aa`.)
57 |
58 | Backreferences that appear _before_ their referenced capturing group (e.g.
59 | `\1(a)`) will always be replaced with the empty string.
60 |
61 | Please note that _before_ depends on the current matching direction. RegExps are
62 | usually matched from left to right but inside lookbehind groups, text is matched
63 | from right to left. I.e. the pattern `(?<=\1(a))b` will match all `b`s preceded
64 | by two `a`s.
65 |
66 | #### Examples
67 |
68 | Examples of **valid** code for this rule:
69 |
70 |
71 | ```js
72 | /(a)?(?:a|\1)/
73 | ```
74 |
75 | Examples of **invalid** code for this rule:
76 |
77 |
78 | ```js
79 | /\1(a)/
80 | /(a\1)/
81 | /(a)|\1/
82 | /(?:(a)|\1)+/ // the \1 can be reached but not without resetting the captured text
83 | ```
84 |
--------------------------------------------------------------------------------
/docs/rules/no-empty-lookaround.md:
--------------------------------------------------------------------------------
1 | # `no-empty-lookaround`
2 |
3 | > Disallow lookarounds that can match the empty string.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"error"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-empty-lookaround.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-empty-lookaround.ts)
9 |
10 | ## Description
11 |
12 | ### What are _empty lookarounds_?
13 |
14 | An empty lookaround is a lookaround for which at least one path in the
15 | lookaround expression contains only elements which 1) are not assertions and can
16 | match the empty string or 2) are empty lookarounds. This means that the
17 | lookaround expression will accept on any position in any string.
18 |
19 | **Examples:**
20 |
21 | - `(?=)`: One of simplest empty lookarounds.
22 | - `(?=a*)`: Since `a*` match the empty string, the lookahead is _empty_.
23 | - `(?=a|b*)`: Only one path has to match the empty string and `b*` does just
24 | that.
25 | - `(?=a|$)`: Even though `$` does match the empty string, it is not and empty
26 | lookaround. Depending on whether the pattern is in multiline mode or not,
27 | `$` is equivalent to either `(?!.)` or `(?![\s\S])` with both being
28 | non-empty lookarounds. Similarly, all other standard assertions (`\b`, `\B`,
29 | `^`) are also not empty.
30 |
31 | ### Why are empty lookarounds a problem?
32 |
33 | Because empty lookarounds accept the empty string, they are essentially
34 | non-functional.
I.e. `(?=a*)b` will match `b` just fine; `(?=a*)` doesn't
35 | affect whether words are matched. The same also happens for negated lookarounds
36 | where every path containing the negated lookaround will not be able to match any
37 | word. I.e. `(?!a*)b` won't match any words.
38 |
39 | The only way to fix empty lookarounds is to either remove them or to rewrite the
40 | lookaround expression to be non-empty.
41 |
--------------------------------------------------------------------------------
/docs/rules/no-lazy-ends.md:
--------------------------------------------------------------------------------
1 | # `no-lazy-ends`
2 |
3 | > Disallow lazy quantifiers at the end of an expression.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-lazy-ends.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-lazy-ends.ts)
9 |
10 | ## Description
11 |
12 | If a lazily quantified element is the last element matched by an expression
13 | (e.g. the `a{2,3}?` in `b+a{2,3}?`), we know that the lazy quantifier will
14 | always only match the element the minimum number of times. The maximum is
15 | completely ignored because the expression can accept after the minimum was
16 | reached.
17 |
18 | If the minimum of the lazy quantifier is 0, we can even remove the quantifier
19 | and the quantified element without changing the meaning of the pattern. E.g.
20 | `a+b*?` and `a+` behave the same.
21 |
22 | If the minimum is 1, we can remove the quantifier. E.g. `a+b+?` and `a+b` behave
23 | the same.
24 |
25 | If the minimum is greater than 1, we can replace the quantifier with a constant,
26 | greedy quantifier. E.g. `a+b{2,4}?` and `a+b{2}` behave the same.
27 |
28 | ### Examples
29 |
30 | Examples of **valid** code for this rule:
31 |
32 |
33 | ```js
34 | /a+?b*/
35 | /a??(?:ba+?|c)*/
36 | /ba*?$/
37 | ```
38 |
39 | Examples of **invalid** code for this rule:
40 |
41 |
42 | ```js
43 | /a??/
44 | /a+b+?/
45 | /a(?:c|ab+?)?/
46 | ```
47 |
--------------------------------------------------------------------------------
/docs/rules/no-obscure-range.md:
--------------------------------------------------------------------------------
1 | # `no-obscure-range`
2 |
3 | > Disallow obscure ranges in character classes.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"error"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-obscure-range.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-obscure-range.ts)
9 |
10 | ## Description
11 |
12 | The range operator (the `-` inside character classes) can easily be misused
13 | (most unintentionally) to construct non-obvious character class. This rule will
14 | disallow all but obvious uses of the range operator.
15 |
16 | ### Examples
17 |
18 | Examples of **valid** code for this rule:
19 |
20 |
21 | ```js
22 | /[a-z]/
23 | /[J-O]/
24 | /[1-9]/
25 | /[\x00-\x40]/
26 | /[\0-\uFFFF]/
27 | /[\0-\u{10FFFF}]/u
28 | /[\1-\5]/
29 | /[\cA-\cZ]/
30 | ```
31 |
32 | Examples of **invalid** code for this rule:
33 |
34 |
35 | ```js
36 | /[A-\x43]/ // what's \x43? Bring me my ASCII table!
37 | /[\41-\x45]/ // the minimum isn't hexadecimal
38 | /[*/+-^&|]/ // because of +-^, it also matches all characters A-Z (among others)
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/rules/no-octal-escape.md:
--------------------------------------------------------------------------------
1 | # `no-octal-escape`
2 |
3 | > Disallow octal escapes outside of character classes.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"error"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-octal-escape.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-octal-escape.ts)
9 |
10 | ## Description
11 |
12 | Octal escapes can easily be confused with backreferences because the same
13 | character sequence (e.g. `\3`) can either be used to escape a character or to
14 | reference a capturing group depending on the number of capturing groups in the
15 | pattern.
16 |
17 | This can be a problem when refactoring regular expressions because an octal
18 | escape can become a backreference or wise versa.
19 |
20 | To prevent this issue, this rule disallows all octal escapes outside of
21 | character classes.
22 |
23 | ### Examples
24 |
25 | Examples of **valid** code for this rule:
26 |
27 |
28 | ```js
29 | /\x10\0/
30 | /(a)\1/
31 | /[\1]/ // allowed because backreferences cannot be in character classes
32 | ```
33 |
34 | Examples of **invalid** code for this rule:
35 |
36 |
37 | ```js
38 | /(a)\2/ // warns about `\2`
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/rules/no-optional-assertion.md:
--------------------------------------------------------------------------------
1 | # `no-optional-assertion`
2 |
3 | > Disallow optional assertions.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"error"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-optional-assertion.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-optional-assertion.ts)
9 |
10 | ## Description
11 |
12 | Assertions that as quantified in some way can be considered optional, if the
13 | quantifier as a minimum of zero.
14 |
15 | A simple example is the following pattern: `/a(?:$)*b/`. The end-of-string
16 | assertion will obviously reject but if that happens, it will simply be ignored
17 | because of the quantifier. The assertion is essentially optional, serving no
18 | function whatsoever.
19 |
20 | More generally, an assertion is optional, if the concatenation of all possible
21 | paths that start at the start of a zero-quantified element, end at the end of
22 | that element, and contain the assertion does not consume characters.
23 |
24 | Here's an example of that: `a(?:foo|(?
35 | ```js
36 | /\w+(?::|\b)/
37 | ```
38 |
39 | Examples of **invalid** code for this rule:
40 |
41 |
42 | ```js
43 | /(?:^)?\w+/ // warns about `^`
44 | /\w+(?::|$)?/ // warns about `$`
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/rules/no-potentially-empty-backreference.md:
--------------------------------------------------------------------------------
1 | # `no-potentially-empty-backreference`
2 |
3 | > Disallow backreferences that reference a group that might not be matched.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-potentially-empty-backreference.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-potentially-empty-backreference.ts)
9 |
10 | ## Description
11 |
12 | If the referenced group of a backreference is not matched because some other
13 | path leads to the backreference, the backreference will be replaced with the
14 | empty string. The same will happen if the captured text of the referenced group
15 | was reset before reaching the backreference.
16 |
17 | This will handle backreferences which will always be replaced with the empty
18 | string for the above reason. Use `no-empty-backreference` for that.
19 |
20 | ### Examples
21 |
22 | Examples of **valid** code for this rule:
23 |
24 |
25 | ```js
26 | /(a+)b\1/
27 | /(a+)b|\1/ // this will be done by no-empty-backreference
28 | ```
29 |
30 | Examples of **invalid** code for this rule:
31 |
32 |
33 | ```js
34 | /(a)?b\1/
35 | /((a)|c)+b\1/
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/rules/no-trivially-nested-lookaround.md:
--------------------------------------------------------------------------------
1 | # `no-trivially-nested-lookaround` :wrench:
2 |
3 | > Disallow lookarounds that only contain another assertion.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-trivially-nested-lookaround.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-trivially-nested-lookaround.ts)
9 |
10 | ## Description
11 |
12 | It's possible to nest lookarounds as deep as you want without changing the
13 | formal language of the regular expression. The nesting does not add meaning only
14 | making the pattern longer.
15 |
16 | ### Examples
17 |
18 | Examples of **valid** code for this rule:
19 |
20 |
21 | ```js
22 | /a(?!$)/
23 | ```
24 |
25 | Examples of **invalid** code for this rule:
26 |
27 |
28 | ```js
29 | /(?=\b)/ // == \b
30 | /(?=(?!a))/ //== (?!a)
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/rules/no-trivially-nested-quantifier.md:
--------------------------------------------------------------------------------
1 | # `no-trivially-nested-quantifier` :wrench:
2 |
3 | > Disallow nested quantifiers that can be rewritten as one quantifier.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-trivially-nested-quantifier.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-trivially-nested-quantifier.ts)
9 |
10 | ## Description
11 |
12 | In some cases, nested quantifiers can be rewritten as one quantifier (e.g.
13 | `(?:a{1,2}){3}` -> `a{3,6}`).
14 |
15 | The rewritten form is simpler and cannot cause exponential backtracking (e.g.
16 | `(?:a{1,2})+` -> `a+`).
17 |
18 | ### Examples
19 |
20 | Examples of **valid** code for this rule:
21 |
22 |
23 | ```js
24 | /(a{1,2})+/ // the rule won't touch capturing groups
25 | /(?:a{2})+/
26 | ```
27 |
28 | Examples of **invalid** code for this rule:
29 |
30 |
31 | ```js
32 | /(?:a{1,2})+/ // == /a+/
33 | /(?:a{1,2}){3,4}/ // == /a{3,8}/
34 | /(?:a{4,}){5}/ // == /a{20,}/
35 | ```
36 |
--------------------------------------------------------------------------------
/docs/rules/no-unnecessary-assertions.md:
--------------------------------------------------------------------------------
1 | # `no-unnecessary-assertions`
2 |
3 | > Disallow assertions that are known to always accept (or reject).
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"error"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-unnecessary-assertions.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-unnecessary-assertions.ts)
9 |
10 | ## Description
11 |
12 | Some assertion are unnecessary because the rest of the pattern forces them to
13 | always be accept (or reject).
14 |
15 | ### Examples
16 |
17 | Examples of **valid** code for this rule:
18 |
19 |
20 | ```js
21 | /\bfoo\b/
22 | ```
23 |
24 | Examples of **invalid** code for this rule:
25 |
26 |
27 | ```js
28 | /#\bfoo/ // \b will always accept
29 | /foo\bbar/ // \b will always reject
30 | /$foo/ // $ will always reject
31 | /(?=\w)\d+/ // (?=\w) will always accept
32 | ```
33 |
34 | ## Limitations
35 |
36 | Right now, this rule is implemented by only looking a single character ahead and
37 | behind. This is enough to determine whether the builtin assertions (`\b`, `\B`,
38 | `^`, `$`) trivially reject or accept but it is not enough for all lookarounds.
39 | The algorithm determining the characters ahead and behind is very conservative
40 | which can lead to false negatives.
41 |
--------------------------------------------------------------------------------
/docs/rules/no-unnecessary-character-class.md:
--------------------------------------------------------------------------------
1 | # `no-unnecessary-character-class` :wrench:
2 |
3 | > Disallow unnecessary character classes.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-unnecessary-character-class.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-unnecessary-character-class.ts)
9 |
10 | ## Description
11 |
12 | Unnecessary character classes contain only one character and can be trivially
13 | removed. E.g. `[a]`, `[\x61]`, `[\?]`.
14 |
15 | ### `avoidEscape`
16 |
17 | Sometimes characters have to be escaped, in order to remove the character class
18 | (e.g. `a[+]` -> `a\+`). The automatic escaping can be disabled by using
19 | `{ avoidEscape: true }` in the rule configuration.
20 |
21 | Note: This option does not affect characters already escaped in the character
22 | class (e.g. `a[\+]` -> `a\+`).
23 |
24 | Note: `\b` means backspace (`\x08`) inside of character classes but it will
25 | interpreted as a boundary assertion anywhere else, so it will be escaped as
26 | `\x08`.
27 |
28 | #### With `avoidEscape: false`
29 |
30 |
31 | ```js
32 | /a[+]/ -> /a\+/
33 | /a[.]/ -> /a\./
34 | /[\b]/ -> /\x08/
35 |
36 | /a[\s]/ -> /a\s/
37 | /a[\+]/ -> /a\+/
38 | ```
39 |
40 | #### With `avoidEscape: true`
41 |
42 |
43 | ```js
44 | /a[+]/ -> /a[+]/
45 | /a[.]/ -> /a[.]/
46 | /[\b]/ -> /[\b]/
47 |
48 | /a[\s]/ -> /a\s/
49 | /a[\+]/ -> /a\+/
50 | ```
51 |
--------------------------------------------------------------------------------
/docs/rules/no-unnecessary-flag.md:
--------------------------------------------------------------------------------
1 | # `no-unnecessary-flag` :wrench:
2 |
3 | > Disallow unnecessary regex flags.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-unnecessary-flag.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-unnecessary-flag.ts)
9 |
10 | ## Description
11 |
12 | This will point out present regex flags that do not change the pattern.
13 |
14 | The `i` flag is only necessary if the pattern contains any characters with case
15 | variations. If no such characters are part of the pattern, the flag is
16 | unnecessary. E.g. `/\.{3}/i`
17 |
18 | The `m` flag changes the meaning of the `^` and `$` anchors, so if the pattern
19 | doesn't contain these anchors, it's unnecessary. E.g. `/foo|[^\r\n]*/m`
20 |
21 | The `s` flag makes the dot (`.`) match all characters instead of the usually
22 | non-line-terminator characters, so if the pattern doesn't contain a dot
23 | character set, it will be unnecessary. E.g. `/[.:]/s`
24 |
25 | No other flags will be checked.
26 |
27 | ### Examples
28 |
29 | Examples of **valid** code for this rule:
30 |
31 |
32 | ```js
33 | /a|b/i
34 | /^foo$/m
35 | /a.*?b/s
36 | ```
37 |
38 | Examples of **invalid** code for this rule:
39 |
40 |
41 | ```js
42 | /\w+/i
43 | /a|b/m
44 | /^foo$/s
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/rules/no-unnecessary-group.md:
--------------------------------------------------------------------------------
1 | # `no-unnecessary-group` :wrench:
2 |
3 | > Disallow unnecessary non-capturing groups.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-unnecessary-group.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-unnecessary-group.ts)
9 |
10 | ## Description
11 |
12 | Non-capturing groups which can be removed without changing the meaning of the
13 | pattern are unnecessary. E.g. `a(?:bc)d` == `abcd` and `a(?:b)*c` == `ab*c`
14 |
15 | Capturing groups will not be reported or removed.
16 |
17 | ### Examples
18 |
19 | Examples of **valid** code for this rule:
20 |
21 |
22 | ```js
23 | /(?:a|b)c/
24 | /(?:a{2})+/
25 |
26 | // will not be removed because...
27 | /(.)\1(?:2\s)/ // ...it would changed the backreference
28 | /\x4(?:1)/ // ...it would complete the hexadecimal escape
29 | /(?:)/ // `//` is not a valid RegExp literal
30 | ```
31 |
32 | Examples of **invalid** code for this rule:
33 |
34 |
35 | ```js
36 | /(?:)a/
37 | /(?:a)/
38 | /(?:a)+/
39 | /a|(?:b|c)/
40 | /foo(?:[abc]*)bar/
41 | ```
42 |
43 | ### `allowTop: true`
44 |
45 | It's sometimes useful to wrap your whole pattern in a non-capturing group (e.g.
46 | if the pattern is used as a building block to construct more complex patterns).
47 | With this option you can allow top-level non-capturing groups.
48 |
49 | Examples of **valid** code for this rule with `allowTop: true`:
50 |
51 |
52 | ```js
53 | /(?:ab)/
54 | ```
55 |
56 | Examples of **invalid** code for this rule with `allowTop: true`:
57 |
58 |
59 | ```js
60 | /(?:a)b/
61 | ```
62 |
--------------------------------------------------------------------------------
/docs/rules/no-unnecessary-lazy.md:
--------------------------------------------------------------------------------
1 | # `no-unnecessary-lazy` :wrench:
2 |
3 | > Disallow unnecessarily lazy quantifiers.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-unnecessary-lazy.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-unnecessary-lazy.ts)
9 |
10 | ## Description
11 |
12 | This rule detects and provides fixers for two kinds of unnecessarily lazy
13 | quantifiers.
14 |
15 | First, it points out lazy constant quantifiers (e.g. `a{6}?`). It's obvious that
16 | the lazy modifier doesn't affect the quantifier, so it can be removed.
17 |
18 | Secondly, it detects lazy modifiers that can be removed based on the characters
19 | of the quantified element and the possible characters after the quantifier.
20 | Let's take `a+?b` as an example. The sequence of `a`s always has to be followed
21 | by a `b`, so the regex engine can't be lazy and match as few `a`s as possible
22 | because it doesn't have a choice. A lazy modifier only changes a pattern if the
23 | regex engine has a choice as to whether it will do another iteration of the
24 | quantified element or try to match the element after the quantifier.
25 |
26 | ### Examples
27 |
28 | Examples of **valid** code for this rule:
29 |
30 |
31 | ```js
32 | /a\w??c/
33 | /a[\s\S]*?bar/
34 | ```
35 |
36 | Examples of **invalid** code for this rule:
37 |
38 |
39 | ```js
40 | /ab{3}?c/ -> /ab{3}c/
41 | /b{2,2}?/ -> /b{2,2}/
42 | /ab+?c/ -> /ab+c/
43 | ```
44 |
--------------------------------------------------------------------------------
/docs/rules/no-unnecessary-quantifier.md:
--------------------------------------------------------------------------------
1 | # `no-unnecessary-quantifier` :wrench:
2 |
3 | > Disallow unnecessary quantifiers.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-unnecessary-quantifier.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-unnecessary-quantifier.ts)
9 |
10 | ## Description
11 |
12 | Unnecessary quantifiers are quantifiers which can be removed without changing
13 | the meaning of the pattern.
14 |
15 | A trivial example is: `a{1}`
Obviously, the quantifier can be removed.
16 |
17 | This is the only auto-fixable unnecessary quantifier. All other unnecessary
18 | quantifiers hint at programmer oversight or fundamental problems with the
19 | pattern.
20 |
21 | A not-so-trivial example is: `(?:a+b*|c*)?`
It's not very obvious that the
22 | `?` quantifier can be removed. Without this quantifier, that pattern can still
23 | match the empty string by choosing 0 many `c` in the `c*` alternative.
24 |
25 | Other examples include `(?:\b|(?=%))+` and `(?:|(?:)){5,9}`.
26 |
--------------------------------------------------------------------------------
/docs/rules/no-zero-quantifier.md:
--------------------------------------------------------------------------------
1 | # `no-zero-quantifier` :wrench:
2 |
3 | > Disallow quantifiers with a maximum of 0.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"error"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/no-zero-quantifier.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/no-zero-quantifier.ts)
9 |
10 | ## Description
11 |
12 | Quantifiers with a maximum of zero mean that the quantified element will never
13 | be matched. They essentially produce dead code.
14 |
15 | **Note:** The rule will not remove zero-quantified elements if they are or
16 | contain a capturing group. In this case, the quantifier and element will simple
17 | be reported.
18 |
19 | ### Examples
20 |
21 | Examples of **valid** code for this rule:
22 |
23 |
24 | ```js
25 | /a{0,1}/;
26 | ```
27 |
28 | Examples of **invalid** code for this rule:
29 |
30 |
31 | ```js
32 | /a{0}/;
33 | /a{0,0}/;
34 | ```
35 |
--------------------------------------------------------------------------------
/docs/rules/optimal-concatenation-quantifier.md:
--------------------------------------------------------------------------------
1 | # `optimal-concatenation-quantifier` :wrench:
2 |
3 | > Use optimal quantifiers for concatenated quantified characters.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/optimal-concatenation-quantifier.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/optimal-concatenation-quantifier.ts)
9 |
10 | ## Description
11 |
12 | If two quantified characters, character classes, or characters are concatenated,
13 | the quantifiers can be optimized if either of the characters elements is a
14 | subset of the other.
15 |
16 | Let's take `\d+\w+` as an example. This can be optimized to the equivalent
17 | pattern `\d\w+`. Not only is the optimized pattern simpler, it is also faster
18 | because the first pattern might take up to _O(n^2)_ steps to fail while the
19 | optimized pattern will fail after at most _O(n)_ steps. Generally, the optimized
20 | pattern will take less backtracking steps to fail.
21 |
22 | Choosing optimal quantifiers does not only make your patterns simpler but also
23 | faster and most robust against ReDos attacks.
24 |
25 | ### `fixable: boolean`
26 |
27 | With this option you can control whether reported issue will be auto-fixable.
28 | You might want to turn the fixability off because the optimally-quantified
29 | pattern does not express your intend.
30 |
--------------------------------------------------------------------------------
/docs/rules/optimal-lookaround-quantifier.md:
--------------------------------------------------------------------------------
1 | # `optimal-lookaround-quantifier`
2 |
3 | > Disallows the alternatives of lookarounds that end with a non-constant
4 | > quantifier.
5 |
6 | configuration in `plugin:clean-regex/recommended`: `"warn"`
7 |
8 |
9 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/optimal-lookaround-quantifier.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/optimal-lookaround-quantifier.ts)
10 |
11 | ## Description
12 |
13 | Non-constant quantifiers are quantifiers that describe a range (e.g. `?`, `*`,
14 | `+`, `{0,1}`, `{5,9}`, `{3,}`). They have to match some number of times (the
15 | minimum) after which further matches are optional until a certain maximum (may
16 | be infinite) is reached.
17 |
18 | It's obvious that `/ba{2}/` and `/ba{2,6}/` will match differently because of
19 | the different quantifiers of `a` but that not the case if for lookarounds. Both
20 | `/b(?=a{2})/` and `/b(?=a{2,6})/` will match strings the same way. I.e. for the
21 | input string `"baaa"`, both will create the same match arrays. The two regular
22 | expression are actually equivalent, meaning that `(?=a{2})` is equivalent to
23 | `(?=a{2,6})`.
24 |
25 | More generally, if a non-constant quantifier is an **end** of the expression
26 | tree of a **lookahead**, that quantifier can be replaced with a constant
27 | quantifier that matched the element minimum-if-the-non-constant-quantifier many
28 | times. For **lookbehinds**, the non-constant quantifier has to be at the
29 | **start** of the expression tree as lookbehinds are matched from right to left.
30 |
31 | ### Examples
32 |
33 | Examples of **valid** code for this rule:
34 |
35 |
36 | ```js
37 | // lookaheads
38 | /\w+(?=\s*:)/
39 |
40 | // lookbehinds
41 | /(?<=ab+)/
42 | ```
43 |
44 | Examples of **invalid** code for this rule:
45 |
46 |
47 | ```js
48 | // lookaheads
49 | /(?=ab+)/ == /(?=ab)/
50 | /(?=ab*)/ == /(?=a)/
51 | /(?!ab?)/ == /(?!a)/
52 | /(?!ab{6,})/ == /(?!ab{6})/
53 |
54 | // lookbehinds
55 | /(?<=a+b)/ == /(?<=ab)/
56 | /(? Disallows unnecessary elements in character classes.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/optimized-character-class.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/optimized-character-class.ts)
9 |
10 | ## Description
11 |
12 | This will will provide fixes to remove unnecessary characters, character ranges,
13 | and character sets from character classes.
14 |
15 | ### Examples
16 |
17 |
18 | ```js
19 | /[a-zf]/ -> /[a-z]/
20 | /[a-z\w]/ -> /[\w]/
21 | /[\s\r\n]/ -> /[\s]/
22 | /[a-zH]/i -> /[a-z]/
23 | ```
24 |
--------------------------------------------------------------------------------
/docs/rules/prefer-character-class.md:
--------------------------------------------------------------------------------
1 | # `prefer-character-class` :wrench:
2 |
3 | > Prefer character classes wherever possible instead of alternations.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/prefer-character-class.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/prefer-character-class.ts)
9 |
10 | ## Description
11 |
12 | Instead of single-character alternatives (e.g. `(?:a|b|c)`), you should prefer
13 | character classes (e.g. `[abc]`).
14 |
15 | The main reason for doing this is performance. A character class doesn't require
16 | backtracking (choosing the correct alternative) and are heavily optimized by the
17 | regex engine. On the other hand, alternatives usually aren't optimized at all
18 | because these optimizations are non-trivial and take too long to do them during
19 | the execution of the program.
20 |
21 | They are also safer than alternatives because they don't use backtracking. While
22 | `^(?:\w|a)+b$` will take _O(2^n)_ time to reject a string of _n_ many `a`s, the
23 | regex `^[\wa]+b$` will reject a string of _n_ many `a`s in _O(n)_.
24 |
25 | ### Limitation
26 |
27 | The rule might not be able to merge alternatives that it knows cause exponential
28 | backtracking. In this case, the rule will simply report the exponential
29 | backtracking without a fix.
30 |
31 | ### Examples
32 |
33 | Examples of **valid** code for this rule:
34 |
35 |
36 | ```js
37 | /(?:a|bb)c/
38 | /(?:a|a*)c/
39 | ```
40 |
41 | Examples of **invalid** code for this rule:
42 |
43 |
44 | ```js
45 | /a|b|c/ -> /[abc]/
46 | /(?:a|b|c)c/ -> /[abc]c/
47 | /(a|b|c)c/ -> /([abc])c/
48 | ```
49 |
--------------------------------------------------------------------------------
/docs/rules/prefer-predefined-assertion.md:
--------------------------------------------------------------------------------
1 | # `prefer-predefined-assertion` :wrench:
2 |
3 | > Prefer predefined assertions over equivalent lookarounds.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/prefer-predefined-assertion.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/prefer-predefined-assertion.ts)
9 |
10 | ## Description
11 |
12 | All predefined assertions (`\b`, `\B`, `^`, and `$`) can be expressed as
13 | lookaheads and lookbehinds. E.g. `/a$/` is the same as `/a(?![^])/`.
14 |
15 | In most cases, it's better to use the predefined assertions because they are
16 | more well known.
17 |
18 | ### Examples
19 |
20 | Examples of **valid** code for this rule:
21 |
22 |
23 | ```js
24 | /a(?=\W)/
25 | ```
26 |
27 | Examples of **invalid** code for this rule:
28 |
29 |
30 | ```js
31 | /a(?![^])/ -> /a$/
32 | /a(?!\w)/ -> /a\b/
33 | /a+(?!\w)(?:\s|bc+)+/ -> /a+\b(?:\s|bc+)+/
34 | ```
35 |
--------------------------------------------------------------------------------
/docs/rules/prefer-predefined-character-set.md:
--------------------------------------------------------------------------------
1 | # `prefer-predefined-character-set` :wrench:
2 |
3 | > Prefer predefined character sets instead of their more verbose form.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/prefer-predefined-character-set.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/prefer-predefined-character-set.ts)
9 |
10 | ## Description
11 |
12 | This will replace the verbose character class elements version of of the `\d`
13 | and `\w` character sets with their character set representation.
14 |
15 | Note: This will not remove any character classes. Use the
16 | `no-unnecessary-character-class` rule for that.
17 |
18 | ### `allowDigitRange: boolean`
19 |
20 | This option determines whether a digit range (`0-9`) is allowed or whether it
21 | should be replaced with `\d`. Note that if the digit range is the whole
22 | character class is equivalent to `\d`, then a digit range will always be
23 | replaced with `\d`. The value defaults to `true`.
24 |
25 | ### Examples
26 |
27 |
28 | ```js
29 | /[0-9]/ // -> /[\d]/
30 | /[0-9a-z_-]/ // -> /[\w-]/
31 | /[0-9a-f]/ // -> /[\da-f]/ with `allowDigitRange: false`
32 | /[0-9a-f]/ // unchanged with `allowDigitRange: true` (default)
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/rules/prefer-predefined-quantifiers.md:
--------------------------------------------------------------------------------
1 | # `prefer-predefined-quantifiers` :wrench:
2 |
3 | > Prefer predefined quantifiers (+\*?) instead of their more verbose form.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/prefer-predefined-quantifiers.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/prefer-predefined-quantifiers.ts)
9 |
10 | ## Description
11 |
12 | Prefer predefined quantifiers over general quantifiers. E.g. `?` instead of
13 | `{0,1}`, `*` instead of `{0,}`, and `+` instead of `{1,}`.
14 |
15 | Predefined use less characters than their verbose counterparts and are therefore
16 | easier to read.
17 |
18 | ### Examples
19 |
20 | Examples of **valid** code for this rule:
21 |
22 |
23 | ```js
24 | /a+b*c?/
25 | /a{2,}b{2,6}c{2}/
26 | ```
27 |
28 | Examples of **invalid** code for this rule:
29 |
30 |
31 | ```js
32 | /a{1,}/
33 | /a{0,}/
34 | /a{0,1}/
35 | ```
36 |
--------------------------------------------------------------------------------
/docs/rules/simple-constant-quantifier.md:
--------------------------------------------------------------------------------
1 | # `simple-constant-quantifier` :wrench:
2 |
3 | > Prefer simple constant quantifiers over the range form.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/simple-constant-quantifier.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/simple-constant-quantifier.ts)
9 |
10 | ## Description
11 |
12 | This rule enforces the usage from simple constant quantifiers (e.g. `a{2}`)
13 | instead of their more verbose range form (e.g. `a{2,2}`).
14 |
15 | ### Examples
16 |
17 | Examples of **valid** code for this rule:
18 |
19 |
20 | ```js
21 | /a+b*c?/
22 | /a{2,}b{2,6}c{2}/
23 | ```
24 |
25 | Examples of **invalid** code for this rule:
26 |
27 |
28 | ```js
29 | /a{2,2}/
30 | /a{100,100}?/
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/rules/sort-flags.md:
--------------------------------------------------------------------------------
1 | # `sort-flags` :wrench:
2 |
3 | > Requires the regex flags to be sorted.
4 |
5 | configuration in `plugin:clean-regex/recommended`: `"warn"`
6 |
7 |
8 | [Source file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/lib/rules/sort-flags.ts)
[Test file](https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/tests/lib/rules/sort-flags.ts)
9 |
10 | ## Description
11 |
12 | The flags of JavaScript regular expressions should be sorted alphabetically
13 | because the flags of the `.flags` property of `RegExp` objects are always
14 | sorted. Not sorting flags in regex literals misleads readers into thinking that
15 | the order may have some purpose which it doesn't.
16 |
17 | ### Examples
18 |
19 | Examples of **valid** code for this rule:
20 |
21 |
22 | ```js
23 | /abc/
24 | /abc/iu
25 | /abc/gimsuy
26 | ```
27 |
28 | Examples of **invalid** code for this rule:
29 |
30 |
31 | ```js
32 | /abc/mi
33 | /abc/us
34 | ```
35 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const { series } = require("gulp");
2 | const fs = require("fs").promises;
3 | require('ts-node').register({ transpileOnly: true });
4 | const { repoTreeRoot, filenameToRule } = require("./lib/rules-util");
5 | const configs = require("./lib/configs").default;
6 | const eslint = require("eslint");
7 |
8 |
9 | /**
10 | * @param {Record} obj
11 | * @returns {T[]}
12 | * @template T
13 | */
14 | function values(obj) {
15 | return Object.keys(obj).map(k => obj[k]);
16 | }
17 |
18 | /**
19 | * Applies all ESLint fixes of this plugin to the given code.
20 | *
21 | * @param {string} code
22 | * @returns {string}
23 | */
24 | function fixCode(code) {
25 | const linter = new eslint.Linter();
26 |
27 | const rules = require("./lib/index").rules;
28 | for (const name in rules) {
29 | if (rules.hasOwnProperty(name)) {
30 | linter.defineRule("clean-regex/" + name, rules[name]);
31 | }
32 | }
33 |
34 | return linter.verifyAndFix(code, {
35 | parserOptions: {
36 | ecmaVersion: 2019,
37 | sourceType: "script"
38 | },
39 | ...configs.recommended
40 | }).output;
41 | }
42 |
43 | /**
44 | * @typedef RuleMeta
45 | * @property {string} name
46 | * @property {"problem" | "suggestion" | "layout"} type
47 | * @property {string} description
48 | * @property {string} docUrl
49 | * @property {boolean} fixable
50 | * @property {"off" | "error" | "warn"} recommendedConfig
51 | * @property {RuleMetaFiles} files
52 | *
53 | * @typedef RuleMetaFiles
54 | * @property {string} source
55 | * @property {string} test
56 | * @property {string} doc
57 | */
58 | async function getRules() {
59 | /** @type {Record} */
60 | const rules = {};
61 | const files = (await fs.readdir(__dirname + "/lib/rules")).filter(p => /\.ts$/i.test(p));
62 |
63 | for (const file of files) {
64 | const rule = filenameToRule(file);
65 |
66 | /** @type {import("eslint").Rule.RuleModule["meta"]} */
67 | const meta = require("./lib/rules/" + file).default.meta;
68 | if (!meta || !meta.docs || !meta.type || !meta.docs.description || !meta.docs.url) {
69 | console.dir(meta);
70 | throw new Error("Incomplete meta data for rule " + rule);
71 | }
72 |
73 | rules[rule] = {
74 | name: rule,
75 | type: meta.type,
76 | description: meta.docs.description,
77 | docUrl: meta.docs.url,
78 | fixable: meta.fixable === "code",
79 | recommendedConfig: /** @type {any} */(configs.recommended.rules["clean-regex/" + rule]),
80 | files: {
81 | source: `lib/rules/${rule}.ts`,
82 | test: `tests/lib/rules/${rule}.ts`,
83 | doc: `docs/rules/${rule}.md`,
84 | },
85 | };
86 | }
87 | return rules;
88 | }
89 |
90 | async function readme() {
91 | const rules = await getRules();
92 | const ruleNames = Object.keys(rules).sort();
93 |
94 | const generatedMd = ["problem", "suggestion", "layout"]
95 | .map(type => {
96 | let mdTable = [
97 | `### ${type === "layout" ? "Layout" : type.substr(0, 1).toUpperCase() + type.substr(1) + "s"}\n`,
98 | "\n",
99 | "| | Rule | Description |\n",
100 | "| :--- | :--- | :--- |\n",
101 | ].join("");
102 |
103 | let ruleCounter = 0;
104 | for (const rule of ruleNames) {
105 | const meta = rules[rule];
106 | if (meta.type !== type) {
107 | continue;
108 | }
109 | const mdColumns = [meta.fixable ? ":wrench:" : "", `[${rule}](${meta.docUrl})`, meta.description];
110 |
111 | mdTable += `| ${mdColumns.join(" | ")} |\n`;
112 | ruleCounter++;
113 | }
114 |
115 | if (ruleCounter === 0) {
116 | return "";
117 | } else {
118 | return mdTable;
119 | }
120 | })
121 | .filter(Boolean)
122 | .join("\n");
123 |
124 | let readme = await fs.readFile("./README.md", "utf8");
125 |
126 | // insert the rule table
127 | readme = readme.replace(
128 | /(^)[\s\S]*?(?=^)/m,
129 | "$1\n" + generatedMd.trim() + "\n"
130 | );
131 |
132 | // simplify the examples
133 | readme = readme.replace(/^Before:\s*^```js($[\s\S]+?)^```\s*^After:\s*^```js$[\s\S]+?^```/gm, (_, b) => {
134 | const before = String(b).trim().split(/\r?\n/g);
135 | const after = before.map(c => fixCode(c));
136 |
137 | return [
138 | "Before:",
139 | "",
140 | "```js",
141 | ...before,
142 | "```",
143 | "",
144 | "After:",
145 | "",
146 | "```js",
147 | ...after,
148 | "```",
149 | ].join("\n");
150 | });
151 |
152 | await fs.writeFile("./README.md", readme, "utf8");
153 | }
154 |
155 | async function generateDocFiles() {
156 | const rules = await getRules();
157 |
158 | for (const rule in rules) {
159 | await generateDocFile(rules[rule]);
160 | }
161 | }
162 | /**
163 | * @param {RuleMeta} meta
164 | */
165 | async function generateDocFile(meta) {
166 | let content = await fs.readFile("./" + meta.files.doc, "utf8").catch(() => "## Description\n\nTODO");
167 | let overview =
168 | [
169 | `# \`${meta.name}\`${meta.fixable ? " :wrench:" : ""}`,
170 | "",
171 | "> " + meta.description,
172 | "",
173 | `configuration in \`plugin:clean-regex/recommended\`: \`"${meta.recommendedConfig}"\``,
174 | "",
175 | "",
176 | `[Source file](${repoTreeRoot}/${meta.files.source})
[Test file](${repoTreeRoot}/${meta.files.test})`,
177 | ].join("\n") + "\n\n";
178 | content = content.replace(/[\s\S]*?(?=^## Description)/m, overview);
179 |
180 | await fs.writeFile("./" + meta.files.doc, content, "utf8");
181 | }
182 |
183 | async function generateIndex() {
184 | const rules = Object.keys(await getRules());
185 |
186 | function toVar(ruleName = "") {
187 | return ruleName.replace(/-(\w)/g, (_, letter) => String(letter).toUpperCase());
188 | }
189 |
190 | const code = `// THIS IS GENERATED CODE
191 | // DO NOT EDIT
192 |
193 | import _configs from "./configs";
194 |
195 | ${rules.map(name => `import ${toVar(name)} from "./rules/${name}";`).join("\n")}
196 |
197 | export const configs = _configs;
198 | export const rules = {
199 | ${rules.map(name => `"${name}": ${toVar(name)},`).join("\n\t")}
200 | };
201 | `;
202 |
203 | await fs.writeFile("./lib/index.ts", code, "utf-8");
204 | }
205 |
206 | async function insertRuleName() {
207 | const rules = await getRules();
208 |
209 | for (const rule of values(rules)) {
210 | const file = "./" + rule.files.source;
211 | let code = await fs.readFile(file, "utf-8");
212 | code = code.replace(/\bgetDocUrl\([^()]*\)/g, () => {
213 | return `getDocUrl(/* #GENERATED */ ${JSON.stringify(rule.name)})`;
214 | });
215 | await fs.writeFile(file, code, "utf-8");
216 | }
217 | }
218 |
219 | module.exports = {
220 | doc: series(readme, generateDocFiles),
221 | updateSourceFile: series(generateIndex, insertRuleName),
222 | };
223 |
--------------------------------------------------------------------------------
/lib/char-util.ts:
--------------------------------------------------------------------------------
1 | import { CharSet, JS } from "refa";
2 | import { AST } from "regexpp";
3 | import { Character, CharacterClass, CharacterClassElement, CharacterSet } from "regexpp/ast";
4 | import { Simple, assertNever } from "./util";
5 |
6 | type Flags = Partial>;
7 |
8 | export function toCharSet(
9 | elements:
10 | | (Simple | Simple)[]
11 | | CharacterClass
12 | | Simple
13 | | Simple,
14 | flags: Flags
15 | ): CharSet {
16 | if (Array.isArray(elements)) {
17 | return JS.createCharSet(
18 | elements.map(e => {
19 | switch (e.type) {
20 | case "Character":
21 | return e.value;
22 | case "CharacterClassRange":
23 | return { min: e.min.value, max: e.max.value };
24 | case "CharacterSet":
25 | return e;
26 | default:
27 | throw assertNever(e);
28 | }
29 | }),
30 | flags
31 | );
32 | } else {
33 | switch (elements.type) {
34 | case "Character": {
35 | return JS.createCharSet([elements.value], flags);
36 | }
37 | case "CharacterClass": {
38 | const chars = toCharSet(elements.elements, flags);
39 | if (elements.negate) {
40 | return chars.negate();
41 | }
42 | return chars;
43 | }
44 | case "CharacterSet": {
45 | return JS.createCharSet([elements], flags);
46 | }
47 | default:
48 | throw assertNever(elements);
49 | }
50 | }
51 | }
52 |
53 | const EMPTY_UTF16_CHARSET = CharSet.empty(0xffff);
54 | const EMPTY_UNICODE_CHARSET = CharSet.empty(0x10ffff);
55 | /**
56 | * Returns an empty character set for the given flags.
57 | */
58 | export function emptyCharSet(flags: Flags): CharSet {
59 | if (flags.unicode) {
60 | return EMPTY_UNICODE_CHARSET;
61 | } else {
62 | return EMPTY_UTF16_CHARSET;
63 | }
64 | }
65 | const ALL_UTF16_CHARSET = CharSet.all(0xffff);
66 | const ALL_UNICODE_CHARSET = CharSet.all(0x10ffff);
67 | /**
68 | * Returns a full character set for the given flags.
69 | */
70 | export function allCharSet(flags: Flags): CharSet {
71 | if (flags.unicode) {
72 | return ALL_UNICODE_CHARSET;
73 | } else {
74 | return ALL_UTF16_CHARSET;
75 | }
76 | }
77 | const LINE_TERMINATOR_UTF16_CHARSET = JS.createCharSet([{ kind: "any" }], { unicode: false }).negate();
78 | const LINE_TERMINATOR_UNICODE_CHARSET = JS.createCharSet([{ kind: "any" }], { unicode: true }).negate();
79 | export function lineTerminatorCharSet(flags: Flags): CharSet {
80 | if (flags.unicode) {
81 | return LINE_TERMINATOR_UNICODE_CHARSET;
82 | } else {
83 | return LINE_TERMINATOR_UTF16_CHARSET;
84 | }
85 | }
86 | const WORD_UTF16_CHARSET = JS.createCharSet([{ kind: "word", negate: false }], { unicode: false });
87 | const WORD_UNICODE_CHARSET = JS.createCharSet([{ kind: "word", negate: false }], { unicode: true, ignoreCase: false });
88 | const WORD_UNICODE_IGNORE_CASE_CHARSET = JS.createCharSet([{ kind: "word", negate: false }], {
89 | unicode: true,
90 | ignoreCase: true,
91 | });
92 | export function wordCharSet(flags: Flags): CharSet {
93 | if (flags.unicode) {
94 | if (flags.ignoreCase) {
95 | return WORD_UNICODE_IGNORE_CASE_CHARSET;
96 | } else {
97 | return WORD_UNICODE_CHARSET;
98 | }
99 | } else {
100 | return WORD_UTF16_CHARSET;
101 | }
102 | }
103 |
104 | /**
105 | * Returns whether the given character class/set matches all characters.
106 | */
107 | export function isMatchAll(char: CharacterClass | CharacterSet, flags: Flags): boolean {
108 | if (char.type === "CharacterSet") {
109 | if (char.kind === "property") {
110 | return JS.createCharSet([char], flags).isAll;
111 | } else if (char.kind === "any") {
112 | return !!flags.dotAll;
113 | } else {
114 | return false;
115 | }
116 | } else {
117 | if (char.negate && char.elements.length === 0) {
118 | return true;
119 | } else {
120 | if (char.negate) {
121 | return toCharSet(char.elements, flags).isEmpty;
122 | } else {
123 | return toCharSet(char.elements, flags).isAll;
124 | }
125 | }
126 | }
127 | }
128 |
129 | /**
130 | * Returns whether the given character class/set matches no characters.
131 | */
132 | export function isMatchNone(char: CharacterClass | CharacterSet, flags: Flags): boolean {
133 | if (char.type === "CharacterSet") {
134 | if (char.kind === "property") {
135 | return JS.createCharSet([char], flags).isEmpty;
136 | } else {
137 | return false;
138 | }
139 | } else {
140 | if (!char.negate && char.elements.length === 0) {
141 | return true;
142 | } else {
143 | if (char.negate) {
144 | return toCharSet(char.elements, flags).isAll;
145 | } else {
146 | return toCharSet(char.elements, flags).isEmpty;
147 | }
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/lib/configs.ts:
--------------------------------------------------------------------------------
1 | import type { Linter } from "eslint";
2 |
3 | export default {
4 | recommended: {
5 | plugins: ["clean-regex"],
6 | rules: {
7 | "clean-regex/confusing-quantifier": "warn",
8 | "clean-regex/consistent-match-all-characters": "warn",
9 | "clean-regex/disjoint-alternatives": "warn",
10 | "clean-regex/identity-escape": "warn",
11 | "clean-regex/no-constant-capturing-group": "warn",
12 | "clean-regex/no-empty-alternative": "warn",
13 | "clean-regex/no-empty-backreference": "error",
14 | "clean-regex/no-empty-lookaround": "error",
15 | "clean-regex/no-lazy-ends": "warn",
16 | "clean-regex/no-obscure-range": "error",
17 | "clean-regex/no-octal-escape": "error",
18 | "clean-regex/no-optional-assertion": "error",
19 | "clean-regex/no-potentially-empty-backreference": "warn",
20 | "clean-regex/no-trivially-nested-lookaround": "warn",
21 | "clean-regex/no-trivially-nested-quantifier": "warn",
22 | "clean-regex/no-unnecessary-assertions": "error",
23 | "clean-regex/no-unnecessary-character-class": "warn",
24 | "clean-regex/no-unnecessary-flag": "warn",
25 | "clean-regex/no-unnecessary-group": "warn",
26 | "clean-regex/no-unnecessary-lazy": "warn",
27 | "clean-regex/no-unnecessary-quantifier": "warn",
28 | "clean-regex/no-zero-quantifier": "error",
29 | "clean-regex/optimal-concatenation-quantifier": "warn",
30 | "clean-regex/optimal-lookaround-quantifier": "warn",
31 | "clean-regex/optimized-character-class": "warn",
32 | "clean-regex/prefer-character-class": "warn",
33 | "clean-regex/prefer-predefined-assertion": "warn",
34 | "clean-regex/prefer-predefined-character-set": "warn",
35 | "clean-regex/prefer-predefined-quantifiers": "warn",
36 | "clean-regex/simple-constant-quantifier": "warn",
37 | "clean-regex/sort-flags": "warn",
38 | },
39 | } as Linter.BaseConfig,
40 | };
41 |
--------------------------------------------------------------------------------
/lib/fa-util.ts:
--------------------------------------------------------------------------------
1 | import { CharSet, DFA, NFA, ReadonlyNFA } from "refa";
2 |
3 | /**
4 | * Returns whether the languages of the given NFA are equal.
5 | *
6 | * This assumes that both NFA do not have unreachable or trap states.
7 | */
8 | export function nfaEquals(a: ReadonlyNFA, b: ReadonlyNFA): boolean {
9 | if (a.isEmpty || b.isEmpty) {
10 | return a.isEmpty === b.isEmpty;
11 | }
12 | if (a.options.maxCharacter !== b.options.maxCharacter) {
13 | return false;
14 | }
15 |
16 | const { maxCharacter } = a.options;
17 | const x = a.nodes;
18 | const y = b.nodes;
19 |
20 | if (x.finals.has(x.initial) !== y.finals.has(y.initial)) {
21 | // one accepts the empty word, the other one doesn't
22 | return false;
23 | }
24 |
25 | function unionAll(iter: Iterable): CharSet {
26 | let total: CharSet | undefined = undefined;
27 |
28 | for (const item of iter) {
29 | if (total === undefined) {
30 | total = item;
31 | } else {
32 | total = total.union(item);
33 | }
34 | }
35 |
36 | return total || CharSet.empty(maxCharacter);
37 | }
38 |
39 | if (!unionAll(x.initial.out.values()).equals(unionAll(y.initial.out.values()))) {
40 | // first characters of the accepted languages are different
41 | return false;
42 | }
43 |
44 | const aDfa = DFA.fromFA(a);
45 | aDfa.minimize();
46 | const bDfa = DFA.fromFA(b);
47 | bDfa.minimize();
48 |
49 | return aDfa.structurallyEqual(bDfa);
50 | }
51 | export function nfaIsSupersetOf(superset: ReadonlyNFA, subset: ReadonlyNFA): boolean {
52 | const union = superset.copy();
53 | union.union(subset);
54 | return nfaEquals(union, superset);
55 | }
56 | export function nfaIsSubsetOf(subset: ReadonlyNFA, superset: ReadonlyNFA): boolean {
57 | return nfaIsSupersetOf(superset, subset);
58 | }
59 | export function nfaUnionAll(nfas: Iterable, options: Readonly): NFA {
60 | const total = NFA.empty(options);
61 | for (const nfa of nfas) {
62 | total.union(nfa);
63 | }
64 | return total;
65 | }
66 |
--------------------------------------------------------------------------------
/lib/format.ts:
--------------------------------------------------------------------------------
1 | import { FiniteAutomaton, JS } from "refa";
2 | import { CharacterClassElement, Node } from "regexpp/ast";
3 | import { assertNever } from "./util";
4 | import { minimalHexEscape as hexEscape } from "./ast-util";
5 |
6 | interface Literal {
7 | readonly source: string;
8 | readonly flags: string;
9 | }
10 |
11 | export function shorten(string: string, maxLength: number, position?: "start" | "end" | "center"): string {
12 | if (string.length <= maxLength) {
13 | return string;
14 | } else {
15 | maxLength--;
16 | if (position === "end" || position == undefined) {
17 | return string.substr(0, maxLength) + "…";
18 | } else if (position === "start") {
19 | return "…" + string.substr(string.length - maxLength, maxLength);
20 | } else if (position === "center") {
21 | const start = maxLength >>> 1;
22 | const end = start + (maxLength % 2);
23 | return string.substr(0, start) + "…" + string.substr(string.length - end, end);
24 | } else {
25 | throw new Error("Invalid position.");
26 | }
27 | }
28 | }
29 |
30 | /**
31 | * Converts the given value to the string of a `RegExp` literal.
32 | *
33 | * @example
34 | * toRegExpString(/foo/i) // returns "/foo/i"
35 | */
36 | export function toRegExpString(value: Literal | FiniteAutomaton): string {
37 | if ("toRegex" in value) {
38 | const re = value.toRegex();
39 | const literal = JS.toLiteral(re);
40 | return toRegExpString(literal);
41 | } else {
42 | return `/${value.source}/${value.flags}`;
43 | }
44 | }
45 |
46 | /**
47 | * Returns a string that mentions the given node or string representation of a node.
48 | */
49 | export function mention(node: Node | string): string {
50 | return "`" + (typeof node === "string" ? node : node.raw) + "`";
51 | }
52 |
53 | /**
54 | * A version of `mention` that add some details about the character class element mentioned.
55 | */
56 | export function mentionCharElement(element: CharacterClassElement): string {
57 | switch (element.type) {
58 | case "Character":
59 | return `${mention(element)} (${hexEscape(element.value)})`;
60 | case "CharacterClassRange":
61 | return `${mention(element)} (${hexEscape(element.min.value)}-${hexEscape(element.max.value)})`;
62 | case "CharacterSet":
63 | switch (element.kind) {
64 | case "digit":
65 | return `${mention(element)} ([${element.negate ? "^" : ""}0-9])`;
66 | case "word":
67 | return `${mention(element)} ([${element.negate ? "^" : ""}0-9A-Za-z_])`;
68 | default:
69 | return mention(element);
70 | }
71 | default:
72 | throw assertNever(element);
73 | }
74 | }
75 |
76 | export function many(count: number, unit: string, unitPlural?: string): string {
77 | if (!unitPlural) {
78 | if (unit.length > 1 && unit[unit.length - 1] === "y") {
79 | unitPlural = unit.substr(0, unit.length - 1) + "ies";
80 | } else {
81 | unitPlural = unit + "s";
82 | }
83 | }
84 |
85 | if (count === 1) {
86 | return "1 " + unit;
87 | } else {
88 | return count + " " + unitPlural;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | // THIS IS GENERATED CODE
2 | // DO NOT EDIT
3 |
4 | import _configs from "./configs";
5 |
6 | import confusingQuantifier from "./rules/confusing-quantifier";
7 | import consistentMatchAllCharacters from "./rules/consistent-match-all-characters";
8 | import disjointAlternatives from "./rules/disjoint-alternatives";
9 | import identityEscape from "./rules/identity-escape";
10 | import noConstantCapturingGroup from "./rules/no-constant-capturing-group";
11 | import noEmptyAlternative from "./rules/no-empty-alternative";
12 | import noEmptyBackreference from "./rules/no-empty-backreference";
13 | import noEmptyLookaround from "./rules/no-empty-lookaround";
14 | import noLazyEnds from "./rules/no-lazy-ends";
15 | import noObscureRange from "./rules/no-obscure-range";
16 | import noOctalEscape from "./rules/no-octal-escape";
17 | import noOptionalAssertion from "./rules/no-optional-assertion";
18 | import noPotentiallyEmptyBackreference from "./rules/no-potentially-empty-backreference";
19 | import noTriviallyNestedLookaround from "./rules/no-trivially-nested-lookaround";
20 | import noTriviallyNestedQuantifier from "./rules/no-trivially-nested-quantifier";
21 | import noUnnecessaryAssertions from "./rules/no-unnecessary-assertions";
22 | import noUnnecessaryCharacterClass from "./rules/no-unnecessary-character-class";
23 | import noUnnecessaryFlag from "./rules/no-unnecessary-flag";
24 | import noUnnecessaryGroup from "./rules/no-unnecessary-group";
25 | import noUnnecessaryLazy from "./rules/no-unnecessary-lazy";
26 | import noUnnecessaryQuantifier from "./rules/no-unnecessary-quantifier";
27 | import noZeroQuantifier from "./rules/no-zero-quantifier";
28 | import optimalConcatenationQuantifier from "./rules/optimal-concatenation-quantifier";
29 | import optimalLookaroundQuantifier from "./rules/optimal-lookaround-quantifier";
30 | import optimizedCharacterClass from "./rules/optimized-character-class";
31 | import preferCharacterClass from "./rules/prefer-character-class";
32 | import preferPredefinedAssertion from "./rules/prefer-predefined-assertion";
33 | import preferPredefinedCharacterSet from "./rules/prefer-predefined-character-set";
34 | import preferPredefinedQuantifiers from "./rules/prefer-predefined-quantifiers";
35 | import simpleConstantQuantifier from "./rules/simple-constant-quantifier";
36 | import sortFlags from "./rules/sort-flags";
37 |
38 | export const configs = _configs;
39 | export const rules = {
40 | "confusing-quantifier": confusingQuantifier,
41 | "consistent-match-all-characters": consistentMatchAllCharacters,
42 | "disjoint-alternatives": disjointAlternatives,
43 | "identity-escape": identityEscape,
44 | "no-constant-capturing-group": noConstantCapturingGroup,
45 | "no-empty-alternative": noEmptyAlternative,
46 | "no-empty-backreference": noEmptyBackreference,
47 | "no-empty-lookaround": noEmptyLookaround,
48 | "no-lazy-ends": noLazyEnds,
49 | "no-obscure-range": noObscureRange,
50 | "no-octal-escape": noOctalEscape,
51 | "no-optional-assertion": noOptionalAssertion,
52 | "no-potentially-empty-backreference": noPotentiallyEmptyBackreference,
53 | "no-trivially-nested-lookaround": noTriviallyNestedLookaround,
54 | "no-trivially-nested-quantifier": noTriviallyNestedQuantifier,
55 | "no-unnecessary-assertions": noUnnecessaryAssertions,
56 | "no-unnecessary-character-class": noUnnecessaryCharacterClass,
57 | "no-unnecessary-flag": noUnnecessaryFlag,
58 | "no-unnecessary-group": noUnnecessaryGroup,
59 | "no-unnecessary-lazy": noUnnecessaryLazy,
60 | "no-unnecessary-quantifier": noUnnecessaryQuantifier,
61 | "no-zero-quantifier": noZeroQuantifier,
62 | "optimal-concatenation-quantifier": optimalConcatenationQuantifier,
63 | "optimal-lookaround-quantifier": optimalLookaroundQuantifier,
64 | "optimized-character-class": optimizedCharacterClass,
65 | "prefer-character-class": preferCharacterClass,
66 | "prefer-predefined-assertion": preferPredefinedAssertion,
67 | "prefer-predefined-character-set": preferPredefinedCharacterSet,
68 | "prefer-predefined-quantifiers": preferPredefinedQuantifiers,
69 | "simple-constant-quantifier": simpleConstantQuantifier,
70 | "sort-flags": sortFlags,
71 | };
72 |
--------------------------------------------------------------------------------
/lib/rules/confusing-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { mention } from "../format";
2 | import { createRuleListener, getDocUrl, CleanRegexRule } from "../rules-util";
3 | import { isPotentiallyEmpty, quantToString } from "../ast-util";
4 |
5 | export default {
6 | meta: {
7 | type: "problem",
8 | docs: {
9 | description: "Warn about confusing quantifiers.",
10 | url: getDocUrl(/* #GENERATED */ "confusing-quantifier"),
11 | },
12 | },
13 |
14 | create(context) {
15 | return createRuleListener(({ visitAST, reportQuantifier }) => {
16 | visitAST({
17 | onQuantifierEnter(node) {
18 | if (node.min > 0 && isPotentiallyEmpty(node.element)) {
19 | const proposal = quantToString({ ...node, min: 0 });
20 | context.report({
21 | message:
22 | `This quantifier is confusing because its minimum is ${node.min} but it can match the empty string.` +
23 | ` Maybe replace it with ${mention(
24 | proposal
25 | )} to reflect that it can match the empty string?`,
26 | ...reportQuantifier(node),
27 | });
28 | return;
29 | }
30 | },
31 | });
32 | });
33 | },
34 | } as CleanRegexRule;
35 |
--------------------------------------------------------------------------------
/lib/rules/consistent-match-all-characters.ts:
--------------------------------------------------------------------------------
1 | import { AnyCharacterSet, CharacterClass, Flags, Node } from "regexpp/ast";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 | import { many, mention } from "../format";
4 | import { assertNever } from "../util";
5 | import { isMatchAll } from "../char-util";
6 |
7 | function removeDescendantNodes(nodes: (T & { node: Node })[]): T[] {
8 | // this is a O(n^2) implementation
9 | // by sorting the nodes and using binary search, this can be implemented in O(n * log(n))
10 |
11 | return nodes.filter(({ node }) => {
12 | return !nodes.some(({ node: n }) => n != node && n.start <= node.start && n.end >= node.end);
13 | });
14 | }
15 |
16 | type Mode = "dot" | "dot-if-dotAll" | "char-class";
17 | const modes: Mode[] = ["dot", "dot-if-dotAll", "char-class"];
18 |
19 | export default {
20 | meta: {
21 | type: "suggestion",
22 | docs: {
23 | description: "Use one character class consistently whenever all characters have to be matched.",
24 | url: getDocUrl(/* #GENERATED */ "consistent-match-all-characters"),
25 | },
26 | fixable: "code",
27 | schema: [
28 | {
29 | type: "object",
30 | properties: {
31 | mode: {
32 | type: "string",
33 | enum: modes,
34 | },
35 | charClass: {
36 | type: "string",
37 | },
38 | },
39 | additionalProperties: false,
40 | },
41 | ],
42 | },
43 |
44 | create(context) {
45 | const options = context.options[0] || {};
46 | const mode: Mode = options.mode || "dot-if-dotAll";
47 | const charClass: string = options.charClass || "[\\s\\S]";
48 |
49 | function getReplacement(flags: Readonly): string {
50 | switch (mode) {
51 | case "dot":
52 | return ".";
53 |
54 | case "dot-if-dotAll":
55 | if (flags.dotAll) {
56 | return ".";
57 | } else {
58 | return charClass;
59 | }
60 |
61 | case "char-class":
62 | return charClass;
63 |
64 | default:
65 | throw assertNever(mode);
66 | }
67 | }
68 |
69 | return createRuleListener(({ visitAST, pattern, flags, replaceElement, replaceLiteral }) => {
70 | const replacement = getReplacement(flags);
71 |
72 | let nodesToReplace: { node: CharacterClass | AnyCharacterSet; message: string; replacement: string }[] = [];
73 |
74 | visitAST({
75 | onCharacterClassEnter(node) {
76 | if (node.raw !== replacement && isMatchAll(node, flags)) {
77 | nodesToReplace.push({
78 | node,
79 | message: `Replace this character class with ${mention(replacement)}.`,
80 | replacement,
81 | });
82 | }
83 | },
84 | onCharacterSetEnter(node) {
85 | if ("." !== replacement && node.kind === "any" && flags.dotAll) {
86 | nodesToReplace.push({
87 | node,
88 | message: `Replace this dot with ${mention(replacement)}.`,
89 | replacement,
90 | });
91 | }
92 | },
93 | });
94 |
95 | // remove nodes contained by other nodes
96 | nodesToReplace = removeDescendantNodes(nodesToReplace);
97 |
98 | // nothing to report
99 | if (nodesToReplace.length === 0) {
100 | return;
101 | }
102 |
103 | if ((replacement === ".") === flags.dotAll) {
104 | // we don't need to change the flags, so just report all
105 | nodesToReplace.forEach(({ node, message, replacement }) => {
106 | context.report({
107 | message: message,
108 | ...replaceElement(node, replacement, { dependsOnFlags: true }),
109 | });
110 | });
111 | } else {
112 | let newFlags;
113 | if (replacement === ".") {
114 | // add s flag
115 | newFlags = flags.raw + "s";
116 | // This is a bit trickier because the sorted-flags rule. If the flags are sorted, we will insert s
117 | // at the correct position, but if they aren't, we will just append the s.
118 | if (flags.raw === [...flags.raw].sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)).join("")) {
119 | // sorted
120 | newFlags = [...newFlags].sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)).join("");
121 | }
122 | } else {
123 | // remove s flag
124 | newFlags = flags.raw.replace(/s/, "");
125 | }
126 |
127 | // sort replacements
128 | nodesToReplace.sort((a, b) => a.node.start - b.node.start);
129 |
130 | // create new pattern
131 | const oldPattern = pattern.raw;
132 | let lastEndIndex = 0;
133 | let newPattern = "";
134 |
135 | for (const { node, replacement } of nodesToReplace) {
136 | newPattern += oldPattern.substr(lastEndIndex, node.start);
137 | newPattern += replacement;
138 | lastEndIndex = node.end;
139 | }
140 | newPattern += oldPattern.substr(lastEndIndex);
141 |
142 | const manyElements = many(nodesToReplace.length, "element");
143 | context.report({
144 | message:
145 | `${manyElements} in the pattern will be replaced with ${mention(replacement)}` +
146 | ` and the s flag will be ${replacement === "." ? "added" : "removed"}.`,
147 | ...replaceLiteral(newPattern, newFlags),
148 | });
149 | }
150 | });
151 | },
152 | } as CleanRegexRule;
153 |
--------------------------------------------------------------------------------
/lib/rules/disjoint-alternatives.ts:
--------------------------------------------------------------------------------
1 | import { JS, NFA, ReadonlyNFA, TooManyNodesError } from "refa";
2 | import { Alternative, CapturingGroup, Group, LookaroundAssertion, Node, Pattern } from "regexpp/ast";
3 | import { mention, toRegExpString } from "../format";
4 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
5 | import { hasSomeDescendant, underAStar } from "../ast-util";
6 | import { nfaEquals, nfaIsSupersetOf, nfaUnionAll } from "../fa-util";
7 |
8 | export default {
9 | meta: {
10 | type: "problem",
11 | docs: {
12 | description: "Disallow different alternatives that can match the same words.",
13 | url: getDocUrl(/* #GENERATED */ "disjoint-alternatives"),
14 | },
15 | },
16 |
17 | create(context) {
18 | return createRuleListener(({ visitAST, flags, pattern, reportElement }) => {
19 | const parser = JS.Parser.fromAst({ pattern, flags });
20 |
21 | /**
22 | * Converts the given alternative to an NFA. The returned NFA does not accept the empty string.
23 | */
24 | function toNfa(alt: Alternative): ReadonlyNFA {
25 | try {
26 | const result = parser.parseElement(alt, { backreferences: "disable", assertions: "disable" });
27 | const nfa = NFA.fromRegex(result.expression, { maxCharacter: result.maxCharacter });
28 | nfa.withoutEmptyWord();
29 | return nfa;
30 | } catch (e) {
31 | // the NFA construction might fail because the NFA is too big
32 | if (e instanceof TooManyNodesError) {
33 | return NFA.empty({ maxCharacter: flags.unicode ? 0x10ffff : 0xffff });
34 | }
35 |
36 | throw e;
37 | }
38 | }
39 |
40 | function findFirstSuperset(alternatives: Alternative[], subset: ReadonlyNFA): Alternative[] {
41 | for (const alt of alternatives) {
42 | if (nfaIsSupersetOf(toNfa(alt), subset)) {
43 | return [alt];
44 | }
45 | }
46 | return [];
47 | }
48 | function findNonDisjoint(alternatives: Alternative[], set: ReadonlyNFA): Alternative[] {
49 | return alternatives.filter(alt => {
50 | return !areDisjoint(toNfa(alt), set);
51 | });
52 | }
53 | function areDisjoint(a: ReadonlyNFA, b: ReadonlyNFA): boolean {
54 | return a.isDisjointWith(b, {
55 | // limit the number of nodes that can be created during the intersection
56 | maxNodes: 1000,
57 | });
58 | }
59 |
60 | type Result = "disjoint" | "reported";
61 | function checkAlternatives(alternatives: readonly Alternative[]): Result {
62 | if (alternatives.length < 2) {
63 | return "disjoint";
64 | }
65 |
66 | let result: Result = "disjoint";
67 |
68 | let total: NFA | undefined = undefined;
69 | for (const alt of alternatives) {
70 | const isLast = alt === alternatives[alternatives.length - 1];
71 | const nfa = toNfa(alt);
72 |
73 | if (nfa.isEmpty) {
74 | // skip this alternative
75 | } else if (!total) {
76 | if (!isLast) {
77 | total = nfa.copy();
78 | }
79 | } else if (areDisjoint(total, nfa)) {
80 | if (!isLast) {
81 | total.union(nfa);
82 | }
83 | } else {
84 | const altIndex = alternatives.indexOf(alt);
85 | const beforeAlternatives = alternatives.slice(0, altIndex);
86 |
87 | const intersection = NFA.fromIntersection(total, nfa);
88 | const isSubset = nfaEquals(nfa, intersection);
89 |
90 | // try to find the alternatives that are not disjoint with this one
91 | const cause = isSubset ? findFirstSuperset(beforeAlternatives, nfa) : [];
92 | if (cause.length === 0) {
93 | cause.push(...findNonDisjoint(beforeAlternatives, nfa));
94 | }
95 | const causeMsg = cause ? cause.map(mention).join(" | ") : "the previous one(s)";
96 |
97 | // find out whether this alternative is a superset of the cause
98 | const isSuperset = nfaIsSupersetOf(nfa, nfaUnionAll(cause.map(toNfa), nfa.options));
99 |
100 | let message;
101 | if (isSubset) {
102 | message = isSuperset
103 | ? `This alternative is the same as ${causeMsg} and can be removed.`
104 | : `This alternative is a subset of ${causeMsg} and can be removed.`;
105 |
106 | // warn that the alternative contains a capturing group
107 | if (hasSomeDescendant(alt, d => d.type === "CapturingGroup")) {
108 | message += " This alternative contains a capturing group so be careful when removing.";
109 | }
110 | } else {
111 | let sharedLanguageMsg;
112 | try {
113 | sharedLanguageMsg = ` The shared language is ${toRegExpString(intersection)}.`;
114 | } catch (e) {
115 | // the regex of the intersection might be too big in which case the implementation will
116 | // throw an error
117 | sharedLanguageMsg = "";
118 | }
119 | message = isSuperset
120 | ? `This alternative is a superset of ${causeMsg}.`
121 | : `This alternative is not disjoint with ${causeMsg}.${sharedLanguageMsg}`;
122 | }
123 |
124 | // whether this ambiguity might cause exponential backtracking
125 | if (underAStar(alt)) {
126 | message += " This alternative is likely to cause exponential backtracking.";
127 | }
128 |
129 | context.report({
130 | message,
131 | ...reportElement(alt),
132 | });
133 | result = "reported";
134 | }
135 | }
136 |
137 | return result;
138 | }
139 |
140 | const ignoreNodes: Set = new Set();
141 | function ignoreParents(node: Node): void {
142 | for (let parent = node.parent; parent; parent = parent.parent) {
143 | ignoreNodes.add(parent);
144 | }
145 | }
146 | function process(node: Group | CapturingGroup | LookaroundAssertion | Pattern): void {
147 | if (!ignoreNodes.has(node)) {
148 | if (checkAlternatives(node.alternatives) === "reported") {
149 | ignoreParents(node);
150 | }
151 | }
152 | }
153 |
154 | visitAST({
155 | onAssertionLeave(node) {
156 | if (node.kind === "lookahead" || node.kind === "lookbehind") {
157 | process(node);
158 | }
159 | },
160 | onCapturingGroupLeave: process,
161 | onGroupLeave: process,
162 | onPatternLeave: process,
163 | });
164 | });
165 | },
166 | } as CleanRegexRule;
167 |
--------------------------------------------------------------------------------
/lib/rules/no-constant-capturing-group.ts:
--------------------------------------------------------------------------------
1 | import { mention } from "../format";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 | import { getConstant } from "../ast-util";
4 |
5 | export default {
6 | meta: {
7 | type: "suggestion",
8 | docs: {
9 | description: "Disallow capturing groups that can match only one word.",
10 | url: getDocUrl(/* #GENERATED */ "no-constant-capturing-group"),
11 | },
12 | schema: [
13 | {
14 | type: "object",
15 | properties: {
16 | ignoreNonEmpty: {
17 | type: "boolean",
18 | },
19 | },
20 | additionalProperties: false,
21 | },
22 | ],
23 | },
24 |
25 | create(context) {
26 | let ignoreNonEmpty = (context.options[0] || {}).ignoreNonEmpty;
27 | if (typeof ignoreNonEmpty !== "boolean") {
28 | ignoreNonEmpty = true;
29 | }
30 |
31 | return createRuleListener(({ visitAST, flags, reportElement }) => {
32 | visitAST({
33 | onCapturingGroupEnter(node) {
34 | if (node.alternatives.length === 1) {
35 | const concatenation = node.alternatives[0].elements;
36 |
37 | if (concatenation.length === 0) {
38 | context.report({
39 | message: "Empty capturing group",
40 | ...reportElement(node),
41 | });
42 | return;
43 | }
44 | }
45 |
46 | const constant = getConstant(node, flags);
47 | if (constant && !(ignoreNonEmpty && constant.word !== "")) {
48 | const word = constant.word
49 | ? `one word which is ${JSON.stringify(constant.word)}`
50 | : "the empty string";
51 | context.report({
52 | message: `The capturing group ${mention(node)} can only capture ${word}.`,
53 | ...reportElement(node),
54 | });
55 | return;
56 | }
57 | },
58 | });
59 | });
60 | },
61 | } as CleanRegexRule;
62 |
--------------------------------------------------------------------------------
/lib/rules/no-empty-alternative.ts:
--------------------------------------------------------------------------------
1 | import { CapturingGroup, Group, Pattern } from "regexpp/ast";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 |
4 | export default {
5 | meta: {
6 | type: "problem",
7 | docs: {
8 | description: "Disallow alternatives without elements.",
9 | url: getDocUrl(/* #GENERATED */ "no-empty-alternative"),
10 | },
11 | },
12 |
13 | create(context) {
14 | return createRuleListener(({ visitAST, reportElement }) => {
15 | function checkAlternatives(node: CapturingGroup | Group | Pattern) {
16 | if (node.alternatives.length >= 2) {
17 | // We want to have at least two alternatives because the zero alternatives isn't possible because of
18 | // the parser and one alternative is already handled by other rules.
19 | for (let i = 0; i < node.alternatives.length; i++) {
20 | const alt = node.alternatives[i];
21 | if (alt.elements.length === 0) {
22 | context.report({
23 | message: "No empty alternatives. Use quantifiers instead.",
24 | ...reportElement(node),
25 | });
26 | // don't report the same node multiple times
27 | return;
28 | }
29 | }
30 | }
31 | }
32 |
33 | visitAST({
34 | onGroupEnter: checkAlternatives,
35 | onCapturingGroupEnter: checkAlternatives,
36 | onPatternEnter: checkAlternatives,
37 | // While lookarounds can contain empty alternatives, lookarounds with empty alternatives are already
38 | // covered by the `no-empty-lookaround`.
39 | });
40 | });
41 | },
42 | } as CleanRegexRule;
43 |
--------------------------------------------------------------------------------
/lib/rules/no-empty-backreference.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { isEmptyBackreference, isZeroLength } from "../ast-util";
3 |
4 | export default {
5 | meta: {
6 | type: "problem",
7 | docs: {
8 | description: "Disallow backreferences that will always be replaced with the empty string.",
9 | url: getDocUrl(/* #GENERATED */ "no-empty-backreference"),
10 | },
11 | },
12 |
13 | create(context) {
14 | return createRuleListener(({ visitAST, reportElement }) => {
15 | visitAST({
16 | onBackreferenceEnter(node) {
17 | if (isZeroLength(node.resolved)) {
18 | context.report({
19 | message: "The referenced capturing group can only match the empty string.",
20 | ...reportElement(node),
21 | });
22 | return;
23 | }
24 |
25 | if (isEmptyBackreference(node)) {
26 | context.report({
27 | message:
28 | "The backreference is not reachable from the referenced capturing group without resetting the captured string.",
29 | ...reportElement(node),
30 | });
31 | return;
32 | }
33 | },
34 | });
35 | });
36 | },
37 | } as CleanRegexRule;
38 |
--------------------------------------------------------------------------------
/lib/rules/no-empty-lookaround.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { mention } from "../format";
3 | import { isPotentiallyEmpty } from "../ast-util";
4 |
5 | export default {
6 | meta: {
7 | type: "problem",
8 | docs: {
9 | description: "Disallow lookarounds that can match the empty string.",
10 | url: getDocUrl(/* #GENERATED */ "no-empty-lookaround"),
11 | },
12 | },
13 |
14 | create(context) {
15 | return createRuleListener(({ visitAST, reportElement }) => {
16 | visitAST({
17 | onAssertionEnter(node) {
18 | if (node.kind !== "lookahead" && node.kind !== "lookbehind") {
19 | return; // we don't need to check standard assertions
20 | }
21 |
22 | // we have to check the alternatives ourselves because negative lookarounds which trivially reject
23 | // cannot match the empty string.
24 | const empty = isPotentiallyEmpty(node.alternatives, { backreferencesAreNonEmpty: true });
25 |
26 | if (empty) {
27 | context.report({
28 | message:
29 | `The ${node.kind} ${mention(node)} is non-functional as it matches the empty string.` +
30 | ` It will always trivially ${node.negate ? "reject" : "accept"}.`,
31 | ...reportElement(node),
32 | });
33 | }
34 | },
35 | });
36 | });
37 | },
38 | } as CleanRegexRule;
39 |
--------------------------------------------------------------------------------
/lib/rules/no-lazy-ends.ts:
--------------------------------------------------------------------------------
1 | import { Alternative, Quantifier } from "regexpp/ast";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 |
4 | function getLazyEndQuantifiers(alternatives: Alternative[]): Quantifier[] {
5 | const quantifiers: Quantifier[] = [];
6 |
7 | for (const { elements } of alternatives) {
8 | if (elements.length > 0) {
9 | const last = elements[elements.length - 1];
10 | switch (last.type) {
11 | case "Quantifier":
12 | if (!last.greedy && last.min !== last.max) {
13 | quantifiers.push(last);
14 | } else if (last.max === 1) {
15 | const element = last.element;
16 | if (element.type === "Group" || element.type === "CapturingGroup") {
17 | quantifiers.push(...getLazyEndQuantifiers(element.alternatives));
18 | }
19 | }
20 | break;
21 |
22 | case "CapturingGroup":
23 | case "Group":
24 | quantifiers.push(...getLazyEndQuantifiers(last.alternatives));
25 | break;
26 |
27 | default:
28 | break;
29 | }
30 | }
31 | }
32 |
33 | return quantifiers;
34 | }
35 |
36 | export default {
37 | meta: {
38 | type: "problem",
39 | docs: {
40 | description: "Disallow lazy quantifiers at the end of an expression.",
41 | url: getDocUrl(/* #GENERATED */ "no-lazy-ends"),
42 | },
43 | },
44 |
45 | create(context) {
46 | return createRuleListener(({ pattern, reportElement, reportQuantifier }) => {
47 | const lazyQuantifiers = getLazyEndQuantifiers(pattern.alternatives);
48 |
49 | for (const lazy of lazyQuantifiers) {
50 | if (lazy.min === 0) {
51 | context.report({
52 | message:
53 | "The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.",
54 | ...reportElement(lazy),
55 | });
56 | } else if (lazy.min === 1) {
57 | context.report({
58 | message: "The quantifier can be removed because the quantifier is lazy and has a minimum of 1.",
59 | ...reportQuantifier(lazy),
60 | });
61 | } else {
62 | context.report({
63 | message: `The quantifier can be replaced with '{${lazy.min}}' because the quantifier is lazy and has a minimum of ${lazy.min}.`,
64 | ...reportQuantifier(lazy),
65 | });
66 | }
67 | }
68 | });
69 | },
70 | } as CleanRegexRule;
71 |
--------------------------------------------------------------------------------
/lib/rules/no-obscure-range.ts:
--------------------------------------------------------------------------------
1 | import { CharRange } from "refa";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 | import { isControlEscapeSequence, isHexadecimalEscapeSequence, isOctalEscapeSequence } from "../ast-util";
4 |
5 | const allowedRanges: readonly CharRange[] = [
6 | // digits 0-9
7 | { min: "0".charCodeAt(0), max: "9".charCodeAt(0) },
8 | // Latin A-Z
9 | { min: "A".charCodeAt(0), max: "Z".charCodeAt(0) },
10 | // Latin a-z
11 | { min: "a".charCodeAt(0), max: "z".charCodeAt(0) },
12 | ];
13 |
14 | function inRange(char: number, range: CharRange): boolean {
15 | return range.min <= char && char <= range.max;
16 | }
17 |
18 | export default {
19 | meta: {
20 | type: "problem",
21 | docs: {
22 | description: "Disallow obscure ranges in character classes.",
23 | url: getDocUrl(/* #GENERATED */ "no-obscure-range"),
24 | },
25 | },
26 |
27 | create(context) {
28 | return createRuleListener(({ visitAST, reportElement }) => {
29 | visitAST({
30 | onCharacterClassRangeEnter(node) {
31 | const { min, max } = node;
32 |
33 | if (min.value == max.value) {
34 | // we don't deal with that
35 | return;
36 | }
37 |
38 | if (isControlEscapeSequence(min) && isControlEscapeSequence(max)) {
39 | // both min and max are control escapes
40 | return;
41 | }
42 | if (isOctalEscapeSequence(min) && isOctalEscapeSequence(max)) {
43 | // both min and max are either octal
44 | return;
45 | }
46 | if ((isHexadecimalEscapeSequence(min) || min.value === 0) && isHexadecimalEscapeSequence(max)) {
47 | // both min and max are hexadecimal (with a small exception for \0)
48 | return;
49 | }
50 |
51 | if (allowedRanges.some(r => inRange(min.value, r) && inRange(max.value, r))) {
52 | return;
53 | }
54 |
55 | context.report({
56 | message: `It's not obvious what characters are matched by ${node.raw}`,
57 | ...reportElement(node),
58 | });
59 | },
60 | });
61 | });
62 | },
63 | } as CleanRegexRule;
64 |
--------------------------------------------------------------------------------
/lib/rules/no-octal-escape.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { isOctalEscapeSequence } from "../ast-util";
3 |
4 | export default {
5 | meta: {
6 | type: "problem",
7 | docs: {
8 | description: "Disallow octal escapes outside of character classes.",
9 | url: getDocUrl(/* #GENERATED */ "no-octal-escape"),
10 | },
11 | },
12 |
13 | create(context) {
14 | return createRuleListener(({ visitAST, reportElement }) => {
15 | visitAST({
16 | onCharacterEnter(node) {
17 | if (node.parent.type === "CharacterClass" || node.parent.type === "CharacterClassRange") {
18 | // inside character classes, octal escapes are fine
19 | return;
20 | }
21 |
22 | if (node.value > 0 && isOctalEscapeSequence(node)) {
23 | context.report({
24 | message: "Do not use octal escapes because they might be confused with backreferences.",
25 | ...reportElement(node),
26 | });
27 | }
28 | },
29 | });
30 | });
31 | },
32 | } as CleanRegexRule;
33 |
--------------------------------------------------------------------------------
/lib/rules/no-optional-assertion.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { mention } from "../format";
3 | import { Assertion, CapturingGroup, Group, Node, Quantifier } from "regexpp/ast";
4 | import { assertNever } from "../util";
5 | import { isZeroLength } from "../ast-util";
6 |
7 | /**
8 | * Returns the closest ascendant quantifier with a minimum of 0.
9 | */
10 | function getZeroQuantifier(node: Node): (Quantifier & { min: 0 }) | null {
11 | if (node.type === "Quantifier" && node.min === 0) {
12 | return node as Quantifier & { min: 0 };
13 | } else if (node.parent) {
14 | return getZeroQuantifier(node.parent);
15 | } else {
16 | return null;
17 | }
18 | }
19 |
20 | /**
21 | * Returns whether the given assertion is optional in regard to the given quantifier with a minimum of 0.
22 | *
23 | * Optional means that all paths in the element if the quantifier which contain the given assertion also have do not
24 | * consume characters. For more information and examples on optional assertions, see the documentation page of this
25 | * rule.
26 | */
27 | function isOptional(assertion: Assertion, quantifier: Quantifier & { min: 0 }): boolean {
28 | /**
29 | * This will implement a bottom-up approach.
30 | */
31 | function isOptionalImpl(element: Assertion | Quantifier | Group | CapturingGroup): boolean {
32 | if (element.parent === quantifier) {
33 | // We reached the top.
34 | // If we made it this far, we could not disprove that the assertion is optional, so it has to optional.
35 | return true;
36 | }
37 |
38 | const parent = element.parent;
39 | if (parent.type === "Alternative") {
40 | // make sure that all element before and after are zero length
41 | for (const e of parent.elements) {
42 | if (e === element) {
43 | continue; // we will ignore this element.
44 | }
45 |
46 | if (!isZeroLength(e)) {
47 | // Some element around our target element can possibly consume characters.
48 | // This means, we found a path from or to the assertion which can consume characters.
49 | return false;
50 | }
51 | }
52 |
53 | if (parent.parent.type === "Pattern") {
54 | throw new Error("The given assertion is not a descendant of the given quantifier.");
55 | } else {
56 | return isOptionalImpl(parent.parent);
57 | }
58 | } else if (parent.type === "Quantifier") {
59 | if (parent.max > 1 && !isZeroLength(parent)) {
60 | // If an ascendant quantifier of the element has maximum of 2 or more, we have to check whether
61 | // the quantifier itself has zero length.
62 | // E.g. in /(?:a|(\b|-){2})?/ the \b is not optional
63 | return false;
64 | }
65 |
66 | return isOptionalImpl(parent);
67 | } else {
68 | throw assertNever(parent);
69 | }
70 | }
71 |
72 | return isOptionalImpl(assertion);
73 | }
74 |
75 | export default {
76 | meta: {
77 | type: "problem",
78 | docs: {
79 | description: "Disallow optional assertions.",
80 | url: getDocUrl(/* #GENERATED */ "no-optional-assertion"),
81 | },
82 | },
83 |
84 | create(context) {
85 | return createRuleListener(({ visitAST, reportElement }) => {
86 | visitAST({
87 | onAssertionEnter(node) {
88 | const q = getZeroQuantifier(node);
89 |
90 | if (q && isOptional(node, q)) {
91 | context.report({
92 | message:
93 | "This assertion effectively optional and does not change the pattern." +
94 | ` Either remove the assertion or change the parent quantifier ${mention(
95 | q.raw.substr(q.element.raw.length)
96 | )}.`,
97 | ...reportElement(node),
98 | });
99 | }
100 | },
101 | });
102 | });
103 | },
104 | } as CleanRegexRule;
105 |
--------------------------------------------------------------------------------
/lib/rules/no-potentially-empty-backreference.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { backreferenceAlwaysAfterGroup, isEmptyBackreference } from "../ast-util";
3 |
4 | export default {
5 | meta: {
6 | type: "problem",
7 | docs: {
8 | description: "Disallow backreferences that reference a group that might not be matched.",
9 | url: getDocUrl(/* #GENERATED */ "no-potentially-empty-backreference"),
10 | },
11 | },
12 |
13 | create(context) {
14 | return createRuleListener(({ visitAST, reportElement }) => {
15 | visitAST({
16 | onBackreferenceEnter(node) {
17 | if (isEmptyBackreference(node)) {
18 | // handled by no-empty-backreference
19 | return;
20 | }
21 |
22 | if (!backreferenceAlwaysAfterGroup(node)) {
23 | context.report({
24 | message:
25 | "Some path leading to the backreference do not go through the referenced capturing group without resetting its text.",
26 | ...reportElement(node),
27 | });
28 | }
29 | },
30 | });
31 | });
32 | },
33 | } as CleanRegexRule;
34 |
--------------------------------------------------------------------------------
/lib/rules/no-trivially-nested-lookaround.ts:
--------------------------------------------------------------------------------
1 | import { Assertion, BoundaryAssertion, LookaroundAssertion } from "regexpp/ast";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 |
4 | function getTriviallyNestedAssertion(node: LookaroundAssertion): Assertion | null {
5 | const alternatives = node.alternatives;
6 | if (alternatives.length === 1) {
7 | const elements = alternatives[0].elements;
8 | if (elements.length === 1) {
9 | const element = elements[0];
10 | if (element.type === "Assertion") {
11 | return element;
12 | }
13 | }
14 | }
15 |
16 | return null;
17 | }
18 |
19 | function negateLookaround(lookaround: LookaroundAssertion): string {
20 | let wasReplaced = false;
21 | const replacement = lookaround.raw.replace(/^(\(\?)([=!])/, (m, g1, g2) => {
22 | wasReplaced = true;
23 | return g1 + (g2 == "=" ? "!" : "=");
24 | });
25 |
26 | if (!wasReplaced) {
27 | throw new Error(`The lookaround ${lookaround.raw} could not be negated!`);
28 | }
29 |
30 | return replacement;
31 | }
32 | function negateBoundary(boundary: BoundaryAssertion): string {
33 | let wasReplaced = false;
34 | const replacement = boundary.raw.replace(/^\\b/i, m => {
35 | wasReplaced = true;
36 | return m == "\\b" ? "\\B" : "\\b";
37 | });
38 |
39 | if (!wasReplaced) {
40 | throw new Error(`The lookaround ${boundary.raw} could not be negated!`);
41 | }
42 |
43 | return replacement;
44 | }
45 |
46 | export default {
47 | meta: {
48 | type: "suggestion",
49 | docs: {
50 | description: "Disallow lookarounds that only contain another assertion.",
51 | url: getDocUrl(/* #GENERATED */ "no-trivially-nested-lookaround"),
52 | },
53 | fixable: "code",
54 | },
55 |
56 | create(context) {
57 | return createRuleListener(({ visitAST, replaceElement }) => {
58 | visitAST({
59 | onAssertionEnter(node) {
60 | if (node.kind === "lookahead" || node.kind === "lookbehind") {
61 | const inner = getTriviallyNestedAssertion(node);
62 |
63 | if (!inner) {
64 | return;
65 | }
66 |
67 | let replacement;
68 |
69 | if (!node.negate) {
70 | // the outer lookaround can be replace with the inner assertion as is
71 | replacement = inner.raw;
72 | } else {
73 | // the outer lookaround can be replace with the inner assertion negated
74 | switch (inner.kind) {
75 | case "lookahead":
76 | case "lookbehind":
77 | replacement = negateLookaround(inner);
78 | break;
79 |
80 | case "word":
81 | replacement = negateBoundary(inner);
82 | break;
83 |
84 | default:
85 | // not possible for anchors. E.g. (?!$), (? {
31 | visitAST({
32 | onCharacterClassEnter(node) {
33 | if (node.elements.length !== 1) {
34 | return;
35 | }
36 |
37 | const element = node.elements[0];
38 | if (element.type === "CharacterSet") {
39 | // e.g. \s \W \p{SOME_NAME}
40 | const set = node.negate ? negateCharacterSetRaw(element) : element.raw;
41 |
42 | context.report({
43 | message: "Unnecessary character class.",
44 | ...replaceElement(node, set),
45 | });
46 | } else if (element.type === "Character") {
47 | if (node.negate) {
48 | // can't do anything. e.g. [^a]
49 | return;
50 | }
51 | if (element.value > 0 && isOctalEscapeSequence(element)) {
52 | // don't use octal escapes outside character classes
53 | return;
54 | }
55 |
56 | if (element.raw === "\\b") {
57 | // \b means backspace in character classes, so we have to escape it
58 | if (!avoidEscape) {
59 | context.report({
60 | message: "Unnecessary character class.",
61 | ...replaceElement(node, "\\x08"),
62 | });
63 | }
64 | return;
65 | }
66 |
67 | if (SPECIAL_CHARACTERS.has(element.raw)) {
68 | // special characters like `.+*?()`
69 | if (!avoidEscape) {
70 | context.report({
71 | message: "Unnecessary character class.",
72 | ...replaceElement(node, "\\" + element.raw),
73 | });
74 | }
75 | return;
76 | }
77 |
78 | if (isEscapeSequence(element)) {
79 | // sequences like `\n` `\xFF`
80 | // It's not an octal escape. Those were handled before.
81 | if (!avoidEscape) {
82 | context.report({
83 | message: "Unnecessary character class.",
84 | ...replaceElement(node, element.raw),
85 | });
86 | }
87 | return;
88 | }
89 |
90 | if (/^\\[^kbBpP]$/.test(element.raw)) {
91 | // except for a select few, all escaped characters can be copied as is
92 | context.report({
93 | message: "Unnecessary character class.",
94 | ...replaceElement(node, element.raw),
95 | });
96 | return;
97 | }
98 |
99 | // At this point we will just insert the character as is but this might lead to syntax changes
100 | // in some edge cases (see test file). To prevent this, we will re-parse the pattern with the
101 | // character class replaced by the character literal and if the re-parsed pattern does not
102 | // change, aside from the character literal, the character class is unnecessary.
103 | // (we also have to check that the character literal has the same value)
104 |
105 | const parent = node.parent;
106 | const before = pattern.raw.substr(0, node.start - pattern.start);
107 | const after = pattern.raw.substr(node.end - pattern.start);
108 | const withoutCharacterClass = before + element.raw + after;
109 | const ast = parseExpression(withoutCharacterClass);
110 | if (ast) {
111 | // replace the group with its contents in the original AST
112 |
113 | let equal;
114 | if (parent.type === "Alternative") {
115 | const parentIndex = parent.elements.indexOf(node);
116 |
117 | const oldElements = parent.elements;
118 | const newElements = [
119 | ...parent.elements.slice(0, parentIndex),
120 | element,
121 | ...parent.elements.slice(parentIndex + 1),
122 | ];
123 |
124 | parent.elements = newElements;
125 |
126 | try {
127 | equal = areEqual(pattern, ast);
128 | } finally {
129 | // switch back
130 | parent.elements = oldElements;
131 | }
132 | } else {
133 | parent.element = element;
134 |
135 | try {
136 | equal = areEqual(pattern, ast);
137 | } finally {
138 | // switch back
139 | parent.element = node;
140 | }
141 | }
142 |
143 | if (equal) {
144 | context.report({
145 | message: "Unnecessary character class.",
146 | ...replaceElement(node, element.raw),
147 | });
148 | }
149 | }
150 | }
151 | },
152 | });
153 | });
154 | },
155 | } as CleanRegexRule;
156 |
--------------------------------------------------------------------------------
/lib/rules/no-unnecessary-flag.ts:
--------------------------------------------------------------------------------
1 | import { visitRegExpAST } from "regexpp";
2 | import { Pattern } from "regexpp/ast";
3 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
4 |
5 | const determiner: Record boolean> = {
6 | iFlag(pattern) {
7 | let unnecessary = true;
8 |
9 | visitRegExpAST(pattern, {
10 | onCharacterEnter(node) {
11 | const value = String.fromCodePoint(node.value);
12 | if (value.toLowerCase() !== value.toUpperCase()) {
13 | unnecessary = false;
14 | }
15 | },
16 | });
17 |
18 | return unnecessary;
19 | },
20 | mFlag(pattern) {
21 | let unnecessary = true;
22 |
23 | visitRegExpAST(pattern, {
24 | onAssertionEnter(node) {
25 | if (node.kind === "start" || node.kind === "end") {
26 | unnecessary = false;
27 | }
28 | },
29 | });
30 |
31 | return unnecessary;
32 | },
33 | sFlag(pattern) {
34 | let unnecessary = true;
35 |
36 | visitRegExpAST(pattern, {
37 | onCharacterSetEnter(node) {
38 | if (node.kind === "any") {
39 | unnecessary = false;
40 | }
41 | },
42 | });
43 |
44 | return unnecessary;
45 | },
46 | };
47 |
48 | export default {
49 | meta: {
50 | type: "suggestion",
51 | docs: {
52 | description: "Disallow unnecessary regex flags.",
53 | url: getDocUrl(/* #GENERATED */ "no-unnecessary-flag"),
54 | },
55 | fixable: "code",
56 | schema: [
57 | {
58 | type: "object",
59 | properties: {
60 | ignore: {
61 | type: "array",
62 | items: {
63 | type: "string",
64 | enum: ["i", "m", "s"],
65 | },
66 | uniqueItems: true,
67 | },
68 | },
69 | additionalProperties: false,
70 | },
71 | ],
72 | },
73 |
74 | create(context) {
75 | return createRuleListener(({ pattern, flags, replaceFlags }) => {
76 | const unnecessaryFlags: [string, string][] = [];
77 |
78 | function checkFlag(flag: string, reason: string): void {
79 | if (!flags.raw.includes(flag)) {
80 | return;
81 | }
82 | if (
83 | context.options &&
84 | context.options[0] &&
85 | context.options[0].ignore &&
86 | context.options[0].ignore.includes(flag)
87 | ) {
88 | return;
89 | }
90 |
91 | const isUnnecessary = determiner[`${flag}Flag`](pattern);
92 |
93 | if (isUnnecessary) {
94 | unnecessaryFlags.push([flag, reason]);
95 | }
96 | }
97 |
98 | checkFlag("i", "does not contain case-variant characters");
99 | checkFlag("m", "does not contain start (^) or end ($) assertions");
100 | checkFlag("s", "does not contain dots (.)");
101 |
102 | if (unnecessaryFlags.length === 1) {
103 | const [flag, reason] = unnecessaryFlags[0];
104 | const newFlags = flags.raw.replace(RegExp(flag, "g"), "");
105 |
106 | context.report({
107 | message: `The ${flag} flags is unnecessary because the pattern ${reason}.`,
108 | ...replaceFlags(newFlags),
109 | });
110 | } else if (unnecessaryFlags.length > 1) {
111 | const uflags = unnecessaryFlags.map(x => x[0]).join("");
112 | const newFlags = flags.raw.replace(RegExp(`[${uflags}]`, "g"), "");
113 |
114 | context.report({
115 | message: `The flags ${uflags} are unnecessary because the pattern ${unnecessaryFlags
116 | .map(x => `[${x[0]}] ${x[1]}`)
117 | .join(", ")}`,
118 | ...replaceFlags(newFlags),
119 | });
120 | }
121 | });
122 | },
123 | } as CleanRegexRule;
124 |
--------------------------------------------------------------------------------
/lib/rules/no-unnecessary-group.ts:
--------------------------------------------------------------------------------
1 | import { Group, QuantifiableElement } from "regexpp/ast";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 | import { areEqual } from "../ast-util";
4 |
5 | /**
6 | * Returns whether the given group is the top-level group of its pattern.
7 | *
8 | * A pattern with a top-level groups is of the form `/(?:...)/flags`.
9 | */
10 | function isTopLevel(group: Group): boolean {
11 | const parent = group.parent;
12 | if (parent.type === "Alternative" && parent.elements.length === 1) {
13 | const parentParent = parent.parent;
14 | if (parentParent.type === "Pattern" && parentParent.alternatives.length === 1) {
15 | return true;
16 | }
17 | }
18 | return false;
19 | }
20 |
21 | export default {
22 | meta: {
23 | type: "suggestion",
24 | docs: {
25 | description: "Disallow unnecessary non-capturing groups.",
26 | url: getDocUrl(/* #GENERATED */ "no-unnecessary-group"),
27 | },
28 | fixable: "code",
29 | },
30 |
31 | create(context) {
32 | const options = context.options[0] || {};
33 | const allowTop = !!options.allowTop;
34 |
35 | return createRuleListener(({ visitAST, pattern, parseExpression, replaceElement }) => {
36 | visitAST({
37 | onGroupEnter(node) {
38 | if (allowTop && isTopLevel(node)) {
39 | return;
40 | }
41 |
42 | // If the pattern is empty, there's nothing we can do.
43 | if (pattern.raw === "(?:)") {
44 | return;
45 | }
46 |
47 | const groupContent = node.raw.substr("(?:".length, node.raw.length - "(?:)".length);
48 |
49 | // If the parent alternative contains only this group, it's unnecessary
50 | if (node.parent.type === "Alternative" && node.parent.elements.length === 1) {
51 | context.report({
52 | message: "Unnecessary non-capturing group.",
53 | ...replaceElement(node, groupContent),
54 | });
55 | return;
56 | }
57 |
58 | // With more than one alternative the group is always necessary
59 | // (The number of alternatives cannot be zero.)
60 | // e.g. (?:a|b)
61 | if (node.alternatives.length !== 1) {
62 | return;
63 | }
64 |
65 | const elements = node.alternatives[0].elements;
66 | const parent = node.parent;
67 |
68 | if (parent.type === "Quantifier") {
69 | // With zero or more than one element quantified the group is always necessary
70 | // e.g. (?:ab)* (?:)*
71 | if (elements.length !== 1) {
72 | return;
73 | }
74 | // if the single element is not quantifiable
75 | // e.g. (?:\b)* (?:a{2})*
76 | const type = elements[0].type;
77 | if (type === "Assertion" || type === "Quantifier") {
78 | return;
79 | }
80 | }
81 |
82 | // remove the group in the source
83 | const beforeGroup = pattern.raw.substr(0, node.start - pattern.start);
84 | const afterGroup = pattern.raw.substr(node.end - pattern.start);
85 | const withoutGroup = beforeGroup + groupContent + afterGroup;
86 |
87 | // if the expression without the group is syntactically valid and semantically equivalent to
88 | // the expression with the group, the group is unnecessary.
89 | // Because of backreferences we have to parse the whole pattern. I.e. the parser will interpret \10
90 | // as the a reference to the tenth capturing group it exists and as \x08 otherwise.
91 | const ast = parseExpression(withoutGroup);
92 | if (ast) {
93 | // replace the group with its contents in the original AST
94 |
95 | let equal;
96 | if (parent.type === "Alternative") {
97 | const parentIndex = parent.elements.indexOf(node);
98 |
99 | const oldElements = parent.elements;
100 | const newElements = [
101 | ...parent.elements.slice(0, parentIndex),
102 | ...elements,
103 | ...parent.elements.slice(parentIndex + 1),
104 | ];
105 |
106 | parent.elements = newElements;
107 |
108 | try {
109 | equal = areEqual(pattern, ast);
110 | } finally {
111 | // switch back
112 | parent.elements = oldElements;
113 | }
114 | } else {
115 | // we can do this because we check at the beginning of the function that
116 | // 1) there is exactly one element
117 | // 2) the element is quantifiable
118 | parent.element = elements[0] as QuantifiableElement;
119 |
120 | try {
121 | equal = areEqual(pattern, ast);
122 | } finally {
123 | // switch back
124 | parent.element = node;
125 | }
126 | }
127 |
128 | if (equal) {
129 | context.report({
130 | message: "Unnecessary non-capturing group.",
131 | ...replaceElement(node, groupContent),
132 | });
133 | }
134 | }
135 | },
136 | });
137 | });
138 | },
139 | } as CleanRegexRule;
140 |
--------------------------------------------------------------------------------
/lib/rules/no-unnecessary-lazy.ts:
--------------------------------------------------------------------------------
1 | import { Quantifier } from "regexpp/ast";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 | import { getFirstCharAfter, getFirstCharConsumedBy, getQuantifierRaw, matchingDirection } from "../ast-util";
4 |
5 | function withoutLazy(node: Quantifier): string {
6 | let raw = getQuantifierRaw(node);
7 | raw = raw.substr(0, raw.length - 1); // remove "?"
8 | return raw;
9 | }
10 |
11 | export default {
12 | meta: {
13 | type: "suggestion",
14 | docs: {
15 | description: "Disallow unnecessarily lazy quantifiers.",
16 | url: getDocUrl(/* #GENERATED */ "no-unnecessary-lazy"),
17 | },
18 | fixable: "code",
19 | },
20 |
21 | create(context) {
22 | return createRuleListener(({ visitAST, flags, replaceQuantifier }) => {
23 | visitAST({
24 | onQuantifierEnter(node) {
25 | if (node.greedy) {
26 | return;
27 | }
28 |
29 | if (node.min === node.max) {
30 | // a constant lazy quantifier (e.g. /a{2}?/)
31 | context.report({
32 | message: "The lazy modifier is unnecessary for constant quantifiers.",
33 | ...replaceQuantifier(node, withoutLazy(node)),
34 | });
35 | return;
36 | }
37 |
38 | // This is more tricky.
39 | // The basic idea here is that if the first character of the quantified element and the first
40 | // character of whatever comes after the quantifier are always different, then the lazy modifier
41 | // doesn't matter.
42 | // E.g. /a+?b+/ == /a+b+/
43 |
44 | const matchingDir = matchingDirection(node);
45 | const firstChar = getFirstCharConsumedBy(node, matchingDir, flags);
46 | if (!firstChar.empty) {
47 | const after = getFirstCharAfter(node, matchingDir, flags);
48 | if (!after.char.edge && firstChar.char.isDisjointWith(after.char.char)) {
49 | context.report({
50 | message:
51 | "The lazy modifier is unnecessary because the first character of the quantified element are always different from the characters that come after the quantifier.",
52 | ...replaceQuantifier(node, withoutLazy(node), { dependsOn: after.elements }),
53 | });
54 | return;
55 | }
56 | }
57 | },
58 | });
59 | });
60 | },
61 | } as CleanRegexRule;
62 |
--------------------------------------------------------------------------------
/lib/rules/no-unnecessary-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { isEmpty, isPotentiallyEmpty, isZeroLength } from "../ast-util";
3 |
4 | export default {
5 | meta: {
6 | type: "suggestion",
7 | docs: {
8 | description: "Disallow unnecessary quantifiers.",
9 | url: getDocUrl(/* #GENERATED */ "no-unnecessary-quantifier"),
10 | },
11 | fixable: "code",
12 | },
13 |
14 | create(context) {
15 | return createRuleListener(({ visitAST, replaceElement, reportQuantifier }) => {
16 | visitAST({
17 | onQuantifierEnter(node) {
18 | if (node.min === 1 && node.max === 1) {
19 | context.report({
20 | message: "Unnecessary quantifier.",
21 | ...replaceElement(node, node.element.raw),
22 | });
23 | return;
24 | }
25 |
26 | // only report from here on
27 |
28 | if (isEmpty(node.element)) {
29 | // we only report the quantifier.
30 | // no-unnecessary-group can then remove the element
31 | context.report({
32 | message: "The quantified element is empty, so the quantifier can be removed.",
33 | ...reportQuantifier(node),
34 | });
35 | return;
36 | }
37 |
38 | if (node.min === 0 && node.max === 1 && isPotentiallyEmpty(node.element)) {
39 | context.report({
40 | message:
41 | "The optional quantifier can be removed because the quantified element can match the empty string.",
42 | ...reportQuantifier(node),
43 | });
44 | return;
45 | }
46 |
47 | if (node.min > 0 && isZeroLength(node.element)) {
48 | context.report({
49 | message:
50 | "The quantified element does not consume characters, so the quantifier (minimum > 0) can be removed.",
51 | ...reportQuantifier(node),
52 | });
53 | return;
54 | }
55 | },
56 | });
57 | });
58 | },
59 | } as CleanRegexRule;
60 |
--------------------------------------------------------------------------------
/lib/rules/no-zero-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { hasSomeDescendant } from "../ast-util";
3 |
4 | export default {
5 | meta: {
6 | type: "problem",
7 | docs: {
8 | description: "Disallow quantifiers with a maximum of 0.",
9 | url: getDocUrl(/* #GENERATED */ "no-zero-quantifier"),
10 | },
11 | fixable: "code",
12 | },
13 |
14 | create(context) {
15 | return createRuleListener(({ visitAST, removeElement, reportElement }) => {
16 | visitAST({
17 | onQuantifierEnter(node) {
18 | if (node.max === 0) {
19 | let props;
20 | if (hasSomeDescendant(node, n => n.type === "CapturingGroup")) {
21 | // we can't just remove a capturing group, so we'll just report
22 | props = reportElement(node);
23 | } else {
24 | props = removeElement(node);
25 | }
26 |
27 | context.report({
28 | message: "The quantifier and the quantified element can be removed.",
29 | ...props,
30 | });
31 | }
32 | },
33 | });
34 | });
35 | },
36 | } as CleanRegexRule;
37 |
--------------------------------------------------------------------------------
/lib/rules/optimal-lookaround-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { Alternative, Quantifier } from "regexpp/ast";
2 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
3 |
4 | function getEndQuantifiers(alternatives: Alternative[]): Quantifier[] {
5 | const quantifiers: Quantifier[] = [];
6 |
7 | for (const { elements } of alternatives) {
8 | if (elements.length > 0) {
9 | const last = elements[elements.length - 1];
10 | switch (last.type) {
11 | case "Quantifier":
12 | if (last.min != last.max) {
13 | quantifiers.push(last);
14 | }
15 | break;
16 |
17 | case "Group":
18 | quantifiers.push(...getEndQuantifiers(last.alternatives));
19 | break;
20 |
21 | // we ignore capturing groups on purpose.
22 | // Example: /(?=(a*))\w+\1/ (no ideal but it illustrates the point)
23 |
24 | default:
25 | break;
26 | }
27 | }
28 | }
29 |
30 | return quantifiers;
31 | }
32 | function getStartQuantifiers(alternatives: Alternative[]): Quantifier[] {
33 | const quantifiers: Quantifier[] = [];
34 |
35 | for (const { elements } of alternatives) {
36 | if (elements.length > 0) {
37 | const first = elements[0];
38 | switch (first.type) {
39 | case "Quantifier":
40 | if (first.min != first.max) {
41 | quantifiers.push(first);
42 | }
43 | break;
44 |
45 | case "Group":
46 | quantifiers.push(...getStartQuantifiers(first.alternatives));
47 | break;
48 |
49 | // we ignore capturing groups on purpose.
50 | // Example: /(?=(a*))\w+\1/ (no ideal but it illustrates the point)
51 |
52 | default:
53 | break;
54 | }
55 | }
56 | }
57 |
58 | return quantifiers;
59 | }
60 |
61 | export default {
62 | meta: {
63 | type: "problem",
64 | docs: {
65 | description: "Disallows the alternatives of lookarounds that end with a non-constant quantifier.",
66 | url: getDocUrl(/* #GENERATED */ "optimal-lookaround-quantifier"),
67 | },
68 | },
69 |
70 | create(context) {
71 | return createRuleListener(({ visitAST, reportElement }) => {
72 | visitAST({
73 | onAssertionEnter(node) {
74 | if (node.kind === "lookahead" || node.kind === "lookbehind") {
75 | let endOrStart;
76 | let quantifiers;
77 | if (node.kind === "lookahead") {
78 | endOrStart = "end";
79 | quantifiers = getEndQuantifiers(node.alternatives);
80 | } /* if (node.kind === "lookbehind") */ else {
81 | endOrStart = "start";
82 | quantifiers = getStartQuantifiers(node.alternatives);
83 | }
84 |
85 | for (const q of quantifiers) {
86 | let proposal;
87 | if (q.min == 0) {
88 | proposal = "removed";
89 | } else if (q.min == 1) {
90 | proposal = `replaced with ${q.element.raw} (no quantifier)`;
91 | } else {
92 | proposal = `replaced with ${q.element.raw}{${q.min}}`;
93 | }
94 |
95 | context.report({
96 | message:
97 | `The quantified expression ${q.raw} at the ${endOrStart} of the expression tree should only be matched a constant number of times.` +
98 | ` The expression can be ${proposal} without affecting the lookaround.`,
99 | ...reportElement(q),
100 | });
101 | }
102 | }
103 | },
104 | });
105 | });
106 | },
107 | } as CleanRegexRule;
108 |
--------------------------------------------------------------------------------
/lib/rules/optimized-character-class.ts:
--------------------------------------------------------------------------------
1 | import { CharacterClassElement, CharacterClassRange } from "regexpp/ast";
2 | import { mentionCharElement } from "../format";
3 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
4 | import { emptyCharSet, toCharSet } from "../char-util";
5 |
6 | export default {
7 | meta: {
8 | type: "suggestion",
9 | docs: {
10 | description: "Disallows unnecessary elements in character classes.",
11 | url: getDocUrl(/* #GENERATED */ "optimized-character-class"),
12 | },
13 | fixable: "code",
14 | },
15 |
16 | create(context) {
17 | return createRuleListener(({ visitAST, flags, removeElement, replaceElement }) => {
18 | visitAST({
19 | onCharacterClassEnter(node) {
20 | const elements = node.elements;
21 | if (elements.length === 0) {
22 | // it's optimal as is
23 | return;
24 | }
25 |
26 | interface Report {
27 | message: string;
28 | replacement: string;
29 | }
30 | const reports = new Map();
31 |
32 | function checkRangeElements(range: CharacterClassRange): void {
33 | // Note: These reports may later be overwritten should the range be removed.
34 |
35 | if (range.min.value === range.max.value) {
36 | reports.set(range, {
37 | message: `${mentionCharElement(range)} contains only a single value.`,
38 | replacement: range.min.raw === "-" ? "\\-" : range.min.raw,
39 | });
40 | }
41 |
42 | if (range.min.value + 1 === range.max.value) {
43 | const min = range.min.raw === "-" ? "\\-" : range.min.raw;
44 | const max = range.max.raw === "-" ? "\\-" : range.max.raw;
45 | reports.set(range, {
46 | message: `${mentionCharElement(range)} contains only its two ends.`,
47 | replacement: min + max,
48 | });
49 | }
50 | }
51 |
52 | for (const element of elements) {
53 | if (element.type === "CharacterClassRange") {
54 | checkRangeElements(element);
55 | }
56 | }
57 |
58 | // detect duplicates and subsets
59 |
60 | // This will be done in 3 phases. First we check all single characters, then all characters ranges,
61 | // and last all character sets. This will ensure that we keep character sets over character ranges,
62 | // and character ranges over character sets.
63 |
64 | const empty = emptyCharSet(flags);
65 | const elementChars = elements.map(e => toCharSet([e], flags));
66 |
67 | const order: CharacterClassElement["type"][] = ["Character", "CharacterClassRange", "CharacterSet"];
68 | for (const currentType of order) {
69 | for (let i = elements.length - 1; i >= 0; i--) {
70 | const current = elements[i];
71 | if (current.type !== currentType) {
72 | continue;
73 | }
74 |
75 | const currentChars = elementChars[i];
76 | const totalWithCurrent = empty.union(...elementChars.filter((_, index) => index !== i));
77 |
78 | if (totalWithCurrent.isSupersetOf(currentChars)) {
79 | elementChars[i] = empty;
80 |
81 | // try to find a single element which is still a superset
82 | let simpleSuper: CharacterClassElement | undefined = undefined;
83 | for (let k = 0; k < elements.length; k++) {
84 | if (elementChars[k].isSupersetOf(currentChars)) {
85 | simpleSuper = elements[k];
86 | break;
87 | }
88 | }
89 |
90 | if (simpleSuper === undefined) {
91 | reports.set(current, {
92 | message: `${mentionCharElement(
93 | current
94 | )} is already included by some combination of other elements.`,
95 | replacement: "",
96 | });
97 | } else {
98 | reports.set(current, {
99 | message: `${mentionCharElement(
100 | current
101 | )} is already included by ${mentionCharElement(simpleSuper)}.`,
102 | replacement: "",
103 | });
104 | }
105 | }
106 | }
107 | }
108 |
109 | for (const [element, { message, replacement }] of reports) {
110 | if (replacement) {
111 | context.report({
112 | message,
113 | ...replaceElement(element, replacement, {
114 | // the replacement might depend on the i flag.
115 | dependsOnFlags: true,
116 | }),
117 | });
118 | } else {
119 | context.report({
120 | message,
121 | ...removeElement(element, {
122 | // the replacement might depend on the i flag.
123 | dependsOnFlags: true,
124 | }),
125 | });
126 | }
127 | }
128 | },
129 | });
130 | });
131 | },
132 | } as CleanRegexRule;
133 |
--------------------------------------------------------------------------------
/lib/rules/prefer-predefined-assertion.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { CharSet } from "refa";
3 | import { CharacterClass, CharacterSet, Element, LookaroundAssertion } from "regexpp/ast";
4 | import { assertionKindToMatchingDirection, getFirstCharAfter, invertMatchingDirection } from "../ast-util";
5 | import { isMatchAll, wordCharSet } from "../char-util";
6 |
7 | function getCharacters(lookaround: LookaroundAssertion): CharacterSet | CharacterClass | null {
8 | if (lookaround.alternatives.length === 1) {
9 | const alt = lookaround.alternatives[0];
10 | if (alt.elements.length === 1) {
11 | const first = alt.elements[0];
12 | if (first.type === "CharacterSet" || first.type === "CharacterClass") {
13 | return first;
14 | }
15 | }
16 | }
17 | return null;
18 | }
19 |
20 | export default {
21 | meta: {
22 | type: "suggestion",
23 | docs: {
24 | description: "Prefer predefined assertions over equivalent lookarounds.",
25 | url: getDocUrl(/* #GENERATED */ "prefer-predefined-assertion"),
26 | },
27 | fixable: "code",
28 | },
29 |
30 | create(context) {
31 | return createRuleListener(({ visitAST, flags, replaceElement }) => {
32 | // /\b/ == /(? chars.isSubsetOf(wordCharSet(flags));
36 | const isNonWord = (chars: CharSet) => chars.isDisjointWith(wordCharSet(flags));
37 |
38 | function replaceWordAssertion(node: LookaroundAssertion, wordNegated: boolean): void {
39 | const direction = assertionKindToMatchingDirection(node.kind);
40 | const dependsOn: Element[] = [];
41 |
42 | /**
43 | * Whether the lookaround is equivalent to (?!\w) / (? {
38 | const digitChars = JS.createCharSet([{ kind: "digit", negate: false }], flags);
39 | const wordChars = JS.createCharSet([{ kind: "word", negate: false }], flags);
40 | const EMPTY = JS.createCharSet([], flags);
41 |
42 | visitAST({
43 | onCharacterClassEnter(node) {
44 | const elements = node.elements;
45 |
46 | if (elements.some(e => e.type === "CharacterSet" && e.kind === "word" && !e.negate)) {
47 | // this will only so \d and \w, so if \w is already present, we can't do anything
48 | return;
49 | }
50 |
51 | const chars = elements.map(e => toCharSet([e], flags));
52 |
53 | // try to do \w
54 |
55 | const hits: number[] = [];
56 | chars.forEach((c, i) => {
57 | if (wordChars.isSupersetOf(c)) {
58 | hits.push(i);
59 | }
60 | });
61 |
62 | function getCharacterClass(hitReplacement: Simple) {
63 | let first = true;
64 | const newElements: Simple[] = [];
65 | elements.forEach((e, i) => {
66 | if (hits.indexOf(i) >= 0) {
67 | if (first) {
68 | newElements.push(hitReplacement);
69 | first = false;
70 | }
71 | } else {
72 | newElements.push(e);
73 | }
74 | });
75 | return elementsToCharacterClass(newElements, node.negate);
76 | }
77 |
78 | let union = EMPTY.union(...hits.map(i => chars[i]));
79 | if (union.equals(wordChars)) {
80 | const replacement = getCharacterClass({
81 | type: "CharacterSet",
82 | kind: "word",
83 | negate: false,
84 | raw: "\\w",
85 | });
86 |
87 | context.report({
88 | message: "Some of the character class elements can be simplified to \\w.",
89 | ...replaceElement(node, replacement),
90 | ...reportElements(hits.map(i => elements[i])), // override report range
91 | });
92 | return;
93 | }
94 |
95 | // try to do \d
96 |
97 | if (elements.some(e => e.type === "CharacterSet" && e.kind === "digit" && !e.negate)) {
98 | return;
99 | }
100 |
101 | hits.length = 0;
102 | chars.forEach((c, i) => {
103 | if (digitChars.isSupersetOf(c)) {
104 | hits.push(i);
105 | }
106 | });
107 |
108 | union = EMPTY.union(...hits.map(i => chars[i]));
109 | if (union.equals(digitChars)) {
110 | const isAllowedDigitRange = allowDigitRange && hits.every(i => isDigitRange(elements[i]));
111 | // only suggest a fix if it isn't an allowed digit range or if the whole character class is
112 | // equal to \d
113 | if (!isAllowedDigitRange || hits.length === elements.length) {
114 | const replacement = getCharacterClass({
115 | type: "CharacterSet",
116 | kind: "digit",
117 | negate: false,
118 | raw: "\\d",
119 | });
120 |
121 | context.report({
122 | message: "Some of the character class elements can be simplified to \\d.",
123 | ...replaceElement(node, replacement),
124 | ...reportElements(hits.map(i => elements[i])), // override report range
125 | });
126 | return;
127 | }
128 | }
129 | },
130 | });
131 | });
132 | },
133 | } as CleanRegexRule;
134 |
--------------------------------------------------------------------------------
/lib/rules/prefer-predefined-quantifiers.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { getQuantifierRaw } from "../ast-util";
3 |
4 | const predefined: { min: number; max: number; raw: string }[] = [
5 | { min: 0, max: Infinity, raw: "*" },
6 | { min: 1, max: Infinity, raw: "+" },
7 | { min: 0, max: 1, raw: "?" },
8 | ];
9 |
10 | export default {
11 | meta: {
12 | type: "suggestion",
13 | docs: {
14 | description: "Prefer predefined quantifiers (+*?) instead of their more verbose form.",
15 | url: getDocUrl(/* #GENERATED */ "prefer-predefined-quantifiers"),
16 | },
17 | fixable: "code",
18 | },
19 |
20 | create(context) {
21 | return createRuleListener(({ visitAST, replaceQuantifier }) => {
22 | visitAST({
23 | onQuantifierEnter(node) {
24 | let currentRaw = getQuantifierRaw(node);
25 | if (!node.greedy) {
26 | currentRaw = currentRaw.substr(0, currentRaw.length - 1);
27 | }
28 |
29 | for (const { min, max, raw } of predefined) {
30 | if (node.min === min && node.max === max && currentRaw !== raw) {
31 | context.report({
32 | message: `Use the predefined quantifier ${raw} instead.`,
33 | ...replaceQuantifier(node, raw + (node.greedy ? "" : "?")),
34 | });
35 | }
36 | }
37 | },
38 | });
39 | });
40 | },
41 | } as CleanRegexRule;
42 |
--------------------------------------------------------------------------------
/lib/rules/simple-constant-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 | import { getQuantifierRaw, quantToString } from "../ast-util";
3 |
4 | export default {
5 | meta: {
6 | type: "suggestion",
7 | docs: {
8 | description: "Prefer simple constant quantifiers over the range form.",
9 | url: getDocUrl(/* #GENERATED */ "simple-constant-quantifier"),
10 | },
11 | fixable: "code",
12 | },
13 |
14 | create(context) {
15 | return createRuleListener(({ visitAST, replaceQuantifier }) => {
16 | visitAST({
17 | onQuantifierEnter(node) {
18 | if (node.min !== node.max || node.min < 2) {
19 | // we let no-unnecessary-quantifier handle the {1} case and no-zero-quantifier will handle {0}
20 | return;
21 | }
22 |
23 | const currentRaw = getQuantifierRaw(node);
24 | const simpleRaw = quantToString(node);
25 |
26 | if (simpleRaw !== currentRaw) {
27 | context.report({
28 | message: `This constant quantifier can be simplified to "${simpleRaw}".`,
29 | ...replaceQuantifier(node, simpleRaw),
30 | });
31 | }
32 | },
33 | });
34 | });
35 | },
36 | } as CleanRegexRule;
37 |
--------------------------------------------------------------------------------
/lib/rules/sort-flags.ts:
--------------------------------------------------------------------------------
1 | import { CleanRegexRule, createRuleListener, getDocUrl } from "../rules-util";
2 |
3 | export default {
4 | meta: {
5 | type: "suggestion",
6 | docs: {
7 | description: "Requires the regex flags to be sorted.",
8 | url: getDocUrl(/* #GENERATED */ "sort-flags"),
9 | },
10 | fixable: "code",
11 | },
12 |
13 | create(context) {
14 | const order = "gimsuy";
15 |
16 | /**
17 | * A compare function for regex flags.
18 | */
19 | const compareFn = (a: string, b: string): number => {
20 | const aIndex = order.indexOf(a);
21 | const bIndex = order.indexOf(b);
22 |
23 | if (aIndex === -1) {
24 | throw new Error(`Unknown flag ${a}.`);
25 | }
26 | if (bIndex === -1) {
27 | throw new Error(`Unknown flag ${b}.`);
28 | }
29 |
30 | return aIndex - bIndex;
31 | };
32 |
33 | return createRuleListener(({ flags, replaceFlags, reportFlags }) => {
34 | try {
35 | const sortedFlags = flags.raw.split("").sort(compareFn).join("");
36 |
37 | if (flags.raw !== sortedFlags) {
38 | context.report({
39 | message: `The flags ${flags.raw} should in the order ${sortedFlags}.`,
40 | ...replaceFlags(sortedFlags),
41 | });
42 | }
43 | } catch (e) {
44 | context.report({
45 | message: e.message,
46 | ...reportFlags(),
47 | });
48 | }
49 | });
50 | },
51 | } as CleanRegexRule;
52 |
--------------------------------------------------------------------------------
/lib/util.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Character,
3 | CharacterClassElement,
4 | EscapeCharacterSet,
5 | UnicodePropertyCharacterSet,
6 | CharacterClassRange,
7 | AnyCharacterSet,
8 | } from "regexpp/ast";
9 |
10 | /**
11 | * Throws an error when invoked.
12 | */
13 | export function assertNever(value: never): never {
14 | throw new Error(`This part of the code should never be reached but ${value} made it through.`);
15 | }
16 |
17 | export function findIndex(arr: readonly T[], condFn: (item: T, index: number) => boolean): number {
18 | return arr.findIndex(condFn);
19 | }
20 | export function findLastIndex(arr: readonly T[], condFn: (item: T, index: number) => boolean): number {
21 | for (let i = arr.length - 1; i >= 0; i--) {
22 | if (condFn(arr[i], i)) {
23 | return i;
24 | }
25 | }
26 | return -1;
27 | }
28 |
29 | type SimpleImpl = Omit;
30 | export type Simple = T extends Character
31 | ? SimpleImpl
32 | : never | T extends UnicodePropertyCharacterSet
33 | ? SimpleImpl
34 | : never | T extends AnyCharacterSet
35 | ? SimpleImpl
36 | : never | T extends EscapeCharacterSet
37 | ? SimpleImpl
38 | : never | T extends CharacterClassRange
39 | ? {
40 | type: "CharacterClassRange";
41 | min: Simple;
42 | max: Simple;
43 | raw: string;
44 | }
45 | : never;
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-clean-regex",
3 | "version": "0.5.2",
4 | "description": "An ESLint plugin for writing better regular expressions.",
5 | "keywords": [
6 | "eslint",
7 | "eslint-plugin",
8 | "regex",
9 | "regexp"
10 | ],
11 | "author": "Michael Schmidt",
12 | "homepage": "https://github.com/RunDevelopment/eslint-plugin-clean-regex#readme",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/RunDevelopment/eslint-plugin-clean-regex.git"
16 | },
17 | "main": "dist/index.js",
18 | "scripts": {
19 | "build": "rimraf dist/** && gulp updateSourceFile && tsc && npm run gen-doc",
20 | "check": "npx eslint --ext .ts lib/** tests/** gulpfile.js && npm run build",
21 | "gen-doc": "gulp doc && npm run format-doc",
22 | "format-doc": "prettier --write --print-width 80 --tab-width 4 --prose-wrap always docs/rules/*.md CONTRIBUTING.md",
23 | "format": "prettier --write lib/** tests/** gulpfile.js",
24 | "prepublishOnly": "npm run check",
25 | "test": "mocha -r ts-node/register 'tests/**/*.ts'"
26 | },
27 | "dependencies": {
28 | "refa": "^0.7.1",
29 | "regexpp": "^3.1.0"
30 | },
31 | "devDependencies": {
32 | "@types/chai": "^4.2.11",
33 | "@types/eslint": "^6.8.0",
34 | "@types/gulp": "^4.0.6",
35 | "@types/mocha": "^7.0.2",
36 | "@types/node": "^13.13.4",
37 | "@typescript-eslint/eslint-plugin": "^4.1.0",
38 | "@typescript-eslint/parser": "^4.1.0",
39 | "chai": "^4.2.0",
40 | "eslint": "^6.8.0",
41 | "eslint-config-prettier": "^6.11.0",
42 | "eslint-plugin-prettier": "^3.1.4",
43 | "gulp": "^4.0.2",
44 | "husky": "^4.3.0",
45 | "lint-staged": "^10.3.0",
46 | "mocha": "^7.1.2",
47 | "prettier": "^2.0.5",
48 | "rimraf": "^3.0.2",
49 | "ts-node": "^9.0.0",
50 | "typescript": "^3.9.7"
51 | },
52 | "engines": {
53 | "node": ">=12.0.0"
54 | },
55 | "license": "MIT",
56 | "husky": {
57 | "hooks": {
58 | "pre-commit": "lint-staged"
59 | }
60 | },
61 | "lint-staged": {
62 | "*.ts": "eslint --cache --fix"
63 | },
64 | "files": [
65 | "dist/**/*.js",
66 | "MIGRATION.md"
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/tests/lib/config.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "chai";
2 | import { rules, configs } from "../../lib";
3 |
4 | describe("Recommended config", function () {
5 | it("should contain all rules", function () {
6 | Object.keys(rules)
7 | .map(r => "clean-regex/" + r)
8 | .forEach(r => {
9 | assert.property(configs.recommended.rules, r);
10 | });
11 | });
12 |
13 | it("should contain no other rules", function () {
14 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
15 | const unknown = Object.keys(configs.recommended.rules!).filter(r => {
16 | if (!/^clean-regex\//.test(r)) {
17 | // not a clean-regex rule
18 | return false;
19 | }
20 | return !(r.substr("clean-regex/".length) in rules);
21 | });
22 |
23 | if (unknown.length) {
24 | assert.fail(`Unknown rules: ${unknown.join(", ")}`);
25 | }
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/tests/lib/fixable.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { assert } from "chai";
3 | import { rules } from "../../lib";
4 |
5 | // a test to verify that all rules are mark as fixable iff they use fixers
6 |
7 | describe("Rules", function () {
8 | const fixerFunctions = /\b(?:replace|remove)(?:Element|Quantifier|Flags|Literal)\b/;
9 |
10 | const rulesDir = `${__dirname}/../../lib/rules`;
11 | const ruleNames = Object.keys(rules);
12 |
13 | for (const ruleName of ruleNames) {
14 | // is fixable?
15 | // eslint-disable-next-line @typescript-eslint/no-var-requires
16 | const fixable = require("../../lib/rules/" + ruleName).default.meta.fixable === "code";
17 | const sourceCode = fs.readFileSync(`${rulesDir}/${ruleName}.ts`, "utf8");
18 |
19 | describe(ruleName, function () {
20 | it(`should${fixable ? "" : " not"} use fixers`, function () {
21 | if (fixable) {
22 | assert.match(sourceCode, fixerFunctions);
23 | } else {
24 | assert.notMatch(sourceCode, fixerFunctions);
25 | }
26 | });
27 | });
28 | }
29 | });
30 |
--------------------------------------------------------------------------------
/tests/lib/rules/confusing-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | const errors = [{ message: /[\s\S]+/ }];
4 |
5 | testRule(__filename, undefined, {
6 | valid: [
7 | String(/a+/),
8 | String(/a?/),
9 | String(/(a|b?)*/),
10 | String(/(a?){0,3}/),
11 | String(/(a|\b)+/),
12 | ],
13 | invalid: [
14 | { code: String(/(a?){5}/), errors },
15 | { code: String(/(?:a?b*|c+){4}/), errors },
16 | ]
17 | });
18 |
--------------------------------------------------------------------------------
/tests/lib/rules/consistent-match-all-characters.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | const errors = [{ message: /[\s\S]+/ }];
4 |
5 | testRule(__filename, undefined, {
6 | valid: [
7 | String(/a+/),
8 | String(/[a-f\w\x00-\xFF]/),
9 |
10 | // default config is { mode: "dot-if-dotAll", charClass: "[\\s\\S]" }
11 | String(/[\s\S]/),
12 | String(/./s),
13 |
14 | { code: String(/[\s\S]/), options: [{ mode: "char-class", charClass: "[\\s\\S]" }] },
15 | { code: String(/[\d\D]/), options: [{ mode: "char-class", charClass: "[\\d\\D]" }] },
16 | { code: String(/[^]/), options: [{ mode: "char-class", charClass: "[^]" }] },
17 |
18 | { code: String(/[^]/), options: [{ mode: "dot-if-dotAll", charClass: "[^]" }] },
19 | { code: String(/./s), options: [{ mode: "dot-if-dotAll", charClass: "[^]" }] },
20 | { code: String(/./su), options: [{ mode: "dot-if-dotAll", charClass: "[^]" }] },
21 |
22 | { code: String(/./s), options: [{ mode: "dot", charClass: "[^]" }] },
23 | { code: String(/./su), options: [{ mode: "dot", charClass: "[^]" }] },
24 | ],
25 | invalid: [
26 | { code: String(/[^]/), output: String(/[\s\S]/), errors },
27 | { code: String(/[\d\D]/), output: String(/[\s\S]/), errors },
28 | { code: String(/[\w\W]/), output: String(/[\s\S]/), errors },
29 | { code: String(/[\w\D]/), output: String(/[\s\S]/), errors },
30 | { code: String(/[\0-\uFFFF]/), output: String(/[\s\S]/), errors },
31 | { code: String(/[\0-\u{10FFFF}]/u), output: String(/[\s\S]/u), errors },
32 |
33 | {
34 | code: String(/[^]/),
35 | output: String(/[\s\S]/),
36 | options: [{ mode: "char-class", charClass: "[\\s\\S]" }],
37 | errors
38 | },
39 | {
40 | code: String(/[\s\S]/),
41 | output: String(/[\d\D]/),
42 | options: [{ mode: "char-class", charClass: "[\\d\\D]" }],
43 | errors
44 | },
45 | {
46 | code: String(/./s),
47 | output: String(/[\d\D]/),
48 | options: [{ mode: "char-class", charClass: "[\\d\\D]" }],
49 | errors
50 | },
51 | {
52 | code: String(/./msu),
53 | output: String(/[\d\D]/mu),
54 | options: [{ mode: "char-class", charClass: "[\\d\\D]" }],
55 | errors
56 | },
57 |
58 | {
59 | code: String(/[\d\D]/),
60 | output: String(/[^]/),
61 | options: [{ mode: "dot-if-dotAll", charClass: "[^]" }],
62 | errors
63 | },
64 | {
65 | code: String(/[\d\D]/s),
66 | output: String(/./s),
67 | options: [{ mode: "dot-if-dotAll", charClass: "[^]" }],
68 | errors
69 | },
70 |
71 | {
72 | code: String(/[\d\D]/),
73 | output: String(/./s),
74 | options: [{ mode: "dot", charClass: "[^]" }],
75 | errors
76 | },
77 | {
78 | code: String(/[\d\D]/mu),
79 | output: String(/./msu),
80 | options: [{ mode: "dot", charClass: "[^]" }],
81 | errors
82 | },
83 | {
84 | code: String(/[\d\D]/s),
85 | output: String(/./s),
86 | options: [{ mode: "dot", charClass: "[^]" }],
87 | errors
88 | },
89 | ]
90 | });
91 |
--------------------------------------------------------------------------------
/tests/lib/rules/disjoint-alternatives.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/a+|b+/),
6 | String(/a?|a{2,}/),
7 | String(/a*|b*/),
8 | ],
9 | invalid: [
10 | {
11 | code: String(/b+(?:\w+|[+-]?\d+)/),
12 | errors: [{ message: "This alternative is not disjoint with `\\w+`. The shared language is /\\d+/i." }]
13 | },
14 | {
15 | code: String(/FOO|foo(?:bar)?/i),
16 | errors: [{ message: "This alternative is a superset of `FOO`." }]
17 | },
18 | {
19 | code: String(/foo(?:bar)?|foo/),
20 | errors: [{ message: "This alternative is a subset of `foo(?:bar)?` and can be removed." }]
21 | },
22 | {
23 | code: String(/(?=[\t ]+[\S]{1,}|[\t ]+['"][\S]|[\t ]+$|$)/),
24 | errors: [{ message: "This alternative is a subset of `[\\t ]+[\\S]{1,}` and can be removed." }]
25 | },
26 | {
27 | code: String(/\w+(?:\s+(?:\S+|"[^"]*"))*/),
28 | errors: [{ message: "This alternative is not disjoint with `\\S+`. The shared language is /\"[^\\s\"]*\"/i. This alternative is likely to cause exponential backtracking." }]
29 | },
30 | {
31 | code: String(/\b(?:\d|foo|\w+)\b/),
32 | errors: [{ message: "This alternative is a superset of `\\d` | `foo`." }]
33 | },
34 | {
35 | code: String(/\d|[a-z]|_|\w/i),
36 | errors: [{ message: "This alternative is the same as `\\d` | `[a-z]` | `_` and can be removed." }]
37 | },
38 | ]
39 | });
40 |
--------------------------------------------------------------------------------
/tests/lib/rules/identity-escape.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/foo\.bar12/),
6 | String(/foo\.bar12/u),
7 | String(/\( \) \[ \] \{ \} \* \+ \? \/ \\ \| \^ \$/),
8 | String(/\( \) \[ \] \{ \} \* \+ \? \/ \\ \| \^ \$/u),
9 |
10 | String(/[\^ab]/),
11 | String(/[\^ab]/u),
12 | String(/[ab\-c]/),
13 | String(/[ab\-c]/u),
14 | String(/[a\--b]/),
15 | String(/[a\--b]/u),
16 |
17 | {
18 | code: String(/]{} \} [\/{}]/),
19 | options: [{
20 | rules: [
21 | {
22 | escape: "allow",
23 | characters: "[^]"
24 | }
25 | ]
26 | }]
27 | }
28 | ],
29 | invalid: [
30 | // test default rules
31 |
32 | {
33 | code: String(/\a \b \c \d \e \f \g \h \i \j \k \l \m \n \o \p \q \r \s \t \u \v \w \x \y \z \A \B \C \D \E \F \G \H \I \J \K \L \M \N \O \P \Q \R \S \T \U \V \W \X \Y \Z/),
34 | output: String(/a \b \c \d e \f g h i j k l m \n o p q \r \s \t u \v \w x y z A \B C \D E F G H I J K L M N O P Q R \S T U V \W X Y Z/),
35 | errors: 38
36 | },
37 | {
38 | code: String(/[\a \b \c \d \e \f \g \h \i \j \k \l \m \n \o \p \q \r \s \t \u \v \w \x \y \z \A \B \C \D \E \F \G \H \I \J \K \L \M \N \O \P \Q \R \S \T \U \V \W \X \Y \Z]/),
39 | output: String(/[a \b \c \d e \f g h i j k l m \n o p q \r \s \t u \v \w x y z A B C \D E F G H I J K L M N O P Q R \S T U V \W X Y Z]/),
40 | errors: 39
41 | },
42 |
43 | {
44 | code: String(/[\\ \( \) \{ \} \* \+ \? \/ \| \^ \$]/),
45 | output: String(/[\\ ( ) { } * + ? / | ^ $]/),
46 | errors: 11
47 | },
48 | {
49 | code: String(/[\\ \( \) \{ \} \* \+ \? \/ \| \^ \$]/u),
50 | output: String(/[\\ ( ) { } * + ? / | ^ $]/u),
51 | errors: 11
52 | },
53 | {
54 | code: String(/[\-abc\-]/),
55 | output: String(/[-abc-]/),
56 | errors: 2
57 | },
58 | {
59 | code: String(/[\-abc\-]/u),
60 | output: String(/[-abc-]/u),
61 | errors: 2
62 | },
63 |
64 | {
65 | code: String(/{ } ] [ [ \] ]/),
66 | output: String(/\{ \} \] [ \[ \] ]/),
67 | errors: 4
68 | },
69 | ]
70 | });
71 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-constant-capturing-group.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/(a*)/),
6 | String(/(a{2,3})/),
7 | String(/(a|b)/),
8 | String(/(a)/i),
9 | String(/(a|A)/),
10 | String(/(a)/),
11 | { code: String(/(a)/), options: [{ ignoreNonEmpty: true }] },
12 | ],
13 | invalid: [
14 | { code: String(/()/), errors: [{ message: "Empty capturing group" }] },
15 | { code: String(/()/), options: [{ ignoreNonEmpty: false }], errors: [{ message: "Empty capturing group" }] },
16 | { code: String(/()/), options: [{ ignoreNonEmpty: true }], errors: [{ message: "Empty capturing group" }] },
17 |
18 | { code: String(/(\b)/), errors: [{ message: "The capturing group `(\\b)` can only capture the empty string." }] },
19 | { code: String(/(\b)/), options: [{ ignoreNonEmpty: false }], errors: [{ message: "The capturing group `(\\b)` can only capture the empty string." }] },
20 | { code: String(/(\b)/), options: [{ ignoreNonEmpty: true }], errors: [{ message: "The capturing group `(\\b)` can only capture the empty string." }] },
21 |
22 | { code: String(/(^\b|(?!b))/), errors: [{ message: "The capturing group `(^\\b|(?!b))` can only capture the empty string." }] },
23 |
24 | { code: String(/(a|a|a)/), options: [{ ignoreNonEmpty: false }], errors: [{ message: "The capturing group `(a|a|a)` can only capture one word which is \"a\"." }] },
25 |
26 | { code: String(/(,)/i), options: [{ ignoreNonEmpty: false }], errors: [{ message: "The capturing group `(,)` can only capture one word which is \",\"." }] },
27 | { code: String(/(\b(?:ab){3}$)/), options: [{ ignoreNonEmpty: false }], errors: [{ message: "The capturing group `(\\b(?:ab){3}$)` can only capture one word which is \"ababab\"." }] },
28 | ]
29 | });
30 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-empty-alternative.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/()|(?:)|(?=)/),
6 | String(/(?:)/),
7 | String(/a*|b+/),
8 | ],
9 | invalid: [
10 | { code: String(/|||||/), errors: 1 },
11 | { code: String(/(a+|b+|)/), errors: 1 },
12 | { code: String(/(?:\|\|||\|)/), errors: 1 },
13 | ]
14 | });
15 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-empty-backreference.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | const zeroLength = [{ message: "The referenced capturing group can only match the empty string." }];
4 | const notReachable = [{ message: "The backreference is not reachable from the referenced capturing group without resetting the captured string." }];
5 |
6 | testRule(__filename, undefined, {
7 | valid: [
8 | String(/(a)\1/),
9 | String(/(a)(b|\1)/),
10 | String(/(a)?(b|\1)?/),
11 | String(/(?=(.+))\1/),
12 | String(/(?\w)\k/),
13 |
14 | // right-to-left matching
15 | String(/\w+(?<=a\1a(b))/),
16 | ],
17 | invalid: [
18 | // zero-length group
19 | { code: String(/()\1/), errors: zeroLength },
20 | { code: String(/(\b)\1/), errors: zeroLength },
21 | { code: String(/(\b|(?=123))\1/), errors: zeroLength },
22 |
23 | // not reachable
24 | { code: String(/(a\1)+/), errors: notReachable },
25 | { code: String(/(a(?=\1))/), errors: notReachable },
26 | { code: String(/(?a\k)/), errors: notReachable },
27 | { code: String(/\1(a)/), errors: notReachable },
28 |
29 | { code: String(/(a)|\1/), errors: notReachable },
30 | { code: String(/(?:(a)|\1)b/), errors: notReachable },
31 |
32 | // right-to-left matching
33 | { code: String(/(?<=(a)\1)/), errors: notReachable },
34 | ]
35 | });
36 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-empty-lookaround.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | const errors = [{ message: /^The (?:lookahead|lookbehind) [\s\S]+ is non-functional as it matches the empty string. It will always trivially (?:accept|reject).$/ }];
4 |
5 | testRule(__filename, undefined, {
6 | valid: [
7 | String(/(?=foo)/),
8 | String(/(?!foo)/),
9 | String(/(?<=foo)/),
10 | String(/(?a*)[\k]/),
21 | ],
22 | invalid: [
23 | // character sets
24 | { code: String(/[\w]/), output: String(/\w/), errors },
25 | { code: String(/[\W]/), output: String(/\W/), errors },
26 | { code: String(/[\s]/), output: String(/\s/), errors },
27 | { code: String(/[\S]/), output: String(/\S/), errors },
28 | { code: String(/[\d]/), output: String(/\d/), errors },
29 | { code: String(/[\p{Script_Extensions=Greek}]/u), output: String(/\p{Script_Extensions=Greek}/u), errors },
30 | { code: String(/[^\s]/), output: String(/\S/), errors },
31 | { code: String(/[^\S]/), output: String(/\s/), errors },
32 | { code: String(/[^\p{Script_Extensions=Greek}]/u), output: String(/\P{Script_Extensions=Greek}/u), errors },
33 |
34 | // special characters
35 | { code: String(/[.]/), output: String(/\./), errors },
36 | { code: String(/[*]/), output: String(/\*/), errors },
37 | { code: String(/[+]/), output: String(/\+/), errors },
38 | { code: String(/[?]/), output: String(/\?/), errors },
39 | { code: String(/[{]/), output: String(/\{/), errors },
40 | { code: String(/[}]/), output: String(/\}/), errors },
41 | { code: String(/[(]/), output: String(/\(/), errors },
42 | { code: String(/[)]/), output: String(/\)/), errors },
43 | { code: String(/[[]/), output: String(/\[/), errors },
44 | { code: String(/[/]/), output: String(/\//), errors },
45 | { code: String(/[$]/), output: String(/\$/), errors },
46 |
47 | // backspace
48 | { code: String(/[\b]/), output: String(/\x08/), errors },
49 |
50 | // escape sequences
51 | { code: String(/[\0]/), output: String(/\0/), errors },
52 | { code: String(/[\x02]/), output: String(/\x02/), errors },
53 | { code: String(/[\uFFFF]/), output: String(/\uFFFF/), errors },
54 | { code: String(/[\u{10FFFF}]/u), output: String(/\u{10FFFF}/u), errors },
55 | { code: String(/[\cI]/), output: String(/\cI/), errors },
56 | { code: String(/[\f]/), output: String(/\f/), errors },
57 | { code: String(/[\n]/), output: String(/\n/), errors },
58 | { code: String(/[\r]/), output: String(/\r/), errors },
59 | { code: String(/[\t]/), output: String(/\t/), errors },
60 | { code: String(/[\v]/), output: String(/\v/), errors },
61 |
62 | // literals
63 | { code: String(/[a]/), output: String(/a/), errors },
64 | { code: String(/[a]/i), output: String(/a/i), errors },
65 | { code: String(/[H]/), output: String(/H/), errors },
66 | { code: String(/[%]/), output: String(/%/), errors },
67 |
68 | // escaped literals
69 | { code: String(/[\a]/), output: String(/\a/), errors },
70 | { code: String(/[\g]/), output: String(/\g/), errors },
71 | { code: String(/[\H]/), output: String(/\H/), errors },
72 | { code: String(/[\"]/), output: String(/\"/), errors },
73 | { code: String(/[\\]/), output: String(/\\/), errors },
74 | { code: String(/[\-]/), output: String(/\-/), errors },
75 | { code: String(/[\]]/), output: String(/\]/), errors },
76 | { code: String(/[\^]/), output: String(/\^/), errors },
77 | { code: String(/[\/]/), output: String(/\//), errors },
78 | { code: String(/[\%]/), output: String(/\%/), errors },
79 |
80 | ]
81 | });
82 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-unnecessary-flag.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | // i
6 | String(/foo/i),
7 | String(/BAR/i),
8 | String(/\x41/i),
9 | String(/[a-zA-Z]/i), // in that case you should use the i flag instead of removing it
10 |
11 | // m
12 | String(/^foo/m),
13 | String(/foo$/m),
14 | String(/^foo$/m),
15 |
16 | // s
17 | String(/./s),
18 |
19 | // ignore
20 | { code: String(/\w/i), options: [{ ignore: ["i"] }] },
21 | { code: String(/\w/m), options: [{ ignore: ["m"] }] },
22 | { code: String(/\w/s), options: [{ ignore: ["s"] }] },
23 | ],
24 | invalid: [
25 | // i
26 | { code: String(/\w/i), output: String(/\w/), errors: [{ message: "The i flags is unnecessary because the pattern does not contain case-variant characters." }] },
27 |
28 | // m
29 | { code: String(/\w/m), output: String(/\w/), errors: [{ message: "The m flags is unnecessary because the pattern does not contain start (^) or end ($) assertions." }] },
30 |
31 | // s
32 | { code: String(/\w/s), output: String(/\w/), errors: [{ message: "The s flags is unnecessary because the pattern does not contain dots (.)." }] },
33 |
34 | // all flags
35 | {
36 | code: String(/\w/ims),
37 | output: String(/\w/),
38 | errors: [
39 | { message: "The flags ims are unnecessary because the pattern [i] does not contain case-variant characters, [m] does not contain start (^) or end ($) assertions, [s] does not contain dots (.)" },
40 | ]
41 | },
42 | ]
43 | });
44 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-unnecessary-group.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | const errors = [{ message: "Unnecessary non-capturing group." }];
4 |
5 | testRule(__filename, undefined, {
6 | valid: [
7 | String(/(?:a{2})+/),
8 | String(/{(?:2)}/),
9 | String(/{(?:2,)}/),
10 | String(/{(?:2,5)}/),
11 | String(/{2,(?:5)}/),
12 | String(/a{(?:5})/),
13 | String(/\u{(?:41)}/),
14 | String(/(.)\1(?:2\s)/),
15 | String(/\0(?:2)/),
16 | String(/\x4(?:1)*/),
17 | String(/\x4(?:1)/),
18 | String(/\x(?:4)1/),
19 | String(/\x(?:41\w+)/),
20 | String(/\u004(?:1)/),
21 | String(/\u00(?:4)1/),
22 | String(/\u0(?:0)41/),
23 | String(/\u(?:0)041/),
24 | String(/\c(?:A)/),
25 | String(/(?:)/),
26 | String(/(?:a|b)c/),
27 |
28 | {
29 | code: String(/(?:foo)/),
30 | options: [{ allowTop: true }]
31 | }
32 | ],
33 | invalid: [
34 | { code: String(/(?:)a/), output: String(/a/), errors },
35 | { code: String(/(?:a)/), output: String(/a/), errors },
36 | { code: String(/(?:a)+/), output: String(/a+/), errors },
37 | { code: String(/(?:\w)/), output: String(/\w/), errors },
38 | { code: String(/(?:[abc])*/), output: String(/[abc]*/), errors },
39 | { code: String(/foo(?:[abc]*)bar/), output: String(/foo[abc]*bar/), errors },
40 | { code: String(/foo(?:bar)/), output: String(/foobar/), errors },
41 | { code: String(/(?:a|b)/), output: String(/a|b/), errors },
42 | { code: String(/a|(?:b|c)/), output: String(/a|b|c/), errors },
43 |
44 | {
45 | code: String(/(?:foo)bar/),
46 | output: String(/foobar/),
47 | options: [{ allowTop: true }],
48 | errors
49 | }
50 | ]
51 | });
52 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-unnecessary-lazy.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | const errors = [{ message: /[\s\S]*/ }];
4 |
5 | testRule(__filename, undefined, {
6 | valid: [
7 | String(/a+?b*/),
8 | String(/[\s\S]+?bar/),
9 | String(/a??a?/),
10 | ],
11 | invalid: [
12 | { code: String(/a{3}?/), output: String(/a{3}/), errors },
13 | { code: String(/a{3,3}?/), output: String(/a{3,3}/), errors },
14 | { code: String(/a{0}?/), output: String(/a{0}/), errors },
15 |
16 | { code: String(/a+?b+/), output: String(/a+b+/), errors },
17 | ]
18 | });
19 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-unnecessary-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/a*/),
6 | String(/(?:a)?/),
7 | String(/(?:\b|a)?/),
8 | String(/(?:\b)*/),
9 | String(/(?:\b|(?!a))*/),
10 | String(/(?:\b|(?!))*/),
11 | String(/#[\da-z]+|#(?:-|([+/\\*~<>=@%|&?!])\1?)|#(?=\()/),
12 | ],
13 | invalid: [
14 | // trivial
15 | { code: String(/a{1}/), output: String(/a/), errors: [{ message: "Unnecessary quantifier." }] },
16 | { code: String(/a{1,1}/), output: String(/a/), errors: [{ message: "Unnecessary quantifier." }] },
17 |
18 | // empty quantified element
19 | { code: String(/(?:)+/), errors: [{ message: "The quantified element is empty, so the quantifier can be removed." }] },
20 | { code: String(/(?:|(?:)){5,9}/), errors: [{ message: "The quantified element is empty, so the quantifier can be removed." }] },
21 | { code: String(/(?:|()()())*/), errors: [{ message: "The quantified element is empty, so the quantifier can be removed." }] },
22 |
23 | // unnecessary optional quantifier (?) because the quantified element is potentially empty
24 | { code: String(/(?:a+b*|c*)?/), errors: [{ message: "The optional quantifier can be removed because the quantified element can match the empty string." }] },
25 | { code: String(/(?:a|b?c?d?e?f?)?/), errors: [{ message: "The optional quantifier can be removed because the quantified element can match the empty string." }] },
26 | { code: String(/(?:a|(?=))?/), errors: [{ message: "The optional quantifier can be removed because the quantified element can match the empty string." }] },
27 |
28 | // quantified elements which do not consume characters
29 | { code: String(/(?:\b)+/), errors: [{ message: "The quantified element does not consume characters, so the quantifier (minimum > 0) can be removed." }] },
30 | { code: String(/(?:\b){5,100}/), errors: [{ message: "The quantified element does not consume characters, so the quantifier (minimum > 0) can be removed." }] },
31 | { code: String(/(?:\b|(?!a))+/), errors: [{ message: "The quantified element does not consume characters, so the quantifier (minimum > 0) can be removed." }] },
32 | { code: String(/(?:\b|(?!)){6}/), errors: [{ message: "The quantified element does not consume characters, so the quantifier (minimum > 0) can be removed." }] },
33 |
34 | ]
35 | });
36 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-zero-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | const errors = [{ message: /[\s\S]+/ }];
4 |
5 | testRule(__filename, undefined, {
6 | valid: [
7 | String(/a{0,1}/),
8 | String(/a{0,}/),
9 | ],
10 | invalid: [
11 | { code: String(/a{0}/), output: String(/(?:)/), errors },
12 | { code: String(/a{0}b/), output: String(/b/), errors },
13 | { code: String(/a{0}|b/), output: String(/|b/), errors },
14 | { code: String(/a{0,0}/), output: String(/(?:)/), errors },
15 | { code: String(/(?:a|b){0,0}/), output: String(/(?:)/), errors },
16 | { code: String(/(?:a+){0}/), output: String(/(?:)/), errors},
17 | { code: String(/(?:\b){0}/), output: String(/(?:)/), errors},
18 |
19 | // keep capturing groups
20 | { code: String(/(a){0}/), output: String(/(a){0}/), errors},
21 | { code: String(/(?:a()){0}/), output: String(/(?:a()){0}/), errors},
22 | ]
23 | });
24 |
--------------------------------------------------------------------------------
/tests/lib/rules/optimal-concatenation-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/\w+\d{4}/),
6 | String(/\w+a/),
7 | String(/\w{3,5}\d{2,4}/),
8 | String(/\w{3,5}\d*/),
9 | String(/a+b+c+d+[abc]+/),
10 | String(/(?:a|::)?\w+/),
11 | String(/aa?/),
12 | ],
13 | invalid: [
14 | { code: String(/a\d*\d*a/), output: String(/a\d*a/), errors: 1 },
15 | { code: String(/\w+\d+/), output: String(/\w+\d/), errors: 1 },
16 | { code: String(/\w+\d?/), output: String(/\w+/), errors: 1 },
17 | { code: String(/a+\w+/), output: String(/a\w+/), errors: 1 },
18 | { code: String(/\w+\d*/), output: String(/\w+/), errors: 1 },
19 | { code: String(/(\d*\w+)/), output: String(/(\w+)/), errors: 1 },
20 | { code: String(/;+.*/), output: String(/;.*/), errors: 1 },
21 | { code: String(/a+(?:a|bb)+/), output: String(/a(?:a|bb)+/), errors: 1 },
22 | { code: String(/\w+(?:a|b)+/), output: String(/\w+(?:a|b)/), errors: 1 },
23 | { code: String(/\d{3,5}\w*/), output: String(/\d{3}\w*/), errors: 1 },
24 |
25 | { code: String(/\w\w*/), output: String(/\w+/), errors: 1 },
26 | { code: String(/\w*\w/), output: String(/\w+/), errors: 1 },
27 | { code: String(/\w?\w/), output: String(/\w{1,2}/), errors: 1 },
28 | { code: String(/\w+\w/), output: String(/\w{2,}/), errors: 1 },
29 | { code: String(/[ab]*(?:a|b)/), output: String(/[ab]+/), errors: 1 },
30 |
31 | {
32 | code: String(/\w+\d*/),
33 | output: String(/\w+\d*/),
34 | options: [{ fixable: false }],
35 | errors: 1
36 | },
37 |
38 | // careful with capturing groups
39 | { code: String(/\w+(?:(a)|b)*/), output: String(/\w+(?:(a)|b)*/), errors: 1 },
40 | { code: String(/(\d)*\w+/), output: String(/(\d)*\w+/), errors: 1 },
41 | { code: String(/(\d)\d+/), output: String(/(\d)\d+/), errors: 1 },
42 | ]
43 | });
44 |
--------------------------------------------------------------------------------
/tests/lib/rules/optimal-lookaround-quantifier.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/(?=(a*))\w+\1/),
6 | String(/(?<=a{4})/)
7 | ],
8 | invalid: [
9 | {
10 | code: String(/(?=ba*)/),
11 | errors: [{ message: "The quantified expression a* at the end of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround." }]
12 | },
13 | {
14 | code: String(/(?=(?:a|b|abc*))/),
15 | errors: [{ message: "The quantified expression c* at the end of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround." }]
16 | },
17 | {
18 | code: String(/(?=(?:a|b|abc+))/),
19 | errors: [{ message: "The quantified expression c+ at the end of the expression tree should only be matched a constant number of times. The expression can be replaced with c (no quantifier) without affecting the lookaround." }]
20 | },
21 | {
22 | code: String(/(?=(?:a|b|abc{4,9}))/),
23 | errors: [{ message: "The quantified expression c{4,9} at the end of the expression tree should only be matched a constant number of times. The expression can be replaced with c{4} without affecting the lookaround." }]
24 | },
25 | {
26 | code: String(/(?<=[a-c]*)/),
27 | errors: [{ message: "The quantified expression [a-c]* at the start of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround." }]
28 | },
29 | {
30 | code: String(/(?<=(c)*ab)/),
31 | errors: [{ message: "The quantified expression (c)* at the start of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround." }]
32 | },
33 | ]
34 | });
35 |
--------------------------------------------------------------------------------
/tests/lib/rules/optimized-character-class.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/[]/),
6 | String(/[^]/),
7 | String(/[a]/),
8 | String(/[abc]/),
9 | String(/[\s\w]/),
10 | String(/[a-fA-F]/),
11 | String(/[a-c]/),
12 | ],
13 | invalid: [
14 | { code: String(/[\wa]/), output: String(/[\w]/), errors: [{ message: "`a` (\\x61) is already included by `\\w` ([0-9A-Za-z_])." }] },
15 | { code: String(/[a\w]/), output: String(/[\w]/), errors: [{ message: "`a` (\\x61) is already included by `\\w` ([0-9A-Za-z_])." }] },
16 | { code: String(/[a-zh]/), output: String(/[a-z]/), errors: [{ message: "`h` (\\x68) is already included by `a-z` (\\x61-\\x7a)." }] },
17 | { code: String(/[\x41A]/), output: String(/[\x41]/), errors: [{ message: "`A` (\\x41) is already included by `\\x41` (\\x41)." }] },
18 | { code: String(/[hH]/i), output: String(/[h]/i), errors: [{ message: "`H` (\\x48) is already included by `h` (\\x68)." }] },
19 | { code: String(/[a-zH]/i), output: String(/[a-z]/i), errors: [{ message: "`H` (\\x48) is already included by `a-z` (\\x61-\\x7a)." }] },
20 |
21 | { code: String(/[a-a]/), output: String(/[a]/), errors: [{ message: "`a-a` (\\x61-\\x61) contains only a single value." }] },
22 | { code: String(/[a-b]/), output: String(/[ab]/), errors: [{ message: "`a-b` (\\x61-\\x62) contains only its two ends." }] },
23 | { code: String(/[---]/), output: String(/[\-]/), errors: [{ message: "`---` (\\x2d-\\x2d) contains only a single value." }] },
24 |
25 | { code: String(/[a-za-f]/), output: String(/[a-z]/), errors: [{ message: "`a-f` (\\x61-\\x66) is already included by `a-z` (\\x61-\\x7a)." }] },
26 | { code: String(/[a-fa-f]/), output: String(/[a-f]/), errors: [{ message: "`a-f` (\\x61-\\x66) is already included by `a-f` (\\x61-\\x66)." }] },
27 | { code: String(/[\wa-f]/), output: String(/[\w]/), errors: [{ message: "`a-f` (\\x61-\\x66) is already included by `\\w` ([0-9A-Za-z_])." }] },
28 | { code: String(/[a-fA-F]/i), output: String(/[a-f]/i), errors: [{ message: "`A-F` (\\x41-\\x46) is already included by `a-f` (\\x61-\\x66)." }] },
29 | { code: String(/[0-9\w]/i), output: String(/[\w]/i), errors: [{ message: "`0-9` (\\x30-\\x39) is already included by `\\w` ([0-9A-Za-z_])." }] },
30 | { code: String(/[a-f\D]/i), output: String(/[\D]/i), errors: [{ message: "`a-f` (\\x61-\\x66) is already included by `\\D` ([^0-9])." }] },
31 |
32 | { code: String(/[\w\d]/i), output: String(/[\w]/i), errors: [{ message: "`\\d` ([0-9]) is already included by `\\w` ([0-9A-Za-z_])." }] },
33 | { code: String(/[\s\s]/), output: String(/[\s]/), errors: [{ message: "`\\s` is already included by `\\s`." }] },
34 |
35 | { code: String(/[\s\n]/), output: String(/[\s]/), errors: [{ message: "`\\n` (\\x0a) is already included by `\\s`." }] },
36 |
37 | { code: String(/[\S\d]/), output: String(/[\S]/), errors: [{ message: "`\\d` ([0-9]) is already included by `\\S`." }] },
38 |
39 | { code: String(/[a-cd-fb-e]/), output: String(/[a-cd-f]/), errors: [{ message: "`b-e` (\\x62-\\x65) is already included by some combination of other elements." }] },
40 |
41 | { code: String(/[\w\p{ASCII}]/u), output: String(/[\p{ASCII}]/u), errors: [{ message: "`\\w` ([0-9A-Za-z_]) is already included by `\\p{ASCII}`." }] },
42 | ]
43 | });
44 |
--------------------------------------------------------------------------------
/tests/lib/rules/prefer-character-class.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/(?:a|b)/),
6 | String(/(?:a|b|c\b)/),
7 | String(/(?:[ab]|c\b)/),
8 | String(/(?:[ab]|c\b)/),
9 | String(/(?:[ab]|cd)/),
10 | String(/(?:[ab]|(c))/),
11 | ],
12 | invalid: [
13 | { code: String(/a|b|c/), output: String(/[abc]/), errors: 1 },
14 | { code: String(/]|a|b/), output: String(/[\]ab]/), errors: 1 },
15 | { code: String(/-|a|c/), output: String(/[-ac]/), errors: 1 },
16 | { code: String(/a|-|c/), output: String(/[a\-c]/), errors: 1 },
17 | { code: String(/a|[-]|c/), output: String(/[a\-c]/), errors: 1 },
18 | { code: String(/(?:a|b|c)/), output: String(/[abc]/), errors: 1 },
19 | { code: String(/(a|b|c)/), output: String(/([abc])/), errors: 1 },
20 | { code: String(/(?a|b|c)/), output: String(/(?[abc])/), errors: 1 },
21 | { code: String(/(?:a|b|c|d\b)/), output: String(/(?:[abc]|d\b)/), errors: 1 },
22 | { code: String(/(?:a|b\b|[c]|d)/), output: String(/(?:[acd]|b\b)/), errors: 1 },
23 | { code: String(/(?:a|\w|\s|["'])/), output: String(/[a\w\s"']/), errors: 1 },
24 | { code: String(/(?:[^\s]|b)/), output: String(/[\Sb]/), errors: 1 },
25 | { code: String(/(?:\w|-|\+|\*|\/)+/), output: String(/[\w\-\+\*\/]+/), errors: 1 },
26 | { code: String(/(?=a|b|c)/), output: String(/(?=[abc])/), errors: 1 },
27 | { code: String(/(?!a|b|c)/), output: String(/(?![abc])/), errors: 1 },
28 | { code: String(/(?<=a|b|c)/), output: String(/(?<=[abc])/), errors: 1 },
29 | { code: String(/(?>=?|<=?|>=?|==?|&&?|&=|\^=?|\|\|?|\|=|\?|:/),
52 | output: String(/--?|-=|\+\+?|\+=|!=?|[~\?:]|\*\*?|\*=|\/=?|%=?|<<=?|>>=?|<=?|>=?|==?|&&?|&=|\^=?|\|\|?|\|=/),
53 | errors: [{ message: "This can be replaced with `[~\\?:]|\\*\\*?|\\*=|\\/=?|%=?|<<=?|>>=?|<=?|>=?|==?|&&?|&=|\\^=?|\\|\\|?|\\|=`." }]
54 | },
55 | {
56 | code: String(/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&|\|\|?|\?|\*|\/|~|\^|%/),
57 | output: String(/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&|\|\|?|[\?\*\/~\^%]/),
58 | errors: [{ message: "This can be replaced with `[\\?\\*\\/~\\^%]`." }]
59 | },
60 | ]
61 | });
62 |
--------------------------------------------------------------------------------
/tests/lib/rules/prefer-predefined-assertion.ts:
--------------------------------------------------------------------------------
1 | import { testRule } from "../../test-util";
2 |
3 | testRule(__filename, undefined, {
4 | valid: [
5 | String(/a(?=\W)/),
6 | ],
7 | invalid: [
8 | { code: String(/a(?=\w)/), output: String(/a\B/), errors: 1 },
9 | { code: String(/a(?!\w)/), output: String(/a\b/), errors: 1 },
10 | { code: String(/(?<=\w)a/), output: String(/\Ba/), errors: 1 },
11 | { code: String(/(? void, patterns: readonly RegExp[]) {
48 | describe(name, function () {
49 | for (const regex of patterns) {
50 | it(regex.toString(), function () {
51 | testFn(parser.parsePattern(regex.source), parser.parseFlags(regex.flags));
52 | });
53 | }
54 | });
55 | }
56 |
57 | testPatterns("constant", (node, flags) => assert.isObject(getConstant(node, flags)), tests.constant);
58 | testPatterns("not constant", (node, flags) => assert.isFalse(getConstant(node, flags)), tests.notConstant);
59 | });
60 |
--------------------------------------------------------------------------------
/tests/project.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { assert } from "chai";
3 |
4 | function relevantFile(path: string): boolean {
5 | return /\.(?:ts|md|json)$/.test(path);
6 | }
7 |
8 | describe("Project", () => {
9 | const rules = fs.readdirSync(`${__dirname}/../lib/rules`).filter(relevantFile);
10 | const ruleTests = fs.readdirSync(`${__dirname}/../tests/lib/rules`).filter(relevantFile);
11 | const ruleDocs = fs.readdirSync(`${__dirname}/../docs/rules`).filter(relevantFile);
12 |
13 | it("should have test files for every rule", () => {
14 | assert.sameMembers(rules, ruleTests);
15 | });
16 |
17 | it("should have doc files for every rule", () => {
18 | assert.sameMembers(
19 | rules,
20 | ruleDocs.map(f => f.replace(/.md$/, ".ts"))
21 | );
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/tests/test-util.ts:
--------------------------------------------------------------------------------
1 | import { RuleTester } from "eslint";
2 | import { filenameToRule } from "../lib/rules-util";
3 |
4 | interface Test {
5 | valid?: Array;
6 | invalid?: RuleTester.InvalidTestCase[];
7 | }
8 |
9 | export function testRule(testFilename: string, config: any, tests: Test): void {
10 | const ruleName = filenameToRule(testFilename);
11 | // eslint-disable-next-line @typescript-eslint/no-var-requires
12 | const rule = require("../lib/rules/" + ruleName).default;
13 | const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 }, ...(config || {}) });
14 |
15 | ruleTester.run(ruleName, rule, tests);
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
7 | "lib": [
8 | "es2015"
9 | ], /* Specify library files to be included in the compilation. */
10 | "allowJs": true, /* Allow javascript files to be compiled. */
11 | "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "./dist", /* Redirect output structure to the directory. */
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
21 | "removeComments": true, /* Do not emit comments to output. */
22 | // "noEmit": true, /* Do not emit outputs. */
23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
24 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
26 |
27 | /* Strict Type-Checking Options */
28 | // "strict": true, /* Enable all strict type-checking options. */
29 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
30 | "strictNullChecks": true, /* Enable strict null checks. */
31 | "strictFunctionTypes": true, /* Enable strict checking of function types. */
32 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
33 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
34 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
35 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
36 |
37 | /* Additional Checks */
38 | "noUnusedLocals": true, /* Report errors on unused locals. */
39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
40 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
41 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
42 |
43 | /* Module Resolution Options */
44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [], /* List of folders to include type definitions from. */
49 | // "types": [], /* Type declaration files to be included in compilation. */
50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 |
65 | /* Advanced Options */
66 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
67 | },
68 | "include": [
69 | "lib/**/*.ts"
70 | ]
71 | }
72 |
--------------------------------------------------------------------------------