From 2f9a6ac96110505db8f12f2a394735bb098d1097 Mon Sep 17 00:00:00 2001 From: Sysix <3897725+Sysix@users.noreply.github.com> Date: Tue, 26 Aug 2025 02:39:53 +0000 Subject: [PATCH] refactor(linter): move lsp relevant code into own file (#13243) Moving all code behind `#[cfg(feature = "language_server")]` into own file. --- crates/oxc_linter/src/fixer/fix.rs | 18 -- crates/oxc_linter/src/fixer/mod.rs | 98 --------- crates/oxc_linter/src/lib.rs | 4 +- crates/oxc_linter/src/lsp.rs | 197 ++++++++++++++++++ crates/oxc_linter/src/service/mod.rs | 4 - .../src/service/offset_to_position.rs | 101 --------- crates/oxc_linter/src/service/runtime.rs | 4 +- crates/oxc_linter/src/tsgolint.rs | 2 +- 8 files changed, 203 insertions(+), 225 deletions(-) create mode 100644 crates/oxc_linter/src/lsp.rs delete mode 100644 crates/oxc_linter/src/service/offset_to_position.rs diff --git a/crates/oxc_linter/src/fixer/fix.rs b/crates/oxc_linter/src/fixer/fix.rs index 8dabd81a075d1..74ac838eccde9 100644 --- a/crates/oxc_linter/src/fixer/fix.rs +++ b/crates/oxc_linter/src/fixer/fix.rs @@ -5,9 +5,6 @@ use bitflags::bitflags; use oxc_allocator::{Allocator, CloneIn}; use oxc_span::{GetSpan, SPAN, Span}; -#[cfg(feature = "language_server")] -use crate::service::offset_to_position::SpanPositionMessage; - bitflags! { /// Flags describing an automatic code fix. /// @@ -292,13 +289,6 @@ pub struct Fix<'a> { pub span: Span, } -#[cfg(feature = "language_server")] -#[derive(Debug)] -pub struct FixWithPosition<'a> { - pub content: Cow<'a, str>, - pub span: SpanPositionMessage<'a>, -} - impl<'new> CloneIn<'new> for Fix<'_> { type Cloned = Fix<'new>; @@ -392,14 +382,6 @@ impl PossibleFixes<'_> { } } -#[cfg(feature = "language_server")] -#[derive(Debug)] -pub enum PossibleFixesWithPosition<'a> { - None, - Single(FixWithPosition<'a>), - Multiple(Vec>), -} - // NOTE (@DonIsaac): having these variants is effectively the same as interning // single or 0-element Vecs. I experimented with using smallvec here, but the // resulting struct size was larger (40 bytes vs 32). So, we're sticking with diff --git a/crates/oxc_linter/src/fixer/mod.rs b/crates/oxc_linter/src/fixer/mod.rs index dfbf28013d63d..5857610307958 100644 --- a/crates/oxc_linter/src/fixer/mod.rs +++ b/crates/oxc_linter/src/fixer/mod.rs @@ -6,15 +6,6 @@ use oxc_span::{GetSpan, Span}; use crate::LintContext; -#[cfg(feature = "language_server")] -use crate::service::offset_to_position::SpanPositionMessage; -#[cfg(feature = "language_server")] -pub use fix::{FixWithPosition, PossibleFixesWithPosition}; -#[cfg(feature = "language_server")] -use oxc_data_structures::rope::Rope; -#[cfg(feature = "language_server")] -use oxc_diagnostics::{OxcCode, Severity}; - mod fix; pub use fix::{CompositeFix, Fix, FixKind, PossibleFixes, RuleFix}; use oxc_allocator::{Allocator, CloneIn}; @@ -235,95 +226,6 @@ pub struct Message<'a> { fixed: bool, } -#[cfg(feature = "language_server")] -#[derive(Debug)] -pub struct MessageWithPosition<'a> { - pub message: Cow<'a, str>, - pub labels: Option>>, - pub help: Option>, - pub severity: Severity, - pub code: OxcCode, - pub url: Option>, - pub fixes: PossibleFixesWithPosition<'a>, -} - -#[cfg(feature = "language_server")] -impl From for MessageWithPosition<'_> { - fn from(from: OxcDiagnostic) -> Self { - Self { - message: from.message.clone(), - labels: None, - help: from.help.clone(), - severity: from.severity, - code: from.code.clone(), - url: from.url.clone(), - fixes: PossibleFixesWithPosition::None, - } - } -} - -// clippy: the source field is checked and assumed to be less than 4GB, and -// we assume that the fix offset will not exceed 2GB in either direction -#[cfg(feature = "language_server")] -#[expect(clippy::cast_possible_truncation)] -pub fn message_to_message_with_position<'a>( - message: &Message<'a>, - source_text: &str, - rope: &Rope, -) -> MessageWithPosition<'a> { - use crate::service::offset_to_position::offset_to_position; - - let labels = message.error.labels.as_ref().map(|labels| { - labels - .iter() - .map(|labeled_span| { - let offset = labeled_span.offset() as u32; - let start_position = offset_to_position(rope, offset, source_text); - let end_position = - offset_to_position(rope, offset + labeled_span.len() as u32, source_text); - let message = labeled_span.label().map(|label| Cow::Owned(label.to_string())); - - SpanPositionMessage::new(start_position, end_position).with_message(message) - }) - .collect::>() - }); - - MessageWithPosition { - message: message.error.message.clone(), - severity: message.error.severity, - help: message.error.help.clone(), - url: message.error.url.clone(), - code: message.error.code.clone(), - labels, - fixes: match &message.fixes { - PossibleFixes::None => PossibleFixesWithPosition::None, - PossibleFixes::Single(fix) => { - PossibleFixesWithPosition::Single(fix_to_fix_with_position(fix, rope, source_text)) - } - PossibleFixes::Multiple(fixes) => PossibleFixesWithPosition::Multiple( - fixes.iter().map(|fix| fix_to_fix_with_position(fix, rope, source_text)).collect(), - ), - }, - } -} - -#[cfg(feature = "language_server")] -fn fix_to_fix_with_position<'a>( - fix: &Fix<'a>, - rope: &Rope, - source_text: &str, -) -> FixWithPosition<'a> { - use crate::service::offset_to_position::offset_to_position; - - let start_position = offset_to_position(rope, fix.span.start, source_text); - let end_position = offset_to_position(rope, fix.span.end, source_text); - FixWithPosition { - content: fix.content.clone(), - span: SpanPositionMessage::new(start_position, end_position) - .with_message(fix.message.as_ref().map(|label| Cow::Owned(label.to_string()))), - } -} - impl<'new> CloneIn<'new> for Message<'_> { type Cloned = Message<'new>; diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index bea97b3b7bcc2..7005d8a3d2ef1 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -21,6 +21,8 @@ mod external_plugin_store; mod fixer; mod frameworks; mod globals; +#[cfg(feature = "language_server")] +mod lsp; mod module_graph_visitor; mod module_record; mod options; @@ -70,7 +72,7 @@ use crate::{ }; #[cfg(feature = "language_server")] -pub use crate::fixer::{FixWithPosition, MessageWithPosition, PossibleFixesWithPosition}; +pub use crate::lsp::{FixWithPosition, MessageWithPosition, PossibleFixesWithPosition}; #[cfg(target_pointer_width = "64")] #[test] diff --git a/crates/oxc_linter/src/lsp.rs b/crates/oxc_linter/src/lsp.rs new file mode 100644 index 0000000000000..fd014102b475b --- /dev/null +++ b/crates/oxc_linter/src/lsp.rs @@ -0,0 +1,197 @@ +use oxc_data_structures::rope::{Rope, get_line_column}; +use std::borrow::Cow; + +use crate::fixer::{Fix, Message, PossibleFixes}; +use oxc_diagnostics::{OxcCode, OxcDiagnostic, Severity}; + +#[derive(Clone, Debug)] +pub struct SpanPositionMessage<'a> { + /// A brief suggestion message describing the fix. Will be shown in + /// editors via code actions. + message: Option>, + + start: SpanPosition, + end: SpanPosition, +} + +impl<'a> SpanPositionMessage<'a> { + pub fn new(start: SpanPosition, end: SpanPosition) -> Self { + Self { start, end, message: None } + } + + pub fn with_message(mut self, message: Option>) -> Self { + self.message = message; + self + } + + pub fn start(&self) -> &SpanPosition { + &self.start + } + + pub fn end(&self) -> &SpanPosition { + &self.end + } + + pub fn message(&self) -> Option<&Cow<'a, str>> { + self.message.as_ref() + } +} + +#[derive(Clone, Debug)] +pub struct SpanPosition { + pub line: u32, + pub character: u32, +} + +impl SpanPosition { + pub fn new(line: u32, column: u32) -> Self { + Self { line, character: column } + } +} + +pub fn offset_to_position(rope: &Rope, offset: u32, source_text: &str) -> SpanPosition { + let (line, column) = get_line_column(rope, offset, source_text); + SpanPosition::new(line, column) +} + +#[derive(Debug)] +pub struct MessageWithPosition<'a> { + pub message: Cow<'a, str>, + pub labels: Option>>, + pub help: Option>, + pub severity: Severity, + pub code: OxcCode, + pub url: Option>, + pub fixes: PossibleFixesWithPosition<'a>, +} + +impl From for MessageWithPosition<'_> { + fn from(from: OxcDiagnostic) -> Self { + Self { + message: from.message.clone(), + labels: None, + help: from.help.clone(), + severity: from.severity, + code: from.code.clone(), + url: from.url.clone(), + fixes: PossibleFixesWithPosition::None, + } + } +} + +// clippy: the source field is checked and assumed to be less than 4GB, and +// we assume that the fix offset will not exceed 2GB in either direction +#[expect(clippy::cast_possible_truncation)] +pub fn message_to_message_with_position<'a>( + message: &Message<'a>, + source_text: &str, + rope: &Rope, +) -> MessageWithPosition<'a> { + let labels = message.error.labels.as_ref().map(|labels| { + labels + .iter() + .map(|labeled_span| { + let offset = labeled_span.offset() as u32; + let start_position = offset_to_position(rope, offset, source_text); + let end_position = + offset_to_position(rope, offset + labeled_span.len() as u32, source_text); + let message = labeled_span.label().map(|label| Cow::Owned(label.to_string())); + + SpanPositionMessage::new(start_position, end_position).with_message(message) + }) + .collect::>() + }); + + MessageWithPosition { + message: message.error.message.clone(), + severity: message.error.severity, + help: message.error.help.clone(), + url: message.error.url.clone(), + code: message.error.code.clone(), + labels, + fixes: match &message.fixes { + PossibleFixes::None => PossibleFixesWithPosition::None, + PossibleFixes::Single(fix) => { + PossibleFixesWithPosition::Single(fix_to_fix_with_position(fix, rope, source_text)) + } + PossibleFixes::Multiple(fixes) => PossibleFixesWithPosition::Multiple( + fixes.iter().map(|fix| fix_to_fix_with_position(fix, rope, source_text)).collect(), + ), + }, + } +} + +#[derive(Debug)] +pub enum PossibleFixesWithPosition<'a> { + None, + Single(FixWithPosition<'a>), + Multiple(Vec>), +} + +#[derive(Debug)] +pub struct FixWithPosition<'a> { + pub content: Cow<'a, str>, + pub span: SpanPositionMessage<'a>, +} + +fn fix_to_fix_with_position<'a>( + fix: &Fix<'a>, + rope: &Rope, + source_text: &str, +) -> FixWithPosition<'a> { + let start_position = offset_to_position(rope, fix.span.start, source_text); + let end_position = offset_to_position(rope, fix.span.end, source_text); + FixWithPosition { + content: fix.content.clone(), + span: SpanPositionMessage::new(start_position, end_position) + .with_message(fix.message.as_ref().map(|label| Cow::Owned(label.to_string()))), + } +} + +#[cfg(test)] +mod test { + use oxc_data_structures::rope::Rope; + + use super::offset_to_position; + + #[test] + fn single_line() { + let source = "foo.bar!;"; + assert_position(source, 0, (0, 0)); + assert_position(source, 4, (0, 4)); + assert_position(source, 9, (0, 9)); + } + + #[test] + fn multi_line() { + let source = "console.log(\n foo.bar!\n);"; + assert_position(source, 0, (0, 0)); + assert_position(source, 12, (0, 12)); + assert_position(source, 13, (1, 0)); + assert_position(source, 23, (1, 10)); + assert_position(source, 24, (2, 0)); + assert_position(source, 26, (2, 2)); + } + + #[test] + fn multi_byte() { + let source = "let foo = \n '👍';"; + assert_position(source, 10, (0, 10)); + assert_position(source, 11, (1, 0)); + assert_position(source, 14, (1, 3)); + assert_position(source, 18, (1, 5)); + assert_position(source, 19, (1, 6)); + } + + #[test] + #[should_panic(expected = "out of bounds")] + fn out_of_bounds() { + offset_to_position(&Rope::from_str("foo"), 100, "foo"); + } + + fn assert_position(source: &str, offset: u32, expected: (u32, u32)) { + let position = offset_to_position(&Rope::from_str(source), offset, source); + assert_eq!(position.line, expected.0); + assert_eq!(position.character, expected.1); + } +} diff --git a/crates/oxc_linter/src/service/mod.rs b/crates/oxc_linter/src/service/mod.rs index c40feb47626b9..a81efadb0bc76 100644 --- a/crates/oxc_linter/src/service/mod.rs +++ b/crates/oxc_linter/src/service/mod.rs @@ -11,10 +11,6 @@ use crate::Linter; mod runtime; use runtime::Runtime; pub use runtime::RuntimeFileSystem; - -#[cfg(feature = "language_server")] -pub mod offset_to_position; - pub struct LintServiceOptions { /// Current working directory cwd: Box, diff --git a/crates/oxc_linter/src/service/offset_to_position.rs b/crates/oxc_linter/src/service/offset_to_position.rs deleted file mode 100644 index d3811b2d3d4e4..0000000000000 --- a/crates/oxc_linter/src/service/offset_to_position.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::borrow::Cow; - -use oxc_data_structures::rope::{Rope, get_line_column}; - -#[derive(Clone, Debug)] -pub struct SpanPositionMessage<'a> { - /// A brief suggestion message describing the fix. Will be shown in - /// editors via code actions. - message: Option>, - - start: SpanPosition, - end: SpanPosition, -} - -impl<'a> SpanPositionMessage<'a> { - pub fn new(start: SpanPosition, end: SpanPosition) -> Self { - Self { start, end, message: None } - } - - pub fn with_message(mut self, message: Option>) -> Self { - self.message = message; - self - } - - pub fn start(&self) -> &SpanPosition { - &self.start - } - - pub fn end(&self) -> &SpanPosition { - &self.end - } - - pub fn message(&self) -> Option<&Cow<'a, str>> { - self.message.as_ref() - } -} - -#[derive(Clone, Debug)] -pub struct SpanPosition { - pub line: u32, - pub character: u32, -} - -impl SpanPosition { - pub fn new(line: u32, column: u32) -> Self { - Self { line, character: column } - } -} - -pub fn offset_to_position(rope: &Rope, offset: u32, source_text: &str) -> SpanPosition { - let (line, column) = get_line_column(rope, offset, source_text); - SpanPosition::new(line, column) -} - -#[cfg(test)] -mod test { - use oxc_data_structures::rope::Rope; - - use super::offset_to_position; - - #[test] - fn single_line() { - let source = "foo.bar!;"; - assert_position(source, 0, (0, 0)); - assert_position(source, 4, (0, 4)); - assert_position(source, 9, (0, 9)); - } - - #[test] - fn multi_line() { - let source = "console.log(\n foo.bar!\n);"; - assert_position(source, 0, (0, 0)); - assert_position(source, 12, (0, 12)); - assert_position(source, 13, (1, 0)); - assert_position(source, 23, (1, 10)); - assert_position(source, 24, (2, 0)); - assert_position(source, 26, (2, 2)); - } - - #[test] - fn multi_byte() { - let source = "let foo = \n '👍';"; - assert_position(source, 10, (0, 10)); - assert_position(source, 11, (1, 0)); - assert_position(source, 14, (1, 3)); - assert_position(source, 18, (1, 5)); - assert_position(source, 19, (1, 6)); - } - - #[test] - #[should_panic(expected = "out of bounds")] - fn out_of_bounds() { - offset_to_position(&Rope::from_str("foo"), 100, "foo"); - } - - fn assert_position(source: &str, offset: u32, expected: (u32, u32)) { - let position = offset_to_position(&Rope::from_str(source), offset, source); - assert_eq!(position.line, expected.0); - assert_eq!(position.character, expected.1); - } -} diff --git a/crates/oxc_linter/src/service/runtime.rs b/crates/oxc_linter/src/service/runtime.rs index 66e97ce48ef2e..7c34602d68ed2 100644 --- a/crates/oxc_linter/src/service/runtime.rs +++ b/crates/oxc_linter/src/service/runtime.rs @@ -31,7 +31,7 @@ use crate::{ }; #[cfg(feature = "language_server")] -use crate::fixer::MessageWithPosition; +use crate::MessageWithPosition; use super::LintServiceOptions; @@ -679,7 +679,7 @@ impl Runtime { use oxc_data_structures::rope::Rope; - use crate::fixer::message_to_message_with_position; + use crate::lsp::message_to_message_with_position; // Wrap allocator in `MessageCloner` so can clone `Message`s into it let message_cloner = MessageCloner::new(allocator); diff --git a/crates/oxc_linter/src/tsgolint.rs b/crates/oxc_linter/src/tsgolint.rs index 72f64242045dc..9e94cfe966d64 100644 --- a/crates/oxc_linter/src/tsgolint.rs +++ b/crates/oxc_linter/src/tsgolint.rs @@ -16,7 +16,7 @@ use crate::fixer::{CompositeFix, Message, PossibleFixes}; use super::{AllowWarnDeny, ConfigStore, ResolvedLinterState, read_to_string}; #[cfg(feature = "language_server")] -use crate::{MessageWithPosition, fixer::message_to_message_with_position}; +use crate::lsp::{MessageWithPosition, message_to_message_with_position}; /// State required to initialize the `tsgolint` linter. #[derive(Debug, Clone)]