Skip to content

Commit

Permalink
Update staticTyping Analyzer to be recursive
Browse files Browse the repository at this point in the history
  • Loading branch information
tylermmorton committed Jul 6, 2023
1 parent 8d938f3 commit e950caf
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 74 deletions.
148 changes: 75 additions & 73 deletions analyzers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,94 +33,96 @@ var builtinAnalyzers = []Analyzer{
staticTyping,
}

// staticTyping enables static type checking on templateProvider parse trees by using
// reflection on the given struct type.
var staticTyping Analyzer = func(helper *AnalysisHelper) AnalyzerFunc {
return func(val reflect.Value, node parse.Node) {
switch typ := node.(type) {
case *parse.IfNode:
for _, cmd := range typ.Pipe.Cmds {
for _, arg := range cmd.Args {
switch argTyp := arg.(type) {
case *parse.FieldNode:
field := helper.GetDefinedField(argTyp.String())
if field == nil {
helper.AddError(node, fmt.Sprintf("field %q not defined in struct %T", argTyp.String(), val.Interface()))
} else if kind, ok := field.IsKind(reflect.Bool); !ok {
helper.AddError(node, fmt.Sprintf("field %q is not type bool: got %s", argTyp.String(), kind))
}
helper.WithContext(setVisited(helper.Context(), argTyp))
func staticTypingRecursive(prefix string, val reflect.Value, node parse.Node, helper *AnalysisHelper) {
switch nodeTyp := node.(type) {
case *parse.IfNode:
for _, cmd := range nodeTyp.Pipe.Cmds {
for _, arg := range cmd.Args {
switch argTyp := arg.(type) {
case *parse.FieldNode:
if isVisited(helper.ctx, argTyp) {
break
}
}
}
break

case *parse.RangeNode:
var argPrefix string
// check the type of the argument passed to range: {{ range Arg }}
for _, cmd := range typ.Pipe.Cmds {
for _, arg := range cmd.Args {
switch argTyp := arg.(type) {
case *parse.FieldNode:
argPrefix = argTyp.String()
field := helper.GetDefinedField(argPrefix)
if field == nil {
helper.AddError(node, fmt.Sprintf("field %q not defined in struct %T", argTyp.String(), val.Interface()))
}
helper.WithContext(setVisited(helper.Context(), argTyp))

// TODO: assert that this field is a slice or array
typ := prefix + argTyp.String()
field := helper.GetDefinedField(typ)
if field == nil {
helper.AddError(node, fmt.Sprintf("field %q not defined in struct %T", typ, val.Interface()))
} else if kind, ok := field.IsKind(reflect.Bool); !ok {
helper.AddError(node, fmt.Sprintf("field %q is not type bool: got %s", typ, kind))
}
helper.WithContext(setVisited(helper.Context(), argTyp))
}
}

// TODO: this is indicative of a needed refactor. this should be recursive?
// Run a type check on the body of the range loop
Traverse(typ.List, func(n parse.Node) {
switch nTyp := n.(type) {
}
break

case *parse.RangeNode:
// TODO: this will break for {{ range }} statements with assignments:
// {{ $i, $v := range .Arg }}
var inferTyp = prefix
// check the type of the argument passed to range: {{ range .Arg }}
for _, cmd := range nodeTyp.Pipe.Cmds {
for _, arg := range cmd.Args {
switch argTyp := arg.(type) {
case *parse.FieldNode:
fqn := argPrefix + nTyp.String()
field := helper.GetDefinedField(fqn)
if isVisited(helper.ctx, argTyp) {
break
}
inferTyp = prefix + argTyp.String()
field := helper.GetDefinedField(inferTyp)
if field == nil {
helper.AddError(node, fmt.Sprintf("field %q not defined in struct %T", fqn, val.Interface()))
helper.AddError(node, fmt.Sprintf("field %q not defined in struct %T", argTyp.String(), val.Interface()))
}
helper.WithContext(setVisited(helper.Context(), nTyp))
helper.WithContext(setVisited(helper.Context(), argTyp))
}
})
}
}

break
// recurse on the body of the range loop using the inferred type
Traverse(nodeTyp.List, func(node parse.Node) {
staticTypingRecursive(inferTyp, val, node, helper)
})

break

case *parse.TemplateNode:
if !helper.IsDefinedTemplate(nodeTyp.Name) {
helper.AddError(node, fmt.Sprintf("template %q is not provided by struct %T or any of its embedded structs", nodeTyp.Name, val.Interface()))
} else if nodeTyp.Pipe == nil {
helper.AddError(node, fmt.Sprintf("template %q is not invoked with a pipeline", nodeTyp.Name))
} else if len(nodeTyp.Pipe.Cmds) == 1 {
// TODO: here we can check the type of the pipeline
// if the command is a DotNode, check the type of the struct for any embedded fields
// if the command is a FieldNode, check the type of the field and mark it as visited
_ = nodeTyp.Pipe.Cmds[0]
}

case *parse.TemplateNode:
if !helper.IsDefinedTemplate(typ.Name) {
helper.AddError(node, fmt.Sprintf("template %q is not provided by struct %T or any of its embedded structs", typ.Name, val.Interface()))
} else if typ.Pipe == nil {
helper.AddError(node, fmt.Sprintf("template %q is not invoked with a pipeline", typ.Name))
} else if len(typ.Pipe.Cmds) == 1 {
// TODO: here we can check the type of the pipeline
// if the command is a DotNode, check the type of the struct for any embedded fields
// if the command is a FieldNode, check the type of the field and mark it as visited
_ = typ.Pipe.Cmds[0]
}
break

// FieldNode is the last node that we want to check. Give a chance for analyzers
// higher up in the parse tree to mark them as visited.
case *parse.FieldNode:
if isVisited(helper.ctx, nodeTyp) {
break
}

// FieldNode is the last node that we want to check. Give a chance for analyzers
// higher up in the parse tree to mark them as visited.
case *parse.FieldNode:
if isVisited(helper.ctx, typ) {
break
}

field := helper.GetDefinedField(typ.String())
if field == nil {
helper.AddError(node, fmt.Sprintf("field %q not defined in struct %T", typ.String(), val.Interface()))
}
helper.WithContext(setVisited(helper.Context(), typ))
typ := prefix + nodeTyp.String()
field := helper.GetDefinedField(typ)
if field == nil {
helper.AddError(node, fmt.Sprintf("field %q not defined in struct %T", typ, val.Interface()))
}
helper.WithContext(setVisited(helper.Context(), nodeTyp))

// TODO: can we make further assertions here about the type of the field?
// TODO: can we make further assertions here about the type of the field?

break
break
}
}

}
// staticTyping enables static type checking on templateProvider parse trees by using
// reflection on the given struct type.
var staticTyping Analyzer = func(helper *AnalysisHelper) AnalyzerFunc {
return func(val reflect.Value, node parse.Node) {
staticTypingRecursive("", val, node, helper)
}
}
42 changes: 41 additions & 1 deletion compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func Test_Compile(t *testing.T) {
"World",
},
},
"Supports usage of {{ range }} statements over struct types": {
"Supports usage of {{ range }} statements over anonymous struct types": {
templateProvider: &StructRange{DefList: []struct {
DefField string
}{
Expand All @@ -92,7 +92,47 @@ func Test_Compile(t *testing.T) {
"World",
},
},
"Supports usage of {{ range }} statements over named struct types": {
templateProvider: &NamedStructRange{NamedStructs: []NamedStruct{
{DefField: "Hello"},
{DefField: "World"},
}},
expectRenderOutput: []string{
"Hello",
"World",
},
},

"Supports usage of {{ if }} statements within {{ range }} bodies": {
templateProvider: &IfWithinRange{
DefList: []DefinedIf{
{DefIf: true, Message: "Hello"},
},
},
expectRenderOutput: []string{
"Hello",
},
},
"Supports usage of {{ range }} statements within {{ range }} bodies": {
templateProvider: &StructRangeWithinRange{
ListOne: []StructOne{
{
ListTwo: []StructTwo{
{DefField: "Hello"},
},
},
{
ListTwo: []StructTwo{
{DefField: "World"},
},
},
},
},
expectRenderOutput: []string{
"Hello",
"World",
},
},
// template nesting tests
"Supports embedded struct fields": {
templateProvider: &EmbeddedField{
Expand Down
36 changes: 36 additions & 0 deletions testdata/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ func (*StructRange) TemplateText() string {
return `{{ range .DefList }}{{ .DefField }}{{ end }}`
}

type NamedStruct struct {
DefField string
}

type NamedStructRange struct {
NamedStructs []NamedStruct
}

func (*NamedStructRange) TemplateText() string {
return `{{ range .NamedStructs }}{{ .DefField }}{{ end }}`
}

type EmbeddedStruct struct {
DefField string
}
Expand Down Expand Up @@ -191,3 +203,27 @@ type OutletWithNestedLayout struct {
func (*OutletWithNestedLayout) TemplateText() string {
return `{{ .Content }}`
}

type IfWithinRange struct {
DefList []DefinedIf
}

func (*IfWithinRange) TemplateText() string {
return `{{ range .DefList }}{{ if .DefIf }}{{ .Message }}{{ end }}{{ end }}`
}

type StructTwo struct {
DefField string
}

type StructOne struct {
ListTwo []StructTwo
}

type StructRangeWithinRange struct {
ListOne []StructOne
}

func (*StructRangeWithinRange) TemplateText() string {
return `{{ range .ListOne }}{{ range .ListTwo }}{{ .DefField }}{{ end }}{{ end }}`
}

0 comments on commit e950caf

Please sign in to comment.