@seljs/checker 1.1.0 → 1.2.0-beta.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/dist/rules/defaults/expression-complexity.cjs +89 -0
- package/dist/rules/defaults/expression-complexity.d.cts +10 -0
- package/dist/rules/defaults/expression-complexity.d.mts +10 -0
- package/dist/rules/defaults/expression-complexity.mjs +88 -0
- package/dist/rules/defaults/index.cjs +1 -0
- package/dist/rules/defaults/index.mjs +1 -0
- package/dist/rules/facade.cjs +3 -1
- package/dist/rules/facade.d.cts +3 -1
- package/dist/rules/facade.d.mts +3 -1
- package/dist/rules/facade.mjs +3 -1
- package/dist/utils/ast-utils.cjs +1 -0
- package/dist/utils/ast-utils.mjs +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require("../../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_ast_utils = require("../../utils/ast-utils.cjs");
|
|
3
|
+
let _seljs_common = require("@seljs/common");
|
|
4
|
+
//#region src/rules/defaults/expression-complexity.ts
|
|
5
|
+
const DEFAULT_THRESHOLDS = {
|
|
6
|
+
maxAstNodes: 50,
|
|
7
|
+
maxDepth: 8,
|
|
8
|
+
maxCalls: 10,
|
|
9
|
+
maxOperators: 15,
|
|
10
|
+
maxBranches: 6
|
|
11
|
+
};
|
|
12
|
+
/** Binary ops that count toward the operators metric (excludes && and ||). */
|
|
13
|
+
const COUNTING_BINARY_OPS = new Set([
|
|
14
|
+
"==",
|
|
15
|
+
"!=",
|
|
16
|
+
"<",
|
|
17
|
+
"<=",
|
|
18
|
+
">",
|
|
19
|
+
">=",
|
|
20
|
+
"+",
|
|
21
|
+
"-",
|
|
22
|
+
"*",
|
|
23
|
+
"/",
|
|
24
|
+
"%",
|
|
25
|
+
"in",
|
|
26
|
+
"[]",
|
|
27
|
+
"[?]"
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Recursively count all complexity metrics in a single AST pass.
|
|
31
|
+
*/
|
|
32
|
+
const countMetrics = ({ node, depth, contractNames }, metrics) => {
|
|
33
|
+
metrics.nodes++;
|
|
34
|
+
if (depth > metrics.maxDepth) metrics.maxDepth = depth;
|
|
35
|
+
if (node.op === "rcall") {
|
|
36
|
+
const receiverNode = node.args[1];
|
|
37
|
+
if ((0, _seljs_common.isAstNode)(receiverNode) && receiverNode.op === "id" && typeof receiverNode.args === "string" && contractNames.has(receiverNode.args)) metrics.calls++;
|
|
38
|
+
}
|
|
39
|
+
if (node.op === "?:" || node.op === "&&" || node.op === "||") metrics.branches++;
|
|
40
|
+
if (COUNTING_BINARY_OPS.has(node.op) || node.op === "!_" || node.op === "-_") metrics.operators++;
|
|
41
|
+
for (const child of require_ast_utils.collectChildren(node)) countMetrics({
|
|
42
|
+
node: child,
|
|
43
|
+
depth: depth + 1,
|
|
44
|
+
contractNames
|
|
45
|
+
}, metrics);
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Factory that enforces expression complexity thresholds.
|
|
49
|
+
*
|
|
50
|
+
* Reports a diagnostic for each metric that exceeds its configured maximum.
|
|
51
|
+
* Setting a threshold to `Infinity` disables that metric.
|
|
52
|
+
*/
|
|
53
|
+
const expressionComplexity = (thresholds) => {
|
|
54
|
+
const resolved = {
|
|
55
|
+
...DEFAULT_THRESHOLDS,
|
|
56
|
+
...thresholds
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
name: "expression-complexity",
|
|
60
|
+
description: "Reject expressions exceeding AST complexity thresholds.",
|
|
61
|
+
defaultSeverity: "error",
|
|
62
|
+
tier: "structural",
|
|
63
|
+
run(context) {
|
|
64
|
+
const contractNames = new Set(context.schema.contracts.map((c) => c.name));
|
|
65
|
+
const metrics = {
|
|
66
|
+
nodes: 0,
|
|
67
|
+
maxDepth: 0,
|
|
68
|
+
calls: 0,
|
|
69
|
+
operators: 0,
|
|
70
|
+
branches: 0
|
|
71
|
+
};
|
|
72
|
+
countMetrics({
|
|
73
|
+
node: context.ast,
|
|
74
|
+
depth: 0,
|
|
75
|
+
contractNames
|
|
76
|
+
}, metrics);
|
|
77
|
+
const diagnostics = [];
|
|
78
|
+
const span = context.expression.length;
|
|
79
|
+
if (metrics.nodes > resolved.maxAstNodes) diagnostics.push(context.reportAt(0, span, `Expression complexity: AST node count ${String(metrics.nodes)} exceeds maximum of ${String(resolved.maxAstNodes)}.`));
|
|
80
|
+
if (metrics.maxDepth > resolved.maxDepth) diagnostics.push(context.reportAt(0, span, `Expression complexity: nesting depth ${String(metrics.maxDepth)} exceeds maximum of ${String(resolved.maxDepth)}.`));
|
|
81
|
+
if (metrics.calls > resolved.maxCalls) diagnostics.push(context.reportAt(0, span, `Expression complexity: contract calls ${String(metrics.calls)} exceeds maximum of ${String(resolved.maxCalls)}.`));
|
|
82
|
+
if (metrics.operators > resolved.maxOperators) diagnostics.push(context.reportAt(0, span, `Expression complexity: operators ${String(metrics.operators)} exceeds maximum of ${String(resolved.maxOperators)}.`));
|
|
83
|
+
if (metrics.branches > resolved.maxBranches) diagnostics.push(context.reportAt(0, span, `Expression complexity: branches ${String(metrics.branches)} exceeds maximum of ${String(resolved.maxBranches)}.`));
|
|
84
|
+
return diagnostics;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
exports.expressionComplexity = expressionComplexity;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { collectChildren } from "../../utils/ast-utils.mjs";
|
|
2
|
+
import { isAstNode } from "@seljs/common";
|
|
3
|
+
//#region src/rules/defaults/expression-complexity.ts
|
|
4
|
+
const DEFAULT_THRESHOLDS = {
|
|
5
|
+
maxAstNodes: 50,
|
|
6
|
+
maxDepth: 8,
|
|
7
|
+
maxCalls: 10,
|
|
8
|
+
maxOperators: 15,
|
|
9
|
+
maxBranches: 6
|
|
10
|
+
};
|
|
11
|
+
/** Binary ops that count toward the operators metric (excludes && and ||). */
|
|
12
|
+
const COUNTING_BINARY_OPS = new Set([
|
|
13
|
+
"==",
|
|
14
|
+
"!=",
|
|
15
|
+
"<",
|
|
16
|
+
"<=",
|
|
17
|
+
">",
|
|
18
|
+
">=",
|
|
19
|
+
"+",
|
|
20
|
+
"-",
|
|
21
|
+
"*",
|
|
22
|
+
"/",
|
|
23
|
+
"%",
|
|
24
|
+
"in",
|
|
25
|
+
"[]",
|
|
26
|
+
"[?]"
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Recursively count all complexity metrics in a single AST pass.
|
|
30
|
+
*/
|
|
31
|
+
const countMetrics = ({ node, depth, contractNames }, metrics) => {
|
|
32
|
+
metrics.nodes++;
|
|
33
|
+
if (depth > metrics.maxDepth) metrics.maxDepth = depth;
|
|
34
|
+
if (node.op === "rcall") {
|
|
35
|
+
const receiverNode = node.args[1];
|
|
36
|
+
if (isAstNode(receiverNode) && receiverNode.op === "id" && typeof receiverNode.args === "string" && contractNames.has(receiverNode.args)) metrics.calls++;
|
|
37
|
+
}
|
|
38
|
+
if (node.op === "?:" || node.op === "&&" || node.op === "||") metrics.branches++;
|
|
39
|
+
if (COUNTING_BINARY_OPS.has(node.op) || node.op === "!_" || node.op === "-_") metrics.operators++;
|
|
40
|
+
for (const child of collectChildren(node)) countMetrics({
|
|
41
|
+
node: child,
|
|
42
|
+
depth: depth + 1,
|
|
43
|
+
contractNames
|
|
44
|
+
}, metrics);
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Factory that enforces expression complexity thresholds.
|
|
48
|
+
*
|
|
49
|
+
* Reports a diagnostic for each metric that exceeds its configured maximum.
|
|
50
|
+
* Setting a threshold to `Infinity` disables that metric.
|
|
51
|
+
*/
|
|
52
|
+
const expressionComplexity = (thresholds) => {
|
|
53
|
+
const resolved = {
|
|
54
|
+
...DEFAULT_THRESHOLDS,
|
|
55
|
+
...thresholds
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
name: "expression-complexity",
|
|
59
|
+
description: "Reject expressions exceeding AST complexity thresholds.",
|
|
60
|
+
defaultSeverity: "error",
|
|
61
|
+
tier: "structural",
|
|
62
|
+
run(context) {
|
|
63
|
+
const contractNames = new Set(context.schema.contracts.map((c) => c.name));
|
|
64
|
+
const metrics = {
|
|
65
|
+
nodes: 0,
|
|
66
|
+
maxDepth: 0,
|
|
67
|
+
calls: 0,
|
|
68
|
+
operators: 0,
|
|
69
|
+
branches: 0
|
|
70
|
+
};
|
|
71
|
+
countMetrics({
|
|
72
|
+
node: context.ast,
|
|
73
|
+
depth: 0,
|
|
74
|
+
contractNames
|
|
75
|
+
}, metrics);
|
|
76
|
+
const diagnostics = [];
|
|
77
|
+
const span = context.expression.length;
|
|
78
|
+
if (metrics.nodes > resolved.maxAstNodes) diagnostics.push(context.reportAt(0, span, `Expression complexity: AST node count ${String(metrics.nodes)} exceeds maximum of ${String(resolved.maxAstNodes)}.`));
|
|
79
|
+
if (metrics.maxDepth > resolved.maxDepth) diagnostics.push(context.reportAt(0, span, `Expression complexity: nesting depth ${String(metrics.maxDepth)} exceeds maximum of ${String(resolved.maxDepth)}.`));
|
|
80
|
+
if (metrics.calls > resolved.maxCalls) diagnostics.push(context.reportAt(0, span, `Expression complexity: contract calls ${String(metrics.calls)} exceeds maximum of ${String(resolved.maxCalls)}.`));
|
|
81
|
+
if (metrics.operators > resolved.maxOperators) diagnostics.push(context.reportAt(0, span, `Expression complexity: operators ${String(metrics.operators)} exceeds maximum of ${String(resolved.maxOperators)}.`));
|
|
82
|
+
if (metrics.branches > resolved.maxBranches) diagnostics.push(context.reportAt(0, span, `Expression complexity: branches ${String(metrics.branches)} exceeds maximum of ${String(resolved.maxBranches)}.`));
|
|
83
|
+
return diagnostics;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { expressionComplexity };
|
package/dist/rules/facade.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_deferred_call = require("./defaults/deferred-call.cjs");
|
|
2
|
+
const require_expression_complexity = require("./defaults/expression-complexity.cjs");
|
|
2
3
|
const require_no_constant_condition = require("./defaults/no-constant-condition.cjs");
|
|
3
4
|
const require_no_mixed_operators = require("./defaults/no-mixed-operators.cjs");
|
|
4
5
|
const require_no_redundant_bool = require("./defaults/no-redundant-bool.cjs");
|
|
@@ -25,7 +26,8 @@ const rules = {
|
|
|
25
26
|
noMixedOperators: require_no_mixed_operators.noMixedOperators,
|
|
26
27
|
noSelfComparison: require_no_self_comparison.noSelfComparison,
|
|
27
28
|
deferredCall: require_deferred_call.deferredCall,
|
|
28
|
-
requireType: require_require_type.requireType
|
|
29
|
+
requireType: require_require_type.requireType,
|
|
30
|
+
expressionComplexity: require_expression_complexity.expressionComplexity
|
|
29
31
|
};
|
|
30
32
|
//#endregion
|
|
31
33
|
exports.rules = rules;
|
package/dist/rules/facade.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SELRule } from "./types.cjs";
|
|
2
|
+
import { ComplexityThresholds } from "./defaults/expression-complexity.cjs";
|
|
2
3
|
|
|
3
4
|
//#region src/rules/facade.d.ts
|
|
4
5
|
/**
|
|
@@ -14,7 +15,8 @@ declare const rules: {
|
|
|
14
15
|
readonly noMixedOperators: SELRule; /** Flags self-comparisons (e.g. `x == x`). */
|
|
15
16
|
readonly noSelfComparison: SELRule; /** Flags contract calls with dynamic arguments that will execute as live RPC calls. */
|
|
16
17
|
readonly deferredCall: SELRule; /** Rule factory that enforces an expression evaluates to the expected CEL type. */
|
|
17
|
-
readonly requireType: (expected: string) => SELRule;
|
|
18
|
+
readonly requireType: (expected: string) => SELRule; /** Factory that enforces expression complexity thresholds. */
|
|
19
|
+
readonly expressionComplexity: (thresholds?: Partial<ComplexityThresholds>) => SELRule;
|
|
18
20
|
};
|
|
19
21
|
//#endregion
|
|
20
22
|
export { rules };
|
package/dist/rules/facade.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SELRule } from "./types.mjs";
|
|
2
|
+
import { ComplexityThresholds } from "./defaults/expression-complexity.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/rules/facade.d.ts
|
|
4
5
|
/**
|
|
@@ -14,7 +15,8 @@ declare const rules: {
|
|
|
14
15
|
readonly noMixedOperators: SELRule; /** Flags self-comparisons (e.g. `x == x`). */
|
|
15
16
|
readonly noSelfComparison: SELRule; /** Flags contract calls with dynamic arguments that will execute as live RPC calls. */
|
|
16
17
|
readonly deferredCall: SELRule; /** Rule factory that enforces an expression evaluates to the expected CEL type. */
|
|
17
|
-
readonly requireType: (expected: string) => SELRule;
|
|
18
|
+
readonly requireType: (expected: string) => SELRule; /** Factory that enforces expression complexity thresholds. */
|
|
19
|
+
readonly expressionComplexity: (thresholds?: Partial<ComplexityThresholds>) => SELRule;
|
|
18
20
|
};
|
|
19
21
|
//#endregion
|
|
20
22
|
export { rules };
|
package/dist/rules/facade.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { deferredCall } from "./defaults/deferred-call.mjs";
|
|
2
|
+
import { expressionComplexity } from "./defaults/expression-complexity.mjs";
|
|
2
3
|
import { noConstantCondition } from "./defaults/no-constant-condition.mjs";
|
|
3
4
|
import { noMixedOperators } from "./defaults/no-mixed-operators.mjs";
|
|
4
5
|
import { noRedundantBool } from "./defaults/no-redundant-bool.mjs";
|
|
@@ -25,7 +26,8 @@ const rules = {
|
|
|
25
26
|
noMixedOperators,
|
|
26
27
|
noSelfComparison,
|
|
27
28
|
deferredCall,
|
|
28
|
-
requireType
|
|
29
|
+
requireType,
|
|
30
|
+
expressionComplexity
|
|
29
31
|
};
|
|
30
32
|
//#endregion
|
|
31
33
|
export { rules };
|
package/dist/utils/ast-utils.cjs
CHANGED
|
@@ -159,6 +159,7 @@ const findNodeWithParentAt = (root, offset, parent = null) => {
|
|
|
159
159
|
};
|
|
160
160
|
};
|
|
161
161
|
//#endregion
|
|
162
|
+
exports.collectChildren = collectChildren;
|
|
162
163
|
exports.findNodeWithParentAt = findNodeWithParentAt;
|
|
163
164
|
exports.nodeSpan = nodeSpan;
|
|
164
165
|
exports.walkAST = walkAST;
|
package/dist/utils/ast-utils.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seljs/checker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-beta.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "https://github.com/abinnovision/seljs"
|
|
6
6
|
},
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@marcbachmann/cel-js": "^7.5.3",
|
|
53
|
-
"@seljs/common": "1.
|
|
53
|
+
"@seljs/common": "1.2.0-beta.3",
|
|
54
54
|
"@seljs/schema": "1.1.0",
|
|
55
|
-
"@seljs/types": "1.
|
|
55
|
+
"@seljs/types": "1.2.0-beta.3",
|
|
56
56
|
"debug": "^4.4.3"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|