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 1 commit
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 the 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 the regex flags to be sorted | :wrench: |
79 changes: 79 additions & 0 deletions docs/rules/sort-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/sort-flags"
description: "require the regex flags to be sorted"
---
# regexp/sort-flags

> require the 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

```json
{
"regexp/sort-flags": ["error", {
"order": ["g", "i", "m", "s", "u", "y"]
}]
}
```

- `"order"` ... An array of your preferred order. If not specified, it will be sorted alphabetically.

### `"order": ["g", "i", "m", "u", "y", "s", "d"]`

<eslint-code-block fix>

```js
/* eslint regexp/sort-flags: ["error", { order: ["g", "i", "m", "u", "y", "s", "d"] }] */

/* ✓ GOOD */
var foo = /abc/gimuys
new RegExp("abc", "gimuysd")

/* ✗ BAD */
var foo = /abc/gimsuy
new RegExp("abc", "dgimsuy")
```

</eslint-code-block>

## :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)
181 changes: 181 additions & 0 deletions lib/rules/sort-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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 the regex flags to be sorted",
ota-meshi marked this conversation as resolved.
Show resolved Hide resolved
// TODO Switch to recommended in the major version.
// recommended: true,
recommended: false,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
order: {
RunDevelopment marked this conversation as resolved.
Show resolved Hide resolved
type: "array",
items: { type: "string", minLength: 1, maxLength: 1 },
uniqueItems: true,
minItems: 2,
},
},
additionalProperties: false,
},
],
messages: {
sortFlags:
"The flags '{{flags}}' should in the order '{{sortedFlags}}'.",
},
type: "suggestion", // "problem",
},
create(context) {
const order: string[] | undefined = context.options[0]?.order
let getOrder: (char: string) => number,
isIgnore: (flags: string) => boolean

if (order) {
getOrder = (c) => order.indexOf(c)

// Ignore if it does not contain more than one flag.
isIgnore = (flags) => {
let cnt = 0
for (const f of flags) {
if (order.includes(f)) {
cnt++
if (cnt > 1) {
return false
}
}
}
return true
}
} else {
getOrder = (c) => c.codePointAt(0)!
isIgnore = () => false
}

/**
* 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 {
if (isIgnore(flagsStr)) {
return flagsStr
}
return [...flagsStr]
.sort((a, b) => {
if (a === b) {
return 0
}
const aOrder = getOrder(a)
const bOrder = getOrder(b)

if (aOrder === -1 || bOrder === -1) {
// Keep original order.
if (bOrder !== -1) {
return compareForOmitAOrder(a, b, bOrder)
}
if (aOrder !== -1) {
return -compareForOmitAOrder(b, a, aOrder)
}

return flagsStr.indexOf(a) - flagsStr.indexOf(b)
}
return aOrder - bOrder
})
.join("")

/**
* Compare function for omit order
*/
function compareForOmitAOrder(
a: string,
b: string,
bOrder: number,
): number {
const aIndex = flagsStr.indexOf(a)
const bIndex = flagsStr.indexOf(b)
if (aIndex < bIndex) {
// Checks if 'b' must move before 'a'.
// e.g. x: 2, a: unknown, b: 1
for (let index = 0; index < aIndex; index++) {
const x = flagsStr[index]
const xOrder = getOrder(x)
if (xOrder !== -1 && bOrder < xOrder) {
return 1
}
}
}
// If 'x' must move before 'b', it does not affect the result of comparing 'a' and 'b'.
// e.g. b: 2, a: unknown, x: 1
return aIndex - bIndex
}
}

/**
* 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[]