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
35 changes: 35 additions & 0 deletions cmd/muxt/testdata/err_path_method_collision.txt
Original file line number Diff line number Diff line change
@@ -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)" }}
<ul></ul>
{{end}}

{{define "GET /items/{id} List(ctx, id)" }}
<li></li>
{{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 "" }
30 changes: 30 additions & 0 deletions cmd/muxt/testdata/err_path_method_unexportable.txt
Original file line number Diff line number Diff line change
@@ -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)" }}
<ul></ul>
{{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 }
61 changes: 61 additions & 0 deletions cmd/muxt/testdata/reference_path_unexported_method.txt
Original file line number Diff line number Diff line change
@@ -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)" }}
<h1>Items</h1>
{{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")
}
}
15 changes: 15 additions & 0 deletions docs/reference/known-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions docs/skills/muxt_debug-generation-errors/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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` |
27 changes: 26 additions & 1 deletion internal/generate/template_route_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"go/types"
"strconv"
"strings"
"unicode"
"unicode/utf8"

"github.com/typelate/muxt/internal/astgen"
"github.com/typelate/muxt/internal/muxt"
Expand All @@ -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
Expand All @@ -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)},
Expand Down Expand Up @@ -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
}
Loading