1pub mod analyze;
2pub mod code_gen;
3pub mod module;
4pub mod resolve;
5
6use std::{
7 cmp::min,
8 fmt::{Display, Formatter},
9};
10
11use anyhow::{Result, anyhow};
12use auto_hash_map::AutoSet;
13use bincode::{Decode, Encode};
14use serde::{Deserialize, Serialize};
15use turbo_rcstr::RcStr;
16use turbo_tasks::{
17 CollectiblesSource, IntoTraitRef, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc,
18 TaskInput, TransientValue, TryFlatJoinIterExt, TryJoinIterExt, Upcast, ValueDefault,
19 ValueToString, Vc, emit, trace::TraceRawVcs,
20};
21use turbo_tasks_fs::{FileContent, FileLine, FileLinesContent, FileSystem, FileSystemPath};
22use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
23
24use crate::{
25 asset::{Asset, AssetContent},
26 condition::ContextCondition,
27 ident::{AssetIdent, Layer},
28 source::Source,
29 source_map::{GenerateSourceMap, SourceMap, TokenWithSource},
30 source_pos::SourcePos,
31};
32
33#[turbo_tasks::value(shared)]
34#[derive(
35 PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput, Serialize, Deserialize,
36)]
37#[serde(rename_all = "camelCase")]
38pub enum IssueSeverity {
39 Bug,
40 Fatal,
41 Error,
42 Warning,
43 Hint,
44 Note,
45 Suggestion,
46 Info,
47}
48
49impl IssueSeverity {
50 pub fn as_str(&self) -> &'static str {
51 match self {
52 IssueSeverity::Bug => "bug",
53 IssueSeverity::Fatal => "fatal",
54 IssueSeverity::Error => "error",
55 IssueSeverity::Warning => "warning",
56 IssueSeverity::Hint => "hint",
57 IssueSeverity::Note => "note",
58 IssueSeverity::Suggestion => "suggestion",
59 IssueSeverity::Info => "info",
60 }
61 }
62
63 pub fn as_help_str(&self) -> &'static str {
64 match self {
65 IssueSeverity::Bug => "bug in implementation",
66 IssueSeverity::Fatal => "unrecoverable problem",
67 IssueSeverity::Error => "problem that cause a broken result",
68 IssueSeverity::Warning => "problem should be addressed in short term",
69 IssueSeverity::Hint => "idea for improvement",
70 IssueSeverity::Note => "detail that is worth mentioning",
71 IssueSeverity::Suggestion => "change proposal for improvement",
72 IssueSeverity::Info => "detail that is worth telling",
73 }
74 }
75}
76
77impl Display for IssueSeverity {
78 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
79 f.write_str(self.as_str())
80 }
81}
82
83#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
87#[turbo_tasks::value(shared)]
88pub enum StyledString {
89 Line(Vec<StyledString>),
93 Stack(Vec<StyledString>),
96 Text(RcStr),
98 Code(RcStr),
101 Strong(RcStr),
103}
104
105impl StyledString {
106 pub fn to_unstyled_string(&self) -> String {
107 match self {
108 StyledString::Line(items) => items
109 .iter()
110 .map(|item| item.to_unstyled_string())
111 .collect::<Vec<_>>()
112 .join(""),
113 StyledString::Stack(items) => items
114 .iter()
115 .map(|item| item.to_unstyled_string())
116 .collect::<Vec<_>>()
117 .join("\n"),
118 StyledString::Text(s) | StyledString::Code(s) | StyledString::Strong(s) => {
119 s.to_string()
120 }
121 }
122 }
123}
124
125#[turbo_tasks::value_trait]
126pub trait Issue {
127 fn severity(&self) -> IssueSeverity {
130 IssueSeverity::Error
131 }
132
133 #[turbo_tasks::function]
136 fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
137
138 #[turbo_tasks::function]
141 fn stage(self: Vc<Self>) -> Vc<IssueStage>;
142
143 #[turbo_tasks::function]
148 fn title(self: Vc<Self>) -> Vc<StyledString>;
149
150 #[turbo_tasks::function]
154 fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
155 Vc::cell(None)
156 }
157
158 #[turbo_tasks::function]
162 fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
163 Vc::cell(None)
164 }
165
166 #[turbo_tasks::function]
169 fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
170 Vc::<RcStr>::default()
171 }
172
173 #[turbo_tasks::function]
177 fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
178 Vc::cell(None)
179 }
180}
181
182#[turbo_tasks::value_trait]
184pub trait ImportTracer {
185 #[turbo_tasks::function]
186 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
187}
188
189#[turbo_tasks::value]
190#[derive(Debug)]
191pub struct DelegatingImportTracer {
192 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
193}
194
195impl DelegatingImportTracer {
196 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
197 Ok(self
198 .delegates
199 .iter()
200 .map(|d| d.get_traces(path.clone()))
201 .try_join()
202 .await?
203 .iter()
204 .flat_map(|v| v.0.iter().cloned())
205 .collect())
206 }
207}
208
209pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
210
211#[turbo_tasks::value(shared)]
212pub struct ImportTraces(pub Vec<ImportTrace>);
213
214#[turbo_tasks::value_impl]
215impl ValueDefault for ImportTraces {
216 #[turbo_tasks::function]
217 fn value_default() -> Vc<Self> {
218 Self::cell(ImportTraces(vec![]))
219 }
220}
221
222pub trait IssueExt {
223 fn emit(self);
224}
225
226impl<T> IssueExt for ResolvedVc<T>
227where
228 T: Upcast<Box<dyn Issue>>,
229{
230 fn emit(self) {
231 emit(ResolvedVc::upcast_non_strict::<Box<dyn Issue>>(self));
232 }
233}
234
235#[turbo_tasks::value(transparent)]
236pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
237
238#[derive(TaskInput, Hash, Eq, PartialEq, Copy, Clone, Encode, Decode, TraceRawVcs, Debug)]
239pub struct IssueFilter {
240 severity: IssueSeverity,
242 foreign_severity: IssueSeverity,
244}
245
246impl IssueFilter {
247 pub const fn everything() -> Self {
248 Self {
249 severity: IssueSeverity::Info,
250 foreign_severity: IssueSeverity::Info,
251 }
252 }
253
254 pub const fn warnings_and_foreign_errors() -> Self {
255 Self {
256 severity: IssueSeverity::Warning,
257 foreign_severity: IssueSeverity::Error,
258 }
259 }
260
261 async fn matches(&self, issue: ResolvedVc<Box<dyn Issue>>) -> Result<bool> {
263 if *self == IssueFilter::everything() {
264 return Ok(true);
265 }
266 let severity = issue.into_trait_ref().await?.severity();
267 Ok(
269 if severity <= self.severity || severity <= self.foreign_severity {
270 if severity <= self.severity && severity <= self.foreign_severity {
273 true
275 } else {
276 let path = issue.file_path().await?;
277 if ContextCondition::InNodeModules.matches(&path) {
278 severity <= self.foreign_severity
279 } else {
280 severity <= self.severity
281 }
282 }
283 } else {
284 false
286 },
287 )
288 }
289}
290
291#[turbo_tasks::value(shared)]
294#[derive(Debug)]
295pub struct CapturedIssues {
296 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
297 tracer: ResolvedVc<DelegatingImportTracer>,
298}
299
300#[turbo_tasks::value_impl]
301impl CapturedIssues {
302 #[turbo_tasks::function]
303 pub fn is_empty(&self) -> Vc<bool> {
304 Vc::cell(self.is_empty_ref())
305 }
306}
307
308impl CapturedIssues {
309 pub fn is_empty_ref(&self) -> bool {
311 self.issues.is_empty()
312 }
313
314 #[allow(clippy::len_without_is_empty)]
316 pub fn len(&self) -> usize {
317 self.issues.len()
318 }
319
320 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
322 self.issues.iter().copied()
323 }
324
325 pub async fn get_plain_issues(&self, filter: IssueFilter) -> Result<Vec<ReadRef<PlainIssue>>> {
327 let mut list = self
328 .issues
329 .iter()
330 .map(async |issue| {
331 if filter.matches(*issue).await? {
332 Ok(Some(
333 PlainIssue::from_issue(**issue, Some(*self.tracer)).await?,
334 ))
335 } else {
336 Ok(None)
337 }
338 })
339 .try_flat_join()
340 .await?;
341 list.sort();
342 Ok(list)
343 }
344}
345
346#[derive(
347 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
348)]
349pub struct IssueSource {
350 source: ResolvedVc<Box<dyn Source>>,
351 range: Option<SourceRange>,
352}
353
354#[derive(
356 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
357)]
358enum SourceRange {
359 LineColumn(SourcePos, SourcePos),
360 ByteOffset(u32, u32),
361}
362
363impl IssueSource {
364 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
367 IssueSource {
368 source,
369 range: None,
370 }
371 }
372
373 pub fn from_line_col(
374 source: ResolvedVc<Box<dyn Source>>,
375 start: SourcePos,
376 end: SourcePos,
377 ) -> Self {
378 IssueSource {
379 source,
380 range: Some(SourceRange::LineColumn(start, end)),
381 }
382 }
383
384 async fn into_plain(self) -> Result<PlainIssueSource> {
385 let Self { mut source, range } = self;
386
387 let range = if let Some(range) = range {
388 let mut range = match range {
389 SourceRange::LineColumn(start, end) => Some((start, end)),
390 SourceRange::ByteOffset(start, end) => {
391 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
392 let start = find_line_and_column(lines.as_ref(), start);
393 let end = find_line_and_column(lines.as_ref(), end);
394 Some((start, end))
395 } else {
396 None
397 }
398 }
399 };
400
401 if let Some((start, end)) = range {
403 let mapped = source_pos(source, start, end).await?;
404
405 if let Some((mapped_source, start, end)) = mapped {
406 range = Some((start, end));
407 source = mapped_source;
408 }
409 }
410 range
411 } else {
412 None
413 };
414 Ok(PlainIssueSource {
415 asset: PlainSource::from_source(*source).await?,
416 range,
417 })
418 }
419
420 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
429 IssueSource {
430 source,
431 range: match (start == 0, end == 0) {
432 (true, true) => None,
433 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
434 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
435 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
436 },
437 }
438 }
439
440 pub async fn from_byte_offset(
450 source: ResolvedVc<Box<dyn Source>>,
451 start: u32,
452 end: u32,
453 ) -> Result<Self> {
454 Ok(IssueSource {
455 source,
456 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
457 let start = find_line_and_column(lines.as_ref(), start);
458 let end = find_line_and_column(lines.as_ref(), end);
459 Some(SourceRange::LineColumn(start, end))
460 } else {
461 None
462 },
463 })
464 }
465
466 pub fn file_path(&self) -> Vc<FileSystemPath> {
468 self.source.ident().path()
469 }
470}
471
472impl IssueSource {
473 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
475 Ok(match &self.range {
476 Some(range) => match range {
477 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
478 SourceRange::LineColumn(start, end) => {
479 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
480 let start = find_offset(lines.as_ref(), *start) + 1;
481 let end = find_offset(lines.as_ref(), *end) + 1;
482 Some((start, end))
483 } else {
484 None
485 }
486 }
487 },
488 _ => None,
489 })
490 }
491}
492
493async fn source_pos(
494 source: ResolvedVc<Box<dyn Source>>,
495 start: SourcePos,
496 end: SourcePos,
497) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
498 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
499 return Ok(None);
500 };
501
502 let srcmap = generator.generate_source_map();
503 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
504 return Ok(None);
505 };
506
507 let find = async |line: u32, col: u32| {
508 let TokenWithSource {
509 token,
510 source_content,
511 } = &srcmap.lookup_token_and_source(line, col).await?;
512
513 match token {
514 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
515 SourcePos {
516 line: t.generated_line as _,
517 column: t.generated_column as _,
518 },
519 *source_content,
520 )),
521 crate::source_map::Token::Original(t) => anyhow::Ok((
522 SourcePos {
523 line: t.original_line as _,
524 column: t.original_column as _,
525 },
526 *source_content,
527 )),
528 }
529 };
530
531 let (start, content_1) = find(start.line, start.column).await?;
532 let (end, content_2) = find(end.line, end.column).await?;
533
534 let Some((content_1, content_2)) = content_1.zip(content_2) else {
535 return Ok(None);
536 };
537
538 if content_1 != content_2 {
539 return Ok(None);
540 }
541
542 Ok(Some((content_1, start, end)))
543}
544
545#[turbo_tasks::value(transparent)]
546pub struct OptionIssueSource(Option<IssueSource>);
547
548#[turbo_tasks::value(transparent)]
549pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
550
551#[derive(
553 Serialize,
554 PartialEq,
555 Eq,
556 PartialOrd,
557 Ord,
558 Clone,
559 Debug,
560 TraceRawVcs,
561 NonLocalValue,
562 DeterministicHash,
563)]
564#[serde(rename_all = "camelCase")]
565pub struct PlainTraceItem {
566 pub fs_name: RcStr,
568 pub root_path: RcStr,
570 pub path: RcStr,
572 pub layer: Option<RcStr>,
574}
575
576impl PlainTraceItem {
577 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
578 let fs_path = asset.path.clone();
581 let fs_name = fs_path.fs.to_string().owned().await?;
582 let root_path = fs_path.fs.root().await?.path.clone();
583 let path = fs_path.path.clone();
584 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
585 Ok(Self {
586 fs_name,
587 root_path,
588 path,
589 layer,
590 })
591 }
592}
593
594pub type PlainTrace = Vec<PlainTraceItem>;
595
596async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
598 let mut plain_traces = traces
599 .into_iter()
600 .map(|trace| async move {
601 let mut plain_trace = trace
602 .into_iter()
603 .filter(|asset| {
604 asset.assets.is_empty()
607 })
608 .map(PlainTraceItem::from_asset_ident)
609 .try_join()
610 .await?;
611
612 plain_trace.dedup();
629
630 Ok(plain_trace)
631 })
632 .try_join()
633 .await?;
634
635 plain_traces.retain(|t| t.len() > 1);
638 plain_traces.sort_by(|a, b| {
641 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
643 });
644
645 if plain_traces.len() > 1 {
653 let mut i = 0;
654 while i < plain_traces.len() - 1 {
655 let mut j = plain_traces.len() - 1;
656 while j > i {
657 if plain_traces[j].ends_with(&plain_traces[i]) {
658 plain_traces.remove(j);
664 }
665 j -= 1;
666 }
667 i += 1;
668 }
669 }
670
671 Ok(plain_traces)
672}
673
674#[turbo_tasks::value(shared)]
675#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
676pub enum IssueStage {
677 Config,
678 AppStructure,
679 ProcessModule,
680 Load,
682 SourceTransform,
683 Parse,
684 Transform,
686 Analysis,
687 Resolve,
688 Bindings,
689 CodeGen,
690 Unsupported,
691 Misc,
692 Other(RcStr),
693}
694
695impl Display for IssueStage {
696 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
697 match self {
698 IssueStage::Config => write!(f, "config"),
699 IssueStage::Resolve => write!(f, "resolve"),
700 IssueStage::ProcessModule => write!(f, "process module"),
701 IssueStage::Load => write!(f, "load"),
702 IssueStage::SourceTransform => write!(f, "source transform"),
703 IssueStage::Parse => write!(f, "parse"),
704 IssueStage::Transform => write!(f, "transform"),
705 IssueStage::Analysis => write!(f, "analysis"),
706 IssueStage::Bindings => write!(f, "bindings"),
707 IssueStage::CodeGen => write!(f, "code gen"),
708 IssueStage::Unsupported => write!(f, "unsupported"),
709 IssueStage::AppStructure => write!(f, "app structure"),
710 IssueStage::Misc => write!(f, "misc"),
711 IssueStage::Other(s) => write!(f, "{s}"),
712 }
713 }
714}
715
716#[turbo_tasks::value(serialization = "none")]
717#[derive(Clone, Debug, PartialOrd, Ord)]
718pub struct PlainIssue {
719 pub severity: IssueSeverity,
720 pub stage: IssueStage,
721
722 pub title: StyledString,
723 pub file_path: RcStr,
724
725 pub description: Option<StyledString>,
726 pub detail: Option<StyledString>,
727 pub documentation_link: RcStr,
728
729 pub source: Option<PlainIssueSource>,
730 pub import_traces: Vec<PlainTrace>,
731}
732
733fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
734 hasher.write_ref(&issue.severity);
735 hasher.write_ref(&issue.file_path);
736 hasher.write_ref(&issue.stage);
737 hasher.write_ref(&issue.title);
738 hasher.write_ref(&issue.description);
739 hasher.write_ref(&issue.detail);
740 hasher.write_ref(&issue.documentation_link);
741
742 if let Some(source) = &issue.source {
743 hasher.write_value(1_u8);
744 hasher.write_ref(&source.range);
747 } else {
748 hasher.write_value(0_u8);
749 }
750
751 if full {
752 hasher.write_ref(&issue.import_traces);
753 }
754}
755
756impl PlainIssue {
757 pub fn internal_hash_ref(&self, full: bool) -> u64 {
766 let mut hasher = Xxh3Hash64Hasher::new();
767 hash_plain_issue(self, &mut hasher, full);
768 hasher.finish()
769 }
770}
771
772#[turbo_tasks::value_impl]
773impl PlainIssue {
774 #[turbo_tasks::function]
777 pub async fn from_issue(
778 issue: ResolvedVc<Box<dyn Issue>>,
779 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
780 ) -> Result<Vc<Self>> {
781 let description: Option<StyledString> = match *issue.description().await? {
782 Some(description) => Some(description.owned().await?),
783 None => None,
784 };
785 let detail = match *issue.detail().await? {
786 Some(detail) => Some(detail.owned().await?),
787 None => None,
788 };
789 let trait_ref = issue.into_trait_ref().await?;
790
791 let severity = trait_ref.severity();
792
793 Ok(Self::cell(Self {
794 severity,
795 file_path: issue.file_path().to_string().owned().await?,
796 stage: issue.stage().owned().await?,
797 title: issue.title().owned().await?,
798 description,
799 detail,
800 documentation_link: issue.documentation_link().owned().await?,
801 source: {
802 if let Some(s) = &*issue.source().await? {
803 Some(s.into_plain().await?)
804 } else {
805 None
806 }
807 },
808 import_traces: match import_tracer {
809 Some(tracer) => {
810 into_plain_trace(
811 tracer
812 .await?
813 .get_traces(issue.file_path().owned().await?)
814 .await?,
815 )
816 .await?
817 }
818 None => vec![],
819 },
820 }))
821 }
822}
823
824#[turbo_tasks::value(serialization = "none")]
825#[derive(Clone, Debug, PartialOrd, Ord)]
826pub struct PlainIssueSource {
827 pub asset: ReadRef<PlainSource>,
828 pub range: Option<(SourcePos, SourcePos)>,
829}
830
831#[turbo_tasks::value(serialization = "none")]
832#[derive(Clone, Debug, PartialOrd, Ord)]
833pub struct PlainSource {
834 pub ident: ReadRef<RcStr>,
835 #[turbo_tasks(debug_ignore)]
836 pub content: ReadRef<FileContent>,
837}
838
839#[turbo_tasks::value_impl]
840impl PlainSource {
841 #[turbo_tasks::function]
842 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
843 let asset_content = asset.content().await?;
844 let content = match *asset_content {
845 AssetContent::File(file_content) => file_content.await?,
846 AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
847 };
848
849 Ok(PlainSource {
850 ident: asset.ident().to_string().await?,
851 content,
852 }
853 .cell())
854 }
855}
856
857#[turbo_tasks::value_trait]
858pub trait IssueReporter {
859 #[turbo_tasks::function]
870 fn report_issues(
871 self: Vc<Self>,
872 source: TransientValue<RawVc>,
873 min_failing_severity: IssueSeverity,
874 ) -> Vc<bool>;
875}
876
877pub trait CollectibleIssuesExt
878where
879 Self: Sized,
880{
881 fn peek_issues(self) -> CapturedIssues;
885
886 fn drop_issues(self);
890}
891
892impl<T> CollectibleIssuesExt for T
893where
894 T: CollectiblesSource + Copy + Send,
895{
896 fn peek_issues(self) -> CapturedIssues {
897 CapturedIssues {
898 issues: self.peek_collectibles(),
899
900 tracer: DelegatingImportTracer {
901 delegates: self.peek_collectibles(),
902 }
903 .resolved_cell(),
904 }
905 }
906
907 fn drop_issues(self) {
908 self.drop_collectibles::<Box<dyn Issue>>();
909 }
910}
911
912pub async fn handle_issues<T: Send>(
916 source_op: OperationVc<T>,
917 issue_reporter: Vc<Box<dyn IssueReporter>>,
918 min_failing_severity: IssueSeverity,
919 path: Option<&str>,
920 operation: Option<&str>,
921) -> Result<()> {
922 let source_vc = source_op.connect();
923 let _ = source_op.resolve_strongly_consistent().await?;
924
925 let has_fatal = issue_reporter.report_issues(
926 TransientValue::new(Vc::into_raw(source_vc)),
927 min_failing_severity,
928 );
929
930 if *has_fatal.await? {
931 let mut message = "Fatal issue(s) occurred".to_owned();
932 if let Some(path) = path.as_ref() {
933 message += &format!(" in {path}");
934 };
935 if let Some(operation) = operation.as_ref() {
936 message += &format!(" ({operation})");
937 };
938
939 Err(anyhow!(message))
940 } else {
941 Ok(())
942 }
943}
944
945fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
946 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
947 Ok(i) => SourcePos {
948 line: i as u32,
949 column: 0,
950 },
951 Err(i) => {
952 if i == 0 {
953 SourcePos {
954 line: 0,
955 column: offset,
956 }
957 } else {
958 let line = &lines[i - 1];
959 SourcePos {
960 line: (i - 1) as u32,
961 column: min(line.content.len() as u32, offset - line.bytes_offset),
962 }
963 }
964 }
965 }
966}
967
968fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
969 let line = &lines[pos.line as usize];
970 line.bytes_offset + pos.column
971}