@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.
- package/CHANGELOG.md +7 -0
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/checker/checker.cjs +482 -0
- package/dist/checker/checker.d.cts +176 -0
- package/dist/checker/checker.d.mts +176 -0
- package/dist/checker/checker.mjs +481 -0
- package/dist/checker/diagnostics.cjs +66 -0
- package/dist/checker/diagnostics.mjs +66 -0
- package/dist/checker/index.cjs +2 -0
- package/dist/checker/index.d.mts +2 -0
- package/dist/checker/index.mjs +3 -0
- package/dist/checker/type-compatibility.cjs +70 -0
- package/dist/checker/type-compatibility.d.cts +8 -0
- package/dist/checker/type-compatibility.d.mts +8 -0
- package/dist/checker/type-compatibility.mjs +69 -0
- package/dist/constants.cjs +14 -0
- package/dist/constants.d.cts +7 -0
- package/dist/constants.d.mts +7 -0
- package/dist/constants.mjs +13 -0
- package/dist/debug.cjs +7 -0
- package/dist/debug.mjs +5 -0
- package/dist/environment/codec-registry.cjs +109 -0
- package/dist/environment/codec-registry.d.cts +45 -0
- package/dist/environment/codec-registry.d.mts +45 -0
- package/dist/environment/codec-registry.mjs +108 -0
- package/dist/environment/hydrate.cjs +163 -0
- package/dist/environment/hydrate.d.cts +52 -0
- package/dist/environment/hydrate.d.mts +52 -0
- package/dist/environment/hydrate.mjs +160 -0
- package/dist/environment/index.cjs +4 -0
- package/dist/environment/index.d.mts +4 -0
- package/dist/environment/index.mjs +5 -0
- package/dist/environment/register-types.cjs +107 -0
- package/dist/environment/register-types.d.cts +15 -0
- package/dist/environment/register-types.d.mts +15 -0
- package/dist/environment/register-types.mjs +106 -0
- package/dist/environment/value-wrappers.cjs +44 -0
- package/dist/environment/value-wrappers.d.cts +20 -0
- package/dist/environment/value-wrappers.d.mts +20 -0
- package/dist/environment/value-wrappers.mjs +41 -0
- package/dist/index.cjs +27 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +13 -0
- package/dist/rules/defaults/deferred-call.cjs +107 -0
- package/dist/rules/defaults/deferred-call.mjs +106 -0
- package/dist/rules/defaults/index.cjs +6 -0
- package/dist/rules/defaults/index.mjs +7 -0
- package/dist/rules/defaults/no-constant-condition.cjs +31 -0
- package/dist/rules/defaults/no-constant-condition.mjs +31 -0
- package/dist/rules/defaults/no-mixed-operators.cjs +39 -0
- package/dist/rules/defaults/no-mixed-operators.mjs +39 -0
- package/dist/rules/defaults/no-redundant-bool.cjs +26 -0
- package/dist/rules/defaults/no-redundant-bool.mjs +26 -0
- package/dist/rules/defaults/no-self-comparison.cjs +44 -0
- package/dist/rules/defaults/no-self-comparison.mjs +43 -0
- package/dist/rules/defaults/require-type.cjs +18 -0
- package/dist/rules/defaults/require-type.mjs +18 -0
- package/dist/rules/facade.cjs +31 -0
- package/dist/rules/facade.d.cts +20 -0
- package/dist/rules/facade.d.mts +20 -0
- package/dist/rules/facade.mjs +31 -0
- package/dist/rules/index.cjs +2 -0
- package/dist/rules/index.d.mts +3 -0
- package/dist/rules/index.mjs +3 -0
- package/dist/rules/runner.cjs +40 -0
- package/dist/rules/runner.d.cts +27 -0
- package/dist/rules/runner.d.mts +27 -0
- package/dist/rules/runner.mjs +40 -0
- package/dist/rules/types.d.cts +77 -0
- package/dist/rules/types.d.mts +77 -0
- package/dist/utils/ast-utils.cjs +164 -0
- package/dist/utils/ast-utils.mjs +162 -0
- package/package.json +24 -17
- package/dist/checker/checker.d.ts +0 -173
- package/dist/checker/checker.js +0 -567
- package/dist/checker/diagnostics.d.ts +0 -10
- package/dist/checker/diagnostics.js +0 -80
- package/dist/checker/index.d.ts +0 -2
- package/dist/checker/index.js +0 -2
- package/dist/checker/type-compatibility.d.ts +0 -16
- package/dist/checker/type-compatibility.js +0 -59
- package/dist/constants.d.ts +0 -4
- package/dist/constants.js +0 -10
- package/dist/debug.d.ts +0 -2
- package/dist/debug.js +0 -2
- package/dist/environment/codec-registry.d.ts +0 -42
- package/dist/environment/codec-registry.js +0 -146
- package/dist/environment/hydrate.d.ts +0 -48
- package/dist/environment/hydrate.js +0 -198
- package/dist/environment/index.d.ts +0 -4
- package/dist/environment/index.js +0 -4
- package/dist/environment/register-types.d.ts +0 -14
- package/dist/environment/register-types.js +0 -154
- package/dist/environment/value-wrappers.d.ts +0 -17
- package/dist/environment/value-wrappers.js +0 -65
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -4
- package/dist/rules/defaults/deferred-call.d.ts +0 -13
- package/dist/rules/defaults/deferred-call.js +0 -162
- package/dist/rules/defaults/index.d.ts +0 -6
- package/dist/rules/defaults/index.js +0 -6
- package/dist/rules/defaults/no-constant-condition.d.ts +0 -7
- package/dist/rules/defaults/no-constant-condition.js +0 -36
- package/dist/rules/defaults/no-mixed-operators.d.ts +0 -9
- package/dist/rules/defaults/no-mixed-operators.js +0 -44
- package/dist/rules/defaults/no-redundant-bool.d.ts +0 -5
- package/dist/rules/defaults/no-redundant-bool.js +0 -27
- package/dist/rules/defaults/no-self-comparison.d.ts +0 -9
- package/dist/rules/defaults/no-self-comparison.js +0 -31
- package/dist/rules/defaults/require-type.d.ts +0 -7
- package/dist/rules/defaults/require-type.js +0 -19
- package/dist/rules/facade.d.ts +0 -22
- package/dist/rules/facade.js +0 -29
- package/dist/rules/index.d.ts +0 -3
- package/dist/rules/index.js +0 -3
- package/dist/rules/runner.d.ts +0 -16
- package/dist/rules/runner.js +0 -30
- package/dist/rules/types.d.ts +0 -73
- package/dist/rules/types.js +0 -1
- package/dist/utils/ast-utils.d.ts +0 -55
- package/dist/utils/ast-utils.js +0 -255
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.js +0 -1
package/dist/checker/checker.js
DELETED
|
@@ -1,567 +0,0 @@
|
|
|
1
|
-
import { contractTypeName } from "@seljs/common";
|
|
2
|
-
import { extractDiagnostics } from "./diagnostics.js";
|
|
3
|
-
import { expectedTypeForOperator } from "./type-compatibility.js";
|
|
4
|
-
import { createCheckerEnvironment } from "../environment/hydrate.js";
|
|
5
|
-
import { runRules } from "../rules/index.js";
|
|
6
|
-
import { findNodeWithParentAt, nodeSpan, walkAST } from "../utils/index.js";
|
|
7
|
-
/**
|
|
8
|
-
* SEL expression checker.
|
|
9
|
-
*
|
|
10
|
-
* Wraps a hydrated cel-js Environment built from a SELSchema and exposes
|
|
11
|
-
* parse/type-check, type inference, cursor-position type lookups, and
|
|
12
|
-
* type-aware completions.
|
|
13
|
-
*/
|
|
14
|
-
export class SELChecker {
|
|
15
|
-
rules;
|
|
16
|
-
env;
|
|
17
|
-
schema;
|
|
18
|
-
structTypeMap;
|
|
19
|
-
constructor(schema, options) {
|
|
20
|
-
this.schema = schema;
|
|
21
|
-
this.env = createCheckerEnvironment(schema);
|
|
22
|
-
this.rules = options?.rules ?? [];
|
|
23
|
-
this.structTypeMap = this.buildStructTypeMap(schema);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Parse and type-check an expression, returning position-aware diagnostics.
|
|
27
|
-
*
|
|
28
|
-
* Uses a single `env.parse()` call, then `.check()` on the result
|
|
29
|
-
* to avoid double-parsing. Rules run against the AST from the same parse:
|
|
30
|
-
* - Structural rules run after successful parse (even if type-check fails)
|
|
31
|
-
* - Type-aware rules run only after both parse and type-check succeed
|
|
32
|
-
*/
|
|
33
|
-
check(expression) {
|
|
34
|
-
let parsed;
|
|
35
|
-
try {
|
|
36
|
-
parsed = this.env.parse(expression);
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
// Parse error — no AST available, no rules can run
|
|
40
|
-
return {
|
|
41
|
-
valid: false,
|
|
42
|
-
diagnostics: extractDiagnostics(expression, error),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
// Run structural rules (these run regardless of type-check result)
|
|
46
|
-
const structuralDiags = this.rules.length > 0
|
|
47
|
-
? runRules({
|
|
48
|
-
expression,
|
|
49
|
-
ast: parsed.ast,
|
|
50
|
-
schema: this.schema,
|
|
51
|
-
rules: this.rules,
|
|
52
|
-
tier: "structural",
|
|
53
|
-
})
|
|
54
|
-
: [];
|
|
55
|
-
// Type-check using the same parse result (no second parse)
|
|
56
|
-
let typeResult;
|
|
57
|
-
try {
|
|
58
|
-
typeResult = parsed.check();
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
return {
|
|
62
|
-
valid: false,
|
|
63
|
-
diagnostics: [
|
|
64
|
-
...extractDiagnostics(expression, error),
|
|
65
|
-
...structuralDiags,
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
if (!typeResult.valid) {
|
|
70
|
-
const typeDiags = typeResult.error
|
|
71
|
-
? extractDiagnostics(expression, typeResult.error)
|
|
72
|
-
: [
|
|
73
|
-
{
|
|
74
|
-
message: "Type check failed",
|
|
75
|
-
severity: "error",
|
|
76
|
-
from: 0,
|
|
77
|
-
to: expression.length,
|
|
78
|
-
},
|
|
79
|
-
];
|
|
80
|
-
return {
|
|
81
|
-
valid: false,
|
|
82
|
-
diagnostics: [...typeDiags, ...structuralDiags],
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
// Run type-aware rules (only on fully valid expressions)
|
|
86
|
-
const typeAwareDiags = this.rules.length > 0
|
|
87
|
-
? runRules({
|
|
88
|
-
expression,
|
|
89
|
-
ast: parsed.ast,
|
|
90
|
-
schema: this.schema,
|
|
91
|
-
rules: this.rules,
|
|
92
|
-
tier: "type-aware",
|
|
93
|
-
resolvedType: typeResult.type,
|
|
94
|
-
})
|
|
95
|
-
: [];
|
|
96
|
-
const allRuleDiags = [...structuralDiags, ...typeAwareDiags];
|
|
97
|
-
const hasRuleError = allRuleDiags.some((d) => d.severity === "error");
|
|
98
|
-
return {
|
|
99
|
-
valid: !hasRuleError,
|
|
100
|
-
type: typeResult.type,
|
|
101
|
-
diagnostics: allRuleDiags,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Get the inferred CEL type of the full expression.
|
|
106
|
-
* Returns undefined if the expression is invalid.
|
|
107
|
-
*/
|
|
108
|
-
typeOf(expression) {
|
|
109
|
-
const result = this.check(expression);
|
|
110
|
-
return result.type;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Get the inferred type at a cursor position (for hover info).
|
|
114
|
-
*
|
|
115
|
-
* Attempts to identify the sub-expression under the cursor and infer its
|
|
116
|
-
* type. Falls back to the full expression type when sub-expression
|
|
117
|
-
* isolation is not possible.
|
|
118
|
-
*/
|
|
119
|
-
typeAt(expression, offset) {
|
|
120
|
-
if (offset < 0 || offset > expression.length) {
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
let parsed;
|
|
124
|
-
try {
|
|
125
|
-
parsed = this.env.parse(expression);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
const hit = findNodeWithParentAt(parsed.ast, offset);
|
|
131
|
-
if (!hit) {
|
|
132
|
-
const fullType = this.typeOf(expression);
|
|
133
|
-
return fullType
|
|
134
|
-
? { type: fullType, from: 0, to: expression.length }
|
|
135
|
-
: undefined;
|
|
136
|
-
}
|
|
137
|
-
const { node } = hit;
|
|
138
|
-
// Identifier: variable or contract name
|
|
139
|
-
if (node.op === "id") {
|
|
140
|
-
const name = node.args;
|
|
141
|
-
const span = nodeSpan(node);
|
|
142
|
-
const variable = this.schema.variables.find((v) => v.name === name);
|
|
143
|
-
if (variable) {
|
|
144
|
-
return { type: variable.type, from: span.from, to: span.to };
|
|
145
|
-
}
|
|
146
|
-
const contract = this.schema.contracts.find((c) => c.name === name);
|
|
147
|
-
if (contract) {
|
|
148
|
-
return {
|
|
149
|
-
type: contractTypeName(contract.name),
|
|
150
|
-
from: span.from,
|
|
151
|
-
to: span.to,
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// Find largest dot-access/call chain containing this offset
|
|
156
|
-
const chainNode = this.findContainingChain(parsed.ast, offset);
|
|
157
|
-
if (chainNode) {
|
|
158
|
-
const chainSpan = nodeSpan(chainNode);
|
|
159
|
-
const chainExpr = expression.slice(chainSpan.from, chainSpan.to);
|
|
160
|
-
const chainType = this.typeOf(chainExpr);
|
|
161
|
-
if (chainType) {
|
|
162
|
-
return { type: chainType, from: chainSpan.from, to: chainSpan.to };
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// Fall back to full expression type
|
|
166
|
-
const fullType = this.typeOf(expression);
|
|
167
|
-
return fullType
|
|
168
|
-
? { type: fullType, from: 0, to: expression.length }
|
|
169
|
-
: undefined;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Get type-aware completions for a cursor context.
|
|
173
|
-
*
|
|
174
|
-
* Supports:
|
|
175
|
-
* - **dot-access**: after `contract.` — lists methods of that contract
|
|
176
|
-
* - **top-level**: at the start or after operators — lists variables,
|
|
177
|
-
* contracts, and functions
|
|
178
|
-
*/
|
|
179
|
-
completionsAt(expression, offset) {
|
|
180
|
-
const beforeCursor = expression.slice(0, offset);
|
|
181
|
-
const lastDotIdx = beforeCursor.lastIndexOf(".");
|
|
182
|
-
if (lastDotIdx > 0) {
|
|
183
|
-
const receiverExpr = this.extractReceiverBefore(beforeCursor, lastDotIdx);
|
|
184
|
-
if (receiverExpr) {
|
|
185
|
-
return this.dotCompletions(receiverExpr);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// Top-level completions
|
|
189
|
-
const items = [];
|
|
190
|
-
for (const variable of this.schema.variables) {
|
|
191
|
-
items.push({
|
|
192
|
-
label: variable.name,
|
|
193
|
-
type: variable.type,
|
|
194
|
-
description: variable.description,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
for (const contract of this.schema.contracts) {
|
|
198
|
-
items.push({
|
|
199
|
-
label: contract.name,
|
|
200
|
-
type: contractTypeName(contract.name),
|
|
201
|
-
description: contract.description,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
for (const fn of this.schema.functions) {
|
|
205
|
-
if (!fn.receiverType) {
|
|
206
|
-
items.push({
|
|
207
|
-
label: fn.name,
|
|
208
|
-
type: fn.returns,
|
|
209
|
-
detail: fn.signature,
|
|
210
|
-
description: fn.description,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const expected = this.expectedTypeAt(expression, offset);
|
|
215
|
-
return { kind: "top-level", expectedType: expected?.expectedType, items };
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Infer the expected type at a cursor position from surrounding context.
|
|
219
|
-
*
|
|
220
|
-
* Supports two contexts:
|
|
221
|
-
* - **Operator**: `expr > |` — infers the left operand type and derives the
|
|
222
|
-
* expected right operand type from the operator.
|
|
223
|
-
* - **Function argument**: `contract.method(arg, |)` — looks up the
|
|
224
|
-
* parameter type from the schema.
|
|
225
|
-
*
|
|
226
|
-
* Returns undefined when context cannot be determined, signaling that
|
|
227
|
-
* no narrowing should occur (safe fallback).
|
|
228
|
-
*/
|
|
229
|
-
expectedTypeAt(expression, offset) {
|
|
230
|
-
const beforeCursor = expression.slice(0, offset);
|
|
231
|
-
// Phase A: Operator context
|
|
232
|
-
const trailing = this.findTrailingOperator(beforeCursor);
|
|
233
|
-
if (trailing) {
|
|
234
|
-
const expected = this.expectedTypeFor({
|
|
235
|
-
kind: "operator",
|
|
236
|
-
leftExpression: trailing.leftExpr,
|
|
237
|
-
operator: trailing.operator,
|
|
238
|
-
});
|
|
239
|
-
if (expected) {
|
|
240
|
-
return { expectedType: expected, context: "operator" };
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
// Phase B: Function argument context
|
|
244
|
-
const callInfo = this.findEnclosingCall(expression, offset);
|
|
245
|
-
if (callInfo) {
|
|
246
|
-
const expected = this.expectedTypeFor({
|
|
247
|
-
kind: "function-arg",
|
|
248
|
-
receiverName: callInfo.receiverName,
|
|
249
|
-
functionName: callInfo.functionName,
|
|
250
|
-
paramIndex: callInfo.paramIndex,
|
|
251
|
-
});
|
|
252
|
-
if (expected) {
|
|
253
|
-
return {
|
|
254
|
-
expectedType: expected,
|
|
255
|
-
context: "function-argument",
|
|
256
|
-
paramIndex: callInfo.paramIndex,
|
|
257
|
-
functionName: callInfo.functionName,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return undefined;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Resolve type and available members for a dot-access receiver expression.
|
|
265
|
-
*/
|
|
266
|
-
dotCompletions(receiverExpression) {
|
|
267
|
-
// Check direct contract name
|
|
268
|
-
const contract = this.schema.contracts.find((c) => c.name === receiverExpression);
|
|
269
|
-
if (contract) {
|
|
270
|
-
return {
|
|
271
|
-
kind: "dot-access",
|
|
272
|
-
receiverType: contractTypeName(contract.name),
|
|
273
|
-
items: contract.methods.map((method) => ({
|
|
274
|
-
label: method.name,
|
|
275
|
-
type: method.returns,
|
|
276
|
-
detail: `(${method.params.map((p) => `${p.name}: ${p.type}`).join(", ")}): ${method.returns}`,
|
|
277
|
-
description: method.description,
|
|
278
|
-
})),
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
// Type-check the receiver expression
|
|
282
|
-
const receiverType = this.typeOf(receiverExpression);
|
|
283
|
-
if (receiverType) {
|
|
284
|
-
// Check if it resolves to a contract type
|
|
285
|
-
const contractByType = this.schema.contracts.find((c) => contractTypeName(c.name) === receiverType);
|
|
286
|
-
if (contractByType) {
|
|
287
|
-
return {
|
|
288
|
-
kind: "dot-access",
|
|
289
|
-
receiverType,
|
|
290
|
-
items: contractByType.methods.map((m) => ({
|
|
291
|
-
label: m.name,
|
|
292
|
-
type: m.returns,
|
|
293
|
-
detail: `(${m.params.map((p) => `${p.name}: ${p.type}`).join(", ")}): ${m.returns}`,
|
|
294
|
-
description: m.description,
|
|
295
|
-
})),
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
// Check for receiver methods matching this type
|
|
299
|
-
const baseType = receiverType.includes("<")
|
|
300
|
-
? receiverType.slice(0, receiverType.indexOf("<"))
|
|
301
|
-
: null;
|
|
302
|
-
const receiverMethods = this.schema.functions
|
|
303
|
-
.filter((f) => f.receiverType === receiverType ||
|
|
304
|
-
(baseType !== null && f.receiverType === baseType))
|
|
305
|
-
.map((f) => ({
|
|
306
|
-
label: f.name,
|
|
307
|
-
type: f.returns,
|
|
308
|
-
detail: f.signature,
|
|
309
|
-
description: f.description,
|
|
310
|
-
}));
|
|
311
|
-
// Include list macros
|
|
312
|
-
if (baseType === "list" || receiverType === "list") {
|
|
313
|
-
const macroItems = this.schema.macros
|
|
314
|
-
.filter((m) => m.pattern.startsWith("list."))
|
|
315
|
-
.map((m) => ({
|
|
316
|
-
label: m.name,
|
|
317
|
-
type: "",
|
|
318
|
-
detail: m.pattern,
|
|
319
|
-
description: m.description,
|
|
320
|
-
}));
|
|
321
|
-
const seen = new Set(receiverMethods.map((m) => m.label));
|
|
322
|
-
for (const item of macroItems) {
|
|
323
|
-
if (!seen.has(item.label)) {
|
|
324
|
-
receiverMethods.push(item);
|
|
325
|
-
seen.add(item.label);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (receiverMethods.length > 0) {
|
|
330
|
-
return { kind: "dot-access", receiverType, items: receiverMethods };
|
|
331
|
-
}
|
|
332
|
-
// Check for struct field completions
|
|
333
|
-
const structItems = this.structFieldsFor(receiverType);
|
|
334
|
-
if (structItems) {
|
|
335
|
-
return { kind: "dot-access", receiverType, items: structItems };
|
|
336
|
-
}
|
|
337
|
-
// Known type but no methods found
|
|
338
|
-
return { kind: "dot-access", receiverType, items: [] };
|
|
339
|
-
}
|
|
340
|
-
return { kind: "dot-access", receiverType: receiverExpression, items: [] };
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Resolve expected type from a structured context.
|
|
344
|
-
*/
|
|
345
|
-
expectedTypeFor(context) {
|
|
346
|
-
if (context.kind === "operator") {
|
|
347
|
-
const leftType = this.typeOf(context.leftExpression);
|
|
348
|
-
if (leftType) {
|
|
349
|
-
return expectedTypeForOperator(leftType, context.operator) ?? undefined;
|
|
350
|
-
}
|
|
351
|
-
return undefined;
|
|
352
|
-
}
|
|
353
|
-
// function-arg
|
|
354
|
-
const rawType = this.resolveRawParamType(context.receiverName, context.functionName, context.paramIndex);
|
|
355
|
-
// Skip union types
|
|
356
|
-
if (rawType && !rawType.includes("|")) {
|
|
357
|
-
return rawType;
|
|
358
|
-
}
|
|
359
|
-
return undefined;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Rebuild the internal environment from an updated schema.
|
|
363
|
-
*/
|
|
364
|
-
updateSchema(schema) {
|
|
365
|
-
this.schema = schema;
|
|
366
|
-
this.env = createCheckerEnvironment(schema);
|
|
367
|
-
this.structTypeMap = this.buildStructTypeMap(schema);
|
|
368
|
-
}
|
|
369
|
-
buildStructTypeMap(schema) {
|
|
370
|
-
const map = new Map();
|
|
371
|
-
for (const type of schema.types) {
|
|
372
|
-
if (type.kind === "struct") {
|
|
373
|
-
map.set(type.name, type);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return map;
|
|
377
|
-
}
|
|
378
|
-
structFieldsFor(typeName) {
|
|
379
|
-
const structType = this.structTypeMap.get(typeName);
|
|
380
|
-
if (!structType) {
|
|
381
|
-
return undefined;
|
|
382
|
-
}
|
|
383
|
-
return (structType.fields ?? []).map((field) => ({
|
|
384
|
-
label: field.name,
|
|
385
|
-
type: field.type,
|
|
386
|
-
detail: field.type,
|
|
387
|
-
description: field.description,
|
|
388
|
-
}));
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Scan backwards from a dot position to extract the receiver expression.
|
|
392
|
-
* Handles balanced parentheses so that `foo(erc20.name().` correctly
|
|
393
|
-
* identifies `erc20.name()` as the receiver, not `foo(erc20.name()`.
|
|
394
|
-
*/
|
|
395
|
-
extractReceiverBefore(text, dotIdx) {
|
|
396
|
-
let i = dotIdx - 1;
|
|
397
|
-
while (i >= 0 && (text[i] === " " || text[i] === "\t")) {
|
|
398
|
-
i--;
|
|
399
|
-
}
|
|
400
|
-
if (i < 0) {
|
|
401
|
-
return "";
|
|
402
|
-
}
|
|
403
|
-
const isIdentStart = (code) => (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95;
|
|
404
|
-
const isIdentChar = (code) => isIdentStart(code) || (code >= 48 && code <= 57);
|
|
405
|
-
const collectChain = () => {
|
|
406
|
-
if (text[i] === ")") {
|
|
407
|
-
const closePos = i;
|
|
408
|
-
let depth = 1;
|
|
409
|
-
i--;
|
|
410
|
-
while (i >= 0 && depth > 0) {
|
|
411
|
-
const ch = text[i];
|
|
412
|
-
if (ch === ")") {
|
|
413
|
-
depth++;
|
|
414
|
-
}
|
|
415
|
-
else if (ch === "(") {
|
|
416
|
-
depth--;
|
|
417
|
-
}
|
|
418
|
-
if (depth > 0) {
|
|
419
|
-
i--;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
if (depth !== 0) {
|
|
423
|
-
i = closePos;
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
i--;
|
|
427
|
-
}
|
|
428
|
-
if (i >= 0 && isIdentChar(text.charCodeAt(i))) {
|
|
429
|
-
while (i >= 0 && isIdentChar(text.charCodeAt(i))) {
|
|
430
|
-
i--;
|
|
431
|
-
}
|
|
432
|
-
const start = i + 1;
|
|
433
|
-
if (!isIdentStart(text.charCodeAt(start))) {
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (i >= 0 && text[i] === ".") {
|
|
437
|
-
i--;
|
|
438
|
-
collectChain();
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
collectChain();
|
|
443
|
-
const start = i + 1;
|
|
444
|
-
return text.slice(start, dotIdx).trimEnd();
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Find the largest call/dot-access chain node that contains the offset.
|
|
448
|
-
*/
|
|
449
|
-
findContainingChain(root, offset) {
|
|
450
|
-
let best;
|
|
451
|
-
walkAST(root, (node) => {
|
|
452
|
-
if (node.op !== "." && node.op !== ".?" && node.op !== "rcall") {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
const span = nodeSpan(node);
|
|
456
|
-
if (offset >= span.from && offset <= span.to) {
|
|
457
|
-
if (!best ||
|
|
458
|
-
span.to - span.from > nodeSpan(best).to - nodeSpan(best).from) {
|
|
459
|
-
best = node;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
return best;
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Scan backwards from the end of text to find a trailing binary operator.
|
|
467
|
-
* Tries multi-character operators first (==, !=, etc.) to avoid partial matches.
|
|
468
|
-
*/
|
|
469
|
-
findTrailingOperator(text) {
|
|
470
|
-
const trimmed = text.trimEnd();
|
|
471
|
-
// Multi-character operators first
|
|
472
|
-
for (const op of ["==", "!=", ">=", "<=", "&&", "||", "in"]) {
|
|
473
|
-
if (trimmed.endsWith(op)) {
|
|
474
|
-
const left = trimmed.slice(0, -op.length).trimEnd();
|
|
475
|
-
if (left) {
|
|
476
|
-
return { leftExpr: left, operator: op };
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
// Single-character operators
|
|
481
|
-
for (const op of [">", "<", "+", "-", "*", "/", "%"]) {
|
|
482
|
-
if (trimmed.endsWith(op)) {
|
|
483
|
-
const before = trimmed.slice(0, -1);
|
|
484
|
-
// Guard against partial multi-char operators (>=, <=, ==, !=, &&, ||)
|
|
485
|
-
const lastChar = before.at(-1);
|
|
486
|
-
if (lastChar === ">" ||
|
|
487
|
-
lastChar === "<" ||
|
|
488
|
-
lastChar === "=" ||
|
|
489
|
-
lastChar === "!" ||
|
|
490
|
-
lastChar === "&" ||
|
|
491
|
-
lastChar === "|") {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
const left = before.trimEnd();
|
|
495
|
-
if (left) {
|
|
496
|
-
return { leftExpr: left, operator: op };
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return undefined;
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Find the enclosing function/method call around the cursor by scanning
|
|
504
|
-
* backwards for an unclosed `(`, then extracting the function name and
|
|
505
|
-
* counting commas to determine the parameter index.
|
|
506
|
-
*/
|
|
507
|
-
findEnclosingCall(expression, offset) {
|
|
508
|
-
let depth = 0;
|
|
509
|
-
let commas = 0;
|
|
510
|
-
let parenPos = -1;
|
|
511
|
-
for (let i = offset - 1; i >= 0; i--) {
|
|
512
|
-
const ch = expression[i];
|
|
513
|
-
if (ch === ")") {
|
|
514
|
-
depth++;
|
|
515
|
-
}
|
|
516
|
-
else if (ch === "(") {
|
|
517
|
-
if (depth === 0) {
|
|
518
|
-
parenPos = i;
|
|
519
|
-
break;
|
|
520
|
-
}
|
|
521
|
-
depth--;
|
|
522
|
-
}
|
|
523
|
-
else if (ch === "," && depth === 0) {
|
|
524
|
-
commas++;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
if (parenPos < 0) {
|
|
528
|
-
return undefined;
|
|
529
|
-
}
|
|
530
|
-
const beforeParen = expression.slice(0, parenPos);
|
|
531
|
-
const callMatch = /(?:(\w+)\.)?(\w+)\s*$/.exec(beforeParen);
|
|
532
|
-
if (!callMatch?.[2]) {
|
|
533
|
-
return undefined;
|
|
534
|
-
}
|
|
535
|
-
return {
|
|
536
|
-
receiverName: callMatch[1],
|
|
537
|
-
functionName: callMatch[2],
|
|
538
|
-
paramIndex: commas,
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Look up the expected parameter type from the schema for a function or
|
|
543
|
-
* method call at the given parameter index.
|
|
544
|
-
*/
|
|
545
|
-
resolveRawParamType(receiverName, functionName, paramIndex) {
|
|
546
|
-
if (receiverName) {
|
|
547
|
-
// Method call: receiver.method(...)
|
|
548
|
-
const contract = this.schema.contracts.find((c) => c.name === receiverName);
|
|
549
|
-
if (contract) {
|
|
550
|
-
const method = contract.methods.find((m) => m.name === functionName);
|
|
551
|
-
if (method && paramIndex < method.params.length) {
|
|
552
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounds checked above
|
|
553
|
-
return method.params[paramIndex].type;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
else {
|
|
558
|
-
// Free function: func(...)
|
|
559
|
-
const fn = this.schema.functions.find((f) => f.name === functionName);
|
|
560
|
-
if (fn && paramIndex < fn.params.length) {
|
|
561
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounds checked above
|
|
562
|
-
return fn.params[paramIndex].type;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return undefined;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { SELDiagnostic } from "./checker.js";
|
|
2
|
-
/**
|
|
3
|
-
* Convert a cel-js error into position-aware SEL diagnostics.
|
|
4
|
-
*
|
|
5
|
-
* The error's `name` property distinguishes parse errors (`"ParseError"`)
|
|
6
|
-
* from type errors (`"TypeError"`). Position information is extracted from
|
|
7
|
-
* the caret annotation embedded in the error message; when absent the
|
|
8
|
-
* diagnostic spans the entire expression.
|
|
9
|
-
*/
|
|
10
|
-
export declare const extractDiagnostics: (expression: string, error: unknown) => SELDiagnostic[];
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extract position information from a cel-js error message.
|
|
3
|
-
*
|
|
4
|
-
* cel-js error messages embed a caret (`^`) on a separate line indicating
|
|
5
|
-
* the error column. The format is:
|
|
6
|
-
* ```
|
|
7
|
-
* Error description
|
|
8
|
-
*
|
|
9
|
-
* > 1 | expression text
|
|
10
|
-
* ^
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* We parse the caret offset relative to the code line prefix (`> N | `)
|
|
14
|
-
* to derive the zero-based character offset within the expression.
|
|
15
|
-
*/
|
|
16
|
-
const extractPositionFromMessage = (message) => {
|
|
17
|
-
const lines = message.split("\n");
|
|
18
|
-
const caretLine = lines.find((line) => /^\s*\^+\s*$/.test(line));
|
|
19
|
-
if (!caretLine) {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
const codeLine = lines.find((line) => line.startsWith(">"));
|
|
23
|
-
if (!codeLine) {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
const pipeIndex = codeLine.indexOf("|");
|
|
27
|
-
if (pipeIndex === -1) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
// Prefix length includes the pipe and the space after it: "> 1 | "
|
|
31
|
-
const prefixLength = pipeIndex + 2;
|
|
32
|
-
const caretStart = caretLine.indexOf("^");
|
|
33
|
-
const caretEnd = caretLine.lastIndexOf("^");
|
|
34
|
-
const from = caretStart - prefixLength;
|
|
35
|
-
const to = caretEnd - prefixLength + 1;
|
|
36
|
-
if (from < 0) {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
return { from, to };
|
|
40
|
-
};
|
|
41
|
-
/**
|
|
42
|
-
* Extract the plain error message without the caret/code-line decoration.
|
|
43
|
-
*/
|
|
44
|
-
const extractPlainMessage = (message) => {
|
|
45
|
-
const lines = message.split("\n");
|
|
46
|
-
const plainLines = lines.filter((line) => !line.startsWith(">") &&
|
|
47
|
-
!/^\s*\^+\s*$/.test(line) &&
|
|
48
|
-
line.trim().length > 0);
|
|
49
|
-
return plainLines.join(" ").trim() || message;
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Convert a cel-js error into position-aware SEL diagnostics.
|
|
53
|
-
*
|
|
54
|
-
* The error's `name` property distinguishes parse errors (`"ParseError"`)
|
|
55
|
-
* from type errors (`"TypeError"`). Position information is extracted from
|
|
56
|
-
* the caret annotation embedded in the error message; when absent the
|
|
57
|
-
* diagnostic spans the entire expression.
|
|
58
|
-
*/
|
|
59
|
-
export const extractDiagnostics = (expression, error) => {
|
|
60
|
-
if (!(error instanceof Error)) {
|
|
61
|
-
return [
|
|
62
|
-
{
|
|
63
|
-
message: String(error),
|
|
64
|
-
severity: "error",
|
|
65
|
-
from: 0,
|
|
66
|
-
to: expression.length,
|
|
67
|
-
},
|
|
68
|
-
];
|
|
69
|
-
}
|
|
70
|
-
const position = extractPositionFromMessage(error.message);
|
|
71
|
-
const message = extractPlainMessage(error.message);
|
|
72
|
-
return [
|
|
73
|
-
{
|
|
74
|
-
message,
|
|
75
|
-
severity: "error",
|
|
76
|
-
from: position?.from ?? 0,
|
|
77
|
-
to: position?.to ?? expression.length,
|
|
78
|
-
},
|
|
79
|
-
];
|
|
80
|
-
};
|
package/dist/checker/index.d.ts
DELETED
package/dist/checker/index.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Given the type of the left operand and the operator,
|
|
3
|
-
* returns the expected type for the right operand.
|
|
4
|
-
*
|
|
5
|
-
* Based on the registered operators in register-types.ts.
|
|
6
|
-
* Type compatibility is strictly same-type (no implicit coercion).
|
|
7
|
-
*
|
|
8
|
-
* Returns undefined when the type/operator combination is unknown,
|
|
9
|
-
* signaling that no narrowing should occur.
|
|
10
|
-
*/
|
|
11
|
-
export declare const expectedTypeForOperator: (leftType: string, operator: string) => string | undefined;
|
|
12
|
-
/**
|
|
13
|
-
* Check if a candidate type is compatible with an expected type.
|
|
14
|
-
* "dyn" is a wildcard — compatible with anything in either direction.
|
|
15
|
-
*/
|
|
16
|
-
export declare const isTypeCompatible: (candidateType: string, expectedType: string) => boolean;
|