From 3c5b577faee56705bc796da6d457f20139149176 Mon Sep 17 00:00:00 2001 From: geircodes Date: Fri, 24 Jul 2026 13:22:56 +0200 Subject: [PATCH] fix(hover): resolve GROUP(TypeName)/QUEUE(TypeName)/RECORD(TypeName) in extractClassName ClassMemberResolver.extractClassName() already unwrapped LIKE(TypeName) to get a navigable type name, but had no equivalent case for a class property declared as GROUP(TypeName)/QUEUE(TypeName)/RECORD(TypeName). Those fell through to the generic comma/paren split, which strips down to the bare keyword ("GROUP"), and since GROUP/QUEUE/RECORD are in CLARION_PRIMITIVES the type was rejected as "not navigable". This breaks ChainedPropertyResolver one segment past any GROUP-typed class property: SELF.Settings.Address (where Settings is declared as GROUP(ConnectionSettingsType) END) resolves hover fine on SELF and on Settings, but hovering Address silently returns nothing, even though the field is indexed correctly and locatable via a plain symbol search. Add a GROUP|QUEUE|RECORD (TypeName) case mirroring the existing LIKE() handling, so the chain keeps walking into the referenced type's fields. --- .../src/test/ChainedPropertyResolver.test.ts | 27 +++++++++++++++++++ server/src/utils/ClassMemberResolver.ts | 9 +++++++ 2 files changed, 36 insertions(+) diff --git a/server/src/test/ChainedPropertyResolver.test.ts b/server/src/test/ChainedPropertyResolver.test.ts index 3948ba0d..9fd25128 100644 --- a/server/src/test/ChainedPropertyResolver.test.ts +++ b/server/src/test/ChainedPropertyResolver.test.ts @@ -93,4 +93,31 @@ suite('ClassMemberResolver.extractClassName', () => { test('LIKE alone (no parens) — returns null', () => { assert.strictEqual(ClassMemberResolver.extractClassName('LIKE'), null); }); + + test('GROUP(TypeName) — returns the referenced type name', () => { + assert.strictEqual(ClassMemberResolver.extractClassName('GROUP(ConnectionSettingsType)'), 'ConnectionSettingsType'); + }); + + test('GROUP(TypeName) with trailing " END" (verbatim scanned declaration text) — real repro shape', () => { + // scanClassBodyForMember captures the whole line after the label, e.g. + // "Settings GROUP(ConnectionSettingsType) END" -> typeStr is + // "GROUP(ConnectionSettingsType) END", not just the paren part. + assert.strictEqual(ClassMemberResolver.extractClassName('GROUP(ConnectionSettingsType) END'), 'ConnectionSettingsType'); + }); + + test('QUEUE(TypeName) — returns the referenced type name', () => { + assert.strictEqual(ClassMemberResolver.extractClassName('QUEUE(BaseQueueType)'), 'BaseQueueType'); + }); + + test('RECORD(TypeName) — returns the referenced type name', () => { + assert.strictEqual(ClassMemberResolver.extractClassName('RECORD(SomeRecordType)'), 'SomeRecordType'); + }); + + test('GROUP(TypeName) with trailing attributes after the paren', () => { + assert.strictEqual(ClassMemberResolver.extractClassName('GROUP(FooType),DIM(2)'), 'FooType'); + }); + + test('GROUP keyword alone still returns null (unaffected by GROUP(TypeName) handling)', () => { + assert.strictEqual(ClassMemberResolver.extractClassName('GROUP'), null); + }); }); diff --git a/server/src/utils/ClassMemberResolver.ts b/server/src/utils/ClassMemberResolver.ts index be565af0..39e613af 100644 --- a/server/src/utils/ClassMemberResolver.ts +++ b/server/src/utils/ClassMemberResolver.ts @@ -874,6 +874,15 @@ export class ClassMemberResolver { const likeMatch = name.match(/^LIKE\s*\(\s*([\w:]+)\s*\)/i); if (likeMatch) return likeMatch[1]; + // GROUP(TypeName) / QUEUE(TypeName) / RECORD(TypeName) — structured-type property + // (e.g. "Settings GROUP(ConnectionSettingsType) END"): resolve to the referenced + // type name, same treatment as LIKE(TypeName) above. Without this, the comma/paren + // split below strips down to the bare keyword ("GROUP"), which then matches + // CLARION_PRIMITIVES and gets rejected as "not navigable" — breaking chained + // hover/member resolution (e.g. SELF.Settings.Address) one level too early. + const structRefMatch = name.match(/^(?:GROUP|QUEUE|RECORD)\s*\(\s*([\w:]+)\s*\)/i); + if (structRefMatch) return structRefMatch[1]; + // Take only the part before comma or parenthesis (attributes/dimensions) name = name.split(/[,(]/)[0].trim();