1use daml_parser::ast::{DiagnosticCategory, Module, Span as ParserSpan};
45use daml_parser::layout::resolve_layout;
46use daml_parser::lexer::{lex_with_trivia, LexError, Token, Trivia};
47use daml_parser::parse::parse_module;
48use std::sync::OnceLock;
49
50pub mod coordinate;
51
52pub use coordinate::{
53 ByteColumn, ByteLineCol, ByteOffset, CharColumn, CharLineCol, InvalidOneBasedCoordinate,
54 LineNumber, Utf16Offset, Utf16Range,
55};
56pub use text_size::{TextRange, TextSize};
57
58#[derive(Debug, Clone, PartialEq, Eq)]
65#[non_exhaustive]
66pub struct Diagnostic {
67 range: TextRange,
68 line: LineNumber,
69 column: CharColumn,
70 end_column: DiagnosticEndColumn,
71 message: String,
72 category: DiagnosticCategory,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum DiagnosticEndColumn {
83 SameLineEnd(CharColumn),
85 Multiline,
87 EmptySpan,
89}
90
91impl Diagnostic {
92 #[must_use]
94 pub const fn range(&self) -> TextRange {
95 self.range
96 }
97
98 #[must_use]
100 pub const fn line(&self) -> LineNumber {
101 self.line
102 }
103
104 #[must_use]
106 pub const fn column(&self) -> CharColumn {
107 self.column
108 }
109
110 #[must_use]
116 pub const fn end_column(&self) -> DiagnosticEndColumn {
117 self.end_column
118 }
119
120 #[must_use]
122 pub fn message(&self) -> &str {
123 &self.message
124 }
125
126 #[must_use]
128 pub const fn category(&self) -> DiagnosticCategory {
129 self.category
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct LineIndex {
142 source_len: usize,
143 line_start_bytes: Vec<usize>,
144 char_offset_by_byte: Vec<usize>,
145 utf16_offset_by_byte: Vec<usize>,
146}
147
148impl LineIndex {
149 #[must_use]
151 pub fn new(source: &str) -> Self {
152 let mut line_start_bytes = vec![0];
153 for (idx, byte) in source.bytes().enumerate() {
154 if byte == b'\n' {
155 line_start_bytes.push(idx + 1);
156 }
157 }
158
159 let mut char_offset_by_byte = vec![0; source.len() + 1];
160 let mut char_count = 0usize;
161 let mut prev = 0usize;
162 for (idx, ch) in source.char_indices() {
163 for slot in char_offset_by_byte.iter_mut().take(idx).skip(prev) {
164 *slot = char_count;
165 }
166 let char_end = idx + ch.len_utf8();
167 for slot in char_offset_by_byte.iter_mut().take(char_end).skip(idx) {
168 *slot = char_count;
169 }
170 char_count += 1;
171 prev = char_end;
172 }
173 for slot in char_offset_by_byte
174 .iter_mut()
175 .take(source.len() + 1)
176 .skip(prev)
177 {
178 *slot = char_count;
179 }
180
181 let mut utf16_offset_by_byte = vec![0; source.len() + 1];
182 let mut utf16 = 0usize;
183 let mut prev = 0usize;
184 for (idx, ch) in source.char_indices() {
185 for slot in utf16_offset_by_byte.iter_mut().take(idx).skip(prev) {
186 *slot = utf16;
187 }
188 let char_end = idx + ch.len_utf8();
189 for slot in utf16_offset_by_byte.iter_mut().take(char_end).skip(idx) {
190 *slot = utf16;
191 }
192 utf16 += ch.len_utf16();
193 prev = char_end;
194 }
195 for slot in utf16_offset_by_byte
196 .iter_mut()
197 .take(source.len() + 1)
198 .skip(prev)
199 {
200 *slot = utf16;
201 }
202
203 Self {
204 source_len: source.len(),
205 line_start_bytes,
206 char_offset_by_byte,
207 utf16_offset_by_byte,
208 }
209 }
210
211 #[must_use]
213 pub const fn source_len_bytes(&self) -> ByteOffset {
214 ByteOffset::new(self.source_len)
215 }
216
217 #[must_use]
219 pub fn line_col(&self, offset: TextSize) -> ByteLineCol {
220 let byte = usize::from(offset).min(self.source_len);
221 let line_idx = match self.line_start_bytes.binary_search(&byte) {
222 Ok(idx) => idx,
223 Err(idx) => idx.saturating_sub(1),
224 };
225 ByteLineCol {
226 line: LineNumber::new(line_idx + 1),
227 column: ByteColumn::new(byte - self.line_start_bytes[line_idx] + 1),
228 }
229 }
230
231 #[must_use]
235 pub fn char_line_col(&self, offset: TextSize) -> CharLineCol {
236 let byte = usize::from(offset).min(self.source_len);
237 let line_idx = match self.line_start_bytes.binary_search(&byte) {
238 Ok(idx) => idx,
239 Err(idx) => idx.saturating_sub(1),
240 };
241 let line_start = self.line_start_bytes[line_idx];
242 CharLineCol {
243 line: LineNumber::new(line_idx + 1),
244 column: CharColumn::new(
245 self.char_offset_by_byte[byte] - self.char_offset_by_byte[line_start] + 1,
246 ),
247 }
248 }
249
250 #[must_use = "handle invalid source coordinates before using the UTF-16 offset"]
261 pub fn utf16_col(
262 &self,
263 line: LineNumber,
264 byte_col: ByteColumn,
265 ) -> Result<Utf16Offset, CoordinateRangeError> {
266 let line_idx = line.get() - 1;
267 let Some(line_start) = self.line_start_bytes.get(line_idx).copied() else {
268 return Err(CoordinateRangeError {
269 line,
270 byte_column: byte_col,
271 source_line_count: LineNumber::new(self.line_start_bytes.len()),
272 max_byte_column: None,
273 kind: CoordinateRangeErrorKind::LineOutOfRange,
274 });
275 };
276
277 let line_end = self.line_end_byte(line_idx);
278 let max_byte_column = ByteColumn::new(line_end - line_start + 1);
279 let byte = byte_col
280 .get()
281 .checked_sub(1)
282 .and_then(|column_offset| line_start.checked_add(column_offset));
283 let Some(byte) = byte.filter(|byte| *byte <= line_end) else {
284 return Err(CoordinateRangeError {
285 line,
286 byte_column: byte_col,
287 source_line_count: LineNumber::new(self.line_start_bytes.len()),
288 max_byte_column: Some(max_byte_column),
289 kind: CoordinateRangeErrorKind::ColumnOutOfRange,
290 });
291 };
292
293 Ok(Utf16Offset::new(
294 self.utf16_offset_by_byte[byte] - self.utf16_offset_by_byte[line_start],
295 ))
296 }
297
298 #[must_use]
300 pub fn utf16_range(&self, range: TextRange) -> Utf16Range {
301 let start = usize::from(range.start()).min(self.source_len);
302 let end = usize::from(range.end()).min(self.source_len).max(start);
303 Utf16Range::new(
304 Utf16Offset::new(self.utf16_offset_by_byte[start]),
305 Utf16Offset::new(self.utf16_offset_by_byte[end]),
306 )
307 }
308
309 fn line_end_byte(&self, line_idx: usize) -> usize {
310 self.line_start_bytes
311 .get(line_idx + 1)
312 .map_or(self.source_len, |next_line_start| next_line_start - 1)
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318#[non_exhaustive]
319pub enum CoordinateRangeErrorKind {
320 LineOutOfRange,
322 ColumnOutOfRange,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq)]
328pub struct CoordinateRangeError {
329 line: LineNumber,
330 byte_column: ByteColumn,
331 source_line_count: LineNumber,
332 max_byte_column: Option<ByteColumn>,
333 kind: CoordinateRangeErrorKind,
334}
335
336impl CoordinateRangeError {
337 #[must_use]
339 pub const fn line(&self) -> LineNumber {
340 self.line
341 }
342
343 #[must_use]
345 pub const fn byte_column(&self) -> ByteColumn {
346 self.byte_column
347 }
348
349 #[must_use]
351 pub const fn source_line_count(&self) -> LineNumber {
352 self.source_line_count
353 }
354
355 #[must_use]
357 pub const fn max_byte_column(&self) -> Option<ByteColumn> {
358 self.max_byte_column
359 }
360
361 #[must_use]
363 pub const fn kind(&self) -> CoordinateRangeErrorKind {
364 self.kind
365 }
366}
367
368impl std::fmt::Display for CoordinateRangeError {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 match self.kind {
371 CoordinateRangeErrorKind::LineOutOfRange => write!(
372 f,
373 "line {} is outside source line range 1..={}",
374 self.line, self.source_line_count
375 ),
376 CoordinateRangeErrorKind::ColumnOutOfRange => {
377 if let Some(max_byte_column) = self.max_byte_column {
378 write!(
379 f,
380 "byte column {} is outside valid range 1..={} on line {}",
381 self.byte_column, max_byte_column, self.line
382 )
383 } else {
384 write!(
385 f,
386 "byte column {} is outside the valid range on line {}",
387 self.byte_column, self.line
388 )
389 }
390 }
391 }
392 }
393}
394
395impl std::error::Error for CoordinateRangeError {}
396
397#[derive(Debug)]
401pub struct SourceTokens {
402 tokens: Vec<Token>,
403 trivia: Vec<Trivia>,
404 lex_errors: Vec<LexError>,
405 laid_out_tokens: OnceLock<Vec<Token>>,
406}
407
408impl Clone for SourceTokens {
409 fn clone(&self) -> Self {
410 let cloned = Self {
411 tokens: self.tokens.clone(),
412 trivia: self.trivia.clone(),
413 lex_errors: self.lex_errors.clone(),
414 laid_out_tokens: OnceLock::new(),
415 };
416 if let Some(laid_out_tokens) = self.laid_out_tokens.get() {
417 let _ = cloned.laid_out_tokens.set(laid_out_tokens.clone());
418 }
419 cloned
420 }
421}
422
423impl PartialEq for SourceTokens {
424 fn eq(&self, other: &Self) -> bool {
425 self.tokens == other.tokens
426 && self.trivia == other.trivia
427 && self.lex_errors == other.lex_errors
428 }
429}
430
431impl Eq for SourceTokens {}
432
433impl SourceTokens {
434 #[must_use]
436 pub fn lex(source: &str) -> Self {
437 let lexed = lex_with_trivia(source);
438 Self {
439 tokens: lexed.tokens,
440 trivia: lexed.trivia,
441 lex_errors: lexed.errors,
442 laid_out_tokens: OnceLock::new(),
443 }
444 }
445
446 #[must_use]
448 pub fn tokens(&self) -> &[Token] {
449 &self.tokens
450 }
451
452 #[must_use]
454 pub fn trivia(&self) -> &[Trivia] {
455 &self.trivia
456 }
457
458 #[must_use]
460 pub fn lex_errors(&self) -> &[LexError] {
461 &self.lex_errors
462 }
463
464 #[must_use]
466 pub fn laid_out_tokens(&self) -> &[Token] {
467 self.laid_out_tokens
468 .get_or_init(|| resolve_layout(self.tokens.clone()))
469 }
470}
471
472#[derive(Debug)]
474pub struct SourceFile {
475 source: String,
476 module: Module,
477 diagnostics: Vec<Diagnostic>,
478 line_index: LineIndex,
479 tokens: OnceLock<SourceTokens>,
480}
481
482impl Clone for SourceFile {
483 fn clone(&self) -> Self {
484 let cloned = Self {
485 source: self.source.clone(),
486 module: self.module.clone(),
487 diagnostics: self.diagnostics.clone(),
488 line_index: self.line_index.clone(),
489 tokens: OnceLock::new(),
490 };
491 if let Some(tokens) = self.tokens.get() {
492 let _ = cloned.tokens.set(tokens.clone());
493 }
494 cloned
495 }
496}
497
498impl PartialEq for SourceFile {
499 fn eq(&self, other: &Self) -> bool {
500 self.source == other.source
501 && self.module == other.module
502 && self.diagnostics == other.diagnostics
503 && self.line_index == other.line_index
504 }
505}
506
507impl Eq for SourceFile {}
508
509impl SourceFile {
510 #[must_use]
520 pub fn parse(source: &str) -> Self {
521 let parsed = parse_module(source);
522 let line_index = LineIndex::new(source);
523 let diagnostics = parsed
524 .diagnostics
525 .into_iter()
526 .map(|diagnostic| {
527 let range = try_parser_span_to_text_range(source, diagnostic.span)
528 .expect("parser span in diagnostic must map to source bytes");
529 let start = range.start();
530 let start_pos = line_index.char_line_col(start);
531 let end_column = diagnostic_end_column(source, range, start_pos.column);
532 Diagnostic {
533 range,
534 line: start_pos.line,
535 column: start_pos.column,
536 end_column,
537 message: diagnostic.message,
538 category: diagnostic.category,
539 }
540 })
541 .collect();
542
543 Self {
544 source: source.to_string(),
545 module: parsed.module,
546 diagnostics,
547 line_index,
548 tokens: OnceLock::new(),
549 }
550 }
551
552 #[must_use]
554 pub fn source(&self) -> &str {
555 &self.source
556 }
557
558 #[must_use]
560 pub const fn module(&self) -> &Module {
561 &self.module
562 }
563
564 #[must_use]
566 pub fn diagnostics(&self) -> &[Diagnostic] {
567 &self.diagnostics
568 }
569
570 #[must_use]
572 pub const fn line_index(&self) -> &LineIndex {
573 &self.line_index
574 }
575
576 #[must_use]
578 pub fn tokens(&self) -> &[Token] {
579 self.source_tokens().tokens()
580 }
581
582 #[must_use]
584 pub fn trivia(&self) -> &[Trivia] {
585 self.source_tokens().trivia()
586 }
587
588 #[must_use]
590 pub fn laid_out_tokens(&self) -> &[Token] {
591 self.source_tokens().laid_out_tokens()
592 }
593
594 #[must_use]
604 pub fn parser_span_to_text_range(&self, span: ParserSpan) -> TextRange {
605 self.try_parser_span_to_text_range(span)
606 .expect("parser span must map to a valid UTF-8 range in source")
607 }
608
609 #[must_use = "handle invalid span offsets before using the range"]
622 pub fn try_parser_span_to_text_range(
623 &self,
624 span: ParserSpan,
625 ) -> Result<TextRange, ParserSpanToTextRangeError> {
626 try_parser_span_to_text_range(&self.source, span)
627 }
628
629 fn source_tokens(&self) -> &SourceTokens {
630 self.tokens.get_or_init(|| SourceTokens::lex(&self.source))
631 }
632}
633
634fn diagnostic_end_column(
635 source: &str,
636 range: TextRange,
637 start_column: CharColumn,
638) -> DiagnosticEndColumn {
639 let span_text = source
640 .get(usize::from(range.start())..usize::from(range.end()))
641 .expect("validated parser span should slice source");
642 if span_text.is_empty() {
643 DiagnosticEndColumn::EmptySpan
644 } else if span_text.contains('\n') {
645 DiagnosticEndColumn::Multiline
646 } else {
647 DiagnosticEndColumn::SameLineEnd(CharColumn::new(
648 start_column.get() + span_text.chars().count(),
649 ))
650 }
651}
652
653#[must_use]
663pub fn parser_span_to_text_range(source: &str, span: ParserSpan) -> TextRange {
664 try_parser_span_to_text_range(source, span)
665 .expect("parser span must map to a valid UTF-8 range")
666}
667
668#[derive(Debug, Clone, Copy, PartialEq, Eq)]
670#[non_exhaustive]
671pub enum ParserSpanToTextRangeErrorKind {
672 OutOfBounds,
674 InvertedSpan,
676 NonUtf8Boundary,
678 TextSizeOverflow,
680}
681
682impl std::fmt::Display for ParserSpanToTextRangeErrorKind {
683 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684 match self {
685 Self::OutOfBounds => f.write_str("out of bounds"),
686 Self::InvertedSpan => f.write_str("start after end"),
687 Self::NonUtf8Boundary => f.write_str("not on a UTF-8 boundary"),
688 Self::TextSizeOverflow => f.write_str("text-size overflow"),
689 }
690 }
691}
692
693#[derive(Debug, Clone, PartialEq, Eq)]
695pub struct ParserSpanToTextRangeError {
696 source_len: usize,
697 span_start: ByteOffset,
698 span_end: ByteOffset,
699 kind: ParserSpanToTextRangeErrorKind,
700}
701
702impl ParserSpanToTextRangeError {
703 #[must_use]
705 pub const fn source_len_bytes(&self) -> ByteOffset {
706 ByteOffset::new(self.source_len)
707 }
708
709 #[must_use]
711 pub const fn span_start(&self) -> ByteOffset {
712 self.span_start
713 }
714
715 #[must_use]
717 pub const fn span_end(&self) -> ByteOffset {
718 self.span_end
719 }
720
721 #[must_use]
723 pub const fn kind(&self) -> ParserSpanToTextRangeErrorKind {
724 self.kind
725 }
726}
727
728impl std::fmt::Display for ParserSpanToTextRangeError {
729 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
730 match self.kind {
731 ParserSpanToTextRangeErrorKind::OutOfBounds
732 | ParserSpanToTextRangeErrorKind::InvertedSpan
733 | ParserSpanToTextRangeErrorKind::NonUtf8Boundary => write!(
734 f,
735 "parser span [{}, {}) is invalid: {} for source length {}",
736 self.span_start.get(),
737 self.span_end.get(),
738 self.kind,
739 self.source_len
740 ),
741 ParserSpanToTextRangeErrorKind::TextSizeOverflow => write!(
742 f,
743 "parser span [{}, {}) is invalid: {}; endpoints cannot be represented as text-size values",
744 self.span_start.get(),
745 self.span_end.get(),
746 self.kind
747 ),
748 }
749 }
750}
751
752impl std::error::Error for ParserSpanToTextRangeError {}
753
754#[must_use = "handle invalid span offsets before converting"]
765pub fn try_parser_span_to_text_range(
766 source: &str,
767 span: ParserSpan,
768) -> Result<TextRange, ParserSpanToTextRangeError> {
769 let source_len = source.len();
770 if span.start_usize() > source_len || span.end_usize() > source_len {
771 return Err(ParserSpanToTextRangeError {
772 source_len,
773 span_start: ByteOffset::new(span.start_usize()),
774 span_end: ByteOffset::new(span.end_usize()),
775 kind: ParserSpanToTextRangeErrorKind::OutOfBounds,
776 });
777 }
778 if span.start > span.end {
779 return Err(ParserSpanToTextRangeError {
780 source_len,
781 span_start: ByteOffset::new(span.start_usize()),
782 span_end: ByteOffset::new(span.end_usize()),
783 kind: ParserSpanToTextRangeErrorKind::InvertedSpan,
784 });
785 }
786 if !source.is_char_boundary(span.start_usize()) || !source.is_char_boundary(span.end_usize()) {
787 return Err(ParserSpanToTextRangeError {
788 source_len,
789 span_start: ByteOffset::new(span.start_usize()),
790 span_end: ByteOffset::new(span.end_usize()),
791 kind: ParserSpanToTextRangeErrorKind::NonUtf8Boundary,
792 });
793 }
794 Ok(TextRange::new(
795 TextSize::try_from(span.start_usize()).map_err(|_| ParserSpanToTextRangeError {
796 source_len,
797 span_start: ByteOffset::new(span.start_usize()),
798 span_end: ByteOffset::new(span.end_usize()),
799 kind: ParserSpanToTextRangeErrorKind::TextSizeOverflow,
800 })?,
801 TextSize::try_from(span.end_usize()).map_err(|_| ParserSpanToTextRangeError {
802 source_len,
803 span_start: ByteOffset::new(span.start_usize()),
804 span_end: ByteOffset::new(span.end_usize()),
805 kind: ParserSpanToTextRangeErrorKind::TextSizeOverflow,
806 })?,
807 ))
808}
809
810#[doc = include_str!("../README.md")]
811#[cfg(doctest)]
812#[doc(hidden)]
813pub struct ReadmeDoctests;
814
815#[cfg(test)]
818#[allow(clippy::unwrap_used)]
819mod tests {
820 use super::*;
821
822 #[test]
823 fn maps_empty_source_to_first_line() {
824 let index = LineIndex::new("");
825 assert_eq!(index.source_len_bytes(), ByteOffset::new(0));
826
827 assert_eq!(
828 index.line_col(0.into()),
829 ByteLineCol {
830 line: LineNumber::new(1),
831 column: ByteColumn::new(1),
832 }
833 );
834 assert_eq!(
835 index.utf16_range(TextRange::empty(0.into())),
836 Utf16Range::new(Utf16Offset::new(0), Utf16Offset::new(0))
837 );
838 }
839
840 #[test]
841 fn maps_ascii_byte_lines() {
842 let source = "module M where\nfoo = 1\n";
843 let index = LineIndex::new(source);
844
845 assert_eq!(
846 index.line_col(15.into()),
847 ByteLineCol {
848 line: LineNumber::new(2),
849 column: ByteColumn::new(1),
850 }
851 );
852 assert_eq!(
853 index.utf16_col(LineNumber::new(2), ByteColumn::new(4)),
854 Ok(Utf16Offset::new(3))
855 );
856 }
857
858 #[test]
859 fn maps_utf8_and_utf16_offsets() {
860 let source = "a😀b\nz";
861 let index = LineIndex::new(source);
862
863 assert_eq!(
864 index.utf16_range(TextRange::new(0.into(), 6.into())),
865 Utf16Range::new(Utf16Offset::new(0), Utf16Offset::new(4))
866 );
867 assert_eq!(
868 index.utf16_col(LineNumber::new(1), ByteColumn::new(6)),
869 Ok(Utf16Offset::new(3))
870 );
871 assert_eq!(
872 index.char_line_col(5.into()),
873 CharLineCol {
874 line: LineNumber::new(1),
875 column: CharColumn::new(3),
876 }
877 );
878 }
879
880 #[test]
881 fn char_line_col_snaps_to_previous_utf8_boundary() {
882 let source = "a😀b";
883 let index = LineIndex::new(source);
884
885 assert_eq!(
887 index.char_line_col(3.into()),
888 CharLineCol {
889 line: LineNumber::new(1),
890 column: CharColumn::new(2),
891 }
892 );
893 }
894
895 #[test]
896 fn preserves_trailing_newline_line_start() {
897 let index = LineIndex::new("a\n");
898
899 assert_eq!(
900 index.line_col(2.into()),
901 ByteLineCol {
902 line: LineNumber::new(2),
903 column: ByteColumn::new(1),
904 }
905 );
906 }
907
908 #[test]
909 fn treats_crlf_as_bytes_without_normalization() {
910 let index = LineIndex::new("a\r\nb");
911
912 assert_eq!(
913 index.line_col(3.into()),
914 ByteLineCol {
915 line: LineNumber::new(2),
916 column: ByteColumn::new(1),
917 }
918 );
919 }
920
921 #[test]
922 fn clamps_ranges_to_source_end() {
923 let index = LineIndex::new("abc");
924 let range = TextRange::new(1.into(), 99.into());
925
926 assert_eq!(
927 index.utf16_range(range),
928 Utf16Range::new(Utf16Offset::new(1), Utf16Offset::new(3))
929 );
930 }
931
932 #[test]
933 fn utf16_col_reports_line_past_source() {
934 let index = LineIndex::new("abc\n");
935
936 let err = index
937 .utf16_col(LineNumber::new(3), ByteColumn::new(1))
938 .unwrap_err();
939
940 assert_eq!(err.kind(), CoordinateRangeErrorKind::LineOutOfRange);
941 assert_eq!(err.line(), LineNumber::new(3));
942 assert_eq!(err.byte_column(), ByteColumn::new(1));
943 assert_eq!(err.source_line_count(), LineNumber::new(2));
944 assert_eq!(err.max_byte_column(), None);
945 assert_eq!(err.to_string(), "line 3 is outside source line range 1..=2");
946 }
947
948 #[test]
949 fn utf16_col_reports_column_past_line_without_clamping_to_eof() {
950 let index = LineIndex::new("abc\nz");
951
952 let err = index
953 .utf16_col(LineNumber::new(1), ByteColumn::new(5))
954 .unwrap_err();
955
956 assert_eq!(err.kind(), CoordinateRangeErrorKind::ColumnOutOfRange);
957 assert_eq!(err.line(), LineNumber::new(1));
958 assert_eq!(err.byte_column(), ByteColumn::new(5));
959 assert_eq!(err.source_line_count(), LineNumber::new(2));
960 assert_eq!(err.max_byte_column(), Some(ByteColumn::new(4)));
961 assert_eq!(
962 err.to_string(),
963 "byte column 5 is outside valid range 1..=4 on line 1"
964 );
965 }
966
967 #[test]
968 fn diagnostic_end_column_names_same_line_multiline_and_empty_spans() {
969 let source = "abc\ndef";
970
971 assert_eq!(
972 diagnostic_end_column(
973 source,
974 TextRange::new(1.into(), 3.into()),
975 CharColumn::new(2)
976 ),
977 DiagnosticEndColumn::SameLineEnd(CharColumn::new(4))
978 );
979 assert_eq!(
980 diagnostic_end_column(
981 source,
982 TextRange::new(1.into(), 5.into()),
983 CharColumn::new(2)
984 ),
985 DiagnosticEndColumn::Multiline
986 );
987 assert_eq!(
988 diagnostic_end_column(source, TextRange::empty(1.into()), CharColumn::new(2)),
989 DiagnosticEndColumn::EmptySpan
990 );
991 }
992
993 #[test]
994 fn byte_and_char_line_col_columns_differ_for_multibyte_utf8() {
995 let source = "😀\n";
996 let index = LineIndex::new(source);
997 let offset = TextSize::from(4);
998
999 let byte_pos = index.line_col(offset);
1000 let char_pos = index.char_line_col(offset);
1001
1002 assert_eq!(byte_pos.line, char_pos.line);
1003 assert_ne!(byte_pos.column.get(), char_pos.column.get());
1004 assert_eq!(byte_pos.column, ByteColumn::new(5));
1005 assert_eq!(char_pos.column, CharColumn::new(2));
1006 }
1007
1008 #[test]
1009 fn parser_span_text_size_overflow_display_includes_kind() {
1010 let text_size_overflow_start = usize::try_from(u32::MAX).unwrap() + 1;
1011 let text_size_overflow_end = usize::try_from(u32::MAX).unwrap() + 2;
1012 let err = ParserSpanToTextRangeError {
1013 source_len: usize::MAX,
1014 span_start: ByteOffset::new(text_size_overflow_start),
1015 span_end: ByteOffset::new(text_size_overflow_end),
1016 kind: ParserSpanToTextRangeErrorKind::TextSizeOverflow,
1017 };
1018
1019 assert_eq!(err.kind().to_string(), "text-size overflow");
1020 assert_eq!(
1021 err.to_string(),
1022 format!(
1023 "parser span [{text_size_overflow_start}, {text_size_overflow_end}) is invalid: text-size overflow; endpoints cannot be represented as text-size values"
1024 )
1025 );
1026 }
1027}