@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 +4 -2
- package/index.js +33 -0
- package/package.json +9 -1
- package/patterns.js +89 -17
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`, `
|
|
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-
|
|
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
|
+
"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}
|
|
81
|
+
replace: `${negated ? '!' : ''}${helper}.${name}({{subject}})`,
|
|
77
82
|
declaredIn: { 'right.object': enumFile, ...propertyFile ? { [`${left}.property`]: propertyFile } : {} },
|
|
78
|
-
message: `${hint} (\`${helper}
|
|
83
|
+
message: `${hint} (\`${helper}.${name}\`), it narrows the type as well.`
|
|
79
84
|
};
|
|
80
85
|
};
|
|
81
|
-
/**
|
|
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}}
|
|
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', '
|
|
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: '
|
|
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 `
|
|
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 }),
|