|
| 1 | +use oxc_data_structures::rope::{Rope, get_line_column}; |
| 2 | +use std::borrow::Cow; |
| 3 | + |
| 4 | +use crate::fixer::{Fix, Message, PossibleFixes}; |
| 5 | +use oxc_diagnostics::{OxcCode, OxcDiagnostic, Severity}; |
| 6 | + |
| 7 | +#[derive(Clone, Debug)] |
| 8 | +pub struct SpanPositionMessage<'a> { |
| 9 | + /// A brief suggestion message describing the fix. Will be shown in |
| 10 | + /// editors via code actions. |
| 11 | + message: Option<Cow<'a, str>>, |
| 12 | + |
| 13 | + start: SpanPosition, |
| 14 | + end: SpanPosition, |
| 15 | +} |
| 16 | + |
| 17 | +impl<'a> SpanPositionMessage<'a> { |
| 18 | + pub fn new(start: SpanPosition, end: SpanPosition) -> Self { |
| 19 | + Self { start, end, message: None } |
| 20 | + } |
| 21 | + |
| 22 | + pub fn with_message(mut self, message: Option<Cow<'a, str>>) -> Self { |
| 23 | + self.message = message; |
| 24 | + self |
| 25 | + } |
| 26 | + |
| 27 | + pub fn start(&self) -> &SpanPosition { |
| 28 | + &self.start |
| 29 | + } |
| 30 | + |
| 31 | + pub fn end(&self) -> &SpanPosition { |
| 32 | + &self.end |
| 33 | + } |
| 34 | + |
| 35 | + pub fn message(&self) -> Option<&Cow<'a, str>> { |
| 36 | + self.message.as_ref() |
| 37 | + } |
| 38 | +} |
| 39 | + |
| 40 | +#[derive(Clone, Debug)] |
| 41 | +pub struct SpanPosition { |
| 42 | + pub line: u32, |
| 43 | + pub character: u32, |
| 44 | +} |
| 45 | + |
| 46 | +impl SpanPosition { |
| 47 | + pub fn new(line: u32, column: u32) -> Self { |
| 48 | + Self { line, character: column } |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +pub fn offset_to_position(rope: &Rope, offset: u32, source_text: &str) -> SpanPosition { |
| 53 | + let (line, column) = get_line_column(rope, offset, source_text); |
| 54 | + SpanPosition::new(line, column) |
| 55 | +} |
| 56 | + |
| 57 | +#[derive(Debug)] |
| 58 | +pub struct MessageWithPosition<'a> { |
| 59 | + pub message: Cow<'a, str>, |
| 60 | + pub labels: Option<Vec<SpanPositionMessage<'a>>>, |
| 61 | + pub help: Option<Cow<'a, str>>, |
| 62 | + pub severity: Severity, |
| 63 | + pub code: OxcCode, |
| 64 | + pub url: Option<Cow<'a, str>>, |
| 65 | + pub fixes: PossibleFixesWithPosition<'a>, |
| 66 | +} |
| 67 | + |
| 68 | +impl From<OxcDiagnostic> for MessageWithPosition<'_> { |
| 69 | + fn from(from: OxcDiagnostic) -> Self { |
| 70 | + Self { |
| 71 | + message: from.message.clone(), |
| 72 | + labels: None, |
| 73 | + help: from.help.clone(), |
| 74 | + severity: from.severity, |
| 75 | + code: from.code.clone(), |
| 76 | + url: from.url.clone(), |
| 77 | + fixes: PossibleFixesWithPosition::None, |
| 78 | + } |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +// clippy: the source field is checked and assumed to be less than 4GB, and |
| 83 | +// we assume that the fix offset will not exceed 2GB in either direction |
| 84 | +#[expect(clippy::cast_possible_truncation)] |
| 85 | +pub fn message_to_message_with_position<'a>( |
| 86 | + message: &Message<'a>, |
| 87 | + source_text: &str, |
| 88 | + rope: &Rope, |
| 89 | +) -> MessageWithPosition<'a> { |
| 90 | + let labels = message.error.labels.as_ref().map(|labels| { |
| 91 | + labels |
| 92 | + .iter() |
| 93 | + .map(|labeled_span| { |
| 94 | + let offset = labeled_span.offset() as u32; |
| 95 | + let start_position = offset_to_position(rope, offset, source_text); |
| 96 | + let end_position = |
| 97 | + offset_to_position(rope, offset + labeled_span.len() as u32, source_text); |
| 98 | + let message = labeled_span.label().map(|label| Cow::Owned(label.to_string())); |
| 99 | + |
| 100 | + SpanPositionMessage::new(start_position, end_position).with_message(message) |
| 101 | + }) |
| 102 | + .collect::<Vec<_>>() |
| 103 | + }); |
| 104 | + |
| 105 | + MessageWithPosition { |
| 106 | + message: message.error.message.clone(), |
| 107 | + severity: message.error.severity, |
| 108 | + help: message.error.help.clone(), |
| 109 | + url: message.error.url.clone(), |
| 110 | + code: message.error.code.clone(), |
| 111 | + labels, |
| 112 | + fixes: match &message.fixes { |
| 113 | + PossibleFixes::None => PossibleFixesWithPosition::None, |
| 114 | + PossibleFixes::Single(fix) => { |
| 115 | + PossibleFixesWithPosition::Single(fix_to_fix_with_position(fix, rope, source_text)) |
| 116 | + } |
| 117 | + PossibleFixes::Multiple(fixes) => PossibleFixesWithPosition::Multiple( |
| 118 | + fixes.iter().map(|fix| fix_to_fix_with_position(fix, rope, source_text)).collect(), |
| 119 | + ), |
| 120 | + }, |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +#[derive(Debug)] |
| 125 | +pub enum PossibleFixesWithPosition<'a> { |
| 126 | + None, |
| 127 | + Single(FixWithPosition<'a>), |
| 128 | + Multiple(Vec<FixWithPosition<'a>>), |
| 129 | +} |
| 130 | + |
| 131 | +#[derive(Debug)] |
| 132 | +pub struct FixWithPosition<'a> { |
| 133 | + pub content: Cow<'a, str>, |
| 134 | + pub span: SpanPositionMessage<'a>, |
| 135 | +} |
| 136 | + |
| 137 | +fn fix_to_fix_with_position<'a>( |
| 138 | + fix: &Fix<'a>, |
| 139 | + rope: &Rope, |
| 140 | + source_text: &str, |
| 141 | +) -> FixWithPosition<'a> { |
| 142 | + let start_position = offset_to_position(rope, fix.span.start, source_text); |
| 143 | + let end_position = offset_to_position(rope, fix.span.end, source_text); |
| 144 | + FixWithPosition { |
| 145 | + content: fix.content.clone(), |
| 146 | + span: SpanPositionMessage::new(start_position, end_position) |
| 147 | + .with_message(fix.message.as_ref().map(|label| Cow::Owned(label.to_string()))), |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +#[cfg(test)] |
| 152 | +mod test { |
| 153 | + use oxc_data_structures::rope::Rope; |
| 154 | + |
| 155 | + use super::offset_to_position; |
| 156 | + |
| 157 | + #[test] |
| 158 | + fn single_line() { |
| 159 | + let source = "foo.bar!;"; |
| 160 | + assert_position(source, 0, (0, 0)); |
| 161 | + assert_position(source, 4, (0, 4)); |
| 162 | + assert_position(source, 9, (0, 9)); |
| 163 | + } |
| 164 | + |
| 165 | + #[test] |
| 166 | + fn multi_line() { |
| 167 | + let source = "console.log(\n foo.bar!\n);"; |
| 168 | + assert_position(source, 0, (0, 0)); |
| 169 | + assert_position(source, 12, (0, 12)); |
| 170 | + assert_position(source, 13, (1, 0)); |
| 171 | + assert_position(source, 23, (1, 10)); |
| 172 | + assert_position(source, 24, (2, 0)); |
| 173 | + assert_position(source, 26, (2, 2)); |
| 174 | + } |
| 175 | + |
| 176 | + #[test] |
| 177 | + fn multi_byte() { |
| 178 | + let source = "let foo = \n '👍';"; |
| 179 | + assert_position(source, 10, (0, 10)); |
| 180 | + assert_position(source, 11, (1, 0)); |
| 181 | + assert_position(source, 14, (1, 3)); |
| 182 | + assert_position(source, 18, (1, 5)); |
| 183 | + assert_position(source, 19, (1, 6)); |
| 184 | + } |
| 185 | + |
| 186 | + #[test] |
| 187 | + #[should_panic(expected = "out of bounds")] |
| 188 | + fn out_of_bounds() { |
| 189 | + offset_to_position(&Rope::from_str("foo"), 100, "foo"); |
| 190 | + } |
| 191 | + |
| 192 | + fn assert_position(source: &str, offset: u32, expected: (u32, u32)) { |
| 193 | + let position = offset_to_position(&Rope::from_str(source), offset, source); |
| 194 | + assert_eq!(position.line, expected.0); |
| 195 | + assert_eq!(position.character, expected.1); |
| 196 | + } |
| 197 | +} |
0 commit comments