Skip to content

Commit 3d794f6

Browse files
committed
refactor(language_server): move functions related to ServerLinter to ServerLinter (#10761)
1 parent bd953fc commit 3d794f6

File tree

2 files changed

+224
-198
lines changed

2 files changed

+224
-198
lines changed

crates/oxc_language_server/src/linter/server_linter.rs

Lines changed: 209 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,185 @@
1+
use std::path::{Path, PathBuf};
12
use std::sync::Arc;
23

4+
use globset::Glob;
5+
use ignore::gitignore::Gitignore;
6+
use log::warn;
7+
use rustc_hash::{FxBuildHasher, FxHashMap};
38
use tower_lsp_server::lsp_types::Uri;
49

5-
use oxc_linter::Linter;
10+
use oxc_linter::{ConfigStore, ConfigStoreBuilder, LintOptions, Linter, Oxlintrc};
11+
use tower_lsp_server::UriExt;
612

7-
use crate::linter::error_with_position::DiagnosticReport;
8-
use crate::linter::isolated_lint_handler::IsolatedLintHandler;
13+
use crate::linter::{
14+
error_with_position::DiagnosticReport,
15+
isolated_lint_handler::{IsolatedLintHandler, IsolatedLintHandlerOptions},
16+
};
17+
use crate::{ConcurrentHashMap, Options};
918

10-
use super::isolated_lint_handler::IsolatedLintHandlerOptions;
19+
use super::config_walker::ConfigWalker;
1120

1221
#[derive(Clone)]
1322
pub struct ServerLinter {
1423
isolated_linter: Arc<IsolatedLintHandler>,
1524
}
1625

1726
impl ServerLinter {
18-
pub fn new_with_linter(linter: Linter, options: IsolatedLintHandlerOptions) -> Self {
27+
/// Searches inside root_uri recursively for the default oxlint config files
28+
/// and insert them inside the nested configuration
29+
pub fn create_nested_configs(
30+
root_uri: &Uri,
31+
options: &Options,
32+
) -> ConcurrentHashMap<PathBuf, ConfigStore> {
33+
// nested config is disabled, no need to search for configs
34+
if !options.use_nested_configs() {
35+
return ConcurrentHashMap::default();
36+
}
37+
38+
let root_path = root_uri.to_file_path().expect("Failed to convert URI to file path");
39+
40+
let paths = ConfigWalker::new(&root_path).paths();
41+
let nested_configs =
42+
ConcurrentHashMap::with_capacity_and_hasher(paths.capacity(), FxBuildHasher);
43+
44+
for path in paths {
45+
let file_path = Path::new(&path);
46+
let Some(dir_path) = file_path.parent() else {
47+
continue;
48+
};
49+
50+
let Ok(oxlintrc) = Oxlintrc::from_file(file_path) else {
51+
warn!("Skipping invalid config file: {}", file_path.display());
52+
continue;
53+
};
54+
let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc)
55+
else {
56+
warn!("Skipping config (builder failed): {}", file_path.display());
57+
continue;
58+
};
59+
let Ok(config_store) = config_store_builder.build() else {
60+
warn!("Skipping config (builder failed): {}", file_path.display());
61+
continue;
62+
};
63+
64+
nested_configs.pin().insert(dir_path.to_path_buf(), config_store);
65+
}
66+
67+
nested_configs
68+
}
69+
70+
pub fn create_ignore_glob(root_uri: &Uri, oxlintrc: &Oxlintrc) -> Vec<Gitignore> {
71+
let mut builder = globset::GlobSetBuilder::new();
72+
// Collecting all ignore files
73+
builder.add(Glob::new("**/.eslintignore").unwrap());
74+
builder.add(Glob::new("**/.gitignore").unwrap());
75+
76+
let ignore_file_glob_set = builder.build().unwrap();
77+
78+
let walk = ignore::WalkBuilder::new(root_uri.to_file_path().unwrap())
79+
.ignore(true)
80+
.hidden(false)
81+
.git_global(false)
82+
.build()
83+
.flatten();
84+
85+
let mut gitignore_globs = vec![];
86+
for entry in walk {
87+
let ignore_file_path = entry.path();
88+
if !ignore_file_glob_set.is_match(ignore_file_path) {
89+
continue;
90+
}
91+
92+
if let Some(ignore_file_dir) = ignore_file_path.parent() {
93+
let mut builder = ignore::gitignore::GitignoreBuilder::new(ignore_file_dir);
94+
builder.add(ignore_file_path);
95+
if let Ok(gitignore) = builder.build() {
96+
gitignore_globs.push(gitignore);
97+
}
98+
}
99+
}
100+
101+
if oxlintrc.ignore_patterns.is_empty() {
102+
return gitignore_globs;
103+
}
104+
105+
let Some(oxlintrc_dir) = oxlintrc.path.parent() else {
106+
warn!("Oxlintrc path has no parent, skipping inline ignore patterns");
107+
return gitignore_globs;
108+
};
109+
110+
let mut builder = ignore::gitignore::GitignoreBuilder::new(oxlintrc_dir);
111+
for entry in &oxlintrc.ignore_patterns {
112+
builder.add_line(None, entry).expect("Failed to add ignore line");
113+
}
114+
gitignore_globs.push(builder.build().unwrap());
115+
gitignore_globs
116+
}
117+
118+
pub fn create_server_linter(
119+
root_uri: &Uri,
120+
options: &Options,
121+
nested_configs: &ConcurrentHashMap<PathBuf, ConfigStore>,
122+
) -> (Self, Oxlintrc) {
123+
let root_path = root_uri.to_file_path().unwrap();
124+
let relative_config_path = options.config_path.clone();
125+
let oxlintrc = if relative_config_path.is_some() {
126+
let config = root_path.join(relative_config_path.unwrap());
127+
if config.try_exists().expect("Could not get fs metadata for config") {
128+
if let Ok(oxlintrc) = Oxlintrc::from_file(&config) {
129+
oxlintrc
130+
} else {
131+
warn!("Failed to initialize oxlintrc config: {}", config.to_string_lossy());
132+
Oxlintrc::default()
133+
}
134+
} else {
135+
warn!(
136+
"Config file not found: {}, fallback to default config",
137+
config.to_string_lossy()
138+
);
139+
Oxlintrc::default()
140+
}
141+
} else {
142+
Oxlintrc::default()
143+
};
144+
145+
// clone because we are returning it for ignore builder
146+
let config_builder =
147+
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc.clone()).unwrap_or_default();
148+
149+
// TODO(refactor): pull this into a shared function, because in oxlint we have the same functionality.
150+
let use_nested_config = options.use_nested_configs();
151+
152+
let use_cross_module = if use_nested_config {
153+
nested_configs.pin().values().any(|config| config.plugins().has_import())
154+
} else {
155+
config_builder.plugins().has_import()
156+
};
157+
158+
let config_store = config_builder.build().expect("Failed to build config store");
159+
160+
let lint_options = LintOptions { fix: options.fix_kind(), ..Default::default() };
161+
162+
let linter = if use_nested_config {
163+
let nested_configs = nested_configs.pin();
164+
let nested_configs_copy: FxHashMap<PathBuf, ConfigStore> = nested_configs
165+
.iter()
166+
.map(|(key, value)| (key.clone(), value.clone()))
167+
.collect::<FxHashMap<_, _>>();
168+
169+
Linter::new_with_nested_configs(lint_options, config_store, nested_configs_copy)
170+
} else {
171+
Linter::new(lint_options, config_store)
172+
};
173+
174+
let server_linter = ServerLinter::new_with_linter(
175+
linter,
176+
IsolatedLintHandlerOptions { use_cross_module, root_path: root_path.to_path_buf() },
177+
);
178+
179+
(server_linter, oxlintrc)
180+
}
181+
182+
fn new_with_linter(linter: Linter, options: IsolatedLintHandlerOptions) -> Self {
19183
let isolated_linter = Arc::new(IsolatedLintHandler::new(linter, options));
20184

21185
Self { isolated_linter }
@@ -28,7 +192,46 @@ impl ServerLinter {
28192

29193
#[cfg(test)]
30194
mod test {
31-
use crate::tester::Tester;
195+
use std::{path::PathBuf, str::FromStr};
196+
197+
use rustc_hash::FxHashMap;
198+
use tower_lsp_server::lsp_types::Uri;
199+
200+
use crate::{
201+
Options,
202+
linter::server_linter::ServerLinter,
203+
tester::{Tester, get_file_uri},
204+
};
205+
206+
#[test]
207+
fn test_create_nested_configs_with_disabled_nested_configs() {
208+
let mut flags = FxHashMap::default();
209+
flags.insert("disable_nested_configs".to_string(), "true".to_string());
210+
211+
let configs = ServerLinter::create_nested_configs(
212+
&Uri::from_str("file:///root/").unwrap(),
213+
&Options { flags, ..Options::default() },
214+
);
215+
216+
assert!(configs.is_empty());
217+
}
218+
219+
#[test]
220+
fn test_create_nested_configs() {
221+
let configs = ServerLinter::create_nested_configs(
222+
&get_file_uri("fixtures/linter/init_nested_configs"),
223+
&Options::default(),
224+
);
225+
let configs = configs.pin();
226+
let mut configs_dirs = configs.keys().collect::<Vec<&PathBuf>>();
227+
// sorting the key because for consistent tests results
228+
configs_dirs.sort();
229+
230+
assert!(configs_dirs.len() == 3);
231+
assert!(configs_dirs[2].ends_with("deep2"));
232+
assert!(configs_dirs[1].ends_with("deep1"));
233+
assert!(configs_dirs[0].ends_with("init_nested_configs"));
234+
}
32235

33236
#[test]
34237
fn test_no_errors() {

0 commit comments

Comments
 (0)