diff --git a/render_test.go b/render_test.go index 32051d5..d65b1c1 100644 --- a/render_test.go +++ b/render_test.go @@ -64,10 +64,11 @@ func (*unhandledNode) isNode() {} // nodeSwitchSkips is the kinds a switch is never handed: its callers step past them first. var nodeSwitchSkips = map[string][]string{ + "childNamed": {"choice", "table", "tableRow"}, "columnItems": {"folder", "table", "tableRow"}, "prove": {"folder"}, "render": {"folder"}, - "childNamed": {"choice", "table", "tableRow"}, + "routeSteps": {"choice", "folder", "nullItem", "template"}, } func TestNodeSwitchesHandleEveryKind(t *testing.T) { @@ -88,16 +89,7 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) { } { samples[reflect.TypeOf(n).Elem().Name()] = n } - var kinds []string - _, files := sourceFiles(t) - for _, file := range files { - for _, decl := range file.Decls { - if d, isFunc := decl.(*ast.FuncDecl); isFunc && d.Name.Name == "isNode" { - kinds = append(kinds, receiverType(d.Recv)) - } - } - } - slices.Sort(kinds) + kinds := nodeKinds(t) sampled := make([]string, 0, len(samples)) for kind := range samples { sampled = append(sampled, kind) @@ -106,7 +98,8 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) { if !slices.Equal(kinds, sampled) { t.Fatalf("node kinds %v, samples %v; give every kind a sample here", kinds, sampled) } - for name, call := range map[string]func(node){ + switches := map[string]func(node){ + "childNamed": func(n node) { _, _ = childNamed(n, "x") }, "columnItems": func(n node) { columnItems(n) }, "contained": func(n node) { contained(n) }, "paths": func(n node) { paths(n, false) }, @@ -116,8 +109,18 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) { render(engine(1).drawState, n, renderEnv{frames: &frames, row: renderedRow{tbl, 0}}) }, "renderEdges": func(n node) { renderEdges(n) }, - "childNamed": func(n node) { _, _ = childNamed(n, "x") }, - } { + "routeSteps": func(n node) { _, _ = routeSteps(nil, nil, tbl, tableRoute{next: n}, 0) }, + } + found := exhaustiveNodeSwitches(t, kinds) + listed := make([]string, 0, len(switches)) + for name := range switches { + listed = append(listed, name) + } + slices.Sort(listed) + if !slices.Equal(found, listed) { + t.Fatalf("the functions switching over node kinds with a panicking default are %v, this test calls %v; call each here", found, listed) + } + for name, call := range switches { mustPanic(t, name+" on an unhandled node", func() { call(&unhandledNode{}) }) for _, kind := range kinds { if slices.Contains(nodeSwitchSkips[name], kind) { @@ -141,3 +144,73 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) { } } } + +func nodeKinds(t *testing.T) []string { + t.Helper() + var kinds []string + _, files := sourceFiles(t) + for _, file := range files { + for _, decl := range file.Decls { + if d, isFunc := decl.(*ast.FuncDecl); isFunc && d.Name.Name == "isNode" { + kinds = append(kinds, receiverType(d.Recv)) + } + } + } + slices.Sort(kinds) + return kinds +} + +// exhaustiveNodeSwitches names each function holding a type switch that has a case for a +// node kind and a default that panics. +func exhaustiveNodeSwitches(t *testing.T, kinds []string) []string { + t.Helper() + var names []string + _, files := sourceFiles(t) + for _, file := range files { + for _, decl := range file.Decls { + d, isFunc := decl.(*ast.FuncDecl) + if !isFunc || d.Body == nil { + continue + } + ast.Inspect(d.Body, func(n ast.Node) bool { + if sw, isTypeSwitch := n.(*ast.TypeSwitchStmt); isTypeSwitch && switchesOverNode(sw, kinds) { + names = append(names, d.Name.Name) + } + return true + }) + } + } + slices.Sort(names) + return slices.Compact(names) +} + +func switchesOverNode(sw *ast.TypeSwitchStmt, kinds []string) bool { + casesNode, panics := false, false + for _, stmt := range sw.Body.List { + clause := stmt.(*ast.CaseClause) + if clause.List == nil { + panics = slices.ContainsFunc(clause.Body, isPanic) + } + for _, e := range clause.List { + if star, isPointer := e.(*ast.StarExpr); isPointer { + if id, isIdent := star.X.(*ast.Ident); isIdent && slices.Contains(kinds, id.Name) { + casesNode = true + } + } + } + } + return casesNode && panics +} + +func isPanic(stmt ast.Stmt) bool { + expr, isExpr := stmt.(*ast.ExprStmt) + if !isExpr { + return false + } + call, isCall := expr.X.(*ast.CallExpr) + if !isCall { + return false + } + id, isIdent := call.Fun.(*ast.Ident) + return isIdent && id.Name == "panic" +}