import { describe, it, expect } from 'vitest' import { isImportLine, parseDeclaration, parseNamespace, parseTraitUse } from '../src/main/symbols-service' import { wordAt } from '../src/renderer/src/editor' describe('parseDeclaration', () => { it('reads every declaration keyword', () => { expect(parseDeclaration('class Agent')).toMatchObject({ kind: 'class', name: 'Agent' }) expect(parseDeclaration('interface Searchable {')).toMatchObject({ kind: 'interface', name: 'Searchable' }) expect(parseDeclaration('trait HasVault')).toMatchObject({ kind: 'trait', name: 'HasVault' }) expect(parseDeclaration('enum Status: string')).toMatchObject({ kind: 'enum', name: 'Status' }) }) it('reads the parents off the declaration line, however many', () => { const d = parseDeclaration('final class VaultSearch extends Base implements Searchable, Arrayable, Jsonable {') expect(d?.parents).toEqual(['Base']) expect(d?.interfaces).toEqual(['Searchable', 'Arrayable', 'Jsonable']) }) it('drops the namespace of a fully qualified parent', () => { const d = parseDeclaration('class A extends \\App\\Base implements \\App\\Contracts\\Runs') expect(d?.parents).toEqual(['Base']) expect(d?.interfaces).toEqual(['Runs']) }) it('reads an interface extending several interfaces, and an enum contract', () => { expect(parseDeclaration('interface A extends B, C')?.parents).toEqual(['B', 'C']) expect(parseDeclaration('enum Status: string implements HasLabel')?.interfaces).toEqual(['HasLabel']) }) it('leaves the lists empty when there is no clause', () => { const d = parseDeclaration('class Plain {') expect(d?.parents).toEqual([]) expect(d?.interfaces).toEqual([]) }) it('accepts the modifiers PHP allows in front', () => { expect(parseDeclaration('final class A extends B')?.name).toBe('A') expect(parseDeclaration('abstract class A')?.name).toBe('A') expect(parseDeclaration('final readonly class A')?.name).toBe('A') expect(parseDeclaration(' class Nested')?.name).toBe('Nested') }) it('ignores a mention that is not a declaration', () => { expect(parseDeclaration('$x = Agent::class;')).toBeNull() expect(parseDeclaration('// class Agent lives here')).toBeNull() expect(parseDeclaration('use App\\Models\\Agent;')).toBeNull() expect(parseDeclaration('return $a instanceof Agent;')).toBeNull() }) }) describe('isImportLine', () => { it('drops a namespace import', () => { expect(isImportLine('use App\\Models\\Agent;')).toBe(true) expect(isImportLine('use App\\Support\\Str as S;')).toBe(true) expect(isImportLine('use function App\\Support\\tap;')).toBe(true) }) it('keeps a trait mixed into a class body', () => { // The indent is the whole difference: a trait `use` is inside the class. expect(isImportLine(' use HasFactory;')).toBe(false) expect(isImportLine('\tuse HasFactory;')).toBe(false) }) it('keeps real code that merely contains the word', () => { expect(isImportLine('$f = function () use ($agent) {};')).toBe(false) expect(isImportLine('// use Agent for this')).toBe(false) }) }) describe('parseNamespace', () => { it('reads the namespace of a file', () => { expect(parseNamespace('namespace App\\Console\\Commands;')).toBe('App\\Console\\Commands') expect(parseNamespace('namespace App;')).toBe('App') }) it('returns null for anything else', () => { expect(parseNamespace('use App\\Models\\Agent;')).toBeNull() expect(parseNamespace('class A {')).toBeNull() }) }) describe('parseTraitUse', () => { it('reads one or several traits from an indented use', () => { expect(parseTraitUse(' use HasFactory;')).toEqual(['HasFactory']) expect(parseTraitUse(' use HasFactory, Notifiable, SoftDeletes;')).toEqual(['HasFactory', 'Notifiable', 'SoftDeletes']) expect(parseTraitUse('\tuse App\\Support\\HasVault;')).toEqual(['HasVault']) }) it('reads a use with a conflict-resolution block', () => { expect(parseTraitUse(' use A, B { A::run insteadof B; }')).toEqual(['A', 'B']) }) it('ignores what is not a trait', () => { expect(parseTraitUse('use App\\Models\\Agent;')).toEqual([]) // import, not indented expect(parseTraitUse(' use function App\\tap;')).toEqual([]) expect(parseTraitUse(' $f = function () use ($x) {};')).toEqual([]) }) }) describe('wordAt', () => { const line = 'return new Agent($x);' it('reads the identifier under the caret', () => { expect(wordAt(line, 12)).toBe('Agent') // inside expect(wordAt(line, 11)).toBe('Agent') // left edge expect(wordAt(line, 16)).toBe('Agent') // right edge }) it('returns empty off an identifier', () => { expect(wordAt(line, 17)).toBe('') // inside "($x" expect(wordAt(' ', 1)).toBe('') }) it('rejects a word that cannot be a class name', () => { expect(wordAt('$x = 42;', 6)).toBe('') // digits only }) it('handles a caret at the string edges', () => { expect(wordAt('Agent', 0)).toBe('Agent') expect(wordAt('Agent', 5)).toBe('Agent') expect(wordAt('Agent', 9)).toBe('') }) })