diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 31a4e791..271fffe9 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -432,6 +432,8 @@ func (e *Executor) createPrinter() (printers.Printer, error) { p = printers.NewCheckstyle() case config.OutFormatCodeClimate: p = printers.NewCodeClimate() + case config.OutFormatHTML: + p = printers.NewHTML() case config.OutFormatJunitXML: p = printers.NewJunitXML() case config.OutFormatGithubActions: diff --git a/pkg/config/output.go b/pkg/config/output.go index c95d58fb..d67f110f 100644 --- a/pkg/config/output.go +++ b/pkg/config/output.go @@ -7,6 +7,7 @@ const ( OutFormatTab = "tab" OutFormatCheckstyle = "checkstyle" OutFormatCodeClimate = "code-climate" + OutFormatHTML = "html" OutFormatJunitXML = "junit-xml" OutFormatGithubActions = "github-actions" ) @@ -18,6 +19,7 @@ var OutFormats = []string{ OutFormatTab, OutFormatCheckstyle, OutFormatCodeClimate, + OutFormatHTML, OutFormatJunitXML, OutFormatGithubActions, } diff --git a/pkg/printers/html.go b/pkg/printers/html.go new file mode 100644 index 00000000..65ab753b --- /dev/null +++ b/pkg/printers/html.go @@ -0,0 +1,155 @@ +package printers + +import ( + "context" + "fmt" + "html/template" + "strings" + + "github.com/golangci/golangci-lint/pkg/logutils" + "github.com/golangci/golangci-lint/pkg/result" +) + +const templateContent = `<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>golangci-lint</title> + <link rel="shortcut icon" type="image/png" href="https://golangci-lint.run/favicon-32x32.png"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.2/css/bulma.min.css" + integrity="sha512-byErQdWdTqREz6DLAA9pCnLbdoGGhXfU6gm1c8bkf7F51JVmUBlayGe2A31VpXWQP+eiJ3ilTAZHCR3vmMyybA==" + crossorigin="anonymous" referrerpolicy="no-referrer"/> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/styles/default.min.css" + integrity="sha512-kZqGbhf9JTB4bVJ0G8HCkqmaPcRgo88F0dneK30yku5Y/dep7CZfCnNml2Je/sY4lBoqoksXz4PtVXS4GHSUzQ==" + crossorigin="anonymous" referrerpolicy="no-referrer"/> + <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/highlight.min.js" + integrity="sha512-s+tOYYcC3Jybgr9mVsdAxsRYlGNq4mlAurOrfNuGMQ/SCofNPu92tjE7YRZCsdEtWL1yGkqk15fU/ark206YTg==" + crossorigin="anonymous" referrerpolicy="no-referrer"></script> + <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/languages/go.min.js" + integrity="sha512-+UYV2NyyynWEQcZ4sMTKmeppyV331gqvMOGZ61/dqc89Tn1H40lF05ACd03RSD9EWwGutNwKj256mIR8waEJBQ==" + crossorigin="anonymous" referrerpolicy="no-referrer"></script> + <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js" + integrity="sha512-qlzIeUtTg7eBpmEaS12NZgxz52YYZVF5myj89mjJEesBd/oE9UPsYOX2QAXzvOAZYEvQohKdcY8zKE02ifXDmA==" + crossorigin="anonymous" referrerpolicy="no-referrer"></script> + <script type="text/javascript" + src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js" + integrity="sha512-9jGNr5Piwe8nzLLYTk8QrEMPfjGU0px80GYzKZUxi7lmCfrBjtyCc1V5kkS5vxVwwIB7Qpzc7UxLiQxfAN30dw==" + crossorigin="anonymous" referrerpolicy="no-referrer"></script> + <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js" + integrity="sha512-kp7YHLxuJDJcOzStgd6vtpxr4ZU9kjn77e6dBsivSz+pUuAuMlE2UTdKB7jjsWT84qbS8kdCWHPETnP/ctrFsA==" + crossorigin="anonymous" referrerpolicy="no-referrer"></script> +</head> +<body> +<section class="section"> + <div class="container"> + <div id="content"></div> + </div> +</section> +<script> + const data = {{ . }}; +</script> +<script type="text/babel"> + class Highlight extends React.Component { + componentDidMount() { + hljs.highlightElement(ReactDOM.findDOMNode(this)); + } + + render() { + return <pre className="go"><code>{this.props.code}</code></pre>; + } + } + + class Issue extends React.Component { + render() { + return ( + <div className="issue box"> + <div> + <div className="columns"> + <div className="column is-four-fifths"> + <h5 className="title is-5 has-text-danger-dark">{this.props.data.Title}</h5> + </div> + <div className="column is-one-fifth"> + <h6 className="title is-6">{this.props.data.Linter}</h6> + </div> + </div> + <strong>{this.props.data.Pos}</strong> + </div> + <div className="highlight"> + <Highlight code={this.props.data.Code}/> + </div> + </div> + ); + } + } + + class Issues extends React.Component { + render() { + if (!this.props.data.Issues || this.props.data.Issues.length === 0) { + return ( + <div> + <div className="notification"> + No issues found! + </div> + </div> + ); + } + + return ( + <div className="issues"> + {this.props.data.Issues.map(issue => (<Issue data={issue}/>))} + </div> + ); + } + } + + ReactDOM.render( + <div className="content"> + <div className="columns is-centered"> + <div className="column is-three-quarters"> + <Issues data={data}/> + </div> + </div> + </div>, + document.getElementById("content") + ); +</script> +</body> +</html>` + +type htmlIssue struct { + Title string + Pos string + Linter string + Code string +} + +type HTML struct{} + +func NewHTML() *HTML { + return &HTML{} +} + +func (h HTML) Print(_ context.Context, issues []result.Issue) error { + var htmlIssues []htmlIssue + + for i := range issues { + pos := fmt.Sprintf("%s:%d", issues[i].FilePath(), issues[i].Line()) + if issues[i].Pos.Column != 0 { + pos += fmt.Sprintf(":%d", issues[i].Pos.Column) + } + + htmlIssues = append(htmlIssues, htmlIssue{ + Title: strings.TrimSpace(issues[i].Text), + Pos: pos, + Linter: issues[i].FromLinter, + Code: strings.Join(issues[i].SourceLines, "\n"), + }) + } + + t, err := template.New("golangci-lint").Parse(templateContent) + if err != nil { + return err + } + + return t.Execute(logutils.StdOut, struct{ Issues []htmlIssue }{Issues: htmlIssues}) +}