c-next 0.1.72 → 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/package.json +1 -1
- package/src/lib/__tests__/parseWithSymbols.test.ts +228 -0
- package/src/lib/parseCHeader.ts +5 -1
- package/src/lib/parseWithSymbols.ts +62 -5
- package/src/lib/types/ISymbolInfo.ts +4 -0
- package/src/lib/utils/SymbolPathUtils.ts +87 -0
- package/src/lib/utils/__tests__/SymbolPathUtils.test.ts +123 -0
package/package.json
CHANGED
|
@@ -236,4 +236,232 @@ describe("parseWithSymbols", () => {
|
|
|
236
236
|
expect(bitmap).toBeDefined();
|
|
237
237
|
});
|
|
238
238
|
});
|
|
239
|
+
|
|
240
|
+
describe("id and parentId fields (Issue #823)", () => {
|
|
241
|
+
it("top-level function has id=name, no parentId", () => {
|
|
242
|
+
const source = `void setup() { }`;
|
|
243
|
+
|
|
244
|
+
const result = parseWithSymbols(source);
|
|
245
|
+
|
|
246
|
+
const func = result.symbols.find((s) => s.name === "setup");
|
|
247
|
+
expect(func?.id).toBe("setup");
|
|
248
|
+
expect(func?.parentId).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("top-level variable has id=name, no parentId", () => {
|
|
252
|
+
const source = `u32 counter <- 0;`;
|
|
253
|
+
|
|
254
|
+
const result = parseWithSymbols(source);
|
|
255
|
+
|
|
256
|
+
const variable = result.symbols.find((s) => s.name === "counter");
|
|
257
|
+
expect(variable?.id).toBe("counter");
|
|
258
|
+
expect(variable?.parentId).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("scope has id=name, no parentId", () => {
|
|
262
|
+
const source = `scope LED { }`;
|
|
263
|
+
|
|
264
|
+
const result = parseWithSymbols(source);
|
|
265
|
+
|
|
266
|
+
const scope = result.symbols.find((s) => s.name === "LED");
|
|
267
|
+
expect(scope?.id).toBe("LED");
|
|
268
|
+
expect(scope?.parentId).toBeUndefined();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("scope method has id=Scope.method, parentId=Scope", () => {
|
|
272
|
+
const source = `
|
|
273
|
+
scope LED {
|
|
274
|
+
public void toggle() { }
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
|
|
278
|
+
const result = parseWithSymbols(source);
|
|
279
|
+
|
|
280
|
+
const func = result.symbols.find((s) => s.name === "toggle");
|
|
281
|
+
expect(func?.id).toBe("LED.toggle");
|
|
282
|
+
expect(func?.parentId).toBe("LED");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("scope variable has id=Scope.varName, parentId=Scope", () => {
|
|
286
|
+
const source = `
|
|
287
|
+
scope LED {
|
|
288
|
+
u8 pin <- 13;
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
const result = parseWithSymbols(source);
|
|
293
|
+
|
|
294
|
+
const variable = result.symbols.find((s) => s.name === "pin");
|
|
295
|
+
expect(variable?.id).toBe("LED.pin");
|
|
296
|
+
expect(variable?.parentId).toBe("LED");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("enum has id=name, no parentId", () => {
|
|
300
|
+
const source = `
|
|
301
|
+
enum Color {
|
|
302
|
+
RED,
|
|
303
|
+
GREEN
|
|
304
|
+
}
|
|
305
|
+
`;
|
|
306
|
+
|
|
307
|
+
const result = parseWithSymbols(source);
|
|
308
|
+
|
|
309
|
+
const enumSym = result.symbols.find(
|
|
310
|
+
(s) => s.name === "Color" && s.kind === "enum",
|
|
311
|
+
);
|
|
312
|
+
expect(enumSym?.id).toBe("Color");
|
|
313
|
+
expect(enumSym?.parentId).toBeUndefined();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("enum member has id=Enum.member, parentId=Enum", () => {
|
|
317
|
+
const source = `
|
|
318
|
+
enum Color {
|
|
319
|
+
RED,
|
|
320
|
+
GREEN
|
|
321
|
+
}
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
const result = parseWithSymbols(source);
|
|
325
|
+
|
|
326
|
+
const member = result.symbols.find(
|
|
327
|
+
(s) => s.name === "RED" && s.kind === "enumMember",
|
|
328
|
+
);
|
|
329
|
+
expect(member?.id).toBe("Color.RED");
|
|
330
|
+
expect(member?.parentId).toBe("Color");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("bitmap has id=name, no parentId", () => {
|
|
334
|
+
const source = `
|
|
335
|
+
bitmap8 Flags {
|
|
336
|
+
a, b, c, d, e, f, g, h
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
const result = parseWithSymbols(source);
|
|
341
|
+
|
|
342
|
+
const bitmap = result.symbols.find(
|
|
343
|
+
(s) => s.name === "Flags" && s.kind === "bitmap",
|
|
344
|
+
);
|
|
345
|
+
expect(bitmap?.id).toBe("Flags");
|
|
346
|
+
expect(bitmap?.parentId).toBeUndefined();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("bitmap field has id=Bitmap.field, parentId=Bitmap", () => {
|
|
350
|
+
const source = `
|
|
351
|
+
bitmap8 Flags {
|
|
352
|
+
a, b, c, d, e, f, g, h
|
|
353
|
+
}
|
|
354
|
+
`;
|
|
355
|
+
|
|
356
|
+
const result = parseWithSymbols(source);
|
|
357
|
+
|
|
358
|
+
const field = result.symbols.find(
|
|
359
|
+
(s) => s.name === "a" && s.kind === "bitmapField",
|
|
360
|
+
);
|
|
361
|
+
expect(field?.id).toBe("Flags.a");
|
|
362
|
+
expect(field?.parentId).toBe("Flags");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("register in scope has nested id path", () => {
|
|
366
|
+
const source = `
|
|
367
|
+
scope Board {
|
|
368
|
+
register GPIO @ 0x40000000 {
|
|
369
|
+
DR: u32 rw @ 0x00,
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
const result = parseWithSymbols(source);
|
|
375
|
+
|
|
376
|
+
const register = result.symbols.find(
|
|
377
|
+
(s) => s.name === "GPIO" && s.kind === "register",
|
|
378
|
+
);
|
|
379
|
+
expect(register?.id).toBe("Board.GPIO");
|
|
380
|
+
expect(register?.parentId).toBe("Board");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("register member has id=Scope.Register.member", () => {
|
|
384
|
+
const source = `
|
|
385
|
+
scope Board {
|
|
386
|
+
register GPIO @ 0x40000000 {
|
|
387
|
+
DR: u32 rw @ 0x00,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
`;
|
|
391
|
+
|
|
392
|
+
const result = parseWithSymbols(source);
|
|
393
|
+
|
|
394
|
+
const member = result.symbols.find(
|
|
395
|
+
(s) => s.name === "DR" && s.kind === "registerMember",
|
|
396
|
+
);
|
|
397
|
+
expect(member?.id).toBe("Board.GPIO.DR");
|
|
398
|
+
expect(member?.parentId).toBe("Board.GPIO");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("struct has correct id and parentId", () => {
|
|
402
|
+
const source = `
|
|
403
|
+
struct Point {
|
|
404
|
+
i32 x;
|
|
405
|
+
i32 y;
|
|
406
|
+
}
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
const result = parseWithSymbols(source);
|
|
410
|
+
|
|
411
|
+
const struct = result.symbols.find((s) => s.name === "Point");
|
|
412
|
+
expect(struct?.id).toBe("Point");
|
|
413
|
+
expect(struct?.parentId).toBeUndefined();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("struct field has id=Struct.field, parentId=Struct", () => {
|
|
417
|
+
const source = `
|
|
418
|
+
struct Point {
|
|
419
|
+
i32 x;
|
|
420
|
+
i32 y;
|
|
421
|
+
}
|
|
422
|
+
`;
|
|
423
|
+
|
|
424
|
+
const result = parseWithSymbols(source);
|
|
425
|
+
|
|
426
|
+
const fieldX = result.symbols.find(
|
|
427
|
+
(s) => s.name === "x" && s.kind === "field",
|
|
428
|
+
);
|
|
429
|
+
expect(fieldX?.id).toBe("Point.x");
|
|
430
|
+
expect(fieldX?.parentId).toBe("Point");
|
|
431
|
+
expect(fieldX?.type).toBe("i32");
|
|
432
|
+
|
|
433
|
+
const fieldY = result.symbols.find(
|
|
434
|
+
(s) => s.name === "y" && s.kind === "field",
|
|
435
|
+
);
|
|
436
|
+
expect(fieldY?.id).toBe("Point.y");
|
|
437
|
+
expect(fieldY?.parentId).toBe("Point");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("struct field in scope has nested id path", () => {
|
|
441
|
+
const source = `
|
|
442
|
+
scope Geometry {
|
|
443
|
+
struct Vector {
|
|
444
|
+
f32 x;
|
|
445
|
+
f32 y;
|
|
446
|
+
f32 z;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
`;
|
|
450
|
+
|
|
451
|
+
const result = parseWithSymbols(source);
|
|
452
|
+
|
|
453
|
+
const struct = result.symbols.find(
|
|
454
|
+
(s) => s.name === "Vector" && s.kind === "struct",
|
|
455
|
+
);
|
|
456
|
+
expect(struct?.id).toBe("Geometry.Vector");
|
|
457
|
+
expect(struct?.parentId).toBe("Geometry");
|
|
458
|
+
|
|
459
|
+
const fieldX = result.symbols.find(
|
|
460
|
+
(s) => s.name === "x" && s.kind === "field",
|
|
461
|
+
);
|
|
462
|
+
expect(fieldX?.id).toBe("Geometry.Vector.x");
|
|
463
|
+
expect(fieldX?.parentId).toBe("Geometry.Vector");
|
|
464
|
+
expect(fieldX?.type).toBe("f32");
|
|
465
|
+
});
|
|
466
|
+
});
|
|
239
467
|
});
|
package/src/lib/parseCHeader.ts
CHANGED
|
@@ -11,6 +11,7 @@ import ISymbolInfo from "./types/ISymbolInfo";
|
|
|
11
11
|
import IParseWithSymbolsResult from "./types/IParseWithSymbolsResult";
|
|
12
12
|
import TSymbolKind from "./types/TSymbolKind";
|
|
13
13
|
import TCSymbol from "../transpiler/types/symbols/c/TCSymbol";
|
|
14
|
+
import SymbolPathUtils from "./utils/SymbolPathUtils";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Map TCSymbol kind to library TSymbolKind
|
|
@@ -63,10 +64,13 @@ function convertTCSymbolsToISymbolInfo(
|
|
|
63
64
|
// struct and enum have no type field
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
const id = SymbolPathUtils.buildSimpleDotPath(parent, sym.name);
|
|
66
68
|
return {
|
|
67
69
|
name: sym.name,
|
|
68
|
-
fullName: parent
|
|
70
|
+
fullName: SymbolPathUtils.buildSimpleDotPath(parent, sym.name),
|
|
69
71
|
kind: mapCSymbolKind(sym.kind),
|
|
72
|
+
id,
|
|
73
|
+
parentId: parent,
|
|
70
74
|
type,
|
|
71
75
|
parent,
|
|
72
76
|
line: sym.sourceLine ?? 0,
|
|
@@ -10,10 +10,16 @@ import TypeResolver from "../utils/TypeResolver";
|
|
|
10
10
|
import ISymbolInfo from "./types/ISymbolInfo";
|
|
11
11
|
import IParseWithSymbolsResult from "./types/IParseWithSymbolsResult";
|
|
12
12
|
import TSymbol from "../transpiler/types/symbols/TSymbol";
|
|
13
|
+
import SymbolPathUtils from "./utils/SymbolPathUtils";
|
|
14
|
+
|
|
15
|
+
// Re-export helpers for use in this module
|
|
16
|
+
const buildScopePath = SymbolPathUtils.buildScopePath;
|
|
17
|
+
const getDotPathId = SymbolPathUtils.getDotPathId;
|
|
18
|
+
const getParentId = SymbolPathUtils.getParentId;
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* ADR-055 Phase 7: Convert TSymbol directly to ISymbolInfo array.
|
|
16
|
-
* Expands compound symbols (bitmaps, enums, registers) into multiple ISymbolInfo entries.
|
|
22
|
+
* Expands compound symbols (bitmaps, enums, structs, registers) into multiple ISymbolInfo entries.
|
|
17
23
|
*/
|
|
18
24
|
function convertTSymbolsToISymbolInfo(symbols: TSymbol[]): ISymbolInfo[] {
|
|
19
25
|
const result: ISymbolInfo[] = [];
|
|
@@ -27,7 +33,7 @@ function convertTSymbolsToISymbolInfo(symbols: TSymbol[]): ISymbolInfo[] {
|
|
|
27
33
|
result.push(...convertEnum(symbol));
|
|
28
34
|
break;
|
|
29
35
|
case "struct":
|
|
30
|
-
result.push(convertStruct(symbol));
|
|
36
|
+
result.push(...convertStruct(symbol));
|
|
31
37
|
break;
|
|
32
38
|
case "function":
|
|
33
39
|
result.push(...convertFunction(symbol));
|
|
@@ -53,6 +59,8 @@ function convertBitmap(
|
|
|
53
59
|
const result: ISymbolInfo[] = [];
|
|
54
60
|
const mangledName = SymbolNameUtils.getMangledName(bitmap);
|
|
55
61
|
const parent = bitmap.scope.name || undefined;
|
|
62
|
+
const bitmapId = getDotPathId(bitmap);
|
|
63
|
+
const bitmapParentId = getParentId(bitmap.scope);
|
|
56
64
|
|
|
57
65
|
result.push({
|
|
58
66
|
name: bitmap.name,
|
|
@@ -60,6 +68,8 @@ function convertBitmap(
|
|
|
60
68
|
kind: "bitmap",
|
|
61
69
|
type: bitmap.backingType,
|
|
62
70
|
parent,
|
|
71
|
+
id: bitmapId,
|
|
72
|
+
parentId: bitmapParentId,
|
|
63
73
|
line: bitmap.sourceLine,
|
|
64
74
|
});
|
|
65
75
|
|
|
@@ -70,6 +80,8 @@ function convertBitmap(
|
|
|
70
80
|
fullName: `${mangledName}.${fieldName}`,
|
|
71
81
|
kind: "bitmapField",
|
|
72
82
|
parent: mangledName,
|
|
83
|
+
id: `${bitmapId}.${fieldName}`,
|
|
84
|
+
parentId: bitmapId,
|
|
73
85
|
line: bitmap.sourceLine,
|
|
74
86
|
size: fieldInfo.width,
|
|
75
87
|
});
|
|
@@ -84,12 +96,16 @@ function convertEnum(
|
|
|
84
96
|
const result: ISymbolInfo[] = [];
|
|
85
97
|
const mangledName = SymbolNameUtils.getMangledName(enumSym);
|
|
86
98
|
const parent = enumSym.scope.name || undefined;
|
|
99
|
+
const enumId = getDotPathId(enumSym);
|
|
100
|
+
const enumParentId = getParentId(enumSym.scope);
|
|
87
101
|
|
|
88
102
|
result.push({
|
|
89
103
|
name: enumSym.name,
|
|
90
104
|
fullName: mangledName,
|
|
91
105
|
kind: "enum",
|
|
92
106
|
parent,
|
|
107
|
+
id: enumId,
|
|
108
|
+
parentId: enumParentId,
|
|
93
109
|
line: enumSym.sourceLine,
|
|
94
110
|
});
|
|
95
111
|
|
|
@@ -100,6 +116,8 @@ function convertEnum(
|
|
|
100
116
|
fullName: `${mangledName}_${memberName}`,
|
|
101
117
|
kind: "enumMember",
|
|
102
118
|
parent: mangledName,
|
|
119
|
+
id: `${enumId}.${memberName}`,
|
|
120
|
+
parentId: enumId,
|
|
103
121
|
line: enumSym.sourceLine,
|
|
104
122
|
});
|
|
105
123
|
}
|
|
@@ -109,17 +127,38 @@ function convertEnum(
|
|
|
109
127
|
|
|
110
128
|
function convertStruct(
|
|
111
129
|
struct: import("../transpiler/types/symbols/IStructSymbol").default,
|
|
112
|
-
): ISymbolInfo {
|
|
130
|
+
): ISymbolInfo[] {
|
|
131
|
+
const result: ISymbolInfo[] = [];
|
|
113
132
|
const mangledName = SymbolNameUtils.getMangledName(struct);
|
|
114
133
|
const parent = struct.scope.name || undefined;
|
|
134
|
+
const structId = getDotPathId(struct);
|
|
135
|
+
const structParentId = getParentId(struct.scope);
|
|
115
136
|
|
|
116
|
-
|
|
137
|
+
result.push({
|
|
117
138
|
name: struct.name,
|
|
118
139
|
fullName: mangledName,
|
|
119
140
|
kind: "struct",
|
|
120
141
|
parent,
|
|
142
|
+
id: structId,
|
|
143
|
+
parentId: structParentId,
|
|
121
144
|
line: struct.sourceLine,
|
|
122
|
-
};
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Add struct fields
|
|
148
|
+
for (const [fieldName, fieldInfo] of struct.fields) {
|
|
149
|
+
result.push({
|
|
150
|
+
name: fieldName,
|
|
151
|
+
fullName: `${mangledName}.${fieldName}`,
|
|
152
|
+
kind: "field",
|
|
153
|
+
type: TypeResolver.getTypeName(fieldInfo.type),
|
|
154
|
+
parent: mangledName,
|
|
155
|
+
id: `${structId}.${fieldName}`,
|
|
156
|
+
parentId: structId,
|
|
157
|
+
line: struct.sourceLine,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
123
162
|
}
|
|
124
163
|
|
|
125
164
|
function convertFunction(
|
|
@@ -142,6 +181,8 @@ function convertFunction(
|
|
|
142
181
|
kind: "function",
|
|
143
182
|
type: returnType,
|
|
144
183
|
parent,
|
|
184
|
+
id: getDotPathId(func),
|
|
185
|
+
parentId: getParentId(func.scope),
|
|
145
186
|
signature,
|
|
146
187
|
accessModifier: func.isExported ? "public" : "private",
|
|
147
188
|
line: func.sourceLine,
|
|
@@ -163,6 +204,8 @@ function convertVariable(
|
|
|
163
204
|
kind: "variable",
|
|
164
205
|
type: typeStr,
|
|
165
206
|
parent,
|
|
207
|
+
id: getDotPathId(variable),
|
|
208
|
+
parentId: getParentId(variable.scope),
|
|
166
209
|
line: variable.sourceLine,
|
|
167
210
|
};
|
|
168
211
|
}
|
|
@@ -173,12 +216,16 @@ function convertRegister(
|
|
|
173
216
|
const result: ISymbolInfo[] = [];
|
|
174
217
|
const mangledName = SymbolNameUtils.getMangledName(register);
|
|
175
218
|
const parent = register.scope.name || undefined;
|
|
219
|
+
const registerId = getDotPathId(register);
|
|
220
|
+
const registerParentId = getParentId(register.scope);
|
|
176
221
|
|
|
177
222
|
result.push({
|
|
178
223
|
name: register.name,
|
|
179
224
|
fullName: mangledName,
|
|
180
225
|
kind: "register",
|
|
181
226
|
parent,
|
|
227
|
+
id: registerId,
|
|
228
|
+
parentId: registerParentId,
|
|
182
229
|
line: register.sourceLine,
|
|
183
230
|
});
|
|
184
231
|
|
|
@@ -189,6 +236,8 @@ function convertRegister(
|
|
|
189
236
|
fullName: `${mangledName}.${memberName}`,
|
|
190
237
|
kind: "registerMember",
|
|
191
238
|
parent: mangledName,
|
|
239
|
+
id: `${registerId}.${memberName}`,
|
|
240
|
+
parentId: registerId,
|
|
192
241
|
accessModifier: memberInfo.access,
|
|
193
242
|
line: register.sourceLine,
|
|
194
243
|
});
|
|
@@ -200,10 +249,18 @@ function convertRegister(
|
|
|
200
249
|
function convertScope(
|
|
201
250
|
scope: import("../transpiler/types/symbols/IScopeSymbol").default,
|
|
202
251
|
): ISymbolInfo {
|
|
252
|
+
const scopeId = buildScopePath(scope);
|
|
253
|
+
const scopeParentId =
|
|
254
|
+
scope.parent && scope.parent.name !== ""
|
|
255
|
+
? buildScopePath(scope.parent)
|
|
256
|
+
: undefined;
|
|
257
|
+
|
|
203
258
|
return {
|
|
204
259
|
name: scope.name,
|
|
205
260
|
fullName: scope.name,
|
|
206
261
|
kind: "namespace",
|
|
262
|
+
id: scopeId,
|
|
263
|
+
parentId: scopeParentId,
|
|
207
264
|
line: scope.sourceLine,
|
|
208
265
|
};
|
|
209
266
|
}
|
|
@@ -12,6 +12,10 @@ interface ISymbolInfo {
|
|
|
12
12
|
fullName: string;
|
|
13
13
|
/** Kind of symbol */
|
|
14
14
|
kind: TSymbolKind;
|
|
15
|
+
/** Dot-path identifier matching C-Next syntax (e.g., "LED.toggle", "Color.Red") */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Parent's dot-path identifier (e.g., "LED" for "LED.toggle"), absent for top-level */
|
|
18
|
+
parentId?: string;
|
|
15
19
|
/** Type of the symbol (e.g., "void", "u32") */
|
|
16
20
|
type?: string;
|
|
17
21
|
/** Parent namespace/class/register name */
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for building dot-path identifiers for symbols.
|
|
3
|
+
* Used by parseWithSymbols and parseCHeader to generate unique symbol IDs.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import IScopeSymbol from "../../transpiler/types/symbols/IScopeSymbol";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build the dot-path for a scope by walking up the parent chain.
|
|
10
|
+
* Returns empty string for global scope.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // For scope "GPIO7" with parent "Teensy4":
|
|
14
|
+
* buildScopePath(gpio7Scope) // => "Teensy4.GPIO7"
|
|
15
|
+
*/
|
|
16
|
+
function buildScopePath(scope: { name: string; parent?: unknown }): string {
|
|
17
|
+
if (scope.name === "") {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parts: string[] = [scope.name];
|
|
22
|
+
let current = scope.parent as IScopeSymbol | undefined;
|
|
23
|
+
|
|
24
|
+
// Walk up the parent chain, stopping at global scope or circular reference
|
|
25
|
+
while (current && current.name !== "" && current !== current.parent) {
|
|
26
|
+
parts.unshift(current.name);
|
|
27
|
+
current = current.parent as IScopeSymbol | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return parts.join(".");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the dot-path ID for a symbol (e.g., "LED.toggle", "Color.Red").
|
|
35
|
+
* For top-level symbols, returns just the name.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* getDotPathId({ name: "toggle", scope: { name: "LED" } }) // => "LED.toggle"
|
|
39
|
+
* getDotPathId({ name: "setup", scope: { name: "" } }) // => "setup"
|
|
40
|
+
*/
|
|
41
|
+
function getDotPathId(symbol: {
|
|
42
|
+
name: string;
|
|
43
|
+
scope: { name: string; parent?: unknown };
|
|
44
|
+
}): string {
|
|
45
|
+
const scopePath = buildScopePath(symbol.scope);
|
|
46
|
+
if (scopePath === "") {
|
|
47
|
+
return symbol.name;
|
|
48
|
+
}
|
|
49
|
+
return `${scopePath}.${symbol.name}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the parentId for a symbol (the dot-path of its parent scope).
|
|
54
|
+
* Returns undefined for top-level symbols.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* getParentId({ name: "LED" }) // => "LED"
|
|
58
|
+
* getParentId({ name: "" }) // => undefined (global scope)
|
|
59
|
+
*/
|
|
60
|
+
function getParentId(scope: {
|
|
61
|
+
name: string;
|
|
62
|
+
parent?: unknown;
|
|
63
|
+
}): string | undefined {
|
|
64
|
+
const scopePath = buildScopePath(scope);
|
|
65
|
+
return scopePath === "" ? undefined : scopePath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a simple dot-path from parent and name.
|
|
70
|
+
* Used for C headers where there's no scope chain.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* buildSimpleDotPath("Color", "RED") // => "Color.RED"
|
|
74
|
+
* buildSimpleDotPath(undefined, "myFunc") // => "myFunc"
|
|
75
|
+
*/
|
|
76
|
+
function buildSimpleDotPath(parent: string | undefined, name: string): string {
|
|
77
|
+
return parent ? `${parent}.${name}` : name;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class SymbolPathUtils {
|
|
81
|
+
static readonly buildScopePath = buildScopePath;
|
|
82
|
+
static readonly getDotPathId = getDotPathId;
|
|
83
|
+
static readonly getParentId = getParentId;
|
|
84
|
+
static readonly buildSimpleDotPath = buildSimpleDotPath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default SymbolPathUtils;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for SymbolPathUtils
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import SymbolPathUtils from "../SymbolPathUtils";
|
|
7
|
+
|
|
8
|
+
describe("SymbolPathUtils", () => {
|
|
9
|
+
describe("buildScopePath", () => {
|
|
10
|
+
it("returns empty string for global scope (empty name)", () => {
|
|
11
|
+
const globalScope = { name: "" };
|
|
12
|
+
expect(SymbolPathUtils.buildScopePath(globalScope)).toBe("");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns scope name for single-level scope", () => {
|
|
16
|
+
const scope = { name: "LED", parent: { name: "" } };
|
|
17
|
+
expect(SymbolPathUtils.buildScopePath(scope)).toBe("LED");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("builds dot-path for nested scopes", () => {
|
|
21
|
+
const grandparent = { name: "Teensy4", parent: { name: "" } };
|
|
22
|
+
const parent = { name: "GPIO7", parent: grandparent };
|
|
23
|
+
const scope = { name: "DataRegister", parent };
|
|
24
|
+
|
|
25
|
+
expect(SymbolPathUtils.buildScopePath(scope)).toBe(
|
|
26
|
+
"Teensy4.GPIO7.DataRegister",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("handles deeply nested scopes (4+ levels)", () => {
|
|
31
|
+
const level1 = { name: "Board", parent: { name: "" } };
|
|
32
|
+
const level2 = { name: "Peripheral", parent: level1 };
|
|
33
|
+
const level3 = { name: "Register", parent: level2 };
|
|
34
|
+
const level4 = { name: "Field", parent: level3 };
|
|
35
|
+
|
|
36
|
+
expect(SymbolPathUtils.buildScopePath(level4)).toBe(
|
|
37
|
+
"Board.Peripheral.Register.Field",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("stops at global scope (empty parent name)", () => {
|
|
42
|
+
const globalScope = { name: "" };
|
|
43
|
+
const scope = { name: "LED", parent: globalScope };
|
|
44
|
+
|
|
45
|
+
expect(SymbolPathUtils.buildScopePath(scope)).toBe("LED");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles circular reference (scope is its own parent)", () => {
|
|
49
|
+
// This is a defensive check - global scope in the real codebase
|
|
50
|
+
// is its own parent to avoid null checks
|
|
51
|
+
const selfRefScope: { name: string; parent: unknown } = {
|
|
52
|
+
name: "Self",
|
|
53
|
+
parent: undefined,
|
|
54
|
+
};
|
|
55
|
+
selfRefScope.parent = selfRefScope;
|
|
56
|
+
|
|
57
|
+
expect(SymbolPathUtils.buildScopePath(selfRefScope)).toBe("Self");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("getDotPathId", () => {
|
|
62
|
+
it("returns just name for top-level symbol", () => {
|
|
63
|
+
const symbol = { name: "setup", scope: { name: "" } };
|
|
64
|
+
expect(SymbolPathUtils.getDotPathId(symbol)).toBe("setup");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns Scope.name for scoped symbol", () => {
|
|
68
|
+
const symbol = {
|
|
69
|
+
name: "toggle",
|
|
70
|
+
scope: { name: "LED", parent: { name: "" } },
|
|
71
|
+
};
|
|
72
|
+
expect(SymbolPathUtils.getDotPathId(symbol)).toBe("LED.toggle");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns full dot-path for deeply nested symbol", () => {
|
|
76
|
+
const grandparent = { name: "Board", parent: { name: "" } };
|
|
77
|
+
const parent = { name: "GPIO", parent: grandparent };
|
|
78
|
+
const symbol = { name: "DR", scope: { name: "Register", parent } };
|
|
79
|
+
|
|
80
|
+
expect(SymbolPathUtils.getDotPathId(symbol)).toBe(
|
|
81
|
+
"Board.GPIO.Register.DR",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("getParentId", () => {
|
|
87
|
+
it("returns undefined for global scope", () => {
|
|
88
|
+
const globalScope = { name: "" };
|
|
89
|
+
expect(SymbolPathUtils.getParentId(globalScope)).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns scope name for single-level scope", () => {
|
|
93
|
+
const scope = { name: "LED", parent: { name: "" } };
|
|
94
|
+
expect(SymbolPathUtils.getParentId(scope)).toBe("LED");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns full dot-path for nested scope", () => {
|
|
98
|
+
const grandparent = { name: "Teensy4", parent: { name: "" } };
|
|
99
|
+
const scope = { name: "GPIO7", parent: grandparent };
|
|
100
|
+
|
|
101
|
+
expect(SymbolPathUtils.getParentId(scope)).toBe("Teensy4.GPIO7");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("buildSimpleDotPath", () => {
|
|
106
|
+
it("returns just name when parent is undefined", () => {
|
|
107
|
+
expect(SymbolPathUtils.buildSimpleDotPath(undefined, "myFunc")).toBe(
|
|
108
|
+
"myFunc",
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns parent.name when parent is defined", () => {
|
|
113
|
+
expect(SymbolPathUtils.buildSimpleDotPath("Color", "RED")).toBe(
|
|
114
|
+
"Color.RED",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("handles empty string parent as truthy (returns path)", () => {
|
|
119
|
+
// Empty string is falsy in JS, so this behaves like undefined
|
|
120
|
+
expect(SymbolPathUtils.buildSimpleDotPath("", "name")).toBe("name");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|