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