1pub mod analyze;
2pub mod code_gen;
3pub mod module;
4pub mod resolve;
5
6use std::{
7 cmp::{Ordering, min},
8 fmt::{Display, Formatter},
9};
10
11use anyhow::{Result, anyhow};
12use async_trait::async_trait;
13use auto_hash_map::AutoSet;
14use serde::{Deserialize, Serialize};
15use turbo_rcstr::RcStr;
16use turbo_tasks::{
17 CollectiblesSource, IntoTraitRef, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc,
18 TaskInput, TransientInstance, TransientValue, 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 ident::{AssetIdent, Layer},
27 source::Source,
28 source_map::{GenerateSourceMap, SourceMap, TokenWithSource},
29 source_pos::SourcePos,
30};
31
32#[turbo_tasks::value(shared)]
33#[derive(PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput)]
34#[serde(rename_all = "camelCase")]
35pub enum IssueSeverity {
36 Bug,
37 Fatal,
38 Error,
39 Warning,
40 Hint,
41 Note,
42 Suggestion,
43 Info,
44}
45
46impl IssueSeverity {
47 pub fn as_str(&self) -> &'static str {
48 match self {
49 IssueSeverity::Bug => "bug",
50 IssueSeverity::Fatal => "fatal",
51 IssueSeverity::Error => "error",
52 IssueSeverity::Warning => "warning",
53 IssueSeverity::Hint => "hint",
54 IssueSeverity::Note => "note",
55 IssueSeverity::Suggestion => "suggestion",
56 IssueSeverity::Info => "info",
57 }
58 }
59
60 pub fn as_help_str(&self) -> &'static str {
61 match self {
62 IssueSeverity::Bug => "bug in implementation",
63 IssueSeverity::Fatal => "unrecoverable problem",
64 IssueSeverity::Error => "problem that cause a broken result",
65 IssueSeverity::Warning => "problem should be addressed in short term",
66 IssueSeverity::Hint => "idea for improvement",
67 IssueSeverity::Note => "detail that is worth mentioning",
68 IssueSeverity::Suggestion => "change proposal for improvement",
69 IssueSeverity::Info => "detail that is worth telling",
70 }
71 }
72}
73
74impl Display for IssueSeverity {
75 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash)]
84#[turbo_tasks::value(shared)]
85pub enum StyledString {
86 Line(Vec<StyledString>),
90 Stack(Vec<StyledString>),
93 Text(RcStr),
95 Code(RcStr),
98 Strong(RcStr),
100}
101
102#[turbo_tasks::value_trait]
103pub trait Issue {
104 fn severity(&self) -> IssueSeverity {
107 IssueSeverity::Error
108 }
109
110 #[turbo_tasks::function]
113 fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
114
115 #[turbo_tasks::function]
118 fn stage(self: Vc<Self>) -> Vc<IssueStage>;
119
120 #[turbo_tasks::function]
125 fn title(self: Vc<Self>) -> Vc<StyledString>;
126
127 #[turbo_tasks::function]
131 fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
132 Vc::cell(None)
133 }
134
135 #[turbo_tasks::function]
139 fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
140 Vc::cell(None)
141 }
142
143 #[turbo_tasks::function]
146 fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
147 Vc::<RcStr>::default()
148 }
149
150 #[turbo_tasks::function]
154 fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
155 Vc::cell(None)
156 }
157}
158
159#[turbo_tasks::value_trait]
161pub trait ImportTracer {
162 #[turbo_tasks::function]
163 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
164}
165
166#[turbo_tasks::value(shared)]
167#[derive(Debug)]
168pub struct DelegatingImportTracer {
169 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
170}
171
172impl DelegatingImportTracer {
173 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
174 Ok(self
175 .delegates
176 .iter()
177 .map(|d| d.get_traces(path.clone()))
178 .try_join()
179 .await?
180 .iter()
181 .flat_map(|v| v.0.iter().cloned())
182 .collect())
183 }
184}
185
186pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
187
188#[turbo_tasks::value(shared)]
189pub struct ImportTraces(pub Vec<ImportTrace>);
190
191#[turbo_tasks::value_impl]
192impl ValueDefault for ImportTraces {
193 #[turbo_tasks::function]
194 fn value_default() -> Vc<Self> {
195 Self::cell(ImportTraces(vec![]))
196 }
197}
198
199#[turbo_tasks::value_trait]
200trait IssueProcessingPath {
201 #[turbo_tasks::function]
202 fn shortest_path(
203 self: Vc<Self>,
204 issue: Vc<Box<dyn Issue>>,
205 ) -> Vc<OptionIssueProcessingPathItems>;
206}
207
208#[turbo_tasks::value]
209pub struct IssueProcessingPathItem {
210 pub file_path: Option<FileSystemPath>,
211 pub description: ResolvedVc<RcStr>,
212}
213
214#[turbo_tasks::value_impl]
215impl ValueToString for IssueProcessingPathItem {
216 #[turbo_tasks::function]
217 async fn to_string(&self) -> Result<Vc<RcStr>> {
218 if let Some(context) = &self.file_path {
219 let description_str = self.description.await?;
220 Ok(Vc::cell(
221 format!("{} ({})", context.value_to_string().await?, description_str).into(),
222 ))
223 } else {
224 Ok(*self.description)
225 }
226 }
227}
228
229#[turbo_tasks::value_impl]
230impl IssueProcessingPathItem {
231 #[turbo_tasks::function]
232 pub async fn into_plain(&self) -> Result<Vc<PlainIssueProcessingPathItem>> {
233 Ok(PlainIssueProcessingPathItem {
234 file_path: if let Some(context) = &self.file_path {
235 Some(context.value_to_string().await?)
236 } else {
237 None
238 },
239 description: self.description.await?,
240 }
241 .cell())
242 }
243}
244
245#[turbo_tasks::value(transparent)]
246pub struct OptionIssueProcessingPathItems(Option<Vec<ResolvedVc<IssueProcessingPathItem>>>);
247
248#[turbo_tasks::value_impl]
249impl OptionIssueProcessingPathItems {
250 #[turbo_tasks::function]
251 pub fn none() -> Vc<Self> {
252 Vc::cell(None)
253 }
254
255 #[turbo_tasks::function]
256 pub async fn into_plain(self: Vc<Self>) -> Result<Vc<PlainIssueProcessingPath>> {
257 Ok(Vc::cell(if let Some(items) = &*self.await? {
258 Some(
259 items
260 .iter()
261 .map(|item| item.into_plain())
262 .try_join()
263 .await?,
264 )
265 } else {
266 None
267 }))
268 }
269}
270
271#[turbo_tasks::value]
272struct RootIssueProcessingPath(ResolvedVc<Box<dyn Issue>>);
273
274#[turbo_tasks::value_impl]
275impl IssueProcessingPath for RootIssueProcessingPath {
276 #[turbo_tasks::function]
277 fn shortest_path(
278 &self,
279 issue: ResolvedVc<Box<dyn Issue>>,
280 ) -> Vc<OptionIssueProcessingPathItems> {
281 if self.0 == issue {
282 Vc::cell(Some(Vec::new()))
283 } else {
284 Vc::cell(None)
285 }
286 }
287}
288
289#[turbo_tasks::value]
290struct ItemIssueProcessingPath(
291 Option<ResolvedVc<IssueProcessingPathItem>>,
292 AutoSet<ResolvedVc<Box<dyn IssueProcessingPath>>>,
293);
294
295#[turbo_tasks::value_impl]
296impl IssueProcessingPath for ItemIssueProcessingPath {
297 #[turbo_tasks::function]
299 async fn shortest_path(
300 &self,
301 issue: Vc<Box<dyn Issue>>,
302 ) -> Result<Vc<OptionIssueProcessingPathItems>> {
303 assert!(!self.1.is_empty());
304 let paths = self
305 .1
306 .iter()
307 .map(|child| child.shortest_path(issue))
308 .collect::<Vec<_>>();
309 let paths = paths.iter().try_join().await?;
310 let mut shortest: Option<&Vec<_>> = None;
311 for path in paths.iter().filter_map(|p| p.as_ref()) {
312 if let Some(old) = shortest {
313 match old.len().cmp(&path.len()) {
314 Ordering::Greater => {
315 shortest = Some(path);
316 }
317 Ordering::Equal => {
318 let (mut a, mut b) = (old.iter(), path.iter());
319 while let (Some(a), Some(b)) = (a.next(), b.next()) {
320 let (a, b) = (a.to_string().await?, b.to_string().await?);
321 match RcStr::cmp(&*a, &*b) {
322 Ordering::Less => break,
323 Ordering::Greater => {
324 shortest = Some(path);
325 break;
326 }
327 Ordering::Equal => {}
328 }
329 }
330 }
331 Ordering::Less => {}
332 }
333 } else {
334 shortest = Some(path);
335 }
336 }
337 Ok(Vc::cell(shortest.map(|path| {
338 if let Some(item) = self.0 {
339 std::iter::once(item).chain(path.iter().copied()).collect()
340 } else {
341 path.clone()
342 }
343 })))
344 }
345}
346
347pub trait IssueExt {
348 fn emit(self);
349}
350
351impl<T> IssueExt for ResolvedVc<T>
352where
353 T: Upcast<Box<dyn Issue>>,
354{
355 fn emit(self) {
356 let issue = ResolvedVc::upcast::<Box<dyn Issue>>(self);
357 emit(issue);
358 emit(ResolvedVc::upcast::<Box<dyn IssueProcessingPath>>(
359 RootIssueProcessingPath::resolved_cell(RootIssueProcessingPath(issue)),
360 ))
361 }
362}
363
364#[turbo_tasks::value(transparent)]
365pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
366
367#[turbo_tasks::value(shared)]
370#[derive(Debug)]
371pub struct CapturedIssues {
372 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
373 #[cfg(feature = "issue_path")]
374 processing_path: ResolvedVc<ItemIssueProcessingPath>,
375 tracer: ResolvedVc<DelegatingImportTracer>,
376}
377
378#[turbo_tasks::value_impl]
379impl CapturedIssues {
380 #[turbo_tasks::function]
381 pub fn is_empty(&self) -> Vc<bool> {
382 Vc::cell(self.is_empty_ref())
383 }
384}
385
386impl CapturedIssues {
387 pub fn is_empty_ref(&self) -> bool {
389 self.issues.is_empty()
390 }
391
392 #[allow(clippy::len_without_is_empty)]
394 pub fn len(&self) -> usize {
395 self.issues.len()
396 }
397
398 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
400 self.issues.iter().copied()
401 }
402
403 pub async fn get_plain_issues(&self) -> Result<Vec<ReadRef<PlainIssue>>> {
405 let mut list = self
406 .issues
407 .iter()
408 .map(|issue| async move {
409 #[cfg(feature = "issue_path")]
410 let processing_path = self.processing_path.shortest_path(**issue);
411 #[cfg(not(feature = "issue_path"))]
412 let processing_path = OptionIssueProcessingPathItems::none();
413 PlainIssue::from_issue(**issue, Some(*self.tracer), processing_path).await
414 })
415 .try_join()
416 .await?;
417 list.sort();
418 Ok(list)
419 }
420}
421
422#[derive(
423 Clone,
424 Copy,
425 Debug,
426 PartialEq,
427 Eq,
428 Serialize,
429 Deserialize,
430 Hash,
431 TaskInput,
432 TraceRawVcs,
433 NonLocalValue,
434)]
435pub struct IssueSource {
436 source: ResolvedVc<Box<dyn Source>>,
437 range: Option<SourceRange>,
438}
439
440#[derive(
442 Clone,
443 Copy,
444 Debug,
445 PartialEq,
446 Eq,
447 Serialize,
448 Deserialize,
449 Hash,
450 TaskInput,
451 TraceRawVcs,
452 NonLocalValue,
453)]
454enum SourceRange {
455 LineColumn(SourcePos, SourcePos),
456 ByteOffset(u32, u32),
457}
458
459impl IssueSource {
460 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
463 IssueSource {
464 source,
465 range: None,
466 }
467 }
468
469 pub fn from_line_col(
470 source: ResolvedVc<Box<dyn Source>>,
471 start: SourcePos,
472 end: SourcePos,
473 ) -> Self {
474 IssueSource {
475 source,
476 range: Some(SourceRange::LineColumn(start, end)),
477 }
478 }
479
480 async fn into_plain(self) -> Result<PlainIssueSource> {
481 let Self { mut source, range } = self;
482
483 let range = if let Some(range) = range {
484 let mut range = match range {
485 SourceRange::LineColumn(start, end) => Some((start, end)),
486 SourceRange::ByteOffset(start, end) => {
487 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
488 let start = find_line_and_column(lines.as_ref(), start);
489 let end = find_line_and_column(lines.as_ref(), end);
490 Some((start, end))
491 } else {
492 None
493 }
494 }
495 };
496
497 if let Some((start, end)) = range {
499 let mapped = source_pos(source, start, end).await?;
500
501 if let Some((mapped_source, start, end)) = mapped {
502 range = Some((start, end));
503 source = mapped_source;
504 }
505 }
506 range
507 } else {
508 None
509 };
510 Ok(PlainIssueSource {
511 asset: PlainSource::from_source(*source).await?,
512 range,
513 })
514 }
515
516 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
525 IssueSource {
526 source,
527 range: match (start == 0, end == 0) {
528 (true, true) => None,
529 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
530 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
531 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
532 },
533 }
534 }
535
536 pub async fn from_byte_offset(
546 source: ResolvedVc<Box<dyn Source>>,
547 start: u32,
548 end: u32,
549 ) -> Result<Self> {
550 Ok(IssueSource {
551 source,
552 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
553 let start = find_line_and_column(lines.as_ref(), start);
554 let end = find_line_and_column(lines.as_ref(), end);
555 Some(SourceRange::LineColumn(start, end))
556 } else {
557 None
558 },
559 })
560 }
561
562 pub fn file_path(&self) -> Vc<FileSystemPath> {
564 self.source.ident().path()
565 }
566}
567
568impl IssueSource {
569 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
571 Ok(match &self.range {
572 Some(range) => match range {
573 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
574 SourceRange::LineColumn(start, end) => {
575 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
576 let start = find_offset(lines.as_ref(), *start) + 1;
577 let end = find_offset(lines.as_ref(), *end) + 1;
578 Some((start, end))
579 } else {
580 None
581 }
582 }
583 },
584 _ => None,
585 })
586 }
587}
588
589async fn source_pos(
590 source: ResolvedVc<Box<dyn Source>>,
591 start: SourcePos,
592 end: SourcePos,
593) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
594 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
595 return Ok(None);
596 };
597
598 let srcmap = generator.generate_source_map();
599 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
600 return Ok(None);
601 };
602
603 let find = async |line: u32, col: u32| {
604 let TokenWithSource {
605 token,
606 source_content,
607 } = &srcmap.lookup_token_and_source(line, col).await?;
608
609 match token {
610 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
611 SourcePos {
612 line: t.generated_line as _,
613 column: t.generated_column as _,
614 },
615 *source_content,
616 )),
617 crate::source_map::Token::Original(t) => anyhow::Ok((
618 SourcePos {
619 line: t.original_line as _,
620 column: t.original_column as _,
621 },
622 *source_content,
623 )),
624 }
625 };
626
627 let (start, content_1) = find(start.line, start.column).await?;
628 let (end, content_2) = find(end.line, end.column).await?;
629
630 let Some((content_1, content_2)) = content_1.zip(content_2) else {
631 return Ok(None);
632 };
633
634 if content_1 != content_2 {
635 return Ok(None);
636 }
637
638 Ok(Some((content_1, start, end)))
639}
640
641#[turbo_tasks::value(transparent)]
642pub struct OptionIssueSource(Option<IssueSource>);
643
644#[turbo_tasks::value(transparent)]
645pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
646
647#[derive(
649 Serialize,
650 PartialEq,
651 Eq,
652 PartialOrd,
653 Ord,
654 Clone,
655 Debug,
656 TraceRawVcs,
657 NonLocalValue,
658 DeterministicHash,
659)]
660#[serde(rename_all = "camelCase")]
661pub struct PlainTraceItem {
662 pub fs_name: RcStr,
664 pub root_path: RcStr,
666 pub path: RcStr,
668 pub layer: Option<RcStr>,
670}
671
672impl PlainTraceItem {
673 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
674 let fs_path = asset.path.clone();
677 let fs_name = fs_path.fs.to_string().owned().await?;
678 let root_path = fs_path.fs.root().await?.path.clone();
679 let path = fs_path.path.clone();
680 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
681 Ok(Self {
682 fs_name,
683 root_path,
684 path,
685 layer,
686 })
687 }
688}
689
690pub type PlainTrace = Vec<PlainTraceItem>;
691
692async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
694 let mut plain_traces = traces
695 .into_iter()
696 .map(|trace| async move {
697 let mut plain_trace = trace
698 .into_iter()
699 .filter(|asset| {
700 asset.assets.is_empty()
703 })
704 .map(PlainTraceItem::from_asset_ident)
705 .try_join()
706 .await?;
707
708 plain_trace.dedup();
725
726 Ok(plain_trace)
727 })
728 .try_join()
729 .await?;
730
731 plain_traces.retain(|t| t.len() > 1);
734 plain_traces.sort_by(|a, b| {
737 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
739 });
740
741 if plain_traces.len() > 1 {
749 let mut i = 0;
750 while i < plain_traces.len() - 1 {
751 let mut j = plain_traces.len() - 1;
752 while j > i {
753 if plain_traces[j].ends_with(&plain_traces[i]) {
754 plain_traces.remove(j);
760 }
761 j -= 1;
762 }
763 i += 1;
764 }
765 }
766
767 Ok(plain_traces)
768}
769
770#[turbo_tasks::value(shared, serialization = "none")]
771#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
772pub enum IssueStage {
773 Config,
774 AppStructure,
775 ProcessModule,
776 Load,
778 SourceTransform,
779 Parse,
780 Transform,
782 Analysis,
783 Resolve,
784 Bindings,
785 CodeGen,
786 Unsupported,
787 Misc,
788 Other(String),
789}
790
791impl Display for IssueStage {
792 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
793 match self {
794 IssueStage::Config => write!(f, "config"),
795 IssueStage::Resolve => write!(f, "resolve"),
796 IssueStage::ProcessModule => write!(f, "process module"),
797 IssueStage::Load => write!(f, "load"),
798 IssueStage::SourceTransform => write!(f, "source transform"),
799 IssueStage::Parse => write!(f, "parse"),
800 IssueStage::Transform => write!(f, "transform"),
801 IssueStage::Analysis => write!(f, "analysis"),
802 IssueStage::Bindings => write!(f, "bindings"),
803 IssueStage::CodeGen => write!(f, "code gen"),
804 IssueStage::Unsupported => write!(f, "unsupported"),
805 IssueStage::AppStructure => write!(f, "app structure"),
806 IssueStage::Misc => write!(f, "misc"),
807 IssueStage::Other(s) => write!(f, "{s}"),
808 }
809 }
810}
811
812#[turbo_tasks::value(serialization = "none")]
813#[derive(Clone, Debug, PartialOrd, Ord)]
814pub struct PlainIssue {
815 pub severity: IssueSeverity,
816 pub stage: IssueStage,
817
818 pub title: StyledString,
819 pub file_path: RcStr,
820
821 pub description: Option<StyledString>,
822 pub detail: Option<StyledString>,
823 pub documentation_link: RcStr,
824
825 pub source: Option<PlainIssueSource>,
826 pub processing_path: ReadRef<PlainIssueProcessingPath>,
827 pub import_traces: Vec<PlainTrace>,
828}
829
830fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
831 hasher.write_ref(&issue.severity);
832 hasher.write_ref(&issue.file_path);
833 hasher.write_ref(&issue.stage);
834 hasher.write_ref(&issue.title);
835 hasher.write_ref(&issue.description);
836 hasher.write_ref(&issue.detail);
837 hasher.write_ref(&issue.documentation_link);
838
839 if let Some(source) = &issue.source {
840 hasher.write_value(1_u8);
841 hasher.write_ref(&source.range);
844 } else {
845 hasher.write_value(0_u8);
846 }
847
848 if full {
849 hasher.write_ref(&issue.processing_path);
850 hasher.write_ref(&issue.import_traces);
851 }
852}
853
854impl PlainIssue {
855 pub fn internal_hash_ref(&self, full: bool) -> u64 {
864 let mut hasher = Xxh3Hash64Hasher::new();
865 hash_plain_issue(self, &mut hasher, full);
866 hasher.finish()
867 }
868}
869
870#[turbo_tasks::value_impl]
871impl PlainIssue {
872 #[turbo_tasks::function]
875 pub async fn from_issue(
876 issue: ResolvedVc<Box<dyn Issue>>,
877 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
878 processing_path: Vc<OptionIssueProcessingPathItems>,
879 ) -> Result<Vc<Self>> {
880 let description: Option<StyledString> = match *issue.description().await? {
881 Some(description) => Some((*description.await?).clone()),
882 None => None,
883 };
884 let detail = match *issue.detail().await? {
885 Some(detail) => Some((*detail.await?).clone()),
886 None => None,
887 };
888 let trait_ref = issue.into_trait_ref().await?;
889
890 let severity = trait_ref.severity();
891
892 Ok(Self::cell(Self {
893 severity,
894 file_path: issue.file_path().to_string().owned().await?,
895 stage: issue.stage().owned().await?,
896 title: issue.title().owned().await?,
897 description,
898 detail,
899 documentation_link: issue.documentation_link().owned().await?,
900 source: {
901 if let Some(s) = &*issue.source().await? {
902 Some(s.into_plain().await?)
903 } else {
904 None
905 }
906 },
907 processing_path: processing_path.into_plain().await?,
908 import_traces: match import_tracer {
909 Some(tracer) => {
910 into_plain_trace(
911 tracer
912 .await?
913 .get_traces(issue.file_path().await?.clone_value())
914 .await?,
915 )
916 .await?
917 }
918 None => vec![],
919 },
920 }))
921 }
922}
923
924#[turbo_tasks::value(serialization = "none")]
925#[derive(Clone, Debug, PartialOrd, Ord)]
926pub struct PlainIssueSource {
927 pub asset: ReadRef<PlainSource>,
928 pub range: Option<(SourcePos, SourcePos)>,
929}
930
931#[turbo_tasks::value(serialization = "none")]
932#[derive(Clone, Debug, PartialOrd, Ord)]
933pub struct PlainSource {
934 pub ident: ReadRef<RcStr>,
935 #[turbo_tasks(debug_ignore)]
936 pub content: ReadRef<FileContent>,
937}
938
939#[turbo_tasks::value_impl]
940impl PlainSource {
941 #[turbo_tasks::function]
942 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
943 let asset_content = asset.content().await?;
944 let content = match *asset_content {
945 AssetContent::File(file_content) => file_content.await?,
946 AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
947 };
948
949 Ok(PlainSource {
950 ident: asset.ident().to_string().await?,
951 content,
952 }
953 .cell())
954 }
955}
956
957#[turbo_tasks::value(transparent, serialization = "none")]
958#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
959pub struct PlainIssueProcessingPath(Option<Vec<ReadRef<PlainIssueProcessingPathItem>>>);
960
961#[turbo_tasks::value(serialization = "none")]
962#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
963pub struct PlainIssueProcessingPathItem {
964 pub file_path: Option<ReadRef<RcStr>>,
965 pub description: ReadRef<RcStr>,
966}
967
968#[turbo_tasks::value_trait]
969pub trait IssueReporter {
970 #[turbo_tasks::function]
982 fn report_issues(
983 self: Vc<Self>,
984 issues: TransientInstance<CapturedIssues>,
985 source: TransientValue<RawVc>,
986 min_failing_severity: IssueSeverity,
987 ) -> Vc<bool>;
988}
989
990#[async_trait]
991pub trait IssueDescriptionExt
992where
993 Self: Sized,
994{
995 #[allow(unused_variables, reason = "behind feature flag")]
996 async fn attach_file_path(
997 self,
998 file_path: impl Into<Option<FileSystemPath>> + Send,
999 description: impl Into<String> + Send,
1000 ) -> Result<Self>;
1001
1002 #[allow(unused_variables, reason = "behind feature flag")]
1003 async fn attach_description(self, description: impl Into<String> + Send) -> Result<Self>;
1004
1005 async fn issue_file_path(
1006 self,
1007 file_path: impl Into<Option<FileSystemPath>> + Send,
1008 description: impl Into<String> + Send,
1009 ) -> Result<Self>;
1010 async fn issue_description(self, description: impl Into<String> + Send) -> Result<Self>;
1011
1012 async fn peek_issues_with_path(self) -> Result<CapturedIssues>;
1015
1016 async fn take_issues_with_path(self) -> Result<CapturedIssues>;
1021}
1022
1023#[async_trait]
1024impl<T> IssueDescriptionExt for T
1025where
1026 T: CollectiblesSource + Copy + Send,
1027{
1028 #[allow(unused_variables, reason = "behind feature flag")]
1029 async fn attach_file_path(
1030 self,
1031 file_path: impl Into<Option<FileSystemPath>> + Send,
1032 description: impl Into<String> + Send,
1033 ) -> Result<Self> {
1034 #[cfg(feature = "issue_path")]
1035 {
1036 let children = self.take_collectibles();
1037 if !children.is_empty() {
1038 emit(ResolvedVc::upcast::<Box<dyn IssueProcessingPath>>(
1039 ItemIssueProcessingPath::resolved_cell(ItemIssueProcessingPath(
1040 Some(IssueProcessingPathItem::resolved_cell(
1041 IssueProcessingPathItem {
1042 file_path: file_path.into(),
1043 description: ResolvedVc::cell(RcStr::from(description.into())),
1044 },
1045 )),
1046 children,
1047 )),
1048 ));
1049 }
1050 }
1051 Ok(self)
1052 }
1053
1054 #[allow(unused_variables, reason = "behind feature flag")]
1055 async fn attach_description(self, description: impl Into<String> + Send) -> Result<T> {
1056 self.attach_file_path(None, description).await
1057 }
1058
1059 async fn issue_file_path(
1060 self,
1061 file_path: impl Into<Option<FileSystemPath>> + Send,
1062 description: impl Into<String> + Send,
1063 ) -> Result<Self> {
1064 #[cfg(feature = "issue_path")]
1065 {
1066 let children = self.take_collectibles();
1067 if !children.is_empty() {
1068 emit(ResolvedVc::upcast::<Box<dyn IssueProcessingPath>>(
1069 ItemIssueProcessingPath::resolved_cell(ItemIssueProcessingPath(
1070 Some(IssueProcessingPathItem::resolved_cell(
1071 IssueProcessingPathItem {
1072 file_path: file_path.into(),
1073 description: ResolvedVc::cell(RcStr::from(description.into())),
1074 },
1075 )),
1076 children,
1077 )),
1078 ));
1079 }
1080 }
1081 #[cfg(not(feature = "issue_path"))]
1082 {
1083 let _ = (file_path, description);
1084 }
1085 Ok(self)
1086 }
1087
1088 async fn issue_description(self, description: impl Into<String> + Send) -> Result<Self> {
1089 self.issue_file_path(None, description).await
1090 }
1091
1092 async fn peek_issues_with_path(self) -> Result<CapturedIssues> {
1093 Ok(CapturedIssues {
1094 issues: self.peek_collectibles(),
1095 #[cfg(feature = "issue_path")]
1096 processing_path: ItemIssueProcessingPath::resolved_cell(ItemIssueProcessingPath(
1097 None,
1098 self.peek_collectibles(),
1099 )),
1100 tracer: DelegatingImportTracer::resolved_cell(DelegatingImportTracer {
1101 delegates: self.peek_collectibles(),
1102 }),
1103 })
1104 }
1105
1106 async fn take_issues_with_path(self) -> Result<CapturedIssues> {
1107 Ok(CapturedIssues {
1108 issues: self.take_collectibles(),
1109 #[cfg(feature = "issue_path")]
1110 processing_path: ItemIssueProcessingPath::resolved_cell(ItemIssueProcessingPath(
1111 None,
1112 self.take_collectibles(),
1113 )),
1114 tracer: DelegatingImportTracer::resolved_cell(DelegatingImportTracer {
1115 delegates: self.take_collectibles(),
1116 }),
1117 })
1118 }
1119}
1120
1121pub async fn handle_issues<T: Send>(
1122 source_op: OperationVc<T>,
1123 issue_reporter: Vc<Box<dyn IssueReporter>>,
1124 min_failing_severity: IssueSeverity,
1125 path: Option<&str>,
1126 operation: Option<&str>,
1127) -> Result<()> {
1128 let source_vc = source_op.connect();
1129 let _ = source_op.resolve_strongly_consistent().await?;
1130 let issues = source_op.peek_issues_with_path().await?;
1131
1132 let has_fatal = issue_reporter.report_issues(
1133 TransientInstance::new(issues),
1134 TransientValue::new(Vc::into_raw(source_vc)),
1135 min_failing_severity,
1136 );
1137
1138 if *has_fatal.await? {
1139 let mut message = "Fatal issue(s) occurred".to_owned();
1140 if let Some(path) = path.as_ref() {
1141 message += &format!(" in {path}");
1142 };
1143 if let Some(operation) = operation.as_ref() {
1144 message += &format!(" ({operation})");
1145 };
1146
1147 Err(anyhow!(message))
1148 } else {
1149 Ok(())
1150 }
1151}
1152
1153fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1154 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1155 Ok(i) => SourcePos {
1156 line: i as u32,
1157 column: 0,
1158 },
1159 Err(i) => {
1160 if i == 0 {
1161 SourcePos {
1162 line: 0,
1163 column: offset,
1164 }
1165 } else {
1166 let line = &lines[i - 1];
1167 SourcePos {
1168 line: (i - 1) as u32,
1169 column: min(line.content.len() as u32, offset - line.bytes_offset),
1170 }
1171 }
1172 }
1173 }
1174}
1175
1176fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1177 let line = &lines[pos.line as usize];
1178 line.bytes_offset + pos.column
1179}