Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add regexp/sort-flags rule #164

Merged
merged 4 commits into from
Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/prefer-t](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-t.html) | enforce using `\t` | :star::wrench: |
| [regexp/prefer-unicode-codepoint-escapes](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-unicode-codepoint-escapes.html) | enforce use of unicode codepoint escapes | :wrench: |
| [regexp/prefer-w](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-w.html) | enforce using `\w` | :star::wrench: |
| [regexp/sort-flags](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-flags.html) | require regex flags to be sorted | :wrench: |

<!--RULES_TABLE_END-->
<!--RULES_SECTION_END-->
Expand Down
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/prefer-t](./prefer-t.md) | enforce using `\t` | :star::wrench: |
| [regexp/prefer-unicode-codepoint-escapes](./prefer-unicode-codepoint-escapes.md) | enforce use of unicode codepoint escapes | :wrench: |
| [regexp/prefer-w](./prefer-w.md) | enforce using `\w` | :star::wrench: |
| [regexp/sort-flags](./sort-flags.md) | require regex flags to be sorted | :wrench: |
53 changes: 53 additions & 0 deletions docs/rules/sort-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/sort-flags"
description: "require regex flags to be sorted"
---
# regexp/sort-flags

> require regex flags to be sorted

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.

## :book: Rule Details

The flags of JavaScript regular expressions should be sorted alphabetically
because the flags of the `.flags` property of `RegExp` objects are always
sorted. Not sorting flags in regex literals misleads readers into thinking that
the order may have some purpose which it doesn't.

<eslint-code-block fix>

```js
/* eslint regexp/sort-flags: "error" */

/* ✓ GOOD */
var foo = /abc/
var foo = /abc/iu
var foo = /abc/gimsuy

/* ✗ BAD */
var foo = /abc/mi
var foo = /abc/us
```

</eslint-code-block>

## :wrench: Options

Nothing.

## :heart: Compatibility

This rule was taken from [eslint-plugin-clean-regex].
This rule is compatible with [clean-regex/sort-flags] rule.

[eslint-plugin-clean-regex]: https://github.com/RunDevelopment/eslint-plugin-clean-regex
[clean-regex/sort-flags]: https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/docs/rules/sort-flags.md

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/sort-flags.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/sort-flags.ts)
95 changes: 95 additions & 0 deletions lib/rules/sort-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { Literal } from "estree"
import type { RegExpVisitor } from "regexpp/visitor"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"

export default createRule("sort-flags", {
meta: {
docs: {
description: "require regex flags to be sorted",
// TODO Switch to recommended in the major version.
// recommended: true,
recommended: false,
},
fixable: "code",
schema: [],
messages: {
sortFlags:
"The flags '{{flags}}' should in the order '{{sortedFlags}}'.",
},
type: "suggestion", // "problem",
},
create(context) {
/**
* Report
*/
function report(
node: Literal,
flags: string,
sortedFlags: string,
flagsRange: [number, number],
) {
const sourceCode = context.getSourceCode()
context.report({
node,
loc: {
start: sourceCode.getLocFromIndex(flagsRange[0]),
end: sourceCode.getLocFromIndex(flagsRange[1]),
},
messageId: "sortFlags",
data: { flags, sortedFlags },
fix(fixer) {
return fixer.replaceTextRange(flagsRange, sortedFlags)
},
})
}

/**
* Sort regexp flags
*/
function sortFlags(flagsStr: string): string {
return [...flagsStr]
.sort((a, b) => a.codePointAt(0)! - b.codePointAt(0)!)
.join("")
}

/**
* Create visitor
*/
function createVisitor({
regexpNode,
}: RegExpContext): RegExpVisitor.Handlers {
if (regexpNode.type === "Literal") {
const flags = regexpNode.regex.flags
const sortedFlags = sortFlags(flags)
if (flags !== sortedFlags) {
report(regexpNode, flags, sortedFlags, [
regexpNode.range![1] - regexpNode.regex.flags.length,
regexpNode.range![1],
])
}
} else {
const flagsArg = regexpNode.arguments[1]
if (
flagsArg.type === "Literal" &&
typeof flagsArg.value === "string"
) {
const flags = flagsArg.value
const sortedFlags = sortFlags(flags)
if (flags !== sortedFlags) {
report(flagsArg, flags, sortedFlags, [
flagsArg.range![0] + 1,
flagsArg.range![1] - 1,
])
}
}
}

return {} // not visit RegExpNodes
}

return defineRegexpVisitor(context, {
createVisitor,
})
},
})
2 changes: 2 additions & 0 deletions lib/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import preferStarQuantifier from "../rules/prefer-star-quantifier"
import preferT from "../rules/prefer-t"
import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-escapes"
import preferW from "../rules/prefer-w"
import sortFlags from "../rules/sort-flags"

export const rules = [
confusingQuantifier,
Expand Down Expand Up @@ -97,4 +98,5 @@ export const rules = [
preferT,
preferUnicodeCodepointEscapes,
preferW,
sortFlags,
] as RuleModule[]
61 changes: 61 additions & 0 deletions tests/lib/rules/sort-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { RuleTester } from "eslint"
import rule from "../../../lib/rules/sort-flags"

const tester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
})

tester.run("sort-flags", rule as any, {
valid: [
String.raw`/\w/i`,
String.raw`/\w/im`,
String.raw`/\w/gi`,
String.raw`/\w/gimsuy`,
String.raw`new RegExp("\\w", "i")`,
String.raw`new RegExp("\\w", "gi")`,
String.raw`new RegExp("\\w", "gimsuy")`,
String.raw`new RegExp("\\w", "dgimsuy")`,

// ignore
String.raw`
const flags = "yusimg"
new RegExp("\\w", flags)
`,
],
invalid: [
{
code: String.raw`/\w/yusimg`,
output: String.raw`/\w/gimsuy`,
errors: [
{
message: "The flags 'yusimg' should in the order 'gimsuy'.",
column: 5,
},
],
},
{
code: String.raw`new RegExp("\\w", "yusimg")`,
output: String.raw`new RegExp("\\w", "gimsuy")`,
errors: [
{
message: "The flags 'yusimg' should in the order 'gimsuy'.",
column: 20,
},
],
},
{
code: String.raw`new RegExp("\\w", "yusimgd")`,
output: String.raw`new RegExp("\\w", "dgimsuy")`,
errors: [
{
message:
"The flags 'yusimgd' should in the order 'dgimsuy'.",
column: 20,
},
],
},
],
})