-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathno-trivially-nested-assertion.ts
125 lines (114 loc) · 3.88 KB
/
no-trivially-nested-assertion.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import type {
Node as RegExpNode,
Assertion,
LookaroundAssertion,
} from "@eslint-community/regexpp/ast"
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
import { hasSomeDescendant } from "regexp-ast-analysis"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"
function isLookaround(node: RegExpNode): node is LookaroundAssertion {
return (
node.type === "Assertion" &&
(node.kind === "lookahead" || node.kind === "lookbehind")
)
}
/**
* If the given lookaround only contains a single assertion, then this
* assertion will be returned.
*/
function getTriviallyNestedAssertion(
node: LookaroundAssertion,
): Assertion | null {
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
}
/**
* Returns the raw of an assertion that is the negation of the given assertion.
*/
function getNegatedRaw(assertion: Assertion): string | null {
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
}
export default 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", // "problem",
},
create(context) {
function createVisitor({
node,
fixReplaceNode,
getRegexpLocation,
}: RegExpContext): RegExpVisitor.Handlers {
return {
onAssertionEnter(aNode) {
if (aNode.parent.type === "Quantifier") {
// Not all quantifiers are allowed to be the element
// of a quantifier, so we don't even try
return
}
if (!isLookaround(aNode)) {
// there can be no nesting in predefined assertions
return
}
const nested = getTriviallyNestedAssertion(aNode)
if (nested === null) {
return
}
if (
aNode.negate &&
isLookaround(nested) &&
nested.negate &&
hasSomeDescendant(
nested,
(d) => d.type === "CapturingGroup",
)
) {
// e.g. /(?!(?!(a)))\1/ != /(?=(a))\1/
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 defineRegexpVisitor(context, {
createVisitor,
})
},
})