@eagleoutice/flowr 2.4.1 → 2.4.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.
- package/README.md +33 -21
- package/control-flow/simple-visitor.js +3 -3
- package/control-flow/useless-loop.d.ts +19 -0
- package/control-flow/useless-loop.js +127 -0
- package/dataflow/environments/built-in.d.ts +0 -1
- package/dataflow/environments/built-in.js +0 -17
- package/dataflow/environments/default-builtin-config.js +1 -1
- package/dataflow/environments/overwrite.js +2 -2
- package/dataflow/graph/graph.d.ts +1 -1
- package/dataflow/internal/linker.js +6 -6
- package/dataflow/internal/process/functions/call/built-in/built-in-expression-list.js +1 -1
- package/documentation/print-linter-wiki.js +1 -0
- package/linter/linter-rules.d.ts +30 -0
- package/linter/linter-rules.js +2 -0
- package/linter/rules/useless-loop.d.ts +47 -0
- package/linter/rules/useless-loop.js +47 -0
- package/package.json +1 -1
- package/queries/catalog/call-context-query/identify-link-to-last-call-relation.js +1 -1
- package/util/version.js +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ It offers a wide variety of features, for example:
|
|
|
24
24
|
|
|
25
25
|
```shell
|
|
26
26
|
$ docker run -it --rm eagleoutice/flowr # or npm run flowr
|
|
27
|
-
flowR repl using flowR v2.4.
|
|
27
|
+
flowR repl using flowR v2.4.2, R v4.5.0 (r-shell engine)
|
|
28
28
|
R> :query @linter "read.csv(\"/root/x.txt\")"
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -39,7 +39,7 @@ It offers a wide variety of features, for example:
|
|
|
39
39
|
╰ **File Path Validity** (file-path-validity):
|
|
40
40
|
╰ certain:
|
|
41
41
|
╰ Path `/root/x.txt` at 1.1-23
|
|
42
|
-
╰ _Metadata_: <code>{"totalReads":1,"totalUnknown":0,"totalWritesBeforeAlways":0,"totalValid":0,"searchTimeMs":
|
|
42
|
+
╰ _Metadata_: <code>{"totalReads":1,"totalUnknown":0,"totalWritesBeforeAlways":0,"totalValid":0,"searchTimeMs":0,"processTimeMs":1}</code>
|
|
43
43
|
╰ **Seeded Randomness** (seeded-randomness):
|
|
44
44
|
╰ _Metadata_: <code>{"consumerCalls":0,"callsWithFunctionProducers":0,"callsWithAssignmentProducers":0,"callsWithNonConstantProducers":0,"searchTimeMs":0,"processTimeMs":0}</code>
|
|
45
45
|
╰ **Absolute Paths** (absolute-file-paths):
|
|
@@ -49,11 +49,13 @@ It offers a wide variety of features, for example:
|
|
|
49
49
|
╰ **Unused Definitions** (unused-definitions):
|
|
50
50
|
╰ _Metadata_: <code>{"totalConsidered":0,"searchTimeMs":0,"processTimeMs":0}</code>
|
|
51
51
|
╰ **Naming Convention** (naming-convention):
|
|
52
|
-
╰ _Metadata_: <code>{"numMatches":0,"numBreak":0,"searchTimeMs":
|
|
52
|
+
╰ _Metadata_: <code>{"numMatches":0,"numBreak":0,"searchTimeMs":0,"processTimeMs":0}</code>
|
|
53
53
|
╰ **Dataframe Access Validation** (dataframe-access-validation):
|
|
54
|
-
╰ _Metadata_: <code>{"numOperations":0,"numAccesses":0,"totalAccessed":0,"searchTimeMs":0,"processTimeMs":
|
|
54
|
+
╰ _Metadata_: <code>{"numOperations":0,"numAccesses":0,"totalAccessed":0,"searchTimeMs":0,"processTimeMs":1}</code>
|
|
55
55
|
╰ **Dead Code** (dead-code):
|
|
56
56
|
╰ _Metadata_: <code>{"consideredNodes":5,"searchTimeMs":0,"processTimeMs":0}</code>
|
|
57
|
+
╰ **Useless Loops** (useless-loop):
|
|
58
|
+
╰ _Metadata_: <code>{"numOfUselessLoops":0,"searchTimeMs":0,"processTimeMs":0}</code>
|
|
57
59
|
[;3mAll queries together required ≈2 ms (1ms accuracy, total 8 ms)[0m[0m
|
|
58
60
|
```
|
|
59
61
|
|
|
@@ -76,7 +78,7 @@ It offers a wide variety of features, for example:
|
|
|
76
78
|
|
|
77
79
|
_Results (prettified and summarized):_
|
|
78
80
|
|
|
79
|
-
Query: **linter** (
|
|
81
|
+
Query: **linter** (14 ms)\
|
|
80
82
|
╰ **Deprecated Functions** (deprecated-functions):\
|
|
81
83
|
╰ _Metadata_: <code>{"totalDeprecatedCalls":0,"totalDeprecatedFunctionDefinitions":0,"searchTimeMs":2,"processTimeMs":0}</code>\
|
|
82
84
|
╰ **File Path Validity** (file-path-validity):\
|
|
@@ -84,24 +86,26 @@ It offers a wide variety of features, for example:
|
|
|
84
86
|
╰ Path `/root/x.txt` at 1.1-23\
|
|
85
87
|
╰ _Metadata_: <code>{"totalReads":1,"totalUnknown":0,"totalWritesBeforeAlways":0,"totalValid":0,"searchTimeMs":4,"processTimeMs":1}</code>\
|
|
86
88
|
╰ **Seeded Randomness** (seeded-randomness):\
|
|
87
|
-
╰ _Metadata_: <code>{"consumerCalls":0,"callsWithFunctionProducers":0,"callsWithAssignmentProducers":0,"callsWithNonConstantProducers":0,"searchTimeMs":0,"processTimeMs":
|
|
89
|
+
╰ _Metadata_: <code>{"consumerCalls":0,"callsWithFunctionProducers":0,"callsWithAssignmentProducers":0,"callsWithNonConstantProducers":0,"searchTimeMs":0,"processTimeMs":1}</code>\
|
|
88
90
|
╰ **Absolute Paths** (absolute-file-paths):\
|
|
89
91
|
╰ certain:\
|
|
90
92
|
╰ Path `/root/x.txt` at 1.1-23\
|
|
91
|
-
╰ _Metadata_: <code>{"totalConsidered":1,"totalUnknown":0,"searchTimeMs":
|
|
93
|
+
╰ _Metadata_: <code>{"totalConsidered":1,"totalUnknown":0,"searchTimeMs":2,"processTimeMs":0}</code>\
|
|
92
94
|
╰ **Unused Definitions** (unused-definitions):\
|
|
93
|
-
╰ _Metadata_: <code>{"totalConsidered":0,"searchTimeMs":
|
|
95
|
+
╰ _Metadata_: <code>{"totalConsidered":0,"searchTimeMs":0,"processTimeMs":0}</code>\
|
|
94
96
|
╰ **Naming Convention** (naming-convention):\
|
|
95
97
|
╰ _Metadata_: <code>{"numMatches":0,"numBreak":0,"searchTimeMs":0,"processTimeMs":0}</code>\
|
|
96
98
|
╰ **Dataframe Access Validation** (dataframe-access-validation):\
|
|
97
|
-
╰ _Metadata_: <code>{"numOperations":0,"numAccesses":0,"totalAccessed":0,"searchTimeMs":0,"processTimeMs":
|
|
99
|
+
╰ _Metadata_: <code>{"numOperations":0,"numAccesses":0,"totalAccessed":0,"searchTimeMs":0,"processTimeMs":3}</code>\
|
|
98
100
|
╰ **Dead Code** (dead-code):\
|
|
99
101
|
╰ _Metadata_: <code>{"consideredNodes":5,"searchTimeMs":1,"processTimeMs":0}</code>\
|
|
100
|
-
|
|
102
|
+
╰ **Useless Loops** (useless-loop):\
|
|
103
|
+
╰ _Metadata_: <code>{"numOfUselessLoops":0,"searchTimeMs":0,"processTimeMs":0}</code>\
|
|
104
|
+
_All queries together required ≈14 ms (1ms accuracy, total 209 ms)_
|
|
101
105
|
|
|
102
106
|
<details> <summary style="color:gray">Show Detailed Results as Json</summary>
|
|
103
107
|
|
|
104
|
-
The analysis required
|
|
108
|
+
The analysis required _208.5 ms_ (including parsing and normalization and the query) within the generation environment.
|
|
105
109
|
|
|
106
110
|
In general, the JSON contains the Ids of the nodes in question as they are present in the normalized AST or the dataflow graph of flowR.
|
|
107
111
|
Please consult the [Interface](https://github.com/flowr-analysis/flowr/wiki/Interface) wiki page for more information on how to get those.
|
|
@@ -152,7 +156,7 @@ It offers a wide variety of features, for example:
|
|
|
152
156
|
"callsWithAssignmentProducers": 0,
|
|
153
157
|
"callsWithNonConstantProducers": 0,
|
|
154
158
|
"searchTimeMs": 0,
|
|
155
|
-
"processTimeMs":
|
|
159
|
+
"processTimeMs": 1
|
|
156
160
|
}
|
|
157
161
|
},
|
|
158
162
|
"absolute-file-paths": {
|
|
@@ -171,7 +175,7 @@ It offers a wide variety of features, for example:
|
|
|
171
175
|
".meta": {
|
|
172
176
|
"totalConsidered": 1,
|
|
173
177
|
"totalUnknown": 0,
|
|
174
|
-
"searchTimeMs":
|
|
178
|
+
"searchTimeMs": 2,
|
|
175
179
|
"processTimeMs": 0
|
|
176
180
|
}
|
|
177
181
|
},
|
|
@@ -179,7 +183,7 @@ It offers a wide variety of features, for example:
|
|
|
179
183
|
"results": [],
|
|
180
184
|
".meta": {
|
|
181
185
|
"totalConsidered": 0,
|
|
182
|
-
"searchTimeMs":
|
|
186
|
+
"searchTimeMs": 0,
|
|
183
187
|
"processTimeMs": 0
|
|
184
188
|
}
|
|
185
189
|
},
|
|
@@ -199,7 +203,7 @@ It offers a wide variety of features, for example:
|
|
|
199
203
|
"numAccesses": 0,
|
|
200
204
|
"totalAccessed": 0,
|
|
201
205
|
"searchTimeMs": 0,
|
|
202
|
-
"processTimeMs":
|
|
206
|
+
"processTimeMs": 3
|
|
203
207
|
}
|
|
204
208
|
},
|
|
205
209
|
"dead-code": {
|
|
@@ -209,14 +213,22 @@ It offers a wide variety of features, for example:
|
|
|
209
213
|
"searchTimeMs": 1,
|
|
210
214
|
"processTimeMs": 0
|
|
211
215
|
}
|
|
216
|
+
},
|
|
217
|
+
"useless-loop": {
|
|
218
|
+
"results": [],
|
|
219
|
+
".meta": {
|
|
220
|
+
"numOfUselessLoops": 0,
|
|
221
|
+
"searchTimeMs": 0,
|
|
222
|
+
"processTimeMs": 0
|
|
223
|
+
}
|
|
212
224
|
}
|
|
213
225
|
},
|
|
214
226
|
".meta": {
|
|
215
|
-
"timing":
|
|
227
|
+
"timing": 14
|
|
216
228
|
}
|
|
217
229
|
},
|
|
218
230
|
".meta": {
|
|
219
|
-
"timing":
|
|
231
|
+
"timing": 14
|
|
220
232
|
}
|
|
221
233
|
}
|
|
222
234
|
```
|
|
@@ -283,7 +295,7 @@ It offers a wide variety of features, for example:
|
|
|
283
295
|
|
|
284
296
|
```shell
|
|
285
297
|
$ docker run -it --rm eagleoutice/flowr # or npm run flowr
|
|
286
|
-
flowR repl using flowR v2.4.
|
|
298
|
+
flowR repl using flowR v2.4.2, R v4.5.0 (r-shell engine)
|
|
287
299
|
R> :slicer test/testfiles/example.R --criterion "11@sum"
|
|
288
300
|
```
|
|
289
301
|
|
|
@@ -330,7 +342,7 @@ It offers a wide variety of features, for example:
|
|
|
330
342
|
|
|
331
343
|
|
|
332
344
|
* 🚀 **fast data- and control-flow graphs**\
|
|
333
|
-
Within just <i><span title="This measurement is automatically fetched from the latest benchmark!">
|
|
345
|
+
Within just <i><span title="This measurement is automatically fetched from the latest benchmark!">132.8 ms</span></i> (as of Aug 19, 2025),
|
|
334
346
|
_flowR_ can analyze the data- and control-flow of the average real-world R script. See the [benchmarks](https://flowr-analysis.github.io/flowr/wiki/stats/benchmark) for more information,
|
|
335
347
|
and consult the [wiki pages](https://github.com/flowr-analysis/flowr/wiki/Dataflow-Graph) for more details on the dataflow graph.
|
|
336
348
|
|
|
@@ -366,7 +378,7 @@ It offers a wide variety of features, for example:
|
|
|
366
378
|
|
|
367
379
|
```shell
|
|
368
380
|
$ docker run -it --rm eagleoutice/flowr # or npm run flowr
|
|
369
|
-
flowR repl using flowR v2.4.
|
|
381
|
+
flowR repl using flowR v2.4.2, R v4.5.0 (r-shell engine)
|
|
370
382
|
R> :dataflow* test/testfiles/example.R
|
|
371
383
|
```
|
|
372
384
|
|
|
@@ -667,7 +679,7 @@ It offers a wide variety of features, for example:
|
|
|
667
679
|
```
|
|
668
680
|
|
|
669
681
|
|
|
670
|
-
(The analysis required
|
|
682
|
+
(The analysis required _14.3 ms_ (including parse and normalize, using the [r-shell](https://github.com/flowr-analysis/flowr/wiki/Engines) engine) within the generation environment.)
|
|
671
683
|
|
|
672
684
|
|
|
673
685
|
|
|
@@ -35,9 +35,9 @@ visitor) {
|
|
|
35
35
|
queue = queue.concat(get.elems.toReversed().map(e => e.id));
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
const incoming = graph.outgoingEdges(current)
|
|
39
|
-
|
|
40
|
-
queue.push(
|
|
38
|
+
const incoming = graph.outgoingEdges(current);
|
|
39
|
+
if (incoming) {
|
|
40
|
+
queue.push(...incoming.keys());
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FlowrConfigOptions } from '../config';
|
|
2
|
+
import type { DataflowGraph } from '../dataflow/graph/graph';
|
|
3
|
+
import type { NormalizedAst } from '../r-bridge/lang-4.x/ast/model/processing/decorate';
|
|
4
|
+
import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id';
|
|
5
|
+
import type { ControlFlowInformation } from './control-flow-graph';
|
|
6
|
+
export declare const loopyFunctions: Set<"builtin:default" | "builtin:eval" | "builtin:apply" | "builtin:expression-list" | "builtin:source" | "builtin:access" | "builtin:if-then-else" | "builtin:get" | "builtin:rm" | "builtin:library" | "builtin:assignment" | "builtin:special-bin-op" | "builtin:pipe" | "builtin:function-definition" | "builtin:quote" | "builtin:for-loop" | "builtin:repeat-loop" | "builtin:while-loop" | "builtin:replacement" | "builtin:list" | "builtin:vector">;
|
|
7
|
+
/**
|
|
8
|
+
* Checks whether a loop only loops once
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
*
|
|
12
|
+
* @param loop - nodeid of the loop to analyse
|
|
13
|
+
* @param dataflow - dataflow graph
|
|
14
|
+
* @param controlflow - control flow graph
|
|
15
|
+
* @param ast - normalized ast
|
|
16
|
+
* @param config - current flowr config
|
|
17
|
+
* @returns true if the given loop only iterates once
|
|
18
|
+
*/
|
|
19
|
+
export declare function onlyLoopsOnce(loop: NodeId, dataflow: DataflowGraph, controlflow: ControlFlowInformation, ast: NormalizedAst, config: FlowrConfigOptions): boolean | undefined;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loopyFunctions = void 0;
|
|
4
|
+
exports.onlyLoopsOnce = onlyLoopsOnce;
|
|
5
|
+
const alias_tracking_1 = require("../dataflow/eval/resolve/alias-tracking");
|
|
6
|
+
const general_1 = require("../dataflow/eval/values/general");
|
|
7
|
+
const r_value_1 = require("../dataflow/eval/values/r-value");
|
|
8
|
+
const vertex_1 = require("../dataflow/graph/vertex");
|
|
9
|
+
const info_1 = require("../dataflow/info");
|
|
10
|
+
const r_function_call_1 = require("../r-bridge/lang-4.x/ast/model/nodes/r-function-call");
|
|
11
|
+
const assert_1 = require("../util/assert");
|
|
12
|
+
const semantic_cfg_guided_visitor_1 = require("./semantic-cfg-guided-visitor");
|
|
13
|
+
exports.loopyFunctions = new Set(['builtin:for-loop', 'builtin:while-loop', 'builtin:repeat-loop']);
|
|
14
|
+
/**
|
|
15
|
+
* Checks whether a loop only loops once
|
|
16
|
+
*
|
|
17
|
+
*
|
|
18
|
+
*
|
|
19
|
+
* @param loop - nodeid of the loop to analyse
|
|
20
|
+
* @param dataflow - dataflow graph
|
|
21
|
+
* @param controlflow - control flow graph
|
|
22
|
+
* @param ast - normalized ast
|
|
23
|
+
* @param config - current flowr config
|
|
24
|
+
* @returns true if the given loop only iterates once
|
|
25
|
+
*/
|
|
26
|
+
function onlyLoopsOnce(loop, dataflow, controlflow, ast, config) {
|
|
27
|
+
const vertex = dataflow.getVertex(loop);
|
|
28
|
+
if (!vertex) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
(0, assert_1.guard)(vertex.tag === vertex_1.VertexType.FunctionCall, 'invalid vertex type for onlyLoopsOnce');
|
|
32
|
+
(0, assert_1.guard)(vertex.origin !== 'unnamed' && exports.loopyFunctions.has(vertex.origin[0]), 'onlyLoopsOnce can only be called with loops');
|
|
33
|
+
// 1. In case of for loop, check if vector has only one element
|
|
34
|
+
if (vertex.origin[0] === 'builtin:for-loop') {
|
|
35
|
+
if (vertex.args.length < 2) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const vectorOfLoop = vertex.args[1];
|
|
39
|
+
if (vectorOfLoop === r_function_call_1.EmptyArgument) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const values = (0, general_1.valueSetGuard)((0, alias_tracking_1.resolveIdToValue)(vectorOfLoop.nodeId, { graph: dataflow, idMap: dataflow.idMap, resolve: config.solver.variables }));
|
|
43
|
+
if (values === undefined || values.elements.length !== 1 || values.elements[0].type !== 'vector' || !(0, r_value_1.isValue)(values.elements[0].elements)) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (values.elements[0].elements.length === 1) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// 2. Use CFG Visitor to determine if loop always exits after the first iteration
|
|
51
|
+
const visitor = new CfgSingleIterationLoopDetector(loop, {
|
|
52
|
+
controlFlow: controlflow,
|
|
53
|
+
normalizedAst: ast,
|
|
54
|
+
dfg: dataflow,
|
|
55
|
+
flowrConfig: config,
|
|
56
|
+
defaultVisitingOrder: 'forward'
|
|
57
|
+
});
|
|
58
|
+
return visitor.loopsOnlyOnce();
|
|
59
|
+
}
|
|
60
|
+
class CfgSingleIterationLoopDetector extends semantic_cfg_guided_visitor_1.SemanticCfgGuidedVisitor {
|
|
61
|
+
onlyLoopyOnce = false;
|
|
62
|
+
loopToCheck;
|
|
63
|
+
constructor(loop, config) {
|
|
64
|
+
super(config);
|
|
65
|
+
this.loopToCheck = loop;
|
|
66
|
+
}
|
|
67
|
+
getBoolArgValue(data) {
|
|
68
|
+
if (data.call.args.length !== 1 || data.call.args[0] === r_function_call_1.EmptyArgument) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const values = (0, general_1.valueSetGuard)((0, alias_tracking_1.resolveIdToValue)(data.call.args[0].nodeId, { graph: this.config.dfg, full: true, idMap: this.config.normalizedAst.idMap, resolve: this.config.flowrConfig.solver.variables }));
|
|
72
|
+
if (values === undefined || values.elements.length !== 1 || values.elements[0].type != 'logical' || !(0, r_value_1.isValue)(values.elements[0].value)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return Boolean(values.elements[0].value);
|
|
76
|
+
}
|
|
77
|
+
startVisitor(_) {
|
|
78
|
+
const g = this.config.controlFlow.graph;
|
|
79
|
+
const n = (i) => g.ingoingEdges(i);
|
|
80
|
+
const exits = new Set(g.getVertex(this.loopToCheck)?.end ?? []);
|
|
81
|
+
(0, assert_1.guard)(exits.size !== 0, "Can't find end of loop");
|
|
82
|
+
const stack = [this.loopToCheck];
|
|
83
|
+
while (stack.length > 0) {
|
|
84
|
+
const current = stack.shift();
|
|
85
|
+
if (!this.visitNode(current)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!exits.has(current)) {
|
|
89
|
+
const next = n(current) ?? [];
|
|
90
|
+
for (const [to] of next) {
|
|
91
|
+
stack.unshift(to);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
onDefaultFunctionCall(data) {
|
|
97
|
+
let stopsLoop = false;
|
|
98
|
+
const alwaysHappens = () => {
|
|
99
|
+
if (!data.call.cds ||
|
|
100
|
+
(data.call.cds.length === 1 && data.call.cds[0].id === this.loopToCheck)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
const cds = data.call.cds.filter(d => d.id !== this.loopToCheck);
|
|
104
|
+
return (0, info_1.happensInEveryBranch)(cds);
|
|
105
|
+
};
|
|
106
|
+
switch (data.call.origin[0]) {
|
|
107
|
+
case 'builtin:return':
|
|
108
|
+
case 'builtin:stop':
|
|
109
|
+
case 'builtin:break':
|
|
110
|
+
stopsLoop = alwaysHappens();
|
|
111
|
+
break;
|
|
112
|
+
case 'builtin:stopifnot': {
|
|
113
|
+
const arg = this.getBoolArgValue(data);
|
|
114
|
+
if (arg !== undefined) {
|
|
115
|
+
stopsLoop = !arg && alwaysHappens();
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.onlyLoopyOnce = this.onlyLoopyOnce || stopsLoop;
|
|
121
|
+
}
|
|
122
|
+
loopsOnlyOnce() {
|
|
123
|
+
this.startVisitor([]);
|
|
124
|
+
return this.onlyLoopyOnce;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=useless-loop.js.map
|
|
@@ -65,7 +65,6 @@ export interface DefaultBuiltInProcessorConfiguration extends ForceArguments {
|
|
|
65
65
|
}
|
|
66
66
|
export type BuiltInEvalHandler = (resolve: VariableResolve, a: RNodeWithParent, env?: REnvironmentInformation, graph?: DataflowGraph, map?: AstIdMap) => Value;
|
|
67
67
|
declare function defaultBuiltInProcessor<OtherInfo>(name: RSymbol<OtherInfo & ParentInformation>, args: readonly RFunctionArgument<OtherInfo & ParentInformation>[], rootId: NodeId, data: DataflowProcessorInformation<OtherInfo & ParentInformation>, { returnsNthArgument, useAsProcessor, forceArgs, readAllArguments, cfg, hasUnknownSideEffects, treatAsFnCall }: DefaultBuiltInProcessorConfiguration): DataflowInformation;
|
|
68
|
-
export declare function registerBuiltInFunctions<Config extends object, Proc extends BuiltInIdentifierProcessorWithConfig<Config>>(both: boolean, names: readonly Identifier[], processor: Proc, config: Config, builtIns: BuiltIns): void;
|
|
69
68
|
export declare const BuiltInProcessorMapper: {
|
|
70
69
|
readonly 'builtin:default': typeof defaultBuiltInProcessor;
|
|
71
70
|
readonly 'builtin:eval': typeof processEvalCall;
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BuiltIns = exports.BuiltInEvalHandlerMapper = exports.BuiltInProcessorMapper = void 0;
|
|
4
4
|
exports.builtInId = builtInId;
|
|
5
5
|
exports.isBuiltIn = isBuiltIn;
|
|
6
|
-
exports.registerBuiltInFunctions = registerBuiltInFunctions;
|
|
7
6
|
const known_call_handling_1 = require("../internal/process/functions/call/known-call-handling");
|
|
8
7
|
const built_in_access_1 = require("../internal/process/functions/call/built-in/built-in-access");
|
|
9
8
|
const built_in_if_then_else_1 = require("../internal/process/functions/call/built-in/built-in-if-then-else");
|
|
@@ -99,22 +98,6 @@ function defaultBuiltInProcessor(name, args, rootId, data, { returnsNthArgument,
|
|
|
99
98
|
}
|
|
100
99
|
return res;
|
|
101
100
|
}
|
|
102
|
-
function registerBuiltInFunctions(both, names, processor, config, builtIns) {
|
|
103
|
-
for (const name of names) {
|
|
104
|
-
(0, assert_1.guard)(processor !== undefined, `Processor for ${name} is undefined, maybe you have an import loop? You may run 'npm run detect-circular-deps' - although by far not all are bad`);
|
|
105
|
-
const id = builtInId(name);
|
|
106
|
-
const d = [{
|
|
107
|
-
type: identifier_1.ReferenceType.BuiltInFunction,
|
|
108
|
-
definedAt: id,
|
|
109
|
-
controlDependencies: undefined,
|
|
110
|
-
processor: (name, args, rootId, data) => processor(name, args, rootId, data, config),
|
|
111
|
-
config,
|
|
112
|
-
name,
|
|
113
|
-
nodeId: id
|
|
114
|
-
}];
|
|
115
|
-
builtIns.set(name, d, both);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
101
|
exports.BuiltInProcessorMapper = {
|
|
119
102
|
'builtin:default': defaultBuiltInProcessor,
|
|
120
103
|
'builtin:eval': built_in_eval_1.processEvalCall,
|
|
@@ -218,7 +218,7 @@ exports.DefaultBuiltinConfig = [
|
|
|
218
218
|
{ type: 'function', names: ['return'], processor: 'builtin:default', config: { returnsNthArgument: 0, cfg: 1 /* ExitPointType.Return */, useAsProcessor: 'builtin:return' }, assumePrimitive: false },
|
|
219
219
|
{ type: 'function', names: ['stop'], processor: 'builtin:default', config: { useAsProcessor: 'builtin:stop' }, assumePrimitive: false },
|
|
220
220
|
{ type: 'function', names: ['stopifnot'], processor: 'builtin:default', config: { useAsProcessor: 'builtin:stopifnot' }, assumePrimitive: false },
|
|
221
|
-
{ type: 'function', names: ['break'], processor: 'builtin:default', config: { cfg: 2 /* ExitPointType.Break */ }, assumePrimitive: false },
|
|
221
|
+
{ type: 'function', names: ['break'], processor: 'builtin:default', config: { useAsProcessor: 'builtin:break', cfg: 2 /* ExitPointType.Break */ }, assumePrimitive: false },
|
|
222
222
|
{ type: 'function', names: ['next'], processor: 'builtin:default', config: { cfg: 3 /* ExitPointType.Next */ }, assumePrimitive: false },
|
|
223
223
|
{ type: 'function', names: ['{'], processor: 'builtin:expression-list', config: {}, assumePrimitive: true },
|
|
224
224
|
{ type: 'function', names: ['source'], processor: 'builtin:source', config: { includeFunctionCall: true, forceFollow: false }, assumePrimitive: false },
|
|
@@ -23,7 +23,7 @@ function overwriteIEnvironmentWith(base, next, includeParent = true, applyCds) {
|
|
|
23
23
|
if (values.length > 1_000_000) {
|
|
24
24
|
log_1.log.warn(`Overwriting environment with ${values.length} definitions for ${key}`);
|
|
25
25
|
}
|
|
26
|
-
const hasMaybe = applyCds
|
|
26
|
+
const hasMaybe = applyCds !== undefined ? true : anyIsMaybeOrEmpty(values);
|
|
27
27
|
if (hasMaybe) {
|
|
28
28
|
const old = map.get(key);
|
|
29
29
|
// we need to make a copy to avoid side effects for old reference in other environments
|
|
@@ -34,7 +34,7 @@ function overwriteIEnvironmentWith(base, next, includeParent = true, applyCds) {
|
|
|
34
34
|
if (applyCds !== undefined) {
|
|
35
35
|
updatedOld.push({
|
|
36
36
|
...v,
|
|
37
|
-
controlDependencies:
|
|
37
|
+
controlDependencies: applyCds.concat(v.controlDependencies ?? [])
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
@@ -160,7 +160,7 @@ export declare class DataflowGraph<Vertex extends DataflowGraphVertexInfo = Data
|
|
|
160
160
|
*
|
|
161
161
|
* @see #edges
|
|
162
162
|
*/
|
|
163
|
-
vertices(includeDefinedFunctions: boolean):
|
|
163
|
+
vertices(includeDefinedFunctions: boolean): IterableIterator<[NodeId, Vertex]>;
|
|
164
164
|
/**
|
|
165
165
|
* @returns the ids of all edges in the graph together with their edge information
|
|
166
166
|
*
|
|
@@ -25,7 +25,7 @@ const built_in_1 = require("../environments/built-in");
|
|
|
25
25
|
const prefix_1 = require("../../util/prefix");
|
|
26
26
|
function findNonLocalReads(graph, ignore) {
|
|
27
27
|
const ignores = new Set(ignore.map(i => i.nodeId));
|
|
28
|
-
const ids = new Set(graph.vertices(true)
|
|
28
|
+
const ids = new Set([...graph.vertices(true)]
|
|
29
29
|
.filter(([_, info]) => info.tag === vertex_1.VertexType.Use || info.tag === vertex_1.VertexType.FunctionCall)
|
|
30
30
|
.map(([id, _]) => id));
|
|
31
31
|
/* find all variable use ids which do not link to a given id */
|
|
@@ -169,7 +169,7 @@ function linkFunctionCall(graph, id, info, idMap, thisGraph, calledFunctionDefin
|
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
171
|
const readBits = edge_1.EdgeType.Reads | edge_1.EdgeType.Calls;
|
|
172
|
-
const functionDefinitionReadIds = edges.
|
|
172
|
+
const functionDefinitionReadIds = [...edges].filter(([_, e]) => (0, edge_1.edgeDoesNotIncludeType)(e.types, edge_1.EdgeType.Argument)
|
|
173
173
|
&& (0, edge_1.edgeIncludesType)(e.types, readBits)).map(([target, _]) => target);
|
|
174
174
|
const functionDefs = getAllLinkedFunctionDefinitions(new Set(functionDefinitionReadIds), graph)[0];
|
|
175
175
|
for (const def of functionDefs.values()) {
|
|
@@ -189,11 +189,11 @@ function linkFunctionCall(graph, id, info, idMap, thisGraph, calledFunctionDefin
|
|
|
189
189
|
* @param thisGraph - The graph to search for function calls in
|
|
190
190
|
*/
|
|
191
191
|
function linkFunctionCalls(graph, idMap, thisGraph) {
|
|
192
|
-
const functionCalls = thisGraph.vertices(true)
|
|
193
|
-
.filter(([_, info]) => info.tag === vertex_1.VertexType.FunctionCall);
|
|
194
192
|
const calledFunctionDefinitions = [];
|
|
195
|
-
for (const [id, info] of
|
|
196
|
-
|
|
193
|
+
for (const [id, info] of thisGraph.vertices(true)) {
|
|
194
|
+
if (info.tag === vertex_1.VertexType.FunctionCall) {
|
|
195
|
+
linkFunctionCall(graph, id, info, idMap, thisGraph, calledFunctionDefinitions);
|
|
196
|
+
}
|
|
197
197
|
}
|
|
198
198
|
return calledFunctionDefinitions;
|
|
199
199
|
}
|
|
@@ -150,7 +150,7 @@ function processExpressionList(name, args, rootId, data) {
|
|
|
150
150
|
controlDependencies: data.controlDependencies
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
|
-
const ingoing = [...remainingRead.values().
|
|
153
|
+
const ingoing = [...remainingRead.values()].flat();
|
|
154
154
|
const rootNode = data.completeAst.idMap.get(rootId);
|
|
155
155
|
const withGroup = rootNode?.grouping;
|
|
156
156
|
if (withGroup) {
|
|
@@ -114,6 +114,7 @@ df <- data.frame(id = 1:5, name = 6:10)
|
|
|
114
114
|
df[6, "value"]
|
|
115
115
|
`, tagTypes);
|
|
116
116
|
rule(shell, 'dead-code', 'DeadCodeConfig', 'DEAD_CODE', 'lint-dead-code', 'if(TRUE) 1 else 2', tagTypes);
|
|
117
|
+
rule(shell, 'useless-loop', 'UselessLoopConfig', 'USELESS_LOOP', 'lint-useless-loop', 'for(i in c(1)) { print(i) }', tagTypes);
|
|
117
118
|
function rule(shell, name, configType, ruleType, testfile, example, types) {
|
|
118
119
|
const rule = linter_rules_1.LintingRules[name];
|
|
119
120
|
const tags = rule.info.tags.toSorted((a, b) => {
|
package/linter/linter-rules.d.ts
CHANGED
|
@@ -225,6 +225,36 @@ export declare const LintingRules: {
|
|
|
225
225
|
readonly defaultConfig: {};
|
|
226
226
|
};
|
|
227
227
|
};
|
|
228
|
+
readonly 'useless-loop': {
|
|
229
|
+
readonly createSearch: () => import("../search/flowr-search-builder").FlowrSearchBuilder<"all", ["filter"], import("../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation, import("../search/flowr-search").FlowrSearchElements<import("../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation, [] | import("../search/flowr-search").FlowrSearchElement<import("../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation>[]>>;
|
|
230
|
+
readonly processSearchResult: (elements: import("../search/flowr-search").FlowrSearchElements<import("../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation, import("../search/flowr-search").FlowrSearchElement<import("../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation>[]>, config: import("./rules/useless-loop").UselessLoopConfig, data: {
|
|
231
|
+
normalize: import("../r-bridge/lang-4.x/ast/model/processing/decorate").NormalizedAst;
|
|
232
|
+
dataflow: import("../dataflow/info").DataflowInformation;
|
|
233
|
+
config: import("../config").FlowrConfigOptions;
|
|
234
|
+
}) => {
|
|
235
|
+
results: {
|
|
236
|
+
certainty: import("./linter-format").LintingResultCertainty.Certain;
|
|
237
|
+
name: string;
|
|
238
|
+
range: import("../util/range").SourceRange;
|
|
239
|
+
}[];
|
|
240
|
+
'.meta': {
|
|
241
|
+
numOfUselessLoops: number;
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
readonly prettyPrint: {
|
|
245
|
+
readonly query: (result: import("./rules/useless-loop").UselessLoopResult) => string;
|
|
246
|
+
readonly full: (result: import("./rules/useless-loop").UselessLoopResult) => string;
|
|
247
|
+
};
|
|
248
|
+
readonly info: {
|
|
249
|
+
readonly name: "Useless Loops";
|
|
250
|
+
readonly description: "Detect loops which only iterate once";
|
|
251
|
+
readonly certainty: import("./linter-format").LintingRuleCertainty.BestEffort;
|
|
252
|
+
readonly tags: readonly [import("./linter-tags").LintingRuleTag.Smell, import("./linter-tags").LintingRuleTag.Readability];
|
|
253
|
+
readonly defaultConfig: {
|
|
254
|
+
readonly loopyFunctions: Set<"builtin:default" | "builtin:eval" | "builtin:apply" | "builtin:expression-list" | "builtin:source" | "builtin:access" | "builtin:if-then-else" | "builtin:get" | "builtin:rm" | "builtin:library" | "builtin:assignment" | "builtin:special-bin-op" | "builtin:pipe" | "builtin:function-definition" | "builtin:quote" | "builtin:for-loop" | "builtin:repeat-loop" | "builtin:while-loop" | "builtin:replacement" | "builtin:list" | "builtin:vector">;
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
};
|
|
228
258
|
};
|
|
229
259
|
export type LintingRuleNames = keyof typeof LintingRules;
|
|
230
260
|
export type LintingRuleMetadata<Name extends LintingRuleNames> = typeof LintingRules[Name] extends LintingRule<infer _Result, infer Metadata, infer _Config, infer _Info, infer _Elements> ? Metadata : never;
|
package/linter/linter-rules.js
CHANGED
|
@@ -9,6 +9,7 @@ const dead_code_1 = require("./rules/dead-code");
|
|
|
9
9
|
const seeded_randomness_1 = require("./rules/seeded-randomness");
|
|
10
10
|
const naming_convention_1 = require("./rules/naming-convention");
|
|
11
11
|
const dataframe_access_validation_1 = require("./rules/dataframe-access-validation");
|
|
12
|
+
const useless_loop_1 = require("./rules/useless-loop");
|
|
12
13
|
/**
|
|
13
14
|
* The registry of currently supported linting rules.
|
|
14
15
|
* A linting rule can be executed on a dataflow pipeline result using {@link executeLintingRule}.
|
|
@@ -22,5 +23,6 @@ exports.LintingRules = {
|
|
|
22
23
|
'naming-convention': naming_convention_1.NAMING_CONVENTION,
|
|
23
24
|
'dataframe-access-validation': dataframe_access_validation_1.DATA_FRAME_ACCESS_VALIDATION,
|
|
24
25
|
'dead-code': dead_code_1.DEAD_CODE,
|
|
26
|
+
'useless-loop': useless_loop_1.USELESS_LOOP
|
|
25
27
|
};
|
|
26
28
|
//# sourceMappingURL=linter-rules.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BuiltInMappingName } from '../../dataflow/environments/built-in';
|
|
2
|
+
import type { MergeableRecord } from '../../util/objects';
|
|
3
|
+
import type { SourceRange } from '../../util/range';
|
|
4
|
+
import type { LintingResult } from '../linter-format';
|
|
5
|
+
import { LintingResultCertainty, LintingRuleCertainty } from '../linter-format';
|
|
6
|
+
import { LintingRuleTag } from '../linter-tags';
|
|
7
|
+
export interface UselessLoopResult extends LintingResult {
|
|
8
|
+
name: string;
|
|
9
|
+
range: SourceRange;
|
|
10
|
+
}
|
|
11
|
+
export interface UselessLoopConfig extends MergeableRecord {
|
|
12
|
+
/** Function origins that are considered loops */
|
|
13
|
+
loopyFunctions: Set<BuiltInMappingName>;
|
|
14
|
+
}
|
|
15
|
+
export interface UselessLoopMetadata extends MergeableRecord {
|
|
16
|
+
numOfUselessLoops: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const USELESS_LOOP: {
|
|
19
|
+
readonly createSearch: () => import("../../search/flowr-search-builder").FlowrSearchBuilder<"all", ["filter"], import("../../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation, import("../../search/flowr-search").FlowrSearchElements<import("../../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation, [] | import("../../search/flowr-search").FlowrSearchElement<import("../../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation>[]>>;
|
|
20
|
+
readonly processSearchResult: (elements: import("../../search/flowr-search").FlowrSearchElements<import("../../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation, import("../../search/flowr-search").FlowrSearchElement<import("../../r-bridge/lang-4.x/ast/model/processing/decorate").ParentInformation>[]>, config: UselessLoopConfig, data: {
|
|
21
|
+
normalize: import("../../r-bridge/lang-4.x/ast/model/processing/decorate").NormalizedAst;
|
|
22
|
+
dataflow: import("../../dataflow/info").DataflowInformation;
|
|
23
|
+
config: import("../../config").FlowrConfigOptions;
|
|
24
|
+
}) => {
|
|
25
|
+
results: {
|
|
26
|
+
certainty: LintingResultCertainty.Certain;
|
|
27
|
+
name: string;
|
|
28
|
+
range: SourceRange;
|
|
29
|
+
}[];
|
|
30
|
+
'.meta': {
|
|
31
|
+
numOfUselessLoops: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
readonly prettyPrint: {
|
|
35
|
+
readonly query: (result: UselessLoopResult) => string;
|
|
36
|
+
readonly full: (result: UselessLoopResult) => string;
|
|
37
|
+
};
|
|
38
|
+
readonly info: {
|
|
39
|
+
readonly name: "Useless Loops";
|
|
40
|
+
readonly description: "Detect loops which only iterate once";
|
|
41
|
+
readonly certainty: LintingRuleCertainty.BestEffort;
|
|
42
|
+
readonly tags: readonly [LintingRuleTag.Smell, LintingRuleTag.Readability];
|
|
43
|
+
readonly defaultConfig: {
|
|
44
|
+
readonly loopyFunctions: Set<"builtin:default" | "builtin:eval" | "builtin:apply" | "builtin:expression-list" | "builtin:source" | "builtin:access" | "builtin:if-then-else" | "builtin:get" | "builtin:rm" | "builtin:library" | "builtin:assignment" | "builtin:special-bin-op" | "builtin:pipe" | "builtin:function-definition" | "builtin:quote" | "builtin:for-loop" | "builtin:repeat-loop" | "builtin:while-loop" | "builtin:replacement" | "builtin:list" | "builtin:vector">;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.USELESS_LOOP = void 0;
|
|
4
|
+
const extract_cfg_1 = require("../../control-flow/extract-cfg");
|
|
5
|
+
const useless_loop_1 = require("../../control-flow/useless-loop");
|
|
6
|
+
const vertex_1 = require("../../dataflow/graph/vertex");
|
|
7
|
+
const flowr_search_builder_1 = require("../../search/flowr-search-builder");
|
|
8
|
+
const dfg_1 = require("../../util/mermaid/dfg");
|
|
9
|
+
const linter_format_1 = require("../linter-format");
|
|
10
|
+
const linter_tags_1 = require("../linter-tags");
|
|
11
|
+
exports.USELESS_LOOP = {
|
|
12
|
+
createSearch: () => flowr_search_builder_1.Q.all().filter(vertex_1.VertexType.FunctionCall),
|
|
13
|
+
processSearchResult: (elements, config, data) => {
|
|
14
|
+
const cfg = (0, extract_cfg_1.extractCfg)(data.normalize, data.config, data.dataflow.graph);
|
|
15
|
+
const results = elements.getElements().filter(e => {
|
|
16
|
+
const vertex = data.dataflow.graph.getVertex(e.node.info.id);
|
|
17
|
+
return vertex
|
|
18
|
+
&& (0, vertex_1.isFunctionCallVertex)(vertex)
|
|
19
|
+
&& vertex.origin !== 'unnamed'
|
|
20
|
+
&& config.loopyFunctions.has(vertex.origin[0]);
|
|
21
|
+
}).filter(loop => (0, useless_loop_1.onlyLoopsOnce)(loop.node.info.id, data.dataflow.graph, cfg, data.normalize, data.config)).map(res => ({
|
|
22
|
+
certainty: linter_format_1.LintingResultCertainty.Certain,
|
|
23
|
+
name: res.node.lexeme,
|
|
24
|
+
range: res.node.info.fullRange
|
|
25
|
+
}));
|
|
26
|
+
return {
|
|
27
|
+
results: results,
|
|
28
|
+
'.meta': {
|
|
29
|
+
numOfUselessLoops: results.length
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
prettyPrint: {
|
|
34
|
+
[linter_format_1.LintingPrettyPrintContext.Query]: result => `${result.name}-loop at ${(0, dfg_1.formatRange)(result.range)} only loops once`,
|
|
35
|
+
[linter_format_1.LintingPrettyPrintContext.Full]: result => `${result.name}-loop at ${(0, dfg_1.formatRange)(result.range)} only loops once`
|
|
36
|
+
},
|
|
37
|
+
info: {
|
|
38
|
+
name: 'Useless Loops',
|
|
39
|
+
description: 'Detect loops which only iterate once',
|
|
40
|
+
certainty: linter_format_1.LintingRuleCertainty.BestEffort,
|
|
41
|
+
tags: [linter_tags_1.LintingRuleTag.Smell, linter_tags_1.LintingRuleTag.Readability],
|
|
42
|
+
defaultConfig: {
|
|
43
|
+
loopyFunctions: useless_loop_1.loopyFunctions
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=useless-loop.js.map
|
package/package.json
CHANGED
|
@@ -108,7 +108,7 @@ function getValueOfArgument(graph, call, argument, additionalAllowedTypes) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
function identifyLinkToLastCallRelation(from, cfg, graph, { callName, ignoreIf, cascadeIf }) {
|
|
111
|
-
if (ignoreIf
|
|
111
|
+
if (ignoreIf?.(from, graph)) {
|
|
112
112
|
return [];
|
|
113
113
|
}
|
|
114
114
|
const found = [];
|
package/util/version.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.flowrVersion = flowrVersion;
|
|
4
4
|
const semver_1 = require("semver");
|
|
5
5
|
// this is automatically replaced with the current version by release-it
|
|
6
|
-
const version = '2.4.
|
|
6
|
+
const version = '2.4.3';
|
|
7
7
|
function flowrVersion() {
|
|
8
8
|
return new semver_1.SemVer(version);
|
|
9
9
|
}
|