@eagleoutice/eslint-config-flowr 2.1.8 → 2.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/patterns.js CHANGED
@@ -1,60 +1,75 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Default patterns of `flowr/replacement-pattern`, see the README for the format.
4
- * The helpers they point at live in flowR: `DfEdge` in `dataflow/graph/edge`, the vertex helpers in
5
- * `dataflow/graph/vertex`, `NoEdges` in `dataflow/graph/graph`, the `RNode` helpers in
6
- * `r-bridge/lang-4.x/ast/model`, and the collection helpers in `util/collections`.
4
+ * The helpers they point at live in flowR, the paths below are the files declaring them.
7
5
  */
8
- const EDGE = 'dataflow/graph/edge';
9
- const VERTEX = 'dataflow/graph/vertex';
10
- const GRAPH = 'dataflow/graph/graph';
11
- const RTYPE = 'r-bridge/lang-4.x/ast/model/type';
12
- const CALL_NODE = 'r-bridge/lang-4.x/ast/model/nodes/r-function-call';
13
- const ARGUMENT_NODE = 'r-bridge/lang-4.x/ast/model/nodes/r-argument';
14
- const SYMBOL_NODE = 'r-bridge/lang-4.x/ast/model/nodes/r-symbol';
15
- const LIST_NODE = 'r-bridge/lang-4.x/ast/model/nodes/r-expression-list';
16
- const RANGE = 'util/range';
17
- const IDENTIFIER = 'dataflow/environments/identifier';
18
- /** `x.types` under the given path */
19
- const types = (prefix) => `[${prefix}.type="MemberExpression"][${prefix}.computed=false][${prefix}.property.name="types"]`;
20
- /** `x.<name>` under the given path, without asserting the node kind, which the caller pins down */
21
- const member = (prefix, name) => `[${prefix}.computed=false][${prefix}.property.name="${name}"]`;
22
- /** `EdgeType.<Member>` / `VertexType.<Member>` / `RType.<Member>` on the right */
23
- const rightIs = (name) => `[right.type="MemberExpression"][right.computed=false][right.object.name="${name}"]`;
24
- /** compared against the literal `0`, either for "some bit is set" or for "none is" */
6
+ const Files = {
7
+ edge: 'dataflow/graph/edge',
8
+ vertex: 'dataflow/graph/vertex',
9
+ graph: 'dataflow/graph/graph',
10
+ rType: 'r-bridge/lang-4.x/ast/model/type',
11
+ call: 'r-bridge/lang-4.x/ast/model/nodes/r-function-call',
12
+ argument: 'r-bridge/lang-4.x/ast/model/nodes/r-argument',
13
+ symbol: 'r-bridge/lang-4.x/ast/model/nodes/r-symbol',
14
+ string: 'r-bridge/lang-4.x/ast/model/nodes/r-string',
15
+ list: 'r-bridge/lang-4.x/ast/model/nodes/r-expression-list',
16
+ model: 'r-bridge/lang-4.x/ast/model/model',
17
+ nodeId: 'r-bridge/lang-4.x/ast/model/processing/node-id',
18
+ range: 'util/range',
19
+ identifier: 'dataflow/environments/identifier',
20
+ environment: 'dataflow/environments/environment',
21
+ arrays: 'util/collections/arrays',
22
+ strings: 'util/text/strings'
23
+ };
24
+ /* selector parts, each about the node at the given path, `''` being the match itself */
25
+ const at = (path, key) => path === '' ? key : `${path}.${key}`;
26
+ /** `x.<name>`, without asserting the node kind */
27
+ const member = (path, name) => `[${at(path, 'computed')}=false][${at(path, 'property.name')}="${name}"]`;
28
+ /** `x.<name>` */
29
+ const prop = (path, name) => `[${at(path, 'type')}="MemberExpression"]${member(path, name)}`;
30
+ /** a call of `x.<method>(...)` */
31
+ const call = (path, method) => `[${at(path, 'type')}="CallExpression"]${prop(at(path, 'callee'), method)}`;
32
+ /** a call of `<Helper>.<method>(...)` with `n` arguments */
33
+ const helperCall = (path, helper, method, n = 1) => `${call(path, method)}[${at(path, 'callee.object.name')}="${helper}"][${at(path, 'arguments.length')}=${n}]`;
34
+ /** the identifier `<name>` */
35
+ const identifier = (path, name) => `[${at(path, 'type')}="Identifier"][${at(path, 'name')}="${name}"]`;
36
+ /** the string literal `<value>`, or any string literal */
37
+ const text = (path, value) => `[${at(path, 'type')}="Literal"][${at(path, 'value')}=${value === undefined ? 'type(string)' : `"${value}"`}]`;
38
+ /** `!x` */
39
+ const not = (path) => `[${at(path, 'type')}="UnaryExpression"][${at(path, 'operator')}="!"]`;
40
+ /** `x === undefined` or, `set`, `x !== undefined` */
41
+ const versusUndefined = (at, set) => `[${at}.type="BinaryExpression"][${at}.operator="${set ? '!==' : '==='}"]${identifier(`${at}.right`, 'undefined')}`;
42
+ /** `<Enum>.<Member>` */
43
+ const enumMember = (at, name) => `[${at}.type="MemberExpression"][${at}.computed=false][${at}.object.name="${name}"]`;
44
+ /** `x.types` */
45
+ const types = (at) => prop(at, 'types');
46
+ /** compared against `0`, for "some bit is set" or, `nonZero` false, for "none is" */
25
47
  const versusZero = (nonZero) => `BinaryExpression[operator=${nonZero ? '/^(!==|>)$/' : '"==="'}][right.type="Literal"][right.value=0]`;
26
- /** the `x.types & T` on the left of such a comparison */
27
- const mask = '[left.type="BinaryExpression"][left.operator="&"]';
28
- /** a call of `<prefix>.<property>(...)` */
29
- const call = (prefix, property) => `[${prefix}.type="CallExpression"][${prefix}.callee.type="MemberExpression"][${prefix}.callee.property.name="${property}"]`;
30
- /** a call of `<Helper>.<property>(<one argument>)` under the given path */
31
- const helperCall = (prefix, helper, property) => `${call(prefix, property)}[${prefix}.callee.object.name="${helper}"][${prefix}.arguments.length=1]`;
32
- /** the same call as the match itself rather than below it */
33
- const rootHelperCall = (helper, property) => `CallExpression[callee.type="MemberExpression"][callee.computed=false][callee.property.name="${property}"]`
34
- + `[callee.object.name="${helper}"][arguments.length=1]`;
35
- /** `x === undefined` / `x !== undefined` under the given path */
36
- const versusUndefined = (prefix, set) => `[${prefix}.type="BinaryExpression"][${prefix}.operator="${set ? '!==' : '==='}"][${prefix}.right.type="Identifier"][${prefix}.right.name="undefined"]`;
37
- const destructuringHint = ' Prefer keeping the edge over destructuring `types`, the helpers take it directly and the wrapper object goes away.';
38
- /** `x.types === T`, on the edge itself or on a destructured `types` */
39
- const edgeIsOnly = (id, { negated = false, destructured = false } = {}) => ({
40
- id,
41
- selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"]`
42
- + (destructured ? `[left.type="Identifier"][left.name="types"]${rightIs('EdgeType')}` : `${types('left')}:not([right.type="Literal"])`),
48
+ /** the root as `===` or, `negated`, `!==` */
49
+ const equality = (negated) => `BinaryExpression[operator="${negated ? '!==' : '==='}"]`;
50
+ const bang = (negated) => negated ? '!' : '';
51
+ /** A pattern and its negated twin, under the two ids given. */
52
+ const negatable = (ids, make) => [{ id: ids[0], ...make(false) }, { id: ids[1], ...make(true) }];
53
+ /** A pattern and its twin spelled with `?.`, which wraps the member expression in a chain. */
54
+ const optionalToo = (ids, make) => [{ id: ids[0], ...make(false) }, { id: ids[1], ...make(true) }];
55
+ /** `x.types === T`, on the edge or on a destructured `types` */
56
+ const edgeIsOnly = (negated, destructured) => ({
57
+ selector: equality(negated) + (destructured ? `${identifier('left', 'types')}${enumMember('right', 'EdgeType')}` : `${types('left')}:not([right.type="Literal"])`),
43
58
  capture: destructured ? { type: 'right' } : { edge: 'left.object', type: 'right' },
44
- replace: `${negated ? '!' : ''}DfEdge.isOnlyType(${destructured ? '{ types }' : '{{edge}}'}, {{type}})`,
45
- declaredIn: destructured ? { 'right.object': EDGE } : { 'left.property': EDGE },
59
+ replace: `${bang(negated)}DfEdge.isOnlyType(${destructured ? '{ types }' : '{{edge}}'}, {{type}})`,
60
+ declaredIn: destructured ? { 'right.object': Files.edge } : { 'left.property': Files.edge },
46
61
  message: `\`${destructured ? '' : 'x.'}types ${negated ? '!==' : '==='} T\` `
47
62
  + (negated ? 'holds as soon as any other type is set' : 'holds only if T is the *only* type')
48
- + `; say so with \`${negated ? '!' : ''}DfEdge.isOnlyType\`, or use \`DfEdge.${negated ? 'doesNotIncludeType' : 'includesType'}\` for "${negated ? 'has not' : 'has'} this type".`
49
- + (destructured ? destructuringHint : '')
63
+ + `; say so with \`${bang(negated)}DfEdge.isOnlyType\`, or use \`DfEdge.${negated ? 'doesNotIncludeType' : 'includesType'}\` for "${negated ? 'has not' : 'has'} this type".`
64
+ + (destructured ? ' Prefer keeping the edge over destructuring `types`, the helpers take it directly.' : '')
50
65
  });
51
66
  /** `(x.types & T) !== 0` and its inverse */
52
67
  const edgeMask = (id, helper, nonZero) => ({
53
68
  id,
54
- selector: `${versusZero(nonZero)}${mask}${types('left.left')}`,
69
+ selector: `${versusZero(nonZero)}[left.type="BinaryExpression"][left.operator="&"]${types('left.left')}`,
55
70
  capture: { edge: 'left.left.object', type: 'left.right' },
56
71
  replace: `DfEdge.${helper}({{edge}}, {{type}})`,
57
- declaredIn: { 'left.left.property': EDGE },
72
+ declaredIn: { 'left.left.property': Files.edge },
58
73
  message: `Use \`DfEdge.${helper}\` instead of testing the bitmask by hand.`
59
74
  });
60
75
  /** `x.types !== 0`, the literal-0 comparisons {@link edgeIsOnly} leaves out */
@@ -63,149 +78,286 @@ const edgeStates = (id, helper, what, nonZero) => ({
63
78
  selector: `${versusZero(nonZero)}${types('left')}`,
64
79
  capture: { edge: 'left.object' },
65
80
  replace: `DfEdge.${helper}({{edge}})`,
66
- declaredIn: { 'left.property': EDGE },
81
+ declaredIn: { 'left.property': Files.edge },
67
82
  message: `Use \`DfEdge.${helper}\` to ask whether the edge states ${what}.`
68
83
  });
69
84
  /**
70
- * A discriminator compared against its enum, `v.tag === VertexType.X` and `n.type === RType.X`.
71
- * `optional` covers the `?.` spelling, which parses as a chain around the same member expression.
72
- * `declaring` maps a path of the match to the file its symbol has to come from; the enum on the right is
73
- * always pinned, the property on the left only where one file declares it for every case.
85
+ * `v.tag === VertexType.X` and `n.type === RType.X`, in all four spellings.
86
+ * The helpers are named after the enum members, so `{{type|last}}` spells the check: `VertexType.Use` to `DfgVertex.isUse`,
87
+ * `RType.Symbol` to `RSymbol.is`. The property is only pinned for vertices, on the `RNode` union `type` has no declaring file.
74
88
  */
75
- const discriminatorIs = (id, { property, enumName, enumFile, propertyFile, helper, member: name = 'is', hint }, { negated = false, optional = false } = {}) => {
89
+ const discriminatorIs = (ids, { property, enumName, enumFile, propertyFile, helper, hint }) => [false, true].flatMap(optional => {
76
90
  const left = optional ? 'left.expression' : 'left';
77
- return {
78
- id,
79
- selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"][left.type="${optional ? 'ChainExpression' : 'MemberExpression'}"]${member(left, property)}${rightIs(enumName)}`,
91
+ return negatable([ids[optional ? 2 : 0], ids[optional ? 3 : 1]], negated => ({
92
+ selector: `${equality(negated)}[left.type="${optional ? 'ChainExpression' : 'MemberExpression'}"]${member(left, property)}${enumMember('right', enumName)}`,
80
93
  capture: { subject: `${left}.object`, type: 'right' },
81
- replace: `${negated ? '!' : ''}${helper}.${name}({{subject}})`,
94
+ replace: `${bang(negated)}${helper}({{subject}})`,
82
95
  declaredIn: { 'right.object': enumFile, ...propertyFile ? { [`${left}.property`]: propertyFile } : {} },
83
- message: `${hint} (\`${helper}.${name}\`), it narrows the type as well.`
84
- };
85
- };
86
- /**
87
- * `v.tag === VertexType.X`. One `DfgVertex` helper answers for every kind, and its members are named after
88
- * the enum, so `{{type|last}}` spells the check without a table: `VertexType.Use` to `DfgVertex.isUse`.
89
- */
90
- const vertexIs = (id, options) => discriminatorIs(id, {
91
- property: 'tag', enumName: 'VertexType', enumFile: VERTEX, propertyFile: VERTEX,
92
- helper: 'DfgVertex', member: 'is{{type|last}}', hint: 'Compare through the vertex helper'
93
- }, options);
94
- /**
95
- * `n.type === RType.X`. Every `RType` member has a helper object of the same name prefixed with `R`
96
- * (`RType.Symbol` to `RSymbol`), so `{{type|last}}` names it without a table.
97
- * Only the enum is pinned to its file: `type` is declared once per node interface, and on the `RNode`
98
- * union it resolves to a synthesized symbol, so demanding a declaring file for it would match nothing.
99
- */
100
- const nodeIs = (id, options) => discriminatorIs(id, {
101
- property: 'type', enumName: 'RType', enumFile: RTYPE,
102
- helper: 'R{{type|last}}', hint: 'Ask the AST node helper'
103
- }, options);
104
- /**
105
- * `<Helper>.is(x) && <extra check on x>`, the body of a more specific guard written out.
106
- * `guard` pins down the right-hand side, `subject` is the path of the operand it repeats.
107
- */
108
- const guardWrittenOut = ({ id, helper, member: narrower, file, guard, subject, message, fix }) => ({
96
+ message: `${hint} (\`${helper}\`), it narrows the type as well.`
97
+ }));
98
+ });
99
+ /** `<Helper>.is(x) && <extra check on x>`, a more specific guard written out; `subject` is where the check repeats `x` */
100
+ const guardWrittenOut = (id, helper, narrower, file, guard, subject, message) => ({
109
101
  id,
110
102
  selector: `LogicalExpression[operator="&&"]${helperCall('left', helper, 'is')}${guard}`,
111
103
  capture: { node: 'left.arguments.0' },
112
104
  replace: `${helper}.${narrower}({{node}})`,
113
105
  declaredIn: { 'left.callee.object': file },
114
106
  sameText: [['left.arguments.0', subject]],
115
- ...fix === undefined ? {} : { fix },
116
107
  message
117
108
  });
109
+ /** `Identifier.getName(<call>.functionName.content)` */
110
+ const calledName = (at) => `${helperCall(at, 'Identifier', 'getName')}${prop(`${at}.arguments.0`, 'content')}${prop(`${at}.arguments.0.object`, 'functionName')}`;
111
+ /**
112
+ * `RFunctionCall.isNamed(x) && Identifier.getName(x.functionName.content) === 'f'`, and the negation
113
+ * `!RFunctionCall.isNamed(x) || ... !== 'f'`. `inSet` covers `Names.has(...)` in place of the comparison.
114
+ */
115
+ const namedCallOf = (inSet) => (negated) => {
116
+ const guard = negated ? 'left.argument' : 'left';
117
+ const test = inSet ? (negated ? 'right.argument' : 'right') : 'right';
118
+ const nameCall = inSet ? `${test}.arguments.0` : 'right.left';
119
+ return {
120
+ selector: `LogicalExpression[operator="${negated ? '||' : '&&'}"]${negated ? not('left') : ''}${helperCall(guard, 'RFunctionCall', 'isNamed')}`
121
+ + (inSet
122
+ ? `${negated ? not('right') : ''}${call(test, 'has')}[${test}.arguments.length=1]`
123
+ : `[right.type="BinaryExpression"][right.operator="${negated ? '!==' : '==='}"]:matches(${text('right.right')}, [right.right.type="Identifier"])`)
124
+ + calledName(nameCall),
125
+ capture: { node: `${guard}.arguments.0`, names: inSet ? `${test}.callee.object` : 'right.right' },
126
+ replace: `${bang(negated)}RFunctionCall.isNamed({{node}}, {{names}})`,
127
+ declaredIn: { [`${guard}.callee.object`]: Files.call },
128
+ sameText: [[`${guard}.arguments.0`, `${nameCall}.arguments.0.object.object`]],
129
+ message: `Use \`${bang(negated)}RFunctionCall.isNamed({{node}}, {{names}})\`, it checks the call and its name in one go and still narrows.`
130
+ };
131
+ };
132
+ /** `g.outgoingEdges(id) ?? []` or `?? NoEdges`, which `edgesFrom`/`edgesTo` already are */
133
+ const edgesFallback = (id, raw, total) => ({
134
+ id,
135
+ selector: 'LogicalExpression[operator="??"]:matches([right.type="ArrayExpression"][right.elements.length=0], [right.type="Identifier"][right.name="NoEdges"])'
136
+ + `${call('left', raw)}[left.callee.computed=false][left.arguments.length=1]`,
137
+ capture: { graph: 'left.callee.object', id: 'left.arguments.0' },
138
+ replace: `{{graph}}.${total}({{id}})`,
139
+ declaredIn: { 'left.callee.property': Files.graph },
140
+ message: `\`${total}\` is \`${raw}\` without the fallback: it answers with the shared empty map itself.`
141
+ });
118
142
  /**
119
- * `symbol.content === 'name'`. `content` of an {@link RSymbol} is an `Identifier`, which carries a namespace
120
- * and turns into an array once it has one, so `===` against a bare name silently misses `pkg::name`.
143
+ * `x.info.fullLexeme ?? x.lexeme` and its mirrors, reading the text or the range of a node by hand.
144
+ * `infoFirst` says which side of the `??` goes through `info`.
121
145
  */
122
- const symbolName = (id, negated) => ({
146
+ const nodeFallback = (ids, infoProp, plainProp, infoFirst, helper, message) => optionalToo(ids, optional => {
147
+ const [infoSide, plainSide] = infoFirst ? ['left', 'right'] : ['right', 'left'];
148
+ const info = optional ? `${infoSide}.expression` : infoSide, plain = optional ? `${plainSide}.expression` : plainSide;
149
+ const kind = optional ? 'ChainExpression' : 'MemberExpression';
150
+ return {
151
+ selector: `LogicalExpression[operator="??"][${infoSide}.type="${kind}"][${plainSide}.type="${kind}"]${member(info, infoProp)}${prop(`${info}.object`, 'info')}${member(plain, plainProp)}`,
152
+ capture: { node: `${plain}.object` },
153
+ replace: `${helper}({{node}})`,
154
+ declaredIn: { [`${info}.property`]: Files.model, [`${plain}.property`]: Files.model },
155
+ sameText: [[`${info}.object.object`, `${plain}.object`]],
156
+ message
157
+ };
158
+ });
159
+ /** `range?.[0]` and friends, the accessors say which index is which */
160
+ const rangeIndex = (id, index, reader) => ({
123
161
  id,
124
- selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"][left.type="MemberExpression"]${member('left', 'content')}[right.type="Literal"][right.value=type(string)]`,
162
+ selector: 'ChainExpression[expression.type="MemberExpression"][expression.computed=true][expression.optional=true]'
163
+ + `[expression.property.type="Literal"][expression.property.value=${index}][expression.object.type="MemberExpression"]`
164
+ + ':matches([expression.object.property.name="location"], [expression.object.property.name="fullRange"])',
165
+ capture: { range: 'expression.object' },
166
+ replace: `SourceRange.${reader}({{range}})`,
167
+ declaredIn: { 'expression.object.property': Files.model },
168
+ message: `\`range?.[${index}]\` hides what the index means; \`SourceRange.${reader}\` says it and answers \`undefined\` for an absent range just the same.`
169
+ });
170
+ /** `arg === EmptyArgument ? undefined : arg.value`, and the inverted spelling */
171
+ const unpacked = (id, empty) => {
172
+ const [value, absent] = empty ? ['alternate', 'consequent'] : ['consequent', 'alternate'];
173
+ return {
174
+ id,
175
+ selector: `ConditionalExpression[test.type="BinaryExpression"][test.operator="${empty ? '===' : '!=='}"]${identifier('test.right', 'EmptyArgument')}`
176
+ + `${identifier(absent, 'undefined')}${prop(value, 'value')}`,
177
+ capture: { arg: 'test.left' },
178
+ replace: 'RArgument.valueOf({{arg}})',
179
+ declaredIn: { 'test.right': Files.call, [`${value}.property`]: Files.argument },
180
+ sameText: [['test.left', `${value}.object`]],
181
+ message: '`RArgument.valueOf` is this: the value of an argument, `undefined` for an empty one.'
182
+ };
183
+ };
184
+ /**
185
+ * `args.find(a => <a is not empty> && a.name?.content === 'x')`.
186
+ * `guard` is how the emptiness is tested: `a !== EmptyArgument`, `!RArgument.isEmpty(a)` or `RArgument.isNotEmpty(a)`.
187
+ */
188
+ const findNamed = (id, guard) => {
189
+ const body = 'arguments.0.body', test = `${body}.left`, rhs = `${body}.right`, content = `${rhs}.left.expression`;
190
+ const [reference, pin, shape] = {
191
+ neq: [`${test}.left`, `${test}.right`, `[${test}.type="BinaryExpression"][${test}.operator="!=="]${identifier(`${test}.right`, 'EmptyArgument')}`],
192
+ isEmpty: [`${test}.argument.arguments.0`, `${test}.argument.callee.object`, `${not(test)}${helperCall(`${test}.argument`, 'RArgument', 'isEmpty')}`],
193
+ isNotEmpty: [`${test}.arguments.0`, `${test}.callee.object`, helperCall(test, 'RArgument', 'isNotEmpty')]
194
+ }[guard];
195
+ return {
196
+ id,
197
+ selector: `CallExpression${prop('callee', 'find')}[arguments.length=1]`
198
+ + `[arguments.0.type="ArrowFunctionExpression"][arguments.0.params.length=1][${body}.type="LogicalExpression"][${body}.operator="&&"]`
199
+ + `${shape}[${rhs}.type="BinaryExpression"][${rhs}.operator="==="][${rhs}.left.type="ChainExpression"]`
200
+ + `${member(content, 'content')}${prop(`${content}.object`, 'name')}:matches([${rhs}.right.type="Literal"], [${rhs}.right.type="Identifier"])`,
201
+ capture: { args: 'callee.object', name: `${rhs}.right` },
202
+ replace: 'RArgument.findNamed({{args}}, {{name}})',
203
+ declaredIn: { [pin]: guard === 'neq' ? Files.call : Files.argument, [`${content}.property`]: Files.symbol },
204
+ sameText: [['arguments.0.params.0', reference], ['arguments.0.params.0', `${content}.object.object`]],
205
+ message: '`RArgument.findNamed` finds the argument given by this exact name and skips the empty ones itself.'
206
+ };
207
+ };
208
+ /** `[...new Set(<x>)]` */
209
+ const spreadSet = (extra) => 'ArrayExpression[elements.length=1][elements.0.type="SpreadElement"][elements.0.argument.type="NewExpression"]'
210
+ + `[elements.0.argument.callee.name="Set"][elements.0.argument.arguments.length=1]${extra}`;
211
+ /** `[...g.edgesFrom(id)].filter(([, e]) => DfEdge.includesType(e, T)).map(([t]) => t)` and its `edgesTo` twin */
212
+ const edgeNeighbors = (id, raw, helper) => {
213
+ const filter = 'callee.object', spread = `${filter}.callee.object.elements.0`, test = `${filter}.arguments.0`;
214
+ return {
215
+ id,
216
+ selector: `CallExpression${call('', 'map')}[arguments.length=1]`
217
+ + '[arguments.0.type="ArrowFunctionExpression"][arguments.0.params.length=1][arguments.0.params.0.type="ArrayPattern"]'
218
+ + '[arguments.0.params.0.elements.length=1][arguments.0.body.type="Identifier"]'
219
+ + `${call(filter, 'filter')}[${filter}.arguments.length=1][${filter}.callee.object.type="ArrayExpression"]`
220
+ + `[${filter}.callee.object.elements.length=1][${spread}.type="SpreadElement"]${call(`${spread}.argument`, raw)}[${spread}.argument.arguments.length=1]`
221
+ + `[${test}.type="ArrowFunctionExpression"][${test}.params.0.type="ArrayPattern"][${test}.params.0.elements.length=2]`
222
+ + `[${test}.params.0.elements.1.type="Identifier"]${helperCall(`${test}.body`, 'DfEdge', 'includesType', 2)}`,
223
+ capture: { graph: `${spread}.argument.callee.object`, node: `${spread}.argument.arguments.0`, types: `${test}.body.arguments.1` },
224
+ replace: `${helper}({{graph}}, {{node}}, {{types}})`,
225
+ declaredIn: { [`${spread}.argument.callee.property`]: Files.graph, [`${test}.body.callee.property`]: Files.edge },
226
+ sameText: [['arguments.0.params.0.elements.0', 'arguments.0.body'], [`${test}.params.0.elements.1`, `${test}.body.arguments.0`]],
227
+ message: `\`${helper}\` is this walk, name it instead of spelling out the filter.`
228
+ };
229
+ };
230
+ /** `x.name === 'lit'` on a vertex */
231
+ const vertexName = (negated) => ({
232
+ selector: `${equality(negated)}${prop('left', 'name')}${text('right')}`,
233
+ capture: { vertex: 'left.object', name: 'right' },
234
+ replace: `Identifier.getName({{vertex}}.name) ${negated ? '!==' : '==='} {{name}}`,
235
+ declaredIn: { 'left.property': Files.vertex },
236
+ /* what the call is, rather than how it is spelled, may be what is meant */
237
+ fix: false,
238
+ message: 'The name of a call is how it is spelled: `pkg::name` is an array here, and an alias or a shadowing definition is not seen at all. Ask what it is with `DfgVertex.hasOrigin`, or `Dataflow.qualify` for the package.'
239
+ });
240
+ /** `symbol.content === 'name'`, which misses `pkg::name` as an identifier with a namespace is an array */
241
+ const symbolName = (negated) => ({
242
+ selector: `${equality(negated)}${prop('left', 'content')}${text('right')}`,
125
243
  capture: { symbol: 'left.object', name: 'right' },
126
244
  replace: `Identifier.getName({{symbol}}.content) ${negated ? '!==' : '==='} {{name}}`,
127
- declaredIn: { 'left.property': SYMBOL_NODE },
128
- /* which of the two is meant is the author's call, so this is offered rather than applied */
245
+ declaredIn: { 'left.property': Files.symbol },
246
+ /* whether the namespace should count is the author's call */
129
247
  fix: false,
130
248
  message: 'An `Identifier` is not its name: `pkg::{{name}}` is an array and never `===` a string. Compare `Identifier.getName({{symbol}}.content)`, or `Identifier.matches` when the namespace should count.'
131
249
  });
132
- /** an empty-array or `NoEdges` fallback, the two ways the absent case used to be written out */
133
- const emptyEdges = ':matches([right.type="ArrayExpression"][right.elements.length=0], [right.type="Identifier"][right.name="NoEdges"])';
134
- /**
135
- * `graph.outgoingEdges(id) ?? <nothing>`, which `edgesFrom()`/`edgesTo()` already are: they answer with the
136
- * shared empty map for a vertex without edges, so the fallback is dead weight and an `[]` one allocates on
137
- * every miss besides. One pattern per direction, as the replacement names the method.
138
- */
139
- const edgesFallback = (id, raw, total) => ({
250
+ /** `c.functionName.lexeme === 'lit'` on a call */
251
+ const calleeLexeme = (negated) => ({
252
+ selector: `${equality(negated)}${prop('left', 'lexeme')}${prop('left.object', 'functionName')}${text('right')}`,
253
+ capture: { call: 'left.object.object', name: 'right' },
254
+ replace: `${bang(negated)}RFunctionCall.isNamed({{call}}, {{name}})`,
255
+ declaredIn: { 'left.property': Files.model },
256
+ /* the lexeme keeps quotes and the namespace, the helper compares the name */
257
+ fix: false,
258
+ message: 'The lexeme is how the callee is spelled, `RFunctionCall.isNamed` compares its name and keeps `pkg::{{name}}` and backticks out of the way.'
259
+ });
260
+ /** `isNamed(n) ? n.functionName.content : undefined`, guarded by the call or by a read of `named` */
261
+ const calledNameOf = (id, guard) => ({
140
262
  id,
141
- selector: `LogicalExpression[operator="??"]${emptyEdges}[left.type="CallExpression"]`
142
- + `[left.callee.type="MemberExpression"][left.callee.computed=false][left.callee.property.name="${raw}"][left.arguments.length=1]`,
143
- capture: { graph: 'left.callee.object', id: 'left.arguments.0' },
144
- replace: `{{graph}}.${total}({{id}})`,
145
- declaredIn: { 'left.callee.property': GRAPH },
146
- message: `\`${total}\` is \`${raw}\` without the fallback: it answers with the shared empty map itself.`
263
+ selector: `ConditionalExpression${identifier('alternate', 'undefined')}${prop('consequent', 'content')}${prop('consequent.object', 'functionName')}`
264
+ + (guard === 'call' ? helperCall('test', 'RFunctionCall', 'isNamed') : prop('test', 'named')),
265
+ capture: { node: 'consequent.object.object' },
266
+ replace: 'RFunctionCall.calledName({{node}})',
267
+ declaredIn: { 'consequent.object.property': Files.call, [guard === 'call' ? 'test.callee.object' : 'test.property']: Files.call },
268
+ sameText: [[guard === 'call' ? 'test.arguments.0' : 'test.object', 'consequent.object.object']],
269
+ message: '`RFunctionCall.calledName` is this: the name of a named call, `undefined` otherwise.'
270
+ });
271
+ /** `v.origin.includes(o)` behind a call guard, as `isFunctionCall(v) && ...` or `!isFunctionCall(v) || !...` */
272
+ const originGuarded = (negated) => {
273
+ const guard = negated ? 'left.argument' : 'left', includes = negated ? 'right.argument' : 'right';
274
+ return {
275
+ selector: `LogicalExpression[operator="${negated ? '||' : '&&'}"]${negated ? `${not('left')}${not('right')}` : ''}${helperCall(guard, 'DfgVertex', 'isFunctionCall')}`
276
+ + `${call(includes, 'includes')}[${includes}.arguments.length=1]${prop(`${includes}.callee.object`, 'origin')}`,
277
+ capture: { vertex: `${guard}.arguments.0`, origin: `${includes}.arguments.0` },
278
+ replace: `${bang(negated)}DfgVertex.hasOrigin({{vertex}}, {{origin}})`,
279
+ declaredIn: { [`${guard}.callee.object`]: Files.vertex, [`${includes}.callee.object.property`]: Files.vertex },
280
+ sameText: [[`${guard}.arguments.0`, `${includes}.callee.object.object`]],
281
+ /* `hasOrigin` does not narrow, code below may rely on the guard */
282
+ fix: false,
283
+ message: `Use \`${bang(negated)}DfgVertex.hasOrigin\`, it is exactly this check. Check the narrowing first, \`hasOrigin\` returns a plain boolean.`
284
+ };
285
+ };
286
+ /** `String(id).startsWith('built-in:')` and the plain `id.startsWith('built-in:')` */
287
+ const builtInPrefix = (id, wrapped) => ({
288
+ id,
289
+ selector: `CallExpression${prop('callee', 'startsWith')}[arguments.length=1]${text('arguments.0', 'built-in:')}`
290
+ + (wrapped ? '[callee.object.type="CallExpression"][callee.object.callee.name="String"][callee.object.arguments.length=1]' : ':not([callee.object.type="CallExpression"])'),
291
+ capture: { id: wrapped ? 'callee.object.arguments.0' : 'callee.object' },
292
+ replace: 'NodeId.isBuiltIn({{id}})',
293
+ helperIn: Files.nodeId,
294
+ /* a lexeme or a path is not a node id */
295
+ ...wrapped ? {} : { fix: false },
296
+ message: 'Ask `NodeId.isBuiltIn`, the `built-in:` prefix is the helper\'s business.'
297
+ });
298
+ const builtInId = (id, selector, name) => ({
299
+ id,
300
+ selector,
301
+ capture: { name },
302
+ replace: 'NodeId.toBuiltIn({{name}})',
303
+ helperIn: Files.nodeId,
304
+ message: 'Build a built-in id with `NodeId.toBuiltIn`, the prefix is the helper\'s business.'
305
+ });
306
+ /** `filter(...).length > 0` or `=== 0`, `some` stops at the first hit */
307
+ const someInsteadOfFilter = (id, nonZero) => ({
308
+ id,
309
+ selector: `${versusZero(nonZero)}${prop('left', 'length')}${call('left.object', 'filter')}[left.object.arguments.length=1]`,
310
+ capture: { array: 'left.object.callee.object', predicate: 'left.object.arguments.0' },
311
+ replace: `${bang(!nonZero)}{{array}}.some({{predicate}})`,
312
+ message: `\`filter(...).length ${nonZero ? '> 0' : '=== 0'}\` walks the whole list and allocates the matches, \`${bang(!nonZero)}some\` stops at the first hit.`
147
313
  });
148
314
  const patterns = [
149
- edgeIsOnly('edge-is-only-type'),
150
- edgeIsOnly('edge-is-not-only-type', { negated: true }),
151
- edgeIsOnly('edge-is-only-type-destructured', { destructured: true }),
152
- edgeIsOnly('edge-is-not-only-type-destructured', { negated: true, destructured: true }),
315
+ ...negatable(['edge-is-only-type', 'edge-is-not-only-type'], negated => edgeIsOnly(negated, false)),
316
+ ...negatable(['edge-is-only-type-destructured', 'edge-is-not-only-type-destructured'], negated => edgeIsOnly(negated, true)),
153
317
  edgeStates('edge-has-any-type', 'hasAnyType', 'anything', true),
154
318
  edgeStates('edge-has-no-type', 'hasNoType', 'nothing', false),
155
319
  edgeMask('edge-includes-type', 'includesType', true),
156
320
  edgeMask('edge-does-not-include-type', 'doesNotIncludeType', false),
157
321
  {
158
- /*
159
- * `SourceLocation.fromNode(n) ?? []` only exists so the positional destructure below it survives an
160
- * absent location; the view reads the same places by name and combines with `?.`
161
- */
322
+ /* the `?? []` only exists so a positional destructure survives an absent location */
162
323
  id: 'location-at-node',
163
324
  selector: `LogicalExpression[operator="??"][right.type="ArrayExpression"][right.elements.length=0]${helperCall('left', 'SourceLocation', 'fromNode')}`,
164
325
  capture: { node: 'left.arguments.0' },
165
326
  replace: 'SourceLocation.at({{node}})',
166
- declaredIn: { 'left.callee.object': RANGE },
167
- /* the destructure around it has to become property reads, which is the author's call */
327
+ declaredIn: { 'left.callee.object': Files.range },
328
+ /* the destructure has to become property reads */
168
329
  fix: false,
169
330
  message: '`SourceLocation.at({{node}})` answers with a view: `.startLine`, `.file`, and `?.` for the absent case, so the `?? []` and the positional destructure both go away.'
170
331
  },
171
332
  {
172
- /*
173
- * every source range is a source location without a file, so the range readers take a location as it
174
- * is and `SourceLocation` re-exports them under its own name
175
- */
333
+ /* a source range is a source location without a file, so the readers take a location as it is */
176
334
  id: 'location-range-hop',
177
- selector: 'CallExpression[callee.type="MemberExpression"][callee.computed=false]'
178
- + '[callee.object.name=/^Source(Range|Location)$/][arguments.length=1]'
179
- + `${helperCall('arguments.0', 'SourceLocation', 'getRange')}`,
335
+ selector: 'CallExpression[callee.type="MemberExpression"][callee.computed=false][callee.object.name=/^Source(Range|Location)$/][arguments.length=1]'
336
+ + helperCall('arguments.0', 'SourceLocation', 'getRange'),
180
337
  capture: { reader: 'callee.property', location: 'arguments.0.arguments.0' },
181
338
  replace: 'SourceLocation.{{reader}}({{location}})',
182
- declaredIn: { 'callee.object': RANGE, 'arguments.0.callee.object': RANGE },
339
+ declaredIn: { 'callee.object': Files.range, 'arguments.0.callee.object': Files.range },
183
340
  message: 'Drop the `getRange` hop, `SourceLocation.{{reader}}` reads the location directly.'
184
341
  },
185
342
  {
186
- /* the readers answer `undefined` for an absent input, so the guard repeats what the type already says */
343
+ /* the readers answer `undefined` for an absent input */
187
344
  id: 'accessor-undefined-guard',
188
- selector: `ConditionalExpression${versusUndefined('test', false)}`
189
- + '[consequent.type="Identifier"][consequent.name="undefined"]'
345
+ selector: `ConditionalExpression${versusUndefined('test', false)}${identifier('consequent', 'undefined')}`
190
346
  + '[alternate.type="CallExpression"][alternate.callee.type="MemberExpression"][alternate.callee.computed=false]'
191
- + '[alternate.callee.property.name=/^(get|format|view|at)/]'
192
- + '[alternate.callee.object.name=/^Source(Range|Location)$/][alternate.arguments.length=1]',
347
+ + '[alternate.callee.property.name=/^(get|format|view|at)/][alternate.callee.object.name=/^Source(Range|Location)$/][alternate.arguments.length=1]',
193
348
  capture: { call: 'alternate' },
194
349
  replace: '{{call}}',
195
350
  sameText: [['test.left', 'alternate.arguments.0']],
196
- declaredIn: { 'alternate.callee.object': RANGE },
351
+ declaredIn: { 'alternate.callee.object': Files.range },
197
352
  message: 'The reader passes an absent input through, the `=== undefined` guard says nothing on top of it.'
198
353
  },
199
354
  {
200
- /* `Identifier.toString` answers `undefined` for an absent identifier, an empty name is a different thing */
201
355
  id: 'identifier-tostring-fallback',
202
- selector: `${rootHelperCall('Identifier', 'toString')}`
203
- + '[arguments.0.type="LogicalExpression"][arguments.0.operator="??"]'
204
- + '[arguments.0.right.type="Literal"][arguments.0.right.value=""]',
356
+ selector: `CallExpression${helperCall('', 'Identifier', 'toString')}[arguments.0.type="LogicalExpression"][arguments.0.operator="??"]${text('arguments.0.right', '')}`,
205
357
  capture: { id: 'arguments.0.left' },
206
358
  replace: 'Identifier.toString({{id}})',
207
- declaredIn: { 'callee.object': IDENTIFIER },
208
- /* an empty string and `undefined` read differently downstream, so this is offered rather than applied */
359
+ declaredIn: { 'callee.object': Files.identifier },
360
+ /* an empty string and `undefined` read differently downstream */
209
361
  fix: false,
210
362
  message: '`Identifier.toString` passes an absent identifier through; the `?? \'\'` turns "no name" into the name `""`.'
211
363
  },
@@ -214,97 +366,224 @@ const patterns = [
214
366
  selector: `:matches(IfStatement, ConditionalExpression, WhileStatement, DoWhileStatement, LogicalExpression, UnaryExpression[operator="!"], ArrowFunctionExpression) > BinaryExpression[operator="&"]${types('left')}`,
215
367
  capture: { edge: 'left.object', type: 'right' },
216
368
  replace: 'DfEdge.includesType({{edge}}, {{type}})',
217
- declaredIn: { 'left.property': EDGE },
369
+ declaredIn: { 'left.property': Files.edge },
218
370
  message: 'Do not rely on the bitmask being truthy, `DfEdge.includesType` says what is meant.'
219
371
  },
220
- {
221
- id: 'vertex-has-origin',
222
- selector: 'LogicalExpression[operator="&&"]'
223
- + `${helperCall('left', 'DfgVertex', 'isFunctionCall')}`
224
- + `${call('right', 'includes')}[right.callee.object.type="MemberExpression"][right.callee.object.property.name="origin"]`,
225
- capture: { vertex: 'left.arguments.0', origin: 'right.arguments.0' },
226
- replace: 'DfgVertex.hasOrigin({{vertex}}, {{origin}})',
227
- declaredIn: { 'left.callee.object': VERTEX, 'right.callee.object.property': VERTEX },
228
- sameText: [['left.arguments.0', 'right.callee.object.object']],
229
- /* `hasOrigin` is no type predicate, so the replacement can drop a narrowing the code below relies on */
230
- fix: false,
231
- message: 'Use `DfgVertex.hasOrigin`, it is exactly this check. Check the narrowing first, `hasOrigin` returns a plain boolean.'
232
- },
372
+ ...negatable(['vertex-has-origin', 'vertex-lacks-origin'], originGuarded),
233
373
  edgesFallback('graph-edges-from', 'outgoingEdges', 'edgesFrom'),
234
374
  edgesFallback('graph-edges-to', 'ingoingEdges', 'edgesTo'),
235
- vertexIs('vertex-is'),
236
- vertexIs('vertex-is-not', { negated: true }),
237
- vertexIs('vertex-is-optional', { optional: true }),
238
- vertexIs('vertex-is-not-optional', { negated: true, optional: true }),
239
- nodeIs('node-is'),
240
- nodeIs('node-is-not', { negated: true }),
241
- nodeIs('node-is-optional', { optional: true }),
242
- nodeIs('node-is-not-optional', { negated: true, optional: true }),
243
- guardWrittenOut({
244
- id: 'call-is-named', helper: 'RFunctionCall', member: 'isNamed', file: CALL_NODE,
245
- guard: `[right.type="MemberExpression"]${member('right', 'named')}`,
246
- subject: 'right.object',
247
- message: 'Use `RFunctionCall.isNamed`, it is this check and narrows to `RNamedFunctionCall`.'
248
- }),
249
- guardWrittenOut({
250
- id: 'call-is-named-strict', helper: 'RFunctionCall', member: 'isNamed', file: CALL_NODE,
251
- guard: `[right.type="BinaryExpression"][right.operator="==="][right.right.type="Literal"][right.right.value=true]${member('right.left', 'named')}`,
252
- subject: 'right.left.object',
253
- message: 'Use `RFunctionCall.isNamed`, it is this check and narrows to `RNamedFunctionCall`.'
254
- }),
255
- guardWrittenOut({
256
- id: 'call-is-unnamed', helper: 'RFunctionCall', member: 'isUnnamed', file: CALL_NODE,
257
- guard: `[right.type="UnaryExpression"][right.operator="!"][right.argument.type="MemberExpression"]${member('right.argument', 'named')}`,
258
- subject: 'right.argument.object',
259
- message: 'Use `RFunctionCall.isUnnamed`, it is this check and narrows to `RUnnamedFunctionCall`.'
260
- }),
261
- guardWrittenOut({
262
- id: 'argument-is-named', helper: 'RArgument', member: 'isNamed', file: ARGUMENT_NODE,
263
- guard: `${versusUndefined('right', true)}${member('right.left', 'name')}`,
264
- subject: 'right.left.object',
265
- message: 'Use `RArgument.isNamed`, it is this check and keeps the `name` non-optional afterwards.'
266
- }),
267
- guardWrittenOut({
268
- id: 'argument-is-with-value', helper: 'RArgument', member: 'isWithValue', file: ARGUMENT_NODE,
269
- guard: `${versusUndefined('right', true)}${member('right.left', 'value')}`,
270
- subject: 'right.left.object',
271
- message: 'Use `RArgument.isWithValue`, it is this check and keeps the `value` non-optional afterwards.'
375
+ ...discriminatorIs(['vertex-is', 'vertex-is-not', 'vertex-is-optional', 'vertex-is-not-optional'], {
376
+ property: 'tag', enumName: 'VertexType', enumFile: Files.vertex, propertyFile: Files.vertex,
377
+ helper: 'DfgVertex.is{{type|last}}', hint: 'Compare through the vertex helper'
272
378
  }),
273
- guardWrittenOut({
274
- id: 'list-is-implicit', helper: 'RExpressionList', member: 'isImplicit', file: LIST_NODE,
275
- guard: `${versusUndefined('right', false)}${member('right.left', 'grouping')}`,
276
- subject: 'right.left.object',
277
- message: 'Use `RExpressionList.isImplicit`, an expression list without `grouping` is exactly the implicit one.'
379
+ ...discriminatorIs(['node-is', 'node-is-not', 'node-is-optional', 'node-is-not-optional'], {
380
+ property: 'type', enumName: 'RType', enumFile: Files.rType,
381
+ helper: 'R{{type|last}}.is', hint: 'Ask the AST node helper'
278
382
  }),
279
- symbolName('symbol-name-comparison', false),
280
- symbolName('symbol-name-comparison-not', true),
383
+ guardWrittenOut('call-is-named', 'RFunctionCall', 'isNamed', Files.call, prop('right', 'named'), 'right.object', 'Use `RFunctionCall.isNamed`, it is this check and narrows to `RNamedFunctionCall`.'),
384
+ guardWrittenOut('call-is-named-strict', 'RFunctionCall', 'isNamed', Files.call, `[right.type="BinaryExpression"][right.operator="==="][right.right.type="Literal"][right.right.value=true]${member('right.left', 'named')}`, 'right.left.object', 'Use `RFunctionCall.isNamed`, it is this check and narrows to `RNamedFunctionCall`.'),
385
+ guardWrittenOut('call-is-unnamed', 'RFunctionCall', 'isUnnamed', Files.call, `${not('right')}${prop('right.argument', 'named')}`, 'right.argument.object', 'Use `RFunctionCall.isUnnamed`, it is this check and narrows to `RUnnamedFunctionCall`.'),
386
+ guardWrittenOut('argument-is-named', 'RArgument', 'isNamed', Files.argument, `${versusUndefined('right', true)}${member('right.left', 'name')}`, 'right.left.object', 'Use `RArgument.isNamed`, it is this check and keeps the `name` non-optional afterwards.'),
387
+ guardWrittenOut('argument-is-with-value', 'RArgument', 'isWithValue', Files.argument, `${versusUndefined('right', true)}${member('right.left', 'value')}`, 'right.left.object', 'Use `RArgument.isWithValue`, it is this check and keeps the `value` non-optional afterwards.'),
388
+ guardWrittenOut('list-is-implicit', 'RExpressionList', 'isImplicit', Files.list, `${versusUndefined('right', false)}${member('right.left', 'grouping')}`, 'right.left.object', 'Use `RExpressionList.isImplicit`, an expression list without `grouping` is exactly the implicit one.'),
389
+ ...negatable(['call-is-named-literal', 'call-is-not-named-literal'], namedCallOf(false)),
390
+ ...negatable(['call-is-named-in-set', 'call-is-not-named-in-set'], namedCallOf(true)),
391
+ ...nodeFallback(['node-lexeme', 'node-lexeme-optional'], 'fullLexeme', 'lexeme', true, 'RNode.lexeme', 'Use `RNode.lexeme`, it reads the code the node spans, the full lexeme first.'),
392
+ ...nodeFallback(['node-own-lexeme', 'node-own-lexeme-optional'], 'fullLexeme', 'lexeme', false, 'RNode.ownLexeme', 'Use `RNode.ownLexeme`, it reads the token the node is made of, its own lexeme first.'),
393
+ ...nodeFallback(['node-range', 'node-range-optional'], 'fullRange', 'location', true, 'SourceRange.fromNode', 'Use `SourceRange.fromNode`, it reads the full range and falls back to the location.'),
394
+ ...nodeFallback(['node-own-range', 'node-own-range-optional'], 'fullRange', 'location', false, 'SourceRange.ownFromNode', 'Use `SourceRange.ownFromNode`, it reads the location of the node\'s own token and falls back to the full range.'),
395
+ rangeIndex('range-start-line', 0, 'getStartLine'),
396
+ rangeIndex('range-start-column', 1, 'getStartColumn'),
397
+ rangeIndex('range-end-line', 2, 'getEndLine'),
398
+ rangeIndex('range-end-column', 3, 'getEndColumn'),
399
+ {
400
+ id: 'node-id-recover-name',
401
+ selector: `ChainExpression[expression.type="MemberExpression"][expression.optional=true]${member('expression', 'lexeme')}`
402
+ + `${call('expression.object', 'get')}[expression.object.callee.computed=false][expression.object.arguments.length=1]`
403
+ + ':matches([expression.object.callee.object.name=/idMap/i], [expression.object.callee.object.property.name=/idMap/i])',
404
+ capture: { id: 'expression.object.arguments.0', map: 'expression.object.callee.object' },
405
+ replace: 'NodeId.recoverName({{id}}, {{map}})',
406
+ declaredIn: { 'expression.property': Files.model },
407
+ message: '`NodeId.recoverName` is this lookup, the lexeme of the node an id belongs to.'
408
+ },
409
+ unpacked('unpack-arg', true),
410
+ unpacked('unpack-arg-inverted', false),
411
+ findNamed('find-named-arg', 'neq'),
412
+ findNamed('find-named-arg-not-empty', 'isNotEmpty'),
413
+ findNamed('find-named-arg-negated-empty', 'isEmpty'),
414
+ {
415
+ id: 'call-sole-argument',
416
+ selector: 'ConditionalExpression[test.type="LogicalExpression"][test.operator="&&"][test.left.type="BinaryExpression"][test.left.operator="==="]'
417
+ + `${prop('test.left.left', 'length')}[test.left.right.type="Literal"][test.left.right.value=1]`
418
+ + '[test.right.type="BinaryExpression"][test.right.operator="!=="][test.right.left.type="MemberExpression"][test.right.left.computed=true]'
419
+ + `[test.right.left.property.value=0]${identifier('test.right.right', 'EmptyArgument')}`
420
+ + `[consequent.type="MemberExpression"][consequent.computed=true][consequent.property.value=0]${identifier('alternate', 'undefined')}`,
421
+ capture: { args: 'test.left.left.object' },
422
+ replace: 'RFunctionCall.soleArgument({{args}})',
423
+ declaredIn: { 'test.right.right': Files.call },
424
+ sameText: [['test.left.left.object', 'test.right.left.object'], ['test.left.left.object', 'consequent.object']],
425
+ message: '`RFunctionCall.soleArgument` is this: the one argument, `undefined` unless there is exactly one and it is not empty.'
426
+ },
427
+ calledNameOf('call-called-name', 'call'),
428
+ calledNameOf('call-called-name-named', 'property'),
429
+ ...negatable(['callee-lexeme-comparison', 'callee-lexeme-comparison-not'], calleeLexeme),
430
+ ...negatable(['vertex-name-comparison', 'vertex-name-comparison-not'], vertexName),
431
+ ...negatable(['symbol-name-comparison', 'symbol-name-comparison-not'], symbolName),
432
+ {
433
+ id: 'vertex-has-origin-array',
434
+ selector: `LogicalExpression[operator="&&"][left.type="LogicalExpression"][left.operator="&&"]${helperCall('left.left', 'DfgVertex', 'isFunctionCall')}`
435
+ + `${helperCall('left.right', 'Array', 'isArray')}${prop('left.right.arguments.0', 'origin')}`
436
+ + `${call('right', 'includes')}[right.arguments.length=1]${prop('right.callee.object', 'origin')}`,
437
+ capture: { vertex: 'left.left.arguments.0', origin: 'right.arguments.0' },
438
+ replace: 'DfgVertex.hasOrigin({{vertex}}, {{origin}})',
439
+ declaredIn: { 'left.left.callee.object': Files.vertex, 'right.callee.object.property': Files.vertex },
440
+ sameText: [['left.left.arguments.0', 'left.right.arguments.0.object'], ['left.left.arguments.0', 'right.callee.object.object']],
441
+ fix: false,
442
+ message: '`DfgVertex.hasOrigin` is this check including the `Array.isArray`. Check the narrowing first, `hasOrigin` returns a plain boolean.'
443
+ },
444
+ {
445
+ id: 'vertex-origin-includes',
446
+ /* the guarded spellings have patterns of their own, and the origin of a `link` is no vertex origin */
447
+ selector: `CallExpression${prop('callee', 'includes')}`
448
+ + ':not(LogicalExpression[operator="&&"] > CallExpression):not(LogicalExpression[operator="||"] > UnaryExpression > CallExpression)'
449
+ + `[arguments.length=1]${prop('callee.object', 'origin')}:not([callee.object.object.property.name="link"])`,
450
+ capture: { vertex: 'callee.object.object', origin: 'arguments.0' },
451
+ replace: 'DfgVertex.hasOrigin({{vertex}}, {{origin}})',
452
+ declaredIn: { 'callee.object.property': Files.vertex },
453
+ /* the vertex may need the narrowing `hasOrigin` adds */
454
+ fix: false,
455
+ message: 'Ask `DfgVertex.hasOrigin` instead of reading the origin list by hand.'
456
+ },
457
+ {
458
+ id: 'graph-vertices-of-type',
459
+ selector: `CallExpression${prop('callee', 'filter')}[arguments.length=1]${call('callee.object', 'vertices')}[callee.object.arguments.length=1]`
460
+ + '[arguments.0.type="ArrowFunctionExpression"][arguments.0.params.length=1][arguments.0.params.0.type="ArrayPattern"][arguments.0.params.0.elements.length=2]'
461
+ + '[arguments.0.params.0.elements.1.type="Identifier"][arguments.0.body.type="BinaryExpression"][arguments.0.body.operator="==="]'
462
+ + `${prop('arguments.0.body.left', 'tag')}${enumMember('arguments.0.body.right', 'VertexType')}`,
463
+ capture: { graph: 'callee.object.callee.object', type: 'arguments.0.body.right' },
464
+ replace: '{{graph}}.verticesOfType({{type}})',
465
+ declaredIn: { 'callee.object.callee.property': Files.graph, 'arguments.0.body.right.object': Files.vertex },
466
+ sameText: [['arguments.0.params.0.elements.1', 'arguments.0.body.left.object']],
467
+ /* the order follows the types, not the insertion */
468
+ fix: false,
469
+ message: '`verticesOfType` goes straight to the vertices of that type instead of walking all of them.'
470
+ },
471
+ edgeNeighbors('edge-targets', 'edgesFrom', 'Dataflow.targetsOf'),
472
+ edgeNeighbors('edge-sources', 'edgesTo', 'Dataflow.sourcesOf'),
473
+ {
474
+ id: 'edge-has-type-between',
475
+ selector: `CallExpression${helperCall('', 'DfEdge', 'includesType', 2)}`
476
+ + `[arguments.0.type="LogicalExpression"][arguments.0.operator="??"]${call('arguments.0.left', 'get')}[arguments.0.left.arguments.length=1]`
477
+ + `${call('arguments.0.left.callee.object', 'edgesFrom')}[arguments.0.left.callee.object.arguments.length=1]`
478
+ + '[arguments.0.right.type="ObjectExpression"][arguments.0.right.properties.length=1][arguments.0.right.properties.0.key.name="types"][arguments.0.right.properties.0.value.value=0]',
479
+ capture: { graph: 'arguments.0.left.callee.object.callee.object', from: 'arguments.0.left.callee.object.arguments.0', to: 'arguments.0.left.arguments.0', types: 'arguments.1' },
480
+ replace: 'Dataflow.hasEdge({{graph}}, {{from}}, {{to}}, {{types}})',
481
+ declaredIn: { 'callee.property': Files.edge, 'arguments.0.left.callee.object.callee.property': Files.graph },
482
+ message: '`Dataflow.hasEdge` asks whether an edge between two vertices carries a type, absent edge included.'
483
+ },
484
+ {
485
+ id: 'environment-frames',
486
+ selector: `ForStatement[init.type="VariableDeclaration"][init.declarations.length=1]${not('test')}${prop('test.argument', 'builtInEnv')}`
487
+ + `[update.type="AssignmentExpression"][update.operator="="]${prop('update.right', 'parent')}`,
488
+ capture: { frame: 'init.declarations.0.id', start: 'init.declarations.0.init', body: 'body' },
489
+ replace: 'for(const {{frame}} of REnvironment.frames({{start}})) {{body}}',
490
+ declaredIn: { 'test.argument.property': Files.environment },
491
+ sameText: [['init.declarations.0.id', 'test.argument.object'], ['init.declarations.0.id', 'update.left'], ['init.declarations.0.id', 'update.right.object']],
492
+ /* the walk may stop early or reassign the variable */
493
+ fix: false,
494
+ message: '`REnvironment.frames` yields these frames, innermost first and without the built-in one.'
495
+ },
496
+ builtInPrefix('built-in-id-string', true),
497
+ builtInPrefix('built-in-id-prefix', false),
498
+ builtInId('to-built-in-template', 'TemplateLiteral[quasis.length=2][expressions.length=1][quasis.0.value.raw="built-in:"][quasis.1.value.raw=""]', 'expressions.0'),
499
+ builtInId('to-built-in-concat', `BinaryExpression[operator="+"]${text('left', 'built-in:')}`, 'right'),
500
+ {
501
+ id: 'remove-r-quotes',
502
+ selector: `CallExpression${prop('callee', 'replace')}[arguments.length=2][arguments.0.type="Literal"][arguments.0.regex.flags="g"]`
503
+ + String.raw `[arguments.0.regex.pattern=/^\^\[[^\]]*[\x22\x27\x60][^\]]*\]\|\[[^\]]*[\x22\x27\x60][^\]]*\]\$$/]`
504
+ + text('arguments.1', ''),
505
+ capture: { str: 'callee.object' },
506
+ replace: 'RString.unquote({{str}})',
507
+ helperIn: Files.string,
508
+ /* the regex strips each end on its own, the helper only a matching pair */
509
+ fix: false,
510
+ message: '`RString.unquote` strips the R quotes of a name, `RString.splitQuotes` also tells which one it was.'
511
+ },
512
+ {
513
+ id: 'escape-regex',
514
+ selector: 'CallExpression[callee.type="MemberExpression"][callee.property.name=/^replace(All)?$/][arguments.length=2][arguments.0.type="Literal"]'
515
+ + String.raw `[arguments.0.regex.flags="g"][arguments.0.regex.pattern=/^\[\.\*\+\?\^\$\{\}\(\)\|\[\\\]\\\\\]$/]`
516
+ + String.raw `[arguments.1.type="Literal"][arguments.1.value=/^\\\$&$/]`,
517
+ capture: { str: 'callee.object' },
518
+ replace: 'escapeRegex({{str}})',
519
+ helperIn: Files.strings,
520
+ message: 'Use `escapeRegex` to match text literally, do not spell out the character class.'
521
+ },
522
+ {
523
+ id: 'identifier-split',
524
+ selector: `CallExpression${prop('callee', 'split')}[arguments.length=1]${text('arguments.0', '::')}`
525
+ + ':matches([callee.object.name=/name|ident|symbol|qualified|fn/i], [callee.object.property.name=/name|ident|symbol|qualified|fn/i], [callee.object.expression.property.name=/name|ident|symbol|qualified|fn/i])',
526
+ capture: { str: 'callee.object' },
527
+ replace: 'Identifier.parse({{str}})',
528
+ helperIn: Files.identifier,
529
+ /* the parts come back as a tuple, and the text may not be an identifier at all */
530
+ fix: false,
531
+ message: 'Do not take `pkg::name` apart by hand: `Identifier.parse`, then `Identifier.getName` and `Identifier.getNamespace` (`:::` is part of the syntax).'
532
+ },
533
+ {
534
+ id: 'unique-array',
535
+ selector: spreadSet(':not([elements.0.argument.arguments.0.type="ArrayExpression"])'),
536
+ capture: { items: 'elements.0.argument.arguments.0' },
537
+ replace: 'uniqueArray({{items}})',
538
+ helperIn: Files.arrays,
539
+ message: 'Use `uniqueArray`, a duplicate-free copy is not a place for a `Set` detour.'
540
+ },
541
+ {
542
+ id: 'unique-array-merge',
543
+ selector: spreadSet('[elements.0.argument.arguments.0.type="ArrayExpression"][elements.0.argument.arguments.0.elements.length=2]'
544
+ + '[elements.0.argument.arguments.0.elements.0.type="SpreadElement"][elements.0.argument.arguments.0.elements.1.type="SpreadElement"]'),
545
+ capture: { left: 'elements.0.argument.arguments.0.elements.0.argument', right: 'elements.0.argument.arguments.0.elements.1.argument' },
546
+ replace: 'uniqueArrayMerge({{left}}, {{right}})',
547
+ helperIn: Files.arrays,
548
+ /* the helper wants arrays, a spread also takes iterables */
549
+ fix: false,
550
+ message: 'Use `uniqueArrayMerge`, it merges two arrays without duplicates.'
551
+ },
552
+ {
553
+ id: 'array-equal',
554
+ selector: `LogicalExpression[operator="&&"][left.type="BinaryExpression"][left.operator="==="]${prop('left.left', 'length')}${prop('left.right', 'length')}`
555
+ + `${call('right', 'every')}[right.arguments.length=1][right.arguments.0.type="ArrowFunctionExpression"][right.arguments.0.params.length=2]`
556
+ + '[right.arguments.0.body.type="BinaryExpression"][right.arguments.0.body.operator="==="][right.arguments.0.body.right.type="MemberExpression"][right.arguments.0.body.right.computed=true]',
557
+ capture: { left: 'left.left.object', right: 'left.right.object' },
558
+ replace: 'arrayEqual({{left}}, {{right}})',
559
+ helperIn: Files.arrays,
560
+ sameText: [['left.left.object', 'right.callee.object'], ['left.right.object', 'right.arguments.0.body.right.object'],
561
+ ['right.arguments.0.params.0', 'right.arguments.0.body.left'], ['right.arguments.0.params.1', 'right.arguments.0.body.right.property']],
562
+ message: 'Use `arrayEqual`, comparing two arrays element by element is its job.'
563
+ },
564
+ {
565
+ id: 'sort-code-unit',
566
+ selector: `CallExpression${prop('callee', 'sort')}[arguments.length=1][arguments.0.type="ArrowFunctionExpression"]`
567
+ + `[arguments.0.params.length=2]${call('arguments.0.body', 'localeCompare')}[arguments.0.body.arguments.length=1]`,
568
+ capture: { list: 'callee.object' },
569
+ replace: '{{list}}.sort(compareByCodeUnit)',
570
+ helperIn: Files.strings,
571
+ sameText: [['arguments.0.params.0', 'arguments.0.body.callee.object'], ['arguments.0.params.1', 'arguments.0.body.arguments.0']],
572
+ /* `localeCompare` is fine for an order only a reader sees */
573
+ fix: false,
574
+ message: '`localeCompare` depends on the locale and the ICU build, so an order that feeds an analysis result should use `compareByCodeUnit`.'
575
+ },
281
576
  {
282
577
  id: 'array-sum',
283
- selector: 'CallExpression[callee.type="MemberExpression"][callee.property.name="reduce"][arguments.length=2]'
284
- + '[arguments.1.type="Literal"][arguments.1.value=0]'
285
- + '[arguments.0.type="ArrowFunctionExpression"][arguments.0.params.length=2]'
286
- + '[arguments.0.body.type="BinaryExpression"][arguments.0.body.operator="+"]',
578
+ selector: `CallExpression${prop('callee', 'reduce')}[arguments.length=2][arguments.1.type="Literal"][arguments.1.value=0]`
579
+ + '[arguments.0.type="ArrowFunctionExpression"][arguments.0.params.length=2][arguments.0.body.type="BinaryExpression"][arguments.0.body.operator="+"]',
287
580
  capture: { array: 'callee.object' },
288
581
  replace: 'arraySum({{array}})',
582
+ helperIn: Files.arrays,
289
583
  sameText: [['arguments.0.params.0', 'arguments.0.body.left'], ['arguments.0.params.1', 'arguments.0.body.right']],
290
584
  message: 'Use `arraySum`, summing a list is not a place to spell out a fold.'
291
585
  },
292
- {
293
- /* `filter` walks the whole list and allocates the matches only to ask whether there is one */
294
- id: 'some-instead-of-filter-length',
295
- selector: `${versusZero(true)}[left.type="MemberExpression"]${member('left', 'length')}`
296
- + `${call('left.object', 'filter')}[left.object.arguments.length=1]`,
297
- capture: { array: 'left.object.callee.object', predicate: 'left.object.arguments.0' },
298
- replace: '{{array}}.some({{predicate}})',
299
- message: '`filter(...).length > 0` walks the whole list and allocates the matches, `some` stops at the first hit.'
300
- },
301
- {
302
- id: 'none-instead-of-filter-length',
303
- selector: `${versusZero(false)}[left.type="MemberExpression"]${member('left', 'length')}`
304
- + `${call('left.object', 'filter')}[left.object.arguments.length=1]`,
305
- capture: { array: 'left.object.callee.object', predicate: 'left.object.arguments.0' },
306
- replace: '!{{array}}.some({{predicate}})',
307
- message: '`filter(...).length === 0` walks the whole list and allocates the matches, `!some` stops at the first hit.'
308
- }
586
+ someInsteadOfFilter('some-instead-of-filter-length', true),
587
+ someInsteadOfFilter('none-instead-of-filter-length', false)
309
588
  ];
310
589
  module.exports = patterns;