Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/oxlint/src/config_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ impl<'a> ConfigLoader<'a> {
self.load_many(configs)
}

fn load_root_config(
pub(crate) fn load_root_config(
&self,
cwd: &Path,
config_path: Option<&PathBuf>,
Expand Down
14 changes: 12 additions & 2 deletions apps/oxlint/src/lsp/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use oxc_linter::ExternalLinter;

#[cfg(feature = "napi")]
use crate::js_config::JsConfigLoaderCb;

mod code_actions;
mod commands;
mod error_with_position;
Expand All @@ -11,11 +14,18 @@ mod tester;
mod utils;

/// Run the language server
pub async fn run_lsp(external_linter: Option<ExternalLinter>) {
pub async fn run_lsp(
external_linter: Option<ExternalLinter>,
#[cfg(feature = "napi")] js_config_loader: Option<JsConfigLoaderCb>,
) {
oxc_language_server::run_server(
"oxlint".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
vec![Box::new(crate::lsp::server_linter::ServerLinterBuilder::new(external_linter))],
vec![Box::new(crate::lsp::server_linter::ServerLinterBuilder::new(
external_linter,
#[cfg(feature = "napi")]
js_config_loader,
))],
)
.await;
}
108 changes: 67 additions & 41 deletions apps/oxlint/src/lsp/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use oxc_language_server::{
};

use crate::{
DEFAULT_OXLINTRC_NAME,
config_loader::{ConfigLoader, build_nested_configs, discover_configs_in_tree},
lsp::{
code_actions::{
Expand All @@ -48,11 +47,20 @@ use crate::{
#[derive(Default)]
pub struct ServerLinterBuilder {
external_linter: Option<ExternalLinter>,
#[cfg(feature = "napi")]
js_config_loader: Option<crate::js_config::JsConfigLoaderCb>,
}

impl ServerLinterBuilder {
pub fn new(external_linter: Option<ExternalLinter>) -> Self {
Self { external_linter }
pub fn new(
external_linter: Option<ExternalLinter>,
#[cfg(feature = "napi")] js_config_loader: Option<crate::js_config::JsConfigLoaderCb>,
) -> Self {
Self {
external_linter,
#[cfg(feature = "napi")]
js_config_loader,
}
}

/// # Panics
Expand Down Expand Up @@ -82,9 +90,8 @@ impl ServerLinterBuilder {
let mut nested_ignore_patterns = Vec::new();
let mut extended_paths = FxHashSet::default();
let nested_configs = if options.use_nested_configs() {
Self::create_nested_configs(
self.create_nested_configs(
&root_path,
self.external_linter.as_ref(),
&mut external_plugin_store,
&mut nested_ignore_patterns,
&mut extended_paths,
Expand All @@ -93,24 +100,23 @@ impl ServerLinterBuilder {
} else {
FxHashMap::default()
};
let config_path = match options.config_path.as_deref() {
Some("") | None => DEFAULT_OXLINTRC_NAME,
Some(v) => v,
};
let config = normalize_path(root_path.join(config_path));
let oxlintrc = if config.try_exists().is_ok_and(|exists| exists) {
if let Ok(oxlintrc) = Oxlintrc::from_file(&config) {
oxlintrc
} else {
warn!("Failed to initialize oxlintrc config: {}", config.to_string_lossy());

let config_path = options.config_path.as_ref().filter(|p| !p.is_empty()).map(PathBuf::from);
let loader = ConfigLoader::new(
self.external_linter.as_ref(),
&mut external_plugin_store,
&[],
Some(root_uri.as_str()),
);
#[cfg(feature = "napi")]
let loader = loader.with_js_config_loader(self.js_config_loader.as_ref());

let oxlintrc = match loader.load_root_config(&root_path, config_path.as_ref()) {
Ok(config) => config,
Err(e) => {
warn!("Failed to load config: {e}");
Oxlintrc::default()
}
} else {
warn!(
"Config file not found: {}, fallback to default config",
config.to_string_lossy()
);
Oxlintrc::default()
};

let base_patterns = oxlintrc.ignore_patterns.clone();
Expand Down Expand Up @@ -313,17 +319,28 @@ impl ServerLinterBuilder {
/// Searches inside root_uri recursively for the default oxlint config files
/// and insert them inside the nested configuration
fn create_nested_configs(
&self,
root_path: &Path,
external_linter: Option<&ExternalLinter>,
external_plugin_store: &mut ExternalPluginStore,
nested_ignore_patterns: &mut Vec<(Vec<String>, PathBuf)>,
extended_paths: &mut FxHashSet<PathBuf>,
workspace_uri: Option<&str>,
) -> FxHashMap<PathBuf, Config> {
let config_paths = discover_configs_in_tree(root_path);

let mut loader =
ConfigLoader::new(external_linter, external_plugin_store, &[], workspace_uri);
#[cfg_attr(not(feature = "napi"), allow(unused_mut))]
let mut loader = ConfigLoader::new(
self.external_linter.as_ref(),
external_plugin_store,
&[],
workspace_uri,
);

#[cfg(feature = "napi")]
{
loader = loader.with_js_config_loader(self.js_config_loader.as_ref());
}

let (configs, errors) = loader.load_discovered(config_paths);

for error in errors {
Expand Down Expand Up @@ -449,15 +466,19 @@ impl Tool for ServerLinter {
LSPLintOptions::default()
}
};
let config_pattern = match options.config_path.as_deref() {
Some("") | None => "**/.oxlintrc.json".to_string(),
Some(v) => v.to_string(),
let mut watchers = match options.config_path.as_deref() {
Some("") | None => {
// Watch both JSON and TS config files
vec!["**/.oxlintrc.json".to_string(), "**/oxlint.config.ts".to_string()]
}
Some(v) => vec![v.to_string()],
};
let mut watchers = vec![config_pattern];

for path in &self.extended_paths {
// ignore .oxlintrc.json files when using nested configs
if path.ends_with(".oxlintrc.json") && options.use_nested_configs() {
// ignore .oxlintrc.json and oxlint.config.ts files when using nested configs
if (path.ends_with(".oxlintrc.json") || path.ends_with("oxlint.config.ts"))
&& options.use_nested_configs()
{
continue;
}

Expand Down Expand Up @@ -998,8 +1019,9 @@ mod test_watchers {
let patterns =
Tester::new("fixtures/lsp/watchers/default", json!({})).get_watcher_patterns();

assert_eq!(patterns.len(), 1);
assert_eq!(patterns.len(), 2);
assert_eq!(patterns[0], "**/.oxlintrc.json".to_string());
assert_eq!(patterns[1], "**/oxlint.config.ts".to_string());
}

#[test]
Expand All @@ -1012,8 +1034,9 @@ mod test_watchers {
)
.get_watcher_patterns();

assert_eq!(patterns.len(), 1);
assert_eq!(patterns.len(), 2);
assert_eq!(patterns[0], "**/.oxlintrc.json".to_string());
assert_eq!(patterns[1], "**/oxlint.config.ts".to_string());
}

#[test]
Expand All @@ -1035,10 +1058,11 @@ mod test_watchers {
let patterns = Tester::new("fixtures/lsp/watchers/linter_extends", json!({}))
.get_watcher_patterns();

// The `.oxlintrc.json` extends `./lint.json -> 2 watchers
assert_eq!(patterns.len(), 2);
// The `.oxlintrc.json` extends `./lint.json` -> 3 watchers (json, ts, lint.json)
assert_eq!(patterns.len(), 3);
assert_eq!(patterns[0], "**/.oxlintrc.json".to_string());
assert_eq!(patterns[1], "lint.json".to_string());
assert_eq!(patterns[1], "**/oxlint.config.ts".to_string());
assert_eq!(patterns[2], "lint.json".to_string());
}

#[test]
Expand Down Expand Up @@ -1066,9 +1090,10 @@ mod test_watchers {
)
.get_watcher_patterns();

assert_eq!(patterns.len(), 2);
assert_eq!(patterns.len(), 3);
assert_eq!(patterns[0], "**/.oxlintrc.json".to_string());
assert_eq!(patterns[1], "**/tsconfig*.json".to_string());
assert_eq!(patterns[1], "**/oxlint.config.ts".to_string());
assert_eq!(patterns[2], "**/tsconfig*.json".to_string());
}
}

Expand Down Expand Up @@ -1119,9 +1144,10 @@ mod test_watchers {
"typeAware": true
}));
assert!(watch_patterns.is_some());
assert_eq!(watch_patterns.as_ref().unwrap().len(), 2);
assert_eq!(watch_patterns.as_ref().unwrap().len(), 3);
assert_eq!(watch_patterns.as_ref().unwrap()[0], "**/.oxlintrc.json".to_string());
assert_eq!(watch_patterns.as_ref().unwrap()[1], "**/tsconfig*.json".to_string());
assert_eq!(watch_patterns.as_ref().unwrap()[1], "**/oxlint.config.ts".to_string());
assert_eq!(watch_patterns.as_ref().unwrap()[2], "**/tsconfig*.json".to_string());
}
}
}
Expand All @@ -1141,12 +1167,12 @@ mod test {

#[test]
fn test_create_nested_configs() {
let builder = ServerLinterBuilder::default();
let mut nested_ignore_patterns = Vec::new();
let mut external_plugin_store = ExternalPluginStore::new(false);
let mut extended_paths = FxHashSet::default();
let configs = ServerLinterBuilder::create_nested_configs(
let configs = builder.create_nested_configs(
&get_file_path("fixtures/lsp/init_nested_configs"),
None,
&mut external_plugin_store,
&mut nested_ignore_patterns,
&mut extended_paths,
Expand Down
7 changes: 6 additions & 1 deletion apps/oxlint/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ async fn main() -> CliRunResult {

// If --lsp flag is set, run the language server
if command.lsp {
run_lsp(None).await;
run_lsp(
None,
#[cfg(feature = "napi")]
None,
)
.await;
return CliRunResult::LintSucceeded;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/oxlint/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ async fn lint_impl(
// TODO: Remove this line once we have solidified the implementation and thoroughly tested it.
let external_linter = if cfg!(feature = "testing") { external_linter } else { None };

crate::lsp::run_lsp(external_linter).await;
crate::lsp::run_lsp(external_linter, js_config_loader).await;
return CliRunResult::LintSucceeded;
}

Expand Down
Loading