@malloydata/motly-ts-parser 0.0.2 → 0.2.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/build/interface/src/types.d.ts +69 -0
- package/build/interface/src/types.js +12 -0
- package/build/{ast.d.ts → parser/src/ast.d.ts} +10 -2
- package/build/parser/src/clone.d.ts +3 -0
- package/build/parser/src/clone.js +35 -0
- package/build/parser/src/index.d.ts +3 -0
- package/build/parser/src/index.js +8 -0
- package/build/{interpreter.d.ts → parser/src/interpreter.d.ts} +2 -2
- package/build/parser/src/interpreter.js +325 -0
- package/build/{parser.js → parser/src/parser.js} +111 -129
- package/build/{session.d.ts → parser/src/session.d.ts} +1 -1
- package/build/{session.js → parser/src/session.js} +8 -35
- package/build/{validate.d.ts → parser/src/validate.d.ts} +1 -1
- package/build/{validate.js → parser/src/validate.js} +51 -65
- package/package.json +3 -4
- package/build/index.d.ts +0 -2
- package/build/index.js +0 -5
- package/build/interpreter.js +0 -236
- /package/build/{ast.js → parser/src/ast.js} +0 -0
- /package/build/{parser.d.ts → parser/src/parser.d.ts} +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** A MOTLY scalar: string, number, boolean, or Date. */
|
|
2
|
+
export type MOTLYScalar = string | number | boolean | Date;
|
|
3
|
+
/** A reference to another node in the MOTLY tree (e.g. `$^parent.name`). */
|
|
4
|
+
export interface MOTLYRef {
|
|
5
|
+
linkTo: string;
|
|
6
|
+
}
|
|
7
|
+
/** An environment variable reference (e.g. `@env.API_KEY`). */
|
|
8
|
+
export interface MOTLYEnvRef {
|
|
9
|
+
env: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A value node in the MOTLY tree.
|
|
13
|
+
*
|
|
14
|
+
* - `eq` — the node's assigned value: a scalar, a reference ({@link MOTLYRef}),
|
|
15
|
+
* or an array of child nodes
|
|
16
|
+
* - `properties` — named child nodes (the node's "tags")
|
|
17
|
+
* - `deleted` — true if this node was explicitly deleted with `-name`
|
|
18
|
+
*/
|
|
19
|
+
export interface MOTLYValue {
|
|
20
|
+
eq?: MOTLYScalar | MOTLYRef | MOTLYEnvRef | MOTLYNode[];
|
|
21
|
+
properties?: Record<string, MOTLYNode>;
|
|
22
|
+
deleted?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A node in the MOTLY tree. Every node is a {@link MOTLYValue}.
|
|
26
|
+
* References are represented as `eq: { linkTo: "..." }` inside a value node.
|
|
27
|
+
*/
|
|
28
|
+
export type MOTLYNode = MOTLYValue;
|
|
29
|
+
/** A parse error with source location span. */
|
|
30
|
+
export interface MOTLYError {
|
|
31
|
+
/** Machine-readable error code (e.g. `"tag-parse-syntax-error"`). */
|
|
32
|
+
code: string;
|
|
33
|
+
/** Human-readable error message. */
|
|
34
|
+
message: string;
|
|
35
|
+
/** Start of the offending region (0-based line, column, and byte offset). */
|
|
36
|
+
begin: {
|
|
37
|
+
line: number;
|
|
38
|
+
column: number;
|
|
39
|
+
offset: number;
|
|
40
|
+
};
|
|
41
|
+
/** End of the offending region (0-based, exclusive). */
|
|
42
|
+
end: {
|
|
43
|
+
line: number;
|
|
44
|
+
column: number;
|
|
45
|
+
offset: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** An error from schema validation. */
|
|
49
|
+
export interface MOTLYSchemaError {
|
|
50
|
+
/** Machine-readable error code (e.g. `"missing-required"`, `"wrong-type"`). */
|
|
51
|
+
code: string;
|
|
52
|
+
/** Human-readable error message. */
|
|
53
|
+
message: string;
|
|
54
|
+
/** Path to the offending node (e.g. `["metadata", "name"]`). */
|
|
55
|
+
path: string[];
|
|
56
|
+
}
|
|
57
|
+
/** Type guard: is this eq value a link reference? */
|
|
58
|
+
export declare function isRef(eq: MOTLYValue["eq"]): eq is MOTLYRef;
|
|
59
|
+
/** Type guard: is this eq value an env reference? */
|
|
60
|
+
export declare function isEnvRef(eq: MOTLYValue["eq"]): eq is MOTLYEnvRef;
|
|
61
|
+
/** An error from reference validation. */
|
|
62
|
+
export interface MOTLYValidationError {
|
|
63
|
+
/** Machine-readable error code (e.g. `"unresolved-reference"`). */
|
|
64
|
+
code: string;
|
|
65
|
+
/** Human-readable error message. */
|
|
66
|
+
message: string;
|
|
67
|
+
/** Path to the offending reference (e.g. `["spec", "ref"]`). */
|
|
68
|
+
path: string[];
|
|
69
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRef = isRef;
|
|
4
|
+
exports.isEnvRef = isEnvRef;
|
|
5
|
+
/** Type guard: is this eq value a link reference? */
|
|
6
|
+
function isRef(eq) {
|
|
7
|
+
return typeof eq === "object" && eq !== null && "linkTo" in eq && !Array.isArray(eq) && !(eq instanceof Date);
|
|
8
|
+
}
|
|
9
|
+
/** Type guard: is this eq value an env reference? */
|
|
10
|
+
function isEnvRef(eq) {
|
|
11
|
+
return typeof eq === "object" && eq !== null && "env" in eq && !Array.isArray(eq) && !(eq instanceof Date);
|
|
12
|
+
}
|
|
@@ -15,6 +15,11 @@ export type ScalarValue = {
|
|
|
15
15
|
kind: "reference";
|
|
16
16
|
ups: number;
|
|
17
17
|
path: RefPathSegment[];
|
|
18
|
+
} | {
|
|
19
|
+
kind: "none";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "env";
|
|
22
|
+
name: string;
|
|
18
23
|
};
|
|
19
24
|
/** A segment in a reference path: either a named property or an array index. */
|
|
20
25
|
export type RefPathSegment = {
|
|
@@ -43,12 +48,15 @@ export type Statement = {
|
|
|
43
48
|
path: string[];
|
|
44
49
|
value: TagValue;
|
|
45
50
|
properties: Statement[] | null;
|
|
46
|
-
|
|
51
|
+
} | {
|
|
52
|
+
kind: "assignBoth";
|
|
53
|
+
path: string[];
|
|
54
|
+
value: TagValue;
|
|
55
|
+
properties: Statement[] | null;
|
|
47
56
|
} | {
|
|
48
57
|
kind: "replaceProperties";
|
|
49
58
|
path: string[];
|
|
50
59
|
properties: Statement[];
|
|
51
|
-
preserveValue: boolean;
|
|
52
60
|
} | {
|
|
53
61
|
kind: "updateProperties";
|
|
54
62
|
path: string[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cloneValue = cloneValue;
|
|
4
|
+
const types_1 = require("../../interface/src/types");
|
|
5
|
+
/** Deep clone a MOTLYValue. */
|
|
6
|
+
function cloneValue(value) {
|
|
7
|
+
const result = {};
|
|
8
|
+
if (value.deleted)
|
|
9
|
+
result.deleted = true;
|
|
10
|
+
if (value.eq !== undefined) {
|
|
11
|
+
if (value.eq instanceof Date) {
|
|
12
|
+
result.eq = new Date(value.eq.getTime());
|
|
13
|
+
}
|
|
14
|
+
else if (Array.isArray(value.eq)) {
|
|
15
|
+
result.eq = value.eq.map(cloneValue);
|
|
16
|
+
}
|
|
17
|
+
else if ((0, types_1.isRef)(value.eq)) {
|
|
18
|
+
result.eq = { linkTo: value.eq.linkTo };
|
|
19
|
+
}
|
|
20
|
+
else if ((0, types_1.isEnvRef)(value.eq)) {
|
|
21
|
+
result.eq = { env: value.eq.env };
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
result.eq = value.eq;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (value.properties) {
|
|
28
|
+
const props = {};
|
|
29
|
+
for (const key of Object.keys(value.properties)) {
|
|
30
|
+
props[key] = cloneValue(value.properties[key]);
|
|
31
|
+
}
|
|
32
|
+
result.properties = props;
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MOTLYSession = exports.isEnvRef = exports.isRef = void 0;
|
|
4
|
+
var types_1 = require("../../interface/src/types");
|
|
5
|
+
Object.defineProperty(exports, "isRef", { enumerable: true, get: function () { return types_1.isRef; } });
|
|
6
|
+
Object.defineProperty(exports, "isEnvRef", { enumerable: true, get: function () { return types_1.isEnvRef; } });
|
|
7
|
+
var session_1 = require("./session");
|
|
8
|
+
Object.defineProperty(exports, "MOTLYSession", { enumerable: true, get: function () { return session_1.MOTLYSession; } });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Statement } from "./ast";
|
|
2
|
-
import { MOTLYValue } from "
|
|
2
|
+
import { MOTLYValue, MOTLYError } from "../../interface/src/types";
|
|
3
3
|
/** Execute a list of parsed statements against an existing MOTLYValue. */
|
|
4
|
-
export declare function execute(statements: Statement[], root: MOTLYValue):
|
|
4
|
+
export declare function execute(statements: Statement[], root: MOTLYValue): MOTLYError[];
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.execute = execute;
|
|
4
|
+
const types_1 = require("../../interface/src/types");
|
|
5
|
+
const clone_1 = require("./clone");
|
|
6
|
+
/** Execute a list of parsed statements against an existing MOTLYValue. */
|
|
7
|
+
function execute(statements, root) {
|
|
8
|
+
const errors = [];
|
|
9
|
+
for (const stmt of statements) {
|
|
10
|
+
executeStatement(stmt, root, errors);
|
|
11
|
+
}
|
|
12
|
+
return errors;
|
|
13
|
+
}
|
|
14
|
+
function executeStatement(stmt, node, errors) {
|
|
15
|
+
switch (stmt.kind) {
|
|
16
|
+
case "setEq":
|
|
17
|
+
executeSetEq(node, stmt.path, stmt.value, stmt.properties, errors);
|
|
18
|
+
break;
|
|
19
|
+
case "assignBoth":
|
|
20
|
+
executeAssignBoth(node, stmt.path, stmt.value, stmt.properties, errors);
|
|
21
|
+
break;
|
|
22
|
+
case "replaceProperties":
|
|
23
|
+
executeReplaceProperties(node, stmt.path, stmt.properties, errors);
|
|
24
|
+
break;
|
|
25
|
+
case "updateProperties":
|
|
26
|
+
executeUpdateProperties(node, stmt.path, stmt.properties, errors);
|
|
27
|
+
break;
|
|
28
|
+
case "define":
|
|
29
|
+
executeDefine(node, stmt.path, stmt.deleted);
|
|
30
|
+
break;
|
|
31
|
+
case "clearAll":
|
|
32
|
+
delete node.eq;
|
|
33
|
+
node.properties = {};
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `name = value` — set eq, preserve existing properties.
|
|
39
|
+
* `name = value { props }` — set eq, then merge properties.
|
|
40
|
+
*/
|
|
41
|
+
function executeSetEq(node, path, value, properties, errors) {
|
|
42
|
+
const [writeKey, parent] = buildAccessPath(node, path);
|
|
43
|
+
const props = getOrCreateProperties(parent);
|
|
44
|
+
// Get or create target (preserves existing node and its properties)
|
|
45
|
+
let target = props[writeKey];
|
|
46
|
+
if (target === undefined) {
|
|
47
|
+
target = {};
|
|
48
|
+
props[writeKey] = target;
|
|
49
|
+
}
|
|
50
|
+
// Set the value slot
|
|
51
|
+
setEqSlot(target, value);
|
|
52
|
+
// If properties block present, MERGE them
|
|
53
|
+
if (properties !== null) {
|
|
54
|
+
for (const s of properties) {
|
|
55
|
+
executeStatement(s, target, errors);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `name := value` — assign value + clear properties.
|
|
61
|
+
* `name := value { props }` — assign value + replace properties.
|
|
62
|
+
* `name := $ref` — clone the referenced subtree.
|
|
63
|
+
* `name := $ref { props }` — clone + replace properties.
|
|
64
|
+
*/
|
|
65
|
+
function executeAssignBoth(node, path, value, properties, errors) {
|
|
66
|
+
if (value.kind === "scalar" &&
|
|
67
|
+
value.value.kind === "reference") {
|
|
68
|
+
// CLONE semantics: resolve + deep copy the target
|
|
69
|
+
let cloned;
|
|
70
|
+
try {
|
|
71
|
+
cloned = resolveAndClone(node, path, value.value.ups, value.value.path);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
75
|
+
errors.push(err);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Check for relative references that escape the clone boundary
|
|
80
|
+
sanitizeClonedRefs(cloned, 0, errors);
|
|
81
|
+
if (properties !== null) {
|
|
82
|
+
cloned.properties = {};
|
|
83
|
+
for (const s of properties) {
|
|
84
|
+
executeStatement(s, cloned, errors);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const [writeKey, parent] = buildAccessPath(node, path);
|
|
88
|
+
getOrCreateProperties(parent)[writeKey] = cloned;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Literal value: create fresh node (replaces everything)
|
|
92
|
+
const result = {};
|
|
93
|
+
setEqSlot(result, value);
|
|
94
|
+
if (properties !== null) {
|
|
95
|
+
for (const s of properties) {
|
|
96
|
+
executeStatement(s, result, errors);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const [writeKey, parent] = buildAccessPath(node, path);
|
|
100
|
+
getOrCreateProperties(parent)[writeKey] = result;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* `name: { props }` — preserve existing value, replace properties.
|
|
105
|
+
*/
|
|
106
|
+
function executeReplaceProperties(node, path, properties, errors) {
|
|
107
|
+
const [writeKey, parent] = buildAccessPath(node, path);
|
|
108
|
+
const result = {};
|
|
109
|
+
// Always preserve the existing value
|
|
110
|
+
const parentProps = getOrCreateProperties(parent);
|
|
111
|
+
const existing = parentProps[writeKey];
|
|
112
|
+
if (existing !== undefined) {
|
|
113
|
+
result.eq = existing.eq;
|
|
114
|
+
}
|
|
115
|
+
for (const stmt of properties) {
|
|
116
|
+
executeStatement(stmt, result, errors);
|
|
117
|
+
}
|
|
118
|
+
parentProps[writeKey] = result;
|
|
119
|
+
}
|
|
120
|
+
function executeUpdateProperties(node, path, properties, errors) {
|
|
121
|
+
const [writeKey, parent] = buildAccessPath(node, path);
|
|
122
|
+
const props = getOrCreateProperties(parent);
|
|
123
|
+
let target = props[writeKey];
|
|
124
|
+
if (target === undefined) {
|
|
125
|
+
target = {};
|
|
126
|
+
props[writeKey] = target;
|
|
127
|
+
}
|
|
128
|
+
for (const stmt of properties) {
|
|
129
|
+
executeStatement(stmt, target, errors);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function executeDefine(node, path, deleted) {
|
|
133
|
+
const [writeKey, parent] = buildAccessPath(node, path);
|
|
134
|
+
const props = getOrCreateProperties(parent);
|
|
135
|
+
if (deleted) {
|
|
136
|
+
props[writeKey] = { deleted: true };
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Get-or-create: if node already exists, leave it alone
|
|
140
|
+
if (props[writeKey] === undefined) {
|
|
141
|
+
props[writeKey] = {};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Navigate to the parent of the final path segment, creating intermediate nodes. */
|
|
146
|
+
function buildAccessPath(node, path) {
|
|
147
|
+
let current = node;
|
|
148
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
149
|
+
const segment = path[i];
|
|
150
|
+
const props = getOrCreateProperties(current);
|
|
151
|
+
let entry = props[segment];
|
|
152
|
+
if (entry === undefined) {
|
|
153
|
+
entry = {};
|
|
154
|
+
props[segment] = entry;
|
|
155
|
+
}
|
|
156
|
+
current = entry;
|
|
157
|
+
}
|
|
158
|
+
return [path[path.length - 1], current];
|
|
159
|
+
}
|
|
160
|
+
/** Set the eq slot on a target node from a TagValue. */
|
|
161
|
+
function setEqSlot(target, value, errors) {
|
|
162
|
+
if (value.kind === "array") {
|
|
163
|
+
target.eq = resolveArray(value.elements, errors ?? []);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const sv = value.value;
|
|
167
|
+
switch (sv.kind) {
|
|
168
|
+
case "string":
|
|
169
|
+
target.eq = sv.value;
|
|
170
|
+
break;
|
|
171
|
+
case "number":
|
|
172
|
+
target.eq = sv.value;
|
|
173
|
+
break;
|
|
174
|
+
case "boolean":
|
|
175
|
+
target.eq = sv.value;
|
|
176
|
+
break;
|
|
177
|
+
case "date":
|
|
178
|
+
target.eq = new Date(sv.value);
|
|
179
|
+
break;
|
|
180
|
+
case "reference":
|
|
181
|
+
target.eq = { linkTo: formatRefString(sv.ups, sv.path) };
|
|
182
|
+
break;
|
|
183
|
+
case "env":
|
|
184
|
+
target.eq = { env: sv.name };
|
|
185
|
+
break;
|
|
186
|
+
case "none":
|
|
187
|
+
delete target.eq;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Resolve an array of AST elements to MOTLYNodes. */
|
|
193
|
+
function resolveArray(elements, errors) {
|
|
194
|
+
return elements.map((el) => resolveArrayElement(el, errors));
|
|
195
|
+
}
|
|
196
|
+
function resolveArrayElement(el, errors) {
|
|
197
|
+
const node = {};
|
|
198
|
+
if (el.value !== null) {
|
|
199
|
+
setEqSlot(node, el.value);
|
|
200
|
+
}
|
|
201
|
+
if (el.properties !== null) {
|
|
202
|
+
for (const stmt of el.properties) {
|
|
203
|
+
executeStatement(stmt, node, errors);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return node;
|
|
207
|
+
}
|
|
208
|
+
/** Format a reference path back to its string form: `$^^name[0].sub` */
|
|
209
|
+
function formatRefString(ups, path) {
|
|
210
|
+
let s = "$";
|
|
211
|
+
for (let i = 0; i < ups; i++)
|
|
212
|
+
s += "^";
|
|
213
|
+
let first = true;
|
|
214
|
+
for (const seg of path) {
|
|
215
|
+
if (seg.kind === "name") {
|
|
216
|
+
if (!first)
|
|
217
|
+
s += ".";
|
|
218
|
+
s += seg.name;
|
|
219
|
+
first = false;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
s += `[${seg.index}]`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return s;
|
|
226
|
+
}
|
|
227
|
+
/** Resolve a reference path in the tree and return a deep clone. */
|
|
228
|
+
function resolveAndClone(root, stmtPath, ups, refPath) {
|
|
229
|
+
const refStr = formatRefString(ups, refPath);
|
|
230
|
+
let start;
|
|
231
|
+
if (ups === 0) {
|
|
232
|
+
// Absolute reference: start at root
|
|
233
|
+
start = root;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Relative reference: go up from the current context.
|
|
237
|
+
// stmtPath is the full write path (including the key being assigned to).
|
|
238
|
+
// Current context = parent of write target = stmtPath[0..len-2].
|
|
239
|
+
// Going up `ups` levels: stmtPath[0..len-2-ups].
|
|
240
|
+
const contextLen = stmtPath.length - 1 - ups;
|
|
241
|
+
if (contextLen < 0) {
|
|
242
|
+
throw cloneError(`Clone reference ${refStr} goes ${ups} level(s) up but only ${stmtPath.length - 1} ancestor(s) available`);
|
|
243
|
+
}
|
|
244
|
+
start = root;
|
|
245
|
+
for (let i = 0; i < contextLen; i++) {
|
|
246
|
+
if (!start.properties || !start.properties[stmtPath[i]]) {
|
|
247
|
+
throw cloneError(`Clone reference ${refStr} could not be resolved: path segment "${stmtPath[i]}" not found`);
|
|
248
|
+
}
|
|
249
|
+
start = start.properties[stmtPath[i]];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Follow refPath segments
|
|
253
|
+
let current = start;
|
|
254
|
+
for (const seg of refPath) {
|
|
255
|
+
if (seg.kind === "name") {
|
|
256
|
+
if (!current.properties || !current.properties[seg.name]) {
|
|
257
|
+
throw cloneError(`Clone reference ${refStr} could not be resolved: property "${seg.name}" not found`);
|
|
258
|
+
}
|
|
259
|
+
current = current.properties[seg.name];
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
if (!current.eq || !Array.isArray(current.eq)) {
|
|
263
|
+
throw cloneError(`Clone reference ${refStr} could not be resolved: index [${seg.index}] used on non-array`);
|
|
264
|
+
}
|
|
265
|
+
if (seg.index >= current.eq.length) {
|
|
266
|
+
throw cloneError(`Clone reference ${refStr} could not be resolved: index [${seg.index}] out of bounds (array length ${current.eq.length})`);
|
|
267
|
+
}
|
|
268
|
+
current = current.eq[seg.index];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return (0, clone_1.cloneValue)(current);
|
|
272
|
+
}
|
|
273
|
+
function cloneError(message) {
|
|
274
|
+
const zero = { line: 0, column: 0, offset: 0 };
|
|
275
|
+
return { code: "unresolved-clone-reference", message, begin: zero, end: zero };
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Walk a cloned subtree and null out any relative (^) references that
|
|
279
|
+
* escape the clone boundary. A reference at depth D with N ups escapes
|
|
280
|
+
* if N > D. Absolute references (ups=0) are left alone.
|
|
281
|
+
*/
|
|
282
|
+
function sanitizeClonedRefs(node, depth, errors) {
|
|
283
|
+
if ((0, types_1.isRef)(node.eq)) {
|
|
284
|
+
const parsed = parseRefUps(node.eq.linkTo);
|
|
285
|
+
if (parsed.ups > 0 && parsed.ups > depth) {
|
|
286
|
+
const zero = { line: 0, column: 0, offset: 0 };
|
|
287
|
+
errors.push({
|
|
288
|
+
code: "clone-reference-out-of-scope",
|
|
289
|
+
message: `Cloned reference "${node.eq.linkTo}" escapes the clone boundary (${parsed.ups} level(s) up from depth ${depth})`,
|
|
290
|
+
begin: zero,
|
|
291
|
+
end: zero,
|
|
292
|
+
});
|
|
293
|
+
delete node.eq;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (node.eq !== undefined && Array.isArray(node.eq)) {
|
|
297
|
+
for (const elem of node.eq) {
|
|
298
|
+
sanitizeClonedRefs(elem, depth + 1, errors);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (node.properties) {
|
|
302
|
+
for (const key of Object.keys(node.properties)) {
|
|
303
|
+
sanitizeClonedRefs(node.properties[key], depth + 1, errors);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Extract the ups count from a linkTo string like "$^^name". */
|
|
308
|
+
function parseRefUps(linkTo) {
|
|
309
|
+
let i = 0;
|
|
310
|
+
if (i < linkTo.length && linkTo[i] === "$")
|
|
311
|
+
i++;
|
|
312
|
+
let ups = 0;
|
|
313
|
+
while (i < linkTo.length && linkTo[i] === "^") {
|
|
314
|
+
ups++;
|
|
315
|
+
i++;
|
|
316
|
+
}
|
|
317
|
+
return { ups };
|
|
318
|
+
}
|
|
319
|
+
/** Get or create the properties object on a MOTLYValue. */
|
|
320
|
+
function getOrCreateProperties(node) {
|
|
321
|
+
if (!node.properties) {
|
|
322
|
+
node.properties = {};
|
|
323
|
+
}
|
|
324
|
+
return node.properties;
|
|
325
|
+
}
|