@eagleoutice/eslint-config-flowr 2.0.2 → 2.1.3

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.
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/patterns.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Default patterns of `flowr/replacement-pattern`, see the README for the format.
3
+ * The helpers they point at live in flowR: `DfEdge` in `dataflow/graph/edge`, the vertex helpers in
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`.
6
+ */
7
+ import type { ReplacementPattern } from './pattern-type';
8
+ declare const patterns: readonly ReplacementPattern[];
9
+ export = patterns;
package/patterns.js ADDED
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ /**
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`.
7
+ */
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
+ /** `x.types` under the given path */
17
+ const types = (prefix) => `[${prefix}.type="MemberExpression"][${prefix}.computed=false][${prefix}.property.name="types"]`;
18
+ /** `x.<name>` under the given path, without asserting the node kind, which the caller pins down */
19
+ const member = (prefix, name) => `[${prefix}.computed=false][${prefix}.property.name="${name}"]`;
20
+ /** `EdgeType.<Member>` / `VertexType.<Member>` / `RType.<Member>` on the right */
21
+ const rightIs = (name) => `[right.type="MemberExpression"][right.computed=false][right.object.name="${name}"]`;
22
+ /** compared against the literal `0`, either for "some bit is set" or for "none is" */
23
+ const versusZero = (nonZero) => `BinaryExpression[operator=${nonZero ? '/^(!==|>)$/' : '"==="'}][right.type="Literal"][right.value=0]`;
24
+ /** the `x.types & T` on the left of such a comparison */
25
+ const mask = '[left.type="BinaryExpression"][left.operator="&"]';
26
+ /** a call of `<prefix>.<property>(...)` */
27
+ const call = (prefix, property) => `[${prefix}.type="CallExpression"][${prefix}.callee.type="MemberExpression"][${prefix}.callee.property.name="${property}"]`;
28
+ /** a call of `<Helper>.<property>(<one argument>)` under the given path */
29
+ const helperCall = (prefix, helper, property) => `${call(prefix, property)}[${prefix}.callee.object.name="${helper}"][${prefix}.arguments.length=1]`;
30
+ /** `x === undefined` / `x !== undefined` under the given path */
31
+ const versusUndefined = (prefix, set) => `[${prefix}.type="BinaryExpression"][${prefix}.operator="${set ? '!==' : '==='}"][${prefix}.right.type="Identifier"][${prefix}.right.name="undefined"]`;
32
+ const destructuringHint = ' Prefer keeping the edge over destructuring `types`, the helpers take it directly and the wrapper object goes away.';
33
+ /** `x.types === T`, on the edge itself or on a destructured `types` */
34
+ const edgeIsOnly = (id, { negated = false, destructured = false } = {}) => ({
35
+ id,
36
+ selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"]`
37
+ + (destructured ? `[left.type="Identifier"][left.name="types"]${rightIs('EdgeType')}` : `${types('left')}:not([right.type="Literal"])`),
38
+ capture: destructured ? { type: 'right' } : { edge: 'left.object', type: 'right' },
39
+ replace: `${negated ? '!' : ''}DfEdge.isOnlyType(${destructured ? '{ types }' : '{{edge}}'}, {{type}})`,
40
+ declaredIn: destructured ? { 'right.object': EDGE } : { 'left.property': EDGE },
41
+ message: `\`${destructured ? '' : 'x.'}types ${negated ? '!==' : '==='} T\` `
42
+ + (negated ? 'holds as soon as any other type is set' : 'holds only if T is the *only* type')
43
+ + `; say so with \`${negated ? '!' : ''}DfEdge.isOnlyType\`, or use \`DfEdge.${negated ? 'doesNotIncludeType' : 'includesType'}\` for "${negated ? 'has not' : 'has'} this type".`
44
+ + (destructured ? destructuringHint : '')
45
+ });
46
+ /** `(x.types & T) !== 0` and its inverse */
47
+ const edgeMask = (id, helper, nonZero) => ({
48
+ id,
49
+ selector: `${versusZero(nonZero)}${mask}${types('left.left')}`,
50
+ capture: { edge: 'left.left.object', type: 'left.right' },
51
+ replace: `DfEdge.${helper}({{edge}}, {{type}})`,
52
+ declaredIn: { 'left.left.property': EDGE },
53
+ message: `Use \`DfEdge.${helper}\` instead of testing the bitmask by hand.`
54
+ });
55
+ /** `x.types !== 0`, the literal-0 comparisons {@link edgeIsOnly} leaves out */
56
+ const edgeStates = (id, helper, what, nonZero) => ({
57
+ id,
58
+ selector: `${versusZero(nonZero)}${types('left')}`,
59
+ capture: { edge: 'left.object' },
60
+ replace: `DfEdge.${helper}({{edge}})`,
61
+ declaredIn: { 'left.property': EDGE },
62
+ message: `Use \`DfEdge.${helper}\` to ask whether the edge states ${what}.`
63
+ });
64
+ /**
65
+ * A discriminator compared against its enum, `v.tag === VertexType.X` and `n.type === RType.X`.
66
+ * `optional` covers the `?.` spelling, which parses as a chain around the same member expression.
67
+ * `declaring` maps a path of the match to the file its symbol has to come from; the enum on the right is
68
+ * always pinned, the property on the left only where one file declares it for every case.
69
+ */
70
+ const discriminatorIs = (id, { property, enumName, enumFile, propertyFile, helper, hint }, { negated = false, optional = false } = {}) => {
71
+ const left = optional ? 'left.expression' : 'left';
72
+ return {
73
+ id,
74
+ selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"][left.type="${optional ? 'ChainExpression' : 'MemberExpression'}"]${member(left, property)}${rightIs(enumName)}`,
75
+ capture: { subject: `${left}.object`, type: 'right' },
76
+ replace: `${negated ? '!' : ''}${helper}.is({{subject}})`,
77
+ declaredIn: { 'right.object': enumFile, ...propertyFile ? { [`${left}.property`]: propertyFile } : {} },
78
+ message: `${hint} (\`${helper}.is\`), it narrows the type as well.`
79
+ };
80
+ };
81
+ /** `v.tag === VertexType.X`, with `{{type|last}}` spelling the name of the matching helper */
82
+ const vertexIs = (id, options) => discriminatorIs(id, {
83
+ property: 'tag', enumName: 'VertexType', enumFile: VERTEX, propertyFile: VERTEX,
84
+ helper: '{{type|last}}Vertex', hint: 'Compare through the vertex helper'
85
+ }, options);
86
+ /**
87
+ * `n.type === RType.X`. Every `RType` member has a helper object of the same name prefixed with `R`
88
+ * (`RType.Symbol` to `RSymbol`), so `{{type|last}}` names it without a table.
89
+ * Only the enum is pinned to its file: `type` is declared once per node interface, and on the `RNode`
90
+ * union it resolves to a synthesized symbol, so demanding a declaring file for it would match nothing.
91
+ */
92
+ const nodeIs = (id, options) => discriminatorIs(id, {
93
+ property: 'type', enumName: 'RType', enumFile: RTYPE,
94
+ helper: 'R{{type|last}}', hint: 'Ask the AST node helper'
95
+ }, options);
96
+ /**
97
+ * `<Helper>.is(x) && <extra check on x>`, the body of a more specific guard written out.
98
+ * `guard` pins down the right-hand side, `subject` is the path of the operand it repeats.
99
+ */
100
+ const guardWrittenOut = ({ id, helper, member: narrower, file, guard, subject, message, fix }) => ({
101
+ id,
102
+ selector: `LogicalExpression[operator="&&"]${helperCall('left', helper, 'is')}${guard}`,
103
+ capture: { node: 'left.arguments.0' },
104
+ replace: `${helper}.${narrower}({{node}})`,
105
+ declaredIn: { 'left.callee.object': file },
106
+ sameText: [['left.arguments.0', subject]],
107
+ ...fix === undefined ? {} : { fix },
108
+ message
109
+ });
110
+ /**
111
+ * `symbol.content === 'name'`. `content` of an {@link RSymbol} is an `Identifier`, which carries a namespace
112
+ * and turns into an array once it has one, so `===` against a bare name silently misses `pkg::name`.
113
+ */
114
+ const symbolName = (id, negated) => ({
115
+ id,
116
+ selector: `BinaryExpression[operator="${negated ? '!==' : '==='}"][left.type="MemberExpression"]${member('left', 'content')}[right.type="Literal"][right.value=type(string)]`,
117
+ capture: { symbol: 'left.object', name: 'right' },
118
+ replace: `Identifier.getName({{symbol}}.content) ${negated ? '!==' : '==='} {{name}}`,
119
+ declaredIn: { 'left.property': SYMBOL_NODE },
120
+ /* which of the two is meant is the author's call, so this is offered rather than applied */
121
+ fix: false,
122
+ 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
+ });
124
+ const patterns = [
125
+ edgeIsOnly('edge-is-only-type'),
126
+ edgeIsOnly('edge-is-not-only-type', { negated: true }),
127
+ edgeIsOnly('edge-is-only-type-destructured', { destructured: true }),
128
+ edgeIsOnly('edge-is-not-only-type-destructured', { negated: true, destructured: true }),
129
+ edgeStates('edge-has-any-type', 'hasAnyType', 'anything', true),
130
+ edgeStates('edge-has-no-type', 'hasNoType', 'nothing', false),
131
+ edgeMask('edge-includes-type', 'includesType', true),
132
+ edgeMask('edge-does-not-include-type', 'doesNotIncludeType', false),
133
+ {
134
+ id: 'edge-includes-type-truthy',
135
+ selector: `:matches(IfStatement, ConditionalExpression, WhileStatement, DoWhileStatement, LogicalExpression, UnaryExpression[operator="!"], ArrowFunctionExpression) > BinaryExpression[operator="&"]${types('left')}`,
136
+ capture: { edge: 'left.object', type: 'right' },
137
+ replace: 'DfEdge.includesType({{edge}}, {{type}})',
138
+ declaredIn: { 'left.property': EDGE },
139
+ message: 'Do not rely on the bitmask being truthy, `DfEdge.includesType` says what is meant.'
140
+ },
141
+ {
142
+ id: 'vertex-has-origin',
143
+ selector: 'LogicalExpression[operator="&&"]'
144
+ + `${helperCall('left', 'FunctionCallVertex', 'is')}`
145
+ + `${call('right', 'includes')}[right.callee.object.type="MemberExpression"][right.callee.object.property.name="origin"]`,
146
+ capture: { vertex: 'left.arguments.0', origin: 'right.arguments.0' },
147
+ replace: 'FunctionCallVertex.hasOrigin({{vertex}}, {{origin}})',
148
+ declaredIn: { 'left.callee.object': VERTEX, 'right.callee.object.property': VERTEX },
149
+ sameText: [['left.arguments.0', 'right.callee.object.object']],
150
+ /* `hasOrigin` is no type predicate, so the replacement can drop a narrowing the code below relies on */
151
+ 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`.'
162
+ },
163
+ vertexIs('vertex-is'),
164
+ vertexIs('vertex-is-not', { negated: true }),
165
+ vertexIs('vertex-is-optional', { optional: true }),
166
+ vertexIs('vertex-is-not-optional', { negated: true, optional: true }),
167
+ nodeIs('node-is'),
168
+ nodeIs('node-is-not', { negated: true }),
169
+ nodeIs('node-is-optional', { optional: true }),
170
+ nodeIs('node-is-not-optional', { negated: true, optional: true }),
171
+ guardWrittenOut({
172
+ id: 'call-is-named', helper: 'RFunctionCall', member: 'isNamed', file: CALL_NODE,
173
+ guard: `[right.type="MemberExpression"]${member('right', 'named')}`,
174
+ subject: 'right.object',
175
+ message: 'Use `RFunctionCall.isNamed`, it is this check and narrows to `RNamedFunctionCall`.'
176
+ }),
177
+ guardWrittenOut({
178
+ id: 'call-is-named-strict', helper: 'RFunctionCall', member: 'isNamed', file: CALL_NODE,
179
+ guard: `[right.type="BinaryExpression"][right.operator="==="][right.right.type="Literal"][right.right.value=true]${member('right.left', 'named')}`,
180
+ subject: 'right.left.object',
181
+ message: 'Use `RFunctionCall.isNamed`, it is this check and narrows to `RNamedFunctionCall`.'
182
+ }),
183
+ guardWrittenOut({
184
+ id: 'call-is-unnamed', helper: 'RFunctionCall', member: 'isUnnamed', file: CALL_NODE,
185
+ guard: `[right.type="UnaryExpression"][right.operator="!"][right.argument.type="MemberExpression"]${member('right.argument', 'named')}`,
186
+ subject: 'right.argument.object',
187
+ message: 'Use `RFunctionCall.isUnnamed`, it is this check and narrows to `RUnnamedFunctionCall`.'
188
+ }),
189
+ guardWrittenOut({
190
+ id: 'argument-is-named', helper: 'RArgument', member: 'isNamed', file: ARGUMENT_NODE,
191
+ guard: `${versusUndefined('right', true)}${member('right.left', 'name')}`,
192
+ subject: 'right.left.object',
193
+ message: 'Use `RArgument.isNamed`, it is this check and keeps the `name` non-optional afterwards.'
194
+ }),
195
+ guardWrittenOut({
196
+ id: 'argument-is-with-value', helper: 'RArgument', member: 'isWithValue', file: ARGUMENT_NODE,
197
+ guard: `${versusUndefined('right', true)}${member('right.left', 'value')}`,
198
+ subject: 'right.left.object',
199
+ message: 'Use `RArgument.isWithValue`, it is this check and keeps the `value` non-optional afterwards.'
200
+ }),
201
+ guardWrittenOut({
202
+ id: 'list-is-implicit', helper: 'RExpressionList', member: 'isImplicit', file: LIST_NODE,
203
+ guard: `${versusUndefined('right', false)}${member('right.left', 'grouping')}`,
204
+ subject: 'right.left.object',
205
+ message: 'Use `RExpressionList.isImplicit`, an expression list without `grouping` is exactly the implicit one.'
206
+ }),
207
+ symbolName('symbol-name-comparison', false),
208
+ symbolName('symbol-name-comparison-not', true),
209
+ {
210
+ id: 'array-sum',
211
+ selector: 'CallExpression[callee.type="MemberExpression"][callee.property.name="reduce"][arguments.length=2]'
212
+ + '[arguments.1.type="Literal"][arguments.1.value=0]'
213
+ + '[arguments.0.type="ArrowFunctionExpression"][arguments.0.params.length=2]'
214
+ + '[arguments.0.body.type="BinaryExpression"][arguments.0.body.operator="+"]',
215
+ capture: { array: 'callee.object' },
216
+ replace: 'arraySum({{array}})',
217
+ sameText: [['arguments.0.params.0', 'arguments.0.body.left'], ['arguments.0.params.1', 'arguments.0.body.right']],
218
+ message: 'Use `arraySum`, summing a list is not a place to spell out a fold.'
219
+ },
220
+ {
221
+ /* `filter` walks the whole list and allocates the matches only to ask whether there is one */
222
+ id: 'some-instead-of-filter-length',
223
+ selector: `${versusZero(true)}[left.type="MemberExpression"]${member('left', 'length')}`
224
+ + `${call('left.object', 'filter')}[left.object.arguments.length=1]`,
225
+ capture: { array: 'left.object.callee.object', predicate: 'left.object.arguments.0' },
226
+ replace: '{{array}}.some({{predicate}})',
227
+ message: '`filter(...).length > 0` walks the whole list and allocates the matches, `some` stops at the first hit.'
228
+ },
229
+ {
230
+ id: 'none-instead-of-filter-length',
231
+ selector: `${versusZero(false)}[left.type="MemberExpression"]${member('left', 'length')}`
232
+ + `${call('left.object', 'filter')}[left.object.arguments.length=1]`,
233
+ capture: { array: 'left.object.callee.object', predicate: 'left.object.arguments.0' },
234
+ replace: '!{{array}}.some({{predicate}})',
235
+ message: '`filter(...).length === 0` walks the whole list and allocates the matches, `!some` stops at the first hit.'
236
+ }
237
+ ];
238
+ module.exports = patterns;
package/plugin.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ declare const plugin: {
2
+ meta: {
3
+ name: string;
4
+ };
5
+ rules: {
6
+ 'use-instead': import("eslint").Rule.RuleModule;
7
+ 'replacement-pattern': import("eslint").Rule.RuleModule;
8
+ };
9
+ };
10
+ export = plugin;
package/plugin.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ /** The flowR-specific rules, see the README for the tags they read. */
6
+ const replacement_pattern_1 = __importDefault(require("./rules/replacement-pattern"));
7
+ const use_instead_1 = __importDefault(require("./rules/use-instead"));
8
+ const plugin = {
9
+ meta: { name: '@eagleoutice/eslint-config-flowr' },
10
+ rules: {
11
+ 'use-instead': use_instead_1.default,
12
+ 'replacement-pattern': replacement_pattern_1.default
13
+ }
14
+ };
15
+ module.exports = plugin;
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export = rule;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ const util_1 = require("./util");
3
+ /** Resolves a dotted path (`left.object`) relative to the matched node. */
4
+ function resolvePath(node, path) {
5
+ let current = node;
6
+ for (const step of path.split('.')) {
7
+ if (current === null || current === undefined) {
8
+ return undefined;
9
+ }
10
+ current = current[step];
11
+ }
12
+ return (current ?? undefined);
13
+ }
14
+ /**
15
+ * Fills `{{name}}` with the source text of a capture and `{{name|last}}` with the part behind its
16
+ * last dot. Returns `undefined` if a capture is missing.
17
+ */
18
+ function render(template, captures, node, sourceCode) {
19
+ let failed = false;
20
+ const filled = template.replace(/\{\{\s*([A-Za-z0-9_]+)\s*(?:\|\s*(last)\s*)?\}\}/g, (_all, name, filter) => {
21
+ const captured = captures[name] === undefined ? undefined : resolvePath(node, captures[name]);
22
+ if (captured === undefined) {
23
+ failed = true;
24
+ return '';
25
+ }
26
+ const text = sourceCode.getText(captured);
27
+ return filter === 'last' ? text.slice(text.lastIndexOf('.') + 1) : text;
28
+ });
29
+ return failed ? undefined : filled;
30
+ }
31
+ const rule = {
32
+ meta: {
33
+ type: 'suggestion',
34
+ fixable: 'code',
35
+ hasSuggestions: true,
36
+ docs: {
37
+ description: 'suggest the flowR helper that replaces a hand-written pattern',
38
+ url: 'https://github.com/flowr-analysis/flowr-lint#flowrreplacement-pattern'
39
+ },
40
+ schema: [{
41
+ type: 'object',
42
+ properties: {
43
+ patterns: {
44
+ type: 'array',
45
+ items: {
46
+ type: 'object',
47
+ properties: {
48
+ /** identifies the pattern in `@lintIgnore` and in the message */
49
+ id: { type: 'string' },
50
+ /** esquery selector, the language `no-restricted-syntax` uses */
51
+ selector: { type: 'string' },
52
+ message: { type: 'string' },
53
+ /** capture name to a path relative to the match */
54
+ capture: { type: 'object', additionalProperties: { type: 'string' } },
55
+ /** replacement template, filled from the captures */
56
+ replace: { type: 'string' },
57
+ /** path to a substring of the file its symbol must be declared in, this is what keeps the pattern flowR-specific */
58
+ declaredIn: { type: 'object', additionalProperties: { type: 'string' } },
59
+ /** pairs of paths that have to spell the same code, esquery has no back-references */
60
+ sameText: { type: 'array', items: { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 2 } },
61
+ /** `true` forces the autofix, `false` forces a suggestion; by default it is fixed when the replacement is already in scope */
62
+ fix: { type: 'boolean' }
63
+ },
64
+ required: ['selector', 'replace'],
65
+ additionalProperties: false
66
+ }
67
+ }
68
+ },
69
+ additionalProperties: false
70
+ }],
71
+ messages: {
72
+ replacement: '{{message}}',
73
+ suggest: 'Replace with `{{replacement}}`.'
74
+ }
75
+ },
76
+ create(context) {
77
+ const sourceCode = context.sourceCode;
78
+ const services = (0, util_1.typeServices)(context);
79
+ const checker = services?.program.getTypeChecker();
80
+ const patterns = context.options[0]?.patterns ?? [];
81
+ /* several patterns may share a selector, so they are grouped instead of overwriting each other */
82
+ const bySelector = new Map();
83
+ for (const pattern of patterns) {
84
+ const group = bySelector.get(pattern.selector);
85
+ if (group) {
86
+ group.push(pattern);
87
+ }
88
+ else {
89
+ bySelector.set(pattern.selector, [pattern]);
90
+ }
91
+ }
92
+ /**
93
+ * The match only counts if every named node resolves to a declaration of the expected flowR file.
94
+ * The declaring file itself is exempt, it is where the helper is built.
95
+ */
96
+ function isFlowr(node, pattern) {
97
+ if (!pattern.declaredIn) {
98
+ return true;
99
+ }
100
+ else if (!services || !checker) {
101
+ return false;
102
+ }
103
+ return Object.entries(pattern.declaredIn).every(([path, expected]) => {
104
+ const file = (0, util_1.declarationFile)((0, util_1.symbolOf)(resolvePath(node, path), services, checker));
105
+ return file !== undefined && file !== context.filename && file.replaceAll('\\', '/').includes(expected);
106
+ });
107
+ }
108
+ /** esquery cannot demand that two sub-nodes are the same, so the pattern names the pairs itself */
109
+ function agrees(node, pattern) {
110
+ return (pattern.sameText ?? []).every(([a, b]) => {
111
+ const left = resolvePath(node, a), right = resolvePath(node, b);
112
+ return left !== undefined && right !== undefined && sourceCode.getText(left) === sourceCode.getText(right);
113
+ });
114
+ }
115
+ function check(node, pattern) {
116
+ const ids = [pattern.id, 'replacement-pattern'].filter(id => id !== undefined);
117
+ if (!agrees(node, pattern) || !isFlowr(node, pattern) || (0, util_1.suppressed)(context, node, ids)) {
118
+ return;
119
+ }
120
+ const captures = pattern.capture ?? {};
121
+ const replacement = render(pattern.replace, captures, node, sourceCode);
122
+ const message = (pattern.message === undefined ? undefined : render(pattern.message, captures, node, sourceCode))
123
+ ?? (replacement ? `Use \`${replacement}\` here.` : `\`${pattern.id ?? pattern.selector}\` has a helper replacement.`);
124
+ if (replacement === undefined) {
125
+ context.report({ node, messageId: 'replacement', data: { message } });
126
+ return;
127
+ }
128
+ const fix = (fixer) => fixer.replaceText(node, replacement);
129
+ /* fixed on the spot where the helper is already in scope, offered as a suggestion otherwise */
130
+ const applicable = pattern.fix ?? (0, util_1.isApplicable)(context, node, replacement);
131
+ context.report({
132
+ node,
133
+ messageId: 'replacement',
134
+ data: { message },
135
+ ...applicable ? { fix } : { suggest: [{ messageId: 'suggest', data: { replacement }, fix }] }
136
+ });
137
+ }
138
+ const listeners = {};
139
+ for (const [selector, group] of bySelector) {
140
+ listeners[selector] = (node) => group.forEach(pattern => check(node, pattern));
141
+ }
142
+ return listeners;
143
+ }
144
+ };
145
+ module.exports = rule;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The slice of the TypeScript compiler API the rules touch, spelled out structurally so that
3
+ * `typescript` stays out of this package's dependencies.
4
+ */
5
+ export interface TsSourceFile {
6
+ fileName: string;
7
+ }
8
+ export interface TsDeclaration {
9
+ getSourceFile(): TsSourceFile | undefined;
10
+ }
11
+ export interface TsJsDocTag {
12
+ name: string;
13
+ text?: readonly {
14
+ text: string;
15
+ }[];
16
+ }
17
+ export interface TsSymbol {
18
+ flags: number;
19
+ declarations?: readonly TsDeclaration[];
20
+ getName(): string;
21
+ getJsDocTags(checker: TsTypeChecker): readonly TsJsDocTag[];
22
+ }
23
+ export interface TsType {
24
+ getProperties(): readonly TsSymbol[];
25
+ }
26
+ export interface TsTypeChecker {
27
+ getSymbolAtLocation(node: unknown): TsSymbol | undefined;
28
+ getAliasedSymbol(symbol: TsSymbol): TsSymbol;
29
+ getTypeOfSymbolAtLocation(symbol: TsSymbol, location: TsDeclaration): TsType;
30
+ }
31
+ /** What typescript-eslint hands to a type-aware rule. */
32
+ export interface TypeServices {
33
+ program: {
34
+ getTypeChecker(): TsTypeChecker;
35
+ };
36
+ esTreeNodeToTSNodeMap: {
37
+ get(node: unknown): unknown;
38
+ };
39
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * The slice of the TypeScript compiler API the rules touch, spelled out structurally so that
4
+ * `typescript` stays out of this package's dependencies.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export = rule;
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ const util_1 = require("./util");
3
+ /** documentation is read once per symbol and reused for every reference */
4
+ const tagCache = new WeakMap();
5
+ /** the `@useInstead` tags of all members of a helper object, keyed by the object's symbol */
6
+ const memberCache = new WeakMap();
7
+ function cachedTag(symbol, checker) {
8
+ let tag = tagCache.get(symbol);
9
+ if (tag === undefined) {
10
+ tag = (0, util_1.useInsteadOf)(symbol, checker) ?? null;
11
+ tagCache.set(symbol, tag);
12
+ }
13
+ return tag;
14
+ }
15
+ function cachedMemberTags(symbol, checker) {
16
+ const cached = memberCache.get(symbol);
17
+ if (cached) {
18
+ return cached;
19
+ }
20
+ const tags = new Map();
21
+ const declaration = symbol.declarations?.[0];
22
+ if (declaration) {
23
+ try {
24
+ for (const property of checker.getTypeOfSymbolAtLocation(symbol, declaration).getProperties()) {
25
+ const tag = (0, util_1.useInsteadOf)(property, checker);
26
+ if (tag !== undefined) {
27
+ tags.set(property.getName(), tag);
28
+ }
29
+ }
30
+ }
31
+ catch {
32
+ /* nothing to check without a resolvable type */
33
+ }
34
+ }
35
+ memberCache.set(symbol, tags);
36
+ return tags;
37
+ }
38
+ /** `{@link Resolve.toValue}` becomes `Resolve.toValue`, owned by `Resolve`. */
39
+ function parseTarget(tag) {
40
+ const display = tag.replace(/^\{\s*@link\s*/, '').replace(/\s*\|.*$/, '').replace(/\s*\}\s*$/, '').trim();
41
+ return { display, owner: display.split('.')[0] };
42
+ }
43
+ /** The references that build the replacement in the first place: helper-object wiring and re-exports. */
44
+ function isWiring(identifier) {
45
+ const parent = identifier.parent;
46
+ switch (parent?.type) {
47
+ case 'Property':
48
+ return parent.value === identifier && parent.parent?.type === 'ObjectExpression';
49
+ case 'SpreadElement':
50
+ return parent.parent?.type === 'ObjectExpression';
51
+ case 'ExportSpecifier':
52
+ case 'ImportSpecifier':
53
+ case 'ImportDefaultSpecifier':
54
+ case 'ImportNamespaceSpecifier':
55
+ return true;
56
+ default:
57
+ return false;
58
+ }
59
+ }
60
+ const rule = {
61
+ meta: {
62
+ type: 'suggestion',
63
+ fixable: 'code',
64
+ hasSuggestions: true,
65
+ docs: {
66
+ description: 'enforce the replacement named by the `@useInstead` documentation tag',
67
+ url: 'https://github.com/flowr-analysis/flowr-lint#flowruse-instead'
68
+ },
69
+ schema: [{
70
+ type: 'object',
71
+ properties: { helperObjectPattern: { type: 'string' } },
72
+ additionalProperties: false
73
+ }],
74
+ messages: {
75
+ useInstead: 'Do not use `{{name}}` directly, use `{{replacement}}` instead.',
76
+ useInsteadPlain: 'Do not use `{{name}}` directly, see its documentation for the replacement.',
77
+ suggest: 'Replace with `{{replacement}}`.'
78
+ }
79
+ },
80
+ create(context) {
81
+ const services = (0, util_1.typeServices)(context);
82
+ if (!services) {
83
+ return {};
84
+ }
85
+ const checker = services.program.getTypeChecker();
86
+ const sourceCode = context.sourceCode;
87
+ /* helper objects are PascalCase, so member checks stay cheap */
88
+ const helperObjectPattern = new RegExp(context.options[0]?.helperObjectPattern ?? '^[A-Z]');
89
+ const candidates = new Map();
90
+ /** the helper objects declared here, a file may use what it wraps */
91
+ const owned = new Set();
92
+ function report(node, name, tag) {
93
+ const { display, owner } = parseTarget(tag);
94
+ if (owned.has(owner) || (0, util_1.suppressed)(context, node, ['use-instead'])) {
95
+ return;
96
+ }
97
+ const fix = (fixer) => fixer.replaceText(node, display);
98
+ /* fixed on the spot where the helper is already in scope, offered as a suggestion otherwise */
99
+ const applicable = display !== '' && (0, util_1.isApplicable)(context, node, display);
100
+ context.report({
101
+ node,
102
+ messageId: display ? 'useInstead' : 'useInsteadPlain',
103
+ data: { name, replacement: display },
104
+ ...display === '' ? {} : applicable ? { fix } : { suggest: [{ messageId: 'suggest', data: { replacement: display }, fix }] }
105
+ });
106
+ }
107
+ return {
108
+ Program() {
109
+ const global = sourceCode.scopeManager.globalScope;
110
+ const module = global?.childScopes.find(s => s.type === 'module') ?? global;
111
+ const foreign = [];
112
+ for (const variable of module?.variables ?? []) {
113
+ const declaration = variable.defs[0]?.name;
114
+ if (declaration?.type !== 'Identifier') {
115
+ continue;
116
+ }
117
+ const symbol = (0, util_1.symbolOf)(declaration, services, checker);
118
+ if (!symbol) {
119
+ continue;
120
+ }
121
+ else if ((0, util_1.declarationFile)(symbol) === context.filename) {
122
+ /* a declaration may always use itself */
123
+ owned.add(variable.name);
124
+ continue;
125
+ }
126
+ if (helperObjectPattern.test(variable.name)) {
127
+ candidates.set(variable.name, symbol);
128
+ }
129
+ const tag = cachedTag(symbol, checker);
130
+ if (tag !== null) {
131
+ foreign.push([variable, declaration, tag]);
132
+ }
133
+ }
134
+ for (const [variable, declaration, tag] of foreign) {
135
+ for (const { identifier } of variable.references) {
136
+ if (identifier !== declaration) {
137
+ const node = identifier;
138
+ if (!isWiring(node)) {
139
+ report(node, variable.name, tag);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ },
145
+ MemberExpression(node) {
146
+ if (node.computed || node.object.type !== 'Identifier' || node.property.type !== 'Identifier') {
147
+ return;
148
+ }
149
+ const symbol = candidates.get(node.object.name);
150
+ if (!symbol) {
151
+ return;
152
+ }
153
+ const tag = cachedMemberTags(symbol, checker).get(node.property.name);
154
+ if (tag !== undefined) {
155
+ report(node, `${node.object.name}.${node.property.name}`, tag);
156
+ }
157
+ }
158
+ };
159
+ }
160
+ };
161
+ module.exports = rule;