Skip to content

Commit 985d489

Browse files
davehentonmarschatthaclaude
authored
feat(plugins): add editorconfig-checker plugin (#2747)
## Summary - add an `editorconfig-checker` plugin definition and fixtures - add parser support and wire in the new output format - add a plugin snapshot test for v3.6.1 - handle empty `editorconfig-checker` output as no issues and skip its plugin test on Windows ## Testing - cargo check - cargo test - `npm test -- editorconfig-checker` - Manual testing with the built `qlty` CLI in a sample Git repo using `.editorconfig`. ## Notes - this PR intentionally keeps the plugin itself Linux/macOS-only --------- Co-authored-by: marschattha <arslan_21@rocketmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c7f67bb commit 985d489

File tree

10 files changed

+284
-1
lines changed

10 files changed

+284
-1
lines changed

qlty-check/src/executor/driver.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::parser::bandit::Bandit;
66
use crate::parser::biome::Biome;
77
use crate::parser::clippy::Clippy;
88
use crate::parser::coffeelint::Coffeelint;
9+
use crate::parser::editorconfig_checker::EditorconfigChecker;
910
use crate::parser::eslint::Eslint;
1011
use crate::parser::golangci_lint::GolangciLint;
1112
use crate::parser::hadolint::Hadolint;
@@ -427,6 +428,7 @@ impl Driver {
427428
OutputFormat::Biome => Box::new(Biome {}),
428429
OutputFormat::Clippy => Box::<Clippy>::default(),
429430
OutputFormat::Coffeelint => Box::new(Coffeelint {}),
431+
OutputFormat::EditorconfigChecker => Box::new(EditorconfigChecker {}),
430432
OutputFormat::Eslint => Box::<Eslint>::default(),
431433
OutputFormat::GolangciLint => Box::new(GolangciLint {}),
432434
OutputFormat::Hadolint => Box::new(Hadolint {}),

qlty-check/src/parser.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod bandit;
77
pub mod biome;
88
pub mod clippy;
99
pub mod coffeelint;
10+
pub mod editorconfig_checker;
1011
pub mod eslint;
1112
pub mod golangci_lint;
1213
pub mod hadolint;
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// [
2+
// {
3+
// "check_name": "editorconfig-checker",
4+
// "description": "Trailing whitespace",
5+
// "fingerprint": "451537eb8214cc2a82277e294151a4e1",
6+
// "severity": "minor",
7+
// "location": {
8+
// "path": "test.py",
9+
// "lines": { "begin": 2, "end": 0 }
10+
// }
11+
// }
12+
// ]
13+
14+
use super::Parser;
15+
use anyhow::Result;
16+
use qlty_types::analysis::v1::{Category, Issue, Level, Location, Range};
17+
use serde::{Deserialize, Serialize};
18+
19+
#[derive(Debug, Deserialize)]
20+
struct CodeClimateIssue {
21+
description: String,
22+
severity: String,
23+
location: CodeClimateLocation,
24+
}
25+
26+
#[derive(Debug, Deserialize)]
27+
struct CodeClimateLocation {
28+
path: String,
29+
lines: CodeClimateLines,
30+
}
31+
32+
#[derive(Debug, Deserialize)]
33+
struct CodeClimateLines {
34+
begin: u32,
35+
#[serde(default)]
36+
end: u32,
37+
}
38+
39+
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
40+
pub struct EditorconfigChecker {}
41+
42+
impl Parser for EditorconfigChecker {
43+
fn parse(&self, _plugin_name: &str, output: &str) -> Result<Vec<Issue>> {
44+
let mut issues = vec![];
45+
let messages: Vec<CodeClimateIssue> = serde_json::from_str(output)?;
46+
47+
for message in messages {
48+
let rule_key = derive_rule_key(&message.description);
49+
// editorconfig-checker's codeclimate formatter sets `end` to
50+
// AdditionalIdenticalErrorCount (a relative count) rather than
51+
// an absolute line number, so we compute: begin + end
52+
let end_line = if message.location.lines.end > 0 {
53+
message.location.lines.begin + message.location.lines.end
54+
} else {
55+
message.location.lines.begin
56+
};
57+
58+
let issue = Issue {
59+
tool: "editorconfig-checker".into(),
60+
message: message.description,
61+
category: Category::Lint.into(),
62+
level: severity_to_level(&message.severity).into(),
63+
rule_key,
64+
location: Some(Location {
65+
path: message.location.path,
66+
range: Some(Range {
67+
start_line: message.location.lines.begin,
68+
end_line,
69+
..Default::default()
70+
}),
71+
}),
72+
..Default::default()
73+
};
74+
75+
issues.push(issue);
76+
}
77+
78+
Ok(issues)
79+
}
80+
}
81+
82+
fn derive_rule_key(description: &str) -> String {
83+
if description.starts_with("Trailing whitespace") {
84+
"trim_trailing_whitespace".into()
85+
} else if description.starts_with("Wrong indent style") {
86+
"indent_style".into()
87+
} else if description.starts_with("Wrong amount of left-padding") {
88+
"indent_size".into()
89+
} else if description.starts_with("Final newline expected")
90+
|| description.starts_with("No final newline expected")
91+
{
92+
"insert_final_newline".into()
93+
} else if description.starts_with("Line too long") {
94+
"max_line_length".into()
95+
} else if description.starts_with("Wrong line endings")
96+
|| description.starts_with("Not all lines have the correct end of line")
97+
{
98+
"end_of_line".into()
99+
} else if description.starts_with("Wrong character encoding") {
100+
"charset".into()
101+
} else {
102+
"unknown".into()
103+
}
104+
}
105+
106+
fn severity_to_level(severity: &str) -> Level {
107+
match severity {
108+
"blocker" | "critical" => Level::High,
109+
"major" => Level::Medium,
110+
"info" | "minor" => Level::Low,
111+
_ => Level::Medium,
112+
}
113+
}
114+
115+
#[cfg(test)]
116+
mod test {
117+
use super::*;
118+
119+
#[test]
120+
fn parse() {
121+
let input = r###"
122+
[{"check_name":"editorconfig-checker","description":"Trailing whitespace","fingerprint":"451537eb8214cc2a82277e294151a4e1","severity":"minor","location":{"path":"test.py","lines":{"begin":2,"end":0}}},{"check_name":"editorconfig-checker","description":"Wrong amount of left-padding spaces(want multiple of 4)","fingerprint":"e901227647c3654fdacb6620554b5828","severity":"minor","location":{"path":"test.py","lines":{"begin":2,"end":0}}},{"check_name":"editorconfig-checker","description":"Wrong indent style found (tabs instead of spaces)","fingerprint":"36189515c2ba2d76f277d94c5a36c6d4","severity":"minor","location":{"path":"test.py","lines":{"begin":3,"end":1}}}]
123+
"###;
124+
125+
let issues = EditorconfigChecker::default().parse("editorconfig-checker", input);
126+
insta::assert_yaml_snapshot!(issues.unwrap(), @r###"
127+
- tool: editorconfig-checker
128+
ruleKey: trim_trailing_whitespace
129+
message: Trailing whitespace
130+
level: LEVEL_LOW
131+
category: CATEGORY_LINT
132+
location:
133+
path: test.py
134+
range:
135+
startLine: 2
136+
endLine: 2
137+
- tool: editorconfig-checker
138+
ruleKey: indent_size
139+
message: Wrong amount of left-padding spaces(want multiple of 4)
140+
level: LEVEL_LOW
141+
category: CATEGORY_LINT
142+
location:
143+
path: test.py
144+
range:
145+
startLine: 2
146+
endLine: 2
147+
- tool: editorconfig-checker
148+
ruleKey: indent_style
149+
message: Wrong indent style found (tabs instead of spaces)
150+
level: LEVEL_LOW
151+
category: CATEGORY_LINT
152+
location:
153+
path: test.py
154+
range:
155+
startLine: 3
156+
endLine: 4
157+
"###);
158+
}
159+
}

qlty-config/src/config/plugin.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,8 @@ pub enum OutputFormat {
518518
Clippy,
519519
#[serde(rename = "coffeelint")]
520520
Coffeelint,
521+
#[serde(rename = "editorconfig_checker")]
522+
EditorconfigChecker,
521523
#[serde(rename = "eslint")]
522524
Eslint,
523525
#[serde(rename = "golangci_lint")]
@@ -577,6 +579,7 @@ impl std::fmt::Display for OutputFormat {
577579
OutputFormat::Biome => write!(f, "biome"),
578580
OutputFormat::Clippy => write!(f, "clippy"),
579581
OutputFormat::Coffeelint => write!(f, "coffeelint"),
582+
OutputFormat::EditorconfigChecker => write!(f, "editorconfig_checker"),
580583
OutputFormat::Eslint => write!(f, "eslint"),
581584
OutputFormat::GolangciLint => write!(f, "golangci_lint"),
582585
OutputFormat::Hadolint => write!(f, "hadolint"),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { linterCheckTest } from "tests";
2+
3+
linterCheckTest("editorconfig-checker", __dirname);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 4
6+
trim_trailing_whitespace = true
7+
insert_final_newline = true
8+
end_of_line = lf
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`linter=editorconfig-checker fixture=basic version=3.6.1 1`] = `
4+
{
5+
"issues": [
6+
{
7+
"category": "CATEGORY_LINT",
8+
"level": "LEVEL_LOW",
9+
"location": {
10+
"path": "basic.in.py",
11+
"range": {
12+
"endLine": 2,
13+
"startLine": 2,
14+
},
15+
},
16+
"message": "Wrong amount of left-padding spaces(want multiple of 4)",
17+
"mode": "MODE_COMMENT",
18+
"ruleKey": "indent_size",
19+
"snippet": " x = 1 ",
20+
"snippetWithContext": "def hello():
21+
x = 1
22+
if True:
23+
pass",
24+
"tool": "editorconfig-checker",
25+
},
26+
{
27+
"category": "CATEGORY_LINT",
28+
"level": "LEVEL_LOW",
29+
"location": {
30+
"path": "basic.in.py",
31+
"range": {
32+
"endLine": 4,
33+
"startLine": 3,
34+
},
35+
},
36+
"message": "Wrong indent style found (tabs instead of spaces)",
37+
"mode": "MODE_COMMENT",
38+
"ruleKey": "indent_style",
39+
"snippet": " if True:
40+
pass",
41+
"snippetWithContext": "def hello():
42+
x = 1
43+
if True:
44+
pass",
45+
"tool": "editorconfig-checker",
46+
},
47+
{
48+
"category": "CATEGORY_LINT",
49+
"level": "LEVEL_LOW",
50+
"location": {
51+
"path": "basic.in.py",
52+
"range": {
53+
"endLine": 2,
54+
"startLine": 2,
55+
},
56+
},
57+
"message": "Trailing whitespace",
58+
"mode": "MODE_COMMENT",
59+
"ruleKey": "trim_trailing_whitespace",
60+
"snippet": " x = 1 ",
61+
"snippetWithContext": "def hello():
62+
x = 1
63+
if True:
64+
pass",
65+
"tool": "editorconfig-checker",
66+
},
67+
],
68+
}
69+
`;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def hello():
2+
x = 1
3+
if True:
4+
pass
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
config_version = "0"
2+
3+
[plugins.releases.editorconfig-checker]
4+
github = "editorconfig-checker/editorconfig-checker"
5+
strip_components = 0
6+
7+
[plugins.definitions.editorconfig-checker]
8+
releases = ["editorconfig-checker"]
9+
file_types = ["ALL"]
10+
config_files = [".editorconfig"]
11+
latest_version = "3.6.1"
12+
known_good_version = "3.6.1"
13+
version_command = "\"${linter}\"/bin/ec-* --version"
14+
description = "Validates files against .editorconfig rules"
15+
affects_cache = [".editorconfig"]
16+
suggested_mode = "comment"
17+
supported_platforms = ["linux", "macos"]
18+
19+
[plugins.definitions.editorconfig-checker.drivers.lint]
20+
script = "\"${linter}\"/bin/ec-* -format codeclimate --no-color ${target}"
21+
success_codes = [0, 1]
22+
output = "stdout"
23+
output_format = "editorconfig_checker"
24+
cache_results = true
25+
batch = true
26+
suggested = "config"
27+
output_missing = "no_issues"

qlty-plugins/plugins/tests/runLinterTest.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@ Debug.inspectOpts!.hideDate = true;
1212

1313
// Currently unsupported tools on Windows
1414
const SKIP_LINTERS = {
15-
win32: ["semgrep", "swiftlint", "dockerfmt", "swiftformat", "stringslint"],
15+
win32: [
16+
"editorconfig-checker",
17+
"semgrep",
18+
"swiftlint",
19+
"dockerfmt",
20+
"swiftformat",
21+
"stringslint",
22+
],
1623
linux: ["stringslint", "swiftformat", "swiftlint"],
1724
} as { [key in NodeJS.Platform]: string[] };
1825

0 commit comments

Comments
 (0)