Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions internal/code_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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 {
Expand Down Expand Up @@ -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 + "`"
}
Comment on lines 216 to 224

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escape strings with control characters correctly

The revised escapeString now hand-builds a double-quoted literal but only escapes backslashes and quotes. If a helper produces a string containing a newline, tab, or other control characters, the function will emit the raw character inside "…", which is invalid Go syntax and causes generated files to fail to compile. The previous implementation used strconv.Quote which handled these escape sequences. Consider delegating to strconv.Quote (or adding explicit escapes for control characters) whenever you return a double-quoted literal.

Useful? React with 👍 / 👎.

Expand Down
6 changes: 5 additions & 1 deletion internal/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
`
Expand Down
17 changes: 16 additions & 1 deletion internal/function_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type FunctionExecutor struct {
prepared bool

stdImportMap map[string]string
stdListErr error
}

func NewFunctionExecutor(ctx *ProcessorContext) *FunctionExecutor {
Expand Down Expand Up @@ -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=<import path>", 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
}
Expand Down Expand Up @@ -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
}

Expand Down
19 changes: 19 additions & 0 deletions test/escape_string_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
33 changes: 33 additions & 0 deletions test/function_executor_error_test.go
Original file line number Diff line number Diff line change
@@ -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=<import path>") {
t.Fatalf("missing placeholder import hint: %s", message)
}
}
70 changes: 70 additions & 0 deletions test/unit_codegen_behaviour_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}