Skip to content
Closed
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
57 changes: 43 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,25 @@
## Architecture Overview

```
Template Name (with route pattern and method call)
go list (golang.org/x/tools/go/packages) ./internal/load
↓ load.Package, load.GenerateSource, load.RoutesSource
source.Package: types + templates variables ./internal/source
↓ muxt.Definitions, muxt.ResolveCall
Resolved routes (muxt.Definition) ./internal/muxt
↓
Parser (./internal/muxt/parse)
↓
Type Checker (go/types ./internal/analysis)
↓
Generator (./internal/muxt/generate)
↓
HTTP Handler Code
Generated files / check reports / mutations ./internal/generate, ./internal/analysis, ./internal/mutation
```

**The package load stops at `internal/load`.** It is the only package that
calls `packages.Load` (the go command, seconds per run). It hydrates a
command's configuration into a `source.Package`: plain data holding the
package's types, and each templates variable's template set, functions,
definitions and ExecuteTemplate calls. Everything below it reads only that,
so it can be tested with inputs built in memory: `internal/typestest` type
checks Go source against stub standard library packages in microseconds, and
`internal/load/loadtest` builds a loaded package from it for tests that go
through `internal/load`. Use them rather than loading a module in unit tests.

**Key concept:** Muxt reads template names like `"GET /{id} GetUser(ctx, id)"` and generates `http.Handler` implementations that:
- Parse URL parameters to the correct Go types
- Call the receiver method with parsed args
Expand Down Expand Up @@ -82,10 +90,26 @@ go test ./...

### 4. Implement Changes

Update the generator code in order:
1. `internal/muxt/` — Core generation logic
2. `internal/source/` — AST helpers (if needed)
3. `internal/cli/` — CLI handling (if needed)
Update the code in order:
1. `internal/muxt/` — Route name parsing and call resolution
2. `internal/generate/` or `internal/analysis/` — What is written or reported
3. `internal/load/` — Only if a run needs something new from the loaded packages
4. `internal/cli/` — CLI handling (if needed)

Before adding an integration script, see whether a unit test can state it,
at the layer that owns the behavior:
- **Flags:** `internal/cli/configurations_test.go` states what a command line
parses into, and which command lines are rejected, without loading anything.
- **What a command does with a valid configuration:**
`internal/{generate,analysis,mutation}/testdata/*.txtar` snapshot generated
files, check reports and mutation dry runs from in-memory packages in
milliseconds. Each archive's configuration is a literal in that package's
`snapshots_test.go`, copied from the command line case it stands for.
Rewrite snapshots with `go test ./internal/generate -run TestSnapshots -update`
and review the diff.

Integration scripts are for what needs the go command: generated code
compiling and serving requests, and files on disk.

### 5. Verify Your Changes

Expand Down Expand Up @@ -150,8 +174,13 @@ ls cmd/muxt/testdata/err_*.txt
## Key Files and Directories

### Source Code
- `internal/muxt/` — Generator logic (parse, type check, generate)
- `internal/source/` — AST analysis helpers
- `internal/load/` — Package loading (the only `go/packages` caller), hydration into `source.Package`, and load diagnostics
- `internal/source/` — The loaded package as plain data: what everything after the load reads
- `internal/muxt/` — Template name parsing and route resolution against go/types
- `internal/generate/` — Routes file generation
- `internal/analysis/` — `muxt check` and the template listings
- `internal/typestest/` — In-memory type checking against stub standard library packages, for tests
- `internal/load/loadtest/` — A loaded package built in memory, for tests that go through `internal/load`
- `internal/cli/` — Command-line interface
- `cmd/muxt/` — Command entry point

Expand Down
47 changes: 24 additions & 23 deletions internal/analysis/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package analysis
import (
"errors"
"fmt"
"go/ast"
"go/token"
"go/types"
"html/template"
Expand All @@ -14,11 +13,10 @@ import (
"text/template/parse"

"github.com/typelate/check"
"golang.org/x/tools/go/packages"

"github.com/typelate/muxt/internal/asteval"
"github.com/typelate/muxt/internal/astgen"
"github.com/typelate/muxt/internal/muxt"
"github.com/typelate/muxt/internal/source"
)

// executeTemplateFunc names the method the endpoint scan reports call
Expand All @@ -33,26 +31,17 @@ type CheckConfiguration struct {
// Check validates the package's templates and returns how many
// ExecuteTemplate call sites it checked, so the caller can report the
// count on success.
func Check(config CheckConfiguration, wd string, log *log.Logger, fileSet *token.FileSet, pl []*packages.Package) (int, error) {
routesPkg, ok := asteval.PackageAtFilepath(pl, wd)
if !ok {
return 0, asteval.NoPackageError(wd, pl)
}

func Check(config CheckConfiguration, log *log.Logger, pkg source.Package) (int, error) {
var errs []error
totalChecked := 0

for _, tv := range config.TemplatesVariables {
lt, err := asteval.LoadTemplates(wd, tv, pl)
if err != nil {
return totalChecked, err
}
global, ts := lt.Global, lt.HTML
for _, lt := range pkg.Variables {
global, ts := newGlobal(pkg, lt), lt.Set

// Route template names are validated here so a malformed name
// surfaces with its position instead of leaving the template to
// be reported as merely unused below.
if _, err := muxt.Definitions(ts, tv, lt.Templates); err != nil {
if _, err := muxt.Definitions(lt); err != nil {
if multiLine, ok := errors.AsType[muxt.MultiLineError](err); ok {
log.Println(multiLine.MultiLineError())
log.Println()
Expand All @@ -65,15 +54,15 @@ func Check(config CheckConfiguration, wd string, log *log.Logger, fileSet *token
executedTemplates := make(map[string][]TemplateExecution)
checkedTemplates := 0

for c := range lt.Templates.ExecuteTemplateCalls() {
for _, c := range lt.Calls {
checkedTemplates++
templateName, dataType := c.TemplateName, c.DataType
templateName, dataType := c.Template, c.Data
if config.Verbose {
log.Println("checking endpoint", templateName)
}
qualifier := astgen.NewTypeFormatter(routesPkg.PkgPath).Qualifier
if err := findTemplateExecution(executedTemplates, global, fileSet, qualifier, ts, c.Call, templateName, dataType); err != nil {
log.Println(fileSet.Position(c.Call.Pos()), executeTemplateFunc, strconv.Quote(templateName), types.TypeString(dataType, qualifier))
qualifier := astgen.NewTypeFormatter(pkg.Types.Path()).Qualifier
if err := findTemplateExecution(executedTemplates, global, qualifier, ts, c.Position, templateName, dataType); err != nil {
log.Println(c.Position, executeTemplateFunc, strconv.Quote(templateName), types.TypeString(dataType, qualifier))
if checkErr, ok := errors.AsType[*check.Error](err); ok {
var sb strings.Builder
if detailErr := checkErr.DetailedError(&sb, qualifier); detailErr != nil {
Expand Down Expand Up @@ -266,8 +255,8 @@ func newTemplateExecution(pos token.Position, n any, templateName string, dataTy
}
}

func findTemplateExecution(executedTemplates map[string][]TemplateExecution, global *check.Global, fileSet *token.FileSet, qualifier types.Qualifier, ts *template.Template, node ast.Node, templateName string, dataType types.Type) error {
executedTemplates[templateName] = append(executedTemplates[templateName], newTemplateExecution(fileSet.Position(node.Pos()), node, templateName, dataType))
func findTemplateExecution(executedTemplates map[string][]TemplateExecution, global *check.Global, qualifier types.Qualifier, ts *template.Template, position token.Position, templateName string, dataType types.Type) error {
executedTemplates[templateName] = append(executedTemplates[templateName], newTemplateExecution(position, nil, templateName, dataType))
ts2 := ts.Lookup(templateName)
if ts2 == nil {
return fmt.Errorf("template %q not found", templateName)
Expand All @@ -282,3 +271,15 @@ func findTemplateExecution(executedTemplates map[string][]TemplateExecution, glo
}
return nil
}

// newGlobal wires a check.Global for type checking a templates variable's
// templates in pkg.
func newGlobal(pkg source.Package, variable source.Variable) *check.Global {
return check.NewGlobal(pkg.Types, pkg.Fset, check.FindTreeFunc(func(name string) (*parse.Tree, bool) {
t := variable.Set.Lookup(name)
if t == nil || t.Tree == nil {
return nil, false
}
return t.Tree, true
}), check.Functions(variable.Functions))
}
37 changes: 7 additions & 30 deletions internal/analysis/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@ package analysis

import (
"bytes"
"cmp"
"go/token"
"go/types"
"io"
"maps"
"slices"
"strings"

"golang.org/x/tools/go/packages"

"github.com/typelate/muxt/internal/asteval"
"github.com/typelate/muxt/internal/muxt"
"github.com/typelate/muxt/internal/source"
)

type DefinitionsConfiguration struct {
Expand Down Expand Up @@ -57,34 +53,15 @@ func (result *Routes) WriteTo(w io.Writer) (int64, error) {
return io.Copy(w, &buf)
}

func NewRoutes(config DefinitionsConfiguration, wd string, _ *token.FileSet, pl []*packages.Package) ([]*Routes, error) {
pkg, ok := asteval.PackageAtFilepath(pl, wd)
if !ok {
return nil, asteval.NoPackageError(wd, pl)
}

config.PackagePath = pkg.PkgPath
config.PackageName = pkg.Name

var receiver *types.Named
if config.ReceiverType != "" {
var err error
receiver, err = asteval.FindType(pl, cmp.Or(config.ReceiverPackage, config.PackagePath), config.ReceiverType)
if err != nil {
return nil, err
}
}

// NewRoutes lists each templates variable's route definitions and
// functions, and the receiver's methods when a receiver type was named.
func NewRoutes(pkg source.Package, receiver *types.Named) ([]*Routes, error) {
var results []*Routes

for _, tv := range config.TemplatesVariables {
lt, ts, err := asteval.HTMLTemplates(tv, pkg)
if err != nil {
return nil, err
}
functions := lt.CollectedFunctions()
for _, tv := range pkg.Variables {
functions := tv.Funcs

definitions, err := muxt.Definitions(ts, tv, lt)
definitions, err := muxt.Definitions(tv)
if err != nil {
return nil, err
}
Expand Down
Loading