Skip to content

Commit 28cc22d

Browse files
mvazquezcclaude
andauthored
Code cleanup and refactoring (openshift-kni#14)
* Code cleanup and refactoring Signed-off-by: Mario Vazquez <mavazque@redhat.com> Co-authored-by: Claude <noreply@anthropic.com> * Added CodeRabbit suggestions Signed-off-by: Mario Vazquez <mavazque@redhat.com> Co-authored-by: Claude <noreply@anthropic.com> * Adds a section to agents.md on design decisions Signed-off-by: Mario Vazquez <mavazque@redhat.com> --------- Signed-off-by: Mario Vazquez <mavazque@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8de9f8b commit 28cc22d

11 files changed

Lines changed: 196 additions & 362 deletions

File tree

AGENTS.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,3 +983,20 @@ Before submitting new rules:
983983
- [ ] `supporting_doc` is included when relevant documentation exists
984984
- [ ] Rules are tested with real or realistic diff data
985985
- [ ] No duplicate rules (check existing rules first)
986+
987+
## Conscious Design Choices
988+
989+
This section documents intentional design decisions that may be flagged by linters or code review tools. These are not defects -- they are deliberate choices with documented rationale.
990+
991+
### Ignoring `fmt.Fprint*` Errors in Report Generators
992+
993+
The text and reporting generators (`pkg/report/text.go`, `pkg/report/reporting.go`) call `fmt.Fprint`, `fmt.Fprintf`, and `fmt.Fprintln` without checking their return values. This is intentional.
994+
995+
**Why this is acceptable:**
996+
997+
- Ignoring `fmt.Fprint*` return values when writing CLI output to an `io.Writer` is idiomatic Go. The standard library tools (`go test`, `go vet`, `go fmt`) follow this same pattern, and neither `go vet` nor `golangci-lint` flag it.
998+
- There are ~90 such calls in `text.go` and ~67 in `reporting.go`. Wrapping all of them with error-capturing wrappers (`writeErr` field, short-circuit checks, `g.write`/`g.writef`/`g.writeln` methods) would add significant structural complexity to what are currently clean, readable print functions.
999+
- In the CLI context, a write failure to stdout causes `SIGPIPE`, terminating the process. A returned error would never reach a caller that could act on it.
1000+
- The HTML generator uses `template.Execute` which handles write errors internally, but that is a fundamentally different pattern (single write call vs. many imperative prints). Comparing them is not meaningful.
1001+
1002+
**When to revisit:** If a future use case requires writing reports over a network connection or other unreliable transport where partial failure must be detected and handled, this decision can be revisited with a clear justification for the added complexity.

pkg/analyzer/analyzer.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,14 @@ func NewFromBytes(rulesData []byte, version string) (*Analyzer, error) {
5151
// The format parameter determines output type: "text" or "html".
5252
// The mode parameter determines output mode: "simple" or "reporting".
5353
func (a *Analyzer) Analyze(w io.Writer, validationReport types.ValidationReport, format, mode string) error {
54-
if mode == "reporting" {
54+
switch mode {
55+
case "reporting":
5556
return a.generateReportingOutput(w, validationReport)
57+
case "simple":
58+
return a.generateFormattedOutput(w, validationReport, format)
59+
default:
60+
return fmt.Errorf("unsupported output mode: %s (valid: simple, reporting)", mode)
5661
}
57-
58-
return a.generateFormattedOutput(w, validationReport, format)
5962
}
6063

6164
// generateReportingOutput generates output in reporting mode.

pkg/parser/diff.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,6 @@ import (
88
"github.com/openshift-kni/rds-analyzer/pkg/types"
99
)
1010

11-
// ANSI color codes for terminal output formatting.
12-
const (
13-
ColorReset = "\033[0m"
14-
ColorRed = "\033[31m"
15-
ColorGreen = "\033[32m"
16-
ColorYellow = "\033[33m"
17-
ColorBlue = "\033[34m"
18-
ColorCyan = "\033[36m"
19-
ColorBold = "\033[1m"
20-
ColorDim = "\033[2m"
21-
)
22-
2311
// diffLineInfo holds parsed line info during diff processing.
2412
type diffLineInfo struct {
2513
content string // Line content without diff marker

pkg/report/html.go

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

33
import (
4+
"html"
45
"html/template"
56
"io"
67
"path/filepath"
@@ -16,10 +17,7 @@ import (
1617
// escapeHTML escapes characters that could break HTML structure.
1718
// Returns template.HTML to prevent double-escaping by the template engine.
1819
func escapeHTML(s string) template.HTML {
19-
s = strings.ReplaceAll(s, "&", "&amp;")
20-
s = strings.ReplaceAll(s, "<", "&lt;")
21-
s = strings.ReplaceAll(s, ">", "&gt;")
22-
return template.HTML(s)
20+
return template.HTML(html.EscapeString(s))
2321
}
2422

2523
// HTMLReport contains all data needed to render the HTML report.
@@ -310,10 +308,7 @@ func (g *HTMLGenerator) processDiffs(diffs []types.Diff, stats *ImpactStats) ([]
310308
for _, d := range diffs {
311309
// Handle empty diffs - add minimal DiffCheck for count rules only.
312310
if d.DiffOutput == "" {
313-
allDiffChecks = append(allDiffChecks, types.DiffCheck{
314-
CRName: d.CRName,
315-
TemplateFileName: filepath.Base(d.CorrelatedTemplate),
316-
})
311+
allDiffChecks = append(allDiffChecks, minimalDiffCheck(d))
317312
continue
318313
}
319314

@@ -454,6 +449,9 @@ func getMatchingRulesHTML(line, diffType string, ruleResult rules.EvaluationResu
454449
for _, condResult := range ruleResult.Conditions {
455450
if condResult.ConditionType == diffType && condResult.Matched {
456451
trimmedMatched := strings.TrimSpace(condResult.MatchedText)
452+
if trimmedMatched == "" {
453+
continue
454+
}
457455
if strings.Contains(trimmedLine, trimmedMatched) || strings.Contains(trimmedMatched, trimmedLine) {
458456
if !seen[condResult.RuleID] {
459457
seen[condResult.RuleID] = true

pkg/report/html_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func TestEscapeHTML(t *testing.T) {
270270
{
271271
name: "multiple special characters",
272272
input: "<script>alert('xss')</script>",
273-
expected: "&lt;script&gt;alert('xss')&lt;/script&gt;",
273+
expected: "&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;",
274274
},
275275
{
276276
name: "yaml content",

pkg/report/reporting.go

Lines changed: 7 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,7 @@ func (g *ReportingGenerator) evaluateAllDiffs(diffs []types.Diff) ([]reportingDi
240240
for _, d := range diffs {
241241
// Handle empty diffs - add minimal DiffCheck for count rules only.
242242
if d.DiffOutput == "" {
243-
allDiffChecks = append(allDiffChecks, types.DiffCheck{
244-
CRName: d.CRName,
245-
TemplateFileName: filepath.Base(d.CorrelatedTemplate),
246-
})
243+
allDiffChecks = append(allDiffChecks, minimalDiffCheck(d))
247244
continue
248245
}
249246

@@ -256,7 +253,7 @@ func (g *ReportingGenerator) evaluateAllDiffs(diffs []types.Diff) ([]reportingDi
256253
allDiffChecks = append(allDiffChecks, diffCheck)
257254

258255
ruleResult := g.ruleEngine.Evaluate(diffCheck)
259-
hasNeedsReview := g.hasUnmatchedLines(diffCheck, ruleResult)
256+
hasNeedsReview := hasUnmatchedLines(diffCheck, ruleResult)
260257
finalImpact := determineImpact(ruleResult, hasNeedsReview)
261258

262259
// Skip NotADeviation diffs for display purposes only.
@@ -280,44 +277,9 @@ func (g *ReportingGenerator) evaluateAllDiffs(diffs []types.Diff) ([]reportingDi
280277
return results, allDiffChecks
281278
}
282279

283-
// hasUnmatchedLines checks if any diff lines are not matched by rules.
284-
func (g *ReportingGenerator) hasUnmatchedLines(diffCheck types.DiffCheck, ruleResult rules.EvaluationResult) bool {
285-
for _, line := range diffCheck.ExpectedNotFound {
286-
if len(g.getMatchingRuleIDs(line, "ExpectedNotFound", ruleResult)) == 0 {
287-
return true
288-
}
289-
}
290-
for _, line := range diffCheck.FoundNotExpected {
291-
if len(g.getMatchingRuleIDs(line, "FoundNotExpected", ruleResult)) == 0 {
292-
return true
293-
}
294-
}
295-
for _, line := range diffCheck.FoundValue {
296-
if len(g.getMatchingRuleIDs(line, "ExpectedFound", ruleResult)) == 0 {
297-
return true
298-
}
299-
}
300-
return false
301-
}
302-
303280
// getMatchingRuleIDs returns rule IDs that matched a specific line.
304281
func (g *ReportingGenerator) getMatchingRuleIDs(line, diffType string, ruleResult rules.EvaluationResult) []string {
305-
trimmedLine := strings.TrimSpace(line)
306-
var ruleIDs []string
307-
seen := make(map[string]bool)
308-
309-
for _, condResult := range ruleResult.Conditions {
310-
if condResult.ConditionType == diffType && condResult.Matched {
311-
trimmedMatched := strings.TrimSpace(condResult.MatchedText)
312-
if strings.Contains(trimmedLine, trimmedMatched) || strings.Contains(trimmedMatched, trimmedLine) {
313-
if !seen[condResult.RuleID] {
314-
seen[condResult.RuleID] = true
315-
ruleIDs = append(ruleIDs, condResult.RuleID)
316-
}
317-
}
318-
}
319-
}
320-
return ruleIDs
282+
return matchingRuleIDs(line, diffType, ruleResult)
321283
}
322284

323285
// extractImpactingRules returns only the rules with Impacting impact.
@@ -360,41 +322,29 @@ func (g *ReportingGenerator) extractUnresolvedLines(diffCheck types.DiffCheck, r
360322

361323
for _, line := range diffCheck.ExpectedNotFound {
362324
if len(g.getMatchingRuleIDs(line, "ExpectedNotFound", ruleResult)) == 0 {
363-
unresolved.expectedNotFound = append(unresolved.expectedNotFound, g.stripANSI(line))
325+
unresolved.expectedNotFound = append(unresolved.expectedNotFound, stripANSI(line))
364326
}
365327
}
366328

367329
for _, line := range diffCheck.FoundNotExpected {
368330
if len(g.getMatchingRuleIDs(line, "FoundNotExpected", ruleResult)) == 0 {
369-
unresolved.foundNotExpected = append(unresolved.foundNotExpected, g.stripANSI(line))
331+
unresolved.foundNotExpected = append(unresolved.foundNotExpected, stripANSI(line))
370332
}
371333
}
372334

373335
// For ExpectedFound (value differences), include both expected and found if unmatched.
374336
for i, line := range diffCheck.FoundValue {
375337
if len(g.getMatchingRuleIDs(line, "ExpectedFound", ruleResult)) == 0 {
376-
unresolved.foundValue = append(unresolved.foundValue, g.stripANSI(line))
338+
unresolved.foundValue = append(unresolved.foundValue, stripANSI(line))
377339
if i < len(diffCheck.ExpectedValue) {
378-
unresolved.expectedValue = append(unresolved.expectedValue, g.stripANSI(diffCheck.ExpectedValue[i]))
340+
unresolved.expectedValue = append(unresolved.expectedValue, stripANSI(diffCheck.ExpectedValue[i]))
379341
}
380342
}
381343
}
382344

383345
return unresolved
384346
}
385347

386-
// stripANSI removes ANSI color codes from a string.
387-
func (g *ReportingGenerator) stripANSI(s string) string {
388-
s = strings.ReplaceAll(s, parser.ColorReset, "")
389-
s = strings.ReplaceAll(s, parser.ColorRed, "")
390-
s = strings.ReplaceAll(s, parser.ColorGreen, "")
391-
s = strings.ReplaceAll(s, parser.ColorYellow, "")
392-
s = strings.ReplaceAll(s, parser.ColorBlue, "")
393-
s = strings.ReplaceAll(s, parser.ColorCyan, "")
394-
s = strings.ReplaceAll(s, parser.ColorBold, "")
395-
return s
396-
}
397-
398348
// printIndentedLines prints lines preserving their relative YAML indentation.
399349
func (g *ReportingGenerator) printIndentedLines(lines []string, baseIndent string) {
400350
if len(lines) == 0 {
@@ -480,15 +430,6 @@ func (g *ReportingGenerator) printContextualDiffView(diffLines []types.DiffLine,
480430
}
481431
}
482432

483-
// extractDiffChecks extracts DiffCheck objects from results.
484-
func (g *ReportingGenerator) extractDiffChecks(results []reportingDiffResult) []types.DiffCheck {
485-
checks := make([]types.DiffCheck, len(results))
486-
for i, r := range results {
487-
checks[i] = r.diffCheck
488-
}
489-
return checks
490-
}
491-
492433
// hasImpactingMissingCRs checks if there are any impacting missing CRs.
493434
func (g *ReportingGenerator) hasImpactingMissingCRs(results map[string]rules.MissingCRResult) bool {
494435
for _, r := range results {

pkg/report/reporting_test.go

Lines changed: 1 addition & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,6 @@ func TestReportingGenerator_TemplateSeparator(t *testing.T) {
393393
}
394394

395395
func TestReportingGenerator_StripANSI(t *testing.T) {
396-
generator := &ReportingGenerator{}
397-
398396
tests := []struct {
399397
input string
400398
expected string
@@ -406,7 +404,7 @@ func TestReportingGenerator_StripANSI(t *testing.T) {
406404
}
407405

408406
for _, tt := range tests {
409-
result := generator.stripANSI(tt.input)
407+
result := stripANSI(tt.input)
410408
if result != tt.expected {
411409
t.Errorf("stripANSI(%q) = %q, want %q", tt.input, result, tt.expected)
412410
}
@@ -560,95 +558,6 @@ func TestReportingGenerator_PrintCountRuleViolations_Empty(t *testing.T) {
560558
}
561559
}
562560

563-
func TestReportingGenerator_ExtractDiffChecks(t *testing.T) {
564-
rulesFile := createTestRulesFile(t)
565-
engine, err := rules.NewEngine(rulesFile)
566-
if err != nil {
567-
t.Fatalf("Failed to create engine: %v", err)
568-
}
569-
570-
generator := NewReportingGenerator(engine)
571-
572-
// Create test data with reportingDiffResult
573-
results := []reportingDiffResult{
574-
{
575-
diffCheck: types.DiffCheck{
576-
CRName: "v1_ConfigMap_ns1_config1",
577-
TemplateFileName: "Config1.yaml",
578-
ExpectedNotFound: []string{"key1: value1"},
579-
},
580-
finalImpact: "Impacting",
581-
},
582-
{
583-
diffCheck: types.DiffCheck{
584-
CRName: "v1_ConfigMap_ns2_config2",
585-
TemplateFileName: "Config2.yaml",
586-
FoundNotExpected: []string{"extra: field"},
587-
},
588-
finalImpact: "NotImpacting",
589-
},
590-
{
591-
diffCheck: types.DiffCheck{
592-
CRName: "v1_Secret_ns3_secret1",
593-
TemplateFileName: "Secret1.yaml",
594-
ExpectedValue: []string{"password: old"},
595-
FoundValue: []string{"password: new"},
596-
},
597-
finalImpact: "NeedsReview",
598-
},
599-
}
600-
601-
checks := generator.extractDiffChecks(results)
602-
603-
// Verify correct number of checks extracted
604-
if len(checks) != 3 {
605-
t.Fatalf("expected 3 checks, got %d", len(checks))
606-
}
607-
608-
// Verify first check
609-
if checks[0].CRName != "v1_ConfigMap_ns1_config1" {
610-
t.Errorf("check[0].CRName = %q, want %q", checks[0].CRName, "v1_ConfigMap_ns1_config1")
611-
}
612-
if checks[0].TemplateFileName != "Config1.yaml" {
613-
t.Errorf("check[0].TemplateFileName = %q, want %q", checks[0].TemplateFileName, "Config1.yaml")
614-
}
615-
if len(checks[0].ExpectedNotFound) != 1 {
616-
t.Errorf("check[0].ExpectedNotFound length = %d, want 1", len(checks[0].ExpectedNotFound))
617-
}
618-
619-
// Verify second check
620-
if checks[1].CRName != "v1_ConfigMap_ns2_config2" {
621-
t.Errorf("check[1].CRName = %q, want %q", checks[1].CRName, "v1_ConfigMap_ns2_config2")
622-
}
623-
if len(checks[1].FoundNotExpected) != 1 {
624-
t.Errorf("check[1].FoundNotExpected length = %d, want 1", len(checks[1].FoundNotExpected))
625-
}
626-
627-
// Verify third check
628-
if checks[2].CRName != "v1_Secret_ns3_secret1" {
629-
t.Errorf("check[2].CRName = %q, want %q", checks[2].CRName, "v1_Secret_ns3_secret1")
630-
}
631-
if len(checks[2].ExpectedValue) != 1 || len(checks[2].FoundValue) != 1 {
632-
t.Errorf("check[2] should have 1 ExpectedValue and 1 FoundValue")
633-
}
634-
}
635-
636-
func TestReportingGenerator_ExtractDiffChecks_Empty(t *testing.T) {
637-
rulesFile := createTestRulesFile(t)
638-
engine, err := rules.NewEngine(rulesFile)
639-
if err != nil {
640-
t.Fatalf("Failed to create engine: %v", err)
641-
}
642-
643-
generator := NewReportingGenerator(engine)
644-
645-
checks := generator.extractDiffChecks([]reportingDiffResult{})
646-
647-
if len(checks) != 0 {
648-
t.Errorf("expected 0 checks for empty input, got %d", len(checks))
649-
}
650-
}
651-
652561
func TestReportingGenerator_ExtractResolvedRules(t *testing.T) {
653562
rulesFile := createTestRulesFile(t)
654563
engine, err := rules.NewEngine(rulesFile)

0 commit comments

Comments
 (0)