@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.
- package/CHANGELOG.md +8 -0
- package/LICENSE.md +190 -0
- package/README.md +5 -0
- package/dist/checker/checker.d.ts +173 -0
- package/dist/checker/checker.js +567 -0
- package/dist/checker/diagnostics.d.ts +10 -0
- package/dist/checker/diagnostics.js +80 -0
- package/dist/checker/index.d.ts +2 -0
- package/dist/checker/index.js +2 -0
- package/dist/checker/type-compatibility.d.ts +16 -0
- package/dist/checker/type-compatibility.js +59 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +10 -0
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +2 -0
- package/dist/environment/codec-registry.d.ts +42 -0
- package/dist/environment/codec-registry.js +146 -0
- package/dist/environment/hydrate.d.ts +48 -0
- package/dist/environment/hydrate.js +198 -0
- package/dist/environment/index.d.ts +4 -0
- package/dist/environment/index.js +4 -0
- package/dist/environment/register-types.d.ts +14 -0
- package/dist/environment/register-types.js +154 -0
- package/dist/environment/value-wrappers.d.ts +17 -0
- package/dist/environment/value-wrappers.js +65 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/rules/defaults/deferred-call.d.ts +13 -0
- package/dist/rules/defaults/deferred-call.js +162 -0
- package/dist/rules/defaults/index.d.ts +6 -0
- package/dist/rules/defaults/index.js +6 -0
- package/dist/rules/defaults/no-constant-condition.d.ts +7 -0
- package/dist/rules/defaults/no-constant-condition.js +36 -0
- package/dist/rules/defaults/no-mixed-operators.d.ts +9 -0
- package/dist/rules/defaults/no-mixed-operators.js +44 -0
- package/dist/rules/defaults/no-redundant-bool.d.ts +5 -0
- package/dist/rules/defaults/no-redundant-bool.js +27 -0
- package/dist/rules/defaults/no-self-comparison.d.ts +9 -0
- package/dist/rules/defaults/no-self-comparison.js +31 -0
- package/dist/rules/defaults/require-type.d.ts +7 -0
- package/dist/rules/defaults/require-type.js +19 -0
- package/dist/rules/facade.d.ts +22 -0
- package/dist/rules/facade.js +29 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.js +3 -0
- package/dist/rules/runner.d.ts +16 -0
- package/dist/rules/runner.js +30 -0
- package/dist/rules/types.d.ts +73 -0
- package/dist/rules/types.js +1 -0
- package/dist/utils/ast-utils.d.ts +55 -0
- package/dist/utils/ast-utils.js +255 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified export for rules.
|
|
3
|
+
*
|
|
4
|
+
* Provides access to individual built-in rules, a convenience array of all
|
|
5
|
+
* built-in rules, and rule factories for custom enforcement.
|
|
6
|
+
*/
|
|
7
|
+
export declare const rules: {
|
|
8
|
+
/** All built-in lint rules. */
|
|
9
|
+
readonly builtIn: readonly [import("./types.js").SELRule, import("./types.js").SELRule, import("./types.js").SELRule, import("./types.js").SELRule, import("./types.js").SELRule];
|
|
10
|
+
/** Flags redundant comparisons to boolean literals (true/false). */
|
|
11
|
+
readonly noRedundantBool: import("./types.js").SELRule;
|
|
12
|
+
/** Flags constant conditions (e.g. `true && x`, `1 == 1`). */
|
|
13
|
+
readonly noConstantCondition: import("./types.js").SELRule;
|
|
14
|
+
/** Requires parentheses when mixing `&&` and `||`. */
|
|
15
|
+
readonly noMixedOperators: import("./types.js").SELRule;
|
|
16
|
+
/** Flags self-comparisons (e.g. `x == x`). */
|
|
17
|
+
readonly noSelfComparison: import("./types.js").SELRule;
|
|
18
|
+
/** Flags contract calls with dynamic arguments that will execute as live RPC calls. */
|
|
19
|
+
readonly deferredCall: import("./types.js").SELRule;
|
|
20
|
+
/** Rule factory that enforces an expression evaluates to the expected CEL type. */
|
|
21
|
+
readonly requireType: (expected: string) => import("./types.js").SELRule;
|
|
22
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { deferredCall, noConstantCondition, noMixedOperators, noRedundantBool, noSelfComparison, requireType, } from "./defaults/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Unified export for rules.
|
|
4
|
+
*
|
|
5
|
+
* Provides access to individual built-in rules, a convenience array of all
|
|
6
|
+
* built-in rules, and rule factories for custom enforcement.
|
|
7
|
+
*/
|
|
8
|
+
export const rules = {
|
|
9
|
+
/** All built-in lint rules. */
|
|
10
|
+
builtIn: [
|
|
11
|
+
noRedundantBool,
|
|
12
|
+
noConstantCondition,
|
|
13
|
+
noMixedOperators,
|
|
14
|
+
noSelfComparison,
|
|
15
|
+
deferredCall,
|
|
16
|
+
],
|
|
17
|
+
/** Flags redundant comparisons to boolean literals (true/false). */
|
|
18
|
+
noRedundantBool,
|
|
19
|
+
/** Flags constant conditions (e.g. `true && x`, `1 == 1`). */
|
|
20
|
+
noConstantCondition,
|
|
21
|
+
/** Requires parentheses when mixing `&&` and `||`. */
|
|
22
|
+
noMixedOperators,
|
|
23
|
+
/** Flags self-comparisons (e.g. `x == x`). */
|
|
24
|
+
noSelfComparison,
|
|
25
|
+
/** Flags contract calls with dynamic arguments that will execute as live RPC calls. */
|
|
26
|
+
deferredCall,
|
|
27
|
+
/** Rule factory that enforces an expression evaluates to the expected CEL type. */
|
|
28
|
+
requireType,
|
|
29
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RuleTier, SELRule } from "./types.js";
|
|
2
|
+
import type { SELDiagnostic } from "../checker/checker.js";
|
|
3
|
+
import type { ASTNode } from "@marcbachmann/cel-js";
|
|
4
|
+
import type { SELSchema } from "@seljs/schema";
|
|
5
|
+
export interface RunRulesOptions {
|
|
6
|
+
expression: string;
|
|
7
|
+
ast: ASTNode;
|
|
8
|
+
schema: SELSchema;
|
|
9
|
+
rules: readonly SELRule[];
|
|
10
|
+
tier: RuleTier;
|
|
11
|
+
resolvedType?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Run enabled rules of the specified tier against a parsed AST.
|
|
15
|
+
*/
|
|
16
|
+
export declare const runRules: ({ expression, ast, schema, rules, tier, resolvedType, }: RunRulesOptions) => SELDiagnostic[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { nodeSpan } from "../utils/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Run enabled rules of the specified tier against a parsed AST.
|
|
4
|
+
*/
|
|
5
|
+
export const runRules = ({ expression, ast, schema, rules, tier, resolvedType, }) => {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
for (const rule of rules) {
|
|
8
|
+
const ruleTier = rule.tier ?? "structural";
|
|
9
|
+
if (ruleTier !== tier) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const severity = rule.defaultSeverity;
|
|
13
|
+
const context = {
|
|
14
|
+
expression,
|
|
15
|
+
ast,
|
|
16
|
+
schema,
|
|
17
|
+
severity,
|
|
18
|
+
resolvedType,
|
|
19
|
+
report(node, message) {
|
|
20
|
+
const span = nodeSpan(node);
|
|
21
|
+
return { message, severity, from: span.from, to: span.to };
|
|
22
|
+
},
|
|
23
|
+
reportAt(from, to, message) {
|
|
24
|
+
return { message, severity, from, to };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
diagnostics.push(...rule.run(context));
|
|
28
|
+
}
|
|
29
|
+
return diagnostics;
|
|
30
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { SELDiagnostic } from "../checker/checker.js";
|
|
2
|
+
import type { ASTNode } from "@marcbachmann/cel-js";
|
|
3
|
+
import type { SELSchema } from "@seljs/schema";
|
|
4
|
+
/**
|
|
5
|
+
* Severity levels for rules.
|
|
6
|
+
*/
|
|
7
|
+
export type RuleSeverity = "error" | "warning" | "info";
|
|
8
|
+
/**
|
|
9
|
+
* Tier determines when a rule runs:
|
|
10
|
+
* - "structural": runs on any successfully-parsed expression, even if type-check fails
|
|
11
|
+
* - "type-aware": runs only when both parse and type-check succeed
|
|
12
|
+
*/
|
|
13
|
+
export type RuleTier = "structural" | "type-aware";
|
|
14
|
+
/**
|
|
15
|
+
* Context passed to each rule's run function.
|
|
16
|
+
*/
|
|
17
|
+
export interface RuleContext {
|
|
18
|
+
/**
|
|
19
|
+
* The raw expression string.
|
|
20
|
+
*/
|
|
21
|
+
expression: string;
|
|
22
|
+
/**
|
|
23
|
+
* The parsed AST root node.
|
|
24
|
+
*/
|
|
25
|
+
ast: ASTNode;
|
|
26
|
+
/**
|
|
27
|
+
* The active schema (contracts, variables, types, functions).
|
|
28
|
+
*/
|
|
29
|
+
schema: SELSchema;
|
|
30
|
+
/**
|
|
31
|
+
* The resolved severity for this rule invocation.
|
|
32
|
+
*/
|
|
33
|
+
severity: RuleSeverity;
|
|
34
|
+
/**
|
|
35
|
+
* Resolved CEL type of the full expression. Only set for type-aware rules.
|
|
36
|
+
*/
|
|
37
|
+
resolvedType?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Create a diagnostic spanning an AST node's source range.
|
|
40
|
+
*/
|
|
41
|
+
report: (node: ASTNode, message: string) => SELDiagnostic;
|
|
42
|
+
/**
|
|
43
|
+
* Create a diagnostic at an explicit position range.
|
|
44
|
+
*/
|
|
45
|
+
reportAt: (from: number, to: number, message: string) => SELDiagnostic;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A lint rule that analyzes a parsed expression and reports diagnostics.
|
|
49
|
+
*/
|
|
50
|
+
export interface SELRule {
|
|
51
|
+
/**
|
|
52
|
+
* Unique rule identifier (kebab-case, e.g. "no-redundant-bool").
|
|
53
|
+
*/
|
|
54
|
+
name: string;
|
|
55
|
+
/**
|
|
56
|
+
* Human-readable description.
|
|
57
|
+
*/
|
|
58
|
+
description: string;
|
|
59
|
+
/**
|
|
60
|
+
* Severity level for diagnostics reported by this rule.
|
|
61
|
+
*/
|
|
62
|
+
defaultSeverity: RuleSeverity;
|
|
63
|
+
/**
|
|
64
|
+
* Execution tier. Defaults to "structural" if omitted.
|
|
65
|
+
* - "structural": rule receives AST even when type-check failed
|
|
66
|
+
* - "type-aware": rule only runs after successful type-check
|
|
67
|
+
*/
|
|
68
|
+
tier?: RuleTier;
|
|
69
|
+
/**
|
|
70
|
+
* Analyze the expression and return diagnostics.
|
|
71
|
+
*/
|
|
72
|
+
run: (context: RuleContext) => SELDiagnostic[];
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ASTNode } from "@marcbachmann/cel-js";
|
|
2
|
+
interface NodeSpan {
|
|
3
|
+
from: number;
|
|
4
|
+
to: number;
|
|
5
|
+
}
|
|
6
|
+
type ASTVisitor = (node: ASTNode, parent: ASTNode | null) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Compute the source span {from, to} of an AST node.
|
|
9
|
+
*
|
|
10
|
+
* Uses the AST structure and `node.input` (original source) to calculate
|
|
11
|
+
* positions. Does NOT use `serialize()` which normalizes whitespace,
|
|
12
|
+
* quotes, and parentheses.
|
|
13
|
+
*
|
|
14
|
+
* Note: for binary operators, `node.pos` points to the operator token,
|
|
15
|
+
* not the start of the expression. We compute `from` by finding the
|
|
16
|
+
* leftmost descendant leaf position instead.
|
|
17
|
+
*/
|
|
18
|
+
declare const nodeSpan: (node: ASTNode) => NodeSpan;
|
|
19
|
+
/**
|
|
20
|
+
* Depth-first walk of the AST, calling visitor on every node.
|
|
21
|
+
*/
|
|
22
|
+
declare const walkAST: (node: ASTNode, visitor: ASTVisitor, parent?: ASTNode | null) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Collect all ASTNode children of a node, regardless of op shape.
|
|
25
|
+
*/
|
|
26
|
+
declare const collectChildren: (node: ASTNode) => ASTNode[];
|
|
27
|
+
/**
|
|
28
|
+
* Find the deepest AST node whose span contains the given offset.
|
|
29
|
+
*
|
|
30
|
+
* Walks the tree depth-first. At each level, if a child's span contains
|
|
31
|
+
* the offset, recurse into that child. Returns the deepest matching node,
|
|
32
|
+
* or undefined if the offset is outside the root span.
|
|
33
|
+
*/
|
|
34
|
+
declare const findNodeAt: (root: ASTNode, offset: number) => ASTNode | undefined;
|
|
35
|
+
interface NodeWithParent {
|
|
36
|
+
node: ASTNode;
|
|
37
|
+
parent: ASTNode | null;
|
|
38
|
+
}
|
|
39
|
+
declare const findNodeWithParentAt: (root: ASTNode, offset: number, parent?: ASTNode | null) => NodeWithParent | undefined;
|
|
40
|
+
interface EnclosingCallInfo {
|
|
41
|
+
functionName: string;
|
|
42
|
+
receiverName?: string;
|
|
43
|
+
paramIndex: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Find the enclosing call/rcall node for a cursor offset and determine
|
|
47
|
+
* which argument index the cursor falls within.
|
|
48
|
+
*
|
|
49
|
+
* Walks the AST to find call/rcall nodes whose span contains the offset,
|
|
50
|
+
* then determines the argument index by checking which arg node's span
|
|
51
|
+
* the offset falls within.
|
|
52
|
+
*/
|
|
53
|
+
declare const findEnclosingCallInfo: (root: ASTNode, offset: number) => EnclosingCallInfo | undefined;
|
|
54
|
+
export { collectChildren, findEnclosingCallInfo, findNodeAt, findNodeWithParentAt, nodeSpan, walkAST, };
|
|
55
|
+
export type { ASTVisitor, EnclosingCallInfo, NodeSpan, NodeWithParent };
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const BINARY_OPS = new Set([
|
|
2
|
+
"==",
|
|
3
|
+
"!=",
|
|
4
|
+
"<",
|
|
5
|
+
"<=",
|
|
6
|
+
">",
|
|
7
|
+
">=",
|
|
8
|
+
"+",
|
|
9
|
+
"-",
|
|
10
|
+
"*",
|
|
11
|
+
"/",
|
|
12
|
+
"%",
|
|
13
|
+
"in",
|
|
14
|
+
"||",
|
|
15
|
+
"&&",
|
|
16
|
+
"[]",
|
|
17
|
+
"[?]",
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Compute the source span {from, to} of an AST node.
|
|
21
|
+
*
|
|
22
|
+
* Uses the AST structure and `node.input` (original source) to calculate
|
|
23
|
+
* positions. Does NOT use `serialize()` which normalizes whitespace,
|
|
24
|
+
* quotes, and parentheses.
|
|
25
|
+
*
|
|
26
|
+
* Note: for binary operators, `node.pos` points to the operator token,
|
|
27
|
+
* not the start of the expression. We compute `from` by finding the
|
|
28
|
+
* leftmost descendant leaf position instead.
|
|
29
|
+
*/
|
|
30
|
+
const nodeSpan = (node) => {
|
|
31
|
+
const src = node.input;
|
|
32
|
+
if (node.op === "id") {
|
|
33
|
+
return { from: node.pos, to: node.pos + node.args.length };
|
|
34
|
+
}
|
|
35
|
+
if (node.op === "value") {
|
|
36
|
+
return valueSpan(src, node.pos);
|
|
37
|
+
}
|
|
38
|
+
// Composite: compute from as leftmost child start, to as rightmost child end
|
|
39
|
+
const children = collectChildren(node);
|
|
40
|
+
let minFrom = node.pos;
|
|
41
|
+
let maxTo = node.pos;
|
|
42
|
+
for (const child of children) {
|
|
43
|
+
const s = nodeSpan(child);
|
|
44
|
+
if (s.from < minFrom) {
|
|
45
|
+
minFrom = s.from;
|
|
46
|
+
}
|
|
47
|
+
if (s.to > maxTo) {
|
|
48
|
+
maxTo = s.to;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// For unary operators, node.pos IS the start (e.g. `!` in `!x`)
|
|
52
|
+
if (node.op === "!_" || node.op === "-_") {
|
|
53
|
+
minFrom = node.pos;
|
|
54
|
+
}
|
|
55
|
+
// Handle trailing syntax for delimited constructs
|
|
56
|
+
if (node.op === "." || node.op === ".?") {
|
|
57
|
+
const fieldName = node.args[1];
|
|
58
|
+
let i = maxTo;
|
|
59
|
+
while (i < src.length && src[i] !== ".") {
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
// skip dot
|
|
63
|
+
i++;
|
|
64
|
+
if (node.op === ".?" && i < src.length && src[i] === "?") {
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
i += fieldName.length;
|
|
68
|
+
return { from: minFrom, to: Math.min(i, src.length) };
|
|
69
|
+
}
|
|
70
|
+
if (node.op === "call" || node.op === "rcall") {
|
|
71
|
+
return { from: minFrom, to: scanTo(src, maxTo, ")") };
|
|
72
|
+
}
|
|
73
|
+
if (node.op === "list" || node.op === "[]" || node.op === "[?]") {
|
|
74
|
+
return { from: minFrom, to: scanTo(src, maxTo, "]") };
|
|
75
|
+
}
|
|
76
|
+
if (node.op === "map") {
|
|
77
|
+
return { from: minFrom, to: scanTo(src, maxTo, "}") };
|
|
78
|
+
}
|
|
79
|
+
return { from: minFrom, to: maxTo };
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Depth-first walk of the AST, calling visitor on every node.
|
|
83
|
+
*/
|
|
84
|
+
const walkAST = (node, visitor, parent = null) => {
|
|
85
|
+
visitor(node, parent);
|
|
86
|
+
for (const child of collectChildren(node)) {
|
|
87
|
+
walkAST(child, visitor, node);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Collect all ASTNode children of a node, regardless of op shape.
|
|
92
|
+
*/
|
|
93
|
+
const collectChildren = (node) => {
|
|
94
|
+
if (node.op === "value" || node.op === "id") {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
if (node.op === "!_" || node.op === "-_") {
|
|
98
|
+
return [node.args];
|
|
99
|
+
}
|
|
100
|
+
if (BINARY_OPS.has(node.op)) {
|
|
101
|
+
const [left, right] = node.args;
|
|
102
|
+
return [left, right];
|
|
103
|
+
}
|
|
104
|
+
switch (node.op) {
|
|
105
|
+
case ".":
|
|
106
|
+
case ".?": {
|
|
107
|
+
const [receiver] = node.args;
|
|
108
|
+
return [receiver];
|
|
109
|
+
}
|
|
110
|
+
case "call": {
|
|
111
|
+
const [, callArgs] = node.args;
|
|
112
|
+
return callArgs;
|
|
113
|
+
}
|
|
114
|
+
case "rcall": {
|
|
115
|
+
const [, receiver, callArgs] = node.args;
|
|
116
|
+
return [receiver, ...callArgs];
|
|
117
|
+
}
|
|
118
|
+
case "list":
|
|
119
|
+
return node.args;
|
|
120
|
+
case "map": {
|
|
121
|
+
const entries = node.args;
|
|
122
|
+
return entries.flat();
|
|
123
|
+
}
|
|
124
|
+
case "?:": {
|
|
125
|
+
const [cond, then, els] = node.args;
|
|
126
|
+
return [cond, then, els];
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const valueSpan = (src, from) => {
|
|
133
|
+
const ch = src[from];
|
|
134
|
+
if (ch === '"' || ch === "'") {
|
|
135
|
+
let i = from + 1;
|
|
136
|
+
while (i < src.length && src[i] !== ch) {
|
|
137
|
+
if (src[i] === "\\") {
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
i++;
|
|
141
|
+
}
|
|
142
|
+
return { from, to: i < src.length ? i + 1 : i };
|
|
143
|
+
}
|
|
144
|
+
// Boolean, null, number: scan past word characters and dots (floats)
|
|
145
|
+
let i = from;
|
|
146
|
+
while (i < src.length && /[\w.]/.test(src[i] ?? "")) {
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
return { from, to: i };
|
|
150
|
+
};
|
|
151
|
+
const scanTo = (src, from, ch) => {
|
|
152
|
+
let i = from;
|
|
153
|
+
while (i < src.length && src[i] !== ch) {
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
return i < src.length ? i + 1 : from;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Find the deepest AST node whose span contains the given offset.
|
|
160
|
+
*
|
|
161
|
+
* Walks the tree depth-first. At each level, if a child's span contains
|
|
162
|
+
* the offset, recurse into that child. Returns the deepest matching node,
|
|
163
|
+
* or undefined if the offset is outside the root span.
|
|
164
|
+
*/
|
|
165
|
+
const findNodeAt = (root, offset) => {
|
|
166
|
+
const rootSpan = nodeSpan(root);
|
|
167
|
+
if (offset < rootSpan.from || offset >= rootSpan.to) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const children = collectChildren(root);
|
|
171
|
+
for (const child of children) {
|
|
172
|
+
const childSpan = nodeSpan(child);
|
|
173
|
+
if (offset >= childSpan.from && offset < childSpan.to) {
|
|
174
|
+
return findNodeAt(child, offset) ?? child;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return root;
|
|
178
|
+
};
|
|
179
|
+
const findNodeWithParentAt = (root, offset, parent = null) => {
|
|
180
|
+
const rootSpan = nodeSpan(root);
|
|
181
|
+
if (offset < rootSpan.from || offset >= rootSpan.to) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const children = collectChildren(root);
|
|
185
|
+
for (const child of children) {
|
|
186
|
+
const childSpan = nodeSpan(child);
|
|
187
|
+
if (offset >= childSpan.from && offset < childSpan.to) {
|
|
188
|
+
return (findNodeWithParentAt(child, offset, root) ?? {
|
|
189
|
+
node: child,
|
|
190
|
+
parent: root,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { node: root, parent };
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Find the enclosing call/rcall node for a cursor offset and determine
|
|
198
|
+
* which argument index the cursor falls within.
|
|
199
|
+
*
|
|
200
|
+
* Walks the AST to find call/rcall nodes whose span contains the offset,
|
|
201
|
+
* then determines the argument index by checking which arg node's span
|
|
202
|
+
* the offset falls within.
|
|
203
|
+
*/
|
|
204
|
+
const findEnclosingCallInfo = (root, offset) => {
|
|
205
|
+
let result;
|
|
206
|
+
walkAST(root, (node) => {
|
|
207
|
+
if (node.op !== "call" && node.op !== "rcall") {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const span = nodeSpan(node);
|
|
211
|
+
if (offset < span.from || offset >= span.to) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (node.op === "call") {
|
|
215
|
+
const [name, args] = node.args;
|
|
216
|
+
const paramIndex = findArgIndex(args, offset);
|
|
217
|
+
if (paramIndex >= 0) {
|
|
218
|
+
result = { functionName: name, paramIndex };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (node.op === "rcall") {
|
|
222
|
+
const [name, receiver, args] = node.args;
|
|
223
|
+
const paramIndex = findArgIndex(args, offset);
|
|
224
|
+
if (paramIndex >= 0) {
|
|
225
|
+
const receiverName = receiver.op === "id" ? receiver.args : undefined;
|
|
226
|
+
result = { functionName: name, receiverName, paramIndex };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return result;
|
|
231
|
+
};
|
|
232
|
+
/** Determine which argument index a cursor offset falls within. */
|
|
233
|
+
const findArgIndex = (args, offset) => {
|
|
234
|
+
for (let i = 0; i < args.length; i++) {
|
|
235
|
+
const arg = args[i];
|
|
236
|
+
if (!arg) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const argSpan = nodeSpan(arg);
|
|
240
|
+
if (offset >= argSpan.from && offset < argSpan.to) {
|
|
241
|
+
return i;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (args.length > 0) {
|
|
245
|
+
const lastArg = args[args.length - 1];
|
|
246
|
+
if (lastArg) {
|
|
247
|
+
const lastSpan = nodeSpan(lastArg);
|
|
248
|
+
if (offset >= lastSpan.to) {
|
|
249
|
+
return args.length;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return args.length === 0 ? 0 : -1;
|
|
254
|
+
};
|
|
255
|
+
export { collectChildren, findEnclosingCallInfo, findNodeAt, findNodeWithParentAt, nodeSpan, walkAST, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ast-utils.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ast-utils.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seljs/checker",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "abi group GmbH",
|
|
8
|
+
"email": "info@abigroup.io",
|
|
9
|
+
"url": "https://abigroup.io/"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"url": "https://github.com/abinnovision/seljs"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"LICENSE.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"format:check": "prettier --check '{{src,test}/**/*,*}.{{t,j}s{,x},json{,5},md,y{,a}ml}'",
|
|
29
|
+
"format:fix": "prettier --write '{{src,test}/**/*,*}.{{t,j}s{,x},json{,5},md,y{,a}ml}'",
|
|
30
|
+
"lint:check": "eslint '{{src,test}/**/*,*}.{t,j}s{,x}'",
|
|
31
|
+
"lint:fix": "eslint '{{src,test}/**/*,*}.{t,j}s{,x}' --fix",
|
|
32
|
+
"test-integration": "vitest run --config test/integration/vitest.config.ts",
|
|
33
|
+
"test-unit": "vitest run",
|
|
34
|
+
"test-unit:watch": "vitest watch",
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
36
|
+
},
|
|
37
|
+
"lint-staged": {
|
|
38
|
+
"{{src,test}/**/*,*}.{{t,j}s{,x},json{,5},md,y{,a}ml}": [
|
|
39
|
+
"prettier --write"
|
|
40
|
+
],
|
|
41
|
+
"{{src,test}/**/*,*}.{t,j}s{,x}": [
|
|
42
|
+
"eslint --fix"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@marcbachmann/cel-js": "^7.5.2",
|
|
47
|
+
"@seljs/common": "1.0.0",
|
|
48
|
+
"@seljs/schema": "1.0.0",
|
|
49
|
+
"@seljs/types": "1.0.0",
|
|
50
|
+
"debug": "^4.4.3"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"zod": "^4.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@abinnovision/eslint-config-base": "^3.2.0",
|
|
57
|
+
"@abinnovision/prettier-config": "^2.1.5",
|
|
58
|
+
"@seljs-internal/tsconfig": "^0.0.0",
|
|
59
|
+
"@types/debug": "^4.1.12",
|
|
60
|
+
"eslint": "^9.39.4",
|
|
61
|
+
"prettier": "^3.8.1",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"vitest": "^4.0.18",
|
|
64
|
+
"zod": "^4.3.6"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"npm": true,
|
|
68
|
+
"npmAccess": "public"
|
|
69
|
+
}
|
|
70
|
+
}
|