Skip to content

Commit f900a5d

Browse files
authored
fix(stylelint): resolve extends/plugins against tool install directory (#2757)
Fixes #2700. ## Summary Users on stylelint ≥17 hit `ConfigurationError: Could not find "stylelint-config-standard"` even though qlty had installed the package into its tool cache via `package_filters`. This PR makes two small, targeted fixes: 1. **`qlty-plugins/plugins/linters/stylelint/plugin.toml`** — pass `--config-basedir "${linter}"` to the `lint` (both version matchers) and `format` scripts. `${linter}` interpolates to the plugin's tool install directory, which is where qlty already installs the user's filtered npm deps. 2. **`qlty-check/src/parser/stylelint.rs`** — harden the JSON parser to skip leading non-JSON lines (Node 21 emits `ExperimentalWarning` to stderr before stylelint's JSON report; stylelint ≥16 routes its report to stderr). Slice from the first `[` so the parser tolerates the warning while still surfacing genuinely malformed input. ## Why both changes are necessary Without (1), the user hits the reported `ConfigurationError`. Stylelint 17's `resolveSilent` uses `import-meta-resolve` exclusively, which doesn't honor `NODE_PATH`; stylelint 16 fell back to `resolve-from` which did, which is why stylelint 16 users don't see the bug. Setting `configBasedir` to the tool dir makes it stylelint's first resolution root. Without (2), applying (1) alone exchanges the `ConfigurationError` for a parser failure, because Node 21 prints `ExperimentalWarning: Importing JSON modules ...` to stderr before stylelint's JSON. The reporter would hit this second error immediately after (1) lands. ## Verification Added a plugin test fixture at `qlty-plugins/plugins/linters/stylelint/fixtures/extends.in/` pinning stylelint 17.3.0 + stylelint-config-standard 39.0.0. End-to-end behaviour confirmed: - **With both fixes applied:** test passes; `block-no-empty` violation reported as expected. Snapshot: `__snapshots__/extends_v17.3.0.shot`. - **Plugin.toml fix reverted (parser fix kept):** test fails with the exact `ConfigurationError` from #2700. Confirms the plugin.toml change is load-bearing. - **Both reverted:** test fails with the parser error, confirming fix (2) is also load-bearing. Existing `basic` fixture at 14.6.1 and 16.15.0 still passes — no regression. Also added a `parse_strips_node_warning_prefix` unit test in `qlty-check/src/parser/stylelint.rs`. ## Test plan - [x] `cargo check` — clean - [x] `cargo test --workspace` — all pass - [x] `cd qlty-plugins/plugins && npm test -- --testPathPattern='linters/stylelint'` — 3/3 pass - [x] Revert-and-reproduce verified the fixture genuinely exercises the bug path at stylelint 17.3.0 - [x] `qlty fmt` / `qlty check --level=low --fix` on touched files — clean ## Notes / caveats - The `${linter}` interpolation is quoted (`"${linter}"`) to survive tool-cache paths with spaces, matching the `editorconfig-checker` plugin's pattern. - `--config-basedir` has been a stylelint CLI flag since 7.x (2017), so both `<16.0.0` and `>=16.0.0` version matchers are safe. - The parser's `extract_json_array` helper uses a simple `find('[')` rather than line-by-line scanning. Stylelint's top-level output is always a JSON array, and any leading `[` inside a warning message would be rare and would still produce a parse error on the right-hand slice. If this proves fragile we can tighten to line-scan.
1 parent b42dfac commit f900a5d

7 files changed

Lines changed: 91 additions & 4 deletions

File tree

qlty-check/src/parser/stylelint.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ pub struct Stylelint {}
2828
impl Parser for Stylelint {
2929
fn parse(&self, _plugin_name: &str, output: &str) -> Result<Vec<Issue>> {
3030
let mut issues = vec![];
31-
let files: Vec<StylelintFile> = serde_json::from_str(output)?;
31+
let json = extract_json_array(output);
32+
let files: Vec<StylelintFile> = serde_json::from_str(json)?;
3233

3334
for file in files {
3435
for message in file.warnings {
@@ -62,6 +63,19 @@ impl Parser for Stylelint {
6263
}
6364
}
6465

66+
// Node emits diagnostic warnings like `(node:XXX) ExperimentalWarning: ...`
67+
// to stderr before stylelint's own output, and stylelint >=16 routes its
68+
// JSON report to stderr. Skip leading non-JSON lines so the parser works
69+
// regardless of Node's diagnostic chatter. We scan for the first `[`, which
70+
// always begins stylelint's top-level array, and fall back to the full
71+
// output if none is found so malformed input still surfaces as a parse error.
72+
fn extract_json_array(output: &str) -> &str {
73+
match output.find('[') {
74+
Some(idx) => &output[idx..],
75+
None => output,
76+
}
77+
}
78+
6579
fn severity_to_level(severity: String) -> Level {
6680
match severity.as_str() {
6781
// In eslint, issues come with a `fatal` attribute that we use to determine if the issue is Level::High or Level::Medium.
@@ -168,4 +182,25 @@ mod test {
168182
endColumn: 4
169183
"#);
170184
}
185+
186+
#[test]
187+
fn parse_strips_node_warning_prefix() {
188+
let input = "(node:543) ExperimentalWarning: Importing JSON modules is an experimental feature\n(Use `node --trace-warnings ...` to show where the warning was created)\n[{\"source\":\"/src/main.css\",\"warnings\":[{\"line\":1,\"column\":3,\"endLine\":1,\"endColumn\":5,\"rule\":\"block-no-empty\",\"severity\":\"error\",\"text\":\"Unexpected empty block (block-no-empty)\"}]}]";
189+
190+
let issues = Stylelint::default().parse("stylelint", input);
191+
insta::assert_yaml_snapshot!(issues.unwrap(), @r#"
192+
- tool: stylelint
193+
ruleKey: block-no-empty
194+
message: Unexpected empty block (block-no-empty)
195+
level: LEVEL_MEDIUM
196+
category: CATEGORY_LINT
197+
location:
198+
path: /src/main.css
199+
range:
200+
startLine: 1
201+
startColumn: 3
202+
endLine: 1
203+
endColumn: 5
204+
"#);
205+
}
171206
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`linter=stylelint fixture=extends version=17.3.0 1`] = `
4+
{
5+
"issues": [
6+
{
7+
"category": "CATEGORY_LINT",
8+
"level": "LEVEL_MEDIUM",
9+
"location": {
10+
"path": "basic.in.css",
11+
"range": {
12+
"endColumn": 5,
13+
"endLine": 1,
14+
"startColumn": 3,
15+
"startLine": 1,
16+
},
17+
},
18+
"message": "Empty block (block-no-empty)",
19+
"mode": "MODE_BLOCK",
20+
"ruleKey": "block-no-empty",
21+
"snippet": "a {}",
22+
"snippetWithContext": "a {}",
23+
"tool": "stylelint",
24+
},
25+
],
26+
}
27+
`;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
config_version = "0"
2+
3+
[[source]]
4+
name = "default"
5+
default = true
6+
7+
[[plugin]]
8+
name = "stylelint"
9+
version = "17.3.0"
10+
package_file = "package.json"
11+
package_filters = ["stylelint"]
12+
config_files = [".stylelintrc.json"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["stylelint-config-standard"]
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "stylelint-extends-fixture",
3+
"version": "0.0.0",
4+
"private": true,
5+
"devDependencies": {
6+
"stylelint": "17.3.0",
7+
"stylelint-config-standard": "39.0.0"
8+
}
9+
}

qlty-plugins/plugins/linters/stylelint/plugin.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ package_file_candidate_filters = ["stylelint"]
3434

3535
[[plugins.definitions.stylelint.drivers.lint.version]]
3636
version_matcher = "<16.0.0"
37-
script = "stylelint --formatter json ${target}"
37+
script = 'stylelint --config-basedir "${linter}" --formatter json ${target}'
3838
success_codes = [0, 2]
3939
error_codes = [1, 78]
4040
output = "stdout"
@@ -47,7 +47,7 @@ output_missing = "error"
4747

4848
[[plugins.definitions.stylelint.drivers.lint.version]]
4949
version_matcher = ">=16.0.0"
50-
script = "stylelint --formatter json ${target}"
50+
script = 'stylelint --config-basedir "${linter}" --formatter json ${target}'
5151
success_codes = [0, 2]
5252
error_codes = [1, 78]
5353
output = "stderr"
@@ -59,7 +59,7 @@ suggested = "config"
5959
output_missing = "error"
6060

6161
[plugins.definitions.stylelint.drivers.format]
62-
script = "stylelint --fix ${target}"
62+
script = 'stylelint --config-basedir "${linter}" --fix ${target}'
6363
success_codes = [0, 2]
6464
error_codes = [1, 78]
6565
cache_results = true

0 commit comments

Comments
 (0)