Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ls):support eslint config with lsp #1927

Closed
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
139 changes: 49 additions & 90 deletions crates/oxc_language_server/src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ use std::{
fs,
path::{Path, PathBuf},
rc::Rc,
sync::{Arc, RwLock},
sync::Arc,
};

use crate::options::LintOptions;
use miette::NamedSource;
use oxc_allocator::Allocator;
use oxc_diagnostics::{miette, Error, Severity};
use oxc_linter::{partial_loader::LINT_PARTIAL_LOADER_EXT, LintContext, LintSettings, Linter};
use oxc_linter_plugin::{make_relative_path_parts, LinterPlugin};

use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::{SourceType, VALID_EXTENSIONS};
Expand Down Expand Up @@ -140,20 +139,14 @@ pub struct FixedContent {
pub range: Range,
}

type Plugin = Arc<RwLock<Option<LinterPlugin>>>;

#[derive(Debug)]
pub struct IsolatedLintHandler {
#[allow(unused)]
options: Arc<LintOptions>,
linter: Arc<Linter>,
#[allow(unused)]
plugin: Plugin,
}

impl IsolatedLintHandler {
pub fn new(options: Arc<LintOptions>, linter: Arc<Linter>, plugin: Plugin) -> Self {
Self { options, linter, plugin }
pub fn new(linter: Arc<Linter>) -> Self {
Self { linter }
}

pub fn run_single(
Expand All @@ -162,49 +155,46 @@ impl IsolatedLintHandler {
content: Option<String>,
) -> Option<Vec<DiagnosticReport>> {
if Self::is_wanted_ext(path) {
Some(Self::lint_path(&self.linter, path, Arc::clone(&self.plugin), content).map_or(
vec![],
|(p, errors)| {
let mut diagnostics: Vec<DiagnosticReport> =
errors.into_iter().map(|e| e.into_diagnostic_report(&p)).collect();
// a diagnostics connected from related_info to original diagnostic
let mut inverted_diagnostics = vec![];
for d in &diagnostics {
let Some(ref related_info) = d.diagnostic.related_information else {
Some(Self::lint_path(&self.linter, path, content).map_or(vec![], |(p, errors)| {
let mut diagnostics: Vec<DiagnosticReport> =
errors.into_iter().map(|e| e.into_diagnostic_report(&p)).collect();
// a diagnostics connected from related_info to original diagnostic
let mut inverted_diagnostics = vec![];
for d in &diagnostics {
let Some(ref related_info) = d.diagnostic.related_information else {
continue;
};

let related_information = Some(vec![DiagnosticRelatedInformation {
location: lsp_types::Location {
uri: lsp_types::Url::from_file_path(path).unwrap(),
range: d.diagnostic.range,
},
message: "original diagnostic".to_string(),
}]);
for r in related_info {
if r.location.range == d.diagnostic.range {
continue;
};

let related_information = Some(vec![DiagnosticRelatedInformation {
location: lsp_types::Location {
uri: lsp_types::Url::from_file_path(path).unwrap(),
range: d.diagnostic.range,
},
message: "original diagnostic".to_string(),
}]);
for r in related_info {
if r.location.range == d.diagnostic.range {
continue;
}
inverted_diagnostics.push(DiagnosticReport {
diagnostic: lsp_types::Diagnostic {
range: r.location.range,
severity: Some(DiagnosticSeverity::HINT),
code: None,
message: r.message.clone(),
source: Some("oxc".into()),
code_description: None,
related_information: related_information.clone(),
tags: None,
data: None,
},
fixed_content: None,
});
}
inverted_diagnostics.push(DiagnosticReport {
diagnostic: lsp_types::Diagnostic {
range: r.location.range,
severity: Some(DiagnosticSeverity::HINT),
code: None,
message: r.message.clone(),
source: Some("oxc".into()),
code_description: None,
related_information: related_information.clone(),
tags: None,
data: None,
},
fixed_content: None,
});
}
diagnostics.append(&mut inverted_diagnostics);
diagnostics
},
))
}
diagnostics.append(&mut inverted_diagnostics);
diagnostics
}))
} else {
None
}
Expand Down Expand Up @@ -251,7 +241,6 @@ impl IsolatedLintHandler {
fn lint_path(
linter: &Linter,
path: &Path,
plugin: Plugin,
source_text: Option<String>,
) -> Option<(PathBuf, Vec<ErrorWithPosition>)> {
let ext = path.extension().and_then(std::ffi::OsStr::to_str)?;
Expand Down Expand Up @@ -291,22 +280,11 @@ impl IsolatedLintHandler {
return Some(Self::wrap_diagnostics(path, source_text, reports));
};

let mut lint_ctx = LintContext::new(
let lint_ctx = LintContext::new(
path.to_path_buf().into_boxed_path(),
&Rc::new(semantic_ret.semantic),
LintSettings::default(),
);
{
if let Ok(guard) = plugin.read() {
if let Some(plugin) = &*guard {
plugin
.lint_file(&mut lint_ctx, make_relative_path_parts(&path.into()))
.unwrap();
}
}
}

drop(plugin); // explicitly drop plugin so that we consume the plugin in this function's body

let result = linter.run(lint_ctx);

Expand Down Expand Up @@ -381,46 +359,27 @@ fn offset_to_position(offset: usize, source_text: &str) -> Option<Position> {

#[derive(Debug)]
pub struct ServerLinter {
linter: Arc<Linter>,
plugin: Plugin,
pub linter: Arc<Linter>,
}

impl ServerLinter {
pub fn new() -> Self {
let linter = Linter::new().with_fix(true);
Self { linter: Arc::new(linter), plugin: Arc::new(RwLock::new(None)) }
Self { linter: Arc::new(linter) }
}

pub fn make_plugin(&self, root_uri: &Url) {
let mut path = root_uri.to_file_path().unwrap();
path.push(".oxc/");
path.push("plugins");
if path.exists() {
let mut plugin = self.plugin.write().unwrap();
plugin.replace(LinterPlugin::new(&path).unwrap());
}
pub fn new_with_linter(linter: Linter) -> Self {
Self { linter: Arc::new(linter) }
}

pub fn run_single(
&self,
root_uri: &Url,
_root_uri: &Url,
uri: &Url,
content: Option<String>,
) -> Option<Vec<DiagnosticReport>> {
let options = LintOptions {
paths: vec![root_uri.to_file_path().unwrap()],
ignore_path: "node_modules".into(),
ignore_pattern: vec!["!**/node_modules/**/*".into()],
fix: true,
..LintOptions::default()
};

IsolatedLintHandler::new(
Arc::new(options),
Arc::clone(&self.linter),
Arc::clone(&self.plugin),
)
.run_single(&uri.to_file_path().unwrap(), content)
IsolatedLintHandler::new(Arc::clone(&self.linter))
.run_single(&uri.to_file_path().unwrap(), content)
}
}

Expand Down
44 changes: 35 additions & 9 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::linter::{DiagnosticReport, ServerLinter};
use globset::Glob;
use ignore::gitignore::Gitignore;
use log::{debug, error, info};
use oxc_linter::{LintOptions, Linter};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Debug;
Expand All @@ -13,7 +14,7 @@ use std::str::FromStr;

use dashmap::DashMap;
use futures::future::join_all;
use tokio::sync::{Mutex, OnceCell, SetError};
use tokio::sync::{Mutex, OnceCell, RwLock, SetError};
use tower_lsp::jsonrpc::{Error, ErrorCode, Result};
use tower_lsp::lsp_types::{
CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams,
Expand All @@ -30,7 +31,7 @@ use tower_lsp::{Client, LanguageServer, LspService, Server};
struct Backend {
client: Client,
root_uri: OnceCell<Option<Url>>,
server_linter: ServerLinter,
server_linter: RwLock<ServerLinter>,
diagnostics_report_map: DashMap<String, Vec<DiagnosticReport>>,
options: Mutex<Options>,
gitignore_glob: Mutex<Option<Gitignore>>,
Expand Down Expand Up @@ -79,6 +80,7 @@ impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
self.init(params.root_uri)?;
self.init_ignore_glob().await;
self.init_linter_config().await;
let options = params.initialization_options.and_then(|mut value| {
let settings = value.get_mut("settings")?.take();
serde_json::from_value::<Options>(settings).ok()
Expand Down Expand Up @@ -164,10 +166,6 @@ impl LanguageServer for Backend {

async fn initialized(&self, _params: InitializedParams) {
debug!("oxc initialized.");

if let Some(Some(root_uri)) = self.root_uri.get() {
self.server_linter.make_plugin(root_uri);
}
}

async fn shutdown(&self) -> Result<()> {
Expand Down Expand Up @@ -311,6 +309,33 @@ impl Backend {
*self.gitignore_glob.lock().await = gitignore_builder.build().ok();
}

async fn init_linter_config(&self) {
let Some(Some(uri)) = self.root_uri.get() else {
return;
};
let Ok(root_path) = uri.to_file_path() else {
return;
};
let mut config_path = None;
let rc_config = root_path.join(".eslintrc");
if rc_config.exists() {
config_path = Some(rc_config);
}
let rc_json_config = root_path.join(".eslintrc.json");
if rc_json_config.exists() {
config_path = Some(rc_json_config);
}
if let Some(config_path) = config_path {
let mut linter = self.server_linter.write().await;
*linter = ServerLinter::new_with_linter(
Linter::from_options(
LintOptions::default().with_fix(true).with_config_path(Some(config_path)),
)
.expect("should initialized linter with new options"),
);
}
}

#[allow(clippy::ptr_arg)]
async fn publish_all_diagnostics(&self, result: &Vec<(PathBuf, Vec<Diagnostic>)>) {
join_all(result.iter().map(|(path, diagnostics)| {
Expand All @@ -325,8 +350,9 @@ impl Backend {

async fn handle_file_update(&self, uri: Url, content: Option<String>, _version: Option<i32>) {
if let Some(Some(root_uri)) = self.root_uri.get() {
self.server_linter.make_plugin(root_uri);
if let Some(diagnostics) = self.server_linter.run_single(root_uri, &uri, content) {
if let Some(diagnostics) =
self.server_linter.read().await.run_single(root_uri, &uri, content)
{
self.client
.publish_diagnostics(
uri.clone(),
Expand Down Expand Up @@ -374,7 +400,7 @@ async fn main() {
let (service, socket) = LspService::build(|client| Backend {
client,
root_uri: OnceCell::new(),
server_linter,
server_linter: RwLock::new(server_linter),
diagnostics_report_map,
options: Mutex::new(Options::default()),
gitignore_glob: Mutex::new(None),
Expand Down