diff --git a/internal/code_processor.go b/internal/code_processor.go index 23ede75..30342f2 100644 --- a/internal/code_processor.go +++ b/internal/code_processor.go @@ -62,6 +62,7 @@ func (cp *CodeProcessor) processLines(file *os.File, filePath string, verbose bo commentPattern := regexp.MustCompile(CommentPattern) +Outer: for scanner.Scan() { line := scanner.Text() @@ -74,17 +75,26 @@ func (cp *CodeProcessor) processLines(file *os.File, filePath string, verbose bo lines = append(lines, line) - if scanner.Scan() { + for { + if !scanner.Scan() { + break Outer + } nextLine := scanner.Text() + if strings.TrimSpace(nextLine) == "" { + lines = append(lines, nextLine) + continue + } newLine, wasModified := cp.processCodeLine(nextLine, funcName, argsStr, filePath, verbose) lines = append(lines, newLine) if wasModified { modified = true } + break } - } else { - lines = append(lines, line) + continue } + + lines = append(lines, line) } if err := scanner.Err(); err != nil { @@ -204,6 +214,11 @@ func (cp *CodeProcessor) writeFile(filePath string, lines []string) error { } func escapeString(s string) string { + if strings.Contains(s, "`") { + escaped := strings.ReplaceAll(s, "\\", "\\\\") + escaped = strings.ReplaceAll(escaped, "\"", "\\\"") + return `"` + escaped + `"` + } if strings.Contains(s, "\\") { return "`" + s + "`" } diff --git a/internal/constants.go b/internal/constants.go index b27893a..5028ddf 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -17,8 +17,12 @@ import ( {{.UserCode}} {{- end}} +func goaheadFirst[T any](v T, _ ...any) T { + return v +} + func main() { - result := {{.CallExpr}} + result := goaheadFirst({{.CallExpr}}) {{.FmtAlias}}.Printf("%#v", result) } ` diff --git a/internal/function_executor.go b/internal/function_executor.go index 3055636..3d2f651 100644 --- a/internal/function_executor.go +++ b/internal/function_executor.go @@ -62,6 +62,7 @@ type FunctionExecutor struct { prepared bool stdImportMap map[string]string + stdListErr error } func NewFunctionExecutor(ctx *ProcessorContext) *FunctionExecutor { @@ -114,7 +115,15 @@ func (fe *FunctionExecutor) ExecuteFunction(funcName string, argsStr string) (st result, err := fe.executeProgram(program) if err != nil { if target.kind == invocationExternal && !target.importResolved { - return "", fmt.Errorf("%w. Add //go:ahead import %s=%s in a function file to declare the package alias", err, target.packageAlias, target.packagePath) + suggestion := fmt.Sprintf("%s=%s", target.packageAlias, target.packagePath) + if target.packagePath == target.packageAlias && fe.stdListErr != nil { + suggestion = fmt.Sprintf("%s=", target.packageAlias) + } + extra := "" + if fe.stdListErr != nil { + extra = fmt.Sprintf(" (automatic standard library resolution failed: %v)", fe.stdListErr) + } + return "", fmt.Errorf("%w. Add //go:ahead import %s in a function file to declare the package alias%s", err, suggestion, extra) } return "", err } @@ -605,6 +614,12 @@ func (fe *FunctionExecutor) ensureStdImportMap() { cmd.Env = sanitizeGoEnv(os.Environ()) output, err := cmd.CombinedOutput() if err != nil { + trimmed := strings.TrimSpace(string(output)) + if trimmed != "" { + fe.stdListErr = fmt.Errorf("go list std: %w: %s", err, trimmed) + } else { + fe.stdListErr = fmt.Errorf("go list std: %w", err) + } return } diff --git a/test/escape_string_test.go b/test/escape_string_test.go new file mode 100644 index 0000000..14ceccf --- /dev/null +++ b/test/escape_string_test.go @@ -0,0 +1,19 @@ +package test + +import ( + "strconv" + "testing" + _ "unsafe" +) + +//go:linkname escapeString github.com/AeonDave/goahead/internal.escapeString +func escapeString(string) string + +func TestEscapeStringPrefersQuotedLiteralWhenBacktickPresent(t *testing.T) { + input := "path\\`dir" + got := escapeString(input) + want := strconv.Quote(input) + if got != want { + t.Fatalf("escapeString(%q) = %q, want %q", input, got, want) + } +} diff --git a/test/function_executor_error_test.go b/test/function_executor_error_test.go new file mode 100644 index 0000000..f1301bc --- /dev/null +++ b/test/function_executor_error_test.go @@ -0,0 +1,33 @@ +package test + +import ( + "strings" + "testing" + + internal "github.com/AeonDave/goahead/internal" +) + +func TestExecuteFunctionReportsStdlibResolutionFailure(t *testing.T) { + ctx := &internal.ProcessorContext{ + Functions: make(map[string]*internal.UserFunction), + FuncFiles: nil, + TempDir: t.TempDir(), + ImportOverrides: make(map[string]string), + } + executor := internal.NewFunctionExecutor(ctx) + + t.Setenv("PATH", "") + + _, err := executor.ExecuteFunction("http.DetectContentType", `"data"`) + if err == nil { + t.Fatalf("expected error when go toolchain is unavailable") + } + + message := err.Error() + if !strings.Contains(message, "automatic standard library resolution failed") { + t.Fatalf("missing stdlib resolution hint in error: %s", message) + } + if !strings.Contains(message, "http=") { + t.Fatalf("missing placeholder import hint: %s", message) + } +} diff --git a/test/unit_codegen_behaviour_test.go b/test/unit_codegen_behaviour_test.go index 25bdee7..3b3146a 100644 --- a/test/unit_codegen_behaviour_test.go +++ b/test/unit_codegen_behaviour_test.go @@ -86,3 +86,73 @@ func main() { } } } + +func TestRunCodegenSkipsBlankLinesAfterComment(t *testing.T) { + dir := t.TempDir() + + writeFile(t, dir, "helpers.go", `//go:build exclude +//go:ahead functions + +package main + +func makeGreeting(name string) string { return "Hello, " + name } +`) + + writeFile(t, dir, "main.go", `package main + +var ( + //:makeGreeting:"gopher" + + greeting = "" +) +`) + + if err := internal.RunCodegen(dir, false); err != nil { + t.Fatalf("RunCodegen failed: %v", err) + } + + content, err := os.ReadFile(filepath.Join(dir, "main.go")) + if err != nil { + t.Fatalf("read main.go: %v", err) + } + got := string(content) + + expected := "//:makeGreeting:\"gopher\"\n\n greeting = \"Hello, gopher\"" + if !strings.Contains(got, expected) { + t.Fatalf("output missing expected block\nwant:\n%s\n---- got ----\n%s", expected, got) + } +} + +func TestRunCodegenHandlesMultiReturnFunctions(t *testing.T) { + dir := t.TempDir() + + writeFile(t, dir, "helpers.go", `//go:build exclude +//go:ahead functions + +package main + +func fetchValue() (string, error) { return "multi", nil } +`) + + writeFile(t, dir, "main.go", `package main + +var ( + //:fetchValue + result = "" +) +`) + + if err := internal.RunCodegen(dir, false); err != nil { + t.Fatalf("RunCodegen failed: %v", err) + } + + content, err := os.ReadFile(filepath.Join(dir, "main.go")) + if err != nil { + t.Fatalf("read main.go: %v", err) + } + got := string(content) + + if !strings.Contains(got, `result = "multi"`) { + t.Fatalf("result assignment not replaced\n---- got ----\n%s", got) + } +}