@seljs/checker 1.0.0

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 (54) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE.md +190 -0
  3. package/README.md +5 -0
  4. package/dist/checker/checker.d.ts +173 -0
  5. package/dist/checker/checker.js +567 -0
  6. package/dist/checker/diagnostics.d.ts +10 -0
  7. package/dist/checker/diagnostics.js +80 -0
  8. package/dist/checker/index.d.ts +2 -0
  9. package/dist/checker/index.js +2 -0
  10. package/dist/checker/type-compatibility.d.ts +16 -0
  11. package/dist/checker/type-compatibility.js +59 -0
  12. package/dist/constants.d.ts +4 -0
  13. package/dist/constants.js +10 -0
  14. package/dist/debug.d.ts +2 -0
  15. package/dist/debug.js +2 -0
  16. package/dist/environment/codec-registry.d.ts +42 -0
  17. package/dist/environment/codec-registry.js +146 -0
  18. package/dist/environment/hydrate.d.ts +48 -0
  19. package/dist/environment/hydrate.js +198 -0
  20. package/dist/environment/index.d.ts +4 -0
  21. package/dist/environment/index.js +4 -0
  22. package/dist/environment/register-types.d.ts +14 -0
  23. package/dist/environment/register-types.js +154 -0
  24. package/dist/environment/value-wrappers.d.ts +17 -0
  25. package/dist/environment/value-wrappers.js +65 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.js +4 -0
  28. package/dist/rules/defaults/deferred-call.d.ts +13 -0
  29. package/dist/rules/defaults/deferred-call.js +162 -0
  30. package/dist/rules/defaults/index.d.ts +6 -0
  31. package/dist/rules/defaults/index.js +6 -0
  32. package/dist/rules/defaults/no-constant-condition.d.ts +7 -0
  33. package/dist/rules/defaults/no-constant-condition.js +36 -0
  34. package/dist/rules/defaults/no-mixed-operators.d.ts +9 -0
  35. package/dist/rules/defaults/no-mixed-operators.js +44 -0
  36. package/dist/rules/defaults/no-redundant-bool.d.ts +5 -0
  37. package/dist/rules/defaults/no-redundant-bool.js +27 -0
  38. package/dist/rules/defaults/no-self-comparison.d.ts +9 -0
  39. package/dist/rules/defaults/no-self-comparison.js +31 -0
  40. package/dist/rules/defaults/require-type.d.ts +7 -0
  41. package/dist/rules/defaults/require-type.js +19 -0
  42. package/dist/rules/facade.d.ts +22 -0
  43. package/dist/rules/facade.js +29 -0
  44. package/dist/rules/index.d.ts +3 -0
  45. package/dist/rules/index.js +3 -0
  46. package/dist/rules/runner.d.ts +16 -0
  47. package/dist/rules/runner.js +30 -0
  48. package/dist/rules/types.d.ts +73 -0
  49. package/dist/rules/types.js +1 -0
  50. package/dist/utils/ast-utils.d.ts +55 -0
  51. package/dist/utils/ast-utils.js +255 -0
  52. package/dist/utils/index.d.ts +1 -0
  53. package/dist/utils/index.js +1 -0
  54. package/package.json +70 -0
@@ -0,0 +1,567 @@
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
+ }
@@ -0,0 +1,10 @@
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[];
@@ -0,0 +1,80 @@
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
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./checker.js";
2
+ export { isTypeCompatible } from "./type-compatibility.js";
@@ -0,0 +1,2 @@
1
+ export * from "./checker.js";
2
+ export { isTypeCompatible } from "./type-compatibility.js";
@@ -0,0 +1,16 @@
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;