Skip to content

Commit 2ce5a33

Browse files
committed
fix(linter): resolve ignorePatterns relative to the config dir (#24339)
Fixes #24310 Root config's `ignorePatterns` are now resolved relative to the directory containing the config file, instead of CWD. Also fixes a latent issue: a `--config <path>` containing `..` silently disabled all `ignorePatterns` (the unnormalized parent dir never prefix-matches lint target paths). Explicit config paths are now lexically normalized, same as Oxfmt. ### Behavior change The new anchoring applies in all cases, including `--config` / LSP `configPath`. This intentionally differs from ESLint, which resolves alternate-config ignore patterns relative to CWD: - `extends`, `overrides[].files`, and JS plugin specifiers are already config-file-relative, including via `--config` - rooting only `ignorePatterns` at CWD would give one file two coordinate systems - Wrappers like Vite+ pass `--config <workspace-root config>` as an implementation detail while CWD stays at the invocation directory, so "`--config` means CWD-relative" does not hold in practice - #24276 Impact should be small: bare names (`dist`, `*.min.js`) and `**/`-prefixed patterns are unaffected by the anchor. The updated `config_ignore_patterns/with_oxlintrc` snapshot shows the `--config` case.
1 parent 18da3a2 commit 2ce5a33

17 files changed

Lines changed: 164 additions & 62 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"ignorePatterns": ["packages/foo/dist"]
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
debugger;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
debugger;

apps/oxlint/src-js/package/config.generated.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,8 @@ export interface Oxlintrc {
543543
*/
544544
globals?: OxlintGlobals;
545545
/**
546-
* Globs to ignore during linting. These are resolved from the configuration file path.
546+
* Globs to ignore during linting. Patterns use gitignore-style matching,
547+
* rooted at the directory containing the configuration file.
547548
*/
548549
ignorePatterns?: string[];
549550
/**

apps/oxlint/src/config_loader.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use oxc_linter::{
1515
};
1616
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
1717

18+
use crate::utils::normalize_path;
1819
use crate::{DEFAULT_JSONC_OXLINTRC_NAME, DEFAULT_OXLINTRC_NAME, DEFAULT_TS_OXLINTRC_NAME};
1920
use crate::{VITE_CONFIG_NAME, vp_version};
2021

@@ -571,7 +572,11 @@ impl<'a> ConfigLoader<'a> {
571572
cwd: &Path,
572573
config_path: &Path,
573574
) -> Result<Oxlintrc, OxcDiagnostic> {
574-
let full_path = cwd.join(config_path);
575+
// Normalize away `.`/`..` components:
576+
// this path (config's parent directory) becomes the root for `ignorePatterns` matching,
577+
// which is compared against the (normalized) lint target paths as a literal prefix.
578+
// If a root containing `..`, it never matches.
579+
let full_path = normalize_path(cwd.join(config_path));
575580
if is_js_config_path(&full_path) {
576581
return self.load_root_js_config(&full_path)?.ok_or_else(|| {
577582
OxcDiagnostic::error(format!(
@@ -826,13 +831,9 @@ mod test {
826831
let result = loader.load_root_config(&cwd, Some(&valid_parent_config));
827832
assert!(result.is_ok(), "Expected config lookup to succeed with parent directory syntax");
828833

829-
// Verify the resolved path is correct
834+
// Verify the resolved path is normalized, without `.`/`..` components
830835
if let Ok(config) = result {
831-
assert_eq!(
832-
config.path.file_name().unwrap().to_str().unwrap(),
833-
"eslintrc.json",
834-
"Config file name should be preserved after path resolution"
835-
);
836+
assert_eq!(config.path, cwd.join("fixtures/cli/linter/eslintrc.json"));
836837
}
837838
}
838839

apps/oxlint/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub mod lsp;
1010
mod mode;
1111
mod output_formatter;
1212
mod result;
13+
mod utils;
1314
mod walk;
1415

1516
#[cfg(test)]

apps/oxlint/src/lint.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,6 @@ impl CliRunner {
289289
enable_plugins.apply_overrides(&mut plugins);
290290
root_config.plugins = Some(plugins);
291291

292-
let base_ignore_patterns = root_config.ignore_patterns.clone();
293-
294292
let config_builder = match ConfigStoreBuilder::from_oxlintrc(
295293
false,
296294
root_config.clone(),
@@ -334,8 +332,13 @@ impl CliRunner {
334332
return crate::mode::run_rules(&lint_config, &output_formatter, stdout);
335333
}
336334

337-
let ignore_matcher =
338-
{ LintIgnoreMatcher::new(&base_ignore_patterns, &self.cwd, nested_ignore_patterns) };
335+
let ignore_matcher = LintIgnoreMatcher::new(
336+
&root_config.ignore_patterns,
337+
// Without a config file there are no patterns and the root is never consulted,
338+
// so the CWD fallback is an arbitrary placeholder.
339+
root_config.dir().unwrap_or(&self.cwd),
340+
nested_ignore_patterns,
341+
);
339342

340343
let files_to_lint = paths
341344
.into_iter()
@@ -844,6 +847,28 @@ mod test {
844847
.test_and_snapshot_multiple(&[args1, args2]);
845848
}
846849

850+
#[test]
851+
// https://github.com/oxc-project/oxc/issues/24310
852+
fn ignore_patterns_ancestor_config() {
853+
// Config is found via ancestor search; its `ignorePatterns` should be
854+
// rooted at the config file's directory, not CWD.
855+
let args = &["."];
856+
Tester::new()
857+
.with_cwd("fixtures/cli/ignore_patterns_ancestor_config/packages/foo".into())
858+
.test_and_snapshot(args);
859+
}
860+
861+
#[test]
862+
fn ignore_patterns_config_path_with_parent_references() {
863+
// `..` components in a `--config` path must be normalized before the config's
864+
// directory is used as the root for `ignorePatterns`, otherwise the patterns
865+
// silently never match.
866+
let args = &["-c", "./packages/../.oxlintrc.json", "."];
867+
Tester::new()
868+
.with_cwd("fixtures/cli/ignore_patterns_ancestor_config".into())
869+
.test_and_snapshot(args);
870+
}
871+
847872
#[test]
848873
fn ignore_patterns_with_symlink() {
849874
let args1 = &[];

apps/oxlint/src/lsp/server_linter.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ use crate::{
4747
options::{
4848
LintOptions as LSPLintOptions, RulesCustomization, Run, UnusedDisableDirectives,
4949
},
50-
utils::{normalize_path, range_overlaps},
50+
utils::range_overlaps,
5151
},
52+
utils::normalize_path,
5253
};
5354

5455
#[derive(Default)]
@@ -130,6 +131,9 @@ impl ServerLinterBuilder {
130131
};
131132

132133
let base_patterns = oxlintrc.ignore_patterns.clone();
134+
// Without a config file there are no patterns and the root is never consulted,
135+
// so the `root_path` fallback is an arbitrary placeholder.
136+
let base_ignore_root = oxlintrc.dir().unwrap_or(&root_path).to_path_buf();
133137

134138
let config_builder = match ConfigStoreBuilder::from_oxlintrc(
135139
false,
@@ -228,7 +232,7 @@ impl ServerLinterBuilder {
228232
ServerLinter::new(
229233
options.run,
230234
root_path.to_path_buf(),
231-
LintIgnoreMatcher::new(&base_patterns, &root_path, nested_ignore_patterns),
235+
LintIgnoreMatcher::new(&base_patterns, &base_ignore_root, nested_ignore_patterns),
232236
Self::create_ignore_glob(&root_path),
233237
extended_paths,
234238
runner,

apps/oxlint/src/lsp/utils.rs

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,8 @@
1-
use std::{
2-
borrow::Cow,
3-
path::{Component, Path, PathBuf},
4-
};
1+
use std::borrow::Cow;
52

63
use oxc_diagnostics::OxcCode;
74
use tower_lsp_server::ls_types::Range;
85

9-
/// Normalize a path by removing `.` and resolving `..` components,
10-
/// without touching the filesystem.
11-
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
12-
let mut result = PathBuf::new();
13-
14-
for component in path.as_ref().components() {
15-
match component {
16-
Component::ParentDir => {
17-
result.pop();
18-
}
19-
Component::CurDir => {
20-
// Skip current directory component
21-
}
22-
Component::Normal(c) => {
23-
result.push(c);
24-
}
25-
Component::RootDir | Component::Prefix(_) => {
26-
result.push(component.as_os_str());
27-
}
28-
}
29-
}
30-
31-
result
32-
}
33-
346
/// Returns `true` if LSP ranges `a` and `b` overlap or touch (share a boundary point).
357
///
368
/// This uses non-strict comparisons (`<=`/`>=`), so adjacent ranges where the end of one
@@ -59,24 +31,14 @@ pub fn get_full_rule_name(rule_code: &OxcCode) -> Option<Cow<'_, str>> {
5931

6032
#[cfg(test)]
6133
mod test {
62-
use std::path::Path;
63-
6434
use tower_lsp_server::ls_types::{Position, Range};
6535

66-
use crate::lsp::utils::{normalize_path, range_overlaps};
36+
use crate::lsp::utils::range_overlaps;
6737

6838
fn range(sl: u32, sc: u32, el: u32, ec: u32) -> Range {
6939
Range::new(Position::new(sl, sc), Position::new(el, ec))
7040
}
7141

72-
#[test]
73-
fn test_normalize_path() {
74-
assert_eq!(
75-
normalize_path(Path::new("/root/directory/./.oxlintrc.json")),
76-
Path::new("/root/directory/.oxlintrc.json")
77-
);
78-
}
79-
8042
#[test]
8143
fn test_range_overlaps_with_equal_ranges() {
8244
let range = range(1, 2, 3, 4);

apps/oxlint/src/snapshots/fixtures__cli__config_ignore_patterns__with_oxlintrc_-c .__test__eslintrc.json --ignore-pattern _.ts .@oxlint.snap

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ source: apps/oxlint/src/tester.rs
55
arguments: -c ./test/eslintrc.json --ignore-pattern *.ts .
66
working directory: fixtures/cli/config_ignore_patterns/with_oxlintrc
77
----------
8-
No files found to lint. Please check your paths and ignore patterns.
9-
Finished in <variable>ms on 0 files with 95 rules using 1 threads.
8+
9+
! unicorn(no-empty-file): Empty files are not allowed.
10+
,-[main.js:1:1]
11+
`----
12+
help: Delete this file or add some code to it.
13+
14+
Found 1 warning and 0 errors.
15+
Finished in <variable>ms on 1 file with 95 rules using 1 threads.
1016
----------
11-
CLI result: LintNoFilesFound
17+
CLI result: LintSucceeded
1218
----------

0 commit comments

Comments
 (0)