diff --git a/cmd/muxt/testdata/err_path_method_collision.txt b/cmd/muxt/testdata/err_path_method_collision.txt new file mode 100644 index 0000000..19dd600 --- /dev/null +++ b/cmd/muxt/testdata/err_path_method_collision.txt @@ -0,0 +1,35 @@ +! muxt generate --use-receiver-type=T + +stderr 'TemplateRoutePaths method name collision: handlers list and List both produce method List' + +-- template.gohtml -- +{{define "GET /items list(ctx)" }} + +{{end}} + +{{define "GET /items/{id} List(ctx, id)" }} +
  • +{{end}} + +-- go.mod -- +module example.com + +go 1.22 +-- template.go -- +package server + +import ( + "context" + "embed" + "html/template" +) + +//go:embed *.gohtml +var formHTML embed.FS + +var templates = template.Must(template.ParseFS(formHTML, "*")) + +type T struct{} + +func (T) list(_ context.Context) []string { return nil } +func (T) List(_ context.Context, _ string) string { return "" } diff --git a/cmd/muxt/testdata/err_path_method_unexportable.txt b/cmd/muxt/testdata/err_path_method_unexportable.txt new file mode 100644 index 0000000..20a8af3 --- /dev/null +++ b/cmd/muxt/testdata/err_path_method_unexportable.txt @@ -0,0 +1,30 @@ +! muxt generate --use-receiver-type=T + +stderr 'cannot export identifier "_list" for TemplateRoutePaths method' + +-- template.gohtml -- +{{define "GET /items _list(ctx)" }} + +{{end}} + +-- go.mod -- +module example.com + +go 1.22 +-- template.go -- +package server + +import ( + "context" + "embed" + "html/template" +) + +//go:embed *.gohtml +var formHTML embed.FS + +var templates = template.Must(template.ParseFS(formHTML, "*")) + +type T struct{} + +func (T) _list(_ context.Context) []string { return nil } diff --git a/cmd/muxt/testdata/reference_path_unexported_method.txt b/cmd/muxt/testdata/reference_path_unexported_method.txt new file mode 100644 index 0000000..f2480cb --- /dev/null +++ b/cmd/muxt/testdata/reference_path_unexported_method.txt @@ -0,0 +1,61 @@ +muxt generate --use-receiver-type=T +muxt check + +cat template_routes.go +stdout 'func \(routePaths TemplateRoutePaths\) ListItems\(\) string' + +exec go test -cover + +-- template.gohtml -- +{{define "GET /items listItems(ctx)" }} +

    Items

    +{{end}} + +-- go.mod -- +module example.com + +go 1.22 +-- template.go -- +package server + +import ( + "context" + "embed" + "html/template" +) + +//go:embed *.gohtml +var formHTML embed.FS + +var templates = template.Must(template.ParseFS(formHTML, "*")) + +type T struct{} + +func (T) listItems(_ context.Context) string { return "items" } +-- template_test.go -- +package server + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func Test(t *testing.T) { + mux := http.NewServeMux() + + TemplateRoutes(mux, T{}) + + req := httptest.NewRequest(http.MethodGet, "/items", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + res := rec.Result() + if res.StatusCode != http.StatusOK { + t.Error("expected OK") + } + + path := TemplateRoutePaths{}.ListItems() + if path != "/items" { + t.Errorf("TemplateRoutePaths{}.ListItems() = %q, want %q", path, "/items") + } +} diff --git a/docs/reference/known-issues.md b/docs/reference/known-issues.md index 36a6950..e050d21 100644 --- a/docs/reference/known-issues.md +++ b/docs/reference/known-issues.md @@ -98,6 +98,21 @@ If functions are added after `ParseFS`, type checking won't recognize them. **Workaround:** Pass data explicitly and ensure subtemplates don't assume specific types. +## TemplateRoutePaths Method Name Collision + +**Issue:** `TemplateRoutePaths` methods are always exported so they can be called from templates. This has two consequences: + +1. **Collision:** If a receiver has both an exported and unexported handler method that differ only in the first letter's case (e.g., `List` and `list`), generation fails because both would produce the same `TemplateRoutePaths.List()` method. +2. **Unexportable identifiers:** Handler methods starting with `_` (e.g., `_list`) cannot be exported, so generation fails. + +**Errors:** +- `TemplateRoutePaths method name collision: handlers list and List both produce method List` +- `cannot export identifier "_list" for TemplateRoutePaths method: first character '_' has no uppercase form` + +**Fix:** Rename the handler method to start with a letter. + +**Test files:** `err_path_method_collision.txt`, `err_path_method_unexportable.txt` + ## Reporting Issues Found a limitation not listed here? [Open an issue](https://github.com/typelate/muxt/issues/new) with: diff --git a/docs/skills/muxt_debug-generation-errors/SKILL.md b/docs/skills/muxt_debug-generation-errors/SKILL.md index 63b2637..385f427 100644 --- a/docs/skills/muxt_debug-generation-errors/SKILL.md +++ b/docs/skills/muxt_debug-generation-errors/SKILL.md @@ -232,6 +232,26 @@ type Article struct { **Test files:** `err_check_with_unused_template.txt`, `err_check_with_dead_code_outside_define.txt` +### Path Method Name Collision + +**Error:** `TemplateRoutePaths method name collision: handlers list and List both produce method List` + +**Cause:** Two handler methods differ only in the case of the first letter (e.g., `list` and `List`). Since `TemplateRoutePaths` methods are always exported for template access, both would produce the same method name. + +**Fix:** Rename one of the handler methods so they don't collide after capitalization. + +**Test file:** `err_path_method_collision.txt` + +### Unexportable Path Method Identifier + +**Error:** `cannot export identifier "_list" for TemplateRoutePaths method: first character '_' has no uppercase form` + +**Cause:** The handler method starts with a character that has no uppercase form (e.g., `_`). `TemplateRoutePaths` methods must be exported for template access. + +**Fix:** Rename the handler method to start with a letter. + +**Test file:** `err_path_method_unexportable.txt` + ### CLI Errors **Error:** `unknown command` or `unknown flag` @@ -273,3 +293,5 @@ type Article struct { | Unknown flag | `err_unknown_flag.txt` | | Invalid identifier flag | `err_invalid_identifier_flag.txt` | | Invalid output filename | `err_invalid_output_filename.txt` | +| Path method name collision | `err_path_method_collision.txt` | +| Unexportable path method | `err_path_method_unexportable.txt` | diff --git a/internal/generate/template_route_path.go b/internal/generate/template_route_path.go index 3ffac21..3753789 100644 --- a/internal/generate/template_route_path.go +++ b/internal/generate/template_route_path.go @@ -9,6 +9,8 @@ import ( "go/types" "strconv" "strings" + "unicode" + "unicode/utf8" "github.com/typelate/muxt/internal/astgen" "github.com/typelate/muxt/internal/muxt" @@ -27,7 +29,16 @@ func routePathTypeAndMethods(imports *File, config RoutesFileConfiguration, defs }, }, } + seen := make(map[string]string, len(defs)) for _, t := range defs { + exported, err := exportIdentifier(t.Identifier()) + if err != nil { + return nil, err + } + if prev, ok := seen[exported]; ok { + return nil, fmt.Errorf("TemplateRoutePaths method name collision: handlers %s and %s both produce method %s", prev, t.Identifier(), exported) + } + seen[exported] = t.Identifier() decl, err := routePathFunc(imports, config, &t) if err != nil { return nil, err @@ -49,8 +60,13 @@ func routePathFunc(file *File, config RoutesFileConfiguration, def *muxt.Definit textMarshalerUnderlying := textMarshalerType.Underlying() textMarshalerInterface := textMarshalerUnderlying.(*types.Interface) + ident, err := exportIdentifier(def.Identifier()) + if err != nil { + return nil, err + } + method := &ast.FuncDecl{ - Name: ast.NewIdent(def.Identifier()), + Name: ast.NewIdent(ident), Recv: &ast.FieldList{ List: []*ast.Field{ {Names: []*ast.Ident{ast.NewIdent(methodReceiverName)}, Type: ast.NewIdent(config.TemplateRoutePathsTypeName)}, @@ -231,3 +247,12 @@ func routePathFunc(file *File, config RoutesFileConfiguration, def *muxt.Definit return method, nil } + +func exportIdentifier(s string) (string, error) { + r, size := utf8.DecodeRuneInString(s) + exported := string(utf8.AppendRune(nil, unicode.ToUpper(r))) + s[size:] + if !token.IsExported(exported) { + return "", fmt.Errorf("cannot export identifier %q for TemplateRoutePaths method: first character %q has no uppercase form", s, r) + } + return exported, nil +}