Skip to content

Commit 22c9271

Browse files
When to skip nameless item (#846)
* different fixes - reworked displaying <item> on conditions "print" and "value but no name" - colon path - some "not implemented" on Network.scvd * fixed empty item handling
1 parent 31a75e9 commit 22c9271

10 files changed

Lines changed: 447 additions & 34 deletions

File tree

src/views/component-viewer/model/scvd-typedef.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ export class ScvdTypedef extends ScvdNode {
281281
}
282282
} else {
283283
member.offset = currentNextOffset.toString(); // set current offset
284-
componentViewerLogger.error(`ScvdTypedef.calculateOffsets: no offset defined for member: ${member.name} in typedef: ${this.getDisplayLabel()}`);
284+
componentViewerLogger.warn(`ScvdTypedef.calculateOffsets: no offset defined for member: ${member.name} in typedef: ${this.getDisplayLabel()}`);
285285
}
286286
member.offset?.configure();
287287
}
@@ -299,7 +299,7 @@ export class ScvdTypedef extends ScvdNode {
299299
this.targetSize = size;
300300

301301
if (currentNextOffset > size) {
302-
componentViewerLogger.error(`ScvdTypedef.calculateOffsets: typedef size (${size}) smaller than members size (${currentNextOffset}) for ${this.getDisplayLabel()}`);
302+
componentViewerLogger.warn(`ScvdTypedef.calculateOffsets: typedef size (${size}) smaller than members size (${currentNextOffset}) for ${this.getDisplayLabel()}`);
303303
} else if (currentNextOffset < size) { // adjust to typedef size if padding is included
304304
currentNextOffset = size;
305305
}

src/views/component-viewer/parser-evaluator/evaluator.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ export class Evaluator {
250250
return this.findReferenceNode(c.callee);
251251
}
252252
case 'EvalPointCall': {
253+
// For intrinsic calls, the callee is metadata (intrinsic name), not a reference to evaluate.
254+
// Only search the arguments for references.
253255
const c = node as EvalPointCall;
254256
if (c.args.length) {
255257
for (const arg of c.args) {
@@ -259,6 +261,7 @@ export class Evaluator {
259261
}
260262
}
261263
}
264+
// If no args, fall back to callee (it's still an identifier node)
262265
return this.findReferenceNode(c.callee);
263266
}
264267
case 'PrintfExpression': {
@@ -549,6 +552,13 @@ export class Evaluator {
549552
resolved.push((arg as StringLiteral).value);
550553
continue;
551554
}
555+
// __Offset_of accepts ColonPath (type:member) as in C++ implementation
556+
if (name === '__Offset_of' && arg.kind === 'ColonPath') {
557+
const cp = arg as ColonPath;
558+
// Convert ColonPath parts to "type:member" string format
559+
resolved.push(cp.parts.join(':'));
560+
continue;
561+
}
552562
// Make the failure explicit; this avoids silently passing evaluated values like 0.
553563
this.diagnostics.record(`${name} expects identifier/string for argument ${idx + 1}, got ${arg.kind}`);
554564
perf?.end(perfStartTime, 'evalIntrinsicArgsMs', 'evalIntrinsicArgsCalls');
@@ -602,7 +612,12 @@ export class Evaluator {
602612
}
603613
if (!ref) {
604614
perf?.end(perfStartKind, 'evalMustRefIdentifierMs', 'evalMustRefIdentifierCalls');
605-
this.diagnostics.record(`Unknown symbol '${id.name}' in ${this.diagnostics.formatNodeForMessage(node)}`);
615+
// Check if this is an intrinsic function name being used as an identifier
616+
if (isIntrinsicName(id.name)) {
617+
this.diagnostics.record(`Intrinsic function '${id.name}' cannot be used as an identifier. Use it as a function call: ${id.name}(...)`);
618+
} else {
619+
this.diagnostics.record(`Unknown symbol '${id.name}' in ${this.diagnostics.formatNodeForMessage(node)}`);
620+
}
606621
return undefined;
607622
}
608623
// Start a new anchor chain at this identifier

src/views/component-viewer/parser-evaluator/parser.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,26 @@ function constValueFromCValue(value: { type: { kind: string }, value: bigint | n
275275
return Number(bigintValue);
276276
}
277277

278+
/**
279+
* Validates that a string doesn't contain leftover XML entity references.
280+
* Returns undefined if valid, or an error message if XML entities are detected.
281+
*
282+
* Common XML entities that should be decoded before parsing:
283+
* - &amp; → &
284+
* - &lt; → <
285+
* - &gt; → >
286+
* - &quot; → "
287+
* - &apos; → '
288+
*/
289+
export function validateNoXmlEntities(expr: string): string | undefined {
290+
const xmlEntityPattern = /&(amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/;
291+
const match = xmlEntityPattern.exec(expr);
292+
if (match) {
293+
return `Expression contains undecoded XML entity '${match[0]}'. XML attributes should be decoded before parsing.`;
294+
}
295+
return undefined;
296+
}
297+
278298
function unescapeString(rawWithQuotes: string): string {
279299
const s = rawWithQuotes.slice(1, -1);
280300
let out = '';
@@ -417,6 +437,13 @@ export class Parser {
417437

418438
public parse(input: string, isPrintExpression: boolean): ParseResult {
419439
this.reinit(input);
440+
441+
// Check for leftover XML entities before parsing
442+
const xmlError = validateNoXmlEntities(input);
443+
if (xmlError) {
444+
this.warn(xmlError, 0, input.length);
445+
}
446+
420447
let ast: ASTNode;
421448
let isPrintf = false;
422449

src/views/component-viewer/scvd-eval-interface.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { ScvdMember } from './model/scvd-member';
2727
import { ScvdVar } from './model/scvd-var';
2828
import { perf } from './stats-config';
2929
import { ScvdEvalInterfaceCache } from './scvd-eval-interface-cache';
30+
import { ScvdComponentViewer } from './model/scvd-component-viewer';
31+
import { ScvdTypedef } from './model/scvd-typedef';
3032

3133
export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicProvider {
3234
private static readonly INVALID_ADDR_MIN = 0xFFFFFFF0;
@@ -115,11 +117,16 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr
115117
}
116118

117119
// Determine element width: prefer target size, then container hint, then byte-width helper.
118-
let widthBytes: number | undefined = currentRef?.getTargetSize ? await currentRef.getTargetSize() : undefined;
120+
// Only query target size if we have a resolved reference (not just the base container)
121+
let widthBytes: number | undefined;
122+
if (container.current && currentRef?.getTargetSize) {
123+
widthBytes = await currentRef.getTargetSize();
124+
}
119125
if ((!widthBytes || widthBytes <= 0) && container.widthBytes) {
120126
widthBytes = container.widthBytes;
121127
}
122-
if ((!widthBytes || widthBytes <= 0) && typeof this.getByteWidth === 'function' && currentRef) {
128+
// Only call getByteWidth if we have a resolved reference (not just the base container)
129+
if ((!widthBytes || widthBytes <= 0) && currentRef && container.current) {
123130
const w = await this.getByteWidth(currentRef);
124131
if (typeof w === 'number' && w > 0) {
125132
widthBytes = w;
@@ -202,7 +209,21 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr
202209
return ref;
203210
}
204211

205-
public async resolveColonPath(_container: RefContainer, _parts: string[]): Promise<EvalValue> {
212+
public async resolveColonPath(_container: RefContainer, parts: string[]): Promise<EvalValue> {
213+
// ColonPath in general expression context (not inside __Offset_of)
214+
// Example: typedef:member evaluates to the offset value itself
215+
// This matches C++ behavior where type:member expressions can be used directly
216+
217+
if (parts.length === 2) {
218+
const [typedefName, memberName] = parts;
219+
const colonPath = `${typedefName}:${memberName}`;
220+
221+
// Reuse __Offset_of logic which handles typedef:member resolution
222+
const offset = await this.__Offset_of(_container, colonPath);
223+
return offset;
224+
}
225+
226+
componentViewerLogger.warn(`[resolveColonPath] Unsupported colon path format with ${parts.length} parts: ${parts.join(':')}`);
206227
return undefined;
207228
}
208229

@@ -387,11 +408,62 @@ export class ScvdEvalInterface implements ModelHost, DataAccessHost, IntrinsicPr
387408
public async __Offset_of(container: RefContainer, typedefMember: string): Promise<number | undefined> {
388409
const perfStartTime = perf?.start() ?? 0;
389410
try {
390-
const memberRef = container.base.getMember(typedefMember);
391-
if (memberRef) {
411+
// Handle both "member" and "typedef:member" formats
412+
const parts = typedefMember.split(':');
413+
414+
if (parts.length === 1) {
415+
// Simple member lookup from current container
416+
const memberRef = container.base.getMember(typedefMember);
417+
if (memberRef) {
418+
const offset = await memberRef.getMemberOffset();
419+
return offset;
420+
}
421+
return undefined;
422+
}
423+
424+
if (parts.length === 2) {
425+
// ColonPath format: "TypedefName:MemberName"
426+
const [typedefName, memberName] = parts;
427+
428+
// Find the root ScvdComponentViewer to access typedefs
429+
let root: ScvdNode = container.base;
430+
while (root.parent !== undefined) {
431+
root = root.parent;
432+
}
433+
434+
// Navigate to typedefs and find the specified typedef
435+
if (!(root instanceof ScvdComponentViewer)) {
436+
componentViewerLogger.error('[__Offset_of] Root is not ScvdComponentViewer');
437+
return undefined;
438+
}
439+
440+
const typedefs = root.typedefs;
441+
if (!typedefs || !typedefs.typedef) {
442+
componentViewerLogger.error('[__Offset_of] No typedefs found in component viewer');
443+
return undefined;
444+
}
445+
446+
const typedef = typedefs.typedef.find((td: ScvdTypedef) => td.name === typedefName);
447+
if (!typedef) {
448+
componentViewerLogger.error(`[__Offset_of] Typedef "${typedefName}" not found`);
449+
return undefined;
450+
}
451+
452+
// Get the member from the typedef
453+
const memberRef = typedef.getMember(memberName);
454+
if (!memberRef) {
455+
componentViewerLogger.error(`[__Offset_of] Member "${memberName}" not found in typedef "${typedefName}"`);
456+
return undefined;
457+
}
458+
392459
const offset = await memberRef.getMemberOffset();
460+
if (offset === undefined) {
461+
componentViewerLogger.warn(`[__Offset_of] Member "${memberName}" in typedef "${typedefName}" has undefined offset`);
462+
}
393463
return offset;
394464
}
465+
466+
componentViewerLogger.error(`[__Offset_of] Invalid format: "${typedefMember}". Expected "member" or "typedef:member"`);
395467
return undefined;
396468
} finally {
397469
perf?.end(perfStartTime, 'symbolOffsetMs', 'symbolOffsetCalls');

src/views/component-viewer/statement-engine/statement-item.ts

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,47 +51,66 @@ export class StatementItem extends StatementBase {
5151
protected override async onExecute(executionContext: ExecutionContext, guiTree: ScvdGuiTree): Promise<void> {
5252
componentViewerLogger.debug(`Line: ${this.line}: Executing <${this.scvdItem.tag}> : ${await this.getLogName()}`);
5353

54-
const guiNameStart = perf?.start() ?? 0;
55-
const guiName = await this.getGuiName();
56-
perf?.end(guiNameStart, 'guiNameMs', 'guiNameCalls');
57-
const childGuiTree = this.getOrCreateGuiChild(guiTree, guiName);
58-
perf?.recordGuiItemNode();
59-
const guiValueStart = perf?.start() ?? 0;
60-
const guiValue = await this.getGuiValue();
61-
perf?.end(guiValueStart, 'guiValueMs', 'guiValueCalls');
62-
childGuiTree.setGuiName(guiName);
63-
childGuiTree.setGuiValue(guiValue);
64-
6554
const printChildren = this.children.filter((child): child is StatementPrint => child instanceof StatementPrint);
55+
56+
// Determine the name and value for this item
57+
let guiName = '';
58+
let guiValue = '';
59+
6660
if (printChildren.length > 0) {
61+
// Item uses print children for name/value - check if any print condition matches
6762
let matched = false;
6863
for (const printChild of printChildren) {
6964
const shouldPrint = await printChild.scvdItem.getConditionResult();
7065
if (shouldPrint !== false) {
71-
const guiNamePrint = await printChild.scvdItem.getGuiName();
72-
const guiValuePrint = await printChild.scvdItem.getGuiValue();
73-
childGuiTree.setGuiName(guiNamePrint);
74-
childGuiTree.setGuiValue(guiValuePrint);
66+
guiName = await printChild.scvdItem.getGuiName() ?? '';
67+
guiValue = await printChild.scvdItem.getGuiValue() ?? '';
7568
matched = true;
7669
break;
7770
}
7871
}
7972
if (!matched) {
80-
childGuiTree.detach();
73+
// No print matched - skip this entire item and its subtree
8174
return;
8275
}
83-
for (const child of this.children) {
84-
if (!(child instanceof StatementPrint)) {
85-
await child.executeStatement(executionContext, childGuiTree);
86-
}
87-
}
88-
return;
76+
} else {
77+
// Item uses its own property/value attributes
78+
const guiNameStart = perf?.start() ?? 0;
79+
guiName = await this.getGuiName() ?? '';
80+
perf?.end(guiNameStart, 'guiNameMs', 'guiNameCalls');
81+
const guiValueStart = perf?.start() ?? 0;
82+
guiValue = await this.getGuiValue() ?? '';
83+
perf?.end(guiValueStart, 'guiValueMs', 'guiValueCalls');
8984
}
9085

91-
if (this.children.length > 0) {
92-
for (const child of this.children) {
86+
// Create the GUI node
87+
const childGuiTree = this.getOrCreateGuiChild(guiTree, guiName);
88+
perf?.recordGuiItemNode();
89+
childGuiTree.setGuiName(guiName);
90+
childGuiTree.setGuiValue(guiValue);
91+
92+
// Execute non-print children
93+
for (const child of this.children) {
94+
if (!(child instanceof StatementPrint)) {
9395
await child.executeStatement(executionContext, childGuiTree);
9496
}
9597
}
98+
99+
// Remove item if it has statement children but no meaningful result:
100+
// - No value AND no GUI children after execution (e.g., "Drives" container with empty list)
101+
// - OR no name, no value, AND has children (e.g., empty Drive with Status child)
102+
const hasNonPrintChildren = this.children.some(child => !(child instanceof StatementPrint));
103+
const hasName = guiName.trim() !== '';
104+
const hasValue = guiValue.trim() !== '';
105+
106+
if (hasNonPrintChildren) {
107+
// For containers: remove if no value and no GUI children
108+
if (!hasValue && !childGuiTree.hasGuiChildren()) {
109+
childGuiTree.detach();
110+
// Also remove if no name, no value (even if it has GUI children that will display)
111+
} else if (!hasName && !hasValue) {
112+
childGuiTree.detach();
113+
}
114+
}
96115
}
97116
}

src/views/component-viewer/test/integration/parser-evaluator/eval-interface/scvd-eval-interface.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const makeStubBase = (name: string): ScvdNode => ({
3333
getMember: jest.fn(),
3434
getDisplayLabel: jest.fn().mockReturnValue(name),
3535
getValueType: jest.fn(),
36+
getArraySize: jest.fn().mockResolvedValue(undefined),
3637
} as unknown as ScvdNode);
3738

3839
const makeContainer = (name: string, widthBytes: number, offsetBytes = 0): RefContainer => ({

0 commit comments

Comments
 (0)