-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathno-optional-assertion.ts
129 lines (120 loc) · 4.55 KB
/
no-optional-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
126
127
128
129
import type {
Alternative,
Assertion,
CapturingGroup,
Group,
Quantifier,
} from "@eslint-community/regexpp/ast"
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
import type { ReadonlyFlags } from "regexp-ast-analysis"
import { isZeroLength } from "regexp-ast-analysis"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"
type ZeroQuantifier = Quantifier & { min: 0 }
/**
* Checks whether the given quantifier is quantifier with a minimum of 0.
*/
function isZeroQuantifier(node: Quantifier): node is ZeroQuantifier {
return node.min === 0
}
/**
* Returns whether the given assertion is optional in regard to the given quantifier with a minimum of 0.
*
* Optional means that all paths in the element if the quantifier which contain the given assertion also have do not
* consume characters. For more information and examples on optional assertions, see the documentation page of this
* rule.
*/
function isOptional(
assertion: Assertion,
quantifier: ZeroQuantifier,
flags: ReadonlyFlags,
): boolean {
let element: Assertion | Quantifier | Group | CapturingGroup = assertion
while (element.parent !== quantifier) {
const parent: Quantifier | Alternative = element.parent
if (parent.type === "Alternative") {
// make sure that all element before and after are zero length
for (const e of parent.elements) {
if (e === element) {
continue // we will ignore this element.
}
if (!isZeroLength(e, flags)) {
// Some element around our target element can possibly consume characters.
// This means, we found a path from or to the assertion which can consume characters.
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 {
// parent.type === "Quantifier"
if (parent.max > 1 && !isZeroLength(parent, flags)) {
// If an ascendant quantifier of the element has maximum of 2 or more, we have to check whether
// the quantifier itself has zero length.
// E.g. in /(?:a|(\b|-){2})?/ the \b is not optional
return false
}
element = parent
}
}
// We reached the top.
// If we made it this far, we could not disprove that the assertion is optional, so it has to optional.
return true
}
export default 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,
}: RegExpContext): RegExpVisitor.Handlers {
// The closest quantifier with a minimum of 0 is stored at index = 0.
const zeroQuantifierStack: ZeroQuantifier[] = []
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 defineRegexpVisitor(context, {
createVisitor,
})
},
})