From de8bebdd69780815139c53b05b9f6b1acf641b4b Mon Sep 17 00:00:00 2001 From: ofabiodev Date: Tue, 11 Aug 2026 10:31:52 -0300 Subject: [PATCH 1/2] feat(cli): wire puff check to compiler --- src/internal/cli/check.go | 45 ++++++++++++++++++++++++++++++++---- src/internal/cli/cli_test.go | 45 ++++++++++++++++++++++++++++++++---- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/internal/cli/check.go b/src/internal/cli/check.go index 25412b2..1c2b890 100644 --- a/src/internal/cli/check.go +++ b/src/internal/cli/check.go @@ -1,19 +1,56 @@ package cli import ( - "fmt" + "encoding/json" + "errors" + "io" + "github.com/puff-lang/puff/internal/compiler" + "github.com/puff-lang/puff/internal/diagnostic" "github.com/spf13/cobra" ) +var errCheckFailed = errors.New("check failed") + func NewCheckCommand() *cobra.Command { - return &cobra.Command{ + var jsonOutput bool + + cmd := &cobra.Command{ Use: "check", Short: "Check a Puff project without generating output", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Fprintln(cmd.OutOrStdout(), "check") + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + result := compiler.Check(cmd.Context(), compiler.CheckOptions{StartDir: "."}) + + if jsonOutput { + encoder := json.NewEncoder(cmd.OutOrStdout()) + encoder.SetIndent("", " ") + if err := encoder.Encode(result.Diagnostics); err != nil { + return err + } + } else { + sources := make(map[string]string, len(result.Files)) + for _, file := range result.Files { + sources[file.RelPath] = file.Text + } + for _, issues := range [][]diagnostic.Diagnostic{result.Diagnostics.Errors, result.Diagnostics.Warnings} { + for _, issue := range issues { + if _, err := io.WriteString(cmd.ErrOrStderr(), diagnostic.FormatDiagnostic(issue, sources[issue.File])); err != nil { + return err + } + } + } + } + + if !result.Diagnostics.OK { + return errCheckFailed + } return nil }, } + + cmd.Flags().BoolVar(&jsonOutput, "json", false, "Print diagnostics as JSON") + + return cmd } diff --git a/src/internal/cli/cli_test.go b/src/internal/cli/cli_test.go index 4a5ccea..a5e5816 100644 --- a/src/internal/cli/cli_test.go +++ b/src/internal/cli/cli_test.go @@ -2,6 +2,8 @@ package cli_test import ( "bytes" + "os" + "path/filepath" "regexp" "testing" @@ -53,14 +55,22 @@ func TestInitCommand(t *testing.T) { } func TestCheckCommand(t *testing.T) { + root := writeCheckProject(t, "on load\nend\n") + nested := filepath.Join(root, "nested") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatalf("create nested directory: %v", err) + } + t.Chdir(nested) + output, err := executeCommand("check") if err != nil { t.Fatalf("expected no error, got %v", err) } - - expected := "check\n" - if output != expected { - t.Fatalf("expected %q, got %q", expected, output) + if output != "" { + t.Fatalf("expected no output, got %q", output) + } + if _, err := os.Stat(filepath.Join(root, "dist")); !os.IsNotExist(err) { + t.Fatalf("check created dist: %v", err) } } @@ -75,3 +85,30 @@ func TestBundleCommand(t *testing.T) { t.Fatalf("expected %q, got %q", expected, output) } } + +func writeCheckProject(t *testing.T, source string) string { + t.Helper() + + root := t.TempDir() + writeCheckFile(t, filepath.Join(root, "puff.toml"), `[pack] +id = "cli-check" +name = "CLI Check" + +[minecraft] +versions = ">=1.21 <=1.21.6" +target = "1.21.6" +`) + writeCheckFile(t, filepath.Join(root, "src", "main.puff"), source) + return root +} + +func writeCheckFile(t *testing.T, path string, content string) { + t.Helper() + + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("create directory for %s: %v", path, err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} From 61ea2be0cddfb6b97715c1ae6531335b2e3d740e Mon Sep 17 00:00:00 2001 From: ofabiodev Date: Tue, 11 Aug 2026 10:36:44 -0300 Subject: [PATCH 2/2] test(cli): cover puff check diagnostics --- src/internal/cli/cli_test.go | 193 ++++++++++++++++++++++++++++++++--- 1 file changed, 181 insertions(+), 12 deletions(-) diff --git a/src/internal/cli/cli_test.go b/src/internal/cli/cli_test.go index a5e5816..b9952b0 100644 --- a/src/internal/cli/cli_test.go +++ b/src/internal/cli/cli_test.go @@ -10,7 +10,7 @@ import ( "github.com/puff-lang/puff/internal/cli" ) -func executeCommand(args ...string) (string, error) { +func executeCommand(args ...string) (string, string, error) { cmd := cli.NewRootCommand() var out bytes.Buffer @@ -21,19 +21,17 @@ func executeCommand(args ...string) (string, error) { cmd.SetArgs(args) err := cmd.Execute() - - if errOut.Len() > 0 { - return out.String() + errOut.String(), err - } - - return out.String(), err + return out.String(), errOut.String(), err } func TestVersionCommand(t *testing.T) { - output, err := executeCommand("version") + output, errOutput, err := executeCommand("version") if err != nil { t.Fatalf("expected no error, got %v", err) } + if errOutput != "" { + t.Fatalf("expected no error output, got %q", errOutput) + } pattern := regexp.MustCompile(`(?m)^puff\s+\S+\ncommit:\s+\S+\ndate:\s+\S+\n$`) @@ -43,10 +41,13 @@ func TestVersionCommand(t *testing.T) { } func TestInitCommand(t *testing.T) { - output, err := executeCommand("init", "example") + output, errOutput, err := executeCommand("init", "example") if err != nil { t.Fatalf("expected no error, got %v", err) } + if errOutput != "" { + t.Fatalf("expected no error output, got %q", errOutput) + } expected := "init example\n" if output != expected { @@ -62,23 +63,185 @@ func TestCheckCommand(t *testing.T) { } t.Chdir(nested) - output, err := executeCommand("check") + output, errOutput, err := executeCommand("check") if err != nil { t.Fatalf("expected no error, got %v", err) } if output != "" { t.Fatalf("expected no output, got %q", output) } + if errOutput != "" { + t.Fatalf("expected no error output, got %q", errOutput) + } + if _, err := os.Stat(filepath.Join(root, "dist")); !os.IsNotExist(err) { + t.Fatalf("check created dist: %v", err) + } +} + +func TestCheckCommandPrintsHumanDiagnostics(t *testing.T) { + root := writeCheckProject(t, "# tags: load\n\non tick\nend\n") + t.Chdir(root) + + output, errOutput, err := executeCommand("check") + if err == nil { + t.Fatal("expected check to fail") + } + expected := "error[MISSING_LOAD_EVENT]: Missing required event: on load\n" + + " --> main.puff:1:1\n" + + " |\n" + + " 1 | # tags: load\n" + + " | ^^^^^^^^^^^^\n" + + " |\n" + + " = hint: Add an on load block or remove the load tag.\n" + if output != "" { + t.Fatalf("expected no standard output, got %q", output) + } + if errOutput != expected { + t.Fatalf("expected %q, got %q", expected, errOutput) + } + if _, err := os.Stat(filepath.Join(root, "dist")); !os.IsNotExist(err) { + t.Fatalf("check created dist: %v", err) + } +} + +func TestCheckCommandPrintsJSONDiagnostics(t *testing.T) { + root := writeCheckProject(t, "# tags: load\n\non tick\nend\n") + t.Chdir(root) + + output, errOutput, err := executeCommand("check", "--json") + if err == nil { + t.Fatal("expected check to fail") + } + if errOutput != "" { + t.Fatalf("expected no error output, got %q", errOutput) + } + expected := "{\n" + + " \"ok\": false,\n" + + " \"errors\": [\n" + + " {\n" + + " \"code\": \"MISSING_LOAD_EVENT\",\n" + + " \"phase\": \"SEMANTICS\",\n" + + " \"severity\": \"ERROR\",\n" + + " \"message\": \"Missing required event: on load\",\n" + + " \"hint\": \"Add an on load block or remove the load tag.\",\n" + + " \"file\": \"main.puff\",\n" + + " \"span\": {\n" + + " \"startLine\": 1,\n" + + " \"startColumn\": 1,\n" + + " \"endLine\": 1,\n" + + " \"endColumn\": 13,\n" + + " \"startOffset\": 0,\n" + + " \"endOffset\": 12\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"warnings\": []\n" + + "}\n" + if output != expected { + t.Fatalf("expected %q, got %q", expected, output) + } + if _, err := os.Stat(filepath.Join(root, "dist")); !os.IsNotExist(err) { + t.Fatalf("check created dist: %v", err) + } +} + +func TestCheckCommandPrintsSuccessfulJSON(t *testing.T) { + root := writeCheckProject(t, "on load\nend\n") + t.Chdir(root) + + output, errOutput, err := executeCommand("check", "--json") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + expected := "{\n" + + " \"ok\": true,\n" + + " \"errors\": [],\n" + + " \"warnings\": []\n" + + "}\n" + if output != expected { + t.Fatalf("expected %q, got %q", expected, output) + } + if errOutput != "" { + t.Fatalf("expected no error output, got %q", errOutput) + } + if _, err := os.Stat(filepath.Join(root, "dist")); !os.IsNotExist(err) { + t.Fatalf("check created dist: %v", err) + } +} + +func TestCheckCommandUsesNearestProject(t *testing.T) { + outer := t.TempDir() + writeCheckProjectAt(t, outer, "# tags: load\n\non tick\nend\n") + inner := filepath.Join(outer, "nested") + writeCheckProjectAt(t, inner, "on load\nend\n") + workingDir := filepath.Join(inner, "deep") + if err := os.MkdirAll(workingDir, 0o755); err != nil { + t.Fatalf("create working directory: %v", err) + } + t.Chdir(workingDir) + + output, errOutput, err := executeCommand("check") + if err != nil { + t.Fatalf("expected nearest project to pass, got %v", err) + } + if output != "" || errOutput != "" { + t.Fatalf("expected no output, got stdout %q and stderr %q", output, errOutput) + } + if _, err := os.Stat(filepath.Join(inner, "dist")); !os.IsNotExist(err) { + t.Fatalf("check created inner dist: %v", err) + } + if _, err := os.Stat(filepath.Join(outer, "dist")); !os.IsNotExist(err) { + t.Fatalf("check created outer dist: %v", err) + } +} + +func TestCheckCommandReportsMissingPuffTOML(t *testing.T) { + root := t.TempDir() + t.Chdir(root) + + output, errOutput, err := executeCommand("check") + if err == nil { + t.Fatal("expected check to fail") + } + expected := "error[MISSING_PUFF_TOML]: Missing puff.toml.\n" + + " --> :1:1\n" + + " |\n" + + " = hint: Run puff init to create a new project.\n" + if output != "" { + t.Fatalf("expected no standard output, got %q", output) + } + if errOutput != expected { + t.Fatalf("expected %q, got %q", expected, errOutput) + } + if _, err := os.Stat(filepath.Join(root, "dist")); !os.IsNotExist(err) { + t.Fatalf("check created dist: %v", err) + } +} + +func TestCheckCommandRejectsArguments(t *testing.T) { + root := writeCheckProject(t, "on load\nend\n") + t.Chdir(root) + + output, errOutput, err := executeCommand("check", "unexpected") + if err == nil { + t.Fatal("expected check to reject arguments") + } + if output != "" || errOutput != "" { + t.Fatalf("expected no output, got stdout %q and stderr %q", output, errOutput) + } if _, err := os.Stat(filepath.Join(root, "dist")); !os.IsNotExist(err) { t.Fatalf("check created dist: %v", err) } } func TestBundleCommand(t *testing.T) { - output, err := executeCommand("bundle", "--target", "1.21.6", "--output", "dist") + output, errOutput, err := executeCommand("bundle", "--target", "1.21.6", "--output", "dist") if err != nil { t.Fatalf("expected no error, got %v", err) } + if errOutput != "" { + t.Fatalf("expected no error output, got %q", errOutput) + } expected := "bundle --target 1.21.6 --output dist\n" if output != expected { @@ -90,6 +253,13 @@ func writeCheckProject(t *testing.T, source string) string { t.Helper() root := t.TempDir() + writeCheckProjectAt(t, root, source) + return root +} + +func writeCheckProjectAt(t *testing.T, root string, source string) { + t.Helper() + writeCheckFile(t, filepath.Join(root, "puff.toml"), `[pack] id = "cli-check" name = "CLI Check" @@ -99,7 +269,6 @@ versions = ">=1.21 <=1.21.6" target = "1.21.6" `) writeCheckFile(t, filepath.Join(root, "src", "main.puff"), source) - return root } func writeCheckFile(t *testing.T, path string, content string) {