Skip to content

Commit

Permalink
Checkstyle reporter (#1129)
Browse files Browse the repository at this point in the history
Added checkstyle reporter

Co-authored-by: Jonathan Drude <jonathan.drude@linetgroup.com>
  • Loading branch information
jonathan-dev and Jonathan Drude authored Oct 10, 2024
1 parent 325b9e0 commit f01f208
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 7 deletions.
21 changes: 20 additions & 1 deletion cmd/pint/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var (
baseBranchFlag = "base-branch"
failOnFlag = "fail-on"
teamCityFlag = "teamcity"
checkStyleFlag = "checkstyle"
)

var ciCmd = &cli.Command{
Expand Down Expand Up @@ -54,6 +55,12 @@ var ciCmd = &cli.Command{
Value: false,
Usage: "Print found problems using TeamCity Service Messages format.",
},
&cli.StringFlag{
Name: checkStyleFlag,
Aliases: []string{"c"},
Value: "",
Usage: "Create a checkstyle xml formatted report of all problems to this path.",
},
},
}

Expand Down Expand Up @@ -118,7 +125,19 @@ func actionCI(c *cli.Context) error {
}

reps := []reporter.Reporter{}

if c.String(checkStyleFlag) != "" {
f, fileErr := os.Create(c.String(checkStyleFlag))
if fileErr != nil {
return fileErr
}
// execute here so we can close the file right after
errRep := reporter.NewCheckStyleReporter(f).Submit(summary)
slog.Error("Error encountered", "error:", errRep)
cerr := f.Close()
if cerr != nil {
return cerr
}
}
if c.Bool(teamCityFlag) {
reps = append(reps, reporter.NewTeamCityReporter(os.Stderr))
} else {
Expand Down
34 changes: 28 additions & 6 deletions cmd/pint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ var lintCmd = &cli.Command{
Value: false,
Usage: "Report problems using TeamCity Service Messages.",
},
&cli.StringFlag{
Name: checkStyleFlag,
Aliases: []string{"c"},
Value: "",
Usage: "Create a checkstyle xml formatted report of all problems to this path.",
},
},
}

Expand Down Expand Up @@ -100,16 +106,32 @@ func actionLint(c *cli.Context) error {
return fmt.Errorf("invalid --%s value: %w", failOnFlag, err)
}

var r reporter.Reporter
reps := []reporter.Reporter{}
if c.Bool(teamCityFlag) {
r = reporter.NewTeamCityReporter(os.Stderr)
reps = append(reps, reporter.NewTeamCityReporter(os.Stderr))
} else {
r = reporter.NewConsoleReporter(os.Stderr, minSeverity)
reps = append(reps, reporter.NewConsoleReporter(os.Stderr, minSeverity))
}

err = r.Submit(summary)
if err != nil {
return err
if c.String(checkStyleFlag) != "" {
f, fileErr := os.Create(c.String(checkStyleFlag))
if fileErr != nil {
return fileErr
}
// execute here so we can close the file right after
errRep := reporter.NewCheckStyleReporter(f).Submit(summary)
slog.Error("Error encountered", "error:", errRep)
cerr := f.Close()
if cerr != nil {
return cerr
}
}

for _, rep := range reps {
err = rep.Submit(summary)
if err != nil {
return fmt.Errorf("submitting reports: %w", err)
}
}

bySeverity := summary.CountBySeverity()
Expand Down
20 changes: 20 additions & 0 deletions cmd/pint/tests/0194_lint_checkstyle.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
env NO_COLOR=1
! exec pint --no-color lint --min-severity=info --checkstyle=checkstyle.xml rules
cmp checkstyle.xml checkstyle_check.xml

-- rules/0001.yml --
groups:
- name: test
rules:
- alert: Example
expr: up
- alert: Example
expr: sum(xxx) with()
-- checkstyle_check.xml --
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="rules/0001.yml">
<error line="5" severity="Warning" message="Text:Alert query doesn&#39;t have any condition, it will always fire if the metric exists.&#xA; Details:Prometheus alerting rules will trigger an alert for each query that returns *any* result.&#xA;Unless you do want an alert to always fire you should write your query in a way that returns results only when some condition is met.&#xA;In most cases this can be achieved by having some condition in the query expression.&#xA;For example `up == 0` or `rate(error_total[2m]) &gt; 0`.&#xA;Be careful as some PromQL operations will cause the query to always return the results, for example using the [bool modifier](https://prometheus.io/docs/prometheus/latest/querying/operators/#comparison-binary-operators)." source="alerts/comparison"></error>
<error line="7" severity="Fatal" message="Text:Prometheus failed to parse the query with this PromQL error: unexpected identifier &#34;with&#34;.&#xA; Details:[Click here](https://prometheus.io/docs/prometheus/latest/querying/basics/) for PromQL documentation." source="promql/syntax"></error>
</file>
</checkstyle>
110 changes: 110 additions & 0 deletions internal/reporter/checkstyle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package reporter

import (
"encoding/xml"
"fmt"
"io"
"strconv"
)

func NewCheckStyleReporter(output io.Writer) CheckStyleReporter {
return CheckStyleReporter{
output: output,
}
}

type CheckStyleReporter struct {
output io.Writer
}

type checkstyleReport map[string][]Report

func createCheckstyleReport(summary Summary) checkstyleReport {
x := make(checkstyleReport)
for _, report := range summary.reports {
x[report.Path.Name] = append(x[report.Path.Name], report)
}
return x
}

func (d checkstyleReport) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
err := e.EncodeToken(xml.StartElement{
Name: xml.Name{Local: "checkstyle"},
Attr: []xml.Attr{
{
Name: xml.Name{Local: "version"},
Value: "4.3",
},
},
})
if err != nil {
return err
}
for dir, reports := range d {
errEnc := e.EncodeToken(xml.StartElement{
Name: xml.Name{Local: "file"},
Attr: []xml.Attr{
{
Name: xml.Name{Local: "name"},
Value: dir,
},
},
})
if errEnc != nil {
return errEnc
}
for _, report := range reports {
errEnc2 := e.Encode(report)
if errEnc2 != nil {
return errEnc2
}
}
errEnc = e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "file"}})
if errEnc != nil {
return errEnc
}
}
err = e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "checkstyle"}})
return err
}

func (r Report) MarshalXML(e *xml.Encoder, _ xml.StartElement) error {
startel := xml.StartElement{
Name: xml.Name{Local: "error"},
Attr: []xml.Attr{
{
Name: xml.Name{Local: "line"},
Value: strconv.Itoa(r.Problem.Lines.First),
},
{
Name: xml.Name{Local: "severity"},
Value: r.Problem.Severity.String(),
},
{
Name: xml.Name{Local: "message"},
Value: fmt.Sprintf("Text:%s\n Details:%s", r.Problem.Text, r.Problem.Details),
},
{
Name: xml.Name{Local: "source"},
Value: r.Problem.Reporter,
},
},
}
err := e.EncodeToken(startel)
if err != nil {
return err
}
err = e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "error"}})

return err
}

func (cs CheckStyleReporter) Submit(summary Summary) error {
checkstyleReport := createCheckstyleReport(summary)
xmlString, err := xml.MarshalIndent(checkstyleReport, "", " ")
if err != nil {
fmt.Printf("%v", err)
}
fmt.Fprint(cs.output, string(xml.Header)+string(xmlString)+"\n")
return nil
}
148 changes: 148 additions & 0 deletions internal/reporter/checkstyle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package reporter_test

import (
"bytes"
"log/slog"
"testing"

"github.com/neilotoole/slogt"
"github.com/stretchr/testify/require"

"github.com/cloudflare/pint/internal/checks"
"github.com/cloudflare/pint/internal/discovery"
"github.com/cloudflare/pint/internal/parser"
"github.com/cloudflare/pint/internal/reporter"
)

func TestCheckstyleReporter(t *testing.T) {
type testCaseT struct {
description string
output string
err string
summary reporter.Summary
}

p := parser.NewParser(false)
mockRules, _ := p.Parse([]byte(`
- record: target is down
expr: up == 0
`))

testCases := []testCaseT{
{
description: "no reports",
summary: reporter.Summary{},
output: `<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3"></checkstyle>
`,
},
{
description: "info report",
summary: reporter.NewSummary([]reporter.Report{
{
Path: discovery.Path{
SymlinkTarget: "foo.txt",
Name: "foo.txt",
},
ModifiedLines: []int{2, 4, 5},
Rule: mockRules[0],
Problem: checks.Problem{
Lines: parser.LineRange{
First: 5,
Last: 6,
},
Reporter: "mock",
Text: "mock text",
Details: "mock details",
Severity: checks.Information,
},
},
}),
output: `<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="foo.txt">
<error line="5" severity="Information" message="Text:mock text&#xA; Details:mock details" source="mock"></error>
</file>
</checkstyle>
`,
},
{
description: "bug report",
summary: reporter.NewSummary([]reporter.Report{
{
Path: discovery.Path{
SymlinkTarget: "foo.txt",
Name: "foo.txt",
},
ModifiedLines: []int{2, 4, 5},
Rule: mockRules[0],
Problem: checks.Problem{
Lines: parser.LineRange{
First: 5,
Last: 6,
},
Reporter: "mock",
Text: "mock text",
Severity: checks.Bug,
},
},
}),
output: `<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="foo.txt">
<error line="5" severity="Bug" message="Text:mock text&#xA; Details:" source="mock"></error>
</file>
</checkstyle>
`,
},
{
description: "escaping characters",
summary: reporter.NewSummary([]reporter.Report{
{
Path: discovery.Path{
SymlinkTarget: "foo.txt",
Name: "foo.txt",
},
ModifiedLines: []int{2, 4, 5},
Rule: mockRules[0],
Problem: checks.Problem{
Lines: parser.LineRange{
First: 5,
Last: 6,
},
Reporter: "mock",
Text: `mock text
with [new lines] and pipe| chars that are 'quoted'
`,
Severity: checks.Bug,
},
},
}),
output: `<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="foo.txt">
<error line="5" severity="Bug" message="Text:mock text&#xA;&#x9;&#x9;with [new lines] and pipe| chars that are &#39;quoted&#39;&#xA;&#x9;&#x9;&#xA; Details:" source="mock"></error>
</file>
</checkstyle>
`,
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
slog.SetDefault(slogt.New(t))

out := bytes.NewBuffer(nil)

reporter := reporter.NewCheckStyleReporter(out)
err := reporter.Submit(tc.summary)

if tc.err != "" {
require.EqualError(t, err, tc.err)
} else {
require.NoError(t, err)
require.Equal(t, tc.output, out.String())
}
})
}
}

0 comments on commit f01f208

Please sign in to comment.