Skip to content

Commit 6b932e3

Browse files
committed
Ensure exhaustive switch statements
PR #162 revealed that missing node type handlers in switch statements cause silent data loss - CDATA content was being dropped because CharDataNode wasn't handled. This PR adds defensive programming to prevent this entire class of bug. Changes: - Added exhaustive switch coverage for all xmlquery.NodeType values - Added exhaustive switch coverage for xml.Token and html.TokenType - Added exhaustive switch coverage for json.Token and json.Delim - Added default cases with panic() to all exhaustive switches - Added explicit documented defaults to intentionally non-exhaustive switches This follows the Go stdlib pattern (runtime/panic.go, go/types) of using panic("unreachable") for "should be impossible" states. If future xmlquery versions add new node types, or if we overlook a handler, tests will fail immediately rather than silently corrupting output. All switches now explicitly handle defaults: - 8 exhaustive switches panic on unknown values - 7 non-exhaustive switches have documented intentional behavior Added TestExhaustiveNodeTypeHandling to verify all node types are processed without panicking. All existing tests pass. This is a non-breaking change - no API modifications, only defensive additions to switch statements. Related: #162
1 parent ec5a59a commit 6b932e3

File tree

4 files changed

+76
-1
lines changed

4 files changed

+76
-1
lines changed

internal/utils/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ func LoadConfig(fileName string) error {
6161
config.NoColor, _ = strconv.ParseBool(value)
6262
case "color":
6363
config.Color, _ = strconv.ParseBool(value)
64+
default:
65+
// Ignore unknown config options for forward compatibility
6466
}
6567
}
6668

internal/utils/jsonutil.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package utils
22

33
import (
4+
"fmt"
45
"strings"
56

67
"github.com/antchfx/xmlquery"
@@ -39,6 +40,11 @@ func NodeToJSON(node *xmlquery.Node, depth int) interface{} {
3940
if text != "" {
4041
textParts = append(textParts, text)
4142
}
43+
case xmlquery.CommentNode, xmlquery.DeclarationNode, xmlquery.ProcessingInstruction, xmlquery.NotationNode:
44+
// Skip these in JSON output
45+
default:
46+
// Should be impossible: all valid child node types handled above
47+
panic(fmt.Sprintf("unknown NodeType as child of DocumentNode: %v", child.Type))
4248
}
4349
}
4450

@@ -53,8 +59,13 @@ func NodeToJSON(node *xmlquery.Node, depth int) interface{} {
5359
case xmlquery.TextNode, xmlquery.CharDataNode:
5460
return strings.TrimSpace(node.Data)
5561

56-
default:
62+
case xmlquery.CommentNode:
63+
// Comments passed as root, return empty
5764
return nil
65+
66+
default:
67+
// Should be impossible: DocumentNode, ElementNode, TextNode, CharDataNode, CommentNode are the only valid root nodes
68+
panic(fmt.Sprintf("unknown NodeType passed to NodeToJSON: %v", node.Type))
5869
}
5970
}
6071

@@ -79,6 +90,11 @@ func nodeToJSONInternal(node *xmlquery.Node, depth int) interface{} {
7990
case xmlquery.ElementNode:
8091
childResult := nodeToJSONInternal(child, depth-1)
8192
addToResult(result, child.Data, childResult)
93+
case xmlquery.CommentNode, xmlquery.ProcessingInstruction:
94+
// Skip these in JSON output
95+
default:
96+
// Should be impossible: all valid element child types handled above
97+
panic(fmt.Sprintf("unknown NodeType as child of ElementNode: %v", child.Type))
8298
}
8399
}
84100

@@ -103,6 +119,11 @@ func getTextContent(node *xmlquery.Node) string {
103119
}
104120
case xmlquery.ElementNode:
105121
parts = append(parts, getTextContent(child))
122+
case xmlquery.CommentNode, xmlquery.ProcessingInstruction:
123+
// Skip these when extracting text
124+
default:
125+
// Should be impossible: all valid element child types handled above
126+
panic(fmt.Sprintf("unknown NodeType in getTextContent: %v", child.Type))
106127
}
107128
}
108129
return strings.Join(parts, "\n")

internal/utils/jsonutil_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,42 @@ func TestXmlToJSON(t *testing.T) {
4545
assert.Equal(t, expectedJson, output.String())
4646
}
4747
}
48+
49+
func TestExhaustiveNodeTypeHandling(t *testing.T) {
50+
// Test that all xmlquery node types are handled without panicking
51+
// This verifies our exhaustive switch statements work correctly
52+
53+
xmlInput := `<?xml version="1.0"?>
54+
<!DOCTYPE root>
55+
<!-- This is a comment -->
56+
<root>
57+
<element>text content</element>
58+
<cdata><![CDATA[raw & unescaped < > content]]></cdata>
59+
<!-- another comment inside -->
60+
<?processing-instruction data?>
61+
<mixed>text<child>more</child>tail</mixed>
62+
</root>`
63+
64+
node, err := xmlquery.Parse(strings.NewReader(xmlInput))
65+
assert.NoError(t, err)
66+
67+
// Should not panic - this exercises all the node types
68+
result := NodeToJSON(node, -1)
69+
assert.NotNil(t, result)
70+
71+
// Verify the result is a map
72+
resultMap, ok := result.(map[string]interface{})
73+
assert.True(t, ok)
74+
75+
// Verify root element exists
76+
root, ok := resultMap["root"]
77+
assert.True(t, ok)
78+
79+
rootMap, ok := root.(map[string]interface{})
80+
assert.True(t, ok)
81+
82+
// Verify CDATA is preserved as text
83+
cdataElem, ok := rootMap["cdata"]
84+
assert.True(t, ok)
85+
assert.Contains(t, cdataElem, "raw & unescaped")
86+
}

internal/utils/utils.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ func FormatXml(reader io.Reader, writer io.Writer, indent string, colors int) er
190190
_, _ = fmt.Fprint(writer, tagColor("<!"), string(typedToken), tagColor(">"))
191191
_, _ = fmt.Fprint(writer, newline, strings.Repeat(indent, level))
192192
default:
193+
// Should be impossible: all xml.Token types handled above
194+
panic(fmt.Sprintf("unknown xml.Token type: %T", token))
193195
}
194196
}
195197

@@ -403,6 +405,9 @@ func FormatHtml(reader io.Reader, writer io.Writer, indent string, colors int) e
403405
if level == 0 {
404406
_, _ = fmt.Fprint(writer, newline)
405407
}
408+
default:
409+
// Should be impossible: all html.TokenType values handled above
410+
panic(fmt.Sprintf("unknown html.TokenType: %v", token))
406411
}
407412
}
408413

@@ -469,6 +474,9 @@ func FormatJson(reader io.Reader, writer io.Writer, indent string, colors int) e
469474
level--
470475
}
471476
_, _ = fmt.Fprint(writer, newline, strings.Repeat(indent, level), tagColor("]"))
477+
default:
478+
// Should be impossible: json.Delim can only be '{', '}', '[', ']'
479+
panic(fmt.Sprintf("unknown json.Delim: %v", tokenType))
472480
}
473481
case string:
474482
escapedToken := strconv.Quote(token.(string))
@@ -485,6 +493,9 @@ func FormatJson(reader io.Reader, writer io.Writer, indent string, colors int) e
485493
_, _ = fmt.Fprintf(writer, "%s%v", prefix, valueColor(token))
486494
case nil:
487495
_, _ = fmt.Fprintf(writer, "%s%s", prefix, valueColor("null"))
496+
default:
497+
// Should be impossible: all json.Token types handled above
498+
panic(fmt.Sprintf("unknown json.Token type: %T", token))
488499
}
489500

490501
switch tokenState {
@@ -494,6 +505,8 @@ func FormatJson(reader io.Reader, writer io.Writer, indent string, colors int) e
494505
suffix = "," + newline + strings.Repeat(indent, level)
495506
case jsonTokenArrayComma:
496507
suffix = "," + newline + strings.Repeat(indent, level)
508+
default:
509+
// Other token states don't affect suffix formatting
497510
}
498511

499512
prefix = suffix

0 commit comments

Comments
 (0)