Skip to content

Checkstyle reporter #1129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here
Close() might error out so we should check for that

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())
}
})
}
}