|
| 1 | +package walk_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "testing" |
| 6 | + |
| 7 | + "github.com/coder/guts/bindings" |
| 8 | + "github.com/coder/guts/bindings/walk" |
| 9 | +) |
| 10 | + |
| 11 | +// recordingVisitor stores every node it visits so tests can assert that |
| 12 | +// Walk descends into expected child slots without panicking. |
| 13 | +type recordingVisitor struct { |
| 14 | + visited []bindings.Node |
| 15 | +} |
| 16 | + |
| 17 | +func (r *recordingVisitor) Visit(node bindings.Node) walk.Visitor { |
| 18 | + r.visited = append(r.visited, node) |
| 19 | + return r |
| 20 | +} |
| 21 | + |
| 22 | +// keyword returns a fresh pointer to a LiteralKeyword. LiteralKeyword is a |
| 23 | +// string-typed Node, so callers need a pointer-to-string-typed-value, not |
| 24 | +// a composite literal. |
| 25 | +func keyword(k bindings.LiteralKeyword) *bindings.LiteralKeyword { |
| 26 | + return &k |
| 27 | +} |
| 28 | + |
| 29 | +// TestWalkCoversAllNodeTypes builds a synthetic tree that contains every |
| 30 | +// concrete bindings.Node implementation and walks it. The default branch |
| 31 | +// in Walk panics on unhandled types, so this test fails loudly if a new |
| 32 | +// node is added to the bindings package without a matching case here. |
| 33 | +func TestWalkCoversAllNodeTypes(t *testing.T) { |
| 34 | + t.Parallel() |
| 35 | + |
| 36 | + // Construct one of each Node. Some nodes (PropertyAssignment, Parameter, |
| 37 | + // HeritageClause, ImportSpecifier, EnumMember) only appear as children |
| 38 | + // of a parent, so the parent's slot is the way Walk reaches them. |
| 39 | + tree := &bindings.Interface{ |
| 40 | + Name: bindings.Identifier{Name: "Root"}, |
| 41 | + Parameters: []*bindings.TypeParameter{ |
| 42 | + {Name: bindings.Identifier{Name: "T"}, Type: keyword(bindings.KeywordString)}, |
| 43 | + }, |
| 44 | + Heritage: []*bindings.HeritageClause{ |
| 45 | + { |
| 46 | + Args: []bindings.ExpressionType{ |
| 47 | + &bindings.ExpressionWithTypeArguments{ |
| 48 | + Expression: &bindings.IdentifierExpression{ |
| 49 | + Name: bindings.Identifier{Name: "Base"}, |
| 50 | + }, |
| 51 | + Arguments: []bindings.ExpressionType{ |
| 52 | + &bindings.ReferenceType{Name: bindings.Identifier{Name: "X"}}, |
| 53 | + }, |
| 54 | + }, |
| 55 | + }, |
| 56 | + }, |
| 57 | + }, |
| 58 | + Fields: []*bindings.PropertySignature{ |
| 59 | + { |
| 60 | + Name: "everything", |
| 61 | + Type: &bindings.TypeIntersection{ |
| 62 | + Types: []bindings.ExpressionType{ |
| 63 | + &bindings.UnionType{ |
| 64 | + Types: []bindings.ExpressionType{ |
| 65 | + &bindings.LiteralType{Value: "a"}, |
| 66 | + &bindings.Null{}, |
| 67 | + keyword(bindings.KeywordString), |
| 68 | + }, |
| 69 | + }, |
| 70 | + &bindings.TypeLiteralNode{ |
| 71 | + Members: []*bindings.PropertySignature{ |
| 72 | + {Name: "nested", Type: keyword(bindings.KeywordString)}, |
| 73 | + }, |
| 74 | + }, |
| 75 | + &bindings.ArrayType{Node: keyword(bindings.KeywordString)}, |
| 76 | + &bindings.TupleType{Node: keyword(bindings.KeywordString)}, |
| 77 | + &bindings.ArrayLiteralType{ |
| 78 | + Elements: []bindings.ExpressionType{&bindings.LiteralType{Value: "elt"}}, |
| 79 | + }, |
| 80 | + bindings.OperatorNode(bindings.KeywordReadonly, keyword(bindings.KeywordString)), |
| 81 | + &bindings.TypeQuery{Name: bindings.Identifier{Name: "Other"}}, |
| 82 | + }, |
| 83 | + }, |
| 84 | + }, |
| 85 | + }, |
| 86 | + } |
| 87 | + |
| 88 | + // Independent node group exercising the value-expression and import |
| 89 | + // nodes added in PR 83 and PR 84. Walking each top-level node here |
| 90 | + // touches the remaining bindings.Node implementations. |
| 91 | + nodes := []bindings.Node{ |
| 92 | + tree, |
| 93 | + &bindings.Alias{ |
| 94 | + Name: bindings.Identifier{Name: "MyAlias"}, |
| 95 | + Type: keyword(bindings.KeywordString), |
| 96 | + }, |
| 97 | + &bindings.Enum{ |
| 98 | + Name: bindings.Identifier{Name: "Color"}, |
| 99 | + Members: []*bindings.EnumMember{ |
| 100 | + {Name: "Red", Value: &bindings.LiteralType{Value: "red"}}, |
| 101 | + }, |
| 102 | + }, |
| 103 | + &bindings.VariableStatement{ |
| 104 | + Declarations: &bindings.VariableDeclarationList{ |
| 105 | + Flags: bindings.NodeFlagsConstant, |
| 106 | + Declarations: []*bindings.VariableDeclaration{ |
| 107 | + { |
| 108 | + Name: bindings.Identifier{Name: "Schema"}, |
| 109 | + Type: &bindings.ReferenceType{Name: bindings.Identifier{Name: "Z"}}, |
| 110 | + Initializer: &bindings.CallExpression{ |
| 111 | + Expression: &bindings.PropertyAccessExpression{ |
| 112 | + Expression: &bindings.IdentifierExpression{ |
| 113 | + Name: bindings.Identifier{Name: "z"}, |
| 114 | + }, |
| 115 | + Name: "object", |
| 116 | + }, |
| 117 | + Arguments: []bindings.ExpressionType{ |
| 118 | + &bindings.ObjectLiteralExpression{ |
| 119 | + Properties: []*bindings.PropertyAssignment{ |
| 120 | + { |
| 121 | + Name: "id", |
| 122 | + Initializer: &bindings.CallExpression{ |
| 123 | + Expression: &bindings.PropertyAccessExpression{ |
| 124 | + Expression: &bindings.IdentifierExpression{ |
| 125 | + Name: bindings.Identifier{Name: "z"}, |
| 126 | + }, |
| 127 | + Name: "string", |
| 128 | + }, |
| 129 | + }, |
| 130 | + }, |
| 131 | + }, |
| 132 | + }, |
| 133 | + &bindings.ArrowFunction{ |
| 134 | + Parameters: []*bindings.Parameter{ |
| 135 | + {Name: "x", Type: keyword(bindings.KeywordString)}, |
| 136 | + }, |
| 137 | + ReturnType: keyword(bindings.KeywordString), |
| 138 | + Body: &bindings.IdentifierExpression{ |
| 139 | + Name: bindings.Identifier{Name: "x"}, |
| 140 | + }, |
| 141 | + }, |
| 142 | + }, |
| 143 | + }, |
| 144 | + }, |
| 145 | + }, |
| 146 | + }, |
| 147 | + }, |
| 148 | + &bindings.ImportDeclaration{ |
| 149 | + Module: "zod", |
| 150 | + Named: []*bindings.ImportSpecifier{ |
| 151 | + {Name: "z"}, |
| 152 | + }, |
| 153 | + }, |
| 154 | + } |
| 155 | + |
| 156 | + v := &recordingVisitor{} |
| 157 | + for _, node := range nodes { |
| 158 | + walk.Walk(v, node) |
| 159 | + } |
| 160 | + |
| 161 | + // Verify that Walk reached every new node type. The set of expected |
| 162 | + // node types here is the union of nodes constructed above. If any are |
| 163 | + // missing, Walk lost a child slot somewhere along the way. |
| 164 | + seen := map[string]bool{} |
| 165 | + for _, n := range v.visited { |
| 166 | + seen[fmt.Sprintf("%T", n)] = true |
| 167 | + } |
| 168 | + |
| 169 | + want := []string{ |
| 170 | + "*bindings.Interface", |
| 171 | + "*bindings.TypeParameter", |
| 172 | + "*bindings.HeritageClause", |
| 173 | + "*bindings.ExpressionWithTypeArguments", |
| 174 | + "*bindings.IdentifierExpression", |
| 175 | + "*bindings.ReferenceType", |
| 176 | + "*bindings.PropertySignature", |
| 177 | + "*bindings.TypeIntersection", |
| 178 | + "*bindings.UnionType", |
| 179 | + "*bindings.LiteralType", |
| 180 | + "*bindings.Null", |
| 181 | + "*bindings.LiteralKeyword", |
| 182 | + "*bindings.TypeLiteralNode", |
| 183 | + "*bindings.ArrayType", |
| 184 | + "*bindings.TupleType", |
| 185 | + "*bindings.ArrayLiteralType", |
| 186 | + "*bindings.OperatorNodeType", |
| 187 | + "*bindings.TypeQuery", |
| 188 | + "*bindings.Alias", |
| 189 | + "*bindings.Enum", |
| 190 | + "*bindings.EnumMember", |
| 191 | + "*bindings.VariableStatement", |
| 192 | + "*bindings.VariableDeclarationList", |
| 193 | + "*bindings.VariableDeclaration", |
| 194 | + "*bindings.CallExpression", |
| 195 | + "*bindings.PropertyAccessExpression", |
| 196 | + "*bindings.ObjectLiteralExpression", |
| 197 | + "*bindings.PropertyAssignment", |
| 198 | + "*bindings.ArrowFunction", |
| 199 | + "*bindings.Parameter", |
| 200 | + "*bindings.ImportDeclaration", |
| 201 | + "*bindings.ImportSpecifier", |
| 202 | + } |
| 203 | + for _, name := range want { |
| 204 | + if !seen[name] { |
| 205 | + t.Errorf("Walk did not visit %s", name) |
| 206 | + } |
| 207 | + } |
| 208 | +} |
| 209 | + |
| 210 | +// TestWalkStopsWhenVisitReturnsNil verifies that Walk honours the Visitor |
| 211 | +// contract: returning nil from Visit halts descent into that subtree. |
| 212 | +func TestWalkStopsWhenVisitReturnsNil(t *testing.T) { |
| 213 | + t.Parallel() |
| 214 | + |
| 215 | + leaf := keyword(bindings.KeywordString) |
| 216 | + parent := &bindings.ArrayType{Node: leaf} |
| 217 | + |
| 218 | + v := &stoppingVisitor{stopAt: parent} |
| 219 | + walk.Walk(v, parent) |
| 220 | + |
| 221 | + if len(v.visited) != 1 { |
| 222 | + t.Fatalf("expected 1 visit, got %d", len(v.visited)) |
| 223 | + } |
| 224 | + if v.visited[0] != bindings.Node(parent) { |
| 225 | + t.Fatalf("expected to visit parent, got %T", v.visited[0]) |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +type stoppingVisitor struct { |
| 230 | + stopAt bindings.Node |
| 231 | + visited []bindings.Node |
| 232 | +} |
| 233 | + |
| 234 | +func (s *stoppingVisitor) Visit(node bindings.Node) walk.Visitor { |
| 235 | + s.visited = append(s.visited, node) |
| 236 | + if node == s.stopAt { |
| 237 | + return nil |
| 238 | + } |
| 239 | + return s |
| 240 | +} |
0 commit comments