From 36c18cdac76d970c9f793afb7310d5453ebc7650 Mon Sep 17 00:00:00 2001 From: Chrstopher Hunter <8398225+crhntr@users.noreply.github.com> Date: Sun, 1 Dec 2024 20:34:01 -0800 Subject: [PATCH] integrate check into generate --- cmd/muxt/generate.go | 8 ++- cmd/muxt/main.go | 4 +- cmd/muxt/script.go | 3 + internal/check/tree.go | 25 +++++-- internal/source/template.go | 85 ++++++++++++++++------ internal/source/template_test.go | 116 +++++++++++++++---------------- routes.go | 42 ++++++++++- routes_test.go | 2 + 8 files changed, 196 insertions(+), 89 deletions(-) diff --git a/cmd/muxt/generate.go b/cmd/muxt/generate.go index ff59073..1f4b1d6 100644 --- a/cmd/muxt/generate.go +++ b/cmd/muxt/generate.go @@ -11,13 +11,17 @@ import ( "github.com/crhntr/muxt/internal/configuration" ) -const CodeGenerationComment = "// Code generated by muxt. DO NOT EDIT." +const ( + CodeGenerationComment = "// Code generated by muxt. DO NOT EDIT." + experimentCheckTypesEnvVar = "MUXT_EXPERIMENT_CHECK_TYPES" +) -func generateCommand(args []string, workingDirectory string, stdout, stderr io.Writer) error { +func generateCommand(workingDirectory string, args []string, getEnv func(string) string, stdout, stderr io.Writer) error { config, err := configuration.NewRoutesFileConfiguration(args, stderr) if err != nil { return err } + config.ExperimentalCheckTypes = getEnv(experimentCheckTypesEnvVar) == "true" s, err := muxt.TemplateRoutesFile(workingDirectory, log.New(stdout, "", 0), config) if err != nil { return err diff --git a/cmd/muxt/main.go b/cmd/muxt/main.go index 68aa5a3..a17b1ef 100644 --- a/cmd/muxt/main.go +++ b/cmd/muxt/main.go @@ -16,11 +16,11 @@ func main() { os.Exit(handleError(command(wd, flag.Args(), os.Getenv, os.Stdout, os.Stderr))) } -func command(wd string, args []string, _ func(string) string, stdout, stderr io.Writer) error { +func command(wd string, args []string, getEnv func(string) string, stdout, stderr io.Writer) error { if len(args) > 0 { switch cmd, cmdArgs := args[0], args[1:]; cmd { case "generate", "gen", "g": - return generateCommand(cmdArgs, wd, stdout, stderr) + return generateCommand(wd, cmdArgs, getEnv, stdout, stderr) case "version", "v": return versionCommand(stdout) } diff --git a/cmd/muxt/script.go b/cmd/muxt/script.go index 5fd20e7..f07f046 100644 --- a/cmd/muxt/script.go +++ b/cmd/muxt/script.go @@ -14,6 +14,9 @@ func scriptCommand() script.Cmd { return func(state *script.State) (string, string, error) { var stdout, stderr bytes.Buffer err := command(state.Getwd(), args, func(s string) string { + if s == experimentCheckTypesEnvVar { + return "true" + } e, _ := state.LookupEnv(s) return e }, &stdout, &stderr) diff --git a/internal/check/tree.go b/internal/check/tree.go index c1b7e68..8ec463c 100644 --- a/internal/check/tree.go +++ b/internal/check/tree.go @@ -5,6 +5,7 @@ import ( "go/token" "go/types" "maps" + "strings" "text/template/parse" ) @@ -223,7 +224,16 @@ func (s *scope) checkCommandNode(tree *parse.Tree, dot types.Type, n *parse.Comm } for i := 0; i < len(argTypes); i++ { at := argTypes[i] - pt := sig.Params().At(i).Type() + var pt types.Type + isVar := sig.Variadic() + argVar := i >= sig.Params().Len()-1 + if isVar && argVar { + ps := sig.Params() + v := ps.At(ps.Len() - 1).Type().(*types.Slice) + pt = v.Elem() + } else { + pt = sig.Params().At(i).Type() + } if !types.AssignableTo(at, pt) { return nil, fmt.Errorf("%s argument %d has type %s expected %s", n.Args[0], i-1, at, pt) } @@ -304,9 +314,16 @@ func (s *scope) checkRangeNode(tree *parse.Tree, dot types.Type, n *parse.RangeN } func (s *scope) checkIdentifierNode(n *parse.IdentifierNode) (types.Type, error) { - tp, ok := s.variables[n.Ident] + if strings.HasPrefix(n.Ident, "$") { + tp, ok := s.variables[n.Ident] + if !ok { + return nil, fmt.Errorf("failed to find identifier %s", n.Ident) + } + return tp, nil + } + fn, ok := s.FindFunction(n.Ident) if !ok { - return nil, fmt.Errorf("failed to find identifier %q", n.Ident) + return nil, fmt.Errorf("failed to find function %s", n.Ident) } - return tp, nil + return fn, nil } diff --git a/internal/source/template.go b/internal/source/template.go index 42b1139..27d42be 100644 --- a/internal/source/template.go +++ b/internal/source/template.go @@ -1,9 +1,12 @@ package source import ( + "bytes" "fmt" "go/ast" + "go/format" "go/token" + "go/types" "html/template" "path/filepath" "slices" @@ -13,7 +16,10 @@ import ( "golang.org/x/tools/go/packages" ) -func Templates(workingDirectory, templatesVariable string, pkg *packages.Package) (*template.Template, error) { +type TemplateFuncMap = map[string]*types.Signature + +func Templates(workingDirectory, templatesVariable string, pkg *packages.Package) (*template.Template, TemplateFuncMap, error) { + funcTypeMap := registerDefaultFunctions(pkg.Types) for _, tv := range IterateValueSpecs(pkg.Syntax) { i := slices.IndexFunc(tv.Names, func(e *ast.Ident) bool { return e.Name == templatesVariable @@ -23,19 +29,43 @@ func Templates(workingDirectory, templatesVariable string, pkg *packages.Package } embeddedPaths, err := relativeFilePaths(workingDirectory, pkg.EmbedFiles...) if err != nil { - return nil, fmt.Errorf("failed to calculate relative path for embedded files: %w", err) + return nil, nil, fmt.Errorf("failed to calculate relative path for embedded files: %w", err) } const templatePackageIdent = "template" - ts, err := evaluateTemplateSelector(nil, tv.Values[i], workingDirectory, templatesVariable, templatePackageIdent, "", "", pkg.Fset, pkg.Syntax, embeddedPaths) + ts, err := evaluateTemplateSelector(nil, pkg.Types, tv.Values[i], workingDirectory, templatesVariable, templatePackageIdent, "", "", pkg.Fset, pkg.Syntax, embeddedPaths, funcTypeMap, make(template.FuncMap)) if err != nil { - return nil, fmt.Errorf("run template %s failed at %w", templatesVariable, err) + return nil, nil, fmt.Errorf("run template %s failed at %w", templatesVariable, err) } - return ts, nil + return ts, funcTypeMap, nil + } + return nil, nil, fmt.Errorf("variable %s not found", templatesVariable) +} + +func findPackage(pkg *types.Package, path string) (*types.Package, bool) { + if pkg.Path() == path { + return pkg, true + } + for _, im := range pkg.Imports() { + if p, ok := findPackage(im, path); ok { + return p, true + } + } + return nil, false +} + +func registerDefaultFunctions(pkg *types.Package) TemplateFuncMap { + funcTypeMap := make(TemplateFuncMap) + fmtPkg, ok := findPackage(pkg, "fmt") + if !ok || fmtPkg == nil { + return funcTypeMap } - return nil, fmt.Errorf("variable %s not found", templatesVariable) + funcTypeMap["printf"] = fmtPkg.Scope().Lookup("Sprintf").Type().(*types.Signature) + funcTypeMap["print"] = fmtPkg.Scope().Lookup("Sprint").Type().(*types.Signature) + funcTypeMap["println"] = fmtPkg.Scope().Lookup("Sprintln").Type().(*types.Signature) + return funcTypeMap } -func evaluateTemplateSelector(ts *template.Template, expression ast.Expr, workingDirectory, templatesVariable, templatePackageIdent, rDelim, lDelim string, fileSet *token.FileSet, files []*ast.File, embeddedPaths []string) (*template.Template, error) { +func evaluateTemplateSelector(ts *template.Template, pkg *types.Package, expression ast.Expr, workingDirectory, templatesVariable, templatePackageIdent, rDelim, lDelim string, fileSet *token.FileSet, files []*ast.File, embeddedPaths []string, funcTypeMaps TemplateFuncMap, fm template.FuncMap) (*template.Template, error) { call, ok := expression.(*ast.CallExpr) if !ok { return nil, contextError(workingDirectory, fileSet, expression.Pos(), fmt.Errorf("expected call expression")) @@ -56,7 +86,7 @@ func evaluateTemplateSelector(ts *template.Template, expression ast.Expr, workin if len(call.Args) != 1 { return nil, contextError(workingDirectory, fileSet, call.Lparen, fmt.Errorf("expected exactly one argument %s got %d", Format(sel.X), len(call.Args))) } - return evaluateTemplateSelector(ts, call.Args[0], workingDirectory, templatesVariable, templatePackageIdent, rDelim, lDelim, fileSet, files, embeddedPaths) + return evaluateTemplateSelector(ts, pkg, call.Args[0], workingDirectory, templatesVariable, templatePackageIdent, rDelim, lDelim, fileSet, files, embeddedPaths, funcTypeMaps, fm) case "New": if len(call.Args) != 1 { return nil, contextError(workingDirectory, fileSet, call.Lparen, fmt.Errorf("expected exactly one string literal argument")) @@ -76,7 +106,7 @@ func evaluateTemplateSelector(ts *template.Template, expression ast.Expr, workin return nil, contextError(workingDirectory, fileSet, call.Fun.Pos(), fmt.Errorf("unsupported function %s", sel.Sel.Name)) } case *ast.CallExpr: - up, err := evaluateTemplateSelector(ts, sel.X, workingDirectory, templatesVariable, templatePackageIdent, rDelim, lDelim, fileSet, files, embeddedPaths) + up, err := evaluateTemplateSelector(ts, pkg, sel.X, workingDirectory, templatesVariable, templatePackageIdent, rDelim, lDelim, fileSet, files, embeddedPaths, funcTypeMaps, fm) if err != nil { return nil, err } @@ -121,44 +151,44 @@ func evaluateTemplateSelector(ts *template.Template, expression ast.Expr, workin } return up.Option(list...), nil case "Funcs": - funcMap, err := evaluateFuncMap(workingDirectory, templatePackageIdent, fileSet, call) - if err != nil { + if err := evaluateFuncMap(workingDirectory, templatePackageIdent, pkg, fileSet, call, fm, funcTypeMaps); err != nil { return nil, err } - return up.Funcs(funcMap), nil + return up.Funcs(fm), nil default: return nil, contextError(workingDirectory, fileSet, call.Fun.Pos(), fmt.Errorf("unsupported method %s", sel.Sel.Name)) } } } -func evaluateFuncMap(workingDirectory, templatePackageIdent string, fileSet *token.FileSet, call *ast.CallExpr) (template.FuncMap, error) { +func evaluateFuncMap(workingDirectory, templatePackageIdent string, pkg *types.Package, fileSet *token.FileSet, call *ast.CallExpr, fm template.FuncMap, funcTypesMap TemplateFuncMap) error { const funcMapTypeIdent = "FuncMap" - fm := make(template.FuncMap) if len(call.Args) != 1 { - return nil, contextError(workingDirectory, fileSet, call.Lparen, fmt.Errorf("expected exactly 1 template.FuncMap composite literal argument")) + return contextError(workingDirectory, fileSet, call.Lparen, fmt.Errorf("expected exactly 1 template.FuncMap composite literal argument")) } arg := call.Args[0] lit, ok := arg.(*ast.CompositeLit) if !ok { - return nil, contextError(workingDirectory, fileSet, arg.Pos(), fmt.Errorf("expected a composite literal with type %s.%s got %s", templatePackageIdent, funcMapTypeIdent, Format(arg))) + return contextError(workingDirectory, fileSet, arg.Pos(), fmt.Errorf("expected a composite literal with type %s.%s got %s", templatePackageIdent, funcMapTypeIdent, Format(arg))) } typeSel, ok := lit.Type.(*ast.SelectorExpr) if !ok || typeSel.Sel.Name != funcMapTypeIdent { - return nil, contextError(workingDirectory, fileSet, arg.Pos(), fmt.Errorf("expected a composite literal with type %s.%s got %s", templatePackageIdent, funcMapTypeIdent, Format(arg))) + return contextError(workingDirectory, fileSet, arg.Pos(), fmt.Errorf("expected a composite literal with type %s.%s got %s", templatePackageIdent, funcMapTypeIdent, Format(arg))) } if tp, ok := typeSel.X.(*ast.Ident); !ok || tp.Name != templatePackageIdent { - return nil, contextError(workingDirectory, fileSet, arg.Pos(), fmt.Errorf("expected a composite literal with type %s.%s got %s", templatePackageIdent, funcMapTypeIdent, Format(arg))) + return contextError(workingDirectory, fileSet, arg.Pos(), fmt.Errorf("expected a composite literal with type %s.%s got %s", templatePackageIdent, funcMapTypeIdent, Format(arg))) } + var buf bytes.Buffer for i, exp := range lit.Elts { el, ok := exp.(*ast.KeyValueExpr) if !ok { - return nil, contextError(workingDirectory, fileSet, exp.Pos(), fmt.Errorf("expected element at index %d to be a key value pair got %s", i, Format(exp))) + return contextError(workingDirectory, fileSet, exp.Pos(), fmt.Errorf("expected element at index %d to be a key value pair got %s", i, Format(exp))) } funcName, err := evaluateStringLiteralExpression(workingDirectory, fileSet, el.Key) if err != nil { - return nil, err + return err } + // template.Parse does not evaluate the function signature parameters; // it ensures the function name is in scope and there is one or two results. // we could use something like func() string { return "" } for this signature @@ -171,8 +201,21 @@ func evaluateFuncMap(workingDirectory, templatePackageIdent string, fileSet *tok // or // fm[funcName] = func() (int, int) {return 0, 0} // will fail because the second result is not an error fm[funcName] = fmt.Sprintln + + if pkg == nil { + continue + } + buf.Reset() + if err := format.Node(&buf, fileSet, el.Value); err != nil { + return err + } + tv, err := types.Eval(fileSet, pkg, lit.Pos(), buf.String()) + if err != nil { + return err + } + funcTypesMap[funcName] = tv.Type.(*types.Signature) } - return fm, nil + return nil } func evaluateCallParseFilesArgs(workingDirectory string, fileSet *token.FileSet, call *ast.CallExpr, files []*ast.File, embeddedPaths []string) ([]string, error) { diff --git a/internal/source/template_test.go b/internal/source/template_test.go index 480dfbb..08934b0 100644 --- a/internal/source/template_test.go +++ b/internal/source/template_test.go @@ -23,14 +23,14 @@ func TestTemplates(t *testing.T) { t.Run("non call", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir) - _, err := source.Templates(dir, "templatesIdent", pkg) + _, _, err := source.Templates(dir, "templatesIdent", pkg) require.ErrorContains(t, err, "run template templatesIdent failed at template.go:32:19: expected call expression") }) t.Run("call ParseFS", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/template_ParseFS.txtar")) pkg := parseGo(t, dir, "index.gohtml", "form.gohtml") - ts, err := source.Templates(dir, "templates", pkg) + ts, _, err := source.Templates(dir, "templates", pkg) require.NoError(t, err) var names []string for _, t := range ts.Templates() { @@ -43,7 +43,7 @@ func TestTemplates(t *testing.T) { t.Run("call ParseFS with assets dir", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/assets_dir.txtar")) pkg := parseGo(t, dir, "assets/index.gohtml", "assets/form.gohtml") - ts, err := source.Templates(dir, "templates", pkg) + ts, _, err := source.Templates(dir, "templates", pkg) require.NoError(t, err) var names []string for _, t := range ts.Templates() { @@ -56,7 +56,7 @@ func TestTemplates(t *testing.T) { t.Run("call New", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - ts, err := source.Templates(dir, "templateNew", pkg) + ts, _, err := source.Templates(dir, "templateNew", pkg) require.NoError(t, err) var names []string for _, t := range ts.Templates() { @@ -69,7 +69,7 @@ func TestTemplates(t *testing.T) { t.Run("call New after calling ParseFS", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - ts, err := source.Templates(dir, "templateParseFSNew", pkg) + ts, _, err := source.Templates(dir, "templateParseFSNew", pkg) require.NoError(t, err) var names []string for _, t := range ts.Templates() { @@ -82,7 +82,7 @@ func TestTemplates(t *testing.T) { t.Run("call New before calling ParseFS", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - ts, err := source.Templates(dir, "templateNewParseFS", pkg) + ts, _, err := source.Templates(dir, "templateNewParseFS", pkg) require.NoError(t, err) var names []string @@ -96,7 +96,7 @@ func TestTemplates(t *testing.T) { t.Run("call new with non args", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateNewMissingArg", pkg) + _, _, err := source.Templates(dir, "templateNewMissingArg", pkg) require.ErrorContains(t, err, "expected exactly one string literal argument") }) @@ -104,7 +104,7 @@ func TestTemplates(t *testing.T) { t.Run("call New on unknown X", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateWrongX", pkg) + _, _, err := source.Templates(dir, "templateWrongX", pkg) require.ErrorContains(t, err, "template.go:20:19: expected template got UNKNOWN") }) @@ -112,7 +112,7 @@ func TestTemplates(t *testing.T) { t.Run("call New with wrong arg count", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateWrongArgCount", pkg) + _, _, err := source.Templates(dir, "templateWrongArgCount", pkg) require.ErrorContains(t, err, "template.go:22:38: expected exactly one string literal argument") }) @@ -120,7 +120,7 @@ func TestTemplates(t *testing.T) { t.Run("call New on unexpected X", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateNewOnIndexed", pkg) + _, _, err := source.Templates(dir, "templateNewOnIndexed", pkg) require.ErrorContains(t, err, "template.go:24:25: expected exactly one argument ts[0] got 2") }) @@ -128,7 +128,7 @@ func TestTemplates(t *testing.T) { t.Run("call New with non string literal arg", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateNewArg42", pkg) + _, _, err := source.Templates(dir, "templateNewArg42", pkg) require.ErrorContains(t, err, "template.go:26:34: expected string literal got 42") }) @@ -136,7 +136,7 @@ func TestTemplates(t *testing.T) { t.Run("call New with non literal arg", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateNewArgIdent", pkg) + _, _, err := source.Templates(dir, "templateNewArgIdent", pkg) require.ErrorContains(t, err, "template.go:28:37: expected string literal got TemplateName") }) @@ -144,7 +144,7 @@ func TestTemplates(t *testing.T) { t.Run("call New with upstream error", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateNewErrUpstream", pkg) + _, _, err := source.Templates(dir, "templateNewErrUpstream", pkg) require.ErrorContains(t, err, "run template templateNewErrUpstream failed at template.go:30:40: expected string literal got fail") }) @@ -152,7 +152,7 @@ func TestTemplates(t *testing.T) { t.Run("unknown templates variable", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "variableDoesNotExist", pkg) + _, _, err := source.Templates(dir, "variableDoesNotExist", pkg) require.NotNil(t, err) require.Equal(t, "variable variableDoesNotExist not found", err.Error()) @@ -161,7 +161,7 @@ func TestTemplates(t *testing.T) { t.Run("unknown templates variable", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "unsupportedMethod", pkg) + _, _, err := source.Templates(dir, "unsupportedMethod", pkg) require.ErrorContains(t, err, "run template unsupportedMethod failed at template.go:34:22: unsupported function Unknown") }) @@ -169,7 +169,7 @@ func TestTemplates(t *testing.T) { t.Run("call Must with unexpected function expression", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "unexpectedFunExpression", pkg) + _, _, err := source.Templates(dir, "unexpectedFunExpression", pkg) require.ErrorContains(t, err, "run template unexpectedFunExpression failed at template.go:36:28: unexpected expression *ast.IndexExpr: x[3]") }) @@ -177,91 +177,91 @@ func TestTemplates(t *testing.T) { t.Run("call Must on non ident receiver", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateMustNonIdentReceiver", pkg) + _, _, err := source.Templates(dir, "templateMustNonIdentReceiver", pkg) require.ErrorContains(t, err, "run template templateMustNonIdentReceiver failed at template.go:38:33: unexpected expression *ast.Ident: f") }) t.Run("call Must with two arguments", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateMustCalledWithTwoArgs", pkg) + _, _, err := source.Templates(dir, "templateMustCalledWithTwoArgs", pkg) require.ErrorContains(t, err, "run template templateMustCalledWithTwoArgs failed at template.go:40:47: expected exactly one argument template got 2") }) t.Run("call Must with one argument", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateMustCalledWithNoArg", pkg) + _, _, err := source.Templates(dir, "templateMustCalledWithNoArg", pkg) require.ErrorContains(t, err, "run template templateMustCalledWithNoArg failed at template.go:42:47: expected exactly one argument template got 0") }) t.Run("call Must wrong template package ident", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateMustWrongPackageIdent", pkg) + _, _, err := source.Templates(dir, "templateMustWrongPackageIdent", pkg) require.ErrorContains(t, err, "run template templateMustWrongPackageIdent failed at template.go:44:34: expected template got wrong") }) t.Run("call ParseFS wrong template package ident", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateParseFSWrongPackageIdent", pkg) + _, _, err := source.Templates(dir, "templateParseFSWrongPackageIdent", pkg) require.ErrorContains(t, err, "run template templateParseFSWrongPackageIdent failed at template.go:46:37: expected template got wrong") }) t.Run("call ParseFS receiver errored", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateParseFSReceiverErr", pkg) + _, _, err := source.Templates(dir, "templateParseFSReceiverErr", pkg) require.ErrorContains(t, err, "run template templateParseFSReceiverErr failed at template.go:48:43: expected exactly one string literal argument") }) t.Run("call ParseFS unexpected receiver", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateParseFSUnexpectedReceiver", pkg) + _, _, err := source.Templates(dir, "templateParseFSUnexpectedReceiver", pkg) require.ErrorContains(t, err, "run template templateParseFSUnexpectedReceiver failed at template.go:50:38: expected exactly one argument x[0] got 2") }) t.Run("call ParseFS with no arguments", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateParseFSNoArgs", pkg) + _, _, err := source.Templates(dir, "templateParseFSNoArgs", pkg) require.ErrorContains(t, err, "template.go:52:42: missing required arguments") }) t.Run("call ParseFS with first arg non ident", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateParseFSFirstArgNonIdent", pkg) + _, _, err := source.Templates(dir, "templateParseFSFirstArgNonIdent", pkg) require.ErrorContains(t, err, "template.go:54:53: first argument to ParseFS must be an identifier") }) t.Run("call ParseFS with first arg non ident", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateParseFSNonStringLiteralGlob", pkg) + _, _, err := source.Templates(dir, "templateParseFSNonStringLiteralGlob", pkg) require.ErrorContains(t, err, "template.go:56:78: expected string literal got 42") }) t.Run("call ParseFS with bad glob", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateParseFSWithBadGlob", pkg) + _, _, err := source.Templates(dir, "templateParseFSWithBadGlob", pkg) require.ErrorContains(t, err, `template.go:58:64: bad pattern "[fail": syntax error in pattern`) }) t.Run("call ParseFS and fail to get relative template path", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/template_ParseFS.txtar")) pkg := parseGo(t, dir) pkg.EmbedFiles = []string{"\x00/index.gohtml"} // null must not be in a path - _, err := source.Templates(dir, "templates", pkg) + _, _, err := source.Templates(dir, "templates", pkg) require.ErrorContains(t, err, `failed to calculate relative path for embedded files: Rel: can't make`) }) t.Run("call ParseFS and filter filepaths by globs", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/template_ParseFS.txtar")) pkg := parseGo(t, dir, "index.gohtml", "script.html") - tsHTML, err := source.Templates(dir, "templatesHTML", pkg) + tsHTML, _, err := source.Templates(dir, "templatesHTML", pkg) require.NoError(t, err) - tsGoHTML, err := source.Templates(dir, "templatesGoHTML", pkg) + tsGoHTML, _, err := source.Templates(dir, "templatesGoHTML", pkg) assert.NotNil(t, tsHTML.Lookup("script.html")) assert.NotNil(t, tsHTML.Lookup("console_log")) assert.Nil(t, tsGoHTML.Lookup("script.html")) @@ -270,19 +270,19 @@ func TestTemplates(t *testing.T) { t.Run("call bad embed pattern", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/bad_embed_pattern.txtar")) pkg := parseGo(t, dir, "greeting.gohtml") - _, err := source.Templates(dir, "templates", pkg) + _, _, err := source.Templates(dir, "templates", pkg) require.ErrorContains(t, err, `template.go:9:2: embed comment malformed: syntax error in pattern`) }) t.Run("call bad embed pattern", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/template_ParseFS.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateEmbedVariableNotFound", pkg) + _, _, err := source.Templates(dir, "templateEmbedVariableNotFound", pkg) require.ErrorContains(t, err, `template.go:22:65: variable hiding not found`) }) t.Run("multiple delimiter types", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/delims.txtar")) pkg := parseGo(t, dir, "default.gohtml", "triple_parens.gohtml", "double_square.gohtml") - templates, err := source.Templates(dir, "templates", pkg) + templates, _, err := source.Templates(dir, "templates", pkg) require.NoError(t, err) var names []string for _, ts := range templates.Templates() { @@ -293,142 +293,142 @@ func TestTemplates(t *testing.T) { t.Run("Run method call gets no args", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateNewHasWrongNumberOfArgs", pkg) + _, _, err := source.Templates(dir, "templateNewHasWrongNumberOfArgs", pkg) require.ErrorContains(t, err, `template.go:60:101: expected exactly one string literal argument`) }) t.Run("Run method call gets wrong type of args", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateNewHasWrongTypeOfArgs", pkg) + _, _, err := source.Templates(dir, "templateNewHasWrongTypeOfArgs", pkg) require.ErrorContains(t, err, `template.go:62:56: expected string literal got 9000`) }) t.Run("Run method call gets too many args", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateNewHasTooManyArgs", pkg) + _, _, err := source.Templates(dir, "templateNewHasTooManyArgs", pkg) require.ErrorContains(t, err, `template.go:64:51: expected exactly one string literal argument`) }) t.Run("Delims method call gets no args", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateDelimsGetsNoArgs", pkg) + _, _, err := source.Templates(dir, "templateDelimsGetsNoArgs", pkg) require.ErrorContains(t, err, `template.go:66:53: expected exactly two string literal arguments`) }) t.Run("Delims method call gets too many args", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateDelimsGetsTooMany", pkg) + _, _, err := source.Templates(dir, "templateDelimsGetsTooMany", pkg) require.ErrorContains(t, err, `template.go:68:54: expected exactly two string literal arguments`) }) t.Run("Delims have wrong type of argument expressions", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateDelimsWrongExpressionArg", pkg) + _, _, err := source.Templates(dir, "templateDelimsWrongExpressionArg", pkg) require.ErrorContains(t, err, `template.go:70:67: expected string literal got y`) }) t.Run("ParseFS method fails", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateParseFSMethodFails", pkg) + _, _, err := source.Templates(dir, "templateParseFSMethodFails", pkg) require.ErrorContains(t, err, `template.go:72:73: expected string literal got fail`) }) t.Run("Options method requires string literals", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateOptionsRequiresStringLiterals", pkg) + _, _, err := source.Templates(dir, "templateOptionsRequiresStringLiterals", pkg) require.ErrorContains(t, err, `template.go:74:67: expected string literal got fail`) }) t.Run("unknown method", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateUnknownMethod", pkg) + _, _, err := source.Templates(dir, "templateUnknownMethod", pkg) require.ErrorContains(t, err, `template.go:76:26: unsupported method Unknown`) }) t.Run("Option call", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") - _, err := source.Templates(dir, "templateOptionCall", pkg) + _, _, err := source.Templates(dir, "templateOptionCall", pkg) require.NoError(t, err) }) t.Run("Option call wrong argument", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/templates.txtar")) pkg := parseGo(t, dir, "index.gohtml") assert.Panics(t, func() { - _, _ = source.Templates(dir, "templateOptionCallUnknownArg", pkg) + _, _, _ = source.Templates(dir, "templateOptionCallUnknownArg", pkg) }) }) t.Run("Funcs call", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "greet.gohtml") - _, err := source.Templates(dir, "templates", pkg) + _, _, err := source.Templates(dir, "templates", pkg) require.NoError(t, err) }) t.Run("Func not defined", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesFuncNotDefined", pkg) + _, _, err := source.Templates(dir, "templatesFuncNotDefined", pkg) require.ErrorContains(t, err, `missing_func.gohtml:1: function "enemy" not defined`) }) t.Run("Func wrong parameter kind", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesWrongArg", pkg) + _, _, err := source.Templates(dir, "templatesWrongArg", pkg) require.ErrorContains(t, err, `expected a composite literal with type template.FuncMap got wrong`) }) t.Run("Func wrong too many args", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesTwoArgs", pkg) + _, _, err := source.Templates(dir, "templatesTwoArgs", pkg) require.ErrorContains(t, err, `expected exactly 1 template.FuncMap composite literal argument`) }) t.Run("Func wrong too no args", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesNoArgs", pkg) + _, _, err := source.Templates(dir, "templatesNoArgs", pkg) require.ErrorContains(t, err, `expected exactly 1 template.FuncMap composite literal argument`) }) t.Run("Func wrong package ident", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesWrongTypePackageName", pkg) + _, _, err := source.Templates(dir, "templatesWrongTypePackageName", pkg) require.ErrorContains(t, err, `expected a composite literal with type template.FuncMap got wrong.FuncMap{}`) }) t.Run("Func wrong Type ident", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesWrongTypeName", pkg) + _, _, err := source.Templates(dir, "templatesWrongTypeName", pkg) require.ErrorContains(t, err, `expected a composite literal with type template.FuncMap got template.Wrong{}`) }) t.Run("Func wrong Type", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesWrongTypeExpression", pkg) + _, _, err := source.Templates(dir, "templatesWrongTypeExpression", pkg) require.ErrorContains(t, err, `expected a composite literal with type template.FuncMap got wrong{}`) }) t.Run("Func wrong elem", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesWrongTypeElem", pkg) + _, _, err := source.Templates(dir, "templatesWrongTypeElem", pkg) require.ErrorContains(t, err, `expected element at index 0 to be a key value pair got wrong`) }) t.Run("Func wrong elem key", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/funcs.txtar")) pkg := parseGo(t, dir, "missing_func.gohtml", "greet.gohtml") - _, err := source.Templates(dir, "templatesWrongElemKey", pkg) + _, _, err := source.Templates(dir, "templatesWrongElemKey", pkg) require.ErrorContains(t, err, `expected string literal got wrong`) }) t.Run("Parse template name from new", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/parse.txtar")) pkg := parseGo(t, dir) - ts, err := source.Templates(dir, "templates", pkg) + ts, _, err := source.Templates(dir, "templates", pkg) require.NoError(t, err) assert.NotNil(t, ts.Lookup("GET /")) }) t.Run("Parse string has multiple routes", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/parse.txtar")) pkg := parseGo(t, dir) - ts, err := source.Templates(dir, "multiple", pkg) + ts, _, err := source.Templates(dir, "multiple", pkg) require.NoError(t, err) assert.NotNil(t, ts.Lookup("GET /")) assert.NotNil(t, ts.Lookup("GET /{name}")) @@ -436,13 +436,13 @@ func TestTemplates(t *testing.T) { t.Run("Parse is missing argument", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/parse.txtar")) pkg := parseGo(t, dir) - _, err := source.Templates(dir, "noArg", pkg) + _, _, err := source.Templates(dir, "noArg", pkg) require.ErrorContains(t, err, "run template noArg failed at parse.go:12:35: expected exactly one string literal argument") }) t.Run("Parse gets wrong argument type", func(t *testing.T) { dir := createTestDir(t, filepath.FromSlash("testdata/template/parse.txtar")) pkg := parseGo(t, dir) - _, err := source.Templates(dir, "wrongArg", pkg) + _, _, err := source.Templates(dir, "wrongArg", pkg) require.ErrorContains(t, err, "run template wrongArg failed at parse.go:14:40: expected string literal got 500") }) } diff --git a/routes.go b/routes.go index c2b4992..a330a56 100644 --- a/routes.go +++ b/routes.go @@ -14,6 +14,7 @@ import ( "slices" "strconv" "strings" + "text/template/parse" "time" "github.com/crhntr/dom" @@ -22,6 +23,7 @@ import ( "golang.org/x/net/html/atom" "golang.org/x/tools/go/packages" + "github.com/crhntr/muxt/internal/check" "github.com/crhntr/muxt/internal/source" ) @@ -54,6 +56,7 @@ const ( ) type RoutesFileConfiguration struct { + ExperimentalCheckTypes, executeFunc bool PackageName, PackagePath, @@ -82,7 +85,7 @@ func TemplateRoutesFile(wd string, logger *log.Logger, config RoutesFileConfigur imports := source.NewImports(&ast.GenDecl{Tok: token.IMPORT}) patterns := []string{ - wd, "net/http", + wd, "net/http", "fmt", } if config.ReceiverPackage != "" { @@ -125,7 +128,7 @@ func TemplateRoutesFile(wd string, logger *log.Logger, config RoutesFileConfigur receiver = types.NewNamed(types.NewTypeName(0, routesPkg.Types, "Receiver", nil), types.NewStruct(nil, nil), nil) } - ts, err := source.Templates(wd, config.TemplatesVariable, routesPkg) + ts, fm, err := source.Templates(wd, config.TemplatesVariable, routesPkg) if err != nil { return "", err } @@ -205,6 +208,16 @@ func TemplateRoutesFile(wd string, logger *log.Logger, config RoutesFileConfigur handlerFunc.Body.List = append(handlerFunc.Body.List, receiverCallStatements...) handlerFunc.Body.List = append(handlerFunc.Body.List, t.executeCall(source.HTTPStatusCode(imports, t.statusCode), ast.NewIdent(dataVarIdent), writeHeader)) routesFunc.Body.List = append(routesFunc.Body.List, t.callHandleFunc(handlerFunc)) + + if config.ExperimentalCheckTypes { + dataVar := sig.Results().At(0) + if types.Identical(dataVar.Type(), types.Universe.Lookup("any").Type()) { + continue + } + if err := check.Tree(t.template.Tree, dataVar.Type(), dataVar.Pkg(), routesPkg.Fset, newForrest(ts), functionMap(fm)); err != nil { + return "", err + } + } } imports.SortImports() @@ -980,3 +993,28 @@ func executeFuncDecl(imports *source.Imports, templatesVariableIdent string) *as }, } } + +type forest template.Template + +func newForrest(templates *template.Template) *forest { + return (*forest)(templates) +} + +func (f *forest) FindTree(name string) (*parse.Tree, bool) { + ts := (*template.Template)(f).Lookup(name) + if ts == nil { + return nil, false + } + return ts.Tree, true +} + +type functionMap map[string]*types.Signature + +func (fm functionMap) FindFunction(name string) (*types.Signature, bool) { + m := (map[string]*types.Signature)(fm) + fn, ok := m[name] + if !ok { + return nil, false + } + return fn, true +} diff --git a/routes_test.go b/routes_test.go index 1671aa2..a1861ed 100644 --- a/routes_test.go +++ b/routes_test.go @@ -1952,6 +1952,8 @@ var templates = template.Must(template.ParseFS(templatesDir, "template.gohtml")) PackagePath: "example.com", ReceiverType: tt.Receiver, OutputFileName: "template_routes.go", + + ExperimentalCheckTypes: true, }) if tt.ExpectedError == "" { require.NoError(t, err)