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
39 changes: 19 additions & 20 deletions crates/oxc_language_server/src/linter/isolated_lint_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use tower_lsp_server::{UriExt, lsp_types::Uri};

use oxc_allocator::Allocator;
use oxc_linter::{
AllowWarnDeny, ConfigStore, DirectivesStore, DisableDirectives, Fix, LINTABLE_EXTENSIONS,
LintOptions, LintService, LintServiceOptions, Linter, Message, PossibleFixes, RuleCommentType,
RuntimeFileSystem, read_to_arena_str, read_to_string,
AllowWarnDeny, ConfigStore, DisableDirectives, Fix, FixKind, LINTABLE_EXTENSIONS, LintOptions,
LintRunner, LintRunnerBuilder, LintServiceOptions, Linter, Message, PossibleFixes,
RuleCommentType, RuntimeFileSystem, read_to_arena_str, read_to_string,
};

use super::error_with_position::{
Expand All @@ -23,13 +23,14 @@ use super::error_with_position::{
#[derive(Debug, Clone)]
pub struct IsolatedLintHandlerOptions {
pub use_cross_module: bool,
pub type_aware: bool,
pub fix_kind: FixKind,
pub root_path: PathBuf,
pub tsconfig_path: Option<PathBuf>,
}

pub struct IsolatedLintHandler {
service: LintService,
directives_coordinator: DirectivesStore,
runner: LintRunner,
unused_directives_severity: Option<AllowWarnDeny>,
}

Expand Down Expand Up @@ -68,8 +69,6 @@ impl IsolatedLintHandler {
config_store: ConfigStore,
options: &IsolatedLintHandlerOptions,
) -> Self {
let directives_coordinator = DirectivesStore::new();

let linter = Linter::new(lint_options, config_store, None);
let mut lint_service_options = LintServiceOptions::new(options.root_path.clone())
.with_cross_module(options.use_cross_module);
Expand All @@ -81,12 +80,12 @@ impl IsolatedLintHandler {
lint_service_options = lint_service_options.with_tsconfig(tsconfig_path);
}

let mut service = LintService::new(linter, lint_service_options);
service.set_disable_directives_map(directives_coordinator.map());

Self {
service,
directives_coordinator,
runner: LintRunnerBuilder::new(lint_service_options, linter)
.with_type_aware(options.type_aware)
.with_fix_kind(options.fix_kind)
.build()
.unwrap(),
unused_directives_severity: lint_options.report_unused_directive,
}
}
Expand All @@ -113,21 +112,21 @@ impl IsolatedLintHandler {
debug!("lint {}", path.display());
let rope = &Rope::from_str(source_text);

let fs = Box::new(IsolatedLintHandlerFileSystem::new(
path.to_path_buf(),
Arc::from(source_text),
));

let mut messages: Vec<DiagnosticReport> = self
.service
.with_file_system(Box::new(IsolatedLintHandlerFileSystem::new(
path.to_path_buf(),
Arc::from(source_text),
)))
.with_paths(vec![Arc::from(path.as_os_str())])
.run_source()
.runner
.run_source(&Arc::from(path.as_os_str()), source_text.to_string(), fs)
.iter()
.map(|message| message_to_lsp_diagnostic(message, uri, source_text, rope))
.collect();

// Add unused directives if configured
if let Some(severity) = self.unused_directives_severity
&& let Some(directives) = self.directives_coordinator.get(path)
&& let Some(directives) = self.runner.directives_coordinator().get(path)
{
messages.extend(
create_unused_directives_messages(&directives, severity, source_text)
Expand Down
1 change: 0 additions & 1 deletion crates/oxc_language_server/src/linter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ pub mod error_with_position;
pub mod isolated_lint_handler;
pub mod options;
pub mod server_linter;
pub mod tsgo_linter;
120 changes: 41 additions & 79 deletions crates/oxc_language_server/src/linter/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use crate::linter::{
error_with_position::DiagnosticReport,
isolated_lint_handler::{IsolatedLintHandler, IsolatedLintHandlerOptions},
options::{LintOptions as LSPLintOptions, Run},
tsgo_linter::TsgoLinter,
};
use crate::utils::normalize_path;
use crate::{ConcurrentHashMap, LINT_CONFIG_FILE};
Expand All @@ -33,9 +32,19 @@ pub enum ServerLinterRun {
Always,
}

impl ServerLinterRun {
fn matches(&self, run: Run) -> bool {
matches!(
(self, run),
(ServerLinterRun::OnType, Run::OnType)
| (ServerLinterRun::OnSave, Run::OnSave)
| (ServerLinterRun::Always, _)
)
}
}

pub struct ServerLinter {
isolated_linter: Arc<Mutex<IsolatedLintHandler>>,
tsgo_linter: Arc<Option<TsgoLinter>>,
ignore_matcher: LintIgnoreMatcher,
gitignore_glob: Vec<Gitignore>,
lint_on_run: Run,
Expand All @@ -46,7 +55,6 @@ pub struct ServerLinter {
#[derive(Debug, Default)]
struct ServerLinterDiagnostics {
isolated_linter: Arc<ConcurrentHashMap<String, Option<Vec<DiagnosticReport>>>>,
tsgo_linter: Arc<ConcurrentHashMap<String, Option<Vec<DiagnosticReport>>>>,
}

impl ServerLinterDiagnostics {
Expand All @@ -59,29 +67,15 @@ impl ServerLinterDiagnostics {
reports.extend(diagnostics.clone());
}
}
if let Some(entry) = self.tsgo_linter.pin().get(path) {
found = true;
if let Some(diagnostics) = entry {
reports.extend(diagnostics.clone());
}
}
if found { Some(reports) } else { None }
}

pub fn remove_diagnostics(&self, path: &str) {
self.isolated_linter.pin().remove(path);
self.tsgo_linter.pin().remove(path);
}

pub fn get_cached_files_of_diagnostics(&self) -> Vec<String> {
let isolated_files = self.isolated_linter.pin().keys().cloned().collect::<Vec<_>>();
let tsgo_files = self.tsgo_linter.pin().keys().cloned().collect::<Vec<_>>();

let mut files = Vec::with_capacity(isolated_files.len() + tsgo_files.len());
files.extend(isolated_files);
files.extend(tsgo_files);
files.dedup();
files
self.isolated_linter.pin().keys().cloned().collect::<Vec<_>>()
}
}

Expand Down Expand Up @@ -156,9 +150,11 @@ impl ServerLinter {

let isolated_linter = IsolatedLintHandler::new(
lint_options,
config_store.clone(), // clone because tsgo linter needs it
config_store,
&IsolatedLintHandlerOptions {
use_cross_module,
type_aware: options.type_aware,
fix_kind: FixKind::from(options.fix_kind.clone()),
root_path: root_path.to_path_buf(),
tsconfig_path: options.ts_config_path.as_ref().map(|path| {
let path = Path::new(path).to_path_buf();
Expand All @@ -178,11 +174,6 @@ impl ServerLinter {
extended_paths,
lint_on_run: options.run,
diagnostics: ServerLinterDiagnostics::default(),
tsgo_linter: if options.type_aware {
Arc::new(Some(TsgoLinter::new(&root_path, config_store, fix_kind)))
} else {
Arc::new(None)
},
}
}

Expand Down Expand Up @@ -329,47 +320,25 @@ impl ServerLinter {
content: Option<String>,
run_type: ServerLinterRun,
) -> Option<Vec<DiagnosticReport>> {
let (oxlint, tsgolint) = match (run_type, self.lint_on_run) {
// run everything on save, or when it is forced
(ServerLinterRun::Always, _) | (ServerLinterRun::OnSave, Run::OnSave) => (true, true),
// run only oxlint on type
// tsgolint does not support memory source_text
(ServerLinterRun::OnType, Run::OnType) => (true, false),
// it does not match, run nothing
(ServerLinterRun::OnType, Run::OnSave) => (false, false),
// In onType mode, only TypeScript type checking runs on save
// If type_aware is disabled (tsgo_linter is None), skip everything to preserve diagnostics
(ServerLinterRun::OnSave, Run::OnType) => {
let should_run_tsgo = self.tsgo_linter.as_ref().is_some();
(false, should_run_tsgo)
}
};
let run = matches!(run_type, ServerLinterRun::Always) || run_type.matches(self.lint_on_run);

// return `None` when both tools do not want to be used
if !oxlint && !tsgolint {
if !run {
return None;
}

if self.is_ignored(uri) {
return None;
}

if oxlint {
let diagnostics = {
let mut isolated_linter = self.isolated_linter.lock().await;
isolated_linter.run_single(uri, content.clone())
};
self.diagnostics.isolated_linter.pin().insert(uri.to_string(), diagnostics);
}
let diagnostics = {
let mut isolated_linter = self.isolated_linter.lock().await;
isolated_linter.run_single(uri, content.clone())
};

if tsgolint && let Some(tsgo_linter) = self.tsgo_linter.as_ref() {
self.diagnostics
.tsgo_linter
.pin()
.insert(uri.to_string(), tsgo_linter.lint_file(uri, content.clone()));
}
self.diagnostics.isolated_linter.pin().insert(uri.to_string(), diagnostics.clone());

self.diagnostics.get_diagnostics(&uri.to_string())
diagnostics
}

pub fn needs_restart(old_options: &LSPLintOptions, new_options: &LSPLintOptions) -> bool {
Expand Down Expand Up @@ -465,31 +434,23 @@ mod test {
fn test_get_diagnostics_found_and_none_entries() {
let key = "file:///test.js".to_string();

// Case 1: Both entries present, Some diagnostics
// Case 1: Entry present, Some diagnostics
let diag = DiagnosticReport::default();
let diag_map = ConcurrentHashMap::default();
diag_map.pin().insert(key.clone(), Some(vec![diag.clone()]));
let tsgo_map = ConcurrentHashMap::default();
tsgo_map.pin().insert(key.clone(), Some(vec![diag]));
diag_map.pin().insert(key.clone(), Some(vec![diag]));

let server_diag = super::ServerLinterDiagnostics {
isolated_linter: std::sync::Arc::new(diag_map),
tsgo_linter: std::sync::Arc::new(tsgo_map),
};
let server_diag =
super::ServerLinterDiagnostics { isolated_linter: std::sync::Arc::new(diag_map) };
let result = server_diag.get_diagnostics(&key);
assert!(result.is_some());
assert_eq!(result.unwrap().len(), 2);
assert_eq!(result.unwrap().len(), 1);

// Case 2: Entry present, but value is None
let diag_map_none = ConcurrentHashMap::default();
diag_map_none.pin().insert(key.clone(), None);
let tsgo_map_none = ConcurrentHashMap::default();
tsgo_map_none.pin().insert(key.clone(), None);

let server_diag_none = ServerLinterDiagnostics {
isolated_linter: std::sync::Arc::new(diag_map_none),
tsgo_linter: std::sync::Arc::new(tsgo_map_none),
};
let server_diag_none =
ServerLinterDiagnostics { isolated_linter: std::sync::Arc::new(diag_map_none) };
let result_none = server_diag_none.get_diagnostics(&key);
assert!(result_none.is_some());
assert_eq!(result_none.unwrap().len(), 0);
Expand All @@ -505,14 +466,19 @@ mod test {
fn test_lint_on_run_on_type_on_type() {
Tester::new(
"fixtures/linter/lint_on_run/on_type",
Some(LintOptions { type_aware: true, run: Run::OnType, ..Default::default() }),
Some(LintOptions {
type_aware: true,
run: Run::OnType,
fix_kind: LintFixKindFlag::All,
..Default::default()
}),
)
.test_and_snapshot_single_file_with_run_type("on-type.ts", Run::OnType);
}

#[test]
#[cfg(not(target_endian = "big"))]
fn test_lint_on_run_on_type_on_save() {
fn test_lint_on_run_on_save_on_save() {
Tester::new(
"fixtures/linter/lint_on_run/on_save",
Some(LintOptions {
Expand All @@ -537,12 +503,12 @@ mod test {

#[test]
#[cfg(not(target_endian = "big"))]
fn test_lint_on_run_on_save_on_save() {
fn test_lint_on_run_on_type_on_save() {
Tester::new(
"fixtures/linter/lint_on_run/on_type",
"fixtures/linter/lint_on_run/on_save",
Some(LintOptions {
type_aware: true,
run: Run::OnSave,
run: Run::OnType,
fix_kind: LintFixKindFlag::All,
..Default::default()
}),
Expand Down Expand Up @@ -676,7 +642,6 @@ mod test {
"fixtures/linter/tsgolint",
Some(LintOptions {
type_aware: true,
run: Run::OnSave,
fix_kind: LintFixKindFlag::All,
..Default::default()
}),
Expand All @@ -686,10 +651,7 @@ mod test {

#[test]
fn test_ignore_js_plugins() {
let tester = Tester::new(
"fixtures/linter/js_plugins",
Some(LintOptions { run: Run::OnSave, ..Default::default() }),
);
let tester = Tester::new("fixtures/linter/js_plugins", Some(LintOptions::default()));
tester.test_and_snapshot_single_file("index.js");
}

Expand Down
Loading
Loading