@@ -610,3 +610,286 @@ for (const sample of gleamSamples) {
610610}
611611
612612console . log ( `Validated ${ gleamSamples . length } Gleam parser fixtures.` ) ;
613+
614+ // Test Makefile Parser
615+ const { parser : makefileParser } = await bundledRequire (
616+ "src/languages/makefile/parser.js" ,
617+ "acode-makefile-parser-test.cjs" ,
618+ ) ;
619+
620+ const makefileSamples = [
621+ {
622+ name : "rules, assignments, recipes, special targets" ,
623+ source : `CC := gcc
624+ CFLAGS = -Wall
625+
626+ .PHONY: all clean
627+
628+ all: main.o
629+ $(CC) $(CFLAGS) -o app main.o
630+
631+ clean:
632+ rm -f app main.o
633+ ` ,
634+ nodes : [
635+ "VariableAssignment" ,
636+ "TargetLine" ,
637+ "RecipeLine" ,
638+ "SpecialTarget" ,
639+ "NamedVarRef" ,
640+ "AssignOp" ,
641+ "RecipeStart" ,
642+ ] ,
643+ } ,
644+ {
645+ name : "conditionals, define, else ifeq" ,
646+ source : `ifeq ($(DEBUG),1)
647+ CFLAGS += -g
648+ else ifeq ($(DEBUG),2)
649+ CFLAGS += -g -O0
650+ else
651+ CFLAGS += -O2
652+ endif
653+
654+ define greet
655+ echo hello
656+ endef
657+ ` ,
658+ nodes : [
659+ "Conditional" ,
660+ "EqConditional" ,
661+ "ElsifClause" ,
662+ "ElseClause" ,
663+ "DefineDirective" ,
664+ "IfeqKw" ,
665+ "EndifKw" ,
666+ "DefineKw" ,
667+ "EndefKw" ,
668+ ] ,
669+ } ,
670+ {
671+ name : "includes, functions, automatic vars, vpath" ,
672+ source : `include config.mk
673+ -include local.mk
674+ vpath %.h include
675+
676+ SOURCES := $(wildcard src/*.c)
677+ OBJS := $(patsubst %.c,%.o,$(SOURCES))
678+ export PATH
679+ override CFLAGS += -std=c11
680+
681+ %.o: %.c
682+ $(CC) -c -o $@ $<
683+
684+ $(info Building $(words $(OBJS)) objects)
685+ ` ,
686+ nodes : [
687+ "IncludeDirective" ,
688+ "IncludeKw" ,
689+ "VpathDirective" ,
690+ "FunctionCall" ,
691+ "FunctionName" ,
692+ "FunctionStatement" ,
693+ "PatternWildcard" ,
694+ "ExportDirective" ,
695+ "OverrideDirective" ,
696+ "AutomaticVariable" ,
697+ ] ,
698+ } ,
699+ {
700+ name : "static pattern rule and line continue" ,
701+ source : `# build objects
702+ objects = foo.o \\
703+ bar.o
704+ $(objects): %.o: %.c
705+ $(CC) -c $(CFLAGS) $< -o $@
706+ ` ,
707+ nodes : [
708+ "Comment" ,
709+ "LineContinue" ,
710+ "TargetLine" ,
711+ "PatternWildcard" ,
712+ "RecipeLine" ,
713+ "AutomaticVariable" ,
714+ ] ,
715+ } ,
716+ {
717+ name : "order-only auto vars, gnu make 4.4 functions, combined directives" ,
718+ source : `.DEFAULT_GOAL := help
719+ export override CFLAGS += -Wall
720+ override define GREET
721+ @echo Hello
722+ endef
723+ private define LOG
724+ @echo Log
725+ endef
726+ prog: override CFLAGS += -g
727+
728+ app: main.o | lib.a
729+ $(CC) -o $@ $< $| $(|D) $(let x,1,$(x)) $(intcmp 1,2,lt,eq,gt)
730+ ` ,
731+ nodes : [
732+ "VariableAssignment" ,
733+ "ExportDirective" ,
734+ "OverrideDirective" ,
735+ "PrivateDirective" ,
736+ "DefineDirective" ,
737+ "TargetLine" ,
738+ "OrderOnlySep" ,
739+ "AutomaticVariable" ,
740+ "FunctionCall" ,
741+ "FunctionName" ,
742+ ] ,
743+ } ,
744+ {
745+ name : "non-start directive words stay plain identifiers" ,
746+ source : `vpath %.h include
747+ FOO else ifeq: bar
748+ ` ,
749+ nodes : [
750+ "VpathDirective" ,
751+ "TargetLine" ,
752+ ] ,
753+ } ,
754+ ] ;
755+
756+ for ( const sample of makefileSamples ) {
757+ const tree = makefileParser . parse ( sample . source ) ;
758+ const names = new Set ( ) ;
759+ const errors = [ ] ;
760+
761+ tree . iterate ( {
762+ enter ( node ) {
763+ names . add ( node . name ) ;
764+ if ( node . type . isError ) {
765+ errors . push ( [ node . from , node . to , sample . source . slice ( node . from , node . to ) ] ) ;
766+ }
767+ } ,
768+ } ) ;
769+
770+ assert . equal (
771+ errors . length ,
772+ 0 ,
773+ `${ sample . name } produced parse errors: ${ JSON . stringify ( errors ) } ` ,
774+ ) ;
775+ assert . equal (
776+ tree . length ,
777+ sample . source . length ,
778+ `${ sample . name } was not fully parsed` ,
779+ ) ;
780+ for ( const node of sample . nodes ) {
781+ assert ( names . has ( node ) , `${ sample . name } did not produce ${ node } ` ) ;
782+ }
783+ }
784+
785+ console . log ( `Validated ${ makefileSamples . length } Makefile parser fixtures.` ) ;
786+
787+ const makefileBehaviorBundle = path . join (
788+ os . tmpdir ( ) ,
789+ "acode-makefile-behavior-test-bundle.mjs" ,
790+ ) ;
791+ const makefileBehaviorSource = String . raw `
792+ import assert from "node:assert/strict";
793+ import { EditorState } from "@codemirror/state";
794+ import { foldable, getIndentation, indentUnit, syntaxTree } from "@codemirror/language";
795+ import { classHighlighter, highlightTree } from "@lezer/highlight";
796+ import { makefile, makefileLanguage } from "./src/languages/makefile/index.js";
797+
798+ const source = [
799+ "# Makefile fixture",
800+ "include config.mk",
801+ "CC := gcc",
802+ ".PHONY: all",
803+ "all: main.o",
804+ "\t$(CC) -o app $@",
805+ "ifeq ($(DEBUG),1)",
806+ " CFLAGS += -g",
807+ "endif",
808+ "$(info hello)",
809+ "",
810+ ].join("\n");
811+
812+ const tree = makefileLanguage.parser.parse(source);
813+ const names = new Set();
814+ const errors = [];
815+ tree.iterate({
816+ enter(node) {
817+ names.add(node.name);
818+ if (node.type.isError) errors.push([node.from, node.to]);
819+ },
820+ });
821+ assert.equal(errors.length, 0, "Makefile fixture produced parse errors");
822+ for (const name of [
823+ "Comment",
824+ "IncludeDirective",
825+ "IncludeKw",
826+ "VariableAssignment",
827+ "SpecialTarget",
828+ "TargetLine",
829+ "RecipeLine",
830+ "Conditional",
831+ "FunctionStatement",
832+ "FunctionName",
833+ "AutomaticVariable",
834+ ]) {
835+ assert(names.has(name), "Makefile fixture did not produce " + name);
836+ }
837+
838+ const spans = [];
839+ highlightTree(tree, classHighlighter, (from, to, cls) => {
840+ spans.push({ text: source.slice(from, to), cls });
841+ });
842+
843+ function isHighlighted(text, tokenClass) {
844+ return spans.some((span) => span.text === text && span.cls.includes(tokenClass));
845+ }
846+
847+ assert(spans.some((span) => span.cls.includes("tok-comment") && span.text.includes("Makefile fixture")));
848+ assert(isHighlighted("include", "tok-keyword"));
849+ assert(isHighlighted("ifeq", "tok-keyword"));
850+ assert(isHighlighted("endif", "tok-keyword"));
851+ assert(isHighlighted(":=", "tok-operator"));
852+ assert(isHighlighted("$@", "tok-variableName"));
853+ assert(isHighlighted("\t", "tok-meta"));
854+
855+ const indentSource = "ifeq (a,b)\nCFLAGS = 1\nelse\nCFLAGS = 0\nendif\n";
856+ const indentState = EditorState.create({
857+ doc: indentSource,
858+ extensions: [makefile(), indentUnit.of(" ")],
859+ });
860+ syntaxTree(indentState);
861+ assert.equal(getIndentation(indentState, indentState.doc.line(2).from), 2);
862+ assert.equal(getIndentation(indentState, indentState.doc.line(4).from), 2);
863+ assert.equal(getIndentation(indentState, indentState.doc.line(5).from), 0);
864+ assert(foldable(indentState, indentState.doc.line(1).from, indentState.doc.line(1).to));
865+
866+ const defineSource = "define greet\necho hi\nendef\n";
867+ const defineState = EditorState.create({
868+ doc: defineSource,
869+ extensions: [makefile()],
870+ });
871+ syntaxTree(defineState);
872+ assert(foldable(defineState, defineState.doc.line(1).from, defineState.doc.line(1).to));
873+
874+ console.log("Validated Makefile parsing, highlighting, indentation, and folding fixtures.");
875+ ` ;
876+
877+ await build ( {
878+ stdin : {
879+ contents : makefileBehaviorSource ,
880+ resolveDir : process . cwd ( ) ,
881+ sourcefile : "makefile-behavior-test.mjs" ,
882+ loader : "js" ,
883+ } ,
884+ bundle : true ,
885+ format : "esm" ,
886+ platform : "node" ,
887+ outfile : makefileBehaviorBundle ,
888+ logLevel : "silent" ,
889+ } ) ;
890+
891+ try {
892+ await import ( pathToFileURL ( makefileBehaviorBundle ) . href + `?t=${ Date . now ( ) } ` ) ;
893+ } finally {
894+ fs . rmSync ( makefileBehaviorBundle , { force : true } ) ;
895+ }
0 commit comments