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, TransientValue,
20 TryFlatJoinIterExt, TryJoinIterExt, Upcast, ValueDefault, ValueToString, ValueToStringRef, Vc,
21 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, task_input)]
40#[derive(PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, Serialize, Deserialize)]
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#[async_trait]
130#[turbo_tasks::value_trait]
131pub trait Issue {
132 fn severity(&self) -> IssueSeverity {
135 IssueSeverity::Error
136 }
137
138 async fn file_path(&self) -> Result<FileSystemPath>;
141
142 fn stage(&self) -> IssueStage;
145
146 async fn title(&self) -> Result<StyledString>;
150
151 async fn description(&self) -> Result<Option<StyledString>> {
154 Ok(None)
155 }
156
157 async fn detail(&self) -> Result<Option<StyledString>> {
161 Ok(None)
162 }
163
164 fn documentation_link(&self) -> RcStr {
167 rcstr!("")
168 }
169
170 fn source(&self) -> Option<IssueSource> {
174 None
175 }
176
177 async fn additional_sources(&self) -> Result<Vec<AdditionalIssueSource>> {
182 Ok(vec![])
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: Box<[IgnoreIssue]>,
284}
285
286impl IssueFilter {
287 pub fn everything() -> Self {
289 IssueFilter {
290 severity: IssueSeverity::Info,
291 foreign_severity: IssueSeverity::Info,
292 ignore_rules: Box::from([]),
293 }
294 }
295
296 pub fn warnings_and_foreign_errors() -> Self {
298 IssueFilter {
299 severity: IssueSeverity::Warning,
300 foreign_severity: IssueSeverity::Error,
301 ignore_rules: Box::from([]),
302 }
303 }
304
305 pub fn with_ignore_rules(mut self, rules: Box<[IgnoreIssue]>) -> Self {
307 self.ignore_rules = rules;
308 self
309 }
310
311 pub async fn matches(&self, issue: ResolvedVc<Box<dyn Issue>>) -> Result<bool> {
313 Ok(self.matches_all_fast_path()
314 || self
315 .matches_ref_slow_path(&*issue.into_trait_ref().await?)
316 .await?)
317 }
318
319 pub async fn matches_ref(&self, issue: &dyn Issue) -> Result<bool> {
320 Ok(self.matches_all_fast_path() || self.matches_ref_slow_path(issue).await?)
321 }
322
323 fn matches_all_fast_path(&self) -> bool {
324 self.severity == IssueSeverity::Info
325 && self.foreign_severity == IssueSeverity::Info
326 && self.ignore_rules.is_empty()
327 }
328
329 async fn matches_ref_slow_path(&self, issue: &dyn Issue) -> Result<bool> {
330 let file_path = issue.file_path().await?;
333
334 let severity = issue.severity();
337 let severity_allowed = if severity <= self.severity || severity <= self.foreign_severity {
339 if severity <= self.severity && severity <= self.foreign_severity {
342 true
344 } else if ContextCondition::InNodeModules.matches(&file_path) {
345 severity <= self.foreign_severity
346 } else {
347 severity <= self.severity
348 }
349 } else {
350 false
352 };
353
354 if !severity_allowed {
355 return Ok(false);
356 }
357
358 if !self.ignore_rules.is_empty() {
362 let file_path_str = file_path.to_string();
363 let mut title_str: Option<String> = None;
364 let mut description_text: Option<Option<String>> = None;
365
366 for rule in &self.ignore_rules {
367 if !rule.path.matches(&file_path_str) {
368 continue;
369 }
370 if let Some(ref title_pat) = rule.title {
371 if title_str.is_none() {
372 title_str = Some(issue.title().await?.to_unstyled_string());
373 }
374 if !title_pat.matches(title_str.as_deref().unwrap()) {
375 continue;
376 }
377 }
378 if let Some(ref desc_pat) = rule.description {
379 if description_text.is_none() {
380 description_text =
381 Some(issue.description().await?.map(|s| s.to_unstyled_string()));
382 }
383 match description_text.as_ref().unwrap().as_deref() {
384 Some(desc) if desc_pat.matches(desc) => {}
385 _ => continue,
386 }
387 }
388 return Ok(false);
390 }
391 }
392
393 Ok(true)
394 }
395}
396
397#[turbo_tasks::value(shared)]
399#[derive(Debug)]
400pub struct CapturedIssues {
401 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
402 tracer: ResolvedVc<DelegatingImportTracer>,
403}
404
405impl CapturedIssues {
406 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
408 self.issues.iter().copied()
409 }
410
411 pub async fn get_plain_issues(&self, filter: &IssueFilter) -> Result<Vec<ReadRef<PlainIssue>>> {
413 let mut list = self
414 .issues
415 .iter()
416 .map(async |issue| {
417 if filter.matches(*issue).await? {
418 Ok(Some(
419 PlainIssue::from_issue(**issue, Some(*self.tracer)).await?,
420 ))
421 } else {
422 Ok(None)
423 }
424 })
425 .try_flat_join()
426 .await?;
427 list.sort();
428 Ok(list)
429 }
430}
431
432#[turbo_tasks::task_input]
433#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TraceRawVcs, Encode, Decode)]
434pub struct IssueSource {
435 source: ResolvedVc<Box<dyn Source>>,
436 range: Option<SourceRange>,
437}
438
439#[turbo_tasks::task_input]
441#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, TraceRawVcs, Encode, Decode)]
442enum SourceRange {
443 LineColumn(SourcePos, SourcePos),
444 ByteOffset(u32, u32),
445}
446
447impl IssueSource {
448 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
451 IssueSource {
452 source,
453 range: None,
454 }
455 }
456
457 pub fn from_line_col(
458 source: ResolvedVc<Box<dyn Source>>,
459 start: SourcePos,
460 end: SourcePos,
461 ) -> Self {
462 IssueSource {
463 source,
464 range: Some(SourceRange::LineColumn(start, end)),
465 }
466 }
467
468 pub fn from_single_line_col(source: ResolvedVc<Box<dyn Source>>, pos: SourcePos) -> Self {
469 IssueSource {
470 source,
471 range: Some(SourceRange::LineColumn(
472 pos,
473 SourcePos {
474 line: pos.line,
475 column: pos.column + 1,
477 },
478 )),
479 }
480 }
481
482 async fn into_plain(self) -> Result<PlainIssueSource> {
483 let Self { mut source, range } = self;
484
485 let range = if let Some(range) = range {
486 let mut range = match range {
487 SourceRange::LineColumn(start, end) => Some((start, end)),
488 SourceRange::ByteOffset(start, end) => {
489 if let Ok(content) = self.source.content().lines().await
493 && let FileLinesContent::Lines(lines) = &*content
494 {
495 let start = find_line_and_column(lines.as_ref(), start);
496 let end = find_line_and_column(lines.as_ref(), end);
497 Some((start, end))
498 } else {
499 None
500 }
501 }
502 };
503
504 if let Some((start, end)) = range {
506 let mapped = source_pos(source, start, end).await?;
507
508 if let Some((mapped_source, start, end)) = mapped {
509 range = Some((start, end));
510 source = mapped_source;
511 }
512 }
513 range
514 } else {
515 None
516 };
517 Ok(PlainIssueSource {
518 asset: PlainSource::from_source(*source).await?,
519 range,
520 })
521 }
522
523 pub fn from_unparsable_json(
526 source: ResolvedVc<Box<dyn Source>>,
527 error: &UnparsableJson,
528 ) -> Self {
529 match (error.start_location, error.end_location) {
530 (None, None) => Self::from_source_only(source),
531 (Some((line, column)), None) | (None, Some((line, column))) => Self::from_line_col(
532 source,
533 SourcePos { line, column },
534 SourcePos { line, column },
535 ),
536 (Some((start_line, start_column)), Some((end_line, end_column))) => {
537 Self::from_line_col(
538 source,
539 SourcePos {
540 line: start_line,
541 column: start_column,
542 },
543 SourcePos {
544 line: end_line,
545 column: end_column,
546 },
547 )
548 }
549 }
550 }
551
552 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
561 IssueSource {
562 source,
563 range: match (start == 0, end == 0) {
564 (true, true) => None,
565 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
566 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
567 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
568 },
569 }
570 }
571
572 pub async fn from_byte_offset(
582 source: ResolvedVc<Box<dyn Source>>,
583 start: u32,
584 end: u32,
585 ) -> Result<Self> {
586 Ok(IssueSource {
587 source,
588 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
589 let start = find_line_and_column(lines.as_ref(), start);
590 let end = find_line_and_column(lines.as_ref(), end);
591 Some(SourceRange::LineColumn(start, end))
592 } else {
593 None
594 },
595 })
596 }
597
598 pub async fn file_path(&self) -> Result<FileSystemPath> {
600 Ok(self.source.ident().await?.path.clone())
601 }
602
603 pub async fn to_generated_code_source(&self) -> Result<Option<AdditionalIssueSource>> {
608 if ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source).is_some() {
609 let description = self.source.description().await?;
610 let generated = Vc::upcast::<Box<dyn Source>>(GeneratedCodeSource::new(*self.source))
611 .to_resolved()
612 .await?;
613 return Ok(Some(AdditionalIssueSource {
614 description: format!("Generated code of {}", description).into(),
615 source: IssueSource {
616 source: generated,
617 range: self.range,
622 },
623 }));
624 }
625 Ok(None)
626 }
627}
628
629impl IssueSource {
630 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
632 Ok(match &self.range {
633 Some(range) => match range {
634 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
635 SourceRange::LineColumn(start, end) => {
636 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
637 let start = find_offset(lines.as_ref(), *start) + 1;
638 let end = find_offset(lines.as_ref(), *end) + 1;
639 Some((start, end))
640 } else {
641 None
642 }
643 }
644 },
645 _ => None,
646 })
647 }
648}
649
650async fn source_pos(
651 source: ResolvedVc<Box<dyn Source>>,
652 start: SourcePos,
653 end: SourcePos,
654) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
655 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
656 return Ok(None);
657 };
658
659 let srcmap = generator.generate_source_map();
660 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
661 return Ok(None);
662 };
663
664 let find = async |line: u32, col: u32| {
665 let TokenWithSource {
666 token,
667 source_content,
668 } = &srcmap.lookup_token_and_source(line, col).await?;
669
670 match token {
671 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
672 SourcePos {
673 line: t.generated_line as _,
674 column: t.generated_column as _,
675 },
676 *source_content,
677 )),
678 crate::source_map::Token::Original(t) => anyhow::Ok((
679 SourcePos {
680 line: t.original_line as _,
681 column: t.original_column as _,
682 },
683 *source_content,
684 )),
685 }
686 };
687
688 let (start, content_1) = find(start.line, start.column).await?;
689 let (end, content_2) = find(end.line, end.column).await?;
690
691 let Some((content_1, content_2)) = content_1.zip(content_2) else {
692 return Ok(None);
693 };
694
695 if content_1 != content_2 {
696 return Ok(None);
697 }
698
699 Ok(Some((content_1, start, end)))
700}
701
702#[turbo_tasks::value(shared)]
706pub struct AdditionalIssueSource {
707 pub description: RcStr,
708 pub source: IssueSource,
709}
710
711#[turbo_tasks::value(shared, transparent)]
712pub struct AdditionalIssueSources(Vec<AdditionalIssueSource>);
713
714#[turbo_tasks::value_impl]
715impl AdditionalIssueSources {
716 #[turbo_tasks::function]
717 pub fn empty() -> Vc<Self> {
718 Vc::cell(Vec::new())
719 }
720}
721
722#[derive(
724 Serialize,
725 PartialEq,
726 Eq,
727 PartialOrd,
728 Ord,
729 Clone,
730 Debug,
731 TraceRawVcs,
732 NonLocalValue,
733 DeterministicHash,
734)]
735#[serde(rename_all = "camelCase")]
736pub struct PlainTraceItem {
737 pub fs_name: RcStr,
739 pub root_path: RcStr,
741 pub path: RcStr,
743 pub layer: Option<RcStr>,
745}
746
747impl PlainTraceItem {
748 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
749 let fs_path = asset.path.clone();
752 let fs_name = fs_path.fs.to_string().owned().await?;
753 let root_path = fs_path.fs.root().await?.path.clone();
754 let path = fs_path.path.clone();
755 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
756 Ok(Self {
757 fs_name,
758 root_path,
759 path,
760 layer,
761 })
762 }
763}
764
765pub type PlainTrace = Vec<PlainTraceItem>;
766
767async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
769 let mut plain_traces = traces
770 .into_iter()
771 .map(|trace| async move {
772 let mut plain_trace = trace
773 .into_iter()
774 .filter(|asset| {
775 asset.assets.is_empty()
778 })
779 .map(PlainTraceItem::from_asset_ident)
780 .try_join()
781 .await?;
782
783 plain_trace.dedup();
800
801 Ok(plain_trace)
802 })
803 .try_join()
804 .await?;
805
806 plain_traces.retain(|t| t.len() > 1);
809 plain_traces.sort_by(|a, b| {
812 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
814 });
815
816 if plain_traces.len() > 1 {
824 let mut i = 0;
825 while i < plain_traces.len() - 1 {
826 let mut j = plain_traces.len() - 1;
827 while j > i {
828 if plain_traces[j].ends_with(&plain_traces[i]) {
829 plain_traces.remove(j);
835 }
836 j -= 1;
837 }
838 i += 1;
839 }
840 }
841
842 Ok(plain_traces)
843}
844
845#[turbo_tasks::value(shared)]
846#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
847pub enum IssueStage {
848 Config,
849 AppStructure,
850 ProcessModule,
851 Load,
853 SourceTransform,
854 Parse,
855 Transform,
857 Analysis,
858 Resolve,
859 Bindings,
860 CodeGen,
861 Emit,
862 Unsupported,
863 Misc,
864 Other(RcStr),
865}
866
867impl Display for IssueStage {
868 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
869 match self {
870 IssueStage::Config => write!(f, "config"),
871 IssueStage::Resolve => write!(f, "resolve"),
872 IssueStage::ProcessModule => write!(f, "process module"),
873 IssueStage::Load => write!(f, "load"),
874 IssueStage::SourceTransform => write!(f, "source transform"),
875 IssueStage::Parse => write!(f, "parse"),
876 IssueStage::Transform => write!(f, "transform"),
877 IssueStage::Analysis => write!(f, "analysis"),
878 IssueStage::Bindings => write!(f, "bindings"),
879 IssueStage::CodeGen => write!(f, "code gen"),
880 IssueStage::Emit => write!(f, "emit"),
881 IssueStage::Unsupported => write!(f, "unsupported"),
882 IssueStage::AppStructure => write!(f, "app structure"),
883 IssueStage::Misc => write!(f, "misc"),
884 IssueStage::Other(s) => write!(f, "{s}"),
885 }
886 }
887}
888
889#[turbo_tasks::value(serialization = "skip")]
890#[derive(Clone, Debug, PartialOrd, Ord)]
891pub struct PlainIssue {
892 pub severity: IssueSeverity,
893 pub stage: IssueStage,
894
895 pub title: StyledString,
896 pub file_path: RcStr,
897
898 pub description: Option<StyledString>,
899 pub detail: Option<StyledString>,
900 pub documentation_link: RcStr,
901
902 pub source: Option<PlainIssueSource>,
903 pub additional_sources: Vec<PlainAdditionalIssueSource>,
904 pub import_traces: Vec<PlainTrace>,
905}
906
907#[turbo_tasks::value(serialization = "skip")]
908#[derive(Clone, Debug, PartialOrd, Ord)]
909pub struct PlainAdditionalIssueSource {
910 pub description: RcStr,
911 pub source: PlainIssueSource,
912}
913
914fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
915 hasher.write_ref(&issue.severity);
916 hasher.write_ref(&issue.file_path);
917 hasher.write_ref(&issue.stage);
918 hasher.write_ref(&issue.title);
919 hasher.write_ref(&issue.description);
920 hasher.write_ref(&issue.detail);
921 hasher.write_ref(&issue.documentation_link);
922
923 if let Some(source) = &issue.source {
924 hasher.write_value(1_u8);
925 hasher.write_ref(&source.range);
928 } else {
929 hasher.write_value(0_u8);
930 }
931
932 if full {
939 hasher.write_ref(&issue.import_traces);
940 }
941}
942
943impl PlainIssue {
944 pub fn internal_hash_ref(&self, full: bool) -> u64 {
952 let mut hasher = Xxh3Hash64Hasher::new();
953 hash_plain_issue(self, &mut hasher, full);
954 hasher.finish()
955 }
956}
957
958#[turbo_tasks::value_impl]
959impl PlainIssue {
960 #[turbo_tasks::function]
963 pub async fn from_issue(
964 issue: ResolvedVc<Box<dyn Issue>>,
965 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
966 ) -> Result<Vc<Self>> {
967 Ok(
968 Self::from_issue_ref(&*issue.into_trait_ref().await?, import_tracer)
969 .await?
970 .cell(),
971 )
972 }
973}
974
975impl PlainIssue {
976 pub async fn from_issue_ref(
977 trait_ref: &dyn Issue,
978 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
979 ) -> Result<Self> {
980 let severity = trait_ref.severity();
981 let file_path = trait_ref.file_path().await?;
982 let file_path_str = file_path.to_string_ref().await?;
983
984 Ok(Self {
985 severity,
986 file_path: file_path_str,
987 stage: trait_ref.stage(),
988 title: trait_ref.title().await?,
989 description: trait_ref.description().await?,
990 detail: trait_ref.detail().await?,
991 documentation_link: trait_ref.documentation_link(),
992 source: {
993 if let Some(s) = trait_ref.source() {
994 Some(s.into_plain().await?)
995 } else {
996 None
997 }
998 },
999 additional_sources: {
1000 trait_ref
1001 .additional_sources()
1002 .await?
1003 .into_iter()
1004 .map(async |s| {
1005 Ok(PlainAdditionalIssueSource {
1006 source: s.source.into_plain().await?,
1007 description: s.description,
1008 })
1009 })
1010 .try_join()
1011 .await?
1012 },
1013 import_traces: match import_tracer {
1014 Some(tracer) => {
1015 into_plain_trace(tracer.await?.get_traces(file_path).await?).await?
1016 }
1017 None => vec![],
1018 },
1019 })
1020 }
1021}
1022
1023#[turbo_tasks::value(serialization = "skip")]
1024#[derive(Clone, Debug, PartialOrd, Ord)]
1025pub struct PlainIssueSource {
1026 pub asset: ReadRef<PlainSource>,
1027 pub range: Option<(SourcePos, SourcePos)>,
1028}
1029
1030#[turbo_tasks::value(serialization = "skip")]
1031#[derive(Clone, Debug, PartialOrd, Ord)]
1032pub struct PlainSource {
1033 pub ident: RcStr,
1034 pub file_path: RcStr,
1035 #[turbo_tasks(debug_ignore)]
1036 pub content: ReadRef<FileContent>,
1037}
1038
1039#[turbo_tasks::value_impl]
1040impl PlainSource {
1041 #[turbo_tasks::function]
1042 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
1043 let content = if let Ok(asset_content) = asset.content().await
1047 && let AssetContent::File(file_content) = &*asset_content
1048 && let Ok(file_content) = file_content.await
1049 {
1050 file_content
1051 } else {
1052 ReadRef::new_owned(FileContent::NotFound)
1053 };
1054 let ident = asset.ident();
1055
1056 Ok(PlainSource {
1057 ident: ident.to_string().owned().await?,
1058 file_path: ident.await?.path.to_string_ref().await?,
1059 content,
1060 }
1061 .cell())
1062 }
1063}
1064
1065#[turbo_tasks::value_trait]
1066pub trait IssueReporter {
1067 #[turbo_tasks::function]
1078 fn report_issues(
1079 self: Vc<Self>,
1080 source: TransientValue<RawVc>,
1081 min_failing_severity: IssueSeverity,
1082 ) -> Vc<bool>;
1083}
1084
1085pub trait CollectibleIssuesExt
1086where
1087 Self: Sized,
1088{
1089 fn peek_issues(self) -> CapturedIssues;
1093
1094 fn drop_issues(self);
1098}
1099
1100impl<T> CollectibleIssuesExt for T
1101where
1102 T: CollectiblesSource + Copy + Send,
1103{
1104 fn peek_issues(self) -> CapturedIssues {
1105 CapturedIssues {
1106 issues: self.peek_collectibles(),
1107
1108 tracer: DelegatingImportTracer {
1109 delegates: self.peek_collectibles(),
1110 }
1111 .resolved_cell(),
1112 }
1113 }
1114
1115 fn drop_issues(self) {
1116 self.drop_collectibles::<Box<dyn Issue>>();
1117 }
1118}
1119
1120pub async fn handle_issues<T: Send>(
1124 source_op: OperationVc<T>,
1125 issue_reporter: Vc<Box<dyn IssueReporter>>,
1126 min_failing_severity: IssueSeverity,
1127 path: Option<&str>,
1128 operation: Option<&str>,
1129) -> Result<()> {
1130 let source_vc = source_op.connect();
1131 let _ = source_op.resolve().strongly_consistent().await?;
1132
1133 let has_fatal = issue_reporter.report_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 bail!(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}