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();