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, bail};
12use async_trait::async_trait;
13use auto_hash_map::AutoSet;
14use bincode::{Decode, Encode};
15use serde::{Deserialize, Serialize};
16use turbo_esregex::EsRegex;
17use turbo_rcstr::{RcStr, rcstr};
18use turbo_tasks::{
19 CollectiblesSource, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc, TaskInput,
20 TransientValue, TryFlatJoinIterExt, TryJoinIterExt, Upcast, ValueDefault, ValueToString,
21 ValueToStringRef, Vc, emit, trace::TraceRawVcs,
22};
23use turbo_tasks_fs::{
24 FileContent, FileLine, FileLinesContent, FileSystem, FileSystemPath, glob::Glob,
25 json::UnparsableJson,
26};
27use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
28
29use crate::{
30 asset::{Asset, AssetContent},
31 condition::ContextCondition,
32 generated_code_source::GeneratedCodeSource,
33 ident::{AssetIdent, Layer},
34 source::Source,
35 source_map::{GenerateSourceMap, SourceMap, TokenWithSource},
36 source_pos::SourcePos,
37};
38
39#[turbo_tasks::value(shared)]
40#[derive(
41 PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput, Serialize, Deserialize,
42)]
43#[serde(rename_all = "camelCase")]
44pub enum IssueSeverity {
45 Bug,
46 Fatal,
47 Error,
48 Warning,
49 Hint,
50 Note,
51 Suggestion,
52 Info,
53}
54
55impl IssueSeverity {
56 pub fn as_str(&self) -> &'static str {
57 match self {
58 IssueSeverity::Bug => "bug",
59 IssueSeverity::Fatal => "fatal",
60 IssueSeverity::Error => "error",
61 IssueSeverity::Warning => "warning",
62 IssueSeverity::Hint => "hint",
63 IssueSeverity::Note => "note",
64 IssueSeverity::Suggestion => "suggestion",
65 IssueSeverity::Info => "info",
66 }
67 }
68
69 pub fn as_help_str(&self) -> &'static str {
70 match self {
71 IssueSeverity::Bug => "bug in implementation",
72 IssueSeverity::Fatal => "unrecoverable problem",
73 IssueSeverity::Error => "problem that cause a broken result",
74 IssueSeverity::Warning => "problem should be addressed in short term",
75 IssueSeverity::Hint => "idea for improvement",
76 IssueSeverity::Note => "detail that is worth mentioning",
77 IssueSeverity::Suggestion => "change proposal for improvement",
78 IssueSeverity::Info => "detail that is worth telling",
79 }
80 }
81}
82
83impl Display for IssueSeverity {
84 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
85 f.write_str(self.as_str())
86 }
87}
88
89#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
93#[turbo_tasks::value(shared)]
94pub enum StyledString {
95 Line(Vec<StyledString>),
99 Stack(Vec<StyledString>),
102 Text(RcStr),
104 Code(RcStr),
107 Strong(RcStr),
109}
110
111impl StyledString {
112 pub fn to_unstyled_string(&self) -> String {
113 match self {
114 StyledString::Line(items) => items
115 .iter()
116 .map(|item| item.to_unstyled_string())
117 .collect::<Vec<_>>()
118 .join(""),
119 StyledString::Stack(items) => items
120 .iter()
121 .map(|item| item.to_unstyled_string())
122 .collect::<Vec<_>>()
123 .join("\n"),
124 StyledString::Text(s) | StyledString::Code(s) | StyledString::Strong(s) => {
125 s.to_string()
126 }
127 }
128 }
129}
130
131#[async_trait]
132#[turbo_tasks::value_trait]
133pub trait Issue {
134 fn severity(&self) -> IssueSeverity {
137 IssueSeverity::Error
138 }
139
140 async fn file_path(&self) -> Result<FileSystemPath>;
143
144 fn stage(&self) -> IssueStage;
147
148 async fn title(&self) -> Result<StyledString>;
152
153 async fn description(&self) -> Result<Option<StyledString>> {
156 Ok(None)
157 }
158
159 async fn detail(&self) -> Result<Option<StyledString>> {
163 Ok(None)
164 }
165
166 fn documentation_link(&self) -> RcStr {
169 rcstr!("")
170 }
171
172 fn source(&self) -> Option<IssueSource> {
176 None
177 }
178
179 async fn additional_sources(&self) -> Result<Vec<AdditionalIssueSource>> {
184 Ok(vec![])
185 }
186}
187
188#[turbo_tasks::value_trait]
190pub trait ImportTracer {
191 #[turbo_tasks::function]
192 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
193}
194
195#[turbo_tasks::value]
196#[derive(Debug)]
197pub struct DelegatingImportTracer {
198 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
199}
200
201impl DelegatingImportTracer {
202 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
203 Ok(self
204 .delegates
205 .iter()
206 .map(|d| d.get_traces(path.clone()))
207 .try_join()
208 .await?
209 .iter()
210 .flat_map(|v| v.0.iter().cloned())
211 .collect())
212 }
213}
214
215pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
216
217#[turbo_tasks::value(shared)]
218pub struct ImportTraces(pub Vec<ImportTrace>);
219
220#[turbo_tasks::value_impl]
221impl ValueDefault for ImportTraces {
222 #[turbo_tasks::function]
223 fn value_default() -> Vc<Self> {
224 Self::cell(ImportTraces(vec![]))
225 }
226}
227
228pub trait IssueExt {
229 fn emit(self);
230}
231
232impl<T> IssueExt for ResolvedVc<T>
233where
234 T: Upcast<Box<dyn Issue>>,
235{
236 fn emit(self) {
237 emit(ResolvedVc::upcast_non_strict::<Box<dyn Issue>>(self));
238 }
239}
240
241#[turbo_tasks::value(transparent)]
242pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
243
244#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
246pub enum IgnoreIssuePattern {
247 ExactString(RcStr),
249 Glob(Glob),
251 Regex(EsRegex),
253}
254
255impl IgnoreIssuePattern {
256 pub fn matches(&self, value: &str) -> bool {
258 match self {
259 IgnoreIssuePattern::ExactString(s) => value == s.as_str(),
260 IgnoreIssuePattern::Glob(glob) => glob.matches(value),
261 IgnoreIssuePattern::Regex(regex) => regex.is_match(value),
262 }
263 }
264}
265
266#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
269pub struct IgnoreIssue {
270 pub path: IgnoreIssuePattern,
272 pub title: Option<IgnoreIssuePattern>,
274 pub description: Option<IgnoreIssuePattern>,
276}
277
278#[turbo_tasks::value(shared)]
279pub struct IssueFilter {
280 severity: IssueSeverity,
282 foreign_severity: IssueSeverity,
284 ignore_rules: Vec<IgnoreIssue>,
286}
287
288#[turbo_tasks::value_impl]
289impl IssueFilter {
290 #[turbo_tasks::function]
292 pub fn everything() -> Vc<Self> {
293 IssueFilter {
294 severity: IssueSeverity::Info,
295 foreign_severity: IssueSeverity::Info,
296 ignore_rules: Vec::new(),
297 }
298 .cell()
299 }
300
301 #[turbo_tasks::function]
303 pub async fn matches(&self, issue: ResolvedVc<Box<dyn Issue>>) -> Result<Vc<bool>> {
304 let has_no_ignore_rules = self.ignore_rules.is_empty();
305 let is_everything = self.severity == IssueSeverity::Info
306 && self.foreign_severity == IssueSeverity::Info
307 && has_no_ignore_rules;
308
309 if is_everything {
310 return Ok(Vc::cell(true));
311 }
312
313 let trait_ref = issue.into_trait_ref().await?;
314
315 let file_path = trait_ref.file_path().await?;
318
319 let severity = trait_ref.severity();
322 let severity_allowed = if severity <= self.severity || severity <= self.foreign_severity {
324 if severity <= self.severity && severity <= self.foreign_severity {
327 true
329 } else if ContextCondition::InNodeModules.matches(&file_path) {
330 severity <= self.foreign_severity
331 } else {
332 severity <= self.severity
333 }
334 } else {
335 false
337 };
338
339 if !severity_allowed {
340 return Ok(Vc::cell(false));
341 }
342
343 if !has_no_ignore_rules {
347 let file_path_str = file_path.to_string();
348 let mut title_str: Option<String> = None;
349 let mut description_text: Option<Option<String>> = None;
350
351 for rule in &self.ignore_rules {
352 if !rule.path.matches(&file_path_str) {
353 continue;
354 }
355 if let Some(ref title_pat) = rule.title {
356 if title_str.is_none() {
357 title_str = Some(trait_ref.title().await?.to_unstyled_string());
358 }
359 if !title_pat.matches(title_str.as_deref().unwrap()) {
360 continue;
361 }
362 }
363 if let Some(ref desc_pat) = rule.description {
364 if description_text.is_none() {
365 description_text = Some(
366 trait_ref
367 .description()
368 .await?
369 .map(|s| s.to_unstyled_string()),
370 );
371 }
372 match description_text.as_ref().unwrap().as_deref() {
373 Some(desc) if desc_pat.matches(desc) => {}
374 _ => continue,
375 }
376 }
377 return Ok(Vc::cell(false));
379 }
380 }
381
382 Ok(Vc::cell(true))
383 }
384}
385
386impl IssueFilter {
387 pub fn warnings_and_foreign_errors() -> Self {
389 IssueFilter {
390 severity: IssueSeverity::Warning,
391 foreign_severity: IssueSeverity::Error,
392 ignore_rules: Vec::new(),
393 }
394 }
395
396 pub fn with_ignore_rules(mut self, rules: Vec<IgnoreIssue>) -> Self {
398 self.ignore_rules = rules;
399 self
400 }
401}
402
403#[turbo_tasks::value(shared)]
405#[derive(Debug)]
406pub struct CapturedIssues {
407 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
408 tracer: ResolvedVc<DelegatingImportTracer>,
409}
410
411impl CapturedIssues {
412 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
414 self.issues.iter().copied()
415 }
416
417 pub async fn get_plain_issues(
419 &self,
420 filter: Vc<IssueFilter>,
421 ) -> Result<Vec<ReadRef<PlainIssue>>> {
422 let mut list = self
423 .issues
424 .iter()
425 .map(async |issue| {
426 if *filter.matches(**issue).await? {
427 Ok(Some(
428 PlainIssue::from_issue(**issue, Some(*self.tracer)).await?,
429 ))
430 } else {
431 Ok(None)
432 }
433 })
434 .try_flat_join()
435 .await?;
436 list.sort();
437 Ok(list)
438 }
439}
440
441#[derive(
442 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
443)]
444pub struct IssueSource {
445 source: ResolvedVc<Box<dyn Source>>,
446 range: Option<SourceRange>,
447}
448
449#[derive(
451 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
452)]
453enum SourceRange {
454 LineColumn(SourcePos, SourcePos),
455 ByteOffset(u32, u32),
456}
457
458impl IssueSource {
459 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
462 IssueSource {
463 source,
464 range: None,
465 }
466 }
467
468 pub fn from_line_col(
469 source: ResolvedVc<Box<dyn Source>>,
470 start: SourcePos,
471 end: SourcePos,
472 ) -> Self {
473 IssueSource {
474 source,
475 range: Some(SourceRange::LineColumn(start, end)),
476 }
477 }
478
479 pub fn from_single_line_col(source: ResolvedVc<Box<dyn Source>>, pos: SourcePos) -> Self {
480 IssueSource {
481 source,
482 range: Some(SourceRange::LineColumn(
483 pos,
484 SourcePos {
485 line: pos.line,
486 column: pos.column + 1,
488 },
489 )),
490 }
491 }
492
493 async fn into_plain(self) -> Result<PlainIssueSource> {
494 let Self { mut source, range } = self;
495
496 let range = if let Some(range) = range {
497 let mut range = match range {
498 SourceRange::LineColumn(start, end) => Some((start, end)),
499 SourceRange::ByteOffset(start, end) => {
500 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
501 let start = find_line_and_column(lines.as_ref(), start);
502 let end = find_line_and_column(lines.as_ref(), end);
503 Some((start, end))
504 } else {
505 None
506 }
507 }
508 };
509
510 if let Some((start, end)) = range {
512 let mapped = source_pos(source, start, end).await?;
513
514 if let Some((mapped_source, start, end)) = mapped {
515 range = Some((start, end));
516 source = mapped_source;
517 }
518 }
519 range
520 } else {
521 None
522 };
523 Ok(PlainIssueSource {
524 asset: PlainSource::from_source(*source).await?,
525 range,
526 })
527 }
528
529 pub fn from_unparsable_json(
532 source: ResolvedVc<Box<dyn Source>>,
533 error: &UnparsableJson,
534 ) -> Self {
535 match (error.start_location, error.end_location) {
536 (None, None) => Self::from_source_only(source),
537 (Some((line, column)), None) | (None, Some((line, column))) => Self::from_line_col(
538 source,
539 SourcePos { line, column },
540 SourcePos { line, column },
541 ),
542 (Some((start_line, start_column)), Some((end_line, end_column))) => {
543 Self::from_line_col(
544 source,
545 SourcePos {
546 line: start_line,
547 column: start_column,
548 },
549 SourcePos {
550 line: end_line,
551 column: end_column,
552 },
553 )
554 }
555 }
556 }
557
558 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
567 IssueSource {
568 source,
569 range: match (start == 0, end == 0) {
570 (true, true) => None,
571 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
572 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
573 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
574 },
575 }
576 }
577
578 pub async fn from_byte_offset(
588 source: ResolvedVc<Box<dyn Source>>,
589 start: u32,
590 end: u32,
591 ) -> Result<Self> {
592 Ok(IssueSource {
593 source,
594 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
595 let start = find_line_and_column(lines.as_ref(), start);
596 let end = find_line_and_column(lines.as_ref(), end);
597 Some(SourceRange::LineColumn(start, end))
598 } else {
599 None
600 },
601 })
602 }
603
604 pub fn file_path(&self) -> Vc<FileSystemPath> {
606 self.source.ident().path()
607 }
608
609 pub async fn to_generated_code_source(&self) -> Result<Option<AdditionalIssueSource>> {
614 if ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source).is_some() {
615 let description = self.source.description().await?;
616 let generated = Vc::upcast::<Box<dyn Source>>(GeneratedCodeSource::new(*self.source))
617 .to_resolved()
618 .await?;
619 return Ok(Some(AdditionalIssueSource {
620 description: format!("Generated code of {}", description).into(),
621 source: IssueSource {
622 source: generated,
623 range: self.range,
628 },
629 }));
630 }
631 Ok(None)
632 }
633}
634
635impl IssueSource {
636 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
638 Ok(match &self.range {
639 Some(range) => match range {
640 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
641 SourceRange::LineColumn(start, end) => {
642 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
643 let start = find_offset(lines.as_ref(), *start) + 1;
644 let end = find_offset(lines.as_ref(), *end) + 1;
645 Some((start, end))
646 } else {
647 None
648 }
649 }
650 },
651 _ => None,
652 })
653 }
654}
655
656async fn source_pos(
657 source: ResolvedVc<Box<dyn Source>>,
658 start: SourcePos,
659 end: SourcePos,
660) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
661 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
662 return Ok(None);
663 };
664
665 let srcmap = generator.generate_source_map();
666 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
667 return Ok(None);
668 };
669
670 let find = async |line: u32, col: u32| {
671 let TokenWithSource {
672 token,
673 source_content,
674 } = &srcmap.lookup_token_and_source(line, col).await?;
675
676 match token {
677 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
678 SourcePos {
679 line: t.generated_line as _,
680 column: t.generated_column as _,
681 },
682 *source_content,
683 )),
684 crate::source_map::Token::Original(t) => anyhow::Ok((
685 SourcePos {
686 line: t.original_line as _,
687 column: t.original_column as _,
688 },
689 *source_content,
690 )),
691 }
692 };
693
694 let (start, content_1) = find(start.line, start.column).await?;
695 let (end, content_2) = find(end.line, end.column).await?;
696
697 let Some((content_1, content_2)) = content_1.zip(content_2) else {
698 return Ok(None);
699 };
700
701 if content_1 != content_2 {
702 return Ok(None);
703 }
704
705 Ok(Some((content_1, start, end)))
706}
707
708#[turbo_tasks::value(shared)]
712pub struct AdditionalIssueSource {
713 pub description: RcStr,
714 pub source: IssueSource,
715}
716
717#[turbo_tasks::value(shared, transparent)]
718pub struct AdditionalIssueSources(Vec<AdditionalIssueSource>);
719
720#[turbo_tasks::value_impl]
721impl AdditionalIssueSources {
722 #[turbo_tasks::function]
723 pub fn empty() -> Vc<Self> {
724 Vc::cell(Vec::new())
725 }
726}
727
728#[derive(
730 Serialize,
731 PartialEq,
732 Eq,
733 PartialOrd,
734 Ord,
735 Clone,
736 Debug,
737 TraceRawVcs,
738 NonLocalValue,
739 DeterministicHash,
740)]
741#[serde(rename_all = "camelCase")]
742pub struct PlainTraceItem {
743 pub fs_name: RcStr,
745 pub root_path: RcStr,
747 pub path: RcStr,
749 pub layer: Option<RcStr>,
751}
752
753impl PlainTraceItem {
754 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
755 let fs_path = asset.path.clone();
758 let fs_name = fs_path.fs.to_string().owned().await?;
759 let root_path = fs_path.fs.root().await?.path.clone();
760 let path = fs_path.path.clone();
761 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
762 Ok(Self {
763 fs_name,
764 root_path,
765 path,
766 layer,
767 })
768 }
769}
770
771pub type PlainTrace = Vec<PlainTraceItem>;
772
773async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
775 let mut plain_traces = traces
776 .into_iter()
777 .map(|trace| async move {
778 let mut plain_trace = trace
779 .into_iter()
780 .filter(|asset| {
781 asset.assets.is_empty()
784 })
785 .map(PlainTraceItem::from_asset_ident)
786 .try_join()
787 .await?;
788
789 plain_trace.dedup();
806
807 Ok(plain_trace)
808 })
809 .try_join()
810 .await?;
811
812 plain_traces.retain(|t| t.len() > 1);
815 plain_traces.sort_by(|a, b| {
818 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
820 });
821
822 if plain_traces.len() > 1 {
830 let mut i = 0;
831 while i < plain_traces.len() - 1 {
832 let mut j = plain_traces.len() - 1;
833 while j > i {
834 if plain_traces[j].ends_with(&plain_traces[i]) {
835 plain_traces.remove(j);
841 }
842 j -= 1;
843 }
844 i += 1;
845 }
846 }
847
848 Ok(plain_traces)
849}
850
851#[turbo_tasks::value(shared)]
852#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
853pub enum IssueStage {
854 Config,
855 AppStructure,
856 ProcessModule,
857 Load,
859 SourceTransform,
860 Parse,
861 Transform,
863 Analysis,
864 Resolve,
865 Bindings,
866 CodeGen,
867 Emit,
868 Unsupported,
869 Misc,
870 Other(RcStr),
871}
872
873impl Display for IssueStage {
874 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
875 match self {
876 IssueStage::Config => write!(f, "config"),
877 IssueStage::Resolve => write!(f, "resolve"),
878 IssueStage::ProcessModule => write!(f, "process module"),
879 IssueStage::Load => write!(f, "load"),
880 IssueStage::SourceTransform => write!(f, "source transform"),
881 IssueStage::Parse => write!(f, "parse"),
882 IssueStage::Transform => write!(f, "transform"),
883 IssueStage::Analysis => write!(f, "analysis"),
884 IssueStage::Bindings => write!(f, "bindings"),
885 IssueStage::CodeGen => write!(f, "code gen"),
886 IssueStage::Emit => write!(f, "emit"),
887 IssueStage::Unsupported => write!(f, "unsupported"),
888 IssueStage::AppStructure => write!(f, "app structure"),
889 IssueStage::Misc => write!(f, "misc"),
890 IssueStage::Other(s) => write!(f, "{s}"),
891 }
892 }
893}
894
895#[turbo_tasks::value(serialization = "skip")]
896#[derive(Clone, Debug, PartialOrd, Ord)]
897pub struct PlainIssue {
898 pub severity: IssueSeverity,
899 pub stage: IssueStage,
900
901 pub title: StyledString,
902 pub file_path: RcStr,
903
904 pub description: Option<StyledString>,
905 pub detail: Option<StyledString>,
906 pub documentation_link: RcStr,
907
908 pub source: Option<PlainIssueSource>,
909 pub additional_sources: Vec<PlainAdditionalIssueSource>,
910 pub import_traces: Vec<PlainTrace>,
911}
912
913#[turbo_tasks::value(serialization = "skip")]
914#[derive(Clone, Debug, PartialOrd, Ord)]
915pub struct PlainAdditionalIssueSource {
916 pub description: RcStr,
917 pub source: PlainIssueSource,
918}
919
920fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
921 hasher.write_ref(&issue.severity);
922 hasher.write_ref(&issue.file_path);
923 hasher.write_ref(&issue.stage);
924 hasher.write_ref(&issue.title);
925 hasher.write_ref(&issue.description);
926 hasher.write_ref(&issue.detail);
927 hasher.write_ref(&issue.documentation_link);
928
929 if let Some(source) = &issue.source {
930 hasher.write_value(1_u8);
931 hasher.write_ref(&source.range);
934 } else {
935 hasher.write_value(0_u8);
936 }
937
938 if full {
945 hasher.write_ref(&issue.import_traces);
946 }
947}
948
949impl PlainIssue {
950 pub fn internal_hash_ref(&self, full: bool) -> u64 {
958 let mut hasher = Xxh3Hash64Hasher::new();
959 hash_plain_issue(self, &mut hasher, full);
960 hasher.finish()
961 }
962}
963
964#[turbo_tasks::value_impl]
965impl PlainIssue {
966 #[turbo_tasks::function]
969 pub async fn from_issue(
970 issue: ResolvedVc<Box<dyn Issue>>,
971 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
972 ) -> Result<Vc<Self>> {
973 let trait_ref = issue.into_trait_ref().await?;
974 let severity = trait_ref.severity();
975 let file_path = trait_ref.file_path().await?;
976 let file_path_str = file_path.to_string_ref().await?;
977
978 Ok(Self::cell(Self {
979 severity,
980 file_path: file_path_str,
981 stage: trait_ref.stage(),
982 title: trait_ref.title().await?,
983 description: trait_ref.description().await?,
984 detail: trait_ref.detail().await?,
985 documentation_link: trait_ref.documentation_link(),
986 source: {
987 if let Some(s) = trait_ref.source() {
988 Some(s.into_plain().await?)
989 } else {
990 None
991 }
992 },
993 additional_sources: {
994 trait_ref
995 .additional_sources()
996 .await?
997 .into_iter()
998 .map(async |s| {
999 Ok(PlainAdditionalIssueSource {
1000 source: s.source.into_plain().await?,
1001 description: s.description,
1002 })
1003 })
1004 .try_join()
1005 .await?
1006 },
1007 import_traces: match import_tracer {
1008 Some(tracer) => {
1009 into_plain_trace(tracer.await?.get_traces(file_path).await?).await?
1010 }
1011 None => vec![],
1012 },
1013 }))
1014 }
1015}
1016
1017#[turbo_tasks::value(serialization = "skip")]
1018#[derive(Clone, Debug, PartialOrd, Ord)]
1019pub struct PlainIssueSource {
1020 pub asset: ReadRef<PlainSource>,
1021 pub range: Option<(SourcePos, SourcePos)>,
1022}
1023
1024#[turbo_tasks::value(serialization = "skip")]
1025#[derive(Clone, Debug, PartialOrd, Ord)]
1026pub struct PlainSource {
1027 pub ident: ReadRef<RcStr>,
1028 pub file_path: ReadRef<RcStr>,
1029 #[turbo_tasks(debug_ignore)]
1030 pub content: ReadRef<FileContent>,
1031}
1032
1033#[turbo_tasks::value_impl]
1034impl PlainSource {
1035 #[turbo_tasks::function]
1036 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
1037 let asset_content = asset.content().await?;
1038 let content = match *asset_content {
1039 AssetContent::File(file_content) => file_content.await?,
1040 AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
1041 };
1042
1043 Ok(PlainSource {
1044 ident: asset.ident().to_string().await?,
1045 file_path: asset.ident().path().to_string().await?,
1046 content,
1047 }
1048 .cell())
1049 }
1050}
1051
1052#[turbo_tasks::value_trait]
1053pub trait IssueReporter {
1054 #[turbo_tasks::function]
1065 fn report_issues(
1066 self: Vc<Self>,
1067 source: TransientValue<RawVc>,
1068 min_failing_severity: IssueSeverity,
1069 ) -> Vc<bool>;
1070}
1071
1072pub trait CollectibleIssuesExt
1073where
1074 Self: Sized,
1075{
1076 fn peek_issues(self) -> CapturedIssues;
1080
1081 fn drop_issues(self);
1085}
1086
1087impl<T> CollectibleIssuesExt for T
1088where
1089 T: CollectiblesSource + Copy + Send,
1090{
1091 fn peek_issues(self) -> CapturedIssues {
1092 CapturedIssues {
1093 issues: self.peek_collectibles(),
1094
1095 tracer: DelegatingImportTracer {
1096 delegates: self.peek_collectibles(),
1097 }
1098 .resolved_cell(),
1099 }
1100 }
1101
1102 fn drop_issues(self) {
1103 self.drop_collectibles::<Box<dyn Issue>>();
1104 }
1105}
1106
1107pub async fn handle_issues<T: Send>(
1111 source_op: OperationVc<T>,
1112 issue_reporter: Vc<Box<dyn IssueReporter>>,
1113 min_failing_severity: IssueSeverity,
1114 path: Option<&str>,
1115 operation: Option<&str>,
1116) -> Result<()> {
1117 let source_vc = source_op.connect();
1118 let _ = source_op.resolve().strongly_consistent().await?;
1119
1120 let has_fatal = issue_reporter.report_issues(
1121 TransientValue::new(Vc::into_raw(source_vc)),
1122 min_failing_severity,
1123 );
1124
1125 if *has_fatal.await? {
1126 let mut message = "Fatal issue(s) occurred".to_owned();
1127 if let Some(path) = path.as_ref() {
1128 message += &format!(" in {path}");
1129 };
1130 if let Some(operation) = operation.as_ref() {
1131 message += &format!(" ({operation})");
1132 };
1133
1134 bail!(message)
1135 } else {
1136 Ok(())
1137 }
1138}
1139
1140fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1141 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1142 Ok(i) => SourcePos {
1143 line: i as u32,
1144 column: 0,
1145 },
1146 Err(i) => {
1147 if i == 0 {
1148 SourcePos {
1149 line: 0,
1150 column: offset,
1151 }
1152 } else {
1153 let line = &lines[i - 1];
1154 SourcePos {
1155 line: (i - 1) as u32,
1156 column: min(line.content.len() as u32, offset - line.bytes_offset),
1157 }
1158 }
1159 }
1160 }
1161}
1162
1163fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1164 let line = &lines[pos.line as usize];
1165 line.bytes_offset + pos.column
1166}