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
18 changes: 0 additions & 18 deletions crates/oxc_linter/src/fixer/fix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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>;

Expand Down Expand Up @@ -392,14 +382,6 @@ impl PossibleFixes<'_> {
}
}

#[cfg(feature = "language_server")]
#[derive(Debug)]
pub enum PossibleFixesWithPosition<'a> {
None,
Single(FixWithPosition<'a>),
Multiple(Vec<FixWithPosition<'a>>),
}

// 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
Expand Down
98 changes: 0 additions & 98 deletions crates/oxc_linter/src/fixer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Vec<SpanPositionMessage<'a>>>,
pub help: Option<Cow<'a, str>>,
pub severity: Severity,
pub code: OxcCode,
pub url: Option<Cow<'a, str>>,
pub fixes: PossibleFixesWithPosition<'a>,
}

#[cfg(feature = "language_server")]
impl From<OxcDiagnostic> 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::<Vec<_>>()
});

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>;

Expand Down
4 changes: 3 additions & 1 deletion crates/oxc_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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]
Expand Down
197 changes: 197 additions & 0 deletions crates/oxc_linter/src/lsp.rs
Original file line number Diff line number Diff line change
@@ -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<Cow<'a, str>>,

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<Cow<'a, str>>) -> 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<Vec<SpanPositionMessage<'a>>>,
pub help: Option<Cow<'a, str>>,
pub severity: Severity,
pub code: OxcCode,
pub url: Option<Cow<'a, str>>,
pub fixes: PossibleFixesWithPosition<'a>,
}

impl From<OxcDiagnostic> 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::<Vec<_>>()
});

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<FixWithPosition<'a>>),
}

#[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);
}
}
4 changes: 0 additions & 4 deletions crates/oxc_linter/src/service/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path>,
Expand Down
Loading
Loading