@seljs/checker 1.0.0 → 1.0.1

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.
Files changed (124) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/_virtual/_rolldown/runtime.cjs +23 -0
  3. package/dist/checker/checker.cjs +482 -0
  4. package/dist/checker/checker.d.cts +176 -0
  5. package/dist/checker/checker.d.mts +176 -0
  6. package/dist/checker/checker.mjs +481 -0
  7. package/dist/checker/diagnostics.cjs +66 -0
  8. package/dist/checker/diagnostics.mjs +66 -0
  9. package/dist/checker/index.cjs +2 -0
  10. package/dist/checker/index.d.mts +2 -0
  11. package/dist/checker/index.mjs +3 -0
  12. package/dist/checker/type-compatibility.cjs +70 -0
  13. package/dist/checker/type-compatibility.d.cts +8 -0
  14. package/dist/checker/type-compatibility.d.mts +8 -0
  15. package/dist/checker/type-compatibility.mjs +69 -0
  16. package/dist/constants.cjs +14 -0
  17. package/dist/constants.d.cts +7 -0
  18. package/dist/constants.d.mts +7 -0
  19. package/dist/constants.mjs +13 -0
  20. package/dist/debug.cjs +7 -0
  21. package/dist/debug.mjs +5 -0
  22. package/dist/environment/codec-registry.cjs +109 -0
  23. package/dist/environment/codec-registry.d.cts +45 -0
  24. package/dist/environment/codec-registry.d.mts +45 -0
  25. package/dist/environment/codec-registry.mjs +108 -0
  26. package/dist/environment/hydrate.cjs +163 -0
  27. package/dist/environment/hydrate.d.cts +52 -0
  28. package/dist/environment/hydrate.d.mts +52 -0
  29. package/dist/environment/hydrate.mjs +160 -0
  30. package/dist/environment/index.cjs +4 -0
  31. package/dist/environment/index.d.mts +4 -0
  32. package/dist/environment/index.mjs +5 -0
  33. package/dist/environment/register-types.cjs +107 -0
  34. package/dist/environment/register-types.d.cts +15 -0
  35. package/dist/environment/register-types.d.mts +15 -0
  36. package/dist/environment/register-types.mjs +106 -0
  37. package/dist/environment/value-wrappers.cjs +44 -0
  38. package/dist/environment/value-wrappers.d.cts +20 -0
  39. package/dist/environment/value-wrappers.d.mts +20 -0
  40. package/dist/environment/value-wrappers.mjs +41 -0
  41. package/dist/index.cjs +27 -0
  42. package/dist/index.d.cts +11 -0
  43. package/dist/index.d.mts +11 -0
  44. package/dist/index.mjs +13 -0
  45. package/dist/rules/defaults/deferred-call.cjs +107 -0
  46. package/dist/rules/defaults/deferred-call.mjs +106 -0
  47. package/dist/rules/defaults/index.cjs +6 -0
  48. package/dist/rules/defaults/index.mjs +7 -0
  49. package/dist/rules/defaults/no-constant-condition.cjs +31 -0
  50. package/dist/rules/defaults/no-constant-condition.mjs +31 -0
  51. package/dist/rules/defaults/no-mixed-operators.cjs +39 -0
  52. package/dist/rules/defaults/no-mixed-operators.mjs +39 -0
  53. package/dist/rules/defaults/no-redundant-bool.cjs +26 -0
  54. package/dist/rules/defaults/no-redundant-bool.mjs +26 -0
  55. package/dist/rules/defaults/no-self-comparison.cjs +44 -0
  56. package/dist/rules/defaults/no-self-comparison.mjs +43 -0
  57. package/dist/rules/defaults/require-type.cjs +18 -0
  58. package/dist/rules/defaults/require-type.mjs +18 -0
  59. package/dist/rules/facade.cjs +31 -0
  60. package/dist/rules/facade.d.cts +20 -0
  61. package/dist/rules/facade.d.mts +20 -0
  62. package/dist/rules/facade.mjs +31 -0
  63. package/dist/rules/index.cjs +2 -0
  64. package/dist/rules/index.d.mts +3 -0
  65. package/dist/rules/index.mjs +3 -0
  66. package/dist/rules/runner.cjs +40 -0
  67. package/dist/rules/runner.d.cts +27 -0
  68. package/dist/rules/runner.d.mts +27 -0
  69. package/dist/rules/runner.mjs +40 -0
  70. package/dist/rules/types.d.cts +77 -0
  71. package/dist/rules/types.d.mts +77 -0
  72. package/dist/utils/ast-utils.cjs +164 -0
  73. package/dist/utils/ast-utils.mjs +162 -0
  74. package/package.json +24 -17
  75. package/dist/checker/checker.d.ts +0 -173
  76. package/dist/checker/checker.js +0 -567
  77. package/dist/checker/diagnostics.d.ts +0 -10
  78. package/dist/checker/diagnostics.js +0 -80
  79. package/dist/checker/index.d.ts +0 -2
  80. package/dist/checker/index.js +0 -2
  81. package/dist/checker/type-compatibility.d.ts +0 -16
  82. package/dist/checker/type-compatibility.js +0 -59
  83. package/dist/constants.d.ts +0 -4
  84. package/dist/constants.js +0 -10
  85. package/dist/debug.d.ts +0 -2
  86. package/dist/debug.js +0 -2
  87. package/dist/environment/codec-registry.d.ts +0 -42
  88. package/dist/environment/codec-registry.js +0 -146
  89. package/dist/environment/hydrate.d.ts +0 -48
  90. package/dist/environment/hydrate.js +0 -198
  91. package/dist/environment/index.d.ts +0 -4
  92. package/dist/environment/index.js +0 -4
  93. package/dist/environment/register-types.d.ts +0 -14
  94. package/dist/environment/register-types.js +0 -154
  95. package/dist/environment/value-wrappers.d.ts +0 -17
  96. package/dist/environment/value-wrappers.js +0 -65
  97. package/dist/index.d.ts +0 -4
  98. package/dist/index.js +0 -4
  99. package/dist/rules/defaults/deferred-call.d.ts +0 -13
  100. package/dist/rules/defaults/deferred-call.js +0 -162
  101. package/dist/rules/defaults/index.d.ts +0 -6
  102. package/dist/rules/defaults/index.js +0 -6
  103. package/dist/rules/defaults/no-constant-condition.d.ts +0 -7
  104. package/dist/rules/defaults/no-constant-condition.js +0 -36
  105. package/dist/rules/defaults/no-mixed-operators.d.ts +0 -9
  106. package/dist/rules/defaults/no-mixed-operators.js +0 -44
  107. package/dist/rules/defaults/no-redundant-bool.d.ts +0 -5
  108. package/dist/rules/defaults/no-redundant-bool.js +0 -27
  109. package/dist/rules/defaults/no-self-comparison.d.ts +0 -9
  110. package/dist/rules/defaults/no-self-comparison.js +0 -31
  111. package/dist/rules/defaults/require-type.d.ts +0 -7
  112. package/dist/rules/defaults/require-type.js +0 -19
  113. package/dist/rules/facade.d.ts +0 -22
  114. package/dist/rules/facade.js +0 -29
  115. package/dist/rules/index.d.ts +0 -3
  116. package/dist/rules/index.js +0 -3
  117. package/dist/rules/runner.d.ts +0 -16
  118. package/dist/rules/runner.js +0 -30
  119. package/dist/rules/types.d.ts +0 -73
  120. package/dist/rules/types.js +0 -1
  121. package/dist/utils/ast-utils.d.ts +0 -55
  122. package/dist/utils/ast-utils.js +0 -255
  123. package/dist/utils/index.d.ts +0 -1
  124. package/dist/utils/index.js +0 -1
@@ -1,154 +0,0 @@
1
- import { SolidityIntTypeWrapper, SOLIDITY_TYPES, toBigInt, toAddress, parseUnitsValue, formatUnitsValue, } from "@seljs/types";
2
- /**
3
- * Try to register an operator, silently ignoring "already registered" errors.
4
- * cel-js auto-derives some cross-type operators (e.g., registering
5
- * `sol_int == int` also creates `int == sol_int`).
6
- */
7
- const tryRegisterOperator = (env, signature, handler) => {
8
- try {
9
- env.registerOperator(signature, handler);
10
- }
11
- catch (error) {
12
- if (!(error instanceof Error) ||
13
- !error.message.includes("already registered")) {
14
- throw error;
15
- }
16
- }
17
- };
18
- const registerAddressOperators = (env) => {
19
- env.registerOperator("sol_address == sol_address", (left, right) => toAddress(left) === toAddress(right));
20
- env.registerOperator("sol_address < sol_address", (left, right) => toAddress(left) < toAddress(right));
21
- env.registerOperator("sol_address <= sol_address", (left, right) => toAddress(left) <= toAddress(right));
22
- env.registerOperator("sol_address > sol_address", (left, right) => toAddress(left) > toAddress(right));
23
- env.registerOperator("sol_address >= sol_address", (left, right) => toAddress(left) >= toAddress(right));
24
- };
25
- const toSolInt = (value) => new SolidityIntTypeWrapper(value);
26
- const registerIntegerOperators = (env) => {
27
- // --- sol_int <op> sol_int arithmetic ---
28
- env.registerOperator("sol_int + sol_int", (a, b) => toSolInt(toBigInt(a) + toBigInt(b)));
29
- env.registerOperator("sol_int - sol_int", (a, b) => toSolInt(toBigInt(a) - toBigInt(b)));
30
- env.registerOperator("sol_int * sol_int", (a, b) => toSolInt(toBigInt(a) * toBigInt(b)));
31
- env.registerOperator("sol_int / sol_int", (a, b) => {
32
- const divisor = toBigInt(b);
33
- if (divisor === 0n) {
34
- throw new Error("division by zero");
35
- }
36
- return toSolInt(toBigInt(a) / divisor);
37
- });
38
- env.registerOperator("sol_int % sol_int", (a, b) => {
39
- const divisor = toBigInt(b);
40
- if (divisor === 0n) {
41
- throw new Error("modulo by zero");
42
- }
43
- return toSolInt(toBigInt(a) % divisor);
44
- });
45
- // Unary negation
46
- env.registerOperator("-sol_int", (a) => toSolInt(-toBigInt(a)));
47
- // --- sol_int <op> sol_int comparison ---
48
- env.registerOperator("sol_int == sol_int", (a, b) => toBigInt(a) === toBigInt(b));
49
- env.registerOperator("sol_int < sol_int", (a, b) => toBigInt(a) < toBigInt(b));
50
- env.registerOperator("sol_int <= sol_int", (a, b) => toBigInt(a) <= toBigInt(b));
51
- env.registerOperator("sol_int > sol_int", (a, b) => toBigInt(a) > toBigInt(b));
52
- env.registerOperator("sol_int >= sol_int", (a, b) => toBigInt(a) >= toBigInt(b));
53
- /*
54
- * --- sol_int <op> int cross-type arithmetic ---
55
- *
56
- * cel-js infers the result type of a binary operator from the LEFT operand.
57
- * For "sol_int <op> int", the result type is "sol_int" — matching the
58
- * SolidityIntTypeWrapper returned by toSolInt(). These are safe.
59
- *
60
- * We intentionally do NOT register "int <op> sol_int" arithmetic operators.
61
- * cel-js would infer the result as "int", but the handler would return a
62
- * SolidityIntTypeWrapper. The codec for "int" cannot unwrap that wrapper,
63
- * causing the raw wrapper to leak into the evaluation result.
64
- *
65
- * Cross-type comparison operators (below) are fine — they return plain
66
- * booleans regardless of operand order.
67
- */
68
- tryRegisterOperator(env, "sol_int + int", (a, b) => toSolInt(toBigInt(a) + toBigInt(b)));
69
- tryRegisterOperator(env, "sol_int - int", (a, b) => toSolInt(toBigInt(a) - toBigInt(b)));
70
- tryRegisterOperator(env, "sol_int * int", (a, b) => toSolInt(toBigInt(a) * toBigInt(b)));
71
- tryRegisterOperator(env, "sol_int / int", (a, b) => {
72
- const divisor = toBigInt(b);
73
- if (divisor === 0n) {
74
- throw new Error("division by zero");
75
- }
76
- return toSolInt(toBigInt(a) / divisor);
77
- });
78
- tryRegisterOperator(env, "sol_int % int", (a, b) => {
79
- const divisor = toBigInt(b);
80
- if (divisor === 0n) {
81
- throw new Error("modulo by zero");
82
- }
83
- return toSolInt(toBigInt(a) % divisor);
84
- });
85
- // --- sol_int <op> int cross-type comparison ---
86
- tryRegisterOperator(env, "sol_int == int", (a, b) => toBigInt(a) === toBigInt(b));
87
- tryRegisterOperator(env, "int == sol_int", (a, b) => toBigInt(a) === toBigInt(b));
88
- tryRegisterOperator(env, "sol_int < int", (a, b) => toBigInt(a) < toBigInt(b));
89
- tryRegisterOperator(env, "int < sol_int", (a, b) => toBigInt(a) < toBigInt(b));
90
- tryRegisterOperator(env, "sol_int <= int", (a, b) => toBigInt(a) <= toBigInt(b));
91
- tryRegisterOperator(env, "int <= sol_int", (a, b) => toBigInt(a) <= toBigInt(b));
92
- tryRegisterOperator(env, "sol_int > int", (a, b) => toBigInt(a) > toBigInt(b));
93
- tryRegisterOperator(env, "int > sol_int", (a, b) => toBigInt(a) > toBigInt(b));
94
- tryRegisterOperator(env, "sol_int >= int", (a, b) => toBigInt(a) >= toBigInt(b));
95
- tryRegisterOperator(env, "int >= sol_int", (a, b) => toBigInt(a) >= toBigInt(b));
96
- };
97
- /**
98
- * Register all Solidity primitive types on a CEL Environment.
99
- */
100
- export const registerSolidityTypes = (env) => {
101
- for (const entry of SOLIDITY_TYPES) {
102
- /*
103
- * Builtin types are handled natively by CEL and don't need registration.
104
- * They are in the registry for completeness/documentation but have no wrapper class or operators.
105
- */
106
- if (entry.type === "builtin") {
107
- continue;
108
- }
109
- env.registerType(entry.name, entry.wrapperClass);
110
- if (entry.name === "sol_address") {
111
- registerAddressOperators(env);
112
- }
113
- if (entry.name === "sol_int") {
114
- registerIntegerOperators(env);
115
- }
116
- // Register all the cast signatures for this type.
117
- const Ctor = entry.wrapperClass;
118
- for (const sig of entry.castSignatures) {
119
- env.registerFunction(sig, (value) => new Ctor(value));
120
- }
121
- }
122
- // --- Standalone helper functions ---
123
- // parseUnits: scale human-readable values to sol_int
124
- env.registerFunction("parseUnits(string, int): sol_int", (value, decimals) => parseUnitsValue(value, Number(toBigInt(decimals))));
125
- env.registerFunction("parseUnits(int, int): sol_int", (value, decimals) => parseUnitsValue(value, Number(toBigInt(decimals))));
126
- env.registerFunction("parseUnits(double, int): sol_int", (value, decimals) => parseUnitsValue(value, Number(toBigInt(decimals))));
127
- env.registerFunction("parseUnits(sol_int, int): sol_int", (value, decimals) => parseUnitsValue(value, Number(toBigInt(decimals))));
128
- // formatUnits: scale sol_int down to human-readable double
129
- env.registerFunction("formatUnits(sol_int, int): double", (value, decimals) => formatUnitsValue(value, Number(toBigInt(decimals))));
130
- env.registerFunction("formatUnits(int, int): double", (value, decimals) => formatUnitsValue(value, Number(toBigInt(decimals))));
131
- // min: return the smaller of two values
132
- env.registerFunction("min(sol_int, sol_int): sol_int", (a, b) => toBigInt(a) <= toBigInt(b) ? toSolInt(a) : toSolInt(b));
133
- env.registerFunction("min(sol_int, int): sol_int", (a, b) => toBigInt(a) <= toBigInt(b) ? toSolInt(a) : toSolInt(b));
134
- env.registerFunction("min(int, sol_int): sol_int", (a, b) => toBigInt(a) <= toBigInt(b) ? toSolInt(a) : toSolInt(b));
135
- env.registerFunction("min(int, int): sol_int", (a, b) => toBigInt(a) <= toBigInt(b) ? toSolInt(a) : toSolInt(b));
136
- // max: return the larger of two values
137
- env.registerFunction("max(sol_int, sol_int): sol_int", (a, b) => toBigInt(a) >= toBigInt(b) ? toSolInt(a) : toSolInt(b));
138
- env.registerFunction("max(sol_int, int): sol_int", (a, b) => toBigInt(a) >= toBigInt(b) ? toSolInt(a) : toSolInt(b));
139
- env.registerFunction("max(int, sol_int): sol_int", (a, b) => toBigInt(a) >= toBigInt(b) ? toSolInt(a) : toSolInt(b));
140
- env.registerFunction("max(int, int): sol_int", (a, b) => toBigInt(a) >= toBigInt(b) ? toSolInt(a) : toSolInt(b));
141
- // abs: return the absolute value
142
- env.registerFunction("abs(sol_int): sol_int", (a) => {
143
- const v = toBigInt(a);
144
- return toSolInt(v < 0n ? -v : v);
145
- });
146
- env.registerFunction("abs(int): sol_int", (a) => {
147
- const v = toBigInt(a);
148
- return toSolInt(v < 0n ? -v : v);
149
- });
150
- // isZeroAddress: check if an address is the zero address
151
- const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
152
- env.registerFunction("isZeroAddress(sol_address): bool", (addr) => toAddress(addr) === ZERO_ADDRESS);
153
- env.registerFunction("isZeroAddress(string): bool", (addr) => toAddress(addr) === ZERO_ADDRESS);
154
- };
@@ -1,17 +0,0 @@
1
- export interface StructInfo {
2
- ctor: new () => object;
3
- fieldNames: string[];
4
- fieldTypes: Record<string, string>;
5
- }
6
- /**
7
- * Maps a CEL type to the native CEL literal type for dual function registration.
8
- */
9
- export declare const toCelLiteralType: (celType: string) => string | null;
10
- /**
11
- * Wraps a raw value into the corresponding SEL wrapper type.
12
- */
13
- export declare const wrapValueForSel: (celType: string, value: unknown) => unknown;
14
- /**
15
- * Recursively wraps a raw struct value with the correct ctor and wrapped field values.
16
- */
17
- export declare const wrapStructValue: (registry: Map<string, StructInfo>, info: StructInfo, value: unknown) => unknown;
@@ -1,65 +0,0 @@
1
- import { SolidityAddressTypeWrapper, SolidityIntTypeWrapper, } from "@seljs/types";
2
- const wrapFieldValue = (registry, celType, value) => {
3
- const nested = registry.get(celType);
4
- if (nested) {
5
- return wrapStructValue(registry, nested, value);
6
- }
7
- return wrapValueForSel(celType, value);
8
- };
9
- /**
10
- * Maps a CEL type to the native CEL literal type for dual function registration.
11
- */
12
- export const toCelLiteralType = (celType) => {
13
- if (celType === "sol_int") {
14
- return "int";
15
- }
16
- if (celType === "sol_address") {
17
- return "string";
18
- }
19
- return null;
20
- };
21
- /**
22
- * Wraps a raw value into the corresponding SEL wrapper type.
23
- */
24
- export const wrapValueForSel = (celType, value) => {
25
- if (value instanceof SolidityAddressTypeWrapper) {
26
- return value;
27
- }
28
- if (value instanceof SolidityIntTypeWrapper) {
29
- return value;
30
- }
31
- if (celType === "sol_address") {
32
- return new SolidityAddressTypeWrapper(value);
33
- }
34
- if (typeof value === "bigint") {
35
- return new SolidityIntTypeWrapper(value);
36
- }
37
- if (typeof value === "number" && Number.isInteger(value)) {
38
- return new SolidityIntTypeWrapper(BigInt(value));
39
- }
40
- return value;
41
- };
42
- /**
43
- * Recursively wraps a raw struct value with the correct ctor and wrapped field values.
44
- */
45
- export const wrapStructValue = (registry, info, value) => {
46
- const { ctor, fieldNames, fieldTypes } = info;
47
- let data;
48
- if (Array.isArray(value)) {
49
- data = Object.fromEntries(fieldNames.map((name, i) => [
50
- name,
51
- wrapFieldValue(registry, fieldTypes[name] ?? "dyn", value[i]),
52
- ]));
53
- }
54
- else {
55
- const raw = value;
56
- data = Object.fromEntries(Object.entries(raw).map(([name, val]) => [
57
- name,
58
- fieldTypes[name]
59
- ? wrapFieldValue(registry, fieldTypes[name], val)
60
- : val,
61
- ]));
62
- }
63
- // eslint-disable-next-line new-cap
64
- return Object.assign(new ctor(), data);
65
- };
package/dist/index.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from "./checker/index.js";
2
- export * from "./constants.js";
3
- export * from "./environment/index.js";
4
- export * from "./rules/index.js";
package/dist/index.js DELETED
@@ -1,4 +0,0 @@
1
- export * from "./checker/index.js";
2
- export * from "./constants.js";
3
- export * from "./environment/index.js";
4
- export * from "./rules/index.js";
@@ -1,13 +0,0 @@
1
- import type { SELRule } from "../types.js";
2
- /**
3
- * Flags contract calls whose arguments cannot be resolved statically,
4
- * meaning they will execute as live RPC calls at evaluation time instead
5
- * of being batched via multicall.
6
- *
7
- * This includes calls with arguments that depend on:
8
- * - Comprehension iteration variables (map/filter/exists)
9
- * - cel.bind() scoped variables
10
- * - Arithmetic or other expressions on call results
11
- * - Struct field access on call results
12
- */
13
- export declare const deferredCall: SELRule;
@@ -1,162 +0,0 @@
1
- import { isAstNode } from "@seljs/common";
2
- import { COMPREHENSION_MACROS, SCALAR_WRAPPER_FUNCTIONS, } from "../../constants.js";
3
- import { walkAST } from "../../utils/index.js";
4
- /**
5
- * Checks whether a scalar wrapper inner value (inside solInt/solAddress) is resolvable.
6
- */
7
- const isScalarInnerResolvable = (inner, scopedVars) => {
8
- if (!isAstNode(inner)) {
9
- return false;
10
- }
11
- if (inner.op === "value") {
12
- return true;
13
- }
14
- return (inner.op === "id" &&
15
- typeof inner.args === "string" &&
16
- !scopedVars.has(inner.args));
17
- };
18
- /**
19
- * Checks whether a call node is a resolvable scalar wrapper (solInt/solAddress).
20
- */
21
- const isResolvableScalarWrapper = (node, scopedVars) => {
22
- const nodeArgs = node.args;
23
- const fnName = nodeArgs[0];
24
- const fnArgs = nodeArgs[1];
25
- if (typeof fnName !== "string" ||
26
- !SCALAR_WRAPPER_FUNCTIONS.has(fnName) ||
27
- !Array.isArray(fnArgs) ||
28
- fnArgs.length !== 1) {
29
- return false;
30
- }
31
- return isScalarInnerResolvable(fnArgs[0], scopedVars);
32
- };
33
- /**
34
- * Checks whether a single argument node is statically resolvable
35
- * (literal, context variable, or nested contract call result).
36
- */
37
- const isResolvableArg = (argNode, contractNames, scopedVars) => {
38
- if (!isAstNode(argNode)) {
39
- return false;
40
- }
41
- // Scalar wrapper: solInt(...), solAddress(...)
42
- if (argNode.op === "call") {
43
- return isResolvableScalarWrapper(argNode, scopedVars);
44
- }
45
- // Literal value
46
- if (argNode.op === "value") {
47
- return true;
48
- }
49
- // Variable reference — resolvable only if NOT scoped
50
- if (argNode.op === "id" && typeof argNode.args === "string") {
51
- return !scopedVars.has(argNode.args);
52
- }
53
- // Nested contract call result — resolvable (will be pre-executed)
54
- if (argNode.op === "rcall") {
55
- return isContractRCall(argNode, contractNames);
56
- }
57
- return false;
58
- };
59
- /**
60
- * Checks whether an rcall node targets a registered contract.
61
- */
62
- const isContractRCall = (node, contractNames) => {
63
- const nodeArgs = node.args;
64
- const receiverNode = nodeArgs[1];
65
- return (isAstNode(receiverNode) &&
66
- receiverNode.op === "id" &&
67
- typeof receiverNode.args === "string" &&
68
- contractNames.has(receiverNode.args));
69
- };
70
- /**
71
- * Extracts the rcall components: method name, receiver node, and argument list.
72
- */
73
- const parseRCallNode = (node) => {
74
- const nodeArgs = node.args;
75
- const method = nodeArgs[0];
76
- const receiver = nodeArgs[1];
77
- const callArgs = nodeArgs[2];
78
- if (typeof method !== "string" || !Array.isArray(callArgs)) {
79
- return undefined;
80
- }
81
- return { method, receiver, callArgs };
82
- };
83
- /**
84
- * Tracks scoped variables introduced by comprehension macros or cel.bind().
85
- */
86
- const trackScopedVars = (parsed, scopedVars) => {
87
- const { method, receiver, callArgs } = parsed;
88
- // Comprehension iteration variables
89
- if (isAstNode(receiver) &&
90
- receiver.op !== "id" &&
91
- COMPREHENSION_MACROS.has(method) &&
92
- callArgs.length >= 1) {
93
- const iterVarNode = callArgs[0];
94
- if (isAstNode(iterVarNode) &&
95
- iterVarNode.op === "id" &&
96
- typeof iterVarNode.args === "string") {
97
- scopedVars.add(iterVarNode.args);
98
- }
99
- }
100
- // cel.bind() variables
101
- if (method === "bind" &&
102
- isAstNode(receiver) &&
103
- receiver.op === "id" &&
104
- receiver.args === "cel" &&
105
- callArgs.length >= 1) {
106
- const nameNode = callArgs[0];
107
- if (isAstNode(nameNode) &&
108
- nameNode.op === "id" &&
109
- typeof nameNode.args === "string") {
110
- scopedVars.add(nameNode.args);
111
- }
112
- }
113
- };
114
- /**
115
- * Flags contract calls whose arguments cannot be resolved statically,
116
- * meaning they will execute as live RPC calls at evaluation time instead
117
- * of being batched via multicall.
118
- *
119
- * This includes calls with arguments that depend on:
120
- * - Comprehension iteration variables (map/filter/exists)
121
- * - cel.bind() scoped variables
122
- * - Arithmetic or other expressions on call results
123
- * - Struct field access on call results
124
- */
125
- export const deferredCall = {
126
- name: "deferred-call",
127
- description: "Flags contract calls with dynamic arguments that cannot be batched via multicall.",
128
- defaultSeverity: "info",
129
- tier: "structural",
130
- run(context) {
131
- const contractNames = new Set(context.schema.contracts.map((c) => c.name));
132
- if (contractNames.size === 0) {
133
- return [];
134
- }
135
- const diagnostics = [];
136
- const scopedVars = new Set();
137
- walkAST(context.ast, (node) => {
138
- if (node.op !== "rcall") {
139
- return;
140
- }
141
- const parsed = parseRCallNode(node);
142
- if (!parsed) {
143
- return;
144
- }
145
- const { method, callArgs } = parsed;
146
- // Track scoped variables
147
- trackScopedVars(parsed, scopedVars);
148
- // Only check contract calls
149
- if (!isContractRCall(node, contractNames)) {
150
- return;
151
- }
152
- const receiverNode = node.args[1];
153
- const contractName = receiverNode.args;
154
- // Check if any argument is unresolvable
155
- const hasDeferred = callArgs.some((argNode) => !isResolvableArg(argNode, contractNames, scopedVars));
156
- if (hasDeferred) {
157
- diagnostics.push(context.report(node, `\`${contractName}.${method}()\` has dynamic arguments and will execute as a live RPC call instead of being batched.`));
158
- }
159
- });
160
- return diagnostics;
161
- },
162
- };
@@ -1,6 +0,0 @@
1
- export * from "./deferred-call.js";
2
- export * from "./no-constant-condition.js";
3
- export * from "./no-mixed-operators.js";
4
- export * from "./no-redundant-bool.js";
5
- export * from "./no-self-comparison.js";
6
- export * from "./require-type.js";
@@ -1,6 +0,0 @@
1
- export * from "./deferred-call.js";
2
- export * from "./no-constant-condition.js";
3
- export * from "./no-mixed-operators.js";
4
- export * from "./no-redundant-bool.js";
5
- export * from "./no-self-comparison.js";
6
- export * from "./require-type.js";
@@ -1,7 +0,0 @@
1
- import type { SELRule } from "../types.js";
2
- /**
3
- * Flags conditions that are always true or always false:
4
- * - `true && x`, `false || x` (boolean literal in logical operator)
5
- * - Both sides are literal values in a comparison
6
- */
7
- export declare const noConstantCondition: SELRule;
@@ -1,36 +0,0 @@
1
- import { walkAST } from "../../utils/index.js";
2
- /**
3
- * Flags conditions that are always true or always false:
4
- * - `true && x`, `false || x` (boolean literal in logical operator)
5
- * - Both sides are literal values in a comparison
6
- */
7
- export const noConstantCondition = {
8
- name: "no-constant-condition",
9
- description: "Disallow constant conditions in logical and comparison operators.",
10
- defaultSeverity: "warning",
11
- tier: "structural",
12
- run(context) {
13
- const diagnostics = [];
14
- walkAST(context.ast, (node) => {
15
- // Check && / || with boolean literal operands
16
- if (node.op === "&&" || node.op === "||") {
17
- const [left, right] = node.args;
18
- if (left.op === "value" && typeof left.args === "boolean") {
19
- diagnostics.push(context.report(node, `Constant condition: left side of \`${node.op}\` is always \`${String(left.args)}\`.`));
20
- }
21
- if (right.op === "value" && typeof right.args === "boolean") {
22
- diagnostics.push(context.report(node, `Constant condition: right side of \`${node.op}\` is always \`${String(right.args)}\`.`));
23
- }
24
- return;
25
- }
26
- // Check == / != where both sides are literals
27
- if (node.op === "==" || node.op === "!=") {
28
- const [left, right] = node.args;
29
- if (left.op === "value" && right.op === "value") {
30
- diagnostics.push(context.report(node, `Constant condition: both sides of \`${node.op}\` are literal values.`));
31
- }
32
- }
33
- });
34
- return diagnostics;
35
- },
36
- };
@@ -1,9 +0,0 @@
1
- import type { SELRule } from "../types.js";
2
- /**
3
- * Flags mixed `&&` and `||` without explicit parentheses.
4
- *
5
- * The cel-js parser discards parentheses from the AST, so this rule
6
- * inspects the original source text via `node.input` to determine
7
- * whether parentheses were explicitly written.
8
- */
9
- export declare const noMixedOperators: SELRule;
@@ -1,44 +0,0 @@
1
- import { nodeSpan, walkAST } from "../../utils/index.js";
2
- /**
3
- * Check whether a child expression has explicit parentheses in the source.
4
- * Scans backwards from the child's start position for a `(`.
5
- */
6
- const hasExplicitParens = (src, child) => {
7
- const span = nodeSpan(child);
8
- let i = span.from - 1;
9
- while (i >= 0 && (src[i] === " " || src[i] === "\t")) {
10
- i--;
11
- }
12
- return i >= 0 && src[i] === "(";
13
- };
14
- /**
15
- * Flags mixed `&&` and `||` without explicit parentheses.
16
- *
17
- * The cel-js parser discards parentheses from the AST, so this rule
18
- * inspects the original source text via `node.input` to determine
19
- * whether parentheses were explicitly written.
20
- */
21
- export const noMixedOperators = {
22
- name: "no-mixed-operators",
23
- description: "Require parentheses when mixing logical operators (&& and ||).",
24
- defaultSeverity: "info",
25
- tier: "structural",
26
- run(context) {
27
- const diagnostics = [];
28
- walkAST(context.ast, (node) => {
29
- if (node.op !== "&&" && node.op !== "||") {
30
- return;
31
- }
32
- const opposite = node.op === "&&" ? "||" : "&&";
33
- const [left, right] = node.args;
34
- for (const child of [left, right]) {
35
- if (child.op === opposite && !hasExplicitParens(node.input, child)) {
36
- diagnostics.push(context.report(node, "Mixed logical operators without parentheses. Add explicit grouping to clarify precedence."));
37
- // one diagnostic per node
38
- break;
39
- }
40
- }
41
- });
42
- return diagnostics;
43
- },
44
- };
@@ -1,5 +0,0 @@
1
- import type { SELRule } from "../types.js";
2
- /**
3
- * Flags redundant boolean comparisons: `x == true`, `x != false`, etc.
4
- */
5
- export declare const noRedundantBool: SELRule;
@@ -1,27 +0,0 @@
1
- import { walkAST } from "../../utils/index.js";
2
- /**
3
- * Flags redundant boolean comparisons: `x == true`, `x != false`, etc.
4
- */
5
- export const noRedundantBool = {
6
- name: "no-redundant-bool",
7
- description: "Disallow redundant comparisons to boolean literals (true/false).",
8
- defaultSeverity: "warning",
9
- tier: "structural",
10
- run(context) {
11
- const diagnostics = [];
12
- walkAST(context.ast, (node) => {
13
- if (node.op !== "==" && node.op !== "!=") {
14
- return;
15
- }
16
- const [left, right] = node.args;
17
- const leftIsBool = left.op === "value" && typeof left.args === "boolean";
18
- const rightIsBool = right.op === "value" && typeof right.args === "boolean";
19
- if (!leftIsBool && !rightIsBool) {
20
- return;
21
- }
22
- const boolValue = leftIsBool ? left.args : right.args;
23
- diagnostics.push(context.report(node, `Redundant comparison to \`${String(boolValue)}\`. Simplify the expression.`));
24
- });
25
- return diagnostics;
26
- },
27
- };
@@ -1,9 +0,0 @@
1
- import type { SELRule } from "../types.js";
2
- /**
3
- * Flags comparisons where both sides are the same expression (tautology/contradiction).
4
- *
5
- * Uses `serialize()` for identity comparison (safe for this purpose —
6
- * normalization is acceptable when comparing subtree identity, unlike
7
- * span calculation where it would produce incorrect positions).
8
- */
9
- export declare const noSelfComparison: SELRule;
@@ -1,31 +0,0 @@
1
- import { serialize } from "@marcbachmann/cel-js";
2
- import { walkAST } from "../../utils/index.js";
3
- const COMPARISON_OPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
4
- const ALWAYS_TRUE_OPS = new Set(["==", "<=", ">="]);
5
- /**
6
- * Flags comparisons where both sides are the same expression (tautology/contradiction).
7
- *
8
- * Uses `serialize()` for identity comparison (safe for this purpose —
9
- * normalization is acceptable when comparing subtree identity, unlike
10
- * span calculation where it would produce incorrect positions).
11
- */
12
- export const noSelfComparison = {
13
- name: "no-self-comparison",
14
- description: "Disallow comparing an expression to itself.",
15
- defaultSeverity: "warning",
16
- tier: "structural",
17
- run(context) {
18
- const diagnostics = [];
19
- walkAST(context.ast, (node) => {
20
- if (!COMPARISON_OPS.has(node.op)) {
21
- return;
22
- }
23
- const [left, right] = node.args;
24
- if (serialize(left) === serialize(right)) {
25
- const alwaysResult = ALWAYS_TRUE_OPS.has(node.op) ? "true" : "false";
26
- diagnostics.push(context.report(node, `Both sides of \`${node.op}\` are identical. This is always \`${alwaysResult}\`.`));
27
- }
28
- });
29
- return diagnostics;
30
- },
31
- };
@@ -1,7 +0,0 @@
1
- import type { SELRule } from "../types.js";
2
- /**
3
- * Rule factory that enforces an expression evaluates to the expected CEL type.
4
- *
5
- * @param expected - The CEL type name the expression must resolve to (e.g. "bool", "uint256")
6
- */
7
- export declare const requireType: (expected: string) => SELRule;
@@ -1,19 +0,0 @@
1
- /**
2
- * Rule factory that enforces an expression evaluates to the expected CEL type.
3
- *
4
- * @param expected - The CEL type name the expression must resolve to (e.g. "bool", "uint256")
5
- */
6
- export const requireType = (expected) => ({
7
- name: `require-type-${expected}`,
8
- description: `Expression must evaluate to ${expected}.`,
9
- defaultSeverity: "error",
10
- tier: "type-aware",
11
- run(context) {
12
- if (context.resolvedType === expected) {
13
- return [];
14
- }
15
- return [
16
- context.reportAt(0, context.expression.length, `Expected expression to evaluate to "${expected}", but got "${String(context.resolvedType)}".`),
17
- ];
18
- },
19
- });