@seljs/checker 1.1.0 → 1.2.0-beta.2

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,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,10 @@
1
+ //#region src/rules/defaults/expression-complexity.d.ts
2
+ interface ComplexityThresholds {
3
+ maxAstNodes: number;
4
+ maxDepth: number;
5
+ maxCalls: number;
6
+ maxOperators: number;
7
+ maxBranches: number;
8
+ }
9
+ //#endregion
10
+ export { ComplexityThresholds };
@@ -0,0 +1,10 @@
1
+ //#region src/rules/defaults/expression-complexity.d.ts
2
+ interface ComplexityThresholds {
3
+ maxAstNodes: number;
4
+ maxDepth: number;
5
+ maxCalls: number;
6
+ maxOperators: number;
7
+ maxBranches: number;
8
+ }
9
+ //#endregion
10
+ export { ComplexityThresholds };
@@ -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 };
@@ -1,4 +1,5 @@
1
1
  require("./deferred-call.cjs");
2
+ require("./expression-complexity.cjs");
2
3
  require("./no-constant-condition.cjs");
3
4
  require("./no-mixed-operators.cjs");
4
5
  require("./no-redundant-bool.cjs");
@@ -1,4 +1,5 @@
1
1
  import "./deferred-call.mjs";
2
+ import "./expression-complexity.mjs";
2
3
  import "./no-constant-condition.mjs";
3
4
  import "./no-mixed-operators.mjs";
4
5
  import "./no-redundant-bool.mjs";
@@ -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;
@@ -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 };
@@ -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 };
@@ -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 };
@@ -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;
@@ -159,4 +159,4 @@ const findNodeWithParentAt = (root, offset, parent = null) => {
159
159
  };
160
160
  };
161
161
  //#endregion
162
- export { findNodeWithParentAt, nodeSpan, walkAST };
162
+ export { collectChildren, findNodeWithParentAt, nodeSpan, walkAST };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seljs/checker",
3
- "version": "1.1.0",
3
+ "version": "1.2.0-beta.2",
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.1.0",
53
+ "@seljs/common": "1.2.0-beta.2",
54
54
  "@seljs/schema": "1.1.0",
55
- "@seljs/types": "1.1.0",
55
+ "@seljs/types": "1.2.0-beta.2",
56
56
  "debug": "^4.4.3"
57
57
  },
58
58
  "devDependencies": {