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
48 changes: 48 additions & 0 deletions cmd/check-templates/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"fmt"
"go/token"
"io"
"os"

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

"github.com/typelate/check"
)

func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}

func run(args []string, stdout, stderr io.Writer) int {
dir := "."
if len(args) > 0 {
dir = args[0]
}

fset := token.NewFileSet()
pkgs, err := packages.Load(&packages.Config{
Fset: fset,
Mode: packages.NeedTypesInfo | packages.NeedName | packages.NeedFiles |
packages.NeedTypes | packages.NeedSyntax | packages.NeedEmbedPatterns |
packages.NeedEmbedFiles | packages.NeedImports | packages.NeedModule,
Dir: dir,
}, dir)
if err != nil {
fmt.Fprintf(stderr, "failed to load packages: %v\n", err)
return 1
}
exitCode := 0
for _, pkg := range pkgs {
for _, e := range pkg.Errors {
fmt.Fprintln(stderr, e)
exitCode = 1
}
if err := check.Package(pkg); err != nil {
fmt.Fprintln(stderr, err)
exitCode = 1
}
}
return exitCode
}
40 changes: 40 additions & 0 deletions cmd/check-templates/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"bytes"
"path/filepath"
"testing"

"rsc.io/script"
"rsc.io/script/scripttest"
)

func Test(t *testing.T) {
e := script.NewEngine()
e.Quiet = true
e.Cmds = scripttest.DefaultCmds()
e.Cmds["check-templates"] = checkTemplatesCommand()
ctx := t.Context()
scripttest.Test(t, ctx, e, nil, filepath.FromSlash("testdata/*.txt"))
}

func checkTemplatesCommand() script.Cmd {
return script.Command(script.CmdUsage{
Summary: "check-templates [dir]",
Args: "[dir]",
}, func(state *script.State, args ...string) (script.WaitFunc, error) {
return func(state *script.State) (string, string, error) {
var stdout, stderr bytes.Buffer
cmdArgs := args
if len(cmdArgs) == 0 {
cmdArgs = []string{state.Getwd()}
}
code := run(cmdArgs, &stdout, &stderr)
var err error
if code != 0 {
err = script.ErrUsage
}
return stdout.String(), stderr.String(), err
}, nil
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Template defined at top level, modified in a function. The additional template
# has a field that doesn't exist on the data type.

! check-templates
stderr 'type check failed:.*about\.gohtml:1:5: executing "about\.gohtml" at <\.Missing>: Missing not found on example\.com/app\.Page'

-- go.mod --
module example.com/app

go 1.25.0
-- main.go --
package main

import (
"embed"
"fmt"
"html/template"
"net/http"
)

//go:embed *.gohtml
var source embed.FS

type Page struct {
Title string
}

var ts = template.Must(template.New("app").ParseFS(source, "index.gohtml"))

func setup() {
template.Must(ts.ParseFS(source, "about.gohtml"))
}

func handleAbout(w http.ResponseWriter, r *http.Request) {
_ = ts.ExecuteTemplate(w, "about.gohtml", Page{Title: "Home"})
}

var _ = fmt.Sprint

-- index.gohtml --
<h1>{{.Title}}</h1>
-- about.gohtml --
<p>{{.Missing}}</p>
38 changes: 38 additions & 0 deletions cmd/check-templates/testdata/err_aliased_import.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Template import with a non-standard alias should still detect errors.

! check-templates
stderr 'type check failed:.*index\.gohtml:1:6: executing "index\.gohtml" at <\.Missing>: Missing not found on example\.com/app\.Page'

-- go.mod --
module example.com/app

go 1.25.0
-- main.go --
package main

import (
"embed"
"fmt"
htmltpl "html/template"
"net/http"
)

var (
//go:embed *.gohtml
source embed.FS

templates = htmltpl.Must(htmltpl.ParseFS(source, "*"))
)

type Page struct {
Title string
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
_ = templates.ExecuteTemplate(w, "index.gohtml", Page{Title: "Home"})
}

var _ = fmt.Sprint

-- index.gohtml --
<h1>{{.Missing}}</h1>
38 changes: 38 additions & 0 deletions cmd/check-templates/testdata/err_closure_missing_field.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Template parsed in outer function, closure calls ExecuteTemplate with missing field.

! check-templates
stderr 'type check failed:.*index\.gohtml:1:6: executing "index\.gohtml" at <\.Missing>: Missing not found on example\.com/app\.Page'

-- go.mod --
module example.com/app

go 1.25.0
-- main.go --
package main

import (
"embed"
"fmt"
"html/template"
"net/http"
)

//go:embed *.gohtml
var source embed.FS

type Page struct {
Title string
}

func routes(mux *http.ServeMux) {
ts := template.Must(template.ParseFS(source, "*"))

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_ = ts.ExecuteTemplate(w, "index.gohtml", Page{Title: "Home"})
})
}

var _ = fmt.Sprint

-- index.gohtml --
<h1>{{.Missing}}</h1>
42 changes: 42 additions & 0 deletions cmd/check-templates/testdata/err_funcs_chain.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Template constructed with Funcs() chained before ParseFS should still
# report errors for missing fields.

! check-templates
stderr 'type check failed:.*index\.gohtml:1:2: executing "index\.gohtml" at <\.Missing>: Missing not found on example\.com/app\.Page'

-- go.mod --
module example.com/app

go 1.25.0
-- main.go --
package main

import (
"embed"
"fmt"
"html/template"
"io"
"strings"
)

var (
//go:embed *.gohtml
source embed.FS

templates = template.Must(template.New("").Funcs(template.FuncMap{
"upper": strings.ToUpper,
}).ParseFS(source, "*"))
)

type Page struct {
Title string
}

func render() {
_ = templates.ExecuteTemplate(io.Discard, "index.gohtml", Page{})
}

var _ = fmt.Sprint

-- index.gohtml --
{{.Missing | upper}}
36 changes: 36 additions & 0 deletions cmd/check-templates/testdata/err_html_template_err_branch_end.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# html/template errors with ErrBranchEnd at Execute time.
# The go test exercises the html/template escape analysis which rejects
# templates whose {{if}} branches end in different HTML contexts.
# Companion: pass_text_template_no_err_branch_end.txt

check-templates
exec go test

-- go.mod --
module example.com/app

go 1.25.0
-- main.go --
package main

import "html/template"

var templates = template.Must(template.New("page").Parse(`<div {{if .}}>{{end}}</div>`))

func main() {}

-- main_test.go --
package main

import (
"io"
"testing"
)

func TestExecuteBranchEnd(t *testing.T) {
err := templates.Execute(io.Discard, false)
if err == nil {
t.Fatal("expected html/template ErrBranchEnd error, got nil")
}
t.Logf("got expected error: %s", err.Error())
}
42 changes: 42 additions & 0 deletions cmd/check-templates/testdata/err_imported_type.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Types imported from other packages should report errors for missing fields.

! check-templates
stderr 'type check failed:.*index\.gohtml:1:2: executing "index\.gohtml" at <\.Missing>: Missing not found on example\.com/app/internal/model\.Page'

-- go.mod --
module example.com/app

go 1.25.0
-- internal/model/types.go --
package model

type Page struct {
Title string
}
-- main.go --
package main

import (
"embed"
"fmt"
"html/template"
"io"

"example.com/app/internal/model"
)

var (
//go:embed *.gohtml
source embed.FS

templates = template.Must(template.ParseFS(source, "*"))
)

func render() {
_ = templates.ExecuteTemplate(io.Discard, "index.gohtml", model.Page{})
}

var _ = fmt.Sprint

-- index.gohtml --
{{.Missing}}
37 changes: 37 additions & 0 deletions cmd/check-templates/testdata/err_inline_struct.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Inline anonymous struct types should report errors for missing fields.

! check-templates
stderr 'type check failed:.*index\.gohtml:1:2: executing "index\.gohtml" at <\.Missing>: Missing not found on struct\{Title string\}'

-- go.mod --
module example.com/app

go 1.25.0
-- main.go --
package main

import (
"embed"
"fmt"
"html/template"
"io"
)

var (
//go:embed *.gohtml
source embed.FS

templates = template.Must(template.ParseFS(source, "*"))
)

func render() {
var data struct {
Title string
}
_ = templates.ExecuteTemplate(io.Discard, "index.gohtml", data)
}

var _ = fmt.Sprint

-- index.gohtml --
{{.Missing}}
Loading