@seljs/checker 1.0.0 → 1.0.1-beta.9
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/_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 +23 -16
- 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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
exports.__toESM = __toESM;
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_diagnostics = require("./diagnostics.cjs");
|
|
3
|
+
const require_type_compatibility = require("./type-compatibility.cjs");
|
|
4
|
+
const require_hydrate = require("../environment/hydrate.cjs");
|
|
5
|
+
const require_ast_utils = require("../utils/ast-utils.cjs");
|
|
6
|
+
const require_runner = require("../rules/runner.cjs");
|
|
7
|
+
require("../rules/index.cjs");
|
|
8
|
+
let _seljs_common = require("@seljs/common");
|
|
9
|
+
//#region src/checker/checker.ts
|
|
10
|
+
/**
|
|
11
|
+
* SEL expression checker.
|
|
12
|
+
*
|
|
13
|
+
* Wraps a hydrated cel-js Environment built from a SELSchema and exposes
|
|
14
|
+
* parse/type-check, type inference, cursor-position type lookups, and
|
|
15
|
+
* type-aware completions.
|
|
16
|
+
*/
|
|
17
|
+
var SELChecker = class {
|
|
18
|
+
rules;
|
|
19
|
+
env;
|
|
20
|
+
schema;
|
|
21
|
+
structTypeMap;
|
|
22
|
+
constructor(schema, options) {
|
|
23
|
+
this.schema = schema;
|
|
24
|
+
this.env = require_hydrate.createCheckerEnvironment(schema);
|
|
25
|
+
this.rules = options?.rules ?? [];
|
|
26
|
+
this.structTypeMap = this.buildStructTypeMap(schema);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse and type-check an expression, returning position-aware diagnostics.
|
|
30
|
+
*
|
|
31
|
+
* Uses a single `env.parse()` call, then `.check()` on the result
|
|
32
|
+
* to avoid double-parsing. Rules run against the AST from the same parse:
|
|
33
|
+
* - Structural rules run after successful parse (even if type-check fails)
|
|
34
|
+
* - Type-aware rules run only after both parse and type-check succeed
|
|
35
|
+
*/
|
|
36
|
+
check(expression) {
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = this.env.parse(expression);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
diagnostics: require_diagnostics.extractDiagnostics(expression, error)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const structuralDiags = this.rules.length > 0 ? require_runner.runRules({
|
|
47
|
+
expression,
|
|
48
|
+
ast: parsed.ast,
|
|
49
|
+
schema: this.schema,
|
|
50
|
+
rules: this.rules,
|
|
51
|
+
tier: "structural"
|
|
52
|
+
}) : [];
|
|
53
|
+
let typeResult;
|
|
54
|
+
try {
|
|
55
|
+
typeResult = parsed.check();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
diagnostics: [...require_diagnostics.extractDiagnostics(expression, error), ...structuralDiags]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!typeResult.valid) return {
|
|
63
|
+
valid: false,
|
|
64
|
+
diagnostics: [...typeResult.error ? require_diagnostics.extractDiagnostics(expression, typeResult.error) : [{
|
|
65
|
+
message: "Type check failed",
|
|
66
|
+
severity: "error",
|
|
67
|
+
from: 0,
|
|
68
|
+
to: expression.length
|
|
69
|
+
}], ...structuralDiags]
|
|
70
|
+
};
|
|
71
|
+
const typeAwareDiags = this.rules.length > 0 ? require_runner.runRules({
|
|
72
|
+
expression,
|
|
73
|
+
ast: parsed.ast,
|
|
74
|
+
schema: this.schema,
|
|
75
|
+
rules: this.rules,
|
|
76
|
+
tier: "type-aware",
|
|
77
|
+
resolvedType: typeResult.type
|
|
78
|
+
}) : [];
|
|
79
|
+
const allRuleDiags = [...structuralDiags, ...typeAwareDiags];
|
|
80
|
+
return {
|
|
81
|
+
valid: !allRuleDiags.some((d) => d.severity === "error"),
|
|
82
|
+
type: typeResult.type,
|
|
83
|
+
diagnostics: allRuleDiags
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the inferred CEL type of the full expression.
|
|
88
|
+
* Returns undefined if the expression is invalid.
|
|
89
|
+
*/
|
|
90
|
+
typeOf(expression) {
|
|
91
|
+
return this.check(expression).type;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get the inferred type at a cursor position (for hover info).
|
|
95
|
+
*
|
|
96
|
+
* Attempts to identify the sub-expression under the cursor and infer its
|
|
97
|
+
* type. Falls back to the full expression type when sub-expression
|
|
98
|
+
* isolation is not possible.
|
|
99
|
+
*/
|
|
100
|
+
typeAt(expression, offset) {
|
|
101
|
+
if (offset < 0 || offset > expression.length) return;
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = this.env.parse(expression);
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const hit = require_ast_utils.findNodeWithParentAt(parsed.ast, offset);
|
|
109
|
+
if (!hit) {
|
|
110
|
+
const fullType = this.typeOf(expression);
|
|
111
|
+
return fullType ? {
|
|
112
|
+
type: fullType,
|
|
113
|
+
from: 0,
|
|
114
|
+
to: expression.length
|
|
115
|
+
} : void 0;
|
|
116
|
+
}
|
|
117
|
+
const { node } = hit;
|
|
118
|
+
if (node.op === "id") {
|
|
119
|
+
const name = node.args;
|
|
120
|
+
const span = require_ast_utils.nodeSpan(node);
|
|
121
|
+
const variable = this.schema.variables.find((v) => v.name === name);
|
|
122
|
+
if (variable) return {
|
|
123
|
+
type: variable.type,
|
|
124
|
+
from: span.from,
|
|
125
|
+
to: span.to
|
|
126
|
+
};
|
|
127
|
+
const contract = this.schema.contracts.find((c) => c.name === name);
|
|
128
|
+
if (contract) return {
|
|
129
|
+
type: (0, _seljs_common.contractTypeName)(contract.name),
|
|
130
|
+
from: span.from,
|
|
131
|
+
to: span.to
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const chainNode = this.findContainingChain(parsed.ast, offset);
|
|
135
|
+
if (chainNode) {
|
|
136
|
+
const chainSpan = require_ast_utils.nodeSpan(chainNode);
|
|
137
|
+
const chainExpr = expression.slice(chainSpan.from, chainSpan.to);
|
|
138
|
+
const chainType = this.typeOf(chainExpr);
|
|
139
|
+
if (chainType) return {
|
|
140
|
+
type: chainType,
|
|
141
|
+
from: chainSpan.from,
|
|
142
|
+
to: chainSpan.to
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const fullType = this.typeOf(expression);
|
|
146
|
+
return fullType ? {
|
|
147
|
+
type: fullType,
|
|
148
|
+
from: 0,
|
|
149
|
+
to: expression.length
|
|
150
|
+
} : void 0;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Get type-aware completions for a cursor context.
|
|
154
|
+
*
|
|
155
|
+
* Supports:
|
|
156
|
+
* - **dot-access**: after `contract.` — lists methods of that contract
|
|
157
|
+
* - **top-level**: at the start or after operators — lists variables,
|
|
158
|
+
* contracts, and functions
|
|
159
|
+
*/
|
|
160
|
+
completionsAt(expression, offset) {
|
|
161
|
+
const beforeCursor = expression.slice(0, offset);
|
|
162
|
+
const lastDotIdx = beforeCursor.lastIndexOf(".");
|
|
163
|
+
if (lastDotIdx > 0) {
|
|
164
|
+
const receiverExpr = this.extractReceiverBefore(beforeCursor, lastDotIdx);
|
|
165
|
+
if (receiverExpr) return this.dotCompletions(receiverExpr);
|
|
166
|
+
}
|
|
167
|
+
const items = [];
|
|
168
|
+
for (const variable of this.schema.variables) items.push({
|
|
169
|
+
label: variable.name,
|
|
170
|
+
type: variable.type,
|
|
171
|
+
description: variable.description
|
|
172
|
+
});
|
|
173
|
+
for (const contract of this.schema.contracts) items.push({
|
|
174
|
+
label: contract.name,
|
|
175
|
+
type: (0, _seljs_common.contractTypeName)(contract.name),
|
|
176
|
+
description: contract.description
|
|
177
|
+
});
|
|
178
|
+
for (const fn of this.schema.functions) if (!fn.receiverType) items.push({
|
|
179
|
+
label: fn.name,
|
|
180
|
+
type: fn.returns,
|
|
181
|
+
detail: fn.signature,
|
|
182
|
+
description: fn.description
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
kind: "top-level",
|
|
186
|
+
expectedType: this.expectedTypeAt(expression, offset)?.expectedType,
|
|
187
|
+
items
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Infer the expected type at a cursor position from surrounding context.
|
|
192
|
+
*
|
|
193
|
+
* Supports two contexts:
|
|
194
|
+
* - **Operator**: `expr > |` — infers the left operand type and derives the
|
|
195
|
+
* expected right operand type from the operator.
|
|
196
|
+
* - **Function argument**: `contract.method(arg, |)` — looks up the
|
|
197
|
+
* parameter type from the schema.
|
|
198
|
+
*
|
|
199
|
+
* Returns undefined when context cannot be determined, signaling that
|
|
200
|
+
* no narrowing should occur (safe fallback).
|
|
201
|
+
*/
|
|
202
|
+
expectedTypeAt(expression, offset) {
|
|
203
|
+
const beforeCursor = expression.slice(0, offset);
|
|
204
|
+
const trailing = this.findTrailingOperator(beforeCursor);
|
|
205
|
+
if (trailing) {
|
|
206
|
+
const expected = this.expectedTypeFor({
|
|
207
|
+
kind: "operator",
|
|
208
|
+
leftExpression: trailing.leftExpr,
|
|
209
|
+
operator: trailing.operator
|
|
210
|
+
});
|
|
211
|
+
if (expected) return {
|
|
212
|
+
expectedType: expected,
|
|
213
|
+
context: "operator"
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const callInfo = this.findEnclosingCall(expression, offset);
|
|
217
|
+
if (callInfo) {
|
|
218
|
+
const expected = this.expectedTypeFor({
|
|
219
|
+
kind: "function-arg",
|
|
220
|
+
receiverName: callInfo.receiverName,
|
|
221
|
+
functionName: callInfo.functionName,
|
|
222
|
+
paramIndex: callInfo.paramIndex
|
|
223
|
+
});
|
|
224
|
+
if (expected) return {
|
|
225
|
+
expectedType: expected,
|
|
226
|
+
context: "function-argument",
|
|
227
|
+
paramIndex: callInfo.paramIndex,
|
|
228
|
+
functionName: callInfo.functionName
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Resolve type and available members for a dot-access receiver expression.
|
|
234
|
+
*/
|
|
235
|
+
dotCompletions(receiverExpression) {
|
|
236
|
+
const contract = this.schema.contracts.find((c) => c.name === receiverExpression);
|
|
237
|
+
if (contract) return {
|
|
238
|
+
kind: "dot-access",
|
|
239
|
+
receiverType: (0, _seljs_common.contractTypeName)(contract.name),
|
|
240
|
+
items: contract.methods.map((method) => ({
|
|
241
|
+
label: method.name,
|
|
242
|
+
type: method.returns,
|
|
243
|
+
detail: `(${method.params.map((p) => `${p.name}: ${p.type}`).join(", ")}): ${method.returns}`,
|
|
244
|
+
description: method.description
|
|
245
|
+
}))
|
|
246
|
+
};
|
|
247
|
+
const receiverType = this.typeOf(receiverExpression);
|
|
248
|
+
if (receiverType) {
|
|
249
|
+
const contractByType = this.schema.contracts.find((c) => (0, _seljs_common.contractTypeName)(c.name) === receiverType);
|
|
250
|
+
if (contractByType) return {
|
|
251
|
+
kind: "dot-access",
|
|
252
|
+
receiverType,
|
|
253
|
+
items: contractByType.methods.map((m) => ({
|
|
254
|
+
label: m.name,
|
|
255
|
+
type: m.returns,
|
|
256
|
+
detail: `(${m.params.map((p) => `${p.name}: ${p.type}`).join(", ")}): ${m.returns}`,
|
|
257
|
+
description: m.description
|
|
258
|
+
}))
|
|
259
|
+
};
|
|
260
|
+
const baseType = receiverType.includes("<") ? receiverType.slice(0, receiverType.indexOf("<")) : null;
|
|
261
|
+
const receiverMethods = this.schema.functions.filter((f) => f.receiverType === receiverType || baseType !== null && f.receiverType === baseType).map((f) => ({
|
|
262
|
+
label: f.name,
|
|
263
|
+
type: f.returns,
|
|
264
|
+
detail: f.signature,
|
|
265
|
+
description: f.description
|
|
266
|
+
}));
|
|
267
|
+
if (baseType === "list" || receiverType === "list") {
|
|
268
|
+
const macroItems = this.schema.macros.filter((m) => m.pattern.startsWith("list.")).map((m) => ({
|
|
269
|
+
label: m.name,
|
|
270
|
+
type: "",
|
|
271
|
+
detail: m.pattern,
|
|
272
|
+
description: m.description
|
|
273
|
+
}));
|
|
274
|
+
const seen = new Set(receiverMethods.map((m) => m.label));
|
|
275
|
+
for (const item of macroItems) if (!seen.has(item.label)) {
|
|
276
|
+
receiverMethods.push(item);
|
|
277
|
+
seen.add(item.label);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (receiverMethods.length > 0) return {
|
|
281
|
+
kind: "dot-access",
|
|
282
|
+
receiverType,
|
|
283
|
+
items: receiverMethods
|
|
284
|
+
};
|
|
285
|
+
const structItems = this.structFieldsFor(receiverType);
|
|
286
|
+
if (structItems) return {
|
|
287
|
+
kind: "dot-access",
|
|
288
|
+
receiverType,
|
|
289
|
+
items: structItems
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
kind: "dot-access",
|
|
293
|
+
receiverType,
|
|
294
|
+
items: []
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
kind: "dot-access",
|
|
299
|
+
receiverType: receiverExpression,
|
|
300
|
+
items: []
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Resolve expected type from a structured context.
|
|
305
|
+
*/
|
|
306
|
+
expectedTypeFor(context) {
|
|
307
|
+
if (context.kind === "operator") {
|
|
308
|
+
const leftType = this.typeOf(context.leftExpression);
|
|
309
|
+
if (leftType) return require_type_compatibility.expectedTypeForOperator(leftType, context.operator) ?? void 0;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const rawType = this.resolveRawParamType(context.receiverName, context.functionName, context.paramIndex);
|
|
313
|
+
if (rawType && !rawType.includes("|")) return rawType;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Rebuild the internal environment from an updated schema.
|
|
317
|
+
*/
|
|
318
|
+
updateSchema(schema) {
|
|
319
|
+
this.schema = schema;
|
|
320
|
+
this.env = require_hydrate.createCheckerEnvironment(schema);
|
|
321
|
+
this.structTypeMap = this.buildStructTypeMap(schema);
|
|
322
|
+
}
|
|
323
|
+
buildStructTypeMap(schema) {
|
|
324
|
+
const map = /* @__PURE__ */ new Map();
|
|
325
|
+
for (const type of schema.types) if (type.kind === "struct") map.set(type.name, type);
|
|
326
|
+
return map;
|
|
327
|
+
}
|
|
328
|
+
structFieldsFor(typeName) {
|
|
329
|
+
const structType = this.structTypeMap.get(typeName);
|
|
330
|
+
if (!structType) return;
|
|
331
|
+
return (structType.fields ?? []).map((field) => ({
|
|
332
|
+
label: field.name,
|
|
333
|
+
type: field.type,
|
|
334
|
+
detail: field.type,
|
|
335
|
+
description: field.description
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Scan backwards from a dot position to extract the receiver expression.
|
|
340
|
+
* Handles balanced parentheses so that `foo(erc20.name().` correctly
|
|
341
|
+
* identifies `erc20.name()` as the receiver, not `foo(erc20.name()`.
|
|
342
|
+
*/
|
|
343
|
+
extractReceiverBefore(text, dotIdx) {
|
|
344
|
+
let i = dotIdx - 1;
|
|
345
|
+
while (i >= 0 && (text[i] === " " || text[i] === " ")) i--;
|
|
346
|
+
if (i < 0) return "";
|
|
347
|
+
const isIdentStart = (code) => code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95;
|
|
348
|
+
const isIdentChar = (code) => isIdentStart(code) || code >= 48 && code <= 57;
|
|
349
|
+
const collectChain = () => {
|
|
350
|
+
if (text[i] === ")") {
|
|
351
|
+
const closePos = i;
|
|
352
|
+
let depth = 1;
|
|
353
|
+
i--;
|
|
354
|
+
while (i >= 0 && depth > 0) {
|
|
355
|
+
const ch = text[i];
|
|
356
|
+
if (ch === ")") depth++;
|
|
357
|
+
else if (ch === "(") depth--;
|
|
358
|
+
if (depth > 0) i--;
|
|
359
|
+
}
|
|
360
|
+
if (depth !== 0) {
|
|
361
|
+
i = closePos;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
i--;
|
|
365
|
+
}
|
|
366
|
+
if (i >= 0 && isIdentChar(text.charCodeAt(i))) {
|
|
367
|
+
while (i >= 0 && isIdentChar(text.charCodeAt(i))) i--;
|
|
368
|
+
const start = i + 1;
|
|
369
|
+
if (!isIdentStart(text.charCodeAt(start))) return;
|
|
370
|
+
if (i >= 0 && text[i] === ".") {
|
|
371
|
+
i--;
|
|
372
|
+
collectChain();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
collectChain();
|
|
377
|
+
const start = i + 1;
|
|
378
|
+
return text.slice(start, dotIdx).trimEnd();
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Find the largest call/dot-access chain node that contains the offset.
|
|
382
|
+
*/
|
|
383
|
+
findContainingChain(root, offset) {
|
|
384
|
+
let best;
|
|
385
|
+
require_ast_utils.walkAST(root, (node) => {
|
|
386
|
+
if (node.op !== "." && node.op !== ".?" && node.op !== "rcall") return;
|
|
387
|
+
const span = require_ast_utils.nodeSpan(node);
|
|
388
|
+
if (offset >= span.from && offset <= span.to) {
|
|
389
|
+
if (!best || span.to - span.from > require_ast_utils.nodeSpan(best).to - require_ast_utils.nodeSpan(best).from) best = node;
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
return best;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Scan backwards from the end of text to find a trailing binary operator.
|
|
396
|
+
* Tries multi-character operators first (==, !=, etc.) to avoid partial matches.
|
|
397
|
+
*/
|
|
398
|
+
findTrailingOperator(text) {
|
|
399
|
+
const trimmed = text.trimEnd();
|
|
400
|
+
for (const op of [
|
|
401
|
+
"==",
|
|
402
|
+
"!=",
|
|
403
|
+
">=",
|
|
404
|
+
"<=",
|
|
405
|
+
"&&",
|
|
406
|
+
"||",
|
|
407
|
+
"in"
|
|
408
|
+
]) if (trimmed.endsWith(op)) {
|
|
409
|
+
const left = trimmed.slice(0, -op.length).trimEnd();
|
|
410
|
+
if (left) return {
|
|
411
|
+
leftExpr: left,
|
|
412
|
+
operator: op
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
for (const op of [
|
|
416
|
+
">",
|
|
417
|
+
"<",
|
|
418
|
+
"+",
|
|
419
|
+
"-",
|
|
420
|
+
"*",
|
|
421
|
+
"/",
|
|
422
|
+
"%"
|
|
423
|
+
]) if (trimmed.endsWith(op)) {
|
|
424
|
+
const before = trimmed.slice(0, -1);
|
|
425
|
+
const lastChar = before.at(-1);
|
|
426
|
+
if (lastChar === ">" || lastChar === "<" || lastChar === "=" || lastChar === "!" || lastChar === "&" || lastChar === "|") continue;
|
|
427
|
+
const left = before.trimEnd();
|
|
428
|
+
if (left) return {
|
|
429
|
+
leftExpr: left,
|
|
430
|
+
operator: op
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Find the enclosing function/method call around the cursor by scanning
|
|
436
|
+
* backwards for an unclosed `(`, then extracting the function name and
|
|
437
|
+
* counting commas to determine the parameter index.
|
|
438
|
+
*/
|
|
439
|
+
findEnclosingCall(expression, offset) {
|
|
440
|
+
let depth = 0;
|
|
441
|
+
let commas = 0;
|
|
442
|
+
let parenPos = -1;
|
|
443
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
444
|
+
const ch = expression[i];
|
|
445
|
+
if (ch === ")") depth++;
|
|
446
|
+
else if (ch === "(") {
|
|
447
|
+
if (depth === 0) {
|
|
448
|
+
parenPos = i;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
depth--;
|
|
452
|
+
} else if (ch === "," && depth === 0) commas++;
|
|
453
|
+
}
|
|
454
|
+
if (parenPos < 0) return;
|
|
455
|
+
const beforeParen = expression.slice(0, parenPos);
|
|
456
|
+
const callMatch = /(?:(\w+)\.)?(\w+)\s*$/.exec(beforeParen);
|
|
457
|
+
if (!callMatch?.[2]) return;
|
|
458
|
+
return {
|
|
459
|
+
receiverName: callMatch[1],
|
|
460
|
+
functionName: callMatch[2],
|
|
461
|
+
paramIndex: commas
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Look up the expected parameter type from the schema for a function or
|
|
466
|
+
* method call at the given parameter index.
|
|
467
|
+
*/
|
|
468
|
+
resolveRawParamType(receiverName, functionName, paramIndex) {
|
|
469
|
+
if (receiverName) {
|
|
470
|
+
const contract = this.schema.contracts.find((c) => c.name === receiverName);
|
|
471
|
+
if (contract) {
|
|
472
|
+
const method = contract.methods.find((m) => m.name === functionName);
|
|
473
|
+
if (method && paramIndex < method.params.length) return method.params[paramIndex].type;
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const fn = this.schema.functions.find((f) => f.name === functionName);
|
|
477
|
+
if (fn && paramIndex < fn.params.length) return fn.params[paramIndex].type;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
//#endregion
|
|
482
|
+
exports.SELChecker = SELChecker;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { SELRule } from "../rules/types.cjs";
|
|
2
|
+
import { SELSchema } from "@seljs/schema";
|
|
3
|
+
|
|
4
|
+
//#region src/checker/checker.d.ts
|
|
5
|
+
interface SELCheckResult {
|
|
6
|
+
valid: boolean;
|
|
7
|
+
type?: string;
|
|
8
|
+
diagnostics: SELDiagnostic[];
|
|
9
|
+
}
|
|
10
|
+
interface TypeAtResult {
|
|
11
|
+
type: string;
|
|
12
|
+
from: number;
|
|
13
|
+
to: number;
|
|
14
|
+
}
|
|
15
|
+
interface ExpectedTypeInfo {
|
|
16
|
+
/**
|
|
17
|
+
* The CEL type expected at this position.
|
|
18
|
+
*/
|
|
19
|
+
expectedType: string;
|
|
20
|
+
/**
|
|
21
|
+
* How the expectation was inferred.
|
|
22
|
+
*/
|
|
23
|
+
context: "operator" | "function-argument";
|
|
24
|
+
/**
|
|
25
|
+
* For function args: which parameter index.
|
|
26
|
+
*/
|
|
27
|
+
paramIndex?: number;
|
|
28
|
+
/**
|
|
29
|
+
* For function args: the function/method name.
|
|
30
|
+
*/
|
|
31
|
+
functionName?: string;
|
|
32
|
+
}
|
|
33
|
+
interface CompletionInfo {
|
|
34
|
+
kind: "top-level" | "dot-access" | "explicit";
|
|
35
|
+
receiverType?: string;
|
|
36
|
+
items: CompletionItem[];
|
|
37
|
+
/**
|
|
38
|
+
* Inferred expected type at cursor, if determinable.
|
|
39
|
+
*/
|
|
40
|
+
expectedType?: string;
|
|
41
|
+
}
|
|
42
|
+
interface CompletionItem {
|
|
43
|
+
label: string;
|
|
44
|
+
type: string;
|
|
45
|
+
detail?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A diagnostic message with optional position info. Used for both parse/type errors
|
|
50
|
+
* and rule violations. Positions are optional because some errors (e.g. from cel-js)
|
|
51
|
+
* may not include reliable spans, and rules may choose to report non-positional issues.
|
|
52
|
+
*/
|
|
53
|
+
interface SELDiagnostic {
|
|
54
|
+
message: string;
|
|
55
|
+
severity: "error" | "warning" | "info";
|
|
56
|
+
from?: number;
|
|
57
|
+
to?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Options for configuring the SELChecker.
|
|
61
|
+
*/
|
|
62
|
+
interface SELCheckerOptions {
|
|
63
|
+
/**
|
|
64
|
+
* Lint rules to enable. Defaults to none (backward compatible).
|
|
65
|
+
*/
|
|
66
|
+
rules?: readonly SELRule[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* SEL expression checker.
|
|
70
|
+
*
|
|
71
|
+
* Wraps a hydrated cel-js Environment built from a SELSchema and exposes
|
|
72
|
+
* parse/type-check, type inference, cursor-position type lookups, and
|
|
73
|
+
* type-aware completions.
|
|
74
|
+
*/
|
|
75
|
+
declare class SELChecker {
|
|
76
|
+
private readonly rules;
|
|
77
|
+
private env;
|
|
78
|
+
private schema;
|
|
79
|
+
private structTypeMap;
|
|
80
|
+
constructor(schema: SELSchema, options?: SELCheckerOptions);
|
|
81
|
+
/**
|
|
82
|
+
* Parse and type-check an expression, returning position-aware diagnostics.
|
|
83
|
+
*
|
|
84
|
+
* Uses a single `env.parse()` call, then `.check()` on the result
|
|
85
|
+
* to avoid double-parsing. Rules run against the AST from the same parse:
|
|
86
|
+
* - Structural rules run after successful parse (even if type-check fails)
|
|
87
|
+
* - Type-aware rules run only after both parse and type-check succeed
|
|
88
|
+
*/
|
|
89
|
+
check(expression: string): SELCheckResult;
|
|
90
|
+
/**
|
|
91
|
+
* Get the inferred CEL type of the full expression.
|
|
92
|
+
* Returns undefined if the expression is invalid.
|
|
93
|
+
*/
|
|
94
|
+
typeOf(expression: string): string | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Get the inferred type at a cursor position (for hover info).
|
|
97
|
+
*
|
|
98
|
+
* Attempts to identify the sub-expression under the cursor and infer its
|
|
99
|
+
* type. Falls back to the full expression type when sub-expression
|
|
100
|
+
* isolation is not possible.
|
|
101
|
+
*/
|
|
102
|
+
typeAt(expression: string, offset: number): TypeAtResult | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Get type-aware completions for a cursor context.
|
|
105
|
+
*
|
|
106
|
+
* Supports:
|
|
107
|
+
* - **dot-access**: after `contract.` — lists methods of that contract
|
|
108
|
+
* - **top-level**: at the start or after operators — lists variables,
|
|
109
|
+
* contracts, and functions
|
|
110
|
+
*/
|
|
111
|
+
completionsAt(expression: string, offset: number): CompletionInfo;
|
|
112
|
+
/**
|
|
113
|
+
* Infer the expected type at a cursor position from surrounding context.
|
|
114
|
+
*
|
|
115
|
+
* Supports two contexts:
|
|
116
|
+
* - **Operator**: `expr > |` — infers the left operand type and derives the
|
|
117
|
+
* expected right operand type from the operator.
|
|
118
|
+
* - **Function argument**: `contract.method(arg, |)` — looks up the
|
|
119
|
+
* parameter type from the schema.
|
|
120
|
+
*
|
|
121
|
+
* Returns undefined when context cannot be determined, signaling that
|
|
122
|
+
* no narrowing should occur (safe fallback).
|
|
123
|
+
*/
|
|
124
|
+
expectedTypeAt(expression: string, offset: number): ExpectedTypeInfo | undefined;
|
|
125
|
+
/**
|
|
126
|
+
* Resolve type and available members for a dot-access receiver expression.
|
|
127
|
+
*/
|
|
128
|
+
dotCompletions(receiverExpression: string): CompletionInfo;
|
|
129
|
+
/**
|
|
130
|
+
* Resolve expected type from a structured context.
|
|
131
|
+
*/
|
|
132
|
+
expectedTypeFor(context: {
|
|
133
|
+
kind: "operator";
|
|
134
|
+
leftExpression: string;
|
|
135
|
+
operator: string;
|
|
136
|
+
} | {
|
|
137
|
+
kind: "function-arg";
|
|
138
|
+
receiverName?: string;
|
|
139
|
+
functionName: string;
|
|
140
|
+
paramIndex: number;
|
|
141
|
+
}): string | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Rebuild the internal environment from an updated schema.
|
|
144
|
+
*/
|
|
145
|
+
updateSchema(schema: SELSchema): void;
|
|
146
|
+
private buildStructTypeMap;
|
|
147
|
+
private structFieldsFor;
|
|
148
|
+
/**
|
|
149
|
+
* Scan backwards from a dot position to extract the receiver expression.
|
|
150
|
+
* Handles balanced parentheses so that `foo(erc20.name().` correctly
|
|
151
|
+
* identifies `erc20.name()` as the receiver, not `foo(erc20.name()`.
|
|
152
|
+
*/
|
|
153
|
+
private extractReceiverBefore;
|
|
154
|
+
/**
|
|
155
|
+
* Find the largest call/dot-access chain node that contains the offset.
|
|
156
|
+
*/
|
|
157
|
+
private findContainingChain;
|
|
158
|
+
/**
|
|
159
|
+
* Scan backwards from the end of text to find a trailing binary operator.
|
|
160
|
+
* Tries multi-character operators first (==, !=, etc.) to avoid partial matches.
|
|
161
|
+
*/
|
|
162
|
+
private findTrailingOperator;
|
|
163
|
+
/**
|
|
164
|
+
* Find the enclosing function/method call around the cursor by scanning
|
|
165
|
+
* backwards for an unclosed `(`, then extracting the function name and
|
|
166
|
+
* counting commas to determine the parameter index.
|
|
167
|
+
*/
|
|
168
|
+
private findEnclosingCall;
|
|
169
|
+
/**
|
|
170
|
+
* Look up the expected parameter type from the schema for a function or
|
|
171
|
+
* method call at the given parameter index.
|
|
172
|
+
*/
|
|
173
|
+
private resolveRawParamType;
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
export { CompletionItem, SELChecker, SELCheckerOptions, SELDiagnostic };
|