c-next 0.2.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "packageManager": "npm@11.9.0",
6
6
  "type": "module",
@@ -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
  });
@@ -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 ? `${parent}.${sym.name}` : sym.name,
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
- return {
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
+ });