@eagleoutice/eslint-config-flowr 2.1.3 → 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
@@ -80,11 +80,13 @@ The shipped set groups into families:
80
80
  | family | shape it finds | helper it points at |
81
81
  | :-- | :-- | :-- |
82
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`, `FunctionCallVertex.is(v) && v.origin.includes(o)` | the `*Vertex` objects |
83
+ | `vertex-is*`, `vertex-has-origin` | `v.tag === VertexType.X`, `Vertex.isFunctionCall(v) && v.origin.includes(o)` | `Vertex` |
84
84
  | `node-is*` | `n.type === RType.X`, negated and through `?.` | `RSymbol`, `RFunctionCall`, and the rest of the `R*` node objects |
85
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
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-no-alloc`, `some-instead-of-filter-length`, `array-sum` | an allocation or a full walk where a shared constant or a helper does it once | `NoEdges`, `some`, `arraySum` |
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` |
88
90
 
89
91
  A pattern is exempt in the file `declaredIn` resolves to, which is why a shape only earns a place here when the
90
92
  match names a flowR symbol: without one the rule cannot tell the helper's own body apart from a call site, and
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.3",
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.js CHANGED
@@ -13,6 +13,8 @@ const CALL_NODE = 'r-bridge/lang-4.x/ast/model/nodes/r-function-call';
13
13
  const ARGUMENT_NODE = 'r-bridge/lang-4.x/ast/model/nodes/r-argument';
14
14
  const SYMBOL_NODE = 'r-bridge/lang-4.x/ast/model/nodes/r-symbol';
15
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';
16
18
  /** `x.types` under the given path */
17
19
  const types = (prefix) => `[${prefix}.type="MemberExpression"][${prefix}.computed=false][${prefix}.property.name="types"]`;
18
20
  /** `x.<name>` under the given path, without asserting the node kind, which the caller pins down */
@@ -27,6 +29,9 @@ const mask = '[left.type="BinaryExpression"][left.operator="&"]';
27
29
  const call = (prefix, property) => `[${prefix}.type="CallExpression"][${prefix}.callee.type="MemberExpression"][${prefix}.callee.property.name="${property}"]`;
28
30
  /** a call of `<Helper>.<property>(<one argument>)` under the given path */
29
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]`;
30
35
  /** `x === undefined` / `x !== undefined` under the given path */
31
36
  const versusUndefined = (prefix, set) => `[${prefix}.type="BinaryExpression"][${prefix}.operator="${set ? '!==' : '==='}"][${prefix}.right.type="Identifier"][${prefix}.right.name="undefined"]`;
32
37
  const destructuringHint = ' Prefer keeping the edge over destructuring `types`, the helpers take it directly and the wrapper object goes away.';
@@ -67,21 +72,24 @@ const edgeStates = (id, helper, what, nonZero) => ({
67
72
  * `declaring` maps a path of the match to the file its symbol has to come from; the enum on the right is
68
73
  * always pinned, the property on the left only where one file declares it for every case.
69
74
  */
70
- const discriminatorIs = (id, { property, enumName, enumFile, propertyFile, helper, hint }, { negated = false, optional = false } = {}) => {
75
+ const discriminatorIs = (id, { property, enumName, enumFile, propertyFile, helper, member: name = 'is', hint }, { negated = false, optional = false } = {}) => {
71
76
  const left = optional ? 'left.expression' : 'left';
72
77
  return {
73
78
  id,
74
79
  selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"][left.type="${optional ? 'ChainExpression' : 'MemberExpression'}"]${member(left, property)}${rightIs(enumName)}`,
75
80
  capture: { subject: `${left}.object`, type: 'right' },
76
- replace: `${negated ? '!' : ''}${helper}.is({{subject}})`,
81
+ replace: `${negated ? '!' : ''}${helper}.${name}({{subject}})`,
77
82
  declaredIn: { 'right.object': enumFile, ...propertyFile ? { [`${left}.property`]: propertyFile } : {} },
78
- message: `${hint} (\`${helper}.is\`), it narrows the type as well.`
83
+ message: `${hint} (\`${helper}.${name}\`), it narrows the type as well.`
79
84
  };
80
85
  };
81
- /** `v.tag === VertexType.X`, with `{{type|last}}` spelling the name of the matching helper */
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
+ */
82
90
  const vertexIs = (id, options) => discriminatorIs(id, {
83
91
  property: 'tag', enumName: 'VertexType', enumFile: VERTEX, propertyFile: VERTEX,
84
- helper: '{{type|last}}Vertex', hint: 'Compare through the vertex helper'
92
+ helper: 'Vertex', member: 'is{{type|last}}', hint: 'Compare through the vertex helper'
85
93
  }, options);
86
94
  /**
87
95
  * `n.type === RType.X`. Every `RType` member has a helper object of the same name prefixed with `R`
@@ -121,6 +129,22 @@ const symbolName = (id, negated) => ({
121
129
  fix: false,
122
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.'
123
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.`
147
+ });
124
148
  const patterns = [
125
149
  edgeIsOnly('edge-is-only-type'),
126
150
  edgeIsOnly('edge-is-not-only-type', { negated: true }),
@@ -130,6 +154,61 @@ const patterns = [
130
154
  edgeStates('edge-has-no-type', 'hasNoType', 'nothing', false),
131
155
  edgeMask('edge-includes-type', 'includesType', true),
132
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
+ },
133
212
  {
134
213
  id: 'edge-includes-type-truthy',
135
214
  selector: `:matches(IfStatement, ConditionalExpression, WhileStatement, DoWhileStatement, LogicalExpression, UnaryExpression[operator="!"], ArrowFunctionExpression) > BinaryExpression[operator="&"]${types('left')}`,
@@ -141,25 +220,18 @@ const patterns = [
141
220
  {
142
221
  id: 'vertex-has-origin',
143
222
  selector: 'LogicalExpression[operator="&&"]'
144
- + `${helperCall('left', 'FunctionCallVertex', 'is')}`
223
+ + `${helperCall('left', 'Vertex', 'isFunctionCall')}`
145
224
  + `${call('right', 'includes')}[right.callee.object.type="MemberExpression"][right.callee.object.property.name="origin"]`,
146
225
  capture: { vertex: 'left.arguments.0', origin: 'right.arguments.0' },
147
- replace: 'FunctionCallVertex.hasOrigin({{vertex}}, {{origin}})',
226
+ replace: 'Vertex.hasOrigin({{vertex}}, {{origin}})',
148
227
  declaredIn: { 'left.callee.object': VERTEX, 'right.callee.object.property': VERTEX },
149
228
  sameText: [['left.arguments.0', 'right.callee.object.object']],
150
229
  /* `hasOrigin` is no type predicate, so the replacement can drop a narrowing the code below relies on */
151
230
  fix: false,
152
- message: 'Use `FunctionCallVertex.hasOrigin`, it is exactly this check. Check the narrowing first, `hasOrigin` returns a plain boolean.'
153
- },
154
- {
155
- /* the `[]` fallback allocates on every miss, and these sit in traversal loops */
156
- id: 'graph-edges-no-alloc',
157
- selector: 'LogicalExpression[operator="??"][right.type="ArrayExpression"][right.elements.length=0][left.type="CallExpression"][left.callee.type="MemberExpression"][left.callee.property.name=/^(outgoingEdges|ingoingEdges)$/]',
158
- capture: { edges: 'left' },
159
- replace: '{{edges}} ?? NoEdges',
160
- declaredIn: { 'left.callee.property': GRAPH },
161
- message: 'The `[]` fallback allocates on every miss, use the shared `NoEdges`.'
231
+ message: 'Use `Vertex.hasOrigin`, it is exactly this check. Check the narrowing first, `hasOrigin` returns a plain boolean.'
162
232
  },
233
+ edgesFallback('graph-edges-from', 'outgoingEdges', 'edgesFrom'),
234
+ edgesFallback('graph-edges-to', 'ingoingEdges', 'edgesTo'),
163
235
  vertexIs('vertex-is'),
164
236
  vertexIs('vertex-is-not', { negated: true }),
165
237
  vertexIs('vertex-is-optional', { optional: true }),