Skip to content

proposal: text/template: annotations to enable static checking of syntax and semantics #64543

@adonovan

Description

@adonovan

Proposal Details

(As they say in the movie business, this is only an idea, not yet a concept.)

Go's text/template package defines a dynamically typed sublanguage for templates, analogous to---but much richer than---the sublanguage of fmt.Printf. It would be convenient for template users if their LSP-enabled editor could give them real-time feedback of syntax and type errors while they are editing, just as it does for Go code or for the untyped sublanguage of fmt.Printf.

In some cases it's easy to detect statically that a string literal is passed to template.Parse and is thus subject to discipline of the template parser. In other cases it's trickier because of the complex flow of template string values through the program, or because the templates are in standalone files, perhaps loaded via go:embed. But parsing is only the first step: the parsed template then flows to a call to Execute, along with an operand value and perhaps also with a FuncMap that maps various names to functions. A static checking tool would need to know the template literal, the keys and value types of the FuncMap, and the type of the operand, in order to perform semantic checks on the template.

One approach, taken by the GoLand IDE (https://www.jetbrains.com/help/go/integration-with-go-templates.html), is to require the user to write special comments in the template to indicate the type of the corresponding Go operand. This is a good start, and I imagine that many users are willing to pay the cost of modifying their templates to benefit from improved static checking. But I wonder whether we can take this approach a step further by declaring a standard form for the necessary type annotations, one that would support FuncMaps as well. In the general case, value flow---of template strings, parsed templates, FuncMaps, and operand values--can be arbitrarily complex, but in most cases I suspect the relationships are simple enough that a few heuristics would go a long way.

For example, this case could be supported without annotations:

const src = `{{..template..}}`

var t *template.Template

func parse(bonusFeature bool) {
        fm := template.FuncMap{   	   
   	   "foo":  foo,
	   ...
	}
	if bonusFeature {
	     fm["bonus"] = bonus
	}	
        t, _ = template.New("name").Parse(src).Funcs(fm)
}

func expand(x *MyData) {
        t.Execute(os.Stdout, x)
})

func foo(string) int
func bonus(string) string

An analyzer could infer that t was parsed from src, with functions "foo" of type func(string) int and (optionally) "bonus" of type func(int) string, enabling static checking of its syntax and inference of a fancy type for t. The call in expand would combine this fancy type with the type of the operand x and report semantic errors.

But its easy to imagine more complex logic surrounding src, fm, t, and x that obfuscates the checker. The purpose of this issue is to gather ideas about what kinds of relationships it would be necessary to express using an annotation mechanism, to come up with a design, and evaluate an experimental implementation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions