Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Yosuke Ota
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,283 @@
# Introduction
[eslint-plugin-regexp](https://www.npmjs.com/package/eslint-plugin-regexp) is ESLint plugin for finding RegExp mistakes and RegExp style guide violations.
<!--PACKAGE_STATUS_START-->
[![NPM license](https://img.shields.io/npm/l/eslint-plugin-regexp.svg)](https://www.npmjs.com/package/eslint-plugin-regexp)
[![NPM version](https://img.shields.io/npm/v/eslint-plugin-regexp.svg)](https://www.npmjs.com/package/eslint-plugin-regexp)
[![NPM downloads](https://img.shields.io/badge/dynamic/json.svg?label=downloads&colorB=green&suffix=/day&query=$.downloads&uri=https://api.npmjs.org//downloads/point/last-day/eslint-plugin-regexp&maxAge=3600)](http://www.npmtrends.com/eslint-plugin-regexp)
[![NPM downloads](https://img.shields.io/npm/dw/eslint-plugin-regexp.svg)](http://www.npmtrends.com/eslint-plugin-regexp)
[![NPM downloads](https://img.shields.io/npm/dm/eslint-plugin-regexp.svg)](http://www.npmtrends.com/eslint-plugin-regexp)
[![NPM downloads](https://img.shields.io/npm/dy/eslint-plugin-regexp.svg)](http://www.npmtrends.com/eslint-plugin-regexp)
[![NPM downloads](https://img.shields.io/npm/dt/eslint-plugin-regexp.svg)](http://www.npmtrends.com/eslint-plugin-regexp)
[![Build Status](https://github.com/ota-meshi/eslint-plugin-regexp/workflows/CI/badge.svg?branch=master)](https://github.com/ota-meshi/eslint-plugin-regexp/actions?query=workflow%3ACI)
[![Coverage Status](https://coveralls.io/repos/github/ota-meshi/eslint-plugin-regexp/badge.svg?branch=master)](https://coveralls.io/github/ota-meshi/eslint-plugin-regexp?branch=master)
<!--PACKAGE_STATUS_END-->
## :name_badge: Features
This ESLint plugin provides linting rules relate to better ways to help you avoid problems when using RegExp.
- Find the wrong usage of regular expressions, and their hints.
- Enforces a consistent style of regular expressions.
- Find hints for writing optimized regular expressions.
- 80 plugin rules for regular expression syntax and features.
You can check on the [Online DEMO](https://ota-meshi.github.io/eslint-plugin-regexp/playground/).
<!--DOCS_IGNORE_START-->
## :book: Documentation
See [documents](https://ota-meshi.github.io/eslint-plugin-regexp/).
## :cd: Installation
```bash
npm install --save-dev eslint eslint-plugin-regexp
```
> **Requirements**
>
> - ESLint v8.44.0 and above
> - Node.js v18.x, v20.x and above
<!--DOCS_IGNORE_END-->
## :book: Usage
<!--USAGE_SECTION_START-->
Add `regexp` to the plugins section of your `eslint.config.js` or `.eslintrc` configuration file (you can omit the `eslint-plugin-` prefix)
and either use one of the two configurations available (`recommended` or `all`) or configure the rules you want:
### The recommended configuration (New Config)
The `plugin.configs["flat/recommended"]` config enables a subset of [the rules](#white_check_mark-rules) that should be most useful to most users.
*See [lib/configs/rules/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/rules/recommended.ts) for more details.*
```js
// eslint.config.js
import * as regexpPlugin from "eslint-plugin-regexp"
export default [
regexpPlugin.configs["flat/recommended"],
];
```
### The recommended configuration (Legacy Config)
The `plugin:regexp/recommended` config enables a subset of [the rules](#white_check_mark-rules) that should be most useful to most users.
*See [lib/configs/rules/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/rules/recommended.ts) for more details.*
```js
// .eslintrc.js
module.exports = {
"plugins": [
"regexp"
],
"extends": [
// add more generic rulesets here, such as:
// 'eslint:recommended',
"plugin:regexp/recommended"
]
}
```
### Advanced Configuration
Override/add specific rules configurations. *See also: [http://eslint.org/docs/user-guide/configuring](http://eslint.org/docs/user-guide/configuring)*.
```js
// eslint.config.js
import * as regexpPlugin from "eslint-plugin-regexp"
export default [
{
plugins: { regexp: regexpPlugin },
rules: {
// Override/add rules settings here, such as:
"regexp/rule-name": "error"
}
}
];
```
```js
// .eslintrc.js
module.exports = {
"plugins": [
"regexp"
],
"rules": {
// Override/add rules settings here, such as:
"regexp/rule-name": "error"
}
}
```
### Using the all configuration
The `plugin.configs["flat/all"]` / `plugin:regexp/all` config enables all rules. It's meant for testing, not for production use because it changes with every minor and major version of the plugin. Use it at your own risk.
*See [lib/configs/rules/all.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/rules/all.ts) for more details.*
<!--USAGE_SECTION_END-->
## :white_check_mark: Rules
<!-- begin auto-generated rules list -->
💼 Configurations enabled in.\
⚠️ Configurations set to warn in.\
🟢 Set in the `flat/recommended` configuration.\
🔵 Set in the `recommended` configuration.\
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
### Possible Errors
| Name                                 | Description | 💼 | ⚠️ | 🔧 | 💡 |
| :--------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | :---- | :---- | :- | :- |
| [no-contradiction-with-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-contradiction-with-assertion.html) | disallow elements that contradict assertions | 🟢 🔵 | | | 💡 |
| [no-control-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-control-character.html) | disallow control characters | | | | 💡 |
| [no-dupe-disjunctions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-dupe-disjunctions.html) | disallow duplicate disjunctions | 🟢 🔵 | | | 💡 |
| [no-empty-alternative](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-alternative.html) | disallow alternatives without elements | | 🟢 🔵 | | 💡 |
| [no-empty-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-capturing-group.html) | disallow capturing group that captures empty. | 🟢 🔵 | | | |
| [no-empty-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-character-class.html) | disallow character classes that match no characters | 🟢 🔵 | | | |
| [no-empty-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-group.html) | disallow empty group | 🟢 🔵 | | | |
| [no-empty-lookarounds-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-lookarounds-assertion.html) | disallow empty lookahead assertion or empty lookbehind assertion | 🟢 🔵 | | | |
| [no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | 🟢 🔵 | | | 💡 |
| [no-invalid-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invalid-regexp.html) | disallow invalid regular expression strings in `RegExp` constructors | 🟢 🔵 | | | |
| [no-lazy-ends](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-lazy-ends.html) | disallow lazy quantifiers at the end of an expression | | 🟢 🔵 | | 💡 |
| [no-misleading-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-misleading-capturing-group.html) | disallow capturing groups that do not behave as one would expect | 🟢 🔵 | | | 💡 |
| [no-misleading-unicode-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-misleading-unicode-character.html) | disallow multi-code-point characters in character classes and quantifiers | 🟢 🔵 | | 🔧 | 💡 |
| [no-missing-g-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-missing-g-flag.html) | disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll` | 🟢 🔵 | | 🔧 | |
| [no-optional-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-optional-assertion.html) | disallow optional assertions | 🟢 🔵 | | | |
| [no-potentially-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-potentially-useless-backreference.html) | disallow backreferences that reference a group that might not be matched | | 🟢 🔵 | | |
| [no-super-linear-backtracking](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-super-linear-backtracking.html) | disallow exponential and polynomial backtracking | 🟢 🔵 | | 🔧 | |
| [no-super-linear-move](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-super-linear-move.html) | disallow quantifiers that cause quadratic moves | | | | |
| [no-useless-assertions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-assertions.html) | disallow assertions that are known to always accept (or reject) | 🟢 🔵 | | | 💡 |
| [no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | 🟢 🔵 | | | |
| [no-useless-dollar-replacements](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-dollar-replacements.html) | disallow useless `$` replacements in replacement string | 🟢 🔵 | | | |
| [strict](https://ota-meshi.github.io/eslint-plugin-regexp/rules/strict.html) | disallow not strictly valid regular expressions | 🟢 🔵 | | 🔧 | 💡 |
### Best Practices
| Name                                  | Description | 💼 | ⚠️ | 🔧 | 💡 |
| :----------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | :---- | :---- | :- | :- |
| [confusing-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/confusing-quantifier.html) | disallow confusing quantifiers | | 🟢 🔵 | | |
| [control-character-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/control-character-escape.html) | enforce consistent escaping of control characters | 🟢 🔵 | | 🔧 | |
| [negation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/negation.html) | enforce use of escapes on negation | 🟢 🔵 | | 🔧 | |
| [no-dupe-characters-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-dupe-characters-character-class.html) | disallow duplicate characters in the RegExp character class | 🟢 🔵 | | 🔧 | |
| [no-empty-string-literal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-string-literal.html) | disallow empty string literals in character classes | 🟢 🔵 | | | |
| [no-extra-lookaround-assertions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-extra-lookaround-assertions.html) | disallow unnecessary nested lookaround assertions | 🟢 🔵 | | 🔧 | |
| [no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | 🟢 🔵 | | 🔧 | |
| [no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html) | disallow legacy RegExp features | 🟢 🔵 | | | |
| [no-non-standard-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-non-standard-flag.html) | disallow non-standard flags | 🟢 🔵 | | | |
| [no-obscure-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-obscure-range.html) | disallow obscure character ranges | 🟢 🔵 | | | |
| [no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | | | | 💡 |
| [no-standalone-backslash](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-standalone-backslash.html) | disallow standalone backslashes (`\`) | | | | |
| [no-trivially-nested-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-trivially-nested-assertion.html) | disallow trivially nested assertions | 🟢 🔵 | | 🔧 | |
| [no-trivially-nested-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-trivially-nested-quantifier.html) | disallow nested quantifiers that can be rewritten as one quantifier | 🟢 🔵 | | 🔧 | |
| [no-unused-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-unused-capturing-group.html) | disallow unused capturing group | 🟢 🔵 | | 🔧 | 💡 |
| [no-useless-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-character-class.html) | disallow character class with one character | 🟢 🔵 | | 🔧 | |
| [no-useless-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-flag.html) | disallow unnecessary regex flags | | 🟢 🔵 | 🔧 | |
| [no-useless-lazy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-lazy.html) | disallow unnecessarily non-greedy quantifiers | 🟢 🔵 | | 🔧 | |
| [no-useless-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-quantifier.html) | disallow quantifiers that can be removed | 🟢 🔵 | | 🔧 | 💡 |
| [no-useless-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-range.html) | disallow unnecessary character ranges | 🟢 🔵 | | 🔧 | |
| [no-useless-set-operand](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-set-operand.html) | disallow unnecessary elements in expression character classes | 🟢 🔵 | | 🔧 | |
| [no-useless-string-literal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-string-literal.html) | disallow string disjunction of single characters in `\q{...}` | 🟢 🔵 | | 🔧 | |
| [no-useless-two-nums-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-two-nums-quantifier.html) | disallow unnecessary `{n,m}` quantifier | 🟢 🔵 | | 🔧 | |
| [no-zero-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-zero-quantifier.html) | disallow quantifiers with a maximum of zero | 🟢 🔵 | | | 💡 |
| [optimal-lookaround-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/optimal-lookaround-quantifier.html) | disallow the alternatives of lookarounds that end with a non-constant quantifier | | 🟢 🔵 | | 💡 |
| [optimal-quantifier-concatenation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/optimal-quantifier-concatenation.html) | require optimal quantifiers for concatenated quantifiers | 🟢 🔵 | | 🔧 | |
| [prefer-escape-replacement-dollar-char](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-escape-replacement-dollar-char.html) | enforces escape of replacement `$` character (`$$`). | | | | |
| [prefer-predefined-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-predefined-assertion.html) | prefer predefined assertion over equivalent lookarounds | 🟢 🔵 | | 🔧 | |
| [prefer-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-quantifier.html) | enforce using quantifier | | | 🔧 | |
| [prefer-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-range.html) | enforce using character class range | 🟢 🔵 | | 🔧 | |
| [prefer-regexp-exec](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-exec.html) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | | | | |
| [prefer-regexp-test](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-test.html) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | | | 🔧 | |
| [prefer-set-operation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-set-operation.html) | prefer character class set operations instead of lookarounds | 🟢 🔵 | | 🔧 | |
| [require-unicode-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-regexp.html) | enforce the use of the `u` flag | | | 🔧 | |
| [require-unicode-sets-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-sets-regexp.html) | enforce the use of the `v` flag | | | 🔧 | |
| [simplify-set-operations](https://ota-meshi.github.io/eslint-plugin-regexp/rules/simplify-set-operations.html) | require simplify set operations | 🟢 🔵 | | 🔧 | |
| [sort-alternatives](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-alternatives.html) | sort alternatives if order doesn't matter | | | 🔧 | |
| [use-ignore-case](https://ota-meshi.github.io/eslint-plugin-regexp/rules/use-ignore-case.html) | use the `i` flag if it simplifies the pattern | 🟢 🔵 | | 🔧 | |
### Stylistic Issues
| Name                             | Description | 💼 | ⚠️ | 🔧 | 💡 |
| :------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- | :---- | :- | :- | :- |
| [grapheme-string-literal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/grapheme-string-literal.html) | enforce single grapheme in string literal | | | | |
| [hexadecimal-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/hexadecimal-escape.html) | enforce consistent usage of hexadecimal escape | | | 🔧 | |
| [letter-case](https://ota-meshi.github.io/eslint-plugin-regexp/rules/letter-case.html) | enforce into your favorite case | | | 🔧 | |
| [match-any](https://ota-meshi.github.io/eslint-plugin-regexp/rules/match-any.html) | enforce match any character style | 🟢 🔵 | | 🔧 | |
| [no-useless-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-escape.html) | disallow unnecessary escape characters in RegExp | 🟢 🔵 | | 🔧 | |
| [no-useless-non-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-capturing-group.html) | disallow unnecessary non-capturing group | 🟢 🔵 | | 🔧 | |
| [prefer-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-character-class.html) | enforce using character class | 🟢 🔵 | | 🔧 | |
| [prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | 🟢 🔵 | | 🔧 | |
| [prefer-lookaround](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-lookaround.html) | prefer lookarounds over capturing group that do not replace | | | 🔧 | |
| [prefer-named-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-backreference.html) | enforce using named backreferences | | | 🔧 | |
| [prefer-named-capture-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-capture-group.html) | enforce using named capture groups | | | | |
| [prefer-named-replacement](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-replacement.html) | enforce using named replacement | | | 🔧 | |
| [prefer-plus-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html) | enforce using `+` quantifier | 🟢 🔵 | | 🔧 | |
| [prefer-question-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html) | enforce using `?` quantifier | 🟢 🔵 | | 🔧 | |
| [prefer-result-array-groups](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-result-array-groups.html) | enforce using result array `groups` | | | 🔧 | |
| [prefer-star-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-star-quantifier.html) | enforce using `*` quantifier | 🟢 🔵 | | 🔧 | |
| [prefer-unicode-codepoint-escapes](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-unicode-codepoint-escapes.html) | enforce use of unicode codepoint escapes | 🟢 🔵 | | 🔧 | |
| [prefer-w](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-w.html) | enforce using `\w` | 🟢 🔵 | | 🔧 | |
| [sort-character-class-elements](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-character-class-elements.html) | enforces elements order in character class | | | 🔧 | |
| [sort-flags](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-flags.html) | require regex flags to be sorted | 🟢 🔵 | | 🔧 | |
| [unicode-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/unicode-escape.html) | enforce consistent usage of unicode escape or unicode codepoint escape | | | 🔧 | |
| [unicode-property](https://ota-meshi.github.io/eslint-plugin-regexp/rules/unicode-property.html) | enforce consistent naming of unicode properties | | | 🔧 | |
<!-- end auto-generated rules list -->
<!--REMOVED_RULES_START-->
### Removed
- :no_entry: These rules have been removed in a previous major release, after they have been deprecated for a while.
| Rule ID | Replaced by | Removed in version |
|:--------|:------------|:-------------------|
| [no-assertion-capturing-group](https://github.com/ota-meshi/eslint-plugin-regexp/blob/v1.15.0/docs/rules/no-assertion-capturing-group.md) | [regexp/no-empty-capturing-group](./no-empty-capturing-group.md) | v2.0.0 |
| [no-useless-exactly-quantifier](https://github.com/ota-meshi/eslint-plugin-regexp/blob/v1.15.0/docs/rules/no-useless-exactly-quantifier.md) | [regexp/no-useless-quantifier](./no-useless-quantifier.md), [regexp/no-zero-quantifier](./no-zero-quantifier.md) | v2.0.0 |
| [no-useless-non-greedy](https://github.com/ota-meshi/eslint-plugin-regexp/blob/v1.15.0/docs/rules/no-useless-non-greedy.md) | [regexp/no-useless-lazy](./no-useless-lazy.md) | v2.0.0 |
| [order-in-character-class](https://github.com/ota-meshi/eslint-plugin-regexp/blob/v1.15.0/docs/rules/order-in-character-class.md) | [regexp/sort-character-class-elements](./sort-character-class-elements.md) | v2.0.0 |
| [prefer-t](https://github.com/ota-meshi/eslint-plugin-regexp/blob/v1.15.0/docs/rules/prefer-t.md) | [regexp/control-character-escape](./control-character-escape.md) | v2.0.0 |
<!--REMOVED_RULES_END-->
## :gear: Settings
See [Settings](https://ota-meshi.github.io/eslint-plugin-regexp/settings/).
<!--DOCS_IGNORE_START-->
## :traffic_light: Semantic Versioning Policy
**eslint-plugin-regexp** follows [Semantic Versioning](http://semver.org/) and [ESLint's Semantic Versioning Policy](https://github.com/eslint/eslint#semantic-versioning-policy).
## :beers: Contributing
Welcome contributing!
Please use GitHub's Issues/PRs.
See [CONTRIBUTING.md](CONTRIBUTING.md).
### Development Tools
- `npm test` runs tests and measures coverage.
- `npm run update` runs in order to update readme and recommended configuration.
- `npm run new [new rule name]` runs to create the files needed for the new rule.
- `npm run docs:watch` starts the website locally.
<!--DOCS_IGNORE_END-->
## :lock: License
See the [LICENSE](LICENSE) file for license rights and limitations (MIT).

View file

@ -0,0 +1,2 @@
import type { RuleModule } from "./types";
export declare const rules: RuleModule[];

View file

@ -0,0 +1,172 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = void 0;
const confusing_quantifier_1 = __importDefault(require("./rules/confusing-quantifier"));
const control_character_escape_1 = __importDefault(require("./rules/control-character-escape"));
const grapheme_string_literal_1 = __importDefault(require("./rules/grapheme-string-literal"));
const hexadecimal_escape_1 = __importDefault(require("./rules/hexadecimal-escape"));
const letter_case_1 = __importDefault(require("./rules/letter-case"));
const match_any_1 = __importDefault(require("./rules/match-any"));
const negation_1 = __importDefault(require("./rules/negation"));
const no_contradiction_with_assertion_1 = __importDefault(require("./rules/no-contradiction-with-assertion"));
const no_control_character_1 = __importDefault(require("./rules/no-control-character"));
const no_dupe_characters_character_class_1 = __importDefault(require("./rules/no-dupe-characters-character-class"));
const no_dupe_disjunctions_1 = __importDefault(require("./rules/no-dupe-disjunctions"));
const no_empty_alternative_1 = __importDefault(require("./rules/no-empty-alternative"));
const no_empty_capturing_group_1 = __importDefault(require("./rules/no-empty-capturing-group"));
const no_empty_character_class_1 = __importDefault(require("./rules/no-empty-character-class"));
const no_empty_group_1 = __importDefault(require("./rules/no-empty-group"));
const no_empty_lookarounds_assertion_1 = __importDefault(require("./rules/no-empty-lookarounds-assertion"));
const no_empty_string_literal_1 = __importDefault(require("./rules/no-empty-string-literal"));
const no_escape_backspace_1 = __importDefault(require("./rules/no-escape-backspace"));
const no_extra_lookaround_assertions_1 = __importDefault(require("./rules/no-extra-lookaround-assertions"));
const no_invalid_regexp_1 = __importDefault(require("./rules/no-invalid-regexp"));
const no_invisible_character_1 = __importDefault(require("./rules/no-invisible-character"));
const no_lazy_ends_1 = __importDefault(require("./rules/no-lazy-ends"));
const no_legacy_features_1 = __importDefault(require("./rules/no-legacy-features"));
const no_misleading_capturing_group_1 = __importDefault(require("./rules/no-misleading-capturing-group"));
const no_misleading_unicode_character_1 = __importDefault(require("./rules/no-misleading-unicode-character"));
const no_missing_g_flag_1 = __importDefault(require("./rules/no-missing-g-flag"));
const no_non_standard_flag_1 = __importDefault(require("./rules/no-non-standard-flag"));
const no_obscure_range_1 = __importDefault(require("./rules/no-obscure-range"));
const no_octal_1 = __importDefault(require("./rules/no-octal"));
const no_optional_assertion_1 = __importDefault(require("./rules/no-optional-assertion"));
const no_potentially_useless_backreference_1 = __importDefault(require("./rules/no-potentially-useless-backreference"));
const no_standalone_backslash_1 = __importDefault(require("./rules/no-standalone-backslash"));
const no_super_linear_backtracking_1 = __importDefault(require("./rules/no-super-linear-backtracking"));
const no_super_linear_move_1 = __importDefault(require("./rules/no-super-linear-move"));
const no_trivially_nested_assertion_1 = __importDefault(require("./rules/no-trivially-nested-assertion"));
const no_trivially_nested_quantifier_1 = __importDefault(require("./rules/no-trivially-nested-quantifier"));
const no_unused_capturing_group_1 = __importDefault(require("./rules/no-unused-capturing-group"));
const no_useless_assertions_1 = __importDefault(require("./rules/no-useless-assertions"));
const no_useless_backreference_1 = __importDefault(require("./rules/no-useless-backreference"));
const no_useless_character_class_1 = __importDefault(require("./rules/no-useless-character-class"));
const no_useless_dollar_replacements_1 = __importDefault(require("./rules/no-useless-dollar-replacements"));
const no_useless_escape_1 = __importDefault(require("./rules/no-useless-escape"));
const no_useless_flag_1 = __importDefault(require("./rules/no-useless-flag"));
const no_useless_lazy_1 = __importDefault(require("./rules/no-useless-lazy"));
const no_useless_non_capturing_group_1 = __importDefault(require("./rules/no-useless-non-capturing-group"));
const no_useless_quantifier_1 = __importDefault(require("./rules/no-useless-quantifier"));
const no_useless_range_1 = __importDefault(require("./rules/no-useless-range"));
const no_useless_set_operand_1 = __importDefault(require("./rules/no-useless-set-operand"));
const no_useless_string_literal_1 = __importDefault(require("./rules/no-useless-string-literal"));
const no_useless_two_nums_quantifier_1 = __importDefault(require("./rules/no-useless-two-nums-quantifier"));
const no_zero_quantifier_1 = __importDefault(require("./rules/no-zero-quantifier"));
const optimal_lookaround_quantifier_1 = __importDefault(require("./rules/optimal-lookaround-quantifier"));
const optimal_quantifier_concatenation_1 = __importDefault(require("./rules/optimal-quantifier-concatenation"));
const prefer_character_class_1 = __importDefault(require("./rules/prefer-character-class"));
const prefer_d_1 = __importDefault(require("./rules/prefer-d"));
const prefer_escape_replacement_dollar_char_1 = __importDefault(require("./rules/prefer-escape-replacement-dollar-char"));
const prefer_lookaround_1 = __importDefault(require("./rules/prefer-lookaround"));
const prefer_named_backreference_1 = __importDefault(require("./rules/prefer-named-backreference"));
const prefer_named_capture_group_1 = __importDefault(require("./rules/prefer-named-capture-group"));
const prefer_named_replacement_1 = __importDefault(require("./rules/prefer-named-replacement"));
const prefer_plus_quantifier_1 = __importDefault(require("./rules/prefer-plus-quantifier"));
const prefer_predefined_assertion_1 = __importDefault(require("./rules/prefer-predefined-assertion"));
const prefer_quantifier_1 = __importDefault(require("./rules/prefer-quantifier"));
const prefer_question_quantifier_1 = __importDefault(require("./rules/prefer-question-quantifier"));
const prefer_range_1 = __importDefault(require("./rules/prefer-range"));
const prefer_regexp_exec_1 = __importDefault(require("./rules/prefer-regexp-exec"));
const prefer_regexp_test_1 = __importDefault(require("./rules/prefer-regexp-test"));
const prefer_result_array_groups_1 = __importDefault(require("./rules/prefer-result-array-groups"));
const prefer_set_operation_1 = __importDefault(require("./rules/prefer-set-operation"));
const prefer_star_quantifier_1 = __importDefault(require("./rules/prefer-star-quantifier"));
const prefer_unicode_codepoint_escapes_1 = __importDefault(require("./rules/prefer-unicode-codepoint-escapes"));
const prefer_w_1 = __importDefault(require("./rules/prefer-w"));
const require_unicode_regexp_1 = __importDefault(require("./rules/require-unicode-regexp"));
const require_unicode_sets_regexp_1 = __importDefault(require("./rules/require-unicode-sets-regexp"));
const simplify_set_operations_1 = __importDefault(require("./rules/simplify-set-operations"));
const sort_alternatives_1 = __importDefault(require("./rules/sort-alternatives"));
const sort_character_class_elements_1 = __importDefault(require("./rules/sort-character-class-elements"));
const sort_flags_1 = __importDefault(require("./rules/sort-flags"));
const strict_1 = __importDefault(require("./rules/strict"));
const unicode_escape_1 = __importDefault(require("./rules/unicode-escape"));
const unicode_property_1 = __importDefault(require("./rules/unicode-property"));
const use_ignore_case_1 = __importDefault(require("./rules/use-ignore-case"));
exports.rules = [
confusing_quantifier_1.default,
control_character_escape_1.default,
grapheme_string_literal_1.default,
hexadecimal_escape_1.default,
letter_case_1.default,
match_any_1.default,
negation_1.default,
no_contradiction_with_assertion_1.default,
no_control_character_1.default,
no_dupe_characters_character_class_1.default,
no_dupe_disjunctions_1.default,
no_empty_alternative_1.default,
no_empty_capturing_group_1.default,
no_empty_character_class_1.default,
no_empty_group_1.default,
no_empty_lookarounds_assertion_1.default,
no_empty_string_literal_1.default,
no_escape_backspace_1.default,
no_extra_lookaround_assertions_1.default,
no_invalid_regexp_1.default,
no_invisible_character_1.default,
no_lazy_ends_1.default,
no_legacy_features_1.default,
no_misleading_capturing_group_1.default,
no_misleading_unicode_character_1.default,
no_missing_g_flag_1.default,
no_non_standard_flag_1.default,
no_obscure_range_1.default,
no_octal_1.default,
no_optional_assertion_1.default,
no_potentially_useless_backreference_1.default,
no_standalone_backslash_1.default,
no_super_linear_backtracking_1.default,
no_super_linear_move_1.default,
no_trivially_nested_assertion_1.default,
no_trivially_nested_quantifier_1.default,
no_unused_capturing_group_1.default,
no_useless_assertions_1.default,
no_useless_backreference_1.default,
no_useless_character_class_1.default,
no_useless_dollar_replacements_1.default,
no_useless_escape_1.default,
no_useless_flag_1.default,
no_useless_lazy_1.default,
no_useless_non_capturing_group_1.default,
no_useless_quantifier_1.default,
no_useless_range_1.default,
no_useless_set_operand_1.default,
no_useless_string_literal_1.default,
no_useless_two_nums_quantifier_1.default,
no_zero_quantifier_1.default,
optimal_lookaround_quantifier_1.default,
optimal_quantifier_concatenation_1.default,
prefer_character_class_1.default,
prefer_d_1.default,
prefer_escape_replacement_dollar_char_1.default,
prefer_lookaround_1.default,
prefer_named_backreference_1.default,
prefer_named_capture_group_1.default,
prefer_named_replacement_1.default,
prefer_plus_quantifier_1.default,
prefer_predefined_assertion_1.default,
prefer_quantifier_1.default,
prefer_question_quantifier_1.default,
prefer_range_1.default,
prefer_regexp_exec_1.default,
prefer_regexp_test_1.default,
prefer_result_array_groups_1.default,
prefer_set_operation_1.default,
prefer_star_quantifier_1.default,
prefer_unicode_codepoint_escapes_1.default,
prefer_w_1.default,
require_unicode_regexp_1.default,
require_unicode_sets_regexp_1.default,
simplify_set_operations_1.default,
sort_alternatives_1.default,
sort_character_class_elements_1.default,
sort_flags_1.default,
strict_1.default,
unicode_escape_1.default,
unicode_property_1.default,
use_ignore_case_1.default,
];

View file

@ -0,0 +1,2 @@
export { rules } from "./rules/all";
export declare const plugins: string[];

View file

@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugins = exports.rules = void 0;
var all_1 = require("./rules/all");
Object.defineProperty(exports, "rules", { enumerable: true, get: function () { return all_1.rules; } });
exports.plugins = ["regexp"];

View file

@ -0,0 +1,5 @@
import * as plugin from "../../index";
export { rules } from "../rules/all";
export declare const plugins: {
regexp: typeof plugin;
};

View file

@ -0,0 +1,40 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugins = exports.rules = void 0;
const plugin = __importStar(require("../../index"));
var all_1 = require("../rules/all");
Object.defineProperty(exports, "rules", { enumerable: true, get: function () { return all_1.rules; } });
exports.plugins = { regexp: plugin };

View file

@ -0,0 +1,5 @@
import * as plugin from "../../index";
export { rules } from "../rules/recommended";
export declare const plugins: {
regexp: typeof plugin;
};

View file

@ -0,0 +1,40 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugins = exports.rules = void 0;
const plugin = __importStar(require("../../index"));
var recommended_1 = require("../rules/recommended");
Object.defineProperty(exports, "rules", { enumerable: true, get: function () { return recommended_1.rules; } });
exports.plugins = { regexp: plugin };

View file

@ -0,0 +1,2 @@
export { rules } from "./rules/recommended";
export declare const plugins: string[];

View file

@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugins = exports.rules = void 0;
var recommended_1 = require("./rules/recommended");
Object.defineProperty(exports, "rules", { enumerable: true, get: function () { return recommended_1.rules; } });
exports.plugins = ["regexp"];

View file

@ -0,0 +1,4 @@
import type { SeverityString } from "../../types";
export declare const rules: {
[x: string]: SeverityString;
};

View file

@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = void 0;
const all_rules_1 = require("../../all-rules");
const recommended_1 = require("./recommended");
const all = {};
for (const rule of all_rules_1.rules) {
all[rule.meta.docs.ruleId] = "error";
}
exports.rules = {
...all,
...recommended_1.rules,
};

View file

@ -0,0 +1,2 @@
import type { SeverityString } from "../../types";
export declare const rules: Record<string, SeverityString>;

View file

@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = void 0;
exports.rules = {
"no-control-regex": "error",
"no-misleading-character-class": "error",
"no-regex-spaces": "error",
"prefer-regex-literals": "error",
"no-invalid-regexp": "off",
"no-useless-backreference": "off",
"no-empty-character-class": "off",
"regexp/confusing-quantifier": "warn",
"regexp/control-character-escape": "error",
"regexp/match-any": "error",
"regexp/negation": "error",
"regexp/no-contradiction-with-assertion": "error",
"regexp/no-dupe-characters-character-class": "error",
"regexp/no-dupe-disjunctions": "error",
"regexp/no-empty-alternative": "warn",
"regexp/no-empty-capturing-group": "error",
"regexp/no-empty-character-class": "error",
"regexp/no-empty-group": "error",
"regexp/no-empty-lookarounds-assertion": "error",
"regexp/no-empty-string-literal": "error",
"regexp/no-escape-backspace": "error",
"regexp/no-extra-lookaround-assertions": "error",
"regexp/no-invalid-regexp": "error",
"regexp/no-invisible-character": "error",
"regexp/no-lazy-ends": "warn",
"regexp/no-legacy-features": "error",
"regexp/no-misleading-capturing-group": "error",
"regexp/no-misleading-unicode-character": "error",
"regexp/no-missing-g-flag": "error",
"regexp/no-non-standard-flag": "error",
"regexp/no-obscure-range": "error",
"regexp/no-optional-assertion": "error",
"regexp/no-potentially-useless-backreference": "warn",
"regexp/no-super-linear-backtracking": "error",
"regexp/no-trivially-nested-assertion": "error",
"regexp/no-trivially-nested-quantifier": "error",
"regexp/no-unused-capturing-group": "error",
"regexp/no-useless-assertions": "error",
"regexp/no-useless-backreference": "error",
"regexp/no-useless-character-class": "error",
"regexp/no-useless-dollar-replacements": "error",
"regexp/no-useless-escape": "error",
"regexp/no-useless-flag": "warn",
"regexp/no-useless-lazy": "error",
"regexp/no-useless-non-capturing-group": "error",
"regexp/no-useless-quantifier": "error",
"regexp/no-useless-range": "error",
"regexp/no-useless-set-operand": "error",
"regexp/no-useless-string-literal": "error",
"regexp/no-useless-two-nums-quantifier": "error",
"regexp/no-zero-quantifier": "error",
"regexp/optimal-lookaround-quantifier": "warn",
"regexp/optimal-quantifier-concatenation": "error",
"regexp/prefer-character-class": "error",
"regexp/prefer-d": "error",
"regexp/prefer-plus-quantifier": "error",
"regexp/prefer-predefined-assertion": "error",
"regexp/prefer-question-quantifier": "error",
"regexp/prefer-range": "error",
"regexp/prefer-set-operation": "error",
"regexp/prefer-star-quantifier": "error",
"regexp/prefer-unicode-codepoint-escapes": "error",
"regexp/prefer-w": "error",
"regexp/simplify-set-operations": "error",
"regexp/sort-flags": "error",
"regexp/strict": "error",
"regexp/use-ignore-case": "error",
};

View file

@ -0,0 +1,15 @@
import * as all from "./configs/all";
import * as flatAll from "./configs/flat/all";
import * as flatRecommended from "./configs/flat/recommended";
import * as recommended from "./configs/recommended";
import type { RuleModule } from "./types";
export * as meta from "./meta";
export declare const configs: {
recommended: typeof recommended;
all: typeof all;
"flat/all": typeof flatAll;
"flat/recommended": typeof flatRecommended;
};
export declare const rules: {
[key: string]: RuleModule;
};

View file

@ -0,0 +1,52 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = exports.configs = exports.meta = void 0;
const all_rules_1 = require("./all-rules");
const all = __importStar(require("./configs/all"));
const flatAll = __importStar(require("./configs/flat/all"));
const flatRecommended = __importStar(require("./configs/flat/recommended"));
const recommended = __importStar(require("./configs/recommended"));
exports.meta = __importStar(require("./meta"));
exports.configs = {
recommended,
all,
"flat/all": flatAll,
"flat/recommended": flatRecommended,
};
exports.rules = all_rules_1.rules.reduce((obj, r) => {
obj[r.meta.docs.ruleName] = r;
return obj;
}, {});

View file

@ -0,0 +1 @@
export declare const name: any, version: any;

View file

@ -0,0 +1,5 @@
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.version = exports.name = void 0;
_a = require("../package.json"), exports.name = _a.name, exports.version = _a.version;

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const regexp_ast_1 = require("../utils/regexp-ast");
exports.default = (0, utils_1.createRule)("confusing-quantifier", {
meta: {
docs: {
description: "disallow confusing quantifiers",
category: "Best Practices",
recommended: true,
default: "warn",
},
schema: [],
messages: {
confusing: "This quantifier is confusing because its minimum is {{min}} but it can match the empty string. Maybe replace it with `{{proposal}}` to reflect that it can match the empty string?",
},
type: "problem",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onQuantifierEnter(qNode) {
if (qNode.min > 0 &&
(0, regexp_ast_analysis_1.isPotentiallyEmpty)(qNode.element, flags)) {
const proposal = (0, regexp_ast_1.quantToString)({ ...qNode, min: 0 });
context.report({
node,
loc: getRegexpLocation(qNode, (0, regexp_ast_1.getQuantifierOffsets)(qNode)),
messageId: "confusing",
data: {
min: String(qNode.min),
proposal,
},
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,74 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const utils_2 = require("../utils/ast-utils/utils");
const mention_1 = require("../utils/mention");
const CONTROL_CHARS = new Map([
[0, "\\0"],
[utils_1.CP_TAB, "\\t"],
[utils_1.CP_LF, "\\n"],
[utils_1.CP_VT, "\\v"],
[utils_1.CP_FF, "\\f"],
[utils_1.CP_CR, "\\r"],
]);
function isRegExpLiteralAt({ node, patternSource }, at) {
if ((0, utils_2.isRegexpLiteral)(node)) {
return true;
}
const replaceRange = patternSource.getReplaceRange(at);
if (replaceRange && replaceRange.type === "RegExp") {
return true;
}
return false;
}
exports.default = (0, utils_1.createRule)("control-character-escape", {
meta: {
docs: {
description: "enforce consistent escaping of control characters",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected control character escape {{actual}}. Use '{{expected}}' instead.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
const { node, getRegexpLocation, fixReplaceNode } = regexpContext;
return {
onCharacterEnter(cNode) {
if (cNode.parent.type === "CharacterClassRange") {
return;
}
const expectedRaw = CONTROL_CHARS.get(cNode.value);
if (expectedRaw === undefined) {
return;
}
if (cNode.raw === expectedRaw) {
return;
}
if (!isRegExpLiteralAt(regexpContext, cNode) &&
cNode.raw === String.fromCodePoint(cNode.value)) {
return;
}
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
data: {
actual: (0, mention_1.mentionChar)(cNode),
expected: expectedRaw,
},
fix: fixReplaceNode(cNode, expectedRaw),
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const segmenter = new Intl.Segmenter();
exports.default = (0, utils_1.createRule)("grapheme-string-literal", {
meta: {
docs: {
description: "enforce single grapheme in string literal",
category: "Stylistic Issues",
recommended: false,
},
schema: [],
messages: {
onlySingleCharacters: "Only single characters and graphemes are allowed inside character classes. Use regular alternatives (e.g. `{{alternatives}}`) for strings instead.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
const { node, getRegexpLocation } = regexpContext;
function isMultipleGraphemes(saNode) {
if (saNode.elements.length <= 1)
return false;
const string = String.fromCodePoint(...saNode.elements.map((element) => element.value));
const segments = [...segmenter.segment(string)];
return segments.length > 1;
}
function buildAlternativeExample(saNode) {
const alternativeRaws = saNode.parent.alternatives
.filter(isMultipleGraphemes)
.map((alt) => alt.raw);
return `(?:${alternativeRaws.join("|")}|[...])`;
}
return {
onStringAlternativeEnter(saNode) {
if (!isMultipleGraphemes(saNode))
return;
context.report({
node,
loc: getRegexpLocation(saNode),
messageId: "onlySingleCharacters",
data: {
alternatives: buildAlternativeExample(saNode),
},
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regex_syntax_1 = require("../utils/regex-syntax");
exports.default = (0, utils_1.createRule)("hexadecimal-escape", {
meta: {
docs: {
description: "enforce consistent usage of hexadecimal escape",
category: "Stylistic Issues",
recommended: false,
},
fixable: "code",
schema: [
{
enum: ["always", "never"],
},
],
messages: {
expectedHexEscape: "Expected hexadecimal escape ('{{hexEscape}}'), but {{unexpectedKind}} escape ('{{rejectEscape}}') is used.",
unexpectedHexEscape: "Unexpected hexadecimal escape ('{{hexEscape}}').",
},
type: "suggestion",
},
create(context) {
const always = context.options[0] !== "never";
function verifyForAlways({ node, getRegexpLocation, fixReplaceNode }, kind, cNode) {
if (kind !== regex_syntax_1.EscapeSequenceKind.unicode &&
kind !== regex_syntax_1.EscapeSequenceKind.unicodeCodePoint) {
return;
}
const hexEscape = `\\x${cNode.value.toString(16).padStart(2, "0")}`;
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "expectedHexEscape",
data: {
hexEscape,
unexpectedKind: kind,
rejectEscape: cNode.raw,
},
fix: fixReplaceNode(cNode, hexEscape),
});
}
function verifyForNever({ node, getRegexpLocation, fixReplaceNode }, kind, cNode) {
if (kind !== regex_syntax_1.EscapeSequenceKind.hexadecimal) {
return;
}
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpectedHexEscape",
data: {
hexEscape: cNode.raw,
},
fix: fixReplaceNode(cNode, () => `\\u00${cNode.raw.slice(2)}`),
});
}
const verify = always ? verifyForAlways : verifyForNever;
function createVisitor(regexpContext) {
return {
onCharacterEnter(cNode) {
if (cNode.value > 0xff) {
return;
}
const kind = (0, regex_syntax_1.getEscapeSequenceKind)(cNode.raw);
if (!kind) {
return;
}
verify(regexpContext, kind, cNode);
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,159 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regex_syntax_1 = require("../utils/regex-syntax");
const CASE_SCHEMA = ["lowercase", "uppercase", "ignore"];
const DEFAULTS = {
caseInsensitive: "lowercase",
unicodeEscape: "lowercase",
hexadecimalEscape: "lowercase",
controlEscape: "uppercase",
};
function parseOptions(option) {
if (!option) {
return DEFAULTS;
}
return {
caseInsensitive: option.caseInsensitive || DEFAULTS.caseInsensitive,
unicodeEscape: option.unicodeEscape || DEFAULTS.unicodeEscape,
hexadecimalEscape: option.hexadecimalEscape || DEFAULTS.hexadecimalEscape,
controlEscape: option.controlEscape || DEFAULTS.controlEscape,
};
}
const CODE_POINT_CASE_CHECKER = {
lowercase: utils_1.isLowercaseLetter,
uppercase: utils_1.isUppercaseLetter,
};
const STRING_CASE_CHECKER = {
lowercase: (s) => s.toLowerCase() === s,
uppercase: (s) => s.toUpperCase() === s,
};
const CONVERTER = {
lowercase: (s) => s.toLowerCase(),
uppercase: (s) => s.toUpperCase(),
};
exports.default = (0, utils_1.createRule)("letter-case", {
meta: {
docs: {
description: "enforce into your favorite case",
category: "Stylistic Issues",
recommended: false,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
caseInsensitive: { enum: CASE_SCHEMA },
unicodeEscape: { enum: CASE_SCHEMA },
hexadecimalEscape: { enum: CASE_SCHEMA },
controlEscape: { enum: CASE_SCHEMA },
},
additionalProperties: false,
},
],
messages: {
unexpected: "'{{char}}' is not in {{case}}",
},
type: "layout",
},
create(context) {
const options = parseOptions(context.options[0]);
function report({ node, getRegexpLocation, fixReplaceNode }, reportNode, letterCase, convertText) {
context.report({
node,
loc: getRegexpLocation(reportNode),
messageId: "unexpected",
data: {
char: reportNode.raw,
case: letterCase,
},
fix: fixReplaceNode(reportNode, () => convertText(CONVERTER[letterCase])),
});
}
function verifyCharacterInCaseInsensitive(regexpContext, cNode) {
if (cNode.parent.type === "CharacterClassRange" ||
options.caseInsensitive === "ignore") {
return;
}
if (CODE_POINT_CASE_CHECKER[options.caseInsensitive](cNode.value) ||
!(0, utils_1.isLetter)(cNode.value)) {
return;
}
report(regexpContext, cNode, options.caseInsensitive, (converter) => converter(String.fromCodePoint(cNode.value)));
}
function verifyCharacterClassRangeInCaseInsensitive(regexpContext, ccrNode) {
if (options.caseInsensitive === "ignore") {
return;
}
if (CODE_POINT_CASE_CHECKER[options.caseInsensitive](ccrNode.min.value) ||
!(0, utils_1.isLetter)(ccrNode.min.value) ||
CODE_POINT_CASE_CHECKER[options.caseInsensitive](ccrNode.max.value) ||
!(0, utils_1.isLetter)(ccrNode.max.value)) {
return;
}
report(regexpContext, ccrNode, options.caseInsensitive, (converter) => `${converter(String.fromCodePoint(ccrNode.min.value))}-${converter(String.fromCodePoint(ccrNode.max.value))}`);
}
function verifyCharacterInUnicodeEscape(regexpContext, cNode) {
if (options.unicodeEscape === "ignore") {
return;
}
const parts = /^(?<prefix>\\u\{?)(?<code>.*?)(?<suffix>\}?)$/u.exec(cNode.raw);
if (STRING_CASE_CHECKER[options.unicodeEscape](parts.groups.code)) {
return;
}
report(regexpContext, cNode, options.unicodeEscape, (converter) => `${parts.groups.prefix}${converter(parts.groups.code)}${parts.groups.suffix}`);
}
function verifyCharacterInHexadecimalEscape(regexpContext, cNode) {
if (options.hexadecimalEscape === "ignore") {
return;
}
const parts = /^\\x(?<code>.*)$/u.exec(cNode.raw);
if (STRING_CASE_CHECKER[options.hexadecimalEscape](parts.groups.code)) {
return;
}
report(regexpContext, cNode, options.hexadecimalEscape, (converter) => `\\x${converter(parts.groups.code)}`);
}
function verifyCharacterInControl(regexpContext, cNode) {
if (options.controlEscape === "ignore") {
return;
}
const parts = /^\\c(?<code>.*)$/u.exec(cNode.raw);
if (STRING_CASE_CHECKER[options.controlEscape](parts.groups.code)) {
return;
}
report(regexpContext, cNode, options.controlEscape, (converter) => `\\c${converter(parts.groups.code)}`);
}
function createVisitor(regexpContext) {
const { flags } = regexpContext;
return {
onCharacterEnter(cNode) {
if (flags.ignoreCase) {
verifyCharacterInCaseInsensitive(regexpContext, cNode);
}
const escapeKind = (0, regex_syntax_1.getEscapeSequenceKind)(cNode.raw);
if (escapeKind === regex_syntax_1.EscapeSequenceKind.unicode ||
escapeKind === regex_syntax_1.EscapeSequenceKind.unicodeCodePoint) {
verifyCharacterInUnicodeEscape(regexpContext, cNode);
}
if (escapeKind === regex_syntax_1.EscapeSequenceKind.hexadecimal) {
verifyCharacterInHexadecimalEscape(regexpContext, cNode);
}
if (escapeKind === regex_syntax_1.EscapeSequenceKind.control) {
verifyCharacterInControl(regexpContext, cNode);
}
},
...(flags.ignoreCase
? {
onCharacterClassRangeEnter(ccrNode) {
verifyCharacterClassRangeInCaseInsensitive(regexpContext, ccrNode);
},
}
: {}),
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,134 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const utils_2 = require("../utils/ast-utils/utils");
const mention_1 = require("../utils/mention");
const OPTION_SS1 = "[\\s\\S]";
const OPTION_SS2 = "[\\S\\s]";
const OPTION_CARET = "[^]";
const OPTION_DOTALL = "dotAll";
exports.default = (0, utils_1.createRule)("match-any", {
meta: {
docs: {
description: "enforce match any character style",
category: "Stylistic Issues",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
allows: {
type: "array",
items: {
type: "string",
enum: [
OPTION_SS1,
OPTION_SS2,
OPTION_CARET,
OPTION_DOTALL,
],
},
uniqueItems: true,
minItems: 1,
},
},
additionalProperties: false,
},
],
messages: {
unexpected: "Unexpected using {{expr}} to match any character.",
},
type: "suggestion",
},
create(context) {
var _a, _b;
const sourceCode = context.sourceCode;
const allowList = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.allows) !== null && _b !== void 0 ? _b : [OPTION_SS1, OPTION_DOTALL];
const allows = new Set(allowList);
const preference = allowList[0] || null;
function fix(fixer, { node, flags, patternSource }, regexpNode) {
var _a, _b;
if (!preference) {
return null;
}
if (preference === OPTION_DOTALL) {
if (!flags.dotAll) {
return null;
}
if (!(0, utils_2.isRegexpLiteral)(node)) {
return null;
}
const range = patternSource.getReplaceRange(regexpNode);
if (range == null) {
return null;
}
const afterRange = [
range.range[1],
node.range[1],
];
return [
range.replace(fixer, "."),
fixer.replaceTextRange(afterRange, sourceCode.text.slice(...afterRange)),
];
}
if (regexpNode.type === "CharacterClass" &&
preference.startsWith("[") &&
preference.endsWith("]")) {
const range = patternSource.getReplaceRange({
start: regexpNode.start + 1,
end: regexpNode.end - 1,
});
return (_a = range === null || range === void 0 ? void 0 : range.replace(fixer, preference.slice(1, -1))) !== null && _a !== void 0 ? _a : null;
}
const range = patternSource.getReplaceRange(regexpNode);
return (_b = range === null || range === void 0 ? void 0 : range.replace(fixer, preference)) !== null && _b !== void 0 ? _b : null;
}
function createVisitor(regexpContext) {
const { node, flags, getRegexpLocation } = regexpContext;
function onClass(ccNode) {
if ((0, regexp_ast_analysis_1.matchesAllCharacters)(ccNode, flags) &&
!(0, regexp_ast_analysis_1.hasStrings)(ccNode, flags) &&
!allows.has(ccNode.raw)) {
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId: "unexpected",
data: {
expr: (0, mention_1.mention)(ccNode),
},
fix(fixer) {
return fix(fixer, regexpContext, ccNode);
},
});
}
}
return {
onCharacterSetEnter(csNode) {
if (csNode.kind === "any" &&
flags.dotAll &&
!allows.has(OPTION_DOTALL)) {
context.report({
node,
loc: getRegexpLocation(csNode),
messageId: "unexpected",
data: {
expr: (0, mention_1.mention)(csNode),
},
fix(fixer) {
return fix(fixer, regexpContext, csNode);
},
});
}
},
onCharacterClassEnter: onClass,
onExpressionCharacterClassEnter: onClass,
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const util_1 = require("../utils/util");
function isNegatableCharacterClassElement(node) {
return (node.type === "CharacterClass" ||
node.type === "ExpressionCharacterClass" ||
(node.type === "CharacterSet" &&
(node.kind !== "property" || !node.strings)));
}
exports.default = (0, utils_1.createRule)("negation", {
meta: {
docs: {
description: "enforce use of escapes on negation",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected negated character class. Use '{{negatedCharSet}}' instead.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, getRegexpLocation, fixReplaceNode, flags, }) {
return {
onCharacterClassEnter(ccNode) {
if (!ccNode.negate || ccNode.elements.length !== 1) {
return;
}
const element = ccNode.elements[0];
if (!isNegatableCharacterClassElement(element)) {
return;
}
if (element.type !== "CharacterSet" && !element.negate) {
return;
}
if (flags.ignoreCase &&
!flags.unicodeSets &&
element.type === "CharacterSet" &&
element.kind === "property") {
const ccSet = (0, regexp_ast_analysis_1.toUnicodeSet)(ccNode, flags);
const negatedElementSet = (0, regexp_ast_analysis_1.toUnicodeSet)({
...element,
negate: !element.negate,
}, flags);
if (!ccSet.equals(negatedElementSet)) {
return;
}
}
const negatedCharSet = getNegationText(element);
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId: "unexpected",
data: { negatedCharSet },
fix: fixReplaceNode(ccNode, negatedCharSet),
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});
function getNegationText(node) {
if (node.type === "CharacterSet") {
let kind = node.raw[1];
if (kind.toLowerCase() === kind) {
kind = kind.toUpperCase();
}
else {
kind = kind.toLowerCase();
}
return `\\${kind}${node.raw.slice(2)}`;
}
if (node.type === "CharacterClass") {
return `[${node.elements.map((e) => e.raw).join("")}]`;
}
if (node.type === "ExpressionCharacterClass") {
return `[${node.raw.slice(2, -1)}]`;
}
return (0, util_1.assertNever)(node);
}

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,252 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const regexp_ast_1 = require("../utils/regexp-ast");
function isTrivialAssertion(assertion, dir, flags) {
if (assertion.kind !== "word") {
if ((0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind) !== dir) {
return true;
}
}
if (assertion.kind === "lookahead" || assertion.kind === "lookbehind") {
if ((0, regexp_ast_analysis_1.isPotentiallyEmpty)(assertion.alternatives, flags)) {
return true;
}
}
const look = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(assertion, dir, flags));
if (look.char.isEmpty || look.char.isAll) {
return true;
}
const after = (0, regexp_ast_analysis_1.getFirstCharAfter)(assertion, dir, flags);
if (!after.edge) {
if (look.exact && look.char.isSupersetOf(after.char)) {
return true;
}
if (look.char.isDisjointWith(after.char)) {
return true;
}
}
return false;
}
function* getNextElements(start, dir, flags) {
let element = start;
for (;;) {
const parent = element.parent;
if (parent.type === "CharacterClass" ||
parent.type === "CharacterClassRange" ||
parent.type === "ClassIntersection" ||
parent.type === "ClassSubtraction" ||
parent.type === "StringAlternative") {
return;
}
if (parent.type === "Quantifier") {
if (parent.max === 1) {
element = parent;
continue;
}
else {
return;
}
}
const elements = parent.elements;
const index = elements.indexOf(element);
const inc = dir === "ltr" ? 1 : -1;
for (let i = index + inc; i >= 0 && i < elements.length; i += inc) {
const e = elements[i];
yield e;
if (!(0, regexp_ast_analysis_1.isZeroLength)(e, flags)) {
return;
}
}
const grandParent = parent.parent;
if ((grandParent.type === "Group" ||
grandParent.type === "CapturingGroup" ||
(grandParent.type === "Assertion" &&
(0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(grandParent.kind) !==
dir)) &&
grandParent.alternatives.length === 1) {
element = grandParent;
continue;
}
return;
}
}
function tryFindContradictionIn(element, dir, condition, flags) {
if (condition(element)) {
return true;
}
if (element.type === "CapturingGroup" || element.type === "Group") {
let some = false;
element.alternatives.forEach((a) => {
if (tryFindContradictionInAlternative(a, dir, condition, flags)) {
some = true;
}
});
return some;
}
if (element.type === "Quantifier" && element.max === 1) {
return tryFindContradictionIn(element.element, dir, condition, flags);
}
if (element.type === "Assertion" &&
(element.kind === "lookahead" || element.kind === "lookbehind") &&
(0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(element.kind) === dir) {
element.alternatives.forEach((a) => tryFindContradictionInAlternative(a, dir, condition, flags));
}
return false;
}
function tryFindContradictionInAlternative(alternative, dir, condition, flags) {
if (condition(alternative)) {
return true;
}
const { elements } = alternative;
const first = dir === "ltr" ? 0 : elements.length;
const inc = dir === "ltr" ? 1 : -1;
for (let i = first; i >= 0 && i < elements.length; i += inc) {
const e = elements[i];
if (tryFindContradictionIn(e, dir, condition, flags)) {
return true;
}
if (!(0, regexp_ast_analysis_1.isZeroLength)(e, flags)) {
break;
}
}
return false;
}
function disjoint(a, b) {
if (a.edge && b.edge) {
return false;
}
return a.char.isDisjointWith(b.char);
}
exports.default = (0, utils_1.createRule)("no-contradiction-with-assertion", {
meta: {
docs: {
description: "disallow elements that contradict assertions",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
alternative: "The alternative {{ alt }} can never be entered because it contradicts with the assertion {{ assertion }}. Either change the alternative or assertion to resolve the contradiction.",
cannotEnterQuantifier: "The quantifier {{ quant }} can never be entered because its element contradicts with the assertion {{ assertion }}. Change or remove the quantifier or change the assertion to resolve the contradiction.",
alwaysEnterQuantifier: "The quantifier {{ quant }} is always entered despite having a minimum of 0. This is because the assertion {{ assertion }} contradicts with the element(s) after the quantifier. Either set the minimum to 1 ({{ newQuant }}) or change the assertion.",
removeQuantifier: "Remove the quantifier.",
changeQuantifier: "Change the quantifier to {{ newQuant }}.",
},
hasSuggestions: true,
type: "problem",
},
create(context) {
function createVisitor(regexpContext) {
const { node, flags, getRegexpLocation, fixReplaceQuant, fixReplaceNode, } = regexpContext;
function analyseAssertion(assertion, dir) {
if (isTrivialAssertion(assertion, dir, flags)) {
return;
}
const assertionLook = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(assertion, dir, flags));
for (const element of getNextElements(assertion, dir, flags)) {
if (tryFindContradictionIn(element, dir, contradicts, flags)) {
break;
}
}
function contradictsAlternative(alternative) {
let consumed = (0, regexp_ast_analysis_1.getFirstConsumedChar)(alternative, dir, flags);
if (consumed.empty) {
consumed = regexp_ast_analysis_1.FirstConsumedChars.concat([
consumed,
(0, regexp_ast_analysis_1.getFirstConsumedCharAfter)(alternative, dir, flags),
], flags);
}
const look = regexp_ast_analysis_1.FirstConsumedChars.toLook(consumed);
if (disjoint(assertionLook, look)) {
context.report({
node,
loc: getRegexpLocation(alternative),
messageId: "alternative",
data: {
assertion: (0, mention_1.mention)(assertion),
alt: (0, mention_1.mention)(alternative),
},
});
return true;
}
return false;
}
function contradictsQuantifier(quant) {
if (quant.max === 0) {
return false;
}
if (quant.min !== 0) {
return false;
}
const consumed = (0, regexp_ast_analysis_1.getFirstConsumedChar)(quant.element, dir, flags);
const look = regexp_ast_analysis_1.FirstConsumedChars.toLook(consumed);
if (disjoint(assertionLook, look)) {
context.report({
node,
loc: getRegexpLocation(quant),
messageId: "cannotEnterQuantifier",
data: {
assertion: (0, mention_1.mention)(assertion),
quant: (0, mention_1.mention)(quant),
},
suggest: [
{
messageId: "removeQuantifier",
fix: fixReplaceNode(quant, ""),
},
],
});
return true;
}
const after = (0, regexp_ast_analysis_1.getFirstCharAfter)(quant, dir, flags);
if (disjoint(assertionLook, after)) {
const newQuant = (0, regexp_ast_1.quantToString)({ ...quant, min: 1 });
context.report({
node,
loc: getRegexpLocation(quant),
messageId: "alwaysEnterQuantifier",
data: {
assertion: (0, mention_1.mention)(assertion),
quant: (0, mention_1.mention)(quant),
newQuant,
},
suggest: [
{
messageId: "changeQuantifier",
data: { newQuant },
fix: fixReplaceQuant(quant, {
min: 1,
max: quant.max,
}),
},
],
});
return true;
}
return false;
}
function contradicts(element) {
if (element.type === "Alternative") {
return contradictsAlternative(element);
}
else if (element.type === "Quantifier") {
return contradictsQuantifier(element);
}
return false;
}
}
return {
onAssertionEnter(assertion) {
analyseAssertion(assertion, "ltr");
analyseAssertion(assertion, "rtl");
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,84 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const unicode_1 = require("../utils/unicode");
const CONTROL_CHARS = new Map([
[0, "\\0"],
[unicode_1.CP_TAB, "\\t"],
[unicode_1.CP_LF, "\\n"],
[unicode_1.CP_VT, "\\v"],
[unicode_1.CP_FF, "\\f"],
[unicode_1.CP_CR, "\\r"],
]);
const ALLOWED_CONTROL_CHARS = /^\\[0fnrtv]$/u;
exports.default = (0, utils_1.createRule)("no-control-character", {
meta: {
docs: {
description: "disallow control characters",
category: "Possible Errors",
recommended: false,
},
schema: [],
messages: {
unexpected: "Unexpected control character {{ char }}.",
escape: "Use {{ escape }} instead.",
},
type: "suggestion",
hasSuggestions: true,
},
create(context) {
function createVisitor(regexpContext) {
const { node, patternSource, getRegexpLocation, fixReplaceNode } = regexpContext;
function isBadEscapeRaw(raw, cp) {
return (raw.codePointAt(0) === cp ||
raw.startsWith("\\x") ||
raw.startsWith("\\u"));
}
function isAllowedEscapeRaw(raw) {
return (ALLOWED_CONTROL_CHARS.test(raw) ||
(raw.startsWith("\\") &&
ALLOWED_CONTROL_CHARS.test(raw.slice(1))));
}
function isBadEscape(char) {
var _a;
const range = (_a = patternSource.getReplaceRange(char)) === null || _a === void 0 ? void 0 : _a.range;
const sourceRaw = range
? context.sourceCode.text.slice(...range)
: char.raw;
if (isAllowedEscapeRaw(char.raw) ||
isAllowedEscapeRaw(sourceRaw)) {
return false;
}
return (isBadEscapeRaw(char.raw, char.value) ||
(char.raw.startsWith("\\") &&
isBadEscapeRaw(char.raw.slice(1), char.value)));
}
return {
onCharacterEnter(cNode) {
if (cNode.value <= 0x1f && isBadEscape(cNode)) {
const suggest = [];
const allowedEscape = CONTROL_CHARS.get(cNode.value);
if (allowedEscape !== undefined) {
suggest.push({
messageId: "escape",
data: { escape: (0, mention_1.mention)(allowedEscape) },
fix: fixReplaceNode(cNode, allowedEscape),
});
}
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
data: { char: (0, mention_1.mentionChar)(cNode) },
suggest,
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,228 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const refa_2 = require("../utils/refa");
const util_1 = require("../utils/util");
function groupElements(elements, flags) {
const duplicates = [];
const characters = new Map();
const characterRanges = new Map();
const characterSetAndClasses = new Map();
function addToGroup(group, key, element) {
const current = group.get(key);
if (current !== undefined) {
duplicates.push({ element: current, duplicate: element });
}
else {
group.set(key, element);
}
}
for (const e of elements) {
if (e.type === "Character") {
const charSet = (0, regexp_ast_analysis_1.toCharSet)(e, flags);
const key = charSet.ranges[0].min;
addToGroup(characters, key, e);
}
else if (e.type === "CharacterClassRange") {
const charSet = (0, regexp_ast_analysis_1.toCharSet)(e, flags);
const key = buildRangeKey(charSet);
addToGroup(characterRanges, key, e);
}
else if (e.type === "CharacterSet" ||
e.type === "CharacterClass" ||
e.type === "ClassStringDisjunction" ||
e.type === "ExpressionCharacterClass") {
const key = e.raw;
addToGroup(characterSetAndClasses, key, e);
}
else {
(0, util_1.assertNever)(e);
}
}
return {
duplicates,
characters: [...characters.values()],
characterRanges: [...characterRanges.values()],
characterSetAndClasses: [...characterSetAndClasses.values()],
};
function buildRangeKey(rangeCharSet) {
return rangeCharSet.ranges
.map((r) => String.fromCodePoint(r.min, r.max))
.join(",");
}
}
function inRange({ min, max }, char) {
return min <= char && char <= max;
}
exports.default = (0, utils_1.createRule)("no-dupe-characters-character-class", {
meta: {
type: "suggestion",
docs: {
description: "disallow duplicate characters in the RegExp character class",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
duplicate: "Unexpected duplicate {{duplicate}}.",
duplicateNonObvious: "Unexpected duplicate. {{duplicate}} is a duplicate of {{element}}.",
subset: "{{subsetElement}} is already included in {{element}}.",
subsetOfMany: "{{subsetElement}} is already included by the elements {{elements}}.",
overlap: "Unexpected overlap of {{elementA}} and {{elementB}} was found '{{overlap}}'.",
},
},
create(context) {
function reportDuplicate(regexpContext, duplicate, element) {
const { node, getRegexpLocation } = regexpContext;
if (duplicate.raw === element.raw) {
context.report({
node,
loc: getRegexpLocation(duplicate),
messageId: "duplicate",
data: {
duplicate: (0, mention_1.mentionChar)(duplicate),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, duplicate),
});
}
else {
context.report({
node,
loc: getRegexpLocation(duplicate),
messageId: "duplicateNonObvious",
data: {
duplicate: (0, mention_1.mentionChar)(duplicate),
element: (0, mention_1.mentionChar)(element),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, duplicate),
});
}
}
function reportOverlap({ node, getRegexpLocation }, element, intersectElement, overlap) {
context.report({
node,
loc: getRegexpLocation(element),
messageId: "overlap",
data: {
elementA: (0, mention_1.mentionChar)(element),
elementB: (0, mention_1.mentionChar)(intersectElement),
overlap,
},
});
}
function reportSubset(regexpContext, subsetElement, element) {
const { node, getRegexpLocation } = regexpContext;
context.report({
node,
loc: getRegexpLocation(subsetElement),
messageId: "subset",
data: {
subsetElement: (0, mention_1.mentionChar)(subsetElement),
element: (0, mention_1.mentionChar)(element),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, subsetElement),
});
}
function reportSubsetOfMany(regexpContext, subsetElement, elements) {
const { node, getRegexpLocation } = regexpContext;
context.report({
node,
loc: getRegexpLocation(subsetElement),
messageId: "subsetOfMany",
data: {
subsetElement: (0, mention_1.mentionChar)(subsetElement),
elements: `'${elements
.map((e) => e.raw)
.join("")}' (${elements.map(mention_1.mentionChar).join(", ")})`,
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, subsetElement),
});
}
function createVisitor(regexpContext) {
const { flags } = regexpContext;
return {
onCharacterClassEnter(ccNode) {
const { duplicates, characters, characterRanges, characterSetAndClasses, } = groupElements(ccNode.elements, flags);
const elementsOtherThanCharacter = [
...characterRanges,
...characterSetAndClasses,
];
const subsets = new Set();
for (const { element, duplicate } of duplicates) {
reportDuplicate(regexpContext, duplicate, element);
subsets.add(duplicate);
}
for (const char of characters) {
for (const other of elementsOtherThanCharacter) {
if ((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags).chars.has(char.value)) {
reportSubset(regexpContext, char, other);
subsets.add(char);
break;
}
}
}
for (const element of elementsOtherThanCharacter) {
for (const other of elementsOtherThanCharacter) {
if (element === other || subsets.has(other)) {
continue;
}
if ((0, regexp_ast_analysis_1.toUnicodeSet)(element, flags).isSubsetOf((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags))) {
reportSubset(regexpContext, element, other);
subsets.add(element);
break;
}
}
}
const characterTotal = (0, regexp_ast_analysis_1.toUnicodeSet)(characters.filter((c) => !subsets.has(c)), flags);
for (const element of elementsOtherThanCharacter) {
if (subsets.has(element)) {
continue;
}
const totalOthers = characterTotal.union(...elementsOtherThanCharacter
.filter((e) => !subsets.has(e) && e !== element)
.map((e) => (0, regexp_ast_analysis_1.toUnicodeSet)(e, flags)));
const elementCharSet = (0, regexp_ast_analysis_1.toUnicodeSet)(element, flags);
if (elementCharSet.isSubsetOf(totalOthers)) {
const superSetElements = ccNode.elements
.filter((e) => !subsets.has(e) && e !== element)
.filter((e) => !(0, regexp_ast_analysis_1.toUnicodeSet)(e, flags).isDisjointWith(elementCharSet));
reportSubsetOfMany(regexpContext, element, superSetElements);
subsets.add(element);
}
}
for (let i = 0; i < characterRanges.length; i++) {
const range = characterRanges[i];
if (subsets.has(range)) {
continue;
}
for (let j = i + 1; j < elementsOtherThanCharacter.length; j++) {
const other = elementsOtherThanCharacter[j];
if (range === other || subsets.has(other)) {
continue;
}
const intersection = (0, regexp_ast_analysis_1.toUnicodeSet)(range, flags).intersect((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags));
if (intersection.isEmpty) {
continue;
}
const interestingRanges = intersection.chars.ranges.filter((r) => inRange(r, range.min.value) ||
inRange(r, range.max.value));
(0, refa_2.assertValidFlags)(flags);
const interest = refa_1.JS.createCharSet(interestingRanges, flags);
if (!interest.isEmpty) {
reportOverlap(regexpContext, range, other, (0, refa_2.toCharSetSource)(interest, flags));
break;
}
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,863 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const char_ranges_1 = require("../utils/char-ranges");
const get_usage_of_pattern_1 = require("../utils/get-usage-of-pattern");
const mention_1 = require("../utils/mention");
const partial_parser_1 = require("../utils/partial-parser");
const refa_2 = require("../utils/refa");
const regexp_ast_1 = require("../utils/regexp-ast");
const util_1 = require("../utils/util");
function isStared(node) {
let max = (0, regexp_ast_analysis_1.getEffectiveMaximumRepetition)(node);
if (node.type === "Quantifier") {
max *= node.max;
}
return max > 10;
}
function hasNothingAfterNode(node) {
const md = (0, regexp_ast_analysis_1.getMatchingDirection)(node);
for (let p = node;; p = p.parent) {
if (p.type === "Assertion" || p.type === "Pattern") {
return true;
}
if (p.type !== "Alternative") {
const parent = p.parent;
if (parent.type === "Quantifier") {
if (parent.max > 1) {
return false;
}
}
else {
const lastIndex = md === "ltr" ? parent.elements.length - 1 : 0;
if (parent.elements[lastIndex] !== p) {
return false;
}
}
}
}
}
function containsAssertions(expression) {
try {
(0, refa_1.visitAst)(expression, {
onAssertionEnter() {
throw new Error();
},
});
return false;
}
catch (_a) {
return true;
}
}
function containsAssertionsOrUnknowns(expression) {
try {
(0, refa_1.visitAst)(expression, {
onAssertionEnter() {
throw new Error();
},
onUnknownEnter() {
throw new Error();
},
});
return false;
}
catch (_a) {
return true;
}
}
function isNonRegular(node) {
return (0, regexp_ast_analysis_1.hasSomeDescendant)(node, (d) => d.type === "Assertion" || d.type === "Backreference");
}
function toNFA(parser, element) {
try {
const { expression, maxCharacter } = parser.parseElement(element, {
backreferences: "unknown",
assertions: "parse",
});
let e;
if (containsAssertions(expression)) {
e = (0, refa_1.transform)(refa_1.Transformers.simplify({
ignoreAmbiguity: true,
ignoreOrder: true,
}), expression);
}
else {
e = expression;
}
return {
nfa: refa_1.NFA.fromRegex(e, { maxCharacter }, { assertions: "disable", unknowns: "disable" }),
partial: containsAssertionsOrUnknowns(e),
};
}
catch (_a) {
return {
nfa: refa_1.NFA.empty({
maxCharacter: parser.maxCharacter,
}),
partial: true,
};
}
}
function* iterateNestedAlternatives(alternative) {
for (const e of alternative.elements) {
if (e.type === "Group" || e.type === "CapturingGroup") {
for (const a of e.alternatives) {
if (e.alternatives.length > 1) {
yield a;
}
yield* iterateNestedAlternatives(a);
}
}
if (e.type === "CharacterClass" && !e.negate) {
const nested = [];
const addToNested = (charElement) => {
switch (charElement.type) {
case "CharacterClassRange": {
const min = charElement.min;
const max = charElement.max;
if (min.value === max.value) {
nested.push(charElement);
}
else if (min.value + 1 === max.value) {
nested.push(min, max);
}
else {
nested.push(charElement, min, max);
}
break;
}
case "ClassStringDisjunction": {
nested.push(...charElement.alternatives);
break;
}
case "CharacterClass": {
if (!charElement.negate) {
charElement.elements.forEach(addToNested);
}
else {
nested.push(charElement);
}
break;
}
case "Character":
case "CharacterSet":
case "ExpressionCharacterClass": {
nested.push(charElement);
break;
}
default:
throw (0, util_1.assertNever)(charElement);
}
};
e.elements.forEach(addToNested);
if (nested.length > 1)
yield* nested;
}
}
}
function* iteratePartialAlternatives(alternative, parser) {
if (isNonRegular(alternative)) {
return;
}
const maxCharacter = parser.maxCharacter;
const partialParser = new partial_parser_1.PartialParser(parser, {
assertions: "throw",
backreferences: "throw",
});
for (const nested of iterateNestedAlternatives(alternative)) {
try {
const expression = partialParser.parse(alternative, nested);
const nfa = refa_1.NFA.fromRegex(expression, { maxCharacter });
yield { nested, nfa };
}
catch (_a) {
}
}
}
function unionAll(nfas) {
if (nfas.length === 0) {
throw new Error("Cannot union 0 NFAs.");
}
else if (nfas.length === 1) {
return nfas[0];
}
const total = nfas[0].copy();
for (let i = 1; i < nfas.length; i++) {
total.union(nfas[i]);
}
return total;
}
const MAX_DFA_NODES = 100000;
function isSubsetOf(superset, subset) {
try {
const a = refa_1.DFA.fromIntersection(superset, subset, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
const b = refa_1.DFA.fromFA(subset, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
a.minimize();
b.minimize();
return a.structurallyEqual(b);
}
catch (_a) {
return null;
}
}
function getSubsetRelation(left, right) {
try {
const inter = refa_1.DFA.fromIntersection(left, right, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
inter.minimize();
const l = refa_1.DFA.fromFA(left, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
l.minimize();
const r = refa_1.DFA.fromFA(right, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
r.minimize();
const subset = l.structurallyEqual(inter);
const superset = r.structurallyEqual(inter);
if (subset && superset) {
return 1;
}
else if (subset) {
return 2;
}
else if (superset) {
return 3;
}
return 0;
}
catch (_a) {
return 4;
}
}
function getPartialSubsetRelation(left, leftIsPartial, right, rightIsPartial) {
const relation = getSubsetRelation(left, right);
if (!leftIsPartial && !rightIsPartial) {
return relation;
}
if (relation === 0 ||
relation === 4) {
return relation;
}
if (leftIsPartial && !rightIsPartial) {
switch (relation) {
case 1:
return 3;
case 2:
return 0;
case 3:
return 3;
default:
return (0, util_1.assertNever)(relation);
}
}
if (rightIsPartial && !leftIsPartial) {
switch (relation) {
case 1:
return 2;
case 2:
return 2;
case 3:
return 0;
default:
return (0, util_1.assertNever)(relation);
}
}
return 0;
}
function faToSource(fa, flags) {
try {
(0, refa_2.assertValidFlags)(flags);
return refa_1.JS.toLiteral(fa.toRegex(), { flags }).source;
}
catch (_a) {
return "<ERROR>";
}
}
function* findDuplicationAstFast(alternatives, flags) {
const shortCircuit = (a) => {
return a.type === "CapturingGroup" ? false : null;
};
for (let i = 0; i < alternatives.length; i++) {
const alternative = alternatives[i];
for (let j = 0; j < i; j++) {
const other = alternatives[j];
if ((0, regexp_ast_1.isEqualNodes)(other, alternative, flags, shortCircuit)) {
yield { type: "Duplicate", alternative, others: [other] };
}
}
}
}
function* findDuplicationAst(alternatives, flags, hasNothingAfter) {
const isCoveredOptions = {
flags,
canOmitRight: hasNothingAfter,
};
const isCoveredOptionsNoPrefix = {
flags,
canOmitRight: false,
};
for (let i = 0; i < alternatives.length; i++) {
const alternative = alternatives[i];
for (let j = 0; j < i; j++) {
const other = alternatives[j];
if ((0, regexp_ast_1.isCoveredNode)(other, alternative, isCoveredOptions)) {
if ((0, regexp_ast_1.isEqualNodes)(other, alternative, flags)) {
yield {
type: "Duplicate",
alternative,
others: [other],
};
}
else if (hasNothingAfter &&
!(0, regexp_ast_1.isCoveredNode)(other, alternative, isCoveredOptionsNoPrefix)) {
yield {
type: "PrefixSubset",
alternative,
others: [other],
};
}
else {
yield { type: "Subset", alternative, others: [other] };
}
}
}
}
}
function* findPrefixDuplicationNfa(alternatives, parser) {
if (alternatives.length === 0) {
return;
}
const all = refa_1.NFA.all({ maxCharacter: alternatives[0][0].maxCharacter });
for (let i = 0; i < alternatives.length; i++) {
const [nfa, partial, alternative] = alternatives[i];
if (!partial) {
const overlapping = alternatives
.slice(0, i)
.filter(([otherNfa]) => !(0, refa_1.isDisjointWith)(nfa, otherNfa));
if (overlapping.length >= 1) {
const othersNfa = unionAll(overlapping.map(([n]) => n));
const others = overlapping.map(([, , a]) => a);
if (isSubsetOf(othersNfa, nfa)) {
yield { type: "PrefixSubset", alternative, others };
}
else {
const nested = tryFindNestedSubsetResult(overlapping.map((o) => [o[0], o[2]]), othersNfa, alternative, parser);
if (nested) {
yield { ...nested, type: "PrefixNestedSubset" };
}
}
}
}
nfa.append(all);
}
}
function* findDuplicationNfa(alternatives, flags, { hasNothingAfter, parser, ignoreOverlap }) {
const previous = [];
for (let i = 0; i < alternatives.length; i++) {
const alternative = alternatives[i];
const { nfa, partial } = toNFA(parser, alternative);
const overlapping = previous.filter(([otherNfa]) => !(0, refa_1.isDisjointWith)(nfa, otherNfa));
if (overlapping.length >= 1) {
const othersNfa = unionAll(overlapping.map(([n]) => n));
const othersPartial = overlapping.some(([, p]) => p);
const others = overlapping.map(([, , a]) => a);
const relation = getPartialSubsetRelation(nfa, partial, othersNfa, othersPartial);
switch (relation) {
case 1:
if (others.length === 1) {
yield {
type: "Duplicate",
alternative,
others: [others[0]],
};
}
else {
yield { type: "Subset", alternative, others };
}
break;
case 2:
yield { type: "Subset", alternative, others };
break;
case 3: {
const reorder = (0, regexp_ast_analysis_1.canReorder)([alternative, ...others], flags);
if (reorder) {
for (const other of others) {
yield {
type: "Subset",
alternative: other,
others: [alternative],
};
}
}
else {
yield { type: "Superset", alternative, others };
}
break;
}
case 0:
case 4: {
const nested = tryFindNestedSubsetResult(overlapping.map((o) => [o[0], o[2]]), othersNfa, alternative, parser);
if (nested) {
yield nested;
break;
}
if (!ignoreOverlap) {
yield {
type: "Overlap",
alternative,
others,
overlap: refa_1.NFA.fromIntersection(nfa, othersNfa),
};
}
break;
}
default:
throw (0, util_1.assertNever)(relation);
}
}
previous.push([nfa, partial, alternative]);
}
if (hasNothingAfter) {
yield* findPrefixDuplicationNfa(previous, parser);
}
}
function tryFindNestedSubsetResult(others, othersNfa, alternative, parser) {
const disjointElements = new Set();
for (const { nested, nfa: nestedNfa } of iteratePartialAlternatives(alternative, parser)) {
if ((0, regexp_ast_analysis_1.hasSomeAncestor)(nested, (a) => disjointElements.has(a))) {
continue;
}
if ((0, refa_1.isDisjointWith)(othersNfa, nestedNfa)) {
disjointElements.add(nested);
continue;
}
if (isSubsetOf(othersNfa, nestedNfa)) {
return {
type: "NestedSubset",
alternative,
nested,
others: others
.filter((o) => !(0, refa_1.isDisjointWith)(o[0], nestedNfa))
.map((o) => o[1]),
};
}
}
return undefined;
}
function* findDuplication(alternatives, flags, options) {
if (options.fastAst) {
yield* findDuplicationAstFast(alternatives, flags);
}
else {
yield* findDuplicationAst(alternatives, flags, options.hasNothingAfter);
}
if (!options.noNfa) {
yield* findDuplicationNfa(alternatives, flags, options);
}
}
const RESULT_TYPE_ORDER = [
"Duplicate",
"Subset",
"NestedSubset",
"PrefixSubset",
"PrefixNestedSubset",
"Superset",
"Overlap",
];
function deduplicateResults(unsorted, { reportExp }) {
const results = [...unsorted].sort((a, b) => RESULT_TYPE_ORDER.indexOf(a.type) -
RESULT_TYPE_ORDER.indexOf(b.type));
const seen = new Map();
return results.filter(({ alternative, type }) => {
const firstSeen = seen.get(alternative);
if (firstSeen === undefined) {
seen.set(alternative, type);
return true;
}
if (reportExp &&
firstSeen === "PrefixSubset" &&
type !== "PrefixSubset") {
seen.set(alternative, type);
return true;
}
return false;
});
}
function mentionNested(nested) {
if (nested.type === "Alternative" || nested.type === "StringAlternative") {
return (0, mention_1.mention)(nested);
}
return (0, mention_1.mentionChar)(nested);
}
function fixRemoveNestedAlternative(context, alternative) {
switch (alternative.type) {
case "Alternative":
return (0, utils_1.fixRemoveAlternative)(context, alternative);
case "StringAlternative":
return (0, utils_1.fixRemoveStringAlternative)(context, alternative);
case "Character":
case "CharacterClassRange":
case "CharacterSet":
case "CharacterClass":
case "ExpressionCharacterClass":
case "ClassStringDisjunction": {
if (alternative.parent.type !== "CharacterClass") {
return () => null;
}
return (0, utils_1.fixRemoveCharacterClassElement)(context, alternative);
}
default:
throw (0, util_1.assertNever)(alternative);
}
}
exports.default = (0, utils_1.createRule)("no-dupe-disjunctions", {
meta: {
docs: {
description: "disallow duplicate disjunctions",
category: "Possible Errors",
recommended: true,
},
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
report: {
type: "string",
enum: ["all", "trivial", "interesting"],
},
reportExponentialBacktracking: {
enum: ["none", "certain", "potential"],
},
reportUnreachable: {
enum: ["certain", "potential"],
},
},
additionalProperties: false,
},
],
messages: {
duplicate: "Unexpected duplicate alternative. This alternative can be removed.{{cap}}{{exp}}",
subset: "Unexpected useless alternative. This alternative is a strict subset of {{others}} and can be removed.{{cap}}{{exp}}",
nestedSubset: "Unexpected useless element. All paths of {{root}} that go through {{nested}} are a strict subset of {{others}}. This element can be removed.{{cap}}{{exp}}",
prefixSubset: "Unexpected useless alternative. This alternative is already covered by {{others}} and can be removed.{{cap}}",
prefixNestedSubset: "Unexpected useless element. All paths of {{root}} that go through {{nested}} are already covered by {{others}}. This element can be removed.{{cap}}",
superset: "Unexpected superset. This alternative is a superset of {{others}}. It might be possible to remove the other alternative(s).{{cap}}{{exp}}",
overlap: "Unexpected overlap. This alternative overlaps with {{others}}. The overlap is {{expr}}.{{cap}}{{exp}}",
remove: "Remove the {{alternative}} {{type}}.",
replaceRange: "Replace {{range}} with {{replacement}}.",
},
type: "suggestion",
},
create(context) {
var _a, _b, _c, _d, _e, _f;
const reportExponentialBacktracking = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.reportExponentialBacktracking) !== null && _b !== void 0 ? _b : "potential";
const reportUnreachable = (_d = (_c = context.options[0]) === null || _c === void 0 ? void 0 : _c.reportUnreachable) !== null && _d !== void 0 ? _d : "certain";
const report = (_f = (_e = context.options[0]) === null || _e === void 0 ? void 0 : _e.report) !== null && _f !== void 0 ? _f : "trivial";
const allowedRanges = (0, char_ranges_1.getAllowedCharRanges)(undefined, context);
function createVisitor(regexpContext) {
const { flags, node, getRegexpLocation, getUsageOfPattern } = regexpContext;
const parser = (0, refa_2.getParser)(regexpContext);
function getFilterInfo(parentNode) {
const usage = getUsageOfPattern();
let stared;
if (isStared(parentNode)) {
stared = 1;
}
else if (usage === get_usage_of_pattern_1.UsageOfPattern.partial ||
usage === get_usage_of_pattern_1.UsageOfPattern.mixed) {
stared = 2;
}
else {
stared = 0;
}
let nothingAfter;
if (!hasNothingAfterNode(parentNode)) {
nothingAfter = 0;
}
else if (usage === get_usage_of_pattern_1.UsageOfPattern.partial ||
usage === get_usage_of_pattern_1.UsageOfPattern.mixed) {
nothingAfter = 2;
}
else {
nothingAfter = 1;
}
let reportExp;
switch (reportExponentialBacktracking) {
case "none":
reportExp = false;
break;
case "certain":
reportExp = stared === 1;
break;
case "potential":
reportExp = stared !== 0;
break;
default:
(0, util_1.assertNever)(reportExponentialBacktracking);
}
let reportPrefix;
switch (reportUnreachable) {
case "certain":
reportPrefix = nothingAfter === 1;
break;
case "potential":
reportPrefix = nothingAfter !== 0;
break;
default:
(0, util_1.assertNever)(reportUnreachable);
}
return { stared, nothingAfter, reportExp, reportPrefix };
}
function verify(parentNode) {
const info = getFilterInfo(parentNode);
const rawResults = findDuplication(parentNode.alternatives, flags, {
fastAst: false,
noNfa: false,
ignoreOverlap: !info.reportExp && report !== "all",
hasNothingAfter: info.reportPrefix,
parser,
});
let results = filterResults([...rawResults], info);
results = deduplicateResults(results, info);
results.forEach((result) => reportResult(result, info));
}
function filterResults(results, { nothingAfter, reportExp, reportPrefix }) {
switch (report) {
case "all": {
return results;
}
case "trivial": {
return results.filter(({ type }) => {
switch (type) {
case "Duplicate":
case "Subset":
case "NestedSubset":
return true;
case "Overlap":
case "Superset":
return reportExp;
case "PrefixSubset":
case "PrefixNestedSubset":
return reportPrefix;
default:
throw (0, util_1.assertNever)(type);
}
});
}
case "interesting": {
return results.filter(({ type }) => {
switch (type) {
case "Duplicate":
case "Subset":
case "NestedSubset":
return true;
case "Overlap":
return reportExp;
case "Superset":
return (reportExp ||
nothingAfter === 0);
case "PrefixSubset":
case "PrefixNestedSubset":
return reportPrefix;
default:
throw (0, util_1.assertNever)(type);
}
});
}
default:
throw (0, util_1.assertNever)(report);
}
}
function printChar(char) {
if ((0, char_ranges_1.inRange)(allowedRanges, char)) {
return String.fromCodePoint(char);
}
if (char === 0)
return "\\0";
if (char <= 0xff)
return `\\x${char.toString(16).padStart(2, "0")}`;
if (char <= 0xffff)
return `\\u${char.toString(16).padStart(4, "0")}`;
return `\\u{${char.toString(16)}}`;
}
function getSuggestions(result) {
if (result.type === "Overlap" || result.type === "Superset") {
return [];
}
const alternative = result.type === "NestedSubset" ||
result.type === "PrefixNestedSubset"
? result.nested
: result.alternative;
const containsCapturingGroup = (0, regexp_ast_analysis_1.hasSomeDescendant)(alternative, (d) => d.type === "CapturingGroup");
if (containsCapturingGroup) {
return [];
}
if (alternative.type === "Character" &&
alternative.parent.type === "CharacterClassRange") {
const range = alternative.parent;
let replacement;
if (range.min.value + 1 === range.max.value) {
replacement =
range.min === alternative
? range.max.raw
: range.min.raw;
}
else {
if (range.min === alternative) {
const min = printChar(range.min.value + 1);
replacement = `${min}-${range.max.raw}`;
}
else {
const max = printChar(range.max.value - 1);
replacement = `${range.min.raw}-${max}`;
}
}
return [
{
messageId: "replaceRange",
data: {
range: (0, mention_1.mentionChar)(range),
replacement: (0, mention_1.mention)(replacement),
},
fix: regexpContext.fixReplaceNode(range, replacement),
},
];
}
return [
{
messageId: "remove",
data: {
alternative: mentionNested(alternative),
type: alternative.type === "Alternative"
? "alternative"
: "element",
},
fix: fixRemoveNestedAlternative(regexpContext, alternative),
},
];
}
function reportResult(result, { stared }) {
let exp;
if (stared === 1) {
exp =
" This ambiguity is likely to cause exponential backtracking.";
}
else if (stared === 2) {
exp =
" This ambiguity might cause exponential backtracking.";
}
else {
exp = "";
}
const reportAlternative = result.type === "NestedSubset" ||
result.type === "PrefixNestedSubset"
? result.nested
: result.alternative;
const loc = getRegexpLocation(reportAlternative);
const cap = (0, regexp_ast_analysis_1.hasSomeDescendant)(reportAlternative, (d) => d.type === "CapturingGroup")
? " Careful! This alternative contains capturing groups which might be difficult to remove."
: "";
const others = (0, mention_1.mention)(result.others.map((a) => a.raw).join("|"));
const suggest = getSuggestions(result);
switch (result.type) {
case "Duplicate":
context.report({
node,
loc,
messageId: "duplicate",
data: { exp, cap, others },
suggest,
});
break;
case "Subset":
context.report({
node,
loc,
messageId: "subset",
data: { exp, cap, others },
suggest,
});
break;
case "NestedSubset":
context.report({
node,
loc,
messageId: "nestedSubset",
data: {
exp,
cap,
others,
root: (0, mention_1.mention)(result.alternative),
nested: mentionNested(result.nested),
},
suggest,
});
break;
case "PrefixSubset":
context.report({
node,
loc,
messageId: "prefixSubset",
data: { exp, cap, others },
suggest,
});
break;
case "PrefixNestedSubset":
context.report({
node,
loc,
messageId: "prefixNestedSubset",
data: {
exp,
cap,
others,
root: (0, mention_1.mention)(result.alternative),
nested: mentionNested(result.nested),
},
suggest,
});
break;
case "Superset":
context.report({
node,
loc,
messageId: "superset",
data: { exp, cap, others },
suggest,
});
break;
case "Overlap":
context.report({
node,
loc,
messageId: "overlap",
data: {
exp,
cap,
others,
expr: (0, mention_1.mention)(faToSource(result.overlap, flags)),
},
suggest,
});
break;
default:
throw (0, util_1.assertNever)(result);
}
}
return {
onPatternEnter: verify,
onGroupEnter: verify,
onCapturingGroupEnter: verify,
onAssertionEnter(aNode) {
if (aNode.kind === "lookahead" ||
aNode.kind === "lookbehind") {
verify(aNode);
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,101 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
function getCapturingGroupOuterSource(node) {
const first = node.alternatives[0];
const last = node.alternatives[node.alternatives.length - 1];
const innerStart = first.start - node.start;
const innerEnd = last.end - node.start;
return [node.raw.slice(0, innerStart), node.raw.slice(innerEnd)];
}
function getFixedNode(regexpNode, alt) {
var _a;
let quant;
if (regexpNode.alternatives.at(0) === alt) {
quant = "??";
}
else if (regexpNode.alternatives.at(-1) === alt) {
quant = "?";
}
else {
return null;
}
const innerAlternatives = regexpNode.alternatives
.filter((a) => a !== alt)
.map((a) => a.raw)
.join("|");
let replacement = `(?:${innerAlternatives})${quant}`;
if (regexpNode.type === "CapturingGroup") {
const [before, after] = getCapturingGroupOuterSource(regexpNode);
replacement = `${before}${replacement}${after}`;
}
else if (((_a = regexpNode.parent) === null || _a === void 0 ? void 0 : _a.type) === "Quantifier") {
replacement = `(?:${replacement})`;
}
return replacement;
}
exports.default = (0, utils_1.createRule)("no-empty-alternative", {
meta: {
docs: {
description: "disallow alternatives without elements",
category: "Possible Errors",
recommended: true,
default: "warn",
},
schema: [],
hasSuggestions: true,
messages: {
empty: "This empty alternative might be a mistake. If not, use a quantifier instead.",
suggest: "Use a quantifier instead.",
},
type: "problem",
},
create(context) {
function createVisitor({ node, getRegexpLocation, fixReplaceNode, }) {
function verifyAlternatives(regexpNode, suggestFixer) {
if (regexpNode.alternatives.length >= 2) {
for (let i = 0; i < regexpNode.alternatives.length; i++) {
const alt = regexpNode.alternatives[i];
const isLast = i === regexpNode.alternatives.length - 1;
if (alt.elements.length === 0) {
const index = alt.start;
const loc = isLast
? getRegexpLocation({
start: index - 1,
end: index,
})
: getRegexpLocation({
start: index,
end: index + 1,
});
const fixed = suggestFixer(alt);
context.report({
node,
loc,
messageId: "empty",
suggest: fixed
? [
{
messageId: "suggest",
fix: fixReplaceNode(regexpNode, fixed),
},
]
: undefined,
});
return;
}
}
}
}
return {
onGroupEnter: (gNode) => verifyAlternatives(gNode, (alt) => getFixedNode(gNode, alt)),
onCapturingGroupEnter: (cgNode) => verifyAlternatives(cgNode, (alt) => getFixedNode(cgNode, alt)),
onPatternEnter: (pNode) => verifyAlternatives(pNode, (alt) => getFixedNode(pNode, alt)),
onClassStringDisjunctionEnter: (csdNode) => verifyAlternatives(csdNode, () => null),
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-capturing-group", {
meta: {
docs: {
description: "disallow capturing group that captures empty.",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected capture empty.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onCapturingGroupEnter(cgNode) {
if ((0, regexp_ast_analysis_1.isZeroLength)(cgNode, flags)) {
context.report({
node,
loc: getRegexpLocation(cgNode),
messageId: "unexpected",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-character-class", {
meta: {
docs: {
description: "disallow character classes that match no characters",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
empty: "This character class matches no characters because it is empty.",
cannotMatchAny: "This character class cannot match any characters.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
const { node, getRegexpLocation, flags } = regexpContext;
return {
onCharacterClassEnter(ccNode) {
if ((0, regexp_ast_analysis_1.matchesNoCharacters)(ccNode, flags)) {
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId: ccNode.elements.length
? "cannotMatchAny"
: "empty",
});
}
},
onExpressionCharacterClassEnter(ccNode) {
if ((0, regexp_ast_analysis_1.matchesNoCharacters)(ccNode, flags)) {
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId: "cannotMatchAny",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-group", {
meta: {
docs: {
description: "disallow empty group",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected empty group.",
},
type: "suggestion",
},
create(context) {
function verifyGroup({ node, getRegexpLocation }, gNode) {
if (gNode.alternatives.every((alt) => alt.elements.length === 0)) {
context.report({
node,
loc: getRegexpLocation(gNode),
messageId: "unexpected",
});
}
}
function createVisitor(regexpContext) {
return {
onGroupEnter(gNode) {
verifyGroup(regexpContext, gNode);
},
onCapturingGroupEnter(cgNode) {
verifyGroup(regexpContext, cgNode);
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-lookarounds-assertion", {
meta: {
docs: {
description: "disallow empty lookahead assertion or empty lookbehind assertion",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected empty {{kind}}. It will trivially {{result}} all inputs.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onAssertionEnter(aNode) {
if (aNode.kind !== "lookahead" &&
aNode.kind !== "lookbehind") {
return;
}
if ((0, regexp_ast_analysis_1.isPotentiallyEmpty)(aNode.alternatives, flags)) {
context.report({
node,
loc: getRegexpLocation(aNode),
messageId: "unexpected",
data: {
kind: aNode.kind,
result: aNode.negate ? "reject" : "accept",
},
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-string-literal", {
meta: {
docs: {
description: "disallow empty string literals in character classes",
category: "Best Practices",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected empty string literal.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
const { node, getRegexpLocation } = regexpContext;
return {
onClassStringDisjunctionEnter(csdNode) {
if (csdNode.alternatives.every((alt) => alt.elements.length === 0)) {
context.report({
node,
loc: getRegexpLocation(csdNode),
messageId: "unexpected",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-escape-backspace", {
meta: {
docs: {
description: "disallow escape backspace (`[\\b]`)",
category: "Possible Errors",
recommended: true,
},
schema: [],
hasSuggestions: true,
messages: {
unexpected: "Unexpected '[\\b]'. Use '\\u0008' instead.",
suggest: "Use '\\u0008'.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, getRegexpLocation, fixReplaceNode, }) {
return {
onCharacterEnter(cNode) {
if (cNode.value === utils_1.CP_BACKSPACE && cNode.raw === "\\b") {
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
suggest: [
{
messageId: "suggest",
fix: fixReplaceNode(cNode, "\\u0008"),
},
],
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-extra-lookaround-assertions", {
meta: {
docs: {
description: "disallow unnecessary nested lookaround assertions",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
canBeInlined: "This {{kind}} assertion is useless and can be inlined.",
canBeConvertedIntoGroup: "This {{kind}} assertion is useless and can be converted into a group.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
return {
onAssertionEnter(aNode) {
if (aNode.kind === "lookahead" ||
aNode.kind === "lookbehind") {
verify(regexpContext, aNode);
}
},
};
}
function verify(regexpContext, assertion) {
for (const alternative of assertion.alternatives) {
const nested = alternative.elements.at(assertion.kind === "lookahead"
?
-1
:
0);
if ((nested === null || nested === void 0 ? void 0 : nested.type) === "Assertion" &&
nested.kind === assertion.kind &&
!nested.negate) {
reportLookaroundAssertion(regexpContext, nested);
}
}
}
function reportLookaroundAssertion({ node, getRegexpLocation, fixReplaceNode }, assertion) {
let messageId, replaceText;
if (assertion.alternatives.length === 1) {
messageId = "canBeInlined";
replaceText = assertion.alternatives[0].raw;
}
else {
messageId = "canBeConvertedIntoGroup";
replaceText = `(?:${assertion.alternatives
.map((alt) => alt.raw)
.join("|")})`;
}
context.report({
node,
loc: getRegexpLocation(assertion),
messageId,
data: {
kind: assertion.kind,
},
fix: fixReplaceNode(assertion, replaceText),
});
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,71 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
function getErrorIndex(error) {
const index = error.index;
if (typeof index === "number") {
return index;
}
return null;
}
exports.default = (0, utils_1.createRule)("no-invalid-regexp", {
meta: {
docs: {
description: "disallow invalid regular expression strings in `RegExp` constructors",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
error: "{{message}}",
duplicateFlag: "Duplicate {{flag}} flag.",
uvFlag: "Regex 'u' and 'v' flags cannot be used together.",
},
type: "problem",
},
create(context) {
function visitInvalid(regexpContext) {
const { node, error, patternSource } = regexpContext;
let loc = undefined;
const index = getErrorIndex(error);
if (index !== null &&
index >= 0 &&
index <= patternSource.value.length) {
loc = patternSource.getAstLocation({
start: Math.max(index - 1, 0),
end: Math.min(index + 1, patternSource.value.length),
});
}
context.report({
node,
loc: loc !== null && loc !== void 0 ? loc : undefined,
messageId: "error",
data: { message: error.message },
});
}
function visitUnknown(regexpContext) {
const { node, flags, flagsString, getFlagsLocation } = regexpContext;
const flagSet = new Set();
for (const flag of flagsString !== null && flagsString !== void 0 ? flagsString : "") {
if (flagSet.has(flag)) {
context.report({
node,
loc: getFlagsLocation(),
messageId: "duplicateFlag",
data: { flag },
});
return;
}
flagSet.add(flag);
}
if (flags.unicode && flags.unicodeSets) {
context.report({
node,
loc: getFlagsLocation(),
messageId: "uvFlag",
});
}
}
return (0, utils_1.defineRegexpVisitor)(context, { visitInvalid, visitUnknown });
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,84 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const refa_1 = require("../utils/refa");
exports.default = (0, utils_1.createRule)("no-invisible-character", {
meta: {
docs: {
description: "disallow invisible raw character",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected invisible character. Use '{{instead}}' instead.",
},
type: "suggestion",
},
create(context) {
const sourceCode = context.sourceCode;
function createLiteralVisitor({ node, flags, getRegexpLocation, fixReplaceNode, }) {
return {
onCharacterEnter(cNode) {
if (cNode.raw === " ") {
return;
}
if (cNode.raw.length === 1 && (0, utils_1.isInvisible)(cNode.value)) {
const instead = (0, refa_1.toCharSetSource)(cNode.value, flags);
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
data: {
instead,
},
fix: fixReplaceNode(cNode, instead),
});
}
},
};
}
function verifyString({ node, flags }) {
const text = sourceCode.getText(node);
let index = 0;
for (const c of text) {
if (c === " ") {
continue;
}
const cp = c.codePointAt(0);
if ((0, utils_1.isInvisible)(cp)) {
const instead = (0, refa_1.toCharSetSource)(cp, flags);
const range = [
node.range[0] + index,
node.range[0] + index + c.length,
];
context.report({
node,
loc: {
start: sourceCode.getLocFromIndex(range[0]),
end: sourceCode.getLocFromIndex(range[1]),
},
messageId: "unexpected",
data: {
instead,
},
fix(fixer) {
return fixer.replaceTextRange(range, instead);
},
});
}
index += c.length;
}
}
return (0, utils_1.defineRegexpVisitor)(context, {
createLiteralVisitor,
createSourceVisitor(regexpContext) {
if (regexpContext.node.type === "Literal") {
verifyString(regexpContext);
}
return {};
},
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,139 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const get_usage_of_pattern_1 = require("../utils/get-usage-of-pattern");
function* extractLazyEndQuantifiers(alternatives) {
for (const { elements } of alternatives) {
if (elements.length > 0) {
const last = elements[elements.length - 1];
switch (last.type) {
case "Quantifier":
if (!last.greedy && last.min !== last.max) {
yield last;
}
else if (last.max === 1) {
const element = last.element;
if (element.type === "Group" ||
element.type === "CapturingGroup") {
yield* extractLazyEndQuantifiers(element.alternatives);
}
}
break;
case "CapturingGroup":
case "Group":
yield* extractLazyEndQuantifiers(last.alternatives);
break;
default:
break;
}
}
}
}
exports.default = (0, utils_1.createRule)("no-lazy-ends", {
meta: {
docs: {
description: "disallow lazy quantifiers at the end of an expression",
category: "Possible Errors",
recommended: true,
default: "warn",
},
schema: [
{
type: "object",
properties: {
ignorePartial: { type: "boolean" },
},
additionalProperties: false,
},
],
hasSuggestions: true,
messages: {
uselessElement: "The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.",
uselessQuantifier: "The quantifier can be removed because the quantifier is lazy and has a minimum of 1.",
uselessRange: "The quantifier can be replaced with '{{{min}}}' because the quantifier is lazy and has a minimum of {{min}}.",
suggestMakeGreedy: "Make the quantifier greedy. (This changes the behavior of the regex.)",
suggestRemoveElement: "Remove the quantified element. (This does not changes the behavior of the regex.)",
suggestRemoveQuantifier: "Remove the quantifier. (This does not changes the behavior of the regex.)",
suggestRange: "Replace the quantifier with '{{{min}}}'. (This does not changes the behavior of the regex.)",
},
type: "problem",
},
create(context) {
var _a, _b;
const ignorePartial = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.ignorePartial) !== null && _b !== void 0 ? _b : true;
function createVisitor({ node, getRegexpLocation, getUsageOfPattern, fixReplaceNode, }) {
if (ignorePartial) {
const usageOfPattern = getUsageOfPattern();
if (usageOfPattern !== get_usage_of_pattern_1.UsageOfPattern.whole) {
return {};
}
}
return {
onPatternEnter(pNode) {
for (const lazy of extractLazyEndQuantifiers(pNode.alternatives)) {
const makeGreedy = {
messageId: "suggestMakeGreedy",
fix: fixReplaceNode(lazy, lazy.raw.slice(0, -1)),
};
if (lazy.min === 0) {
const replacement = pNode.alternatives.length === 1 &&
pNode.alternatives[0].elements.length === 1 &&
pNode.alternatives[0].elements[0] === lazy
? "(?:)"
: "";
context.report({
node,
loc: getRegexpLocation(lazy),
messageId: "uselessElement",
suggest: [
{
messageId: "suggestRemoveElement",
fix: fixReplaceNode(lazy, replacement),
},
makeGreedy,
],
});
}
else if (lazy.min === 1) {
context.report({
node,
loc: getRegexpLocation(lazy),
messageId: "uselessQuantifier",
suggest: [
{
messageId: "suggestRemoveQuantifier",
fix: fixReplaceNode(lazy, lazy.element.raw),
},
makeGreedy,
],
});
}
else {
context.report({
node,
loc: getRegexpLocation(lazy),
messageId: "uselessRange",
data: {
min: String(lazy.min),
},
suggest: [
{
messageId: "suggestRange",
data: {
min: String(lazy.min),
},
fix: fixReplaceNode(lazy, `${lazy.element.raw}{${lazy.min}}`),
},
makeGreedy,
],
});
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const type_tracker_1 = require("../utils/type-tracker");
const eslint_utils_1 = require("@eslint-community/eslint-utils");
const STATIC_PROPERTIES = [
"input",
"$_",
"lastMatch",
"$&",
"lastParen",
"$+",
"leftContext",
"$`",
"rightContext",
"$'",
"$1",
"$2",
"$3",
"$4",
"$5",
"$6",
"$7",
"$8",
"$9",
];
const PROTOTYPE_METHODS = ["compile"];
exports.default = (0, utils_1.createRule)("no-legacy-features", {
meta: {
docs: {
description: "disallow legacy RegExp features",
category: "Best Practices",
recommended: true,
},
schema: [
{
type: "object",
properties: {
staticProperties: {
type: "array",
items: { enum: STATIC_PROPERTIES },
uniqueItems: true,
},
prototypeMethods: {
type: "array",
items: { enum: PROTOTYPE_METHODS },
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages: {
forbiddenStaticProperty: "'{{name}}' static property is forbidden.",
forbiddenPrototypeMethods: "RegExp.prototype.{{name}} method is forbidden.",
},
type: "suggestion",
},
create(context) {
var _a, _b, _c, _d;
const staticProperties = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.staticProperties) !== null && _b !== void 0 ? _b : STATIC_PROPERTIES;
const prototypeMethods = (_d = (_c = context.options[0]) === null || _c === void 0 ? void 0 : _c.prototypeMethods) !== null && _d !== void 0 ? _d : PROTOTYPE_METHODS;
const typeTracer = (0, type_tracker_1.createTypeTracker)(context);
return {
...(staticProperties.length
? {
Program(program) {
const scope = context.sourceCode.getScope(program);
const tracker = new eslint_utils_1.ReferenceTracker(scope);
const regexpTraceMap = {};
for (const sp of staticProperties) {
regexpTraceMap[sp] = { [eslint_utils_1.READ]: true };
}
for (const { node, path, } of tracker.iterateGlobalReferences({
RegExp: regexpTraceMap,
})) {
context.report({
node,
messageId: "forbiddenStaticProperty",
data: { name: path.join(".") },
});
}
},
}
: {}),
...(prototypeMethods.length
? {
MemberExpression(node) {
if (node.computed ||
node.property.type !== "Identifier" ||
!prototypeMethods.includes(node.property.name) ||
node.object.type === "Super") {
return;
}
if (typeTracer.isRegExp(node.object)) {
context.report({
node,
messageId: "forbiddenPrototypeMethods",
data: { name: node.property.name },
});
}
},
}
: {}),
};
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,305 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const fix_simplify_quantifier_1 = require("../utils/fix-simplify-quantifier");
const mention_1 = require("../utils/mention");
const refa_2 = require("../utils/refa");
const regexp_ast_1 = require("../utils/regexp-ast");
const util_1 = require("../utils/util");
function* getStartQuantifiers(root, direction, flags) {
if (Array.isArray(root)) {
for (const a of root) {
yield* getStartQuantifiers(a, direction, flags);
}
return;
}
switch (root.type) {
case "Character":
case "CharacterClass":
case "CharacterSet":
case "ExpressionCharacterClass":
case "Backreference":
break;
case "Assertion":
break;
case "Alternative": {
const elements = direction === "ltr" ? root.elements : (0, util_1.reversed)(root.elements);
for (const e of elements) {
if ((0, regexp_ast_analysis_1.isEmpty)(e, flags))
continue;
yield* getStartQuantifiers(e, direction, flags);
break;
}
break;
}
case "CapturingGroup":
break;
case "Group":
yield* getStartQuantifiers(root.alternatives, direction, flags);
break;
case "Quantifier":
yield root;
if (root.max === 1) {
yield* getStartQuantifiers(root.element, direction, flags);
}
break;
default:
yield (0, util_1.assertNever)(root);
}
}
const getCache = (0, util_1.cachedFn)((_flags) => new WeakMap());
function getSingleRepeatedChar(element, flags, cache = getCache(flags)) {
let value = cache.get(element);
if (value === undefined) {
value = uncachedGetSingleRepeatedChar(element, flags, cache);
cache.set(element, value);
}
return value;
}
function uncachedGetSingleRepeatedChar(element, flags, cache) {
switch (element.type) {
case "Alternative": {
let total = undefined;
for (const e of element.elements) {
const c = getSingleRepeatedChar(e, flags, cache);
if (total === undefined) {
total = c;
}
else {
total = total.intersect(c);
}
if (total.isEmpty)
return total;
}
return total !== null && total !== void 0 ? total : regexp_ast_analysis_1.Chars.empty(flags);
}
case "Assertion":
return regexp_ast_analysis_1.Chars.empty(flags);
case "Backreference":
return regexp_ast_analysis_1.Chars.empty(flags);
case "Character":
case "CharacterClass":
case "CharacterSet":
case "ExpressionCharacterClass": {
const set = (0, regexp_ast_analysis_1.toUnicodeSet)(element, flags);
if (set.accept.isEmpty) {
return set.chars;
}
return set.wordSets
.map((wordSet) => {
let total = undefined;
for (const c of wordSet) {
if (total === undefined) {
total = c;
}
else {
total = total.intersect(c);
}
if (total.isEmpty)
return total;
}
return total !== null && total !== void 0 ? total : regexp_ast_analysis_1.Chars.empty(flags);
})
.reduce((a, b) => a.union(b));
}
case "CapturingGroup":
case "Group":
return element.alternatives
.map((a) => getSingleRepeatedChar(a, flags, cache))
.reduce((a, b) => a.union(b));
case "Quantifier":
if (element.max === 0)
return regexp_ast_analysis_1.Chars.empty(flags);
return getSingleRepeatedChar(element.element, flags, cache);
default:
return (0, util_1.assertNever)(element);
}
}
function getTradingQuantifiersAfter(start, startChar, direction, flags) {
const results = [];
(0, regexp_ast_analysis_1.followPaths)(start, "next", startChar, {
join(states) {
return refa_1.CharSet.empty(startChar.maximum).union(...states);
},
continueAfter(_, state) {
return !state.isEmpty;
},
continueInto(element, state) {
return element.type !== "Assertion" && !state.isEmpty;
},
leave(element, state) {
switch (element.type) {
case "Assertion":
case "Backreference":
case "Character":
case "CharacterClass":
case "CharacterSet":
case "ExpressionCharacterClass":
return state.intersect(getSingleRepeatedChar(element, flags));
case "CapturingGroup":
case "Group":
case "Quantifier":
return state;
default:
return (0, util_1.assertNever)(element);
}
},
enter(element, state) {
if (element.type === "Quantifier" &&
element.min !== element.max) {
const qChar = getSingleRepeatedChar(element, flags);
const intersection = qChar.intersect(state);
if (!intersection.isEmpty) {
results.push({
quant: element,
quantRepeatedChar: qChar,
intersection,
});
}
}
return state;
},
}, direction);
return results;
}
exports.default = (0, utils_1.createRule)("no-misleading-capturing-group", {
meta: {
docs: {
description: "disallow capturing groups that do not behave as one would expect",
category: "Possible Errors",
recommended: true,
},
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
reportBacktrackingEnds: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
removeQuant: "{{quant}} can be removed because it is already included by {{cause}}." +
" This makes the capturing group misleading, because it actually captures less text than its pattern suggests.",
replaceQuant: "{{quant}} can be replaced with {{fix}} because of {{cause}}." +
" This makes the capturing group misleading, because it actually captures less text than its pattern suggests.",
suggestionRemove: "Remove {{quant}}.",
suggestionReplace: "Replace {{quant}} with {{fix}}.",
nonAtomic: "The quantifier {{quant}} is not atomic for the characters {{chars}}, so it might capture fewer characters than expected. This makes the capturing group misleading, because the quantifier will capture fewer characters than its pattern suggests in some edge cases.",
suggestionNonAtomic: "Make the quantifier atomic by adding {{fix}}. Careful! This is going to change the behavior of the regex in some edge cases.",
trading: "The quantifier {{quant}} can exchange characters ({{chars}}) with {{other}}. This makes the capturing group misleading, because the quantifier will capture fewer characters than its pattern suggests.",
},
type: "problem",
},
create(context) {
var _a, _b;
const reportBacktrackingEnds = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.reportBacktrackingEnds) !== null && _b !== void 0 ? _b : true;
function createVisitor(regexpContext) {
const { node, flags, getRegexpLocation } = regexpContext;
const parser = (0, refa_2.getParser)(regexpContext);
function reportStartQuantifiers(capturingGroup) {
const direction = (0, regexp_ast_analysis_1.getMatchingDirection)(capturingGroup);
const startQuantifiers = getStartQuantifiers(capturingGroup.alternatives, direction, flags);
for (const quantifier of startQuantifiers) {
const result = (0, regexp_ast_1.canSimplifyQuantifier)(quantifier, flags, parser);
if (!result.canSimplify)
return;
const cause = (0, mention_1.joinEnglishList)(result.dependencies.map((d) => (0, mention_1.mention)(d)));
const [replacement, fix] = (0, fix_simplify_quantifier_1.fixSimplifyQuantifier)(quantifier, result, regexpContext);
if (quantifier.min === 0) {
const removesCapturingGroup = (0, regexp_ast_1.hasCapturingGroup)(quantifier);
context.report({
node,
loc: getRegexpLocation(quantifier),
messageId: "removeQuant",
data: {
quant: (0, mention_1.mention)(quantifier),
cause,
},
suggest: removesCapturingGroup
? undefined
: [
{
messageId: "suggestionRemove",
data: {
quant: (0, mention_1.mention)(quantifier),
},
fix,
},
],
});
}
else {
context.report({
node,
loc: getRegexpLocation(quantifier),
messageId: "replaceQuant",
data: {
quant: (0, mention_1.mention)(quantifier),
fix: (0, mention_1.mention)(replacement),
cause,
},
suggest: [
{
messageId: "suggestionReplace",
data: {
quant: (0, mention_1.mention)(quantifier),
fix: (0, mention_1.mention)(replacement),
},
fix,
},
],
});
}
}
}
function reportTradingEndQuantifiers(capturingGroup) {
const direction = (0, regexp_ast_analysis_1.getMatchingDirection)(capturingGroup);
const endQuantifiers = getStartQuantifiers(capturingGroup.alternatives, (0, regexp_ast_analysis_1.invertMatchingDirection)(direction), flags);
for (const quantifier of endQuantifiers) {
if (!quantifier.greedy) {
continue;
}
if (quantifier.min === quantifier.max) {
continue;
}
const qChar = getSingleRepeatedChar(quantifier, flags);
if (qChar.isEmpty) {
continue;
}
for (const trader of getTradingQuantifiersAfter(quantifier, qChar, direction, flags)) {
if ((0, regexp_ast_analysis_1.hasSomeDescendant)(capturingGroup, trader.quant)) {
continue;
}
if (trader.quant.min >= 1 &&
!(0, regexp_ast_analysis_1.isPotentiallyZeroLength)(trader.quant.element, flags))
context.report({
node,
loc: getRegexpLocation(quantifier),
messageId: "trading",
data: {
quant: (0, mention_1.mention)(quantifier),
other: (0, mention_1.mention)(trader.quant),
chars: (0, refa_2.toCharSetSource)(trader.intersection, flags),
},
});
}
}
}
return {
onCapturingGroupLeave(capturingGroup) {
reportStartQuantifiers(capturingGroup);
if (reportBacktrackingEnds) {
reportTradingEndQuantifiers(capturingGroup);
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,209 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const regex_syntax_1 = require("../utils/regex-syntax");
const segmenter = new Intl.Segmenter();
function startsWithSurrogate(s) {
if (s.length < 2) {
return false;
}
const h = s.charCodeAt(0);
const l = s.charCodeAt(1);
return h >= 0xd800 && h <= 0xdbff && l >= 0xdc00 && l <= 0xdfff;
}
function getProblem(grapheme, flags) {
if (grapheme.length > 2 ||
(grapheme.length === 2 && !startsWithSurrogate(grapheme))) {
return "Multi";
}
else if (!flags.unicode &&
!flags.unicodeSets &&
startsWithSurrogate(grapheme)) {
return "Surrogate";
}
return null;
}
function getGraphemeBeforeQuant(quant) {
const alt = quant.parent;
let start = quant.start;
for (let i = alt.elements.indexOf(quant) - 1; i >= 0; i--) {
const e = alt.elements[i];
if (e.type === "Character" && !(0, regex_syntax_1.isEscapeSequence)(e.raw)) {
start = e.start;
}
else {
break;
}
}
const before = alt.raw.slice(start - alt.start, quant.element.end - alt.start);
const segments = [...segmenter.segment(before)];
const segment = segments[segments.length - 1];
return segment.segment;
}
function getGraphemeProblems(cc, flags) {
const offset = cc.negate ? 2 : 1;
const ignoreElements = cc.elements.filter((element) => element.type === "CharacterClass" ||
element.type === "ExpressionCharacterClass" ||
element.type === "ClassStringDisjunction");
const problems = [];
for (const { segment, index } of segmenter.segment(cc.raw.slice(offset, -1))) {
const problem = getProblem(segment, flags);
if (problem !== null) {
const start = offset + index + cc.start;
const end = start + segment.length;
if (ignoreElements.some((ignore) => ignore.start <= start && end <= ignore.end)) {
continue;
}
problems.push({
grapheme: segment,
problem,
start,
end,
elements: cc.elements.filter((e) => e.start < end && e.end > start),
});
}
}
return problems;
}
function getGraphemeProblemsFix(problems, cc, flags) {
if (cc.negate) {
return null;
}
if (!problems.every((p) => p.start === p.elements[0].start &&
p.end === p.elements[p.elements.length - 1].end)) {
return null;
}
const prefixGraphemes = problems.map((p) => p.grapheme);
let ccRaw = cc.raw;
for (let i = problems.length - 1; i >= 0; i--) {
const { start, end } = problems[i];
ccRaw = ccRaw.slice(0, start - cc.start) + ccRaw.slice(end - cc.start);
}
if (flags.unicodeSets) {
const prefix = prefixGraphemes.join("|");
return `[\\q{${prefix}}${ccRaw.slice(1, -1)}]`;
}
if (ccRaw.startsWith("[^")) {
ccRaw = `[\\${ccRaw.slice(1)}`;
}
const prefix = prefixGraphemes.sort((a, b) => b.length - a.length).join("|");
let fix = prefix;
let singleAlternative = problems.length === 1;
if (ccRaw !== "[]") {
fix += `|${ccRaw}`;
singleAlternative = false;
}
if (singleAlternative && cc.parent.type === "Alternative") {
return fix;
}
if (cc.parent.type === "Alternative" && cc.parent.elements.length === 1) {
return fix;
}
return `(?:${fix})`;
}
exports.default = (0, utils_1.createRule)("no-misleading-unicode-character", {
meta: {
docs: {
description: "disallow multi-code-point characters in character classes and quantifiers",
category: "Possible Errors",
recommended: true,
},
schema: [
{
type: "object",
properties: {
fixable: { type: "boolean" },
},
additionalProperties: false,
},
],
fixable: "code",
hasSuggestions: true,
messages: {
characterClass: "The character(s) {{ graphemes }} are all represented using multiple {{ unit }}.{{ uFlag }}",
quantifierMulti: "The character {{ grapheme }} is represented using multiple Unicode code points. The quantifier only applies to the last code point {{ last }} and not to the whole character.",
quantifierSurrogate: "The character {{ grapheme }} is represented using a surrogate pair. The quantifier only applies to the tailing surrogate {{ last }} and not to the whole character.",
fixCharacterClass: "Move the character(s) {{ graphemes }} outside the character class.",
fixQuantifier: "Wrap a group around {{ grapheme }}.",
},
type: "problem",
},
create(context) {
var _a, _b;
const fixable = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.fixable) !== null && _b !== void 0 ? _b : false;
function makeFix(fix, messageId, data) {
if (fixable) {
return { fix };
}
return {
suggest: [{ messageId, data, fix }],
};
}
function createVisitor(regexpContext) {
const { node, patternSource, flags, getRegexpLocation, fixReplaceNode, } = regexpContext;
return {
onCharacterClassEnter(ccNode) {
const problems = getGraphemeProblems(ccNode, flags);
if (problems.length === 0) {
return;
}
const range = {
start: problems[0].start,
end: problems[problems.length - 1].end,
};
const fix = getGraphemeProblemsFix(problems, ccNode, flags);
const graphemes = problems
.map((p) => (0, mention_1.mention)(p.grapheme))
.join(", ");
const uFlag = problems.every((p) => p.problem === "Surrogate");
context.report({
node,
loc: getRegexpLocation(range),
messageId: "characterClass",
data: {
graphemes,
unit: flags.unicode || flags.unicodeSets
? "code points"
: "char codes",
uFlag: uFlag ? " Use the `u` flag." : "",
},
...makeFix(fixReplaceNode(ccNode, () => fix), "fixCharacterClass", { graphemes }),
});
},
onQuantifierEnter(qNode) {
if (qNode.element.type !== "Character") {
return;
}
const grapheme = getGraphemeBeforeQuant(qNode);
const problem = getProblem(grapheme, flags);
if (problem === null) {
return;
}
context.report({
node,
loc: getRegexpLocation(qNode),
messageId: `quantifier${problem}`,
data: {
grapheme: (0, mention_1.mention)(grapheme),
last: (0, mention_1.mentionChar)(qNode.element),
},
...makeFix((fixer) => {
const range = patternSource.getReplaceRange({
start: qNode.element.end - grapheme.length,
end: qNode.element.end,
});
if (!range) {
return null;
}
return range.replace(fixer, `(?:${grapheme})`);
}, "fixQuantifier", { grapheme: (0, mention_1.mention)(grapheme) }),
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,97 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const ast_utils_1 = require("../utils/ast-utils");
const type_tracker_1 = require("../utils/type-tracker");
function parseOption(userOption) {
let strictTypes = true;
if (userOption) {
if (userOption.strictTypes != null) {
strictTypes = userOption.strictTypes;
}
}
return {
strictTypes,
};
}
exports.default = (0, utils_1.createRule)("no-missing-g-flag", {
meta: {
docs: {
description: "disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll`",
category: "Possible Errors",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
strictTypes: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
missingGlobalFlag: "The pattern given to the argument of `String#{{method}}()` requires the `g` flag, but is missing it.",
},
type: "problem",
},
create(context) {
const { strictTypes } = parseOption(context.options[0]);
const typeTracer = (0, type_tracker_1.createTypeTracker)(context);
function visit(regexpContext) {
const { regexpNode, flags, flagsString } = regexpContext;
if (flags.global ||
flagsString == null) {
return;
}
for (const ref of (0, ast_utils_1.extractExpressionReferences)(regexpNode, context)) {
verifyExpressionReference(ref, regexpContext);
}
}
function verifyExpressionReference(ref, { regexpNode, fixReplaceFlags, flagsString, }) {
if (ref.type !== "argument") {
return;
}
const node = ref.callExpression;
if (node.arguments[0] !== ref.node ||
!(0, ast_utils_1.isKnownMethodCall)(node, {
matchAll: 1,
replaceAll: 2,
})) {
return;
}
if (strictTypes
? !typeTracer.isString(node.callee.object)
: !typeTracer.maybeString(node.callee.object)) {
return;
}
context.report({
node: ref.node,
messageId: "missingGlobalFlag",
data: {
method: node.callee.property.name,
},
fix: buildFixer(),
});
function buildFixer() {
if (node.arguments[0] !== regexpNode ||
((regexpNode.type === "NewExpression" ||
regexpNode.type === "CallExpression") &&
regexpNode.arguments[1] &&
regexpNode.arguments[1].type !== "Literal")) {
return null;
}
return fixReplaceFlags(`${flagsString}g`, false);
}
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor(regexpContext) {
visit(regexpContext);
return {};
},
visitInvalid: visit,
visitUnknown: visit,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const STANDARD_FLAGS = "dgimsuvy";
exports.default = (0, utils_1.createRule)("no-non-standard-flag", {
meta: {
docs: {
description: "disallow non-standard flags",
category: "Best Practices",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected non-standard flag '{{flag}}'.",
},
type: "suggestion",
},
create(context) {
function visit({ regexpNode, getFlagsLocation, flagsString, }) {
if (flagsString) {
const nonStandard = [...flagsString].filter((f) => !STANDARD_FLAGS.includes(f));
if (nonStandard.length > 0) {
context.report({
node: regexpNode,
loc: getFlagsLocation(),
messageId: "unexpected",
data: { flag: nonStandard[0] },
});
}
}
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor(regexpContext) {
visit(regexpContext);
return {};
},
visitInvalid: visit,
visitUnknown: visit,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,68 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const char_ranges_1 = require("../utils/char-ranges");
const mention_1 = require("../utils/mention");
const regex_syntax_1 = require("../utils/regex-syntax");
exports.default = (0, utils_1.createRule)("no-obscure-range", {
meta: {
docs: {
description: "disallow obscure character ranges",
category: "Best Practices",
recommended: true,
},
schema: [
{
type: "object",
properties: {
allowed: (0, char_ranges_1.getAllowedCharValueSchema)(),
},
additionalProperties: false,
},
],
messages: {
unexpected: "Unexpected obscure character range. The characters of {{range}} are not obvious.",
},
type: "suggestion",
},
create(context) {
var _a;
const allowedRanges = (0, char_ranges_1.getAllowedCharRanges)((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.allowed, context);
function createVisitor({ node, getRegexpLocation, }) {
return {
onCharacterClassRangeEnter(rNode) {
const { min, max } = rNode;
if (min.value === max.value) {
return;
}
if ((0, regex_syntax_1.isControlEscape)(min.raw) && (0, regex_syntax_1.isControlEscape)(max.raw)) {
return;
}
if ((0, regex_syntax_1.isOctalEscape)(min.raw) && (0, regex_syntax_1.isOctalEscape)(max.raw)) {
return;
}
if (((0, regex_syntax_1.isHexLikeEscape)(min.raw) || min.value === 0) &&
(0, regex_syntax_1.isHexLikeEscape)(max.raw)) {
return;
}
if (!(0, regex_syntax_1.isEscapeSequence)(min.raw) &&
!(0, regex_syntax_1.isEscapeSequence)(max.raw) &&
(0, char_ranges_1.inRange)(allowedRanges, min.value, max.value)) {
return;
}
context.report({
node,
loc: getRegexpLocation(rNode),
messageId: "unexpected",
data: {
range: (0, mention_1.mentionChar)(rNode),
},
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regex_syntax_1 = require("../utils/regex-syntax");
exports.default = (0, utils_1.createRule)("no-octal", {
meta: {
docs: {
description: "disallow octal escape sequence",
category: "Best Practices",
recommended: false,
},
schema: [],
messages: {
unexpected: "Unexpected octal escape sequence '{{expr}}'.",
replaceHex: "Replace the octal escape sequence with a hexadecimal escape sequence.",
},
type: "suggestion",
hasSuggestions: true,
},
create(context) {
function createVisitor({ node, fixReplaceNode, getRegexpLocation, }) {
return {
onCharacterEnter(cNode) {
if (cNode.raw === "\\0") {
return;
}
if (!(0, regex_syntax_1.isOctalEscape)(cNode.raw)) {
return;
}
const report = cNode.raw.startsWith("\\0") ||
!(cNode.parent.type === "CharacterClass" ||
cNode.parent.type === "CharacterClassRange");
if (report) {
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
data: {
expr: cNode.raw,
},
suggest: [
{
messageId: "replaceHex",
fix: fixReplaceNode(cNode, () => {
return `\\x${cNode.value
.toString(16)
.padStart(2, "0")}`;
}),
},
],
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
function isZeroQuantifier(node) {
return node.min === 0;
}
function isOptional(assertion, quantifier, flags) {
let element = assertion;
while (element.parent !== quantifier) {
const parent = element.parent;
if (parent.type === "Alternative") {
for (const e of parent.elements) {
if (e === element) {
continue;
}
if (!(0, regexp_ast_analysis_1.isZeroLength)(e, flags)) {
return false;
}
}
if (parent.parent.type === "Pattern") {
throw new Error("The given assertion is not a descendant of the given quantifier.");
}
element = parent.parent;
}
else {
if (parent.max > 1 && !(0, regexp_ast_analysis_1.isZeroLength)(parent, flags)) {
return false;
}
element = parent;
}
}
return true;
}
exports.default = (0, utils_1.createRule)("no-optional-assertion", {
meta: {
docs: {
description: "disallow optional assertions",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
optionalAssertion: "This assertion effectively optional and does not change the pattern. Either remove the assertion or change the parent quantifier '{{quantifier}}'.",
},
type: "problem",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
const zeroQuantifierStack = [];
return {
onQuantifierEnter(q) {
if (isZeroQuantifier(q)) {
zeroQuantifierStack.unshift(q);
}
},
onQuantifierLeave(q) {
if (zeroQuantifierStack[0] === q) {
zeroQuantifierStack.shift();
}
},
onAssertionEnter(assertion) {
const q = zeroQuantifierStack[0];
if (q && isOptional(assertion, q, flags)) {
context.report({
node,
loc: getRegexpLocation(assertion),
messageId: "optionalAssertion",
data: {
quantifier: q.raw.substr(q.element.raw.length),
},
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-potentially-useless-backreference", {
meta: {
docs: {
description: "disallow backreferences that reference a group that might not be matched",
category: "Possible Errors",
recommended: true,
default: "warn",
},
schema: [],
messages: {
potentiallyUselessBackreference: "Some paths leading to the backreference do not go through the referenced capturing group or the captured text might be reset before reaching the backreference.",
},
type: "problem",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onBackreferenceEnter(backreference) {
if ((0, regexp_ast_analysis_1.isEmptyBackreference)(backreference, flags)) {
return;
}
if (!(0, regexp_ast_analysis_1.isStrictBackreference)(backreference)) {
context.report({
node,
loc: getRegexpLocation(backreference),
messageId: "potentiallyUselessBackreference",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-standalone-backslash", {
meta: {
docs: {
description: "disallow standalone backslashes (`\\`)",
category: "Best Practices",
recommended: false,
},
schema: [],
messages: {
unexpected: "Unexpected standalone backslash (`\\`). It looks like an escape sequence, but it's a single `\\` character pattern.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, getRegexpLocation, }) {
return {
onCharacterEnter(cNode) {
if (cNode.value === utils_1.CP_BACK_SLASH && cNode.raw === "\\") {
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,105 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const scslre_1 = require("scslre");
const utils_1 = require("../utils");
const get_usage_of_pattern_1 = require("../utils/get-usage-of-pattern");
const mention_1 = require("../utils/mention");
const refa_1 = require("../utils/refa");
function unionLocations(a, b) {
function less(x, y) {
if (x.line < y.line) {
return true;
}
else if (x.line > y.line) {
return false;
}
return x.column < y.column;
}
return {
start: { ...(less(a.start, b.start) ? a.start : b.start) },
end: { ...(less(a.end, b.end) ? b.end : a.end) },
};
}
exports.default = (0, utils_1.createRule)("no-super-linear-backtracking", {
meta: {
docs: {
description: "disallow exponential and polynomial backtracking",
category: "Possible Errors",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
report: {
enum: ["certain", "potential"],
},
},
additionalProperties: false,
},
],
messages: {
self: "This quantifier can reach itself via the loop {{parent}}." +
" Using any string accepted by {{attack}}, this can be exploited to cause at least polynomial backtracking." +
"{{exp}}",
trade: "The quantifier {{start}} can exchange characters with {{end}}." +
" Using any string accepted by {{attack}}, this can be exploited to cause at least polynomial backtracking." +
"{{exp}}",
},
type: "problem",
},
create(context) {
var _a, _b;
const reportUncertain = ((_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.report) !== null && _b !== void 0 ? _b : "certain") ===
"potential";
function createVisitor(regexpContext) {
const { node, patternAst, flags, getRegexpLocation, fixReplaceNode, getUsageOfPattern, } = regexpContext;
const result = (0, scslre_1.analyse)((0, refa_1.getJSRegexppAst)(regexpContext), {
reportTypes: { Move: false },
assumeRejectingSuffix: reportUncertain &&
getUsageOfPattern() !== get_usage_of_pattern_1.UsageOfPattern.whole,
});
for (const report of result.reports) {
const exp = report.exponential
? " This is going to cause exponential backtracking resulting in exponential worst-case runtime behavior."
: getUsageOfPattern() !== get_usage_of_pattern_1.UsageOfPattern.whole
? " This might cause exponential backtracking."
: "";
const attack = `/${report.character.literal.source}+/${flags.ignoreCase ? "i" : ""}`;
const fix = fixReplaceNode(patternAst, () => { var _a, _b; return (_b = (_a = report.fix()) === null || _a === void 0 ? void 0 : _a.source) !== null && _b !== void 0 ? _b : null; });
if (report.type === "Self") {
context.report({
node,
loc: getRegexpLocation(report.quant),
messageId: "self",
data: {
exp,
attack,
parent: (0, mention_1.mention)(report.parentQuant),
},
fix,
});
}
else if (report.type === "Trade") {
context.report({
node,
loc: unionLocations(getRegexpLocation(report.startQuant), getRegexpLocation(report.endQuant)),
messageId: "trade",
data: {
exp,
attack,
start: (0, mention_1.mention)(report.startQuant),
end: (0, mention_1.mention)(report.endQuant),
},
fix,
});
}
}
return {};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,194 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const scslre_1 = require("scslre");
const utils_1 = require("../utils");
const get_usage_of_pattern_1 = require("../utils/get-usage-of-pattern");
const refa_2 = require("../utils/refa");
function dedupeReports(reports) {
const seen = new Set();
const result = [];
for (const r of reports) {
if (!seen.has(r.quant)) {
result.push(r);
seen.add(r.quant);
}
}
return result;
}
function* findReachableQuantifiers(node, flags) {
switch (node.type) {
case "CapturingGroup":
case "Group":
case "Pattern": {
for (const a of node.alternatives) {
yield* findReachableQuantifiers(a, flags);
}
break;
}
case "Assertion": {
if (node.kind === "lookahead" || node.kind === "lookbehind") {
for (const a of node.alternatives) {
yield* findReachableQuantifiers(a, flags);
}
}
break;
}
case "Quantifier": {
yield node;
break;
}
case "Alternative": {
const dir = (0, regexp_ast_analysis_1.getMatchingDirection)(node);
for (let i = 0; i < node.elements.length; i++) {
const elementIndex = dir === "ltr" ? i : node.elements.length - 1 - i;
const element = node.elements[elementIndex];
yield* findReachableQuantifiers(element, flags);
if (!(0, regexp_ast_analysis_1.isPotentiallyEmpty)(element, flags)) {
break;
}
}
break;
}
default:
break;
}
}
const TRANSFORMER_OPTIONS = {
ignoreAmbiguity: true,
ignoreOrder: true,
};
const PASS_1 = refa_1.Transformers.simplify(TRANSFORMER_OPTIONS);
const PASS_2 = new refa_1.CombinedTransformer([
refa_1.Transformers.inline(TRANSFORMER_OPTIONS),
refa_1.Transformers.removeDeadBranches(TRANSFORMER_OPTIONS),
refa_1.Transformers.replaceAssertions({
...TRANSFORMER_OPTIONS,
replacement: "empty-set",
}),
]);
exports.default = (0, utils_1.createRule)("no-super-linear-move", {
meta: {
docs: {
description: "disallow quantifiers that cause quadratic moves",
category: "Possible Errors",
recommended: false,
},
schema: [
{
type: "object",
properties: {
report: {
enum: ["certain", "potential"],
},
ignoreSticky: {
type: "boolean",
},
ignorePartial: {
type: "boolean",
},
},
additionalProperties: false,
},
],
messages: {
unexpected: "Any attack string {{attack}} plus some rejecting suffix will cause quadratic runtime because of this quantifier.",
},
type: "problem",
},
create(context) {
var _a, _b, _c, _d, _e, _f;
const reportUncertain = ((_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.report) !== null && _b !== void 0 ? _b : "certain") ===
"potential";
const ignoreSticky = (_d = (_c = context.options[0]) === null || _c === void 0 ? void 0 : _c.ignoreSticky) !== null && _d !== void 0 ? _d : true;
const ignorePartial = (_f = (_e = context.options[0]) === null || _e === void 0 ? void 0 : _e.ignorePartial) !== null && _f !== void 0 ? _f : true;
function getScslreReports(regexpContext, assumeRejectingSuffix) {
const { flags } = regexpContext;
const result = (0, scslre_1.analyse)((0, refa_2.getJSRegexppAst)(regexpContext, true), {
reportTypes: { Move: true, Self: false, Trade: false },
assumeRejectingSuffix,
});
return result.reports.map((r) => {
if (r.type !== "Move") {
throw new Error("Unexpected report type");
}
return {
quant: r.quant,
attack: `/${r.character.literal.source}+/${flags.ignoreCase ? "i" : ""}`,
};
});
}
function* getSimpleReports(regexpContext, assumeRejectingSuffix) {
const { patternAst, flags } = regexpContext;
const parser = refa_1.JS.Parser.fromAst((0, refa_2.getJSRegexppAst)(regexpContext, true));
for (const q of findReachableQuantifiers(patternAst, flags)) {
if (q.max !== Infinity) {
continue;
}
if (q.element.type === "Assertion" ||
q.element.type === "Backreference") {
continue;
}
let e = parser.parseElement(q.element, {
assertions: "parse",
backreferences: "disable",
}).expression;
e = (0, refa_1.transform)(PASS_1, e);
e = (0, refa_1.transform)(PASS_2, e);
if (e.alternatives.length === 0) {
continue;
}
let hasCharacters = false;
(0, refa_1.visitAst)(e, {
onCharacterClassEnter() {
hasCharacters = true;
},
});
if (!hasCharacters) {
continue;
}
if (!assumeRejectingSuffix) {
const after = (0, regexp_ast_analysis_1.getFirstConsumedCharAfter)(q, (0, regexp_ast_analysis_1.getMatchingDirection)(q), flags);
if (after.empty && after.look.char.isAll) {
continue;
}
}
const attack = `/${refa_1.JS.toLiteral({
type: "Quantifier",
alternatives: e.alternatives,
min: 1,
max: Infinity,
lazy: false,
}).source}/${flags.ignoreCase ? "i" : ""}`;
yield { quant: q, attack };
}
}
function createVisitor(regexpContext) {
const { node, flags, getRegexpLocation, getUsageOfPattern } = regexpContext;
if (ignoreSticky && flags.sticky) {
return {};
}
const usage = getUsageOfPattern();
if (ignorePartial && usage === get_usage_of_pattern_1.UsageOfPattern.partial) {
return {};
}
const assumeRejectingSuffix = reportUncertain && usage !== get_usage_of_pattern_1.UsageOfPattern.whole;
for (const report of dedupeReports([
...getSimpleReports(regexpContext, assumeRejectingSuffix),
...getScslreReports(regexpContext, assumeRejectingSuffix),
])) {
context.report({
node,
loc: getRegexpLocation(report.quant),
messageId: "unexpected",
data: { attack: report.attack },
});
}
return {};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
function isLookaround(node) {
return (node.type === "Assertion" &&
(node.kind === "lookahead" || node.kind === "lookbehind"));
}
function getTriviallyNestedAssertion(node) {
const alternatives = node.alternatives;
if (alternatives.length === 1) {
const elements = alternatives[0].elements;
if (elements.length === 1) {
const element = elements[0];
if (element.type === "Assertion") {
return element;
}
}
}
return null;
}
function getNegatedRaw(assertion) {
if (assertion.kind === "word") {
return assertion.negate ? "\\b" : "\\B";
}
else if (assertion.kind === "lookahead") {
return `(?${assertion.negate ? "=" : "!"}${assertion.raw.slice(3)}`;
}
else if (assertion.kind === "lookbehind") {
return `(?<${assertion.negate ? "=" : "!"}${assertion.raw.slice(4)}`;
}
return null;
}
exports.default = (0, utils_1.createRule)("no-trivially-nested-assertion", {
meta: {
docs: {
description: "disallow trivially nested assertions",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected trivially nested assertion.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, fixReplaceNode, getRegexpLocation, }) {
return {
onAssertionEnter(aNode) {
if (aNode.parent.type === "Quantifier") {
return;
}
if (!isLookaround(aNode)) {
return;
}
const nested = getTriviallyNestedAssertion(aNode);
if (nested === null) {
return;
}
if (aNode.negate &&
isLookaround(nested) &&
nested.negate &&
(0, regexp_ast_analysis_1.hasSomeDescendant)(nested, (d) => d.type === "CapturingGroup")) {
return;
}
const replacement = aNode.negate
? getNegatedRaw(nested)
: nested.raw;
if (replacement === null) {
return;
}
context.report({
node,
loc: getRegexpLocation(aNode),
messageId: "unexpected",
fix: fixReplaceNode(aNode, replacement),
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,141 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regexp_ast_1 = require("../utils/regexp-ast");
function getCombinedQuant(parent, child) {
if (parent.max === 0 || child.max === 0) {
return null;
}
else if (parent.greedy === child.greedy) {
const greedy = parent.greedy;
const a = child.min;
const b = child.max;
const c = parent.min;
const d = parent.max;
const condition = b === Infinity && c === 0
? a <= 1
: c === d || b * c + 1 >= a * (c + 1);
if (condition) {
return {
min: a * c,
max: b * d,
greedy,
};
}
return null;
}
return null;
}
function getSimplifiedChildQuant(parent, child) {
if (parent.max === 0 || child.max === 0) {
return null;
}
else if (parent.greedy !== child.greedy) {
return null;
}
let min = child.min;
let max = child.max;
if (min === 0 && parent.min === 0) {
min = 1;
}
if (parent.max === Infinity && (min === 0 || min === 1) && max > 1) {
max = 1;
}
return { min, max, greedy: child.greedy };
}
function isTrivialQuantifier(quant) {
return quant.min === quant.max && (quant.min === 0 || quant.min === 1);
}
function* iterateSingleQuantifiers(group) {
for (const { elements } of group.alternatives) {
if (elements.length === 1) {
const single = elements[0];
if (single.type === "Quantifier") {
yield single;
}
}
}
}
exports.default = (0, utils_1.createRule)("no-trivially-nested-quantifier", {
meta: {
docs: {
description: "disallow nested quantifiers that can be rewritten as one quantifier",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
nested: "These two quantifiers are trivially nested and can be replaced with '{{quant}}'.",
childOne: "This nested quantifier can be removed.",
childSimpler: "This nested quantifier can be simplified to '{{quant}}'.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, fixReplaceNode, fixReplaceQuant, getRegexpLocation, }) {
return {
onQuantifierEnter(qNode) {
if (isTrivialQuantifier(qNode)) {
return;
}
const element = qNode.element;
if (element.type !== "Group") {
return;
}
for (const child of iterateSingleQuantifiers(element)) {
if (isTrivialQuantifier(child)) {
continue;
}
if (element.alternatives.length === 1) {
const quant = getCombinedQuant(qNode, child);
if (!quant) {
continue;
}
const quantStr = (0, regexp_ast_1.quantToString)(quant);
const replacement = child.element.raw + quantStr;
context.report({
node,
loc: getRegexpLocation(qNode),
messageId: "nested",
data: { quant: quantStr },
fix: fixReplaceNode(qNode, replacement),
});
}
else {
const quant = getSimplifiedChildQuant(qNode, child);
if (!quant) {
continue;
}
if (quant.min === child.min &&
quant.max === child.max) {
continue;
}
if (quant.min === 1 && quant.max === 1) {
context.report({
node,
loc: getRegexpLocation(child),
messageId: "childOne",
fix: fixReplaceNode(child, child.element.raw),
});
}
else {
quant.greedy = undefined;
context.report({
node,
loc: getRegexpLocation(child),
messageId: "childSimpler",
data: { quant: (0, regexp_ast_1.quantToString)(quant) },
fix: fixReplaceQuant(child, quant),
});
}
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,151 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
function getCapturingGroupIdentifier(group) {
if (group.name) {
return `'${group.name}'`;
}
return `number ${(0, regexp_ast_analysis_1.getCapturingGroupNumber)(group)}`;
}
exports.default = (0, utils_1.createRule)("no-unused-capturing-group", {
meta: {
docs: {
description: "disallow unused capturing group",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
fixable: { type: "boolean" },
allowNamed: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
unusedCapturingGroup: "Capturing group {{identifier}} is defined but never used.",
makeNonCapturing: "Making this a non-capturing group.",
},
type: "suggestion",
hasSuggestions: true,
},
create(context) {
var _a, _b, _c, _d;
const fixable = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.fixable) !== null && _b !== void 0 ? _b : false;
const allowNamed = (_d = (_c = context.options[0]) === null || _c === void 0 ? void 0 : _c.allowNamed) !== null && _d !== void 0 ? _d : false;
function reportUnused(unused, regexpContext) {
const { node, getRegexpLocation, fixReplaceNode, getAllCapturingGroups, } = regexpContext;
if (allowNamed) {
for (const cgNode of unused) {
if (cgNode.name) {
unused.delete(cgNode);
}
}
}
const fixableGroups = new Set();
for (const group of [...getAllCapturingGroups()].reverse()) {
if (unused.has(group)) {
fixableGroups.add(group);
}
else {
break;
}
}
for (const cgNode of unused) {
const fix = fixableGroups.has(cgNode)
? fixReplaceNode(cgNode, cgNode.raw.replace(/^\((?:\?<[^<>]+>)?/u, "(?:"))
: null;
context.report({
node,
loc: getRegexpLocation(cgNode),
messageId: "unusedCapturingGroup",
data: { identifier: getCapturingGroupIdentifier(cgNode) },
fix: fixable ? fix : null,
suggest: fix
? [{ messageId: "makeNonCapturing", fix }]
: null,
});
}
}
function getCapturingGroupReferences(regexpContext) {
const capturingGroupReferences = regexpContext.getCapturingGroupReferences();
if (!capturingGroupReferences.length) {
return null;
}
const indexRefs = [];
const namedRefs = [];
let hasUnknownName = false;
let hasSplit = false;
for (const ref of capturingGroupReferences) {
if (ref.type === "UnknownUsage" || ref.type === "UnknownRef") {
return null;
}
if (ref.type === "ArrayRef" ||
ref.type === "ReplacementRef" ||
ref.type === "ReplacerFunctionRef") {
if (ref.kind === "index") {
if (ref.ref != null) {
indexRefs.push(ref.ref);
}
else {
return null;
}
}
else {
if (ref.ref) {
namedRefs.push(ref.ref);
}
else {
hasUnknownName = true;
}
}
}
else if (ref.type === "Split") {
hasSplit = true;
}
}
return {
unusedIndexRef(index) {
if (hasSplit) {
return false;
}
return !indexRefs.includes(index);
},
unusedNamedRef(name) {
if (hasUnknownName) {
return false;
}
return !namedRefs.includes(name);
},
};
}
function createVisitor(regexpContext) {
const references = getCapturingGroupReferences(regexpContext);
if (!references) {
return {};
}
const unused = new Set();
const allCapturingGroups = regexpContext.getAllCapturingGroups();
for (let index = 0; index < allCapturingGroups.length; index++) {
const cgNode = allCapturingGroups[index];
if (cgNode.references.length ||
!references.unusedIndexRef(index + 1)) {
continue;
}
if (cgNode.name && !references.unusedNamedRef(cgNode.name)) {
continue;
}
unused.add(cgNode);
}
reportUnused(unused, regexpContext);
return {};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,378 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const util_1 = require("../utils/util");
function containsAssertion(n) {
return (0, regexp_ast_analysis_1.hasSomeDescendant)(n, (d) => d.type === "Assertion");
}
function isSingleCharacterAssertion(assertion, direction, flags) {
switch (assertion.kind) {
case "word":
return false;
case "start":
return direction === "rtl";
case "end":
return direction === "ltr";
default:
break;
}
if ((0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind) !== direction) {
return false;
}
return assertion.alternatives.every((alt) => {
if (!containsAssertion(alt)) {
const range = (0, regexp_ast_analysis_1.getLengthRange)(alt, flags);
return range.min === 1 && range.max === 1;
}
let consumed = false;
let asserted = false;
const elements = direction === "ltr" ? alt.elements : [...alt.elements].reverse();
for (const e of elements) {
if (!consumed) {
if (e.type === "Assertion" &&
isSingleCharacterAssertion(e, direction, flags)) {
asserted = true;
continue;
}
if (containsAssertion(e)) {
return false;
}
const range = (0, regexp_ast_analysis_1.getLengthRange)(e, flags);
if (range.max === 0) {
continue;
}
else if (range.min === 1 && range.max === 1) {
consumed = true;
}
else {
return false;
}
}
else {
const otherDir = (0, regexp_ast_analysis_1.invertMatchingDirection)(direction);
if (e.type === "Assertion" &&
isSingleCharacterAssertion(e, otherDir, flags)) {
continue;
}
return false;
}
}
return consumed || asserted;
});
}
function firstLookCharsIntersection(a, b) {
const char = a.char.intersect(b.char);
return {
char: a.char.intersect(b.char),
exact: (a.exact && b.exact) || char.isEmpty,
edge: a.edge && b.edge,
};
}
function createReorderingGetFirstCharAfter(forbidden) {
function hasForbidden(element) {
if (element.type === "Assertion" && forbidden.has(element)) {
return true;
}
for (const f of forbidden) {
if ((0, regexp_ast_analysis_1.hasSomeDescendant)(element, f)) {
return true;
}
}
return false;
}
return (afterThis, direction, flags) => {
let result = (0, regexp_ast_analysis_1.getFirstCharAfter)(afterThis, direction, flags);
if (afterThis.parent.type === "Alternative") {
const { elements } = afterThis.parent;
const inc = direction === "ltr" ? -1 : +1;
const start = elements.indexOf(afterThis);
for (let i = start + inc; i >= 0 && i < elements.length; i += inc) {
const other = elements[i];
if (!(0, regexp_ast_analysis_1.isZeroLength)(other, flags)) {
break;
}
if (hasForbidden(other)) {
break;
}
const otherResult = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(other, direction, flags));
result = firstLookCharsIntersection(result, otherResult);
}
}
return result;
};
}
function removeAlternative(alternative) {
const parent = alternative.parent;
if (parent.alternatives.length > 1) {
let { start, end } = alternative;
if (parent.alternatives[0] === alternative) {
end++;
}
else {
start--;
}
const before = parent.raw.slice(0, start - parent.start);
const after = parent.raw.slice(end - parent.start);
return [parent, before + after];
}
switch (parent.type) {
case "Pattern":
return [parent, "[]"];
case "Assertion": {
const assertionParent = parent.parent;
if (parent.negate) {
return [
assertionParent.type === "Quantifier"
? assertionParent
: parent,
"",
];
}
if (assertionParent.type === "Quantifier") {
if (assertionParent.min === 0) {
return [assertionParent, ""];
}
return removeAlternative(assertionParent.parent);
}
return removeAlternative(assertionParent);
}
case "CapturingGroup": {
const before = parent.raw.slice(0, alternative.start - parent.start);
const after = parent.raw.slice(alternative.end - parent.start);
return [parent, `${before}[]${after}`];
}
case "Group": {
const groupParent = parent.parent;
if (groupParent.type === "Quantifier") {
if (groupParent.min === 0) {
return [groupParent, ""];
}
return removeAlternative(groupParent.parent);
}
return removeAlternative(groupParent);
}
default:
return (0, util_1.assertNever)(parent);
}
}
const messages = {
alwaysRejectByChar: "{{assertion}} will always reject because it is {{followedOrPreceded}} by a character.",
alwaysAcceptByChar: "{{assertion}} will always accept because it is never {{followedOrPreceded}} by a character.",
alwaysRejectByNonLineTerminator: "{{assertion}} will always reject because it is {{followedOrPreceded}} by a non-line-terminator character.",
alwaysAcceptByLineTerminator: "{{assertion}} will always accept because it is {{followedOrPreceded}} by a line-terminator character.",
alwaysAcceptByLineTerminatorOrEdge: "{{assertion}} will always accept because it is {{followedOrPreceded}} by a line-terminator character or the {{startOrEnd}} of the input string.",
alwaysAcceptOrRejectFollowedByWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a word character.",
alwaysAcceptOrRejectFollowedByNonWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a non-word character.",
alwaysAcceptOrRejectPrecededByWordFollowedByNonWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a word character and followed by a non-word character.",
alwaysAcceptOrRejectPrecededByWordFollowedByWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a word character and followed by a word character.",
alwaysForLookaround: "The {{kind}} {{assertion}} will always {{acceptOrReject}}.",
alwaysForNegativeLookaround: "The negative {{kind}} {{assertion}} will always {{acceptOrReject}}.",
acceptSuggestion: "Remove the assertion. (Replace with empty string.)",
rejectSuggestion: "Remove branch of the assertion. (Replace with empty set.)",
};
exports.default = (0, utils_1.createRule)("no-useless-assertions", {
meta: {
docs: {
description: "disallow assertions that are known to always accept (or reject)",
category: "Possible Errors",
recommended: true,
},
hasSuggestions: true,
schema: [],
messages,
type: "problem",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, fixReplaceNode, }) {
const reported = new Set();
function replaceWithEmptyString(assertion) {
if (assertion.parent.type === "Quantifier") {
return fixReplaceNode(assertion.parent, "");
}
return fixReplaceNode(assertion, "");
}
function replaceWithEmptySet(assertion) {
if (assertion.parent.type === "Quantifier") {
if (assertion.parent.min === 0) {
return fixReplaceNode(assertion.parent, "");
}
const [element, replacement] = removeAlternative(assertion.parent.parent);
return fixReplaceNode(element, replacement);
}
const [element, replacement] = removeAlternative(assertion.parent);
return fixReplaceNode(element, replacement);
}
function report(assertion, messageId, data) {
reported.add(assertion);
const { acceptOrReject } = data;
context.report({
node,
loc: getRegexpLocation(assertion),
messageId,
data: {
assertion: (0, mention_1.mention)(assertion),
...data,
},
suggest: [
{
messageId: `${acceptOrReject}Suggestion`,
fix: acceptOrReject === "accept"
? replaceWithEmptyString(assertion)
: replaceWithEmptySet(assertion),
},
],
});
}
function verifyStartOrEnd(assertion, getFirstCharAfterFn) {
const direction = (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind);
const next = getFirstCharAfterFn(assertion, direction, flags);
const followedOrPreceded = assertion.kind === "end" ? "followed" : "preceded";
const lineTerminator = regexp_ast_analysis_1.Chars.lineTerminator(flags);
if (next.edge) {
if (!flags.multiline) {
if (next.char.isEmpty) {
report(assertion, "alwaysAcceptByChar", {
followedOrPreceded,
acceptOrReject: "accept",
});
}
}
else {
if (next.char.isSubsetOf(lineTerminator)) {
report(assertion, "alwaysAcceptByLineTerminatorOrEdge", {
followedOrPreceded,
startOrEnd: assertion.kind,
acceptOrReject: "accept",
});
}
}
}
else {
if (!flags.multiline) {
report(assertion, "alwaysRejectByChar", {
followedOrPreceded,
acceptOrReject: "reject",
});
}
else {
if (next.char.isDisjointWith(lineTerminator)) {
report(assertion, "alwaysRejectByNonLineTerminator", {
followedOrPreceded,
acceptOrReject: "reject",
});
}
else if (next.char.isSubsetOf(lineTerminator)) {
report(assertion, "alwaysAcceptByLineTerminator", {
followedOrPreceded,
acceptOrReject: "accept",
});
}
}
}
}
function verifyWordBoundary(assertion, getFirstCharAfterFn) {
const word = regexp_ast_analysis_1.Chars.word(flags);
const next = getFirstCharAfterFn(assertion, "ltr", flags);
const prev = getFirstCharAfterFn(assertion, "rtl", flags);
const nextIsWord = next.char.isSubsetOf(word) && !next.edge;
const prevIsWord = prev.char.isSubsetOf(word) && !prev.edge;
const nextIsNonWord = next.char.isDisjointWith(word);
const prevIsNonWord = prev.char.isDisjointWith(word);
const accept = assertion.negate ? "reject" : "accept";
const reject = assertion.negate ? "accept" : "reject";
if (prevIsNonWord) {
if (nextIsWord) {
report(assertion, "alwaysAcceptOrRejectFollowedByWord", {
acceptOrReject: accept,
});
}
if (nextIsNonWord) {
report(assertion, "alwaysAcceptOrRejectFollowedByNonWord", {
acceptOrReject: reject,
});
}
}
if (prevIsWord) {
if (nextIsNonWord) {
report(assertion, "alwaysAcceptOrRejectPrecededByWordFollowedByNonWord", {
acceptOrReject: accept,
});
}
if (nextIsWord) {
report(assertion, "alwaysAcceptOrRejectPrecededByWordFollowedByWord", {
acceptOrReject: reject,
});
}
}
}
function verifyLookaround(assertion, getFirstCharAfterFn) {
if ((0, regexp_ast_analysis_1.isPotentiallyEmpty)(assertion.alternatives, flags)) {
return;
}
const direction = (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind);
const after = getFirstCharAfterFn(assertion, direction, flags);
const firstOf = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(assertion.alternatives, direction, flags));
const accept = assertion.negate ? "reject" : "accept";
const reject = assertion.negate ? "accept" : "reject";
if (after.char.isDisjointWith(firstOf.char) &&
!(after.edge && firstOf.edge)) {
report(assertion, assertion.negate
? "alwaysForNegativeLookaround"
: "alwaysForLookaround", {
kind: assertion.kind,
acceptOrReject: reject,
});
}
const edgeSubset = firstOf.edge || !after.edge;
if (firstOf.exact &&
edgeSubset &&
after.char.isSubsetOf(firstOf.char) &&
isSingleCharacterAssertion(assertion, (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind), flags)) {
report(assertion, assertion.negate
? "alwaysForNegativeLookaround"
: "alwaysForLookaround", {
kind: assertion.kind,
acceptOrReject: accept,
});
}
}
function verifyAssertion(assertion, getFirstCharAfterFn) {
switch (assertion.kind) {
case "start":
case "end":
verifyStartOrEnd(assertion, getFirstCharAfterFn);
break;
case "word":
verifyWordBoundary(assertion, getFirstCharAfterFn);
break;
case "lookahead":
case "lookbehind":
verifyLookaround(assertion, getFirstCharAfterFn);
break;
default:
throw (0, util_1.assertNever)(assertion);
}
}
const allAssertions = [];
return {
onAssertionEnter(assertion) {
verifyAssertion(assertion, regexp_ast_analysis_1.getFirstCharAfter);
allAssertions.push(assertion);
},
onPatternLeave() {
const reorderingGetFirstCharAfter = createReorderingGetFirstCharAfter(reported);
for (const assertion of allAssertions) {
if (!reported.has(assertion)) {
verifyAssertion(assertion, reorderingGetFirstCharAfter);
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,116 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
function hasNegatedLookaroundInBetween(from, to) {
for (let p = from.parent; p && p !== to; p = p.parent) {
if (p.type === "Assertion" &&
(p.kind === "lookahead" || p.kind === "lookbehind") &&
p.negate) {
return true;
}
}
return false;
}
function getUselessProblem(backRef, flags) {
const groups = [backRef.resolved].flat();
const problems = [];
for (const group of groups) {
const messageId = getUselessMessageId(backRef, group, flags);
if (!messageId) {
return null;
}
problems.push({ messageId, group });
}
if (problems.length === 0) {
return null;
}
let problemsToReport;
const problemsInSameDisjunction = problems.filter((problem) => problem.messageId !== "disjunctive");
if (problemsInSameDisjunction.length) {
problemsToReport = problemsInSameDisjunction;
}
else {
problemsToReport = problems;
}
const [{ messageId, group }, ...other] = problemsToReport;
let otherGroups = "";
if (other.length === 1) {
otherGroups = " and another group";
}
else if (other.length > 1) {
otherGroups = ` and other ${other.length} groups`;
}
return {
messageId,
group,
otherGroups,
};
}
function getUselessMessageId(backRef, group, flags) {
const closestAncestor = (0, regexp_ast_analysis_1.getClosestAncestor)(backRef, group);
if (closestAncestor === group) {
return "nested";
}
else if (closestAncestor.type !== "Alternative") {
return "disjunctive";
}
if (hasNegatedLookaroundInBetween(group, closestAncestor)) {
return "intoNegativeLookaround";
}
const matchingDir = (0, regexp_ast_analysis_1.getMatchingDirection)(closestAncestor);
if (matchingDir === "ltr" && backRef.end <= group.start) {
return "forward";
}
else if (matchingDir === "rtl" && group.end <= backRef.start) {
return "backward";
}
if ((0, regexp_ast_analysis_1.isZeroLength)(group, flags)) {
return "empty";
}
return null;
}
exports.default = (0, utils_1.createRule)("no-useless-backreference", {
meta: {
docs: {
description: "disallow useless backreferences in regular expressions",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
nested: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} from within that group.",
forward: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which appears later in the pattern.",
backward: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which appears before in the same lookbehind.",
disjunctive: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which is in another alternative.",
intoNegativeLookaround: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which is in a negative lookaround.",
empty: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which always captures zero characters.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onBackreferenceEnter(backRef) {
const problem = getUselessProblem(backRef, flags);
if (problem) {
context.report({
node,
loc: getRegexpLocation(backRef),
messageId: problem.messageId,
data: {
bref: (0, mention_1.mention)(backRef),
group: (0, mention_1.mention)(problem.group),
otherGroups: problem.otherGroups,
},
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,212 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regex_syntax_1 = require("../utils/regex-syntax");
const ESCAPES_OUTSIDE_CHARACTER_CLASS = new Set("$()*+./?[{|");
const ESCAPES_OUTSIDE_CHARACTER_CLASS_WITH_U = new Set([
...ESCAPES_OUTSIDE_CHARACTER_CLASS,
"}",
]);
exports.default = (0, utils_1.createRule)("no-useless-character-class", {
meta: {
docs: {
description: "disallow character class with one character",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
ignores: {
type: "array",
items: {
type: "string",
minLength: 1,
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages: {
unexpectedCharacterClassWith: "Unexpected character class with one {{type}}. Can remove brackets{{additional}}.",
unexpectedUnnecessaryNestingCharacterClass: "Unexpected unnecessary nesting character class. Can remove brackets.",
},
type: "suggestion",
},
create(context) {
var _a, _b;
const ignores = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.ignores) !== null && _b !== void 0 ? _b : ["="];
function createVisitor({ node, pattern, flags, fixReplaceNode, getRegexpLocation, }) {
const characterClassStack = [];
return {
onExpressionCharacterClassEnter(eccNode) {
characterClassStack.push(eccNode);
},
onExpressionCharacterClassLeave() {
characterClassStack.pop();
},
onCharacterClassEnter(ccNode) {
characterClassStack.push(ccNode);
},
onCharacterClassLeave(ccNode) {
var _a, _b;
characterClassStack.pop();
if (ccNode.negate) {
return;
}
let messageId, messageData;
const unwrapped = ccNode.elements.map((_e, index) => {
var _a, _b;
const element = ccNode.elements[index];
return ((_b = (_a = (index === 0
? getEscapedFirstRawIfNeeded(element)
: null)) !== null && _a !== void 0 ? _a : (index === ccNode.elements.length - 1
? getEscapedLastRawIfNeeded(element)
: null)) !== null && _b !== void 0 ? _b : element.raw);
});
if (ccNode.elements.length !== 1 &&
ccNode.parent.type === "CharacterClass") {
messageId = "unexpectedUnnecessaryNestingCharacterClass";
messageData = {
type: "unnecessary nesting character class",
};
if (!ccNode.elements.length) {
const nextElement = ccNode.parent.elements[ccNode.parent.elements.indexOf(ccNode) + 1];
if (nextElement &&
isNeedEscapedForFirstElement(nextElement)) {
unwrapped.push("\\");
}
}
}
else {
if (ccNode.elements.length !== 1) {
return;
}
const element = ccNode.elements[0];
if (ignores.length > 0 &&
ignores.includes(element.raw)) {
return;
}
if (element.type === "Character") {
if (element.raw === "\\b") {
return;
}
if (/^\\\d+$/u.test(element.raw) &&
!element.raw.startsWith("\\0")) {
return;
}
if (ignores.length > 0 &&
ignores.includes(String.fromCodePoint(element.value))) {
return;
}
if (!(0, utils_1.canUnwrapped)(ccNode, element.raw)) {
return;
}
messageData = { type: "character" };
}
else if (element.type === "CharacterClassRange") {
if (element.min.value !== element.max.value) {
return;
}
messageData = {
type: "character class range",
additional: " and range",
};
unwrapped[0] =
(_b = (_a = getEscapedFirstRawIfNeeded(element.min)) !== null && _a !== void 0 ? _a : getEscapedLastRawIfNeeded(element.min)) !== null && _b !== void 0 ? _b : element.min.raw;
}
else if (element.type === "ClassStringDisjunction") {
if (!characterClassStack.length) {
return;
}
messageData = { type: "string literal" };
}
else if (element.type === "CharacterSet") {
messageData = { type: "character class escape" };
}
else if (element.type === "CharacterClass" ||
element.type === "ExpressionCharacterClass") {
messageData = { type: "character class" };
}
else {
return;
}
messageId = "unexpectedCharacterClassWith";
}
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId,
data: {
type: messageData.type,
additional: messageData.additional || "",
},
fix: fixReplaceNode(ccNode, unwrapped.join("")),
});
function isNeedEscapedForFirstElement(element) {
const char = element.type === "Character"
? element.raw
: element.type === "CharacterClassRange"
? element.min.raw
: null;
if (char == null) {
return false;
}
if (characterClassStack.length) {
if (regex_syntax_1.RESERVED_DOUBLE_PUNCTUATOR_CHARS.has(char) &&
pattern[ccNode.start - 1] === char) {
return true;
}
return (char === "^" &&
ccNode.parent.type === "CharacterClass" &&
ccNode.parent.elements[0] === ccNode);
}
return (flags.unicode
? ESCAPES_OUTSIDE_CHARACTER_CLASS_WITH_U
: ESCAPES_OUTSIDE_CHARACTER_CLASS).has(char);
}
function needEscapedForLastElement(element) {
const char = element.type === "Character"
? element.raw
: element.type === "CharacterClassRange"
? element.max.raw
: null;
if (char == null) {
return false;
}
if (characterClassStack.length) {
return (regex_syntax_1.RESERVED_DOUBLE_PUNCTUATOR_CHARS.has(char) &&
pattern[ccNode.end] === char);
}
return false;
}
function getEscapedFirstRawIfNeeded(firstElement) {
if (isNeedEscapedForFirstElement(firstElement)) {
return `\\${firstElement.raw}`;
}
return null;
}
function getEscapedLastRawIfNeeded(lastElement) {
if (needEscapedForLastElement(lastElement)) {
const lastRaw = lastElement.type === "Character"
? lastElement.raw
: lastElement.type === "CharacterClassRange"
? lastElement.max.raw
: "";
const prefix = lastElement.raw.slice(0, -lastRaw.length);
return `${prefix}\\${lastRaw}`;
}
return null;
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

Some files were not shown because too many files have changed in this diff Show more