@eagleoutice/eslint-config-flowr 2.1.1 → 2.1.4

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/README.md CHANGED
@@ -75,6 +75,24 @@ To add one, append to [`patterns.ts`](patterns.ts):
75
75
  | `fix` | `true` always fixes, `false` always suggests, which is right when the replacement can change type narrowing. Omitted, it fixes when the replacement is in scope |
76
76
  | `id` | names the pattern in `@lintIgnore` and in the message |
77
77
 
78
+ The shipped set groups into families:
79
+
80
+ | family | shape it finds | helper it points at |
81
+ | :-- | :-- | :-- |
82
+ | `edge-*` | `e.types === T`, `(e.types & T) !== 0`, and the bitmask spellings around them | `DfEdge` |
83
+ | `vertex-is*`, `vertex-has-origin` | `v.tag === VertexType.X`, `Vertex.isFunctionCall(v) && v.origin.includes(o)` | `Vertex` |
84
+ | `node-is*` | `n.type === RType.X`, negated and through `?.` | `RSymbol`, `RFunctionCall`, and the rest of the `R*` node objects |
85
+ | `argument-is-*`, `call-is-*`, `list-is-implicit` | a guard written out, `RFunctionCall.is(n) && n.named` | the narrower guard of the same object |
86
+ | `symbol-name-comparison*` | `symbol.content === 'name'`, which misses `pkg::name` because an identifier with a namespace is an array | `Identifier.getName` / `Identifier.matches` |
87
+ | `graph-edges-from`, `graph-edges-to` | `g.outgoingEdges(id) ?? []` / `?? NoEdges`, a fallback the total accessors already are | `edgesFrom` / `edgesTo` |
88
+ | `location-range-hop`, `location-at-node`, `accessor-undefined-guard`, `identifier-tostring-fallback` | an unwrap, a positional destructure, or an absent-value guard in front of a reader that passes `undefined` through by itself | `SourceLocation`, `Identifier` |
89
+ | `some-instead-of-filter-length`, `array-sum` | a full walk where a helper does it once | `some`, `arraySum` |
90
+
91
+ A pattern is exempt in the file `declaredIn` resolves to, which is why a shape only earns a place here when the
92
+ match names a flowR symbol: without one the rule cannot tell the helper's own body apart from a call site, and
93
+ `@lintIgnore` on the declaration would be papering over that. A guard that *is* the raw comparison
94
+ (`RSymbol.is` compares `type` against `RType.Symbol`) is the case the tag is actually for.
95
+
78
96
  An own `patterns` array replaces the defaults rather than merging with them. To propose one, open an issue with the
79
97
  [replacement pattern template](.github/ISSUE_TEMPLATE/replacement-pattern.yaml).
80
98
 
@@ -83,8 +101,7 @@ An own `patterns` array replaces the defaults rather than merging with them. To
83
101
  | | silences |
84
102
  | :-- | :-- |
85
103
  | `// eslint-disable-next-line` | the next line, as usual |
86
- | `@performanceCritical` | a hot path that has to keep the raw form |
87
- | `@lintIgnore <ids>` | the named rules or pattern ids, all of them when given none |
104
+ | `@lintIgnore <ids>` | the named rules or pattern ids, all of them when given none (put the reason in the prose above it) |
88
105
 
89
106
  A tag on a declaration covers everything below it, a header comment above the imports covers the file. The lookup is
90
107
  guarded by a single string search, so files without either tag pay nothing.
package/index.js CHANGED
@@ -85,7 +85,40 @@ const config = [
85
85
  ],
86
86
  '@stylistic/comma-dangle': ['error', 'only-multiline'],
87
87
  '@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
88
+ /*
89
+ * spacing that is already all but settled in flowR, pinned so it stays that way; every one of these
90
+ * is autofixable, so a violation costs nobody a thought
91
+ */
92
+ '@stylistic/array-bracket-spacing': ['error', 'never'],
93
+ '@stylistic/computed-property-spacing': ['error', 'never'],
94
+ '@stylistic/space-in-parens': ['error', 'never'],
95
+ '@stylistic/block-spacing': ['error', 'always'],
96
+ '@stylistic/space-before-blocks': ['error', 'always'],
97
+ '@stylistic/rest-spread-spacing': ['error', 'never'],
98
+ '@stylistic/template-curly-spacing': ['error', 'never'],
99
+ '@stylistic/switch-colon-spacing': ['error', { before: false, after: true }],
100
+ '@stylistic/no-whitespace-before-property': 'error',
101
+ '@stylistic/space-unary-ops': ['error', { words: true, nonwords: false }],
102
+ '@stylistic/semi-spacing': ['error', { before: false, after: true }],
103
+ '@stylistic/comma-style': ['error', 'last'],
104
+ '@stylistic/dot-location': ['error', 'property'],
105
+ '@stylistic/no-extra-semi': 'error',
88
106
  'jsdoc/check-alignment': 'error',
107
+ /*
108
+ * the same alignment `key-spacing` asks of object values, asked of tsdoc: the `-` of every `@param`
109
+ * in a block lines up, so the names read as a column instead of as prose
110
+ */
111
+ 'jsdoc/check-line-alignment': ['error', 'always'],
112
+ /* `@param name - what it is`, the spelling the codebase already uses everywhere */
113
+ 'jsdoc/require-hyphen-before-param-description': ['error', 'always'],
114
+ 'jsdoc/no-multi-asterisks': 'error',
115
+ 'jsdoc/require-asterisk-prefix': 'error',
116
+ 'jsdoc/no-blank-blocks': 'error',
117
+ 'jsdoc/check-property-names': 'error',
118
+ 'jsdoc/empty-tags': 'error',
119
+ 'jsdoc/require-returns-check': 'error',
120
+ /* a block reads top down: what it is, what it takes, what it answers, then the pointers */
121
+ 'jsdoc/sort-tags': ['error', { linesBetween: 0 }],
89
122
  'jsdoc/check-indentation': 'off',
90
123
  'jsdoc/no-types': 'error',
91
124
  'jsdoc/no-undefined-types': 'off',
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@eagleoutice/eslint-config-flowr",
3
- "version": "2.1.1",
3
+ "version": "2.1.4",
4
4
  "description": "Linting rules for flowr and friends",
5
5
  "license": "ISC",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/flowr-analysis/flowr-lint.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/flowr-analysis/flowr-lint/issues"
12
+ },
13
+ "homepage": "https://github.com/flowr-analysis/flowr-lint#readme",
6
14
  "main": "index.js",
7
15
  "files": [
8
16
  "index.js",
package/patterns.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Default patterns of `flowr/replacement-pattern`, see the README for the format.
3
3
  * The helpers they point at live in flowR: `DfEdge` in `dataflow/graph/edge`, the vertex helpers in
4
- * `dataflow/graph/vertex`, and `NoEdges` in `dataflow/graph/graph`.
4
+ * `dataflow/graph/vertex`, `NoEdges` in `dataflow/graph/graph`, the `RNode` helpers in
5
+ * `r-bridge/lang-4.x/ast/model`, and the collection helpers in `util/collections`.
5
6
  */
6
7
  import type { ReplacementPattern } from './pattern-type';
7
8
  declare const patterns: readonly ReplacementPattern[];
package/patterns.js CHANGED
@@ -2,16 +2,24 @@
2
2
  /**
3
3
  * Default patterns of `flowr/replacement-pattern`, see the README for the format.
4
4
  * The helpers they point at live in flowR: `DfEdge` in `dataflow/graph/edge`, the vertex helpers in
5
- * `dataflow/graph/vertex`, and `NoEdges` in `dataflow/graph/graph`.
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`.
6
7
  */
7
8
  const EDGE = 'dataflow/graph/edge';
8
9
  const VERTEX = 'dataflow/graph/vertex';
9
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';
10
18
  /** `x.types` under the given path */
11
19
  const types = (prefix) => `[${prefix}.type="MemberExpression"][${prefix}.computed=false][${prefix}.property.name="types"]`;
12
- /** `x.tag` under the given path */
13
- const tag = (prefix) => `[${prefix}.computed=false][${prefix}.property.name="tag"]`;
14
- /** `EdgeType.<Member>` / `VertexType.<Member>` on the right */
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 */
15
23
  const rightIs = (name) => `[right.type="MemberExpression"][right.computed=false][right.object.name="${name}"]`;
16
24
  /** compared against the literal `0`, either for "some bit is set" or for "none is" */
17
25
  const versusZero = (nonZero) => `BinaryExpression[operator=${nonZero ? '/^(!==|>)$/' : '"==="'}][right.type="Literal"][right.value=0]`;
@@ -19,6 +27,13 @@ const versusZero = (nonZero) => `BinaryExpression[operator=${nonZero ? '/^(!==|>
19
27
  const mask = '[left.type="BinaryExpression"][left.operator="&"]';
20
28
  /** a call of `<prefix>.<property>(...)` */
21
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"]`;
22
37
  const destructuringHint = ' Prefer keeping the edge over destructuring `types`, the helpers take it directly and the wrapper object goes away.';
23
38
  /** `x.types === T`, on the edge itself or on a destructured `types` */
24
39
  const edgeIsOnly = (id, { negated = false, destructured = false } = {}) => ({
@@ -51,32 +66,84 @@ const edgeStates = (id, helper, what, nonZero) => ({
51
66
  declaredIn: { 'left.property': EDGE },
52
67
  message: `Use \`DfEdge.${helper}\` to ask whether the edge states ${what}.`
53
68
  });
54
- /** `v.tag === VertexType.X`, with `{{type|last}}` spelling the name of the matching helper */
55
- const vertexIs = (id, { negated = false, optional = false } = {}) => {
56
- const vertex = optional ? 'left.expression.object' : 'left.object';
69
+ /**
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.
74
+ */
75
+ const discriminatorIs = (id, { property, enumName, enumFile, propertyFile, helper, member: name = 'is', hint }, { negated = false, optional = false } = {}) => {
57
76
  const left = optional ? 'left.expression' : 'left';
58
77
  return {
59
78
  id,
60
- selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"][left.type="${optional ? 'ChainExpression' : 'MemberExpression'}"]${tag(left)}${rightIs('VertexType')}`,
61
- capture: { vertex, type: 'right' },
62
- replace: `${negated ? '!' : ''}{{type|last}}Vertex.is({{vertex}})`,
63
- declaredIn: { [`${left}.property`]: VERTEX, 'right.object': VERTEX },
64
- message: 'Compare through the vertex helper (`{{type|last}}Vertex.is`), it narrows the type as well.'
79
+ selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"][left.type="${optional ? 'ChainExpression' : 'MemberExpression'}"]${member(left, property)}${rightIs(enumName)}`,
80
+ capture: { subject: `${left}.object`, type: 'right' },
81
+ replace: `${negated ? '!' : ''}${helper}.${name}({{subject}})`,
82
+ declaredIn: { 'right.object': enumFile, ...propertyFile ? { [`${left}.property`]: propertyFile } : {} },
83
+ message: `${hint} (\`${helper}.${name}\`), it narrows the type as well.`
65
84
  };
66
85
  };
67
- /** `FunctionCallVertex.is(v) && v.origin.includes(T)`, the body of `hasOrigin` written out */
68
- const hasOrigin = (id) => ({
86
+ /**
87
+ * `v.tag === VertexType.X`. One `Vertex` helper answers for every kind, and its members are named after the
88
+ * enum, so `{{type|last}}` spells the check without a table: `VertexType.Use` to `Vertex.isUse`.
89
+ */
90
+ const vertexIs = (id, options) => discriminatorIs(id, {
91
+ property: 'tag', enumName: 'VertexType', enumFile: VERTEX, propertyFile: VERTEX,
92
+ helper: 'Vertex', 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 }) => ({
69
109
  id,
70
- selector: 'LogicalExpression[operator="&&"]'
71
- + `${call('left', 'is')}[left.callee.object.name="FunctionCallVertex"]`
72
- + `${call('right', 'includes')}[right.callee.object.type="MemberExpression"][right.callee.object.property.name="origin"]`,
73
- capture: { vertex: 'left.arguments.0', origin: 'right.arguments.0' },
74
- replace: 'FunctionCallVertex.hasOrigin({{vertex}}, {{origin}})',
75
- declaredIn: { 'left.callee.object': VERTEX, 'right.callee.object.property': VERTEX },
76
- sameText: [['left.arguments.0', 'right.callee.object.object']],
77
- /* `hasOrigin` is no type predicate, so the replacement can drop a narrowing the code below relies on */
110
+ selector: `LogicalExpression[operator="&&"]${helperCall('left', helper, 'is')}${guard}`,
111
+ capture: { node: 'left.arguments.0' },
112
+ replace: `${helper}.${narrower}({{node}})`,
113
+ declaredIn: { 'left.callee.object': file },
114
+ sameText: [['left.arguments.0', subject]],
115
+ ...fix === undefined ? {} : { fix },
116
+ message
117
+ });
118
+ /**
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`.
121
+ */
122
+ const symbolName = (id, negated) => ({
123
+ id,
124
+ selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"][left.type="MemberExpression"]${member('left', 'content')}[right.type="Literal"][right.value=type(string)]`,
125
+ capture: { symbol: 'left.object', name: 'right' },
126
+ 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 */
78
129
  fix: false,
79
- message: 'Use `FunctionCallVertex.hasOrigin`, it is exactly this check. Check the narrowing first, `hasOrigin` returns a plain boolean.'
130
+ 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
+ });
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) => ({
140
+ 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.`
80
147
  });
81
148
  const patterns = [
82
149
  edgeIsOnly('edge-is-only-type'),
@@ -87,6 +154,61 @@ const patterns = [
87
154
  edgeStates('edge-has-no-type', 'hasNoType', 'nothing', false),
88
155
  edgeMask('edge-includes-type', 'includesType', true),
89
156
  edgeMask('edge-does-not-include-type', 'doesNotIncludeType', false),
157
+ {
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
+ */
162
+ id: 'location-at-node',
163
+ selector: `LogicalExpression[operator="??"][right.type="ArrayExpression"][right.elements.length=0]${helperCall('left', 'SourceLocation', 'fromNode')}`,
164
+ capture: { node: 'left.arguments.0' },
165
+ 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 */
168
+ fix: false,
169
+ 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
+ },
171
+ {
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
+ */
176
+ 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')}`,
180
+ capture: { reader: 'callee.property', location: 'arguments.0.arguments.0' },
181
+ replace: 'SourceLocation.{{reader}}({{location}})',
182
+ declaredIn: { 'callee.object': RANGE, 'arguments.0.callee.object': RANGE },
183
+ message: 'Drop the `getRange` hop, `SourceLocation.{{reader}}` reads the location directly.'
184
+ },
185
+ {
186
+ /* the readers answer `undefined` for an absent input, so the guard repeats what the type already says */
187
+ id: 'accessor-undefined-guard',
188
+ selector: `ConditionalExpression${versusUndefined('test', false)}`
189
+ + '[consequent.type="Identifier"][consequent.name="undefined"]'
190
+ + '[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]',
193
+ capture: { call: 'alternate' },
194
+ replace: '{{call}}',
195
+ sameText: [['test.left', 'alternate.arguments.0']],
196
+ declaredIn: { 'alternate.callee.object': RANGE },
197
+ message: 'The reader passes an absent input through, the `=== undefined` guard says nothing on top of it.'
198
+ },
199
+ {
200
+ /* `Identifier.toString` answers `undefined` for an absent identifier, an empty name is a different thing */
201
+ 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=""]',
205
+ capture: { id: 'arguments.0.left' },
206
+ 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 */
209
+ fix: false,
210
+ message: '`Identifier.toString` passes an absent identifier through; the `?? \'\'` turns "no name" into the name `""`.'
211
+ },
90
212
  {
91
213
  id: 'edge-includes-type-truthy',
92
214
  selector: `:matches(IfStatement, ConditionalExpression, WhileStatement, DoWhileStatement, LogicalExpression, UnaryExpression[operator="!"], ArrowFunctionExpression) > BinaryExpression[operator="&"]${types('left')}`,
@@ -95,19 +217,94 @@ const patterns = [
95
217
  declaredIn: { 'left.property': EDGE },
96
218
  message: 'Do not rely on the bitmask being truthy, `DfEdge.includesType` says what is meant.'
97
219
  },
98
- hasOrigin('vertex-has-origin'),
99
220
  {
100
- /* the `[]` fallback allocates on every miss, and these sit in traversal loops */
101
- id: 'graph-edges-no-alloc',
102
- selector: 'LogicalExpression[operator="??"][right.type="ArrayExpression"][right.elements.length=0][left.type="CallExpression"][left.callee.type="MemberExpression"][left.callee.property.name=/^(outgoingEdges|ingoingEdges)$/]',
103
- capture: { edges: 'left' },
104
- replace: '{{edges}} ?? NoEdges',
105
- declaredIn: { 'left.callee.property': GRAPH },
106
- message: 'The `[]` fallback allocates on every miss, use the shared `NoEdges`.'
221
+ id: 'vertex-has-origin',
222
+ selector: 'LogicalExpression[operator="&&"]'
223
+ + `${helperCall('left', 'Vertex', '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: 'Vertex.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 `Vertex.hasOrigin`, it is exactly this check. Check the narrowing first, `hasOrigin` returns a plain boolean.'
107
232
  },
233
+ edgesFallback('graph-edges-from', 'outgoingEdges', 'edgesFrom'),
234
+ edgesFallback('graph-edges-to', 'ingoingEdges', 'edgesTo'),
108
235
  vertexIs('vertex-is'),
109
236
  vertexIs('vertex-is-not', { negated: true }),
110
237
  vertexIs('vertex-is-optional', { optional: true }),
111
- vertexIs('vertex-is-not-optional', { negated: true, 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.'
272
+ }),
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.'
278
+ }),
279
+ symbolName('symbol-name-comparison', false),
280
+ symbolName('symbol-name-comparison-not', true),
281
+ {
282
+ 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="+"]',
287
+ capture: { array: 'callee.object' },
288
+ replace: 'arraySum({{array}})',
289
+ sameText: [['arguments.0.params.0', 'arguments.0.body.left'], ['arguments.0.params.1', 'arguments.0.body.right']],
290
+ message: 'Use `arraySum`, summing a list is not a place to spell out a fold.'
291
+ },
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
+ }
112
309
  ];
113
310
  module.exports = patterns;
package/rules/util.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Shared helpers of the flowR rules.
3
- * Tags: `@useInstead <target>` names a replacement, `@performanceCritical` and `@lintIgnore [ids]` silence the rules.
3
+ * Tags: `@useInstead <target>` names a replacement, `@lintIgnore [ids]` silences the rules.
4
4
  */
5
5
  import type { Rule } from 'eslint';
6
6
  import type { Node } from 'estree';
@@ -8,8 +8,8 @@ import type { TsSymbol, TsTypeChecker, TypeServices } from './ts-types';
8
8
  /** The type-aware services, or `undefined` if the file is parsed without a program. */
9
9
  export declare function typeServices(context: Rule.RuleContext): TypeServices | undefined;
10
10
  /**
11
- * Whether `node` sits below a declaration documented with a suppressing tag, or in a file whose
12
- * header comment carries one. Guarded by one string search, so untagged files pay nothing.
11
+ * Whether `node` sits below a declaration documented with `@lintIgnore`, or in a file whose header comment
12
+ * carries it. Guarded by one string search, so untagged files pay nothing.
13
13
  */
14
14
  export declare function suppressed(context: Rule.RuleContext, node: Rule.Node, ids: readonly string[]): boolean;
15
15
  /** The symbol an ESTree node resolves to, with import aliases followed. */
package/rules/util.js CHANGED
@@ -7,7 +7,7 @@ exports.declarationFile = declarationFile;
7
7
  exports.useInsteadOf = useInsteadOf;
8
8
  exports.isApplicable = isApplicable;
9
9
  const USE_INSTEAD_TAG = 'useInstead';
10
- const SUPPRESS_TAGS = ['@performanceCritical', '@lintIgnore'];
10
+ const SUPPRESS_TAG = '@lintIgnore';
11
11
  /* ts.SymbolFlags.Alias, spelled out to keep typescript out of the dependencies */
12
12
  const ALIAS_FLAG = 1 << 21;
13
13
  /** The type-aware services, or `undefined` if the file is parsed without a program. */
@@ -23,9 +23,6 @@ function silences(comment, ids) {
23
23
  if (!isDocComment(comment)) {
24
24
  return false;
25
25
  }
26
- else if (comment.value.includes('@performanceCritical')) {
27
- return true;
28
- }
29
26
  const ignore = /@lintIgnore([^\n@*]*)/.exec(comment.value);
30
27
  if (!ignore) {
31
28
  return false;
@@ -34,12 +31,12 @@ function silences(comment, ids) {
34
31
  return listed.length === 0 || listed.some(id => ids.includes(id));
35
32
  }
36
33
  /**
37
- * Whether `node` sits below a declaration documented with a suppressing tag, or in a file whose
38
- * header comment carries one. Guarded by one string search, so untagged files pay nothing.
34
+ * Whether `node` sits below a declaration documented with `@lintIgnore`, or in a file whose header comment
35
+ * carries it. Guarded by one string search, so untagged files pay nothing.
39
36
  */
40
37
  function suppressed(context, node, ids) {
41
38
  const sourceCode = context.sourceCode;
42
- if (!SUPPRESS_TAGS.some(tag => sourceCode.text.includes(tag))) {
39
+ if (!sourceCode.text.includes(SUPPRESS_TAG)) {
43
40
  return false;
44
41
  }
45
42
  /* a header comment marks the whole file: above the imports, or separated by a blank line */
package/tsdoc.json CHANGED
@@ -5,10 +5,6 @@
5
5
  "tagName": "@useInstead",
6
6
  "syntaxKind": "block"
7
7
  },
8
- {
9
- "tagName": "@performanceCritical",
10
- "syntaxKind": "modifier"
11
- },
12
8
  {
13
9
  "tagName": "@lintIgnore",
14
10
  "syntaxKind": "block"