Skip to content

Commit a367b14

Browse files
SevInfclaude
andcommitted
test(psl-parser): add AST accessor and red tree tests to satisfy coverage gate
Cover all previously untested AST wrapper methods (EnumDeclarationAst, TypesBlockAst, BlockDeclarationAst, NamedTypeDeclarationAst, FieldDeclarationAst.attributes, FunctionCallAst, ModelAttributeAst.argList) and SyntaxNode.textLength/prevSibling to bring functions coverage to 100%. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 34c8ced commit a367b14

2 files changed

Lines changed: 287 additions & 0 deletions

File tree

packages/1-framework/2-authoring/psl-parser/test/syntax/ast.test.ts

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,3 +531,254 @@ describe('DocumentAst', () => {
531531
expect(decls[1]).toBeInstanceOf(EnumDeclarationAst);
532532
});
533533
});
534+
535+
describe('EnumDeclarationAst', () => {
536+
function buildEnum() {
537+
const b = new GreenNodeBuilder();
538+
b.startNode('EnumDeclaration');
539+
b.token('Ident', 'enum');
540+
b.token('Whitespace', ' ');
541+
b.startNode('Identifier');
542+
b.token('Ident', 'Role');
543+
b.finishNode();
544+
b.token('Whitespace', ' ');
545+
b.token('LBrace', '{');
546+
b.token('Newline', '\n');
547+
b.token('Whitespace', ' ');
548+
b.startNode('EnumValueDeclaration');
549+
b.startNode('Identifier');
550+
b.token('Ident', 'ADMIN');
551+
b.finishNode();
552+
b.finishNode();
553+
b.token('Newline', '\n');
554+
b.token('Whitespace', ' ');
555+
b.startNode('EnumValueDeclaration');
556+
b.startNode('Identifier');
557+
b.token('Ident', 'USER');
558+
b.finishNode();
559+
b.finishNode();
560+
b.token('Newline', '\n');
561+
b.token('RBrace', '}');
562+
return b.finishNode();
563+
}
564+
565+
it('exposes keyword, name, braces', () => {
566+
const root = createSyntaxTree(buildEnum());
567+
const decl = EnumDeclarationAst.cast(root)!;
568+
expect(decl.keyword()?.text).toBe('enum');
569+
expect(decl.name()?.token()?.text).toBe('Role');
570+
expect(decl.lbrace()?.text).toBe('{');
571+
expect(decl.rbrace()?.text).toBe('}');
572+
});
573+
574+
it('iterates values', () => {
575+
const root = createSyntaxTree(buildEnum());
576+
const decl = EnumDeclarationAst.cast(root)!;
577+
const values = Array.from(decl.values());
578+
expect(values).toHaveLength(2);
579+
expect(values[0]!.name()?.token()?.text).toBe('ADMIN');
580+
expect(values[1]!.name()?.token()?.text).toBe('USER');
581+
});
582+
});
583+
584+
describe('TypesBlockAst', () => {
585+
function buildTypesBlock() {
586+
const b = new GreenNodeBuilder();
587+
b.startNode('TypesBlock');
588+
b.token('Ident', 'type');
589+
b.token('Whitespace', ' ');
590+
b.token('LBrace', '{');
591+
b.token('Newline', '\n');
592+
b.token('Whitespace', ' ');
593+
b.startNode('NamedTypeDeclaration');
594+
b.startNode('Identifier');
595+
b.token('Ident', 'UserId');
596+
b.finishNode();
597+
b.token('Whitespace', ' ');
598+
b.token('Equals', '=');
599+
b.token('Whitespace', ' ');
600+
b.startNode('TypeAnnotation');
601+
b.token('Ident', 'Int');
602+
b.finishNode();
603+
b.finishNode();
604+
b.token('Newline', '\n');
605+
b.token('RBrace', '}');
606+
return b.finishNode();
607+
}
608+
609+
it('exposes keyword, braces', () => {
610+
const root = createSyntaxTree(buildTypesBlock());
611+
const decl = TypesBlockAst.cast(root)!;
612+
expect(decl.keyword()?.text).toBe('type');
613+
expect(decl.lbrace()?.text).toBe('{');
614+
expect(decl.rbrace()?.text).toBe('}');
615+
});
616+
617+
it('iterates declarations', () => {
618+
const root = createSyntaxTree(buildTypesBlock());
619+
const decl = TypesBlockAst.cast(root)!;
620+
const namedTypes = Array.from(decl.declarations());
621+
expect(namedTypes).toHaveLength(1);
622+
expect(namedTypes[0]!.name()?.token()?.text).toBe('UserId');
623+
});
624+
});
625+
626+
describe('NamedTypeDeclarationAst', () => {
627+
function buildNamedType() {
628+
const b = new GreenNodeBuilder();
629+
b.startNode('NamedTypeDeclaration');
630+
b.startNode('Identifier');
631+
b.token('Ident', 'UserId');
632+
b.finishNode();
633+
b.token('Whitespace', ' ');
634+
b.token('Equals', '=');
635+
b.token('Whitespace', ' ');
636+
b.startNode('TypeAnnotation');
637+
b.startNode('Identifier');
638+
b.token('Ident', 'Int');
639+
b.finishNode();
640+
b.finishNode();
641+
b.token('Whitespace', ' ');
642+
b.startNode('FieldAttribute');
643+
b.token('At', '@');
644+
b.startNode('Identifier');
645+
b.token('Ident', 'db');
646+
b.finishNode();
647+
b.finishNode();
648+
return b.finishNode();
649+
}
650+
651+
it('exposes name, equals, typeAnnotation, attributes', () => {
652+
const root = createSyntaxTree(buildNamedType());
653+
const decl = NamedTypeDeclarationAst.cast(root)!;
654+
expect(decl.name()?.token()?.text).toBe('UserId');
655+
expect(decl.equals()?.text).toBe('=');
656+
expect(decl.typeAnnotation()?.name()?.token()?.text).toBe('Int');
657+
const attrs = Array.from(decl.attributes());
658+
expect(attrs).toHaveLength(1);
659+
expect(attrs[0]!.name()?.token()?.text).toBe('db');
660+
});
661+
});
662+
663+
describe('BlockDeclarationAst', () => {
664+
function buildBlock() {
665+
const b = new GreenNodeBuilder();
666+
b.startNode('BlockDeclaration');
667+
b.token('Ident', 'datasource');
668+
b.token('Whitespace', ' ');
669+
b.startNode('Identifier');
670+
b.token('Ident', 'db');
671+
b.finishNode();
672+
b.token('Whitespace', ' ');
673+
b.token('LBrace', '{');
674+
b.token('Newline', '\n');
675+
b.token('Whitespace', ' ');
676+
b.startNode('KeyValuePair');
677+
b.startNode('Identifier');
678+
b.token('Ident', 'provider');
679+
b.finishNode();
680+
b.token('Whitespace', ' ');
681+
b.token('Equals', '=');
682+
b.token('Whitespace', ' ');
683+
b.startNode('StringLiteralExpr');
684+
b.token('StringLiteral', '"postgresql"');
685+
b.finishNode();
686+
b.finishNode();
687+
b.token('Newline', '\n');
688+
b.token('RBrace', '}');
689+
return b.finishNode();
690+
}
691+
692+
it('exposes keyword, name, braces', () => {
693+
const root = createSyntaxTree(buildBlock());
694+
const decl = BlockDeclarationAst.cast(root)!;
695+
expect(decl.keyword()?.text).toBe('datasource');
696+
expect(decl.name()?.token()?.text).toBe('db');
697+
expect(decl.lbrace()?.text).toBe('{');
698+
expect(decl.rbrace()?.text).toBe('}');
699+
});
700+
701+
it('iterates entries', () => {
702+
const root = createSyntaxTree(buildBlock());
703+
const decl = BlockDeclarationAst.cast(root)!;
704+
const entries = Array.from(decl.entries());
705+
expect(entries).toHaveLength(1);
706+
expect(entries[0]!.key()?.token()?.text).toBe('provider');
707+
});
708+
});
709+
710+
describe('FieldDeclarationAst.attributes', () => {
711+
it('iterates field attributes', () => {
712+
const b = new GreenNodeBuilder();
713+
b.startNode('FieldDeclaration');
714+
b.startNode('Identifier');
715+
b.token('Ident', 'id');
716+
b.finishNode();
717+
b.token('Whitespace', ' ');
718+
b.startNode('TypeAnnotation');
719+
b.token('Ident', 'Int');
720+
b.finishNode();
721+
b.token('Whitespace', ' ');
722+
b.startNode('FieldAttribute');
723+
b.token('At', '@');
724+
b.startNode('Identifier');
725+
b.token('Ident', 'id');
726+
b.finishNode();
727+
b.finishNode();
728+
const root = createSyntaxTree(b.finishNode());
729+
const field = FieldDeclarationAst.cast(root)!;
730+
const attrs = Array.from(field.attributes());
731+
expect(attrs).toHaveLength(1);
732+
expect(attrs[0]!.name()?.token()?.text).toBe('id');
733+
});
734+
});
735+
736+
describe('FunctionCallAst', () => {
737+
it('exposes name, parens, and args', () => {
738+
const b = new GreenNodeBuilder();
739+
b.startNode('FunctionCall');
740+
b.startNode('Identifier');
741+
b.token('Ident', 'autoincrement');
742+
b.finishNode();
743+
b.token('LParen', '(');
744+
b.token('RParen', ')');
745+
const root = createSyntaxTree(b.finishNode());
746+
const fn = FunctionCallAst.cast(root)!;
747+
expect(fn.name()?.token()?.text).toBe('autoincrement');
748+
expect(fn.lparen()?.text).toBe('(');
749+
expect(fn.rparen()?.text).toBe(')');
750+
expect(Array.from(fn.args())).toHaveLength(0);
751+
});
752+
});
753+
754+
describe('ModelAttributeAst.argList', () => {
755+
it('exposes argList with args', () => {
756+
// @@unique([email])
757+
const b = new GreenNodeBuilder();
758+
b.startNode('ModelAttribute');
759+
b.token('DoubleAt', '@@');
760+
b.startNode('Identifier');
761+
b.token('Ident', 'unique');
762+
b.finishNode();
763+
b.startNode('AttributeArgList');
764+
b.token('LParen', '(');
765+
b.startNode('AttributeArg');
766+
b.startNode('ArrayLiteral');
767+
b.token('LBracket', '[');
768+
b.startNode('Identifier');
769+
b.token('Ident', 'email');
770+
b.finishNode();
771+
b.token('RBracket', ']');
772+
b.finishNode();
773+
b.finishNode();
774+
b.token('RParen', ')');
775+
b.finishNode();
776+
const root = createSyntaxTree(b.finishNode());
777+
const attr = ModelAttributeAst.cast(root)!;
778+
const argList = attr.argList();
779+
expect(argList).toBeDefined();
780+
expect(argList!.lparen()?.text).toBe('(');
781+
const args = Array.from(argList!.args());
782+
expect(args).toHaveLength(1);
783+
});
784+
});

packages/1-framework/2-authoring/psl-parser/test/syntax/red.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,42 @@ describe('SyntaxNode.nextSibling / prevSibling', () => {
152152
expect(root.nextSibling).toBeUndefined();
153153
expect(root.prevSibling).toBeUndefined();
154154
});
155+
156+
it('navigates prevSibling from last child back', () => {
157+
const b = new GreenNodeBuilder();
158+
b.startNode('Document');
159+
b.startNode('Identifier');
160+
b.token('Ident', 'A');
161+
b.finishNode();
162+
b.token('Whitespace', ' ');
163+
b.startNode('Identifier');
164+
b.token('Ident', 'B');
165+
b.finishNode();
166+
const green = b.finishNode();
167+
const root = createSyntaxTree(green);
168+
169+
// Get the last child node (Identifier "B")
170+
const children = Array.from(root.children());
171+
const lastNode = children[2]; // Identifier "B"
172+
expect(lastNode).toBeInstanceOf(SyntaxNode);
173+
if (lastNode instanceof SyntaxNode) {
174+
const prev = lastNode.prevSibling;
175+
expect(prev).toBeDefined();
176+
expect(prev).not.toBeInstanceOf(SyntaxNode);
177+
if (prev && !(prev instanceof SyntaxNode)) {
178+
expect(prev.kind).toBe('Whitespace');
179+
}
180+
}
181+
});
182+
});
183+
184+
describe('SyntaxNode.textLength', () => {
185+
it('returns total text length of the subtree', () => {
186+
const source = 'model User {\n id Int @id\n}';
187+
const green = buildSampleTree();
188+
const root = createSyntaxTree(green);
189+
expect(root.textLength).toBe(source.length);
190+
});
155191
});
156192

157193
describe('SyntaxNode.ancestors', () => {

0 commit comments

Comments
 (0)