├── .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(/^(\(\? { 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 | --------------------------------------------------------------------------------