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 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, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc, TaskInput,
19 TransientValue, TryFlatJoinIterExt, TryJoinIterExt, Upcast, ValueDefault, ValueToString, Vc,
20 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 generated_code_source::GeneratedCodeSource,
32 ident::{AssetIdent, Layer},
33 source::Source,
34 source_map::{GenerateSourceMap, SourceMap, TokenWithSource},
35 source_pos::SourcePos,
36};
37
38#[turbo_tasks::value(shared)]
39#[derive(
40 PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput, Serialize, Deserialize,
41)]
42#[serde(rename_all = "camelCase")]
43pub enum IssueSeverity {
44 Bug,
45 Fatal,
46 Error,
47 Warning,
48 Hint,
49 Note,
50 Suggestion,
51 Info,
52}
53
54impl IssueSeverity {
55 pub fn as_str(&self) -> &'static str {
56 match self {
57 IssueSeverity::Bug => "bug",
58 IssueSeverity::Fatal => "fatal",
59 IssueSeverity::Error => "error",
60 IssueSeverity::Warning => "warning",
61 IssueSeverity::Hint => "hint",
62 IssueSeverity::Note => "note",
63 IssueSeverity::Suggestion => "suggestion",
64 IssueSeverity::Info => "info",
65 }
66 }
67
68 pub fn as_help_str(&self) -> &'static str {
69 match self {
70 IssueSeverity::Bug => "bug in implementation",
71 IssueSeverity::Fatal => "unrecoverable problem",
72 IssueSeverity::Error => "problem that cause a broken result",
73 IssueSeverity::Warning => "problem should be addressed in short term",
74 IssueSeverity::Hint => "idea for improvement",
75 IssueSeverity::Note => "detail that is worth mentioning",
76 IssueSeverity::Suggestion => "change proposal for improvement",
77 IssueSeverity::Info => "detail that is worth telling",
78 }
79 }
80}
81
82impl Display for IssueSeverity {
83 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
84 f.write_str(self.as_str())
85 }
86}
87
88#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
92#[turbo_tasks::value(shared)]
93pub enum StyledString {
94 Line(Vec<StyledString>),
98 Stack(Vec<StyledString>),
101 Text(RcStr),
103 Code(RcStr),
106 Strong(RcStr),
108}
109
110impl StyledString {
111 pub fn to_unstyled_string(&self) -> String {
112 match self {
113 StyledString::Line(items) => items
114 .iter()
115 .map(|item| item.to_unstyled_string())
116 .collect::<Vec<_>>()
117 .join(""),
118 StyledString::Stack(items) => items
119 .iter()
120 .map(|item| item.to_unstyled_string())
121 .collect::<Vec<_>>()
122 .join("\n"),
123 StyledString::Text(s) | StyledString::Code(s) | StyledString::Strong(s) => {
124 s.to_string()
125 }
126 }
127 }
128}
129
130#[turbo_tasks::value_trait]
131pub trait Issue {
132 fn severity(&self) -> IssueSeverity {
135 IssueSeverity::Error
136 }
137
138 #[turbo_tasks::function]
141 fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
142
143 #[turbo_tasks::function]
146 fn stage(self: Vc<Self>) -> Vc<IssueStage>;
147
148 #[turbo_tasks::function]
153 fn title(self: Vc<Self>) -> Vc<StyledString>;
154
155 #[turbo_tasks::function]
159 fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
160 Vc::cell(None)
161 }
162
163 #[turbo_tasks::function]
167 fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
168 Vc::cell(None)
169 }
170
171 #[turbo_tasks::function]
174 fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
175 Vc::<RcStr>::default()
176 }
177
178 #[turbo_tasks::function]
182 fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
183 Vc::cell(None)
184 }
185
186 #[turbo_tasks::function]
191 fn additional_sources(self: Vc<Self>) -> Vc<AdditionalIssueSources> {
192 AdditionalIssueSources::empty()
193 }
194}
195
196#[turbo_tasks::value_trait]
198pub trait ImportTracer {
199 #[turbo_tasks::function]
200 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
201}
202
203#[turbo_tasks::value]
204#[derive(Debug)]
205pub struct DelegatingImportTracer {
206 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
207}
208
209impl DelegatingImportTracer {
210 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
211 Ok(self
212 .delegates
213 .iter()
214 .map(|d| d.get_traces(path.clone()))
215 .try_join()
216 .await?
217 .iter()
218 .flat_map(|v| v.0.iter().cloned())
219 .collect())
220 }
221}
222
223pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
224
225#[turbo_tasks::value(shared)]
226pub struct ImportTraces(pub Vec<ImportTrace>);
227
228#[turbo_tasks::value_impl]
229impl ValueDefault for ImportTraces {
230 #[turbo_tasks::function]
231 fn value_default() -> Vc<Self> {
232 Self::cell(ImportTraces(vec![]))
233 }
234}
235
236pub trait IssueExt {
237 fn emit(self);
238}
239
240impl<T> IssueExt for ResolvedVc<T>
241where
242 T: Upcast<Box<dyn Issue>>,
243{
244 fn emit(self) {
245 emit(ResolvedVc::upcast_non_strict::<Box<dyn Issue>>(self));
246 }
247}
248
249#[turbo_tasks::value(transparent)]
250pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
251
252#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
254pub enum IgnoreIssuePattern {
255 ExactString(RcStr),
257 Glob(Glob),
259 Regex(EsRegex),
261}
262
263impl IgnoreIssuePattern {
264 pub fn matches(&self, value: &str) -> bool {
266 match self {
267 IgnoreIssuePattern::ExactString(s) => value == s.as_str(),
268 IgnoreIssuePattern::Glob(glob) => glob.matches(value),
269 IgnoreIssuePattern::Regex(regex) => regex.is_match(value),
270 }
271 }
272}
273
274#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
277pub struct IgnoreIssue {
278 pub path: IgnoreIssuePattern,
280 pub title: Option<IgnoreIssuePattern>,
282 pub description: Option<IgnoreIssuePattern>,
284}
285
286#[turbo_tasks::value(shared)]
287pub struct IssueFilter {
288 severity: IssueSeverity,
290 foreign_severity: IssueSeverity,
292 ignore_rules: Vec<IgnoreIssue>,
294}
295
296#[turbo_tasks::value_impl]
297impl IssueFilter {
298 #[turbo_tasks::function]
300 pub fn everything() -> Vc<Self> {
301 IssueFilter {
302 severity: IssueSeverity::Info,
303 foreign_severity: IssueSeverity::Info,
304 ignore_rules: Vec::new(),
305 }
306 .cell()
307 }
308
309 #[turbo_tasks::function]
311 pub async fn matches(&self, issue: ResolvedVc<Box<dyn Issue>>) -> Result<Vc<bool>> {
312 let has_no_ignore_rules = self.ignore_rules.is_empty();
313 let is_everything = self.severity == IssueSeverity::Info
314 && self.foreign_severity == IssueSeverity::Info
315 && has_no_ignore_rules;
316
317 if is_everything {
318 return Ok(Vc::cell(true));
319 }
320
321 let file_path = issue.file_path().await?;
324
325 let severity = issue.into_trait_ref().await?.severity();
328 let severity_allowed = if severity <= self.severity || severity <= self.foreign_severity {
330 if severity <= self.severity && severity <= self.foreign_severity {
333 true
335 } else if ContextCondition::InNodeModules.matches(&file_path) {
336 severity <= self.foreign_severity
337 } else {
338 severity <= self.severity
339 }
340 } else {
341 false
343 };
344
345 if !severity_allowed {
346 return Ok(Vc::cell(false));
347 }
348
349 if !has_no_ignore_rules {
353 let file_path_str = file_path.to_string();
354 let mut title_str: Option<String> = None;
355 let mut description_text: Option<Option<String>> = None;
356
357 for rule in &self.ignore_rules {
358 if !rule.path.matches(&file_path_str) {
359 continue;
360 }
361 if let Some(ref title_pat) = rule.title {
362 if title_str.is_none() {
363 title_str = Some(issue.title().await?.to_unstyled_string());
364 }
365 if !title_pat.matches(title_str.as_deref().unwrap()) {
366 continue;
367 }
368 }
369 if let Some(ref desc_pat) = rule.description {
370 if description_text.is_none() {
371 let desc_opt = issue.description().await?;
372 description_text = Some(match desc_opt.as_ref() {
373 Some(desc_vc) => Some(desc_vc.await?.to_unstyled_string()),
374 None => None,
375 });
376 }
377 match description_text.as_ref().unwrap().as_deref() {
378 Some(desc) if desc_pat.matches(desc) => {}
379 _ => continue,
380 }
381 }
382 return Ok(Vc::cell(false));
384 }
385 }
386
387 Ok(Vc::cell(true))
388 }
389}
390
391impl IssueFilter {
392 pub fn warnings_and_foreign_errors() -> Self {
394 IssueFilter {
395 severity: IssueSeverity::Warning,
396 foreign_severity: IssueSeverity::Error,
397 ignore_rules: Vec::new(),
398 }
399 }
400
401 pub fn with_ignore_rules(mut self, rules: Vec<IgnoreIssue>) -> Self {
403 self.ignore_rules = rules;
404 self
405 }
406}
407
408#[turbo_tasks::value(shared)]
410#[derive(Debug)]
411pub struct CapturedIssues {
412 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
413 tracer: ResolvedVc<DelegatingImportTracer>,
414}
415
416impl CapturedIssues {
417 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
419 self.issues.iter().copied()
420 }
421
422 pub async fn get_plain_issues(
424 &self,
425 filter: Vc<IssueFilter>,
426 ) -> Result<Vec<ReadRef<PlainIssue>>> {
427 let mut list = self
428 .issues
429 .iter()
430 .map(async |issue| {
431 if *filter.matches(**issue).await? {
432 Ok(Some(
433 PlainIssue::from_issue(**issue, Some(*self.tracer)).await?,
434 ))
435 } else {
436 Ok(None)
437 }
438 })
439 .try_flat_join()
440 .await?;
441 list.sort();
442 Ok(list)
443 }
444}
445
446#[derive(
447 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
448)]
449pub struct IssueSource {
450 source: ResolvedVc<Box<dyn Source>>,
451 range: Option<SourceRange>,
452}
453
454#[derive(
456 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
457)]
458enum SourceRange {
459 LineColumn(SourcePos, SourcePos),
460 ByteOffset(u32, u32),
461}
462
463impl IssueSource {
464 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
467 IssueSource {
468 source,
469 range: None,
470 }
471 }
472
473 pub fn from_line_col(
474 source: ResolvedVc<Box<dyn Source>>,
475 start: SourcePos,
476 end: SourcePos,
477 ) -> Self {
478 IssueSource {
479 source,
480 range: Some(SourceRange::LineColumn(start, end)),
481 }
482 }
483
484 pub fn from_single_line_col(source: ResolvedVc<Box<dyn Source>>, pos: SourcePos) -> Self {
485 IssueSource {
486 source,
487 range: Some(SourceRange::LineColumn(
488 pos,
489 SourcePos {
490 line: pos.line,
491 column: pos.column + 1,
493 },
494 )),
495 }
496 }
497
498 async fn into_plain(self) -> Result<PlainIssueSource> {
499 let Self { mut source, range } = self;
500
501 let range = if let Some(range) = range {
502 let mut range = match range {
503 SourceRange::LineColumn(start, end) => Some((start, end)),
504 SourceRange::ByteOffset(start, end) => {
505 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
506 let start = find_line_and_column(lines.as_ref(), start);
507 let end = find_line_and_column(lines.as_ref(), end);
508 Some((start, end))
509 } else {
510 None
511 }
512 }
513 };
514
515 if let Some((start, end)) = range {
517 let mapped = source_pos(source, start, end).await?;
518
519 if let Some((mapped_source, start, end)) = mapped {
520 range = Some((start, end));
521 source = mapped_source;
522 }
523 }
524 range
525 } else {
526 None
527 };
528 Ok(PlainIssueSource {
529 asset: PlainSource::from_source(*source).await?,
530 range,
531 })
532 }
533
534 pub fn from_unparsable_json(
537 source: ResolvedVc<Box<dyn Source>>,
538 error: &UnparsableJson,
539 ) -> Self {
540 match (error.start_location, error.end_location) {
541 (None, None) => Self::from_source_only(source),
542 (Some((line, column)), None) | (None, Some((line, column))) => Self::from_line_col(
543 source,
544 SourcePos { line, column },
545 SourcePos { line, column },
546 ),
547 (Some((start_line, start_column)), Some((end_line, end_column))) => {
548 Self::from_line_col(
549 source,
550 SourcePos {
551 line: start_line,
552 column: start_column,
553 },
554 SourcePos {
555 line: end_line,
556 column: end_column,
557 },
558 )
559 }
560 }
561 }
562
563 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
572 IssueSource {
573 source,
574 range: match (start == 0, end == 0) {
575 (true, true) => None,
576 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
577 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
578 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
579 },
580 }
581 }
582
583 pub async fn from_byte_offset(
593 source: ResolvedVc<Box<dyn Source>>,
594 start: u32,
595 end: u32,
596 ) -> Result<Self> {
597 Ok(IssueSource {
598 source,
599 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
600 let start = find_line_and_column(lines.as_ref(), start);
601 let end = find_line_and_column(lines.as_ref(), end);
602 Some(SourceRange::LineColumn(start, end))
603 } else {
604 None
605 },
606 })
607 }
608
609 pub fn file_path(&self) -> Vc<FileSystemPath> {
611 self.source.ident().path()
612 }
613
614 pub async fn to_generated_code_source(&self) -> Result<Option<AdditionalIssueSource>> {
619 if ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source).is_some() {
620 let description = self.source.description().await?;
621 let generated = Vc::upcast::<Box<dyn Source>>(GeneratedCodeSource::new(*self.source))
622 .to_resolved()
623 .await?;
624 return Ok(Some(AdditionalIssueSource {
625 description: format!("Generated code of {}", description).into(),
626 source: IssueSource {
627 source: generated,
628 range: self.range,
633 },
634 }));
635 }
636 Ok(None)
637 }
638}
639
640impl IssueSource {
641 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
643 Ok(match &self.range {
644 Some(range) => match range {
645 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
646 SourceRange::LineColumn(start, end) => {
647 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
648 let start = find_offset(lines.as_ref(), *start) + 1;
649 let end = find_offset(lines.as_ref(), *end) + 1;
650 Some((start, end))
651 } else {
652 None
653 }
654 }
655 },
656 _ => None,
657 })
658 }
659}
660
661async fn source_pos(
662 source: ResolvedVc<Box<dyn Source>>,
663 start: SourcePos,
664 end: SourcePos,
665) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
666 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
667 return Ok(None);
668 };
669
670 let srcmap = generator.generate_source_map();
671 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
672 return Ok(None);
673 };
674
675 let find = async |line: u32, col: u32| {
676 let TokenWithSource {
677 token,
678 source_content,
679 } = &srcmap.lookup_token_and_source(line, col).await?;
680
681 match token {
682 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
683 SourcePos {
684 line: t.generated_line as _,
685 column: t.generated_column as _,
686 },
687 *source_content,
688 )),
689 crate::source_map::Token::Original(t) => anyhow::Ok((
690 SourcePos {
691 line: t.original_line as _,
692 column: t.original_column as _,
693 },
694 *source_content,
695 )),
696 }
697 };
698
699 let (start, content_1) = find(start.line, start.column).await?;
700 let (end, content_2) = find(end.line, end.column).await?;
701
702 let Some((content_1, content_2)) = content_1.zip(content_2) else {
703 return Ok(None);
704 };
705
706 if content_1 != content_2 {
707 return Ok(None);
708 }
709
710 Ok(Some((content_1, start, end)))
711}
712
713#[turbo_tasks::value(transparent)]
714pub struct OptionIssueSource(Option<IssueSource>);
715
716#[turbo_tasks::value(transparent)]
717pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
718
719#[turbo_tasks::value(shared)]
723pub struct AdditionalIssueSource {
724 pub description: RcStr,
725 pub source: IssueSource,
726}
727
728#[turbo_tasks::value(shared, transparent)]
729pub struct AdditionalIssueSources(Vec<AdditionalIssueSource>);
730
731#[turbo_tasks::value_impl]
732impl AdditionalIssueSources {
733 #[turbo_tasks::function]
734 pub fn empty() -> Vc<Self> {
735 Vc::cell(Vec::new())
736 }
737}
738
739#[derive(
741 Serialize,
742 PartialEq,
743 Eq,
744 PartialOrd,
745 Ord,
746 Clone,
747 Debug,
748 TraceRawVcs,
749 NonLocalValue,
750 DeterministicHash,
751)]
752#[serde(rename_all = "camelCase")]
753pub struct PlainTraceItem {
754 pub fs_name: RcStr,
756 pub root_path: RcStr,
758 pub path: RcStr,
760 pub layer: Option<RcStr>,
762}
763
764impl PlainTraceItem {
765 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
766 let fs_path = asset.path.clone();
769 let fs_name = fs_path.fs.to_string().owned().await?;
770 let root_path = fs_path.fs.root().await?.path.clone();
771 let path = fs_path.path.clone();
772 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
773 Ok(Self {
774 fs_name,
775 root_path,
776 path,
777 layer,
778 })
779 }
780}
781
782pub type PlainTrace = Vec<PlainTraceItem>;
783
784async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
786 let mut plain_traces = traces
787 .into_iter()
788 .map(|trace| async move {
789 let mut plain_trace = trace
790 .into_iter()
791 .filter(|asset| {
792 asset.assets.is_empty()
795 })
796 .map(PlainTraceItem::from_asset_ident)
797 .try_join()
798 .await?;
799
800 plain_trace.dedup();
817
818 Ok(plain_trace)
819 })
820 .try_join()
821 .await?;
822
823 plain_traces.retain(|t| t.len() > 1);
826 plain_traces.sort_by(|a, b| {
829 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
831 });
832
833 if plain_traces.len() > 1 {
841 let mut i = 0;
842 while i < plain_traces.len() - 1 {
843 let mut j = plain_traces.len() - 1;
844 while j > i {
845 if plain_traces[j].ends_with(&plain_traces[i]) {
846 plain_traces.remove(j);
852 }
853 j -= 1;
854 }
855 i += 1;
856 }
857 }
858
859 Ok(plain_traces)
860}
861
862#[turbo_tasks::value(shared)]
863#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
864pub enum IssueStage {
865 Config,
866 AppStructure,
867 ProcessModule,
868 Load,
870 SourceTransform,
871 Parse,
872 Transform,
874 Analysis,
875 Resolve,
876 Bindings,
877 CodeGen,
878 Unsupported,
879 Misc,
880 Other(RcStr),
881}
882
883impl Display for IssueStage {
884 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
885 match self {
886 IssueStage::Config => write!(f, "config"),
887 IssueStage::Resolve => write!(f, "resolve"),
888 IssueStage::ProcessModule => write!(f, "process module"),
889 IssueStage::Load => write!(f, "load"),
890 IssueStage::SourceTransform => write!(f, "source transform"),
891 IssueStage::Parse => write!(f, "parse"),
892 IssueStage::Transform => write!(f, "transform"),
893 IssueStage::Analysis => write!(f, "analysis"),
894 IssueStage::Bindings => write!(f, "bindings"),
895 IssueStage::CodeGen => write!(f, "code gen"),
896 IssueStage::Unsupported => write!(f, "unsupported"),
897 IssueStage::AppStructure => write!(f, "app structure"),
898 IssueStage::Misc => write!(f, "misc"),
899 IssueStage::Other(s) => write!(f, "{s}"),
900 }
901 }
902}
903
904#[turbo_tasks::value(serialization = "none")]
905#[derive(Clone, Debug, PartialOrd, Ord)]
906pub struct PlainIssue {
907 pub severity: IssueSeverity,
908 pub stage: IssueStage,
909
910 pub title: StyledString,
911 pub file_path: RcStr,
912
913 pub description: Option<StyledString>,
914 pub detail: Option<StyledString>,
915 pub documentation_link: RcStr,
916
917 pub source: Option<PlainIssueSource>,
918 pub additional_sources: Vec<PlainAdditionalIssueSource>,
919 pub import_traces: Vec<PlainTrace>,
920}
921
922#[turbo_tasks::value(serialization = "none")]
923#[derive(Clone, Debug, PartialOrd, Ord)]
924pub struct PlainAdditionalIssueSource {
925 pub description: RcStr,
926 pub source: PlainIssueSource,
927}
928
929fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
930 hasher.write_ref(&issue.severity);
931 hasher.write_ref(&issue.file_path);
932 hasher.write_ref(&issue.stage);
933 hasher.write_ref(&issue.title);
934 hasher.write_ref(&issue.description);
935 hasher.write_ref(&issue.detail);
936 hasher.write_ref(&issue.documentation_link);
937
938 if let Some(source) = &issue.source {
939 hasher.write_value(1_u8);
940 hasher.write_ref(&source.range);
943 } else {
944 hasher.write_value(0_u8);
945 }
946
947 if full {
954 hasher.write_ref(&issue.import_traces);
955 }
956}
957
958impl PlainIssue {
959 pub fn internal_hash_ref(&self, full: bool) -> u64 {
967 let mut hasher = Xxh3Hash64Hasher::new();
968 hash_plain_issue(self, &mut hasher, full);
969 hasher.finish()
970 }
971}
972
973#[turbo_tasks::value_impl]
974impl PlainIssue {
975 #[turbo_tasks::function]
978 pub async fn from_issue(
979 issue: ResolvedVc<Box<dyn Issue>>,
980 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
981 ) -> Result<Vc<Self>> {
982 let description: Option<StyledString> = match *issue.description().await? {
983 Some(description) => Some(description.owned().await?),
984 None => None,
985 };
986 let detail = match *issue.detail().await? {
987 Some(detail) => Some(detail.owned().await?),
988 None => None,
989 };
990 let trait_ref = issue.into_trait_ref().await?;
991
992 let severity = trait_ref.severity();
993
994 Ok(Self::cell(Self {
995 severity,
996 file_path: issue.file_path().to_string().owned().await?,
997 stage: issue.stage().owned().await?,
998 title: issue.title().owned().await?,
999 description,
1000 detail,
1001 documentation_link: issue.documentation_link().owned().await?,
1002 source: {
1003 if let Some(s) = &*issue.source().await? {
1004 Some(s.into_plain().await?)
1005 } else {
1006 None
1007 }
1008 },
1009 additional_sources: {
1010 let sources = issue.additional_sources().await?;
1011 let mut result = Vec::new();
1012 for s in sources.iter() {
1013 result.push(PlainAdditionalIssueSource {
1014 description: s.description.clone(),
1015 source: s.source.into_plain().await?,
1016 });
1017 }
1018 result
1019 },
1020 import_traces: match import_tracer {
1021 Some(tracer) => {
1022 into_plain_trace(
1023 tracer
1024 .await?
1025 .get_traces(issue.file_path().owned().await?)
1026 .await?,
1027 )
1028 .await?
1029 }
1030 None => vec![],
1031 },
1032 }))
1033 }
1034}
1035
1036#[turbo_tasks::value(serialization = "none")]
1037#[derive(Clone, Debug, PartialOrd, Ord)]
1038pub struct PlainIssueSource {
1039 pub asset: ReadRef<PlainSource>,
1040 pub range: Option<(SourcePos, SourcePos)>,
1041}
1042
1043#[turbo_tasks::value(serialization = "none")]
1044#[derive(Clone, Debug, PartialOrd, Ord)]
1045pub struct PlainSource {
1046 pub ident: ReadRef<RcStr>,
1047 pub file_path: ReadRef<RcStr>,
1048 #[turbo_tasks(debug_ignore)]
1049 pub content: ReadRef<FileContent>,
1050}
1051
1052#[turbo_tasks::value_impl]
1053impl PlainSource {
1054 #[turbo_tasks::function]
1055 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
1056 let asset_content = asset.content().await?;
1057 let content = match *asset_content {
1058 AssetContent::File(file_content) => file_content.await?,
1059 AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
1060 };
1061
1062 Ok(PlainSource {
1063 ident: asset.ident().to_string().await?,
1064 file_path: asset.ident().path().to_string().await?,
1065 content,
1066 }
1067 .cell())
1068 }
1069}
1070
1071#[turbo_tasks::value_trait]
1072pub trait IssueReporter {
1073 #[turbo_tasks::function]
1084 fn report_issues(
1085 self: Vc<Self>,
1086 source: TransientValue<RawVc>,
1087 min_failing_severity: IssueSeverity,
1088 ) -> Vc<bool>;
1089}
1090
1091pub trait CollectibleIssuesExt
1092where
1093 Self: Sized,
1094{
1095 fn peek_issues(self) -> CapturedIssues;
1099
1100 fn drop_issues(self);
1104}
1105
1106impl<T> CollectibleIssuesExt for T
1107where
1108 T: CollectiblesSource + Copy + Send,
1109{
1110 fn peek_issues(self) -> CapturedIssues {
1111 CapturedIssues {
1112 issues: self.peek_collectibles(),
1113
1114 tracer: DelegatingImportTracer {
1115 delegates: self.peek_collectibles(),
1116 }
1117 .resolved_cell(),
1118 }
1119 }
1120
1121 fn drop_issues(self) {
1122 self.drop_collectibles::<Box<dyn Issue>>();
1123 }
1124}
1125
1126pub async fn handle_issues<T: Send>(
1130 source_op: OperationVc<T>,
1131 issue_reporter: Vc<Box<dyn IssueReporter>>,
1132 min_failing_severity: IssueSeverity,
1133 path: Option<&str>,
1134 operation: Option<&str>,
1135) -> Result<()> {
1136 let source_vc = source_op.connect();
1137 let _ = source_op.resolve_strongly_consistent().await?;
1138
1139 let has_fatal = issue_reporter.report_issues(
1140 TransientValue::new(Vc::into_raw(source_vc)),
1141 min_failing_severity,
1142 );
1143
1144 if *has_fatal.await? {
1145 let mut message = "Fatal issue(s) occurred".to_owned();
1146 if let Some(path) = path.as_ref() {
1147 message += &format!(" in {path}");
1148 };
1149 if let Some(operation) = operation.as_ref() {
1150 message += &format!(" ({operation})");
1151 };
1152
1153 bail!(message)
1154 } else {
1155 Ok(())
1156 }
1157}
1158
1159fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1160 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1161 Ok(i) => SourcePos {
1162 line: i as u32,
1163 column: 0,
1164 },
1165 Err(i) => {
1166 if i == 0 {
1167 SourcePos {
1168 line: 0,
1169 column: offset,
1170 }
1171 } else {
1172 let line = &lines[i - 1];
1173 SourcePos {
1174 line: (i - 1) as u32,
1175 column: min(line.content.len() as u32, offset - line.bytes_offset),
1176 }
1177 }
1178 }
1179 }
1180}
1181
1182fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1183 let line = &lines[pos.line as usize];
1184 line.bytes_offset + pos.column
1185}