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_esregex::EsRegex;
16use turbo_rcstr::RcStr;
17use turbo_tasks::{
18 CollectiblesSource, IntoTraitRef, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc,
19 TaskInput, TransientValue, TryFlatJoinIterExt, TryJoinIterExt, Upcast, ValueDefault,
20 ValueToString, Vc, emit, trace::TraceRawVcs,
21};
22use turbo_tasks_fs::{
23 FileContent, FileLine, FileLinesContent, FileSystem, FileSystemPath, glob::Glob,
24 json::UnparsableJson,
25};
26use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
27
28use crate::{
29 asset::{Asset, AssetContent},
30 condition::ContextCondition,
31 ident::{AssetIdent, Layer},
32 source::Source,
33 source_map::{GenerateSourceMap, SourceMap, TokenWithSource},
34 source_pos::SourcePos,
35};
36
37#[turbo_tasks::value(shared)]
38#[derive(
39 PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput, Serialize, Deserialize,
40)]
41#[serde(rename_all = "camelCase")]
42pub enum IssueSeverity {
43 Bug,
44 Fatal,
45 Error,
46 Warning,
47 Hint,
48 Note,
49 Suggestion,
50 Info,
51}
52
53impl IssueSeverity {
54 pub fn as_str(&self) -> &'static str {
55 match self {
56 IssueSeverity::Bug => "bug",
57 IssueSeverity::Fatal => "fatal",
58 IssueSeverity::Error => "error",
59 IssueSeverity::Warning => "warning",
60 IssueSeverity::Hint => "hint",
61 IssueSeverity::Note => "note",
62 IssueSeverity::Suggestion => "suggestion",
63 IssueSeverity::Info => "info",
64 }
65 }
66
67 pub fn as_help_str(&self) -> &'static str {
68 match self {
69 IssueSeverity::Bug => "bug in implementation",
70 IssueSeverity::Fatal => "unrecoverable problem",
71 IssueSeverity::Error => "problem that cause a broken result",
72 IssueSeverity::Warning => "problem should be addressed in short term",
73 IssueSeverity::Hint => "idea for improvement",
74 IssueSeverity::Note => "detail that is worth mentioning",
75 IssueSeverity::Suggestion => "change proposal for improvement",
76 IssueSeverity::Info => "detail that is worth telling",
77 }
78 }
79}
80
81impl Display for IssueSeverity {
82 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83 f.write_str(self.as_str())
84 }
85}
86
87#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
91#[turbo_tasks::value(shared)]
92pub enum StyledString {
93 Line(Vec<StyledString>),
97 Stack(Vec<StyledString>),
100 Text(RcStr),
102 Code(RcStr),
105 Strong(RcStr),
107}
108
109impl StyledString {
110 pub fn to_unstyled_string(&self) -> String {
111 match self {
112 StyledString::Line(items) => items
113 .iter()
114 .map(|item| item.to_unstyled_string())
115 .collect::<Vec<_>>()
116 .join(""),
117 StyledString::Stack(items) => items
118 .iter()
119 .map(|item| item.to_unstyled_string())
120 .collect::<Vec<_>>()
121 .join("\n"),
122 StyledString::Text(s) | StyledString::Code(s) | StyledString::Strong(s) => {
123 s.to_string()
124 }
125 }
126 }
127}
128
129#[turbo_tasks::value_trait]
130pub trait Issue {
131 fn severity(&self) -> IssueSeverity {
134 IssueSeverity::Error
135 }
136
137 #[turbo_tasks::function]
140 fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
141
142 #[turbo_tasks::function]
145 fn stage(self: Vc<Self>) -> Vc<IssueStage>;
146
147 #[turbo_tasks::function]
152 fn title(self: Vc<Self>) -> Vc<StyledString>;
153
154 #[turbo_tasks::function]
158 fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
159 Vc::cell(None)
160 }
161
162 #[turbo_tasks::function]
166 fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
167 Vc::cell(None)
168 }
169
170 #[turbo_tasks::function]
173 fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
174 Vc::<RcStr>::default()
175 }
176
177 #[turbo_tasks::function]
181 fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
182 Vc::cell(None)
183 }
184}
185
186#[turbo_tasks::value_trait]
188pub trait ImportTracer {
189 #[turbo_tasks::function]
190 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
191}
192
193#[turbo_tasks::value]
194#[derive(Debug)]
195pub struct DelegatingImportTracer {
196 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
197}
198
199impl DelegatingImportTracer {
200 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
201 Ok(self
202 .delegates
203 .iter()
204 .map(|d| d.get_traces(path.clone()))
205 .try_join()
206 .await?
207 .iter()
208 .flat_map(|v| v.0.iter().cloned())
209 .collect())
210 }
211}
212
213pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
214
215#[turbo_tasks::value(shared)]
216pub struct ImportTraces(pub Vec<ImportTrace>);
217
218#[turbo_tasks::value_impl]
219impl ValueDefault for ImportTraces {
220 #[turbo_tasks::function]
221 fn value_default() -> Vc<Self> {
222 Self::cell(ImportTraces(vec![]))
223 }
224}
225
226pub trait IssueExt {
227 fn emit(self);
228}
229
230impl<T> IssueExt for ResolvedVc<T>
231where
232 T: Upcast<Box<dyn Issue>>,
233{
234 fn emit(self) {
235 emit(ResolvedVc::upcast_non_strict::<Box<dyn Issue>>(self));
236 }
237}
238
239#[turbo_tasks::value(transparent)]
240pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
241
242#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
244pub enum IgnoreIssuePattern {
245 ExactString(RcStr),
247 Glob(Glob),
249 Regex(EsRegex),
251}
252
253impl IgnoreIssuePattern {
254 pub fn matches(&self, value: &str) -> bool {
256 match self {
257 IgnoreIssuePattern::ExactString(s) => value == s.as_str(),
258 IgnoreIssuePattern::Glob(glob) => glob.matches(value),
259 IgnoreIssuePattern::Regex(regex) => regex.is_match(value),
260 }
261 }
262}
263
264#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
267pub struct IgnoreIssue {
268 pub path: IgnoreIssuePattern,
270 pub title: Option<IgnoreIssuePattern>,
272 pub description: Option<IgnoreIssuePattern>,
274}
275
276#[turbo_tasks::value(shared)]
277pub struct IssueFilter {
278 severity: IssueSeverity,
280 foreign_severity: IssueSeverity,
282 ignore_rules: Vec<IgnoreIssue>,
284}
285
286#[turbo_tasks::value_impl]
287impl IssueFilter {
288 #[turbo_tasks::function]
290 pub fn everything() -> Vc<Self> {
291 IssueFilter {
292 severity: IssueSeverity::Info,
293 foreign_severity: IssueSeverity::Info,
294 ignore_rules: Vec::new(),
295 }
296 .cell()
297 }
298
299 #[turbo_tasks::function]
301 pub async fn matches(&self, issue: ResolvedVc<Box<dyn Issue>>) -> Result<Vc<bool>> {
302 let has_no_ignore_rules = self.ignore_rules.is_empty();
303 let is_everything = self.severity == IssueSeverity::Info
304 && self.foreign_severity == IssueSeverity::Info
305 && has_no_ignore_rules;
306
307 if is_everything {
308 return Ok(Vc::cell(true));
309 }
310
311 let file_path = issue.file_path().await?;
314
315 let severity = issue.into_trait_ref().await?.severity();
318 let severity_allowed = if severity <= self.severity || severity <= self.foreign_severity {
320 if severity <= self.severity && severity <= self.foreign_severity {
323 true
325 } else if ContextCondition::InNodeModules.matches(&file_path) {
326 severity <= self.foreign_severity
327 } else {
328 severity <= self.severity
329 }
330 } else {
331 false
333 };
334
335 if !severity_allowed {
336 return Ok(Vc::cell(false));
337 }
338
339 if !has_no_ignore_rules {
343 let file_path_str = file_path.to_string();
344 let mut title_str: Option<String> = None;
345 let mut description_text: Option<Option<String>> = None;
346
347 for rule in &self.ignore_rules {
348 if !rule.path.matches(&file_path_str) {
349 continue;
350 }
351 if let Some(ref title_pat) = rule.title {
352 if title_str.is_none() {
353 title_str = Some(issue.title().await?.to_unstyled_string());
354 }
355 if !title_pat.matches(title_str.as_deref().unwrap()) {
356 continue;
357 }
358 }
359 if let Some(ref desc_pat) = rule.description {
360 if description_text.is_none() {
361 let desc_opt = issue.description().await?;
362 description_text = Some(match desc_opt.as_ref() {
363 Some(desc_vc) => Some(desc_vc.await?.to_unstyled_string()),
364 None => None,
365 });
366 }
367 match description_text.as_ref().unwrap().as_deref() {
368 Some(desc) if desc_pat.matches(desc) => {}
369 _ => continue,
370 }
371 }
372 return Ok(Vc::cell(false));
374 }
375 }
376
377 Ok(Vc::cell(true))
378 }
379}
380
381impl IssueFilter {
382 pub fn warnings_and_foreign_errors() -> Self {
384 IssueFilter {
385 severity: IssueSeverity::Warning,
386 foreign_severity: IssueSeverity::Error,
387 ignore_rules: Vec::new(),
388 }
389 }
390
391 pub fn with_ignore_rules(mut self, rules: Vec<IgnoreIssue>) -> Self {
393 self.ignore_rules = rules;
394 self
395 }
396}
397
398#[turbo_tasks::value(shared)]
401#[derive(Debug)]
402pub struct CapturedIssues {
403 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
404 tracer: ResolvedVc<DelegatingImportTracer>,
405}
406
407#[turbo_tasks::value_impl]
408impl CapturedIssues {
409 #[turbo_tasks::function]
410 pub fn is_empty(&self) -> Vc<bool> {
411 Vc::cell(self.is_empty_ref())
412 }
413}
414
415impl CapturedIssues {
416 pub fn is_empty_ref(&self) -> bool {
418 self.issues.is_empty()
419 }
420
421 #[allow(clippy::len_without_is_empty)]
423 pub fn len(&self) -> usize {
424 self.issues.len()
425 }
426
427 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
429 self.issues.iter().copied()
430 }
431
432 pub async fn get_plain_issues(
434 &self,
435 filter: Vc<IssueFilter>,
436 ) -> Result<Vec<ReadRef<PlainIssue>>> {
437 let mut list = self
438 .issues
439 .iter()
440 .map(async |issue| {
441 if *filter.matches(**issue).await? {
442 Ok(Some(
443 PlainIssue::from_issue(**issue, Some(*self.tracer)).await?,
444 ))
445 } else {
446 Ok(None)
447 }
448 })
449 .try_flat_join()
450 .await?;
451 list.sort();
452 Ok(list)
453 }
454}
455
456#[derive(
457 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
458)]
459pub struct IssueSource {
460 source: ResolvedVc<Box<dyn Source>>,
461 range: Option<SourceRange>,
462}
463
464#[derive(
466 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
467)]
468enum SourceRange {
469 LineColumn(SourcePos, SourcePos),
470 ByteOffset(u32, u32),
471}
472
473impl IssueSource {
474 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
477 IssueSource {
478 source,
479 range: None,
480 }
481 }
482
483 pub fn from_line_col(
484 source: ResolvedVc<Box<dyn Source>>,
485 start: SourcePos,
486 end: SourcePos,
487 ) -> Self {
488 IssueSource {
489 source,
490 range: Some(SourceRange::LineColumn(start, end)),
491 }
492 }
493
494 pub fn from_single_line_col(source: ResolvedVc<Box<dyn Source>>, pos: SourcePos) -> Self {
495 IssueSource {
496 source,
497 range: Some(SourceRange::LineColumn(
498 pos,
499 SourcePos {
500 line: pos.line,
501 column: pos.column + 1,
503 },
504 )),
505 }
506 }
507
508 async fn into_plain(self) -> Result<PlainIssueSource> {
509 let Self { mut source, range } = self;
510
511 let range = if let Some(range) = range {
512 let mut range = match range {
513 SourceRange::LineColumn(start, end) => Some((start, end)),
514 SourceRange::ByteOffset(start, end) => {
515 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
516 let start = find_line_and_column(lines.as_ref(), start);
517 let end = find_line_and_column(lines.as_ref(), end);
518 Some((start, end))
519 } else {
520 None
521 }
522 }
523 };
524
525 if let Some((start, end)) = range {
527 let mapped = source_pos(source, start, end).await?;
528
529 if let Some((mapped_source, start, end)) = mapped {
530 range = Some((start, end));
531 source = mapped_source;
532 }
533 }
534 range
535 } else {
536 None
537 };
538 Ok(PlainIssueSource {
539 asset: PlainSource::from_source(*source).await?,
540 range,
541 })
542 }
543
544 pub fn from_unparsable_json(
547 source: ResolvedVc<Box<dyn Source>>,
548 error: &UnparsableJson,
549 ) -> Self {
550 match (error.start_location, error.end_location) {
551 (None, None) => Self::from_source_only(source),
552 (Some((line, column)), None) | (None, Some((line, column))) => Self::from_line_col(
553 source,
554 SourcePos { line, column },
555 SourcePos { line, column },
556 ),
557 (Some((start_line, start_column)), Some((end_line, end_column))) => {
558 Self::from_line_col(
559 source,
560 SourcePos {
561 line: start_line,
562 column: start_column,
563 },
564 SourcePos {
565 line: end_line,
566 column: end_column,
567 },
568 )
569 }
570 }
571 }
572
573 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
582 IssueSource {
583 source,
584 range: match (start == 0, end == 0) {
585 (true, true) => None,
586 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
587 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
588 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
589 },
590 }
591 }
592
593 pub async fn from_byte_offset(
603 source: ResolvedVc<Box<dyn Source>>,
604 start: u32,
605 end: u32,
606 ) -> Result<Self> {
607 Ok(IssueSource {
608 source,
609 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
610 let start = find_line_and_column(lines.as_ref(), start);
611 let end = find_line_and_column(lines.as_ref(), end);
612 Some(SourceRange::LineColumn(start, end))
613 } else {
614 None
615 },
616 })
617 }
618
619 pub fn file_path(&self) -> Vc<FileSystemPath> {
621 self.source.ident().path()
622 }
623}
624
625impl IssueSource {
626 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
628 Ok(match &self.range {
629 Some(range) => match range {
630 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
631 SourceRange::LineColumn(start, end) => {
632 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
633 let start = find_offset(lines.as_ref(), *start) + 1;
634 let end = find_offset(lines.as_ref(), *end) + 1;
635 Some((start, end))
636 } else {
637 None
638 }
639 }
640 },
641 _ => None,
642 })
643 }
644}
645
646async fn source_pos(
647 source: ResolvedVc<Box<dyn Source>>,
648 start: SourcePos,
649 end: SourcePos,
650) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
651 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
652 return Ok(None);
653 };
654
655 let srcmap = generator.generate_source_map();
656 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
657 return Ok(None);
658 };
659
660 let find = async |line: u32, col: u32| {
661 let TokenWithSource {
662 token,
663 source_content,
664 } = &srcmap.lookup_token_and_source(line, col).await?;
665
666 match token {
667 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
668 SourcePos {
669 line: t.generated_line as _,
670 column: t.generated_column as _,
671 },
672 *source_content,
673 )),
674 crate::source_map::Token::Original(t) => anyhow::Ok((
675 SourcePos {
676 line: t.original_line as _,
677 column: t.original_column as _,
678 },
679 *source_content,
680 )),
681 }
682 };
683
684 let (start, content_1) = find(start.line, start.column).await?;
685 let (end, content_2) = find(end.line, end.column).await?;
686
687 let Some((content_1, content_2)) = content_1.zip(content_2) else {
688 return Ok(None);
689 };
690
691 if content_1 != content_2 {
692 return Ok(None);
693 }
694
695 Ok(Some((content_1, start, end)))
696}
697
698#[turbo_tasks::value(transparent)]
699pub struct OptionIssueSource(Option<IssueSource>);
700
701#[turbo_tasks::value(transparent)]
702pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
703
704#[derive(
706 Serialize,
707 PartialEq,
708 Eq,
709 PartialOrd,
710 Ord,
711 Clone,
712 Debug,
713 TraceRawVcs,
714 NonLocalValue,
715 DeterministicHash,
716)]
717#[serde(rename_all = "camelCase")]
718pub struct PlainTraceItem {
719 pub fs_name: RcStr,
721 pub root_path: RcStr,
723 pub path: RcStr,
725 pub layer: Option<RcStr>,
727}
728
729impl PlainTraceItem {
730 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
731 let fs_path = asset.path.clone();
734 let fs_name = fs_path.fs.to_string().owned().await?;
735 let root_path = fs_path.fs.root().await?.path.clone();
736 let path = fs_path.path.clone();
737 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
738 Ok(Self {
739 fs_name,
740 root_path,
741 path,
742 layer,
743 })
744 }
745}
746
747pub type PlainTrace = Vec<PlainTraceItem>;
748
749async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
751 let mut plain_traces = traces
752 .into_iter()
753 .map(|trace| async move {
754 let mut plain_trace = trace
755 .into_iter()
756 .filter(|asset| {
757 asset.assets.is_empty()
760 })
761 .map(PlainTraceItem::from_asset_ident)
762 .try_join()
763 .await?;
764
765 plain_trace.dedup();
782
783 Ok(plain_trace)
784 })
785 .try_join()
786 .await?;
787
788 plain_traces.retain(|t| t.len() > 1);
791 plain_traces.sort_by(|a, b| {
794 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
796 });
797
798 if plain_traces.len() > 1 {
806 let mut i = 0;
807 while i < plain_traces.len() - 1 {
808 let mut j = plain_traces.len() - 1;
809 while j > i {
810 if plain_traces[j].ends_with(&plain_traces[i]) {
811 plain_traces.remove(j);
817 }
818 j -= 1;
819 }
820 i += 1;
821 }
822 }
823
824 Ok(plain_traces)
825}
826
827#[turbo_tasks::value(shared)]
828#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
829pub enum IssueStage {
830 Config,
831 AppStructure,
832 ProcessModule,
833 Load,
835 SourceTransform,
836 Parse,
837 Transform,
839 Analysis,
840 Resolve,
841 Bindings,
842 CodeGen,
843 Unsupported,
844 Misc,
845 Other(RcStr),
846}
847
848impl Display for IssueStage {
849 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
850 match self {
851 IssueStage::Config => write!(f, "config"),
852 IssueStage::Resolve => write!(f, "resolve"),
853 IssueStage::ProcessModule => write!(f, "process module"),
854 IssueStage::Load => write!(f, "load"),
855 IssueStage::SourceTransform => write!(f, "source transform"),
856 IssueStage::Parse => write!(f, "parse"),
857 IssueStage::Transform => write!(f, "transform"),
858 IssueStage::Analysis => write!(f, "analysis"),
859 IssueStage::Bindings => write!(f, "bindings"),
860 IssueStage::CodeGen => write!(f, "code gen"),
861 IssueStage::Unsupported => write!(f, "unsupported"),
862 IssueStage::AppStructure => write!(f, "app structure"),
863 IssueStage::Misc => write!(f, "misc"),
864 IssueStage::Other(s) => write!(f, "{s}"),
865 }
866 }
867}
868
869#[turbo_tasks::value(serialization = "none")]
870#[derive(Clone, Debug, PartialOrd, Ord)]
871pub struct PlainIssue {
872 pub severity: IssueSeverity,
873 pub stage: IssueStage,
874
875 pub title: StyledString,
876 pub file_path: RcStr,
877
878 pub description: Option<StyledString>,
879 pub detail: Option<StyledString>,
880 pub documentation_link: RcStr,
881
882 pub source: Option<PlainIssueSource>,
883 pub import_traces: Vec<PlainTrace>,
884}
885
886fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
887 hasher.write_ref(&issue.severity);
888 hasher.write_ref(&issue.file_path);
889 hasher.write_ref(&issue.stage);
890 hasher.write_ref(&issue.title);
891 hasher.write_ref(&issue.description);
892 hasher.write_ref(&issue.detail);
893 hasher.write_ref(&issue.documentation_link);
894
895 if let Some(source) = &issue.source {
896 hasher.write_value(1_u8);
897 hasher.write_ref(&source.range);
900 } else {
901 hasher.write_value(0_u8);
902 }
903
904 if full {
905 hasher.write_ref(&issue.import_traces);
906 }
907}
908
909impl PlainIssue {
910 pub fn internal_hash_ref(&self, full: bool) -> u64 {
919 let mut hasher = Xxh3Hash64Hasher::new();
920 hash_plain_issue(self, &mut hasher, full);
921 hasher.finish()
922 }
923}
924
925#[turbo_tasks::value_impl]
926impl PlainIssue {
927 #[turbo_tasks::function]
930 pub async fn from_issue(
931 issue: ResolvedVc<Box<dyn Issue>>,
932 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
933 ) -> Result<Vc<Self>> {
934 let description: Option<StyledString> = match *issue.description().await? {
935 Some(description) => Some(description.owned().await?),
936 None => None,
937 };
938 let detail = match *issue.detail().await? {
939 Some(detail) => Some(detail.owned().await?),
940 None => None,
941 };
942 let trait_ref = issue.into_trait_ref().await?;
943
944 let severity = trait_ref.severity();
945
946 Ok(Self::cell(Self {
947 severity,
948 file_path: issue.file_path().to_string().owned().await?,
949 stage: issue.stage().owned().await?,
950 title: issue.title().owned().await?,
951 description,
952 detail,
953 documentation_link: issue.documentation_link().owned().await?,
954 source: {
955 if let Some(s) = &*issue.source().await? {
956 Some(s.into_plain().await?)
957 } else {
958 None
959 }
960 },
961 import_traces: match import_tracer {
962 Some(tracer) => {
963 into_plain_trace(
964 tracer
965 .await?
966 .get_traces(issue.file_path().owned().await?)
967 .await?,
968 )
969 .await?
970 }
971 None => vec![],
972 },
973 }))
974 }
975}
976
977#[turbo_tasks::value(serialization = "none")]
978#[derive(Clone, Debug, PartialOrd, Ord)]
979pub struct PlainIssueSource {
980 pub asset: ReadRef<PlainSource>,
981 pub range: Option<(SourcePos, SourcePos)>,
982}
983
984#[turbo_tasks::value(serialization = "none")]
985#[derive(Clone, Debug, PartialOrd, Ord)]
986pub struct PlainSource {
987 pub ident: ReadRef<RcStr>,
988 #[turbo_tasks(debug_ignore)]
989 pub content: ReadRef<FileContent>,
990}
991
992#[turbo_tasks::value_impl]
993impl PlainSource {
994 #[turbo_tasks::function]
995 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
996 let asset_content = asset.content().await?;
997 let content = match *asset_content {
998 AssetContent::File(file_content) => file_content.await?,
999 AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
1000 };
1001
1002 Ok(PlainSource {
1003 ident: asset.ident().to_string().await?,
1004 content,
1005 }
1006 .cell())
1007 }
1008}
1009
1010#[turbo_tasks::value_trait]
1011pub trait IssueReporter {
1012 #[turbo_tasks::function]
1023 fn report_issues(
1024 self: Vc<Self>,
1025 source: TransientValue<RawVc>,
1026 min_failing_severity: IssueSeverity,
1027 ) -> Vc<bool>;
1028}
1029
1030pub trait CollectibleIssuesExt
1031where
1032 Self: Sized,
1033{
1034 fn peek_issues(self) -> CapturedIssues;
1038
1039 fn drop_issues(self);
1043}
1044
1045impl<T> CollectibleIssuesExt for T
1046where
1047 T: CollectiblesSource + Copy + Send,
1048{
1049 fn peek_issues(self) -> CapturedIssues {
1050 CapturedIssues {
1051 issues: self.peek_collectibles(),
1052
1053 tracer: DelegatingImportTracer {
1054 delegates: self.peek_collectibles(),
1055 }
1056 .resolved_cell(),
1057 }
1058 }
1059
1060 fn drop_issues(self) {
1061 self.drop_collectibles::<Box<dyn Issue>>();
1062 }
1063}
1064
1065pub async fn handle_issues<T: Send>(
1069 source_op: OperationVc<T>,
1070 issue_reporter: Vc<Box<dyn IssueReporter>>,
1071 min_failing_severity: IssueSeverity,
1072 path: Option<&str>,
1073 operation: Option<&str>,
1074) -> Result<()> {
1075 let source_vc = source_op.connect();
1076 let _ = source_op.resolve_strongly_consistent().await?;
1077
1078 let has_fatal = issue_reporter.report_issues(
1079 TransientValue::new(Vc::into_raw(source_vc)),
1080 min_failing_severity,
1081 );
1082
1083 if *has_fatal.await? {
1084 let mut message = "Fatal issue(s) occurred".to_owned();
1085 if let Some(path) = path.as_ref() {
1086 message += &format!(" in {path}");
1087 };
1088 if let Some(operation) = operation.as_ref() {
1089 message += &format!(" ({operation})");
1090 };
1091
1092 Err(anyhow!(message))
1093 } else {
1094 Ok(())
1095 }
1096}
1097
1098fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1099 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1100 Ok(i) => SourcePos {
1101 line: i as u32,
1102 column: 0,
1103 },
1104 Err(i) => {
1105 if i == 0 {
1106 SourcePos {
1107 line: 0,
1108 column: offset,
1109 }
1110 } else {
1111 let line = &lines[i - 1];
1112 SourcePos {
1113 line: (i - 1) as u32,
1114 column: min(line.content.len() as u32, offset - line.bytes_offset),
1115 }
1116 }
1117 }
1118 }
1119}
1120
1121fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1122 let line = &lines[pos.line as usize];
1123 line.bytes_offset + pos.column
1124}