From bc52196cb392226de5626df8648c5a707a3d4604 Mon Sep 17 00:00:00 2001 From: marschattha Date: Tue, 19 Nov 2024 00:33:23 +0500 Subject: [PATCH] Add golangci lint suggestions --- .../fixtures/__snapshots__/basic_v1.59.1.shot | 38 +- .../fixtures/__snapshots__/basic_v1.61.0.shot | 38 +- .../__snapshots__/unused_func_v1.59.1.shot | 4 +- .../__snapshots__/unused_func_v1.61.0.shot | 4 +- plugins/linters/golangci-lint/plugin.toml | 4 +- qlty-check/src/executor/driver.rs | 2 + qlty-check/src/parser.rs | 1 + qlty-check/src/parser/golangci_lint.rs | 377 ++++++++++++++++++ qlty-check/src/patch_builder.rs | 74 +++- .../planner/patch_builder/parse.04.input.go | 13 + .../planner/patch_builder/parse.04.output.txt | 1 + qlty-config/src/config/plugin.rs | 3 + 12 files changed, 537 insertions(+), 22 deletions(-) create mode 100644 qlty-check/src/parser/golangci_lint.rs create mode 100644 qlty-check/tests/fixtures/planner/patch_builder/parse.04.input.go create mode 100644 qlty-check/tests/fixtures/planner/patch_builder/parse.04.output.txt diff --git a/plugins/linters/golangci-lint/fixtures/__snapshots__/basic_v1.59.1.shot b/plugins/linters/golangci-lint/fixtures/__snapshots__/basic_v1.59.1.shot index c1531dec1..1f9dfaed5 100644 --- a/plugins/linters/golangci-lint/fixtures/__snapshots__/basic_v1.59.1.shot +++ b/plugins/linters/golangci-lint/fixtures/__snapshots__/basic_v1.59.1.shot @@ -5,12 +5,10 @@ exports[`linter=golangci-lint fixture=basic version=1.59.1 1`] = ` "issues": [ { "category": "CATEGORY_LINT", - "level": "LEVEL_HIGH", + "level": "LEVEL_MEDIUM", "location": { "path": "basic.in.go", "range": { - "endColumn": 12, - "endLine": 8, "startColumn": 12, "startLine": 8, }, @@ -32,12 +30,10 @@ func main() { }, { "category": "CATEGORY_LINT", - "level": "LEVEL_HIGH", + "level": "LEVEL_MEDIUM", "location": { "path": "basic.in.go", "range": { - "endColumn": 34, - "endLine": 6, "startColumn": 34, "startLine": 6, }, @@ -55,6 +51,36 @@ import "time" func main() { time.Parse("asdf", "") }", + "suggestions": [ + { + "patch": "--- original ++++ modified +@@ -3,7 +3,7 @@ + import "time" + + // ✋✋✋✋ +-// this is the main function 🏃 ++// this is the main function 🏃. + func main() { + time.Parse("asdf", "") + } +", + "replacements": [ + { + "data": "// this is the main function 🏃.", + "location": { + "path": "basic.in.go", + "range": { + "endColumn": 34, + "endLine": 6, + "startLine": 6, + }, + }, + }, + ], + "source": "SUGGESTION_SOURCE_TOOL", + }, + ], "tool": "golangci-lint", }, ], diff --git a/plugins/linters/golangci-lint/fixtures/__snapshots__/basic_v1.61.0.shot b/plugins/linters/golangci-lint/fixtures/__snapshots__/basic_v1.61.0.shot index ea75062cf..bef3c45c7 100644 --- a/plugins/linters/golangci-lint/fixtures/__snapshots__/basic_v1.61.0.shot +++ b/plugins/linters/golangci-lint/fixtures/__snapshots__/basic_v1.61.0.shot @@ -5,12 +5,10 @@ exports[`linter=golangci-lint fixture=basic version=1.61.0 1`] = ` "issues": [ { "category": "CATEGORY_LINT", - "level": "LEVEL_HIGH", + "level": "LEVEL_MEDIUM", "location": { "path": "basic.in.go", "range": { - "endColumn": 12, - "endLine": 8, "startColumn": 12, "startLine": 8, }, @@ -32,12 +30,10 @@ func main() { }, { "category": "CATEGORY_LINT", - "level": "LEVEL_HIGH", + "level": "LEVEL_MEDIUM", "location": { "path": "basic.in.go", "range": { - "endColumn": 34, - "endLine": 6, "startColumn": 34, "startLine": 6, }, @@ -55,6 +51,36 @@ import "time" func main() { time.Parse("asdf", "") }", + "suggestions": [ + { + "patch": "--- original ++++ modified +@@ -3,7 +3,7 @@ + import "time" + + // ✋✋✋✋ +-// this is the main function 🏃 ++// this is the main function 🏃. + func main() { + time.Parse("asdf", "") + } +", + "replacements": [ + { + "data": "// this is the main function 🏃.", + "location": { + "path": "basic.in.go", + "range": { + "endColumn": 34, + "endLine": 6, + "startLine": 6, + }, + }, + }, + ], + "source": "SUGGESTION_SOURCE_TOOL", + }, + ], "tool": "golangci-lint", }, ], diff --git a/plugins/linters/golangci-lint/fixtures/__snapshots__/unused_func_v1.59.1.shot b/plugins/linters/golangci-lint/fixtures/__snapshots__/unused_func_v1.59.1.shot index c15bd17c4..41558cbf3 100644 --- a/plugins/linters/golangci-lint/fixtures/__snapshots__/unused_func_v1.59.1.shot +++ b/plugins/linters/golangci-lint/fixtures/__snapshots__/unused_func_v1.59.1.shot @@ -5,12 +5,10 @@ exports[`linter=golangci-lint fixture=unused_func version=1.59.1 1`] = ` "issues": [ { "category": "CATEGORY_LINT", - "level": "LEVEL_HIGH", + "level": "LEVEL_MEDIUM", "location": { "path": "unused_func.in.go", "range": { - "endColumn": 6, - "endLine": 5, "startColumn": 6, "startLine": 5, }, diff --git a/plugins/linters/golangci-lint/fixtures/__snapshots__/unused_func_v1.61.0.shot b/plugins/linters/golangci-lint/fixtures/__snapshots__/unused_func_v1.61.0.shot index a7a3f591c..4d12dab94 100644 --- a/plugins/linters/golangci-lint/fixtures/__snapshots__/unused_func_v1.61.0.shot +++ b/plugins/linters/golangci-lint/fixtures/__snapshots__/unused_func_v1.61.0.shot @@ -5,12 +5,10 @@ exports[`linter=golangci-lint fixture=unused_func version=1.61.0 1`] = ` "issues": [ { "category": "CATEGORY_LINT", - "level": "LEVEL_HIGH", + "level": "LEVEL_MEDIUM", "location": { "path": "unused_func.in.go", "range": { - "endColumn": 6, - "endLine": 5, "startColumn": 6, "startLine": 5, }, diff --git a/plugins/linters/golangci-lint/plugin.toml b/plugins/linters/golangci-lint/plugin.toml index 5172becb4..9e1ab81c0 100644 --- a/plugins/linters/golangci-lint/plugin.toml +++ b/plugins/linters/golangci-lint/plugin.toml @@ -23,12 +23,12 @@ security = true suggested = "targets" [plugins.definitions.golangci-lint.drivers.lint] -script = "golangci-lint run --out-format sarif --timeout 10m --exclude gofmt --allow-parallel-runners --issues-exit-code 0 ${target}" +script = "golangci-lint run --out-format json --timeout 10m --exclude gofmt --allow-parallel-runners --issues-exit-code 0 ${target}" target = { type = "literal", path = "./..." } runs_from = { type = "root_or_parent_with", path = "go.mod" } success_codes = [0, 2, 7] output = "stdout" -output_format = "sarif" +output_format = "golangci_lint" suggested = "targets" [[plugins.definitions.golangci-lint.environment]] diff --git a/qlty-check/src/executor/driver.rs b/qlty-check/src/executor/driver.rs index b5986820a..15f37f2a2 100644 --- a/qlty-check/src/executor/driver.rs +++ b/qlty-check/src/executor/driver.rs @@ -5,6 +5,7 @@ use crate::parser::bandit::Bandit; use crate::parser::clippy::Clippy; use crate::parser::coffeelint::Coffeelint; use crate::parser::eslint::Eslint; +use crate::parser::golangci_lint::GolangciLint; use crate::parser::hadolint::Hadolint; use crate::parser::knip::Knip; use crate::parser::markdownlint::Markdownlint; @@ -428,6 +429,7 @@ impl Driver { OutputFormat::Radarlint => Box::new(Radarlint {}), OutputFormat::Coffeelint => Box::new(Coffeelint {}), OutputFormat::Ruff => Box::new(Ruff {}), + OutputFormat::GolangciLint => Box::new(GolangciLint {}), OutputFormat::Sarif => { let level = match self.output_level { diff --git a/qlty-check/src/parser.rs b/qlty-check/src/parser.rs index c5b1d83f1..f8fb5029f 100644 --- a/qlty-check/src/parser.rs +++ b/qlty-check/src/parser.rs @@ -26,6 +26,7 @@ pub mod taplo; pub mod trivy_sarif; pub mod trufflehog; pub mod tsc; +pub mod golangci_lint; pub trait Parser { fn parse(&self, plugin_name: &str, output: &str) -> Result>; diff --git a/qlty-check/src/parser/golangci_lint.rs b/qlty-check/src/parser/golangci_lint.rs new file mode 100644 index 000000000..6a866efae --- /dev/null +++ b/qlty-check/src/parser/golangci_lint.rs @@ -0,0 +1,377 @@ +use super::Parser; +use anyhow::Result; +use qlty_types::analysis::v1::{ + Category, Issue, Level, Location, Range, Replacement, Suggestion, SuggestionSource, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Clone)] +struct GolangciLintOutput { + #[serde(rename = "Issues")] + issues: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +struct GolangciLintIssue { + #[serde(rename = "FromLinter")] + from_linter: String, + #[serde(rename = "Text")] + text: String, + #[serde(rename = "Severity")] + severity: String, + #[serde(rename = "Replacement")] + replacement: Option, + #[serde(rename = "Pos")] + pos: Pos, + #[serde(rename = "LineRange")] + line_range: Option, + #[serde(rename = "SourceLines")] + source_lines: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +struct GolangciLintReplacement { + #[serde(rename = "NewLines")] + new_lines: Vec, +} + +#[derive(Debug, Deserialize, Clone)] +struct Pos { + #[serde(rename = "Filename")] + filename: String, + #[serde(rename = "Line")] + line: u32, + #[serde(rename = "Column")] + column: u32, +} + +#[derive(Debug, Deserialize, Clone)] +struct LineRange { + #[serde(rename = "From")] + from: u32, + #[serde(rename = "To")] + to: u32, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct GolangciLint {} + +impl Parser for GolangciLint { + fn parse(&self, plugin_name: &str, output: &str) -> Result> { + let mut issues = vec![]; + let golangcilint_output: GolangciLintOutput = serde_json::from_str(output)?; + + for golangcilint_issue in golangcilint_output.issues { + let suggestions = build_suggestions(&golangcilint_issue); + + let issue = Issue { + tool: plugin_name.into(), + message: golangcilint_issue.text, + category: Category::Lint.into(), + level: level_to_issue_level(golangcilint_issue.severity).into(), + rule_key: golangcilint_issue.from_linter, + location: Some(Location { + path: golangcilint_issue.pos.filename.clone(), + range: Some(Range { + start_line: golangcilint_issue.pos.line, + start_column: golangcilint_issue.pos.column, + ..Default::default() + }), + }), + suggestions, + ..Default::default() + }; + + issues.push(issue); + } + + Ok(issues) + } +} + +fn level_to_issue_level(severity: String) -> Level { + match severity.as_str() { + "ignore" => Level::Low, + "warn" => Level::Medium, + "error" => Level::High, + _ => Level::Medium, + } +} + +fn build_suggestions( + golangcilint_issue: &GolangciLintIssue, +) -> Vec { + let mut suggestions = vec![]; + + if let Some(replacement) = &golangcilint_issue.replacement { + let replacement_text = replacement.new_lines.join("\n"); + + let (start_line, end_line) = if let Some(line_range) = &golangcilint_issue.line_range { + (line_range.from, line_range.to) + } else { + (golangcilint_issue.pos.line, golangcilint_issue.pos.line) + }; + + suggestions.push(Suggestion { + source: SuggestionSource::Tool.into(), + replacements: vec![Replacement { + location: Some(Location { + path: golangcilint_issue.pos.filename.clone(), + range: Some(Range { + start_line, + end_line, + end_column: golangcilint_issue + .source_lines + .last() + .map_or(0, |l| (l.len() + 1) as u32), + ..Default::default() + }), + }), + data: replacement_text.clone(), + }], + ..Default::default() + }); + } + + suggestions +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse() { + let input = r###" + { + "Issues": [ + { + "FromLinter": "errcheck", + "Text": "Error return value of `time.Parse` is not checked", + "Severity": "", + "SourceLines": ["\ttime.Parse(\"asdf\", \"\")"], + "Replacement": null, + "Pos": { + "Filename": "basic.in.go", + "Offset": 217, + "Line": 12, + "Column": 12 + }, + "ExpectNoLint": false, + "ExpectedNoLintLinter": "" + }, + { + "FromLinter": "godot", + "Text": "Comment should end in a period", + "Severity": "", + "SourceLines": ["// this is the main function 🏃"], + "Replacement": { + "NeedOnlyDelete": false, + "NewLines": ["// this is the main function 🏃."], + "Inline": null + }, + "Pos": { + "Filename": "basic.in.go", + "Offset": 42, + "Line": 7, + "Column": 34 + }, + "ExpectNoLint": false, + "ExpectedNoLintLinter": "" + }, + { + "FromLinter": "goimports", + "Text": "File is not `goimports`-ed", + "Severity": "", + "SourceLines": ["import \"time\"", "import \"fmt\""], + "Replacement": { + "NeedOnlyDelete": false, + "NewLines": ["import (", "\t\"fmt\"", "\t\"time\"", ")"], + "Inline": null + }, + "LineRange": { "From": 3, "To": 4 }, + "Pos": { "Filename": "basic.in.go", "Offset": 0, "Line": 3, "Column": 0 }, + "ExpectNoLint": false, + "ExpectedNoLintLinter": "" + } + ], + "Report": { + "Linters": [ + { "Name": "asasalint" }, + { "Name": "asciicheck", "Enabled": true }, + { "Name": "bidichk" }, + { "Name": "bodyclose", "Enabled": true }, + { "Name": "canonicalheader" }, + { "Name": "containedctx" }, + { "Name": "contextcheck" }, + { "Name": "copyloopvar" }, + { "Name": "cyclop" }, + { "Name": "decorder" }, + { "Name": "deadcode" }, + { "Name": "depguard", "Enabled": true }, + { "Name": "dogsled", "Enabled": true }, + { "Name": "dupl" }, + { "Name": "dupword" }, + { "Name": "durationcheck" }, + { "Name": "errcheck", "Enabled": true, "EnabledByDefault": true }, + { "Name": "errchkjson" }, + { "Name": "errname" }, + { "Name": "errorlint" }, + { "Name": "execinquery" }, + { "Name": "exhaustive" }, + { "Name": "exhaustivestruct" }, + { "Name": "exhaustruct" }, + { "Name": "exportloopref", "Enabled": true }, + { "Name": "forbidigo" }, + { "Name": "forcetypeassert" }, + { "Name": "fatcontext" }, + { "Name": "funlen" }, + { "Name": "gci" }, + { "Name": "ginkgolinter" }, + { "Name": "gocheckcompilerdirectives" }, + { "Name": "gochecknoglobals" }, + { "Name": "gochecknoinits", "Enabled": true }, + { "Name": "gochecksumtype" }, + { "Name": "gocognit" }, + { "Name": "goconst" }, + { "Name": "gocritic" }, + { "Name": "gocyclo" }, + { "Name": "godot", "Enabled": true }, + { "Name": "godox" }, + { "Name": "err113" }, + { "Name": "gofmt", "Enabled": true }, + { "Name": "gofumpt" }, + { "Name": "goheader", "Enabled": true }, + { "Name": "goimports", "Enabled": true }, + { "Name": "golint" }, + { "Name": "mnd" }, + { "Name": "gomnd" }, + { "Name": "gomoddirectives" }, + { "Name": "gomodguard" }, + { "Name": "goprintffuncname", "Enabled": true }, + { "Name": "gosec", "Enabled": true }, + { "Name": "gosimple", "Enabled": true, "EnabledByDefault": true }, + { "Name": "gosmopolitan" }, + { "Name": "govet", "Enabled": true, "EnabledByDefault": true }, + { "Name": "grouper" }, + { "Name": "ifshort" }, + { "Name": "importas" }, + { "Name": "inamedparam" }, + { "Name": "ineffassign", "Enabled": true, "EnabledByDefault": true }, + { "Name": "interfacebloat" }, + { "Name": "interfacer" }, + { "Name": "intrange" }, + { "Name": "ireturn" }, + { "Name": "lll" }, + { "Name": "loggercheck" }, + { "Name": "maintidx" }, + { "Name": "makezero" }, + { "Name": "maligned" }, + { "Name": "mirror" }, + { "Name": "misspell", "Enabled": true }, + { "Name": "musttag" }, + { "Name": "nakedret", "Enabled": true }, + { "Name": "nestif" }, + { "Name": "nilerr" }, + { "Name": "nilnil" }, + { "Name": "nlreturn" }, + { "Name": "noctx" }, + { "Name": "nonamedreturns" }, + { "Name": "nosnakecase" }, + { "Name": "nosprintfhostport" }, + { "Name": "paralleltest" }, + { "Name": "perfsprint" }, + { "Name": "prealloc" }, + { "Name": "predeclared" }, + { "Name": "promlinter" }, + { "Name": "protogetter" }, + { "Name": "reassign" }, + { "Name": "revive" }, + { "Name": "rowserrcheck", "Enabled": true }, + { "Name": "sloglint" }, + { "Name": "scopelint" }, + { "Name": "sqlclosecheck" }, + { "Name": "spancheck" }, + { "Name": "staticcheck", "Enabled": true, "EnabledByDefault": true }, + { "Name": "structcheck" }, + { "Name": "stylecheck", "Enabled": true }, + { "Name": "tagalign" }, + { "Name": "tagliatelle" }, + { "Name": "tenv" }, + { "Name": "testableexamples" }, + { "Name": "testifylint" }, + { "Name": "testpackage" }, + { "Name": "thelper" }, + { "Name": "tparallel" }, + { "Name": "typecheck", "Enabled": true, "EnabledByDefault": true }, + { "Name": "unconvert", "Enabled": true }, + { "Name": "unparam" }, + { "Name": "unused", "Enabled": true, "EnabledByDefault": true }, + { "Name": "usestdlibvars" }, + { "Name": "varcheck" }, + { "Name": "varnamelen" }, + { "Name": "wastedassign" }, + { "Name": "whitespace", "Enabled": true }, + { "Name": "wrapcheck" }, + { "Name": "wsl" }, + { "Name": "zerologlint" }, + { "Name": "nolintlint", "Enabled": true } + ] + } + } + "###; + + let issues = GolangciLint::default().parse("golangcilint", input); + insta::assert_yaml_snapshot!(issues.unwrap(), @r###" + - tool: golangcilint + ruleKey: errcheck + message: "Error return value of `time.Parse` is not checked" + level: LEVEL_MEDIUM + category: CATEGORY_LINT + location: + path: basic.in.go + range: + startLine: 12 + startColumn: 12 + - tool: golangcilint + ruleKey: godot + message: Comment should end in a period + level: LEVEL_MEDIUM + category: CATEGORY_LINT + location: + path: basic.in.go + range: + startLine: 7 + startColumn: 34 + suggestions: + - source: SUGGESTION_SOURCE_TOOL + replacements: + - data: // this is the main function 🏃. + location: + path: basic.in.go + range: + startLine: 7 + endLine: 7 + endColumn: 34 + - tool: golangcilint + ruleKey: goimports + message: "File is not `goimports`-ed" + level: LEVEL_MEDIUM + category: CATEGORY_LINT + location: + path: basic.in.go + range: + startLine: 3 + suggestions: + - source: SUGGESTION_SOURCE_TOOL + replacements: + - data: "import (\n\t\"fmt\"\n\t\"time\"\n)" + location: + path: basic.in.go + range: + startLine: 3 + endLine: 4 + endColumn: 13 + "###); + } +} diff --git a/qlty-check/src/patch_builder.rs b/qlty-check/src/patch_builder.rs index 2ac6f97a6..9d881d8a4 100644 --- a/qlty-check/src/patch_builder.rs +++ b/qlty-check/src/patch_builder.rs @@ -122,7 +122,12 @@ fn calculate_byte_offset(content: &str, line: usize, column: usize) -> Option 0 && line <= lines.len() { let line_str = lines[line - 1]; // Convert 1-based line to 0-based index - let index = column - 1; // Convert 1-based column to 0-based index + let index = if column > 0 { + column - 1 // Convert 1-based column to 0-based index + } else { + 0 // If column is 0/missing, use the start of the line + }; + let byte_offset = if let Some((byte_index, _)) = line_str.char_indices().nth(index) { // Calculate the absolute byte offset from the start of the content content @@ -180,7 +185,7 @@ fn replace_in_range( mod test { use super::*; use crate::{ - parser::{clippy::Clippy, shellcheck::Shellcheck, Parser}, + parser::{clippy::Clippy, golangci_lint::GolangciLint, shellcheck::Shellcheck, Parser}, source_reader::SourceReaderFs, }; use std::path::PathBuf; @@ -540,4 +545,69 @@ mod test { endColumn: 33 "#) } + + #[test] + fn parse_line_replacements_golangci() { + let input = include_str!("../tests/fixtures/planner/patch_builder/parse.04.output.txt"); + let patch_builder = new_from_cache([( + "/tmp/src/main.go".into(), + include_str!("../tests/fixtures/planner/patch_builder/parse.04.input.go").into(), + )]); + + let issues = GolangciLint {}.parse("golangci-lint", input).ok().unwrap(); + let issues = transformed_issues(issues, &patch_builder); + insta::assert_yaml_snapshot!(issues, @r###" + - tool: golangci-lint + ruleKey: errcheck + message: "Error return value of `time.Parse` is not checked" + level: LEVEL_MEDIUM + category: CATEGORY_LINT + location: + path: /tmp/src/main.go + range: + startLine: 12 + startColumn: 12 + - tool: golangci-lint + ruleKey: godot + message: Comment should end in a period + level: LEVEL_MEDIUM + category: CATEGORY_LINT + location: + path: /tmp/src/main.go + range: + startLine: 7 + startColumn: 34 + suggestions: + - patch: "--- original\n+++ modified\n@@ -4,7 +4,7 @@\n import \"fmt\"\n\n // ✋✋✋✋\n-// this is the main function 🏃\n+// this is the main function 🏃.\n func main() {\n \t// This is a comment\n \tfmt.Println(\"Heloo World!\") // Intentional typo: \"Heloo\" instead of \"Hello\"\n" + source: SUGGESTION_SOURCE_TOOL + replacements: + - data: // this is the main function 🏃. + location: + path: /tmp/src/main.go + range: + startLine: 7 + endLine: 7 + endColumn: 34 + - tool: golangci-lint + ruleKey: goimports + message: "File is not `goimports`-ed" + level: LEVEL_MEDIUM + category: CATEGORY_LINT + location: + path: /tmp/src/main.go + range: + startLine: 3 + suggestions: + - patch: "--- original\n+++ modified\n@@ -1,7 +1,9 @@\n package main\n\n-import \"time\"\n-import \"fmt\"\n+import (\n+\t\"fmt\"\n+\t\"time\"\n+)\n\n // ✋✋✋✋\n // this is the main function 🏃\n" + source: SUGGESTION_SOURCE_TOOL + replacements: + - data: "import (\n\t\"fmt\"\n\t\"time\"\n)" + location: + path: /tmp/src/main.go + range: + startLine: 3 + endLine: 4 + endColumn: 13 + "###); + } } diff --git a/qlty-check/tests/fixtures/planner/patch_builder/parse.04.input.go b/qlty-check/tests/fixtures/planner/patch_builder/parse.04.input.go new file mode 100644 index 000000000..9438bf131 --- /dev/null +++ b/qlty-check/tests/fixtures/planner/patch_builder/parse.04.input.go @@ -0,0 +1,13 @@ +package main + +import "time" +import "fmt" + +// ✋✋✋✋ +// this is the main function 🏃 +func main() { + // This is a comment + fmt.Println("Heloo World!") // Intentional typo: "Heloo" instead of "Hello" + + time.Parse("asdf", "") +} diff --git a/qlty-check/tests/fixtures/planner/patch_builder/parse.04.output.txt b/qlty-check/tests/fixtures/planner/patch_builder/parse.04.output.txt new file mode 100644 index 000000000..ed91a6b2c --- /dev/null +++ b/qlty-check/tests/fixtures/planner/patch_builder/parse.04.output.txt @@ -0,0 +1 @@ +{"Issues":[{"FromLinter":"errcheck","Text":"Error return value of `time.Parse` is not checked","Severity":"","SourceLines":["\ttime.Parse(\"asdf\", \"\")"],"Replacement":null,"Pos":{"Filename":"/tmp/src/main.go","Offset":217,"Line":12,"Column":12},"ExpectNoLint":false,"ExpectedNoLintLinter":""},{"FromLinter":"godot","Text":"Comment should end in a period","Severity":"","SourceLines":["// this is the main function 🏃"],"Replacement":{"NeedOnlyDelete":false,"NewLines":["// this is the main function 🏃."],"Inline":null},"Pos":{"Filename":"/tmp/src/main.go","Offset":42,"Line":7,"Column":34},"ExpectNoLint":false,"ExpectedNoLintLinter":""},{"FromLinter":"goimports","Text":"File is not `goimports`-ed","Severity":"","SourceLines":["import \"time\"","import \"fmt\""],"Replacement":{"NeedOnlyDelete":false,"NewLines":["import (","\t\"fmt\"","\t\"time\"",")"],"Inline":null},"LineRange":{"From":3,"To":4},"Pos":{"Filename":"/tmp/src/main.go","Offset":0,"Line":3,"Column":0},"ExpectNoLint":false,"ExpectedNoLintLinter":""}],"Report":{"Linters":[{"Name":"asasalint"},{"Name":"asciicheck","Enabled":true},{"Name":"bidichk"},{"Name":"bodyclose","Enabled":true},{"Name":"canonicalheader"},{"Name":"containedctx"},{"Name":"contextcheck"},{"Name":"copyloopvar"},{"Name":"cyclop"},{"Name":"decorder"},{"Name":"deadcode"},{"Name":"depguard","Enabled":true},{"Name":"dogsled","Enabled":true},{"Name":"dupl"},{"Name":"dupword"},{"Name":"durationcheck"},{"Name":"errcheck","Enabled":true,"EnabledByDefault":true},{"Name":"errchkjson"},{"Name":"errname"},{"Name":"errorlint"},{"Name":"execinquery"},{"Name":"exhaustive"},{"Name":"exhaustivestruct"},{"Name":"exhaustruct"},{"Name":"exportloopref","Enabled":true},{"Name":"forbidigo"},{"Name":"forcetypeassert"},{"Name":"fatcontext"},{"Name":"funlen"},{"Name":"gci"},{"Name":"ginkgolinter"},{"Name":"gocheckcompilerdirectives"},{"Name":"gochecknoglobals"},{"Name":"gochecknoinits","Enabled":true},{"Name":"gochecksumtype"},{"Name":"gocognit"},{"Name":"goconst"},{"Name":"gocritic"},{"Name":"gocyclo"},{"Name":"godot","Enabled":true},{"Name":"godox"},{"Name":"err113"},{"Name":"gofmt","Enabled":true},{"Name":"gofumpt"},{"Name":"goheader","Enabled":true},{"Name":"goimports","Enabled":true},{"Name":"golint"},{"Name":"mnd"},{"Name":"gomnd"},{"Name":"gomoddirectives"},{"Name":"gomodguard"},{"Name":"goprintffuncname","Enabled":true},{"Name":"gosec","Enabled":true},{"Name":"gosimple","Enabled":true,"EnabledByDefault":true},{"Name":"gosmopolitan"},{"Name":"govet","Enabled":true,"EnabledByDefault":true},{"Name":"grouper"},{"Name":"ifshort"},{"Name":"importas"},{"Name":"inamedparam"},{"Name":"ineffassign","Enabled":true,"EnabledByDefault":true},{"Name":"interfacebloat"},{"Name":"interfacer"},{"Name":"intrange"},{"Name":"ireturn"},{"Name":"lll"},{"Name":"loggercheck"},{"Name":"maintidx"},{"Name":"makezero"},{"Name":"maligned"},{"Name":"mirror"},{"Name":"misspell","Enabled":true},{"Name":"musttag"},{"Name":"nakedret","Enabled":true},{"Name":"nestif"},{"Name":"nilerr"},{"Name":"nilnil"},{"Name":"nlreturn"},{"Name":"noctx"},{"Name":"nonamedreturns"},{"Name":"nosnakecase"},{"Name":"nosprintfhostport"},{"Name":"paralleltest"},{"Name":"perfsprint"},{"Name":"prealloc"},{"Name":"predeclared"},{"Name":"promlinter"},{"Name":"protogetter"},{"Name":"reassign"},{"Name":"revive"},{"Name":"rowserrcheck","Enabled":true},{"Name":"sloglint"},{"Name":"scopelint"},{"Name":"sqlclosecheck"},{"Name":"spancheck"},{"Name":"staticcheck","Enabled":true,"EnabledByDefault":true},{"Name":"structcheck"},{"Name":"stylecheck","Enabled":true},{"Name":"tagalign"},{"Name":"tagliatelle"},{"Name":"tenv"},{"Name":"testableexamples"},{"Name":"testifylint"},{"Name":"testpackage"},{"Name":"thelper"},{"Name":"tparallel"},{"Name":"typecheck","Enabled":true,"EnabledByDefault":true},{"Name":"unconvert","Enabled":true},{"Name":"unparam"},{"Name":"unused","Enabled":true,"EnabledByDefault":true},{"Name":"usestdlibvars"},{"Name":"varcheck"},{"Name":"varnamelen"},{"Name":"wastedassign"},{"Name":"whitespace","Enabled":true},{"Name":"wrapcheck"},{"Name":"wsl"},{"Name":"zerologlint"},{"Name":"nolintlint","Enabled":true}]}} diff --git a/qlty-config/src/config/plugin.rs b/qlty-config/src/config/plugin.rs index 8c76b1851..c30c5bc32 100644 --- a/qlty-config/src/config/plugin.rs +++ b/qlty-config/src/config/plugin.rs @@ -492,6 +492,8 @@ pub enum OutputFormat { Coffeelint, #[serde(rename = "ruff")] Ruff, + #[serde(rename = "golangci_lint")] + GolangciLint, } impl std::fmt::Display for OutputFormat { @@ -522,6 +524,7 @@ impl std::fmt::Display for OutputFormat { OutputFormat::Tsc => write!(f, "tsc"), OutputFormat::Coffeelint => write!(f, "coffeelint"), OutputFormat::Ruff => write!(f, "ruff"), + OutputFormat::GolangciLint => write!(f, "golangci_lint"), } } }