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, TryFlatJoinIterExt,
20 TryJoinIterExt, Upcast, ValueDefault, ValueToString, ValueToStringRef, Vc, emit,
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, task_input)]
39#[derive(PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub enum IssueSeverity {
42 Bug,
43 Fatal,
44 Error,
45 Warning,
46 Hint,
47 Note,
48 Suggestion,
49 Info,
50}
51
52impl IssueSeverity {
53 pub fn as_str(&self) -> &'static str {
54 match self {
55 IssueSeverity::Bug => "bug",
56 IssueSeverity::Fatal => "fatal",
57 IssueSeverity::Error => "error",
58 IssueSeverity::Warning => "warning",
59 IssueSeverity::Hint => "hint",
60 IssueSeverity::Note => "note",
61 IssueSeverity::Suggestion => "suggestion",
62 IssueSeverity::Info => "info",
63 }
64 }
65
66 pub fn as_help_str(&self) -> &'static str {
67 match self {
68 IssueSeverity::Bug => "bug in implementation",
69 IssueSeverity::Fatal => "unrecoverable problem",
70 IssueSeverity::Error => "problem that cause a broken result",
71 IssueSeverity::Warning => "problem should be addressed in short term",
72 IssueSeverity::Hint => "idea for improvement",
73 IssueSeverity::Note => "detail that is worth mentioning",
74 IssueSeverity::Suggestion => "change proposal for improvement",
75 IssueSeverity::Info => "detail that is worth telling",
76 }
77 }
78}
79
80impl Display for IssueSeverity {
81 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82 f.write_str(self.as_str())
83 }
84}
85
86#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
90#[turbo_tasks::value(shared)]
91pub enum StyledString {
92 Line(Vec<StyledString>),
96 Stack(Vec<StyledString>),
99 Text(RcStr),
101 Code(RcStr),
104 Strong(RcStr),
106}
107
108impl StyledString {
109 pub fn to_unstyled_string(&self) -> String {
110 match self {
111 StyledString::Line(items) => items
112 .iter()
113 .map(|item| item.to_unstyled_string())
114 .collect::<Vec<_>>()
115 .join(""),
116 StyledString::Stack(items) => items
117 .iter()
118 .map(|item| item.to_unstyled_string())
119 .collect::<Vec<_>>()
120 .join("\n"),
121 StyledString::Text(s) | StyledString::Code(s) | StyledString::Strong(s) => {
122 s.to_string()
123 }
124 }
125 }
126}
127
128#[async_trait]
129#[turbo_tasks::value_trait]
130pub trait Issue {
131 fn severity(&self) -> IssueSeverity {
134 IssueSeverity::Error
135 }
136
137 async fn file_path(&self) -> Result<FileSystemPath>;
140
141 fn stage(&self) -> IssueStage;
144
145 async fn title(&self) -> Result<StyledString>;
149
150 async fn description(&self) -> Result<Option<StyledString>> {
153 Ok(None)
154 }
155
156 async fn detail(&self) -> Result<Option<StyledString>> {
160 Ok(None)
161 }
162
163 fn documentation_link(&self) -> RcStr {
166 rcstr!("")
167 }
168
169 fn source(&self) -> Option<IssueSource> {
173 None
174 }
175
176 async fn additional_sources(&self) -> Result<Vec<AdditionalIssueSource>> {
181 Ok(vec![])
182 }
183}
184
185#[turbo_tasks::value_trait]
187pub trait ImportTracer {
188 #[turbo_tasks::function]
189 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
190}
191
192#[turbo_tasks::value]
193#[derive(Debug)]
194pub struct DelegatingImportTracer {
195 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
196}
197
198impl DelegatingImportTracer {
199 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
200 Ok(self
201 .delegates
202 .iter()
203 .map(|d| d.get_traces(path.clone()))
204 .try_join()
205 .await?
206 .iter()
207 .flat_map(|v| v.0.iter().cloned())
208 .collect())
209 }
210}
211
212pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
213
214#[turbo_tasks::value(shared)]
215pub struct ImportTraces(pub Vec<ImportTrace>);
216
217#[turbo_tasks::value_impl]
218impl ValueDefault for ImportTraces {
219 #[turbo_tasks::function]
220 fn value_default() -> Vc<Self> {
221 Self::cell(ImportTraces(vec![]))
222 }
223}
224
225pub trait IssueExt {
226 fn emit(self);
227}
228
229impl<T> IssueExt for ResolvedVc<T>
230where
231 T: Upcast<Box<dyn Issue>>,
232{
233 fn emit(self) {
234 emit(ResolvedVc::upcast_non_strict::<Box<dyn Issue>>(self));
235 }
236}
237
238#[turbo_tasks::value(transparent)]
239pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
240
241#[derive(Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
243pub enum IgnoreIssuePattern {
244 ExactString(RcStr),
246 Glob(Glob),
248 Regex(EsRegex),
250}
251
252impl IgnoreIssuePattern {
253 pub fn matches(&self, value: &str) -> bool {
255 match self {
256 IgnoreIssuePattern::ExactString(s) => value == s.as_str(),
257 IgnoreIssuePattern::Glob(glob) => glob.matches(value),
258 IgnoreIssuePattern::Regex(regex) => regex.is_match(value),
259 }
260 }
261}
262
263#[derive(Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode)]
266pub struct IgnoreIssue {
267 pub path: IgnoreIssuePattern,
269 pub title: Option<IgnoreIssuePattern>,
271 pub description: Option<IgnoreIssuePattern>,
273}
274
275#[turbo_tasks::value(shared)]
276pub struct IssueFilter {
277 severity: IssueSeverity,
279 foreign_severity: IssueSeverity,
281 ignore_rules: Box<[IgnoreIssue]>,
283}
284
285impl IssueFilter {
286 pub fn everything() -> Self {
288 IssueFilter {
289 severity: IssueSeverity::Info,
290 foreign_severity: IssueSeverity::Info,
291 ignore_rules: Box::from([]),
292 }
293 }
294
295 pub fn warnings_and_foreign_errors() -> Self {
297 IssueFilter {
298 severity: IssueSeverity::Warning,
299 foreign_severity: IssueSeverity::Error,
300 ignore_rules: Box::from([]),
301 }
302 }
303
304 pub fn with_ignore_rules(mut self, rules: Box<[IgnoreIssue]>) -> Self {
306 self.ignore_rules = rules;
307 self
308 }
309
310 pub async fn matches(&self, issue: ResolvedVc<Box<dyn Issue>>) -> Result<bool> {
312 Ok(self.matches_all_fast_path()
313 || self
314 .matches_ref_slow_path(&*issue.into_trait_ref().await?)
315 .await?)
316 }
317
318 pub async fn matches_ref(&self, issue: &dyn Issue) -> Result<bool> {
319 Ok(self.matches_all_fast_path() || self.matches_ref_slow_path(issue).await?)
320 }
321
322 fn matches_all_fast_path(&self) -> bool {
323 self.severity == IssueSeverity::Info
324 && self.foreign_severity == IssueSeverity::Info
325 && self.ignore_rules.is_empty()
326 }
327
328 async fn matches_ref_slow_path(&self, issue: &dyn Issue) -> Result<bool> {
329 let file_path = issue.file_path().await?;
332
333 let severity = issue.severity();
336 let severity_allowed = if severity <= self.severity || severity <= self.foreign_severity {
338 if severity <= self.severity && severity <= self.foreign_severity {
341 true
343 } else if ContextCondition::InNodeModules.matches(&file_path) {
344 severity <= self.foreign_severity
345 } else {
346 severity <= self.severity
347 }
348 } else {
349 false
351 };
352
353 if !severity_allowed {
354 return Ok(false);
355 }
356
357 if !self.ignore_rules.is_empty() {
361 let file_path_str = file_path.to_string();
362 let mut title_str: Option<String> = None;
363 let mut description_text: Option<Option<String>> = None;
364
365 for rule in &self.ignore_rules {
366 if !rule.path.matches(&file_path_str) {
367 continue;
368 }
369 if let Some(ref title_pat) = rule.title {
370 if title_str.is_none() {
371 title_str = Some(issue.title().await?.to_unstyled_string());
372 }
373 if !title_pat.matches(title_str.as_deref().unwrap()) {
374 continue;
375 }
376 }
377 if let Some(ref desc_pat) = rule.description {
378 if description_text.is_none() {
379 description_text =
380 Some(issue.description().await?.map(|s| s.to_unstyled_string()));
381 }
382 match description_text.as_ref().unwrap().as_deref() {
383 Some(desc) if desc_pat.matches(desc) => {}
384 _ => continue,
385 }
386 }
387 return Ok(false);
389 }
390 }
391
392 Ok(true)
393 }
394}
395
396#[turbo_tasks::value(shared)]
398#[derive(Debug)]
399pub struct CapturedIssues {
400 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
401 tracer: ResolvedVc<DelegatingImportTracer>,
402}
403
404impl CapturedIssues {
405 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
407 self.issues.iter().copied()
408 }
409
410 pub async fn get_plain_issues(&self, filter: &IssueFilter) -> Result<Vec<ReadRef<PlainIssue>>> {
412 let mut list = self
413 .issues
414 .iter()
415 .map(async |issue| {
416 if filter.matches(*issue).await? {
417 Ok(Some(
418 PlainIssue::from_issue(**issue, Some(*self.tracer)).await?,
419 ))
420 } else {
421 Ok(None)
422 }
423 })
424 .try_flat_join()
425 .await?;
426 list.sort();
427 Ok(list)
428 }
429}
430
431#[turbo_tasks::task_input]
432#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Encode, Decode)]
433pub struct IssueSource {
434 source: ResolvedVc<Box<dyn Source>>,
435 range: Option<SourceRange>,
436}
437
438#[turbo_tasks::task_input]
440#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Encode, Decode)]
441enum SourceRange {
442 LineColumn(SourcePos, SourcePos),
443 ByteOffset(u32, u32),
444}
445
446impl IssueSource {
447 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
450 IssueSource {
451 source,
452 range: None,
453 }
454 }
455
456 pub fn without_range(self) -> Self {
458 IssueSource {
459 range: None,
460 ..self
461 }
462 }
463
464 pub fn from_line_col(
465 source: ResolvedVc<Box<dyn Source>>,
466 start: SourcePos,
467 end: SourcePos,
468 ) -> Self {
469 IssueSource {
470 source,
471 range: Some(SourceRange::LineColumn(start, end)),
472 }
473 }
474
475 pub fn from_single_line_col(source: ResolvedVc<Box<dyn Source>>, pos: SourcePos) -> Self {
476 IssueSource {
477 source,
478 range: Some(SourceRange::LineColumn(
479 pos,
480 SourcePos {
481 line: pos.line,
482 column: pos.column + 1,
484 },
485 )),
486 }
487 }
488
489 async fn into_plain(self) -> Result<PlainIssueSource> {
490 let Self { mut source, range } = self;
491
492 let range = if let Some(range) = range {
493 let mut range = match range {
494 SourceRange::LineColumn(start, end) => Some((start, end)),
495 SourceRange::ByteOffset(start, end) => {
496 if let Ok(content) = self.source.content().lines().await
500 && let FileLinesContent::Lines(lines) = &*content
501 {
502 let start = find_line_and_column(lines.as_ref(), start);
503 let end = find_line_and_column(lines.as_ref(), end);
504 Some((start, end))
505 } else {
506 None
507 }
508 }
509 };
510
511 if let Some((start, end)) = range {
513 let mapped = source_pos(source, start, end).await?;
514
515 if let Some((mapped_source, start, end)) = mapped {
516 range = Some((start, end));
517 source = mapped_source;
518 }
519 }
520 range
521 } else {
522 None
523 };
524 Ok(PlainIssueSource {
525 asset: PlainSource::from_source(*source).await?,
526 range,
527 })
528 }
529
530 pub fn from_unparsable_json(
533 source: ResolvedVc<Box<dyn Source>>,
534 error: &UnparsableJson,
535 ) -> Self {
536 match (error.start_location, error.end_location) {
537 (None, None) => Self::from_source_only(source),
538 (Some((line, column)), None) | (None, Some((line, column))) => Self::from_line_col(
539 source,
540 SourcePos { line, column },
541 SourcePos { line, column },
542 ),
543 (Some((start_line, start_column)), Some((end_line, end_column))) => {
544 Self::from_line_col(
545 source,
546 SourcePos {
547 line: start_line,
548 column: start_column,
549 },
550 SourcePos {
551 line: end_line,
552 column: end_column,
553 },
554 )
555 }
556 }
557 }
558
559 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
568 IssueSource {
569 source,
570 range: match (start == 0, end == 0) {
571 (true, true) => None,
572 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
573 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
574 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
575 },
576 }
577 }
578
579 pub async fn from_byte_offset(
589 source: ResolvedVc<Box<dyn Source>>,
590 start: u32,
591 end: u32,
592 ) -> Result<Self> {
593 Ok(IssueSource {
594 source,
595 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
596 let start = find_line_and_column(lines.as_ref(), start);
597 let end = find_line_and_column(lines.as_ref(), end);
598 Some(SourceRange::LineColumn(start, end))
599 } else {
600 None
601 },
602 })
603 }
604
605 pub async fn file_path(&self) -> Result<FileSystemPath> {
607 Ok(self.source.ident().await?.path.clone())
608 }
609
610 pub async fn to_generated_code_source(&self) -> Result<Option<AdditionalIssueSource>> {
615 if ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source).is_some() {
616 let description = self.source.description().await?;
617 let generated = Vc::upcast::<Box<dyn Source>>(GeneratedCodeSource::new(*self.source))
618 .to_resolved()
619 .await?;
620 return Ok(Some(AdditionalIssueSource {
621 description: format!("Generated code of {}", description).into(),
622 source: IssueSource {
623 source: generated,
624 range: self.range,
629 },
630 }));
631 }
632 Ok(None)
633 }
634}
635
636impl IssueSource {
637 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
639 Ok(match &self.range {
640 Some(range) => match range {
641 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
642 SourceRange::LineColumn(start, end) => {
643 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
644 let start = find_offset(lines.as_ref(), *start) + 1;
645 let end = find_offset(lines.as_ref(), *end) + 1;
646 Some((start, end))
647 } else {
648 None
649 }
650 }
651 },
652 _ => None,
653 })
654 }
655}
656
657async fn source_pos(
658 source: ResolvedVc<Box<dyn Source>>,
659 start: SourcePos,
660 end: SourcePos,
661) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
662 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
663 return Ok(None);
664 };
665
666 let srcmap = generator.generate_source_map();
667 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
668 return Ok(None);
669 };
670
671 let find = async |line: u32, col: u32| {
672 let TokenWithSource {
673 token,
674 source_content,
675 } = &srcmap.lookup_token_and_source(line, col).await?;
676
677 match token {
678 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
679 SourcePos {
680 line: t.generated_line as _,
681 column: t.generated_column as _,
682 },
683 *source_content,
684 )),
685 crate::source_map::Token::Original(t) => anyhow::Ok((
686 SourcePos {
687 line: t.original_line as _,
688 column: t.original_column as _,
689 },
690 *source_content,
691 )),
692 }
693 };
694
695 let (start, content_1) = find(start.line, start.column).await?;
696 let (end, content_2) = find(end.line, end.column).await?;
697
698 let Some((content_1, content_2)) = content_1.zip(content_2) else {
699 return Ok(None);
700 };
701
702 if content_1 != content_2 {
703 return Ok(None);
704 }
705
706 Ok(Some((content_1, start, end)))
707}
708
709#[turbo_tasks::value(shared)]
713pub struct AdditionalIssueSource {
714 pub description: RcStr,
715 pub source: IssueSource,
716}
717
718#[turbo_tasks::value(shared, transparent)]
719pub struct AdditionalIssueSources(Vec<AdditionalIssueSource>);
720
721#[turbo_tasks::value_impl]
722impl AdditionalIssueSources {
723 #[turbo_tasks::function]
724 pub fn empty() -> Vc<Self> {
725 Vc::cell(Vec::new())
726 }
727}
728
729#[derive(
731 Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug, NonLocalValue, DeterministicHash,
732)]
733#[serde(rename_all = "camelCase")]
734pub struct PlainTraceItem {
735 pub fs_name: RcStr,
737 pub root_path: RcStr,
739 pub path: RcStr,
741 pub layer: Option<RcStr>,
743}
744
745impl PlainTraceItem {
746 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
747 let fs_path = asset.path.clone();
750 let fs_name = fs_path.fs.to_string().owned().await?;
751 let root_path = fs_path.fs.root().await?.path.clone();
752 let path = fs_path.path.clone();
753 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
754 Ok(Self {
755 fs_name,
756 root_path,
757 path,
758 layer,
759 })
760 }
761}
762
763pub type PlainTrace = Vec<PlainTraceItem>;
764
765async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
767 let mut plain_traces = traces
768 .into_iter()
769 .map(async |trace| {
770 let mut plain_trace = trace
771 .into_iter()
772 .filter(|asset| {
773 asset.assets.is_empty()
776 })
777 .map(PlainTraceItem::from_asset_ident)
778 .try_join()
779 .await?;
780
781 plain_trace.dedup();
798
799 Ok(plain_trace)
800 })
801 .try_join()
802 .await?;
803
804 plain_traces.retain(|t| t.len() > 1);
807 plain_traces.sort_by(|a, b| {
810 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
812 });
813
814 if plain_traces.len() > 1 {
822 let mut i = 0;
823 while i < plain_traces.len() - 1 {
824 let mut j = plain_traces.len() - 1;
825 while j > i {
826 if plain_traces[j].ends_with(&plain_traces[i]) {
827 plain_traces.remove(j);
833 }
834 j -= 1;
835 }
836 i += 1;
837 }
838 }
839
840 Ok(plain_traces)
841}
842
843#[turbo_tasks::value(shared)]
844#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
845pub enum IssueStage {
846 Config,
847 AppStructure,
848 ProcessModule,
849 Load,
851 SourceTransform,
852 Parse,
853 Transform,
855 Analysis,
856 Resolve,
857 Bindings,
858 CodeGen,
859 Emit,
860 Unsupported,
861 Misc,
862 Other(RcStr),
863}
864
865impl Display for IssueStage {
866 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
867 match self {
868 IssueStage::Config => write!(f, "config"),
869 IssueStage::Resolve => write!(f, "resolve"),
870 IssueStage::ProcessModule => write!(f, "process module"),
871 IssueStage::Load => write!(f, "load"),
872 IssueStage::SourceTransform => write!(f, "source transform"),
873 IssueStage::Parse => write!(f, "parse"),
874 IssueStage::Transform => write!(f, "transform"),
875 IssueStage::Analysis => write!(f, "analysis"),
876 IssueStage::Bindings => write!(f, "bindings"),
877 IssueStage::CodeGen => write!(f, "code gen"),
878 IssueStage::Emit => write!(f, "emit"),
879 IssueStage::Unsupported => write!(f, "unsupported"),
880 IssueStage::AppStructure => write!(f, "app structure"),
881 IssueStage::Misc => write!(f, "misc"),
882 IssueStage::Other(s) => write!(f, "{s}"),
883 }
884 }
885}
886
887#[turbo_tasks::value(serialization = "skip")]
888#[derive(Clone, Debug, PartialOrd, Ord)]
889pub struct PlainIssue {
890 pub severity: IssueSeverity,
891 pub stage: IssueStage,
892
893 pub title: StyledString,
894 pub file_path: RcStr,
895
896 pub description: Option<StyledString>,
897 pub detail: Option<StyledString>,
898 pub documentation_link: RcStr,
899
900 pub source: Option<PlainIssueSource>,
901 pub additional_sources: Vec<PlainAdditionalIssueSource>,
902 pub import_traces: Vec<PlainTrace>,
903}
904
905#[turbo_tasks::value(serialization = "skip")]
910#[derive(Debug)]
911pub struct PlainIssues(pub Vec<ReadRef<PlainIssue>>);
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 Ok(
974 Self::from_issue_ref(&*issue.into_trait_ref().await?, import_tracer)
975 .await?
976 .cell(),
977 )
978 }
979}
980
981impl PlainIssue {
982 pub async fn from_issue_ref(
983 trait_ref: &dyn Issue,
984 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
985 ) -> Result<Self> {
986 let severity = trait_ref.severity();
987 let file_path = trait_ref.file_path().await?;
988 let file_path_str = file_path.to_string_ref().await?;
989
990 Ok(Self {
991 severity,
992 file_path: file_path_str,
993 stage: trait_ref.stage(),
994 title: trait_ref.title().await?,
995 description: trait_ref.description().await?,
996 detail: trait_ref.detail().await?,
997 documentation_link: trait_ref.documentation_link(),
998 source: {
999 if let Some(s) = trait_ref.source() {
1000 Some(s.into_plain().await?)
1001 } else {
1002 None
1003 }
1004 },
1005 additional_sources: {
1006 trait_ref
1007 .additional_sources()
1008 .await?
1009 .into_iter()
1010 .map(async |s| {
1011 Ok(PlainAdditionalIssueSource {
1012 source: s.source.into_plain().await?,
1013 description: s.description,
1014 })
1015 })
1016 .try_join()
1017 .await?
1018 },
1019 import_traces: match import_tracer {
1020 Some(tracer) => {
1021 into_plain_trace(tracer.await?.get_traces(file_path).await?).await?
1022 }
1023 None => vec![],
1024 },
1025 })
1026 }
1027}
1028
1029#[turbo_tasks::value(serialization = "skip")]
1030#[derive(Clone, Debug, PartialOrd, Ord)]
1031pub struct PlainIssueSource {
1032 pub asset: ReadRef<PlainSource>,
1033 pub range: Option<(SourcePos, SourcePos)>,
1034}
1035
1036#[turbo_tasks::value(serialization = "skip")]
1037#[derive(Clone, Debug, PartialOrd, Ord)]
1038pub struct PlainSource {
1039 pub ident: RcStr,
1040 pub file_path: RcStr,
1041 #[turbo_tasks(debug_ignore)]
1042 pub content: ReadRef<FileContent>,
1043}
1044
1045#[turbo_tasks::value_impl]
1046impl PlainSource {
1047 #[turbo_tasks::function]
1048 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
1049 let content = if let Ok(asset_content) = asset.content().await
1053 && let AssetContent::File(file_content) = &*asset_content
1054 && let Ok(file_content) = file_content.await
1055 {
1056 file_content
1057 } else {
1058 ReadRef::new_owned(FileContent::NotFound)
1059 };
1060 let ident = asset.ident();
1061
1062 Ok(PlainSource {
1063 ident: ident.to_string().owned().await?,
1064 file_path: ident.await?.path.to_string_ref().await?,
1065 content,
1066 }
1067 .cell())
1068 }
1069}
1070
1071#[async_trait]
1072#[turbo_tasks::value_trait]
1073pub trait IssueReporter {
1074 async fn report_issues(
1090 &self,
1091 issues: ReadRef<PlainIssues>,
1092 source: RawVc,
1093 min_failing_severity: IssueSeverity,
1094 ) -> Result<bool>;
1095}
1096
1097pub trait CollectibleIssuesExt
1098where
1099 Self: Sized,
1100{
1101 fn peek_issues(self) -> CapturedIssues;
1105
1106 fn drop_issues(self);
1110}
1111
1112impl<T> CollectibleIssuesExt for T
1113where
1114 T: CollectiblesSource + Copy + Send,
1115{
1116 fn peek_issues(self) -> CapturedIssues {
1117 CapturedIssues {
1118 issues: self.peek_collectibles(),
1119
1120 tracer: DelegatingImportTracer {
1121 delegates: self.peek_collectibles(),
1122 }
1123 .resolved_cell(),
1124 }
1125 }
1126
1127 fn drop_issues(self) {
1128 self.drop_collectibles::<Box<dyn Issue>>();
1129 }
1130}
1131
1132#[turbo_tasks::function(operation, root)]
1139async fn collect_issues(source: OperationVc<()>) -> Result<Vc<PlainIssues>> {
1140 let plain = source
1141 .peek_issues()
1142 .get_plain_issues(&IssueFilter::everything())
1143 .await?;
1144 Ok(PlainIssues(plain).cell())
1145}
1146
1147pub async fn handle_issues<T: Send>(
1151 source_op: OperationVc<T>,
1152 issue_reporter: Vc<Box<dyn IssueReporter>>,
1153 min_failing_severity: IssueSeverity,
1154 path: Option<&str>,
1155 operation: Option<&str>,
1156) -> Result<()> {
1157 let source_vc = source_op.connect();
1158 let _ = source_op.resolve().strongly_consistent().await?;
1159 let source_raw = Vc::into_raw(source_vc);
1160
1161 let erased_source = OperationVc::<()>::try_from(source_raw)?;
1166 let issues = collect_issues(erased_source)
1167 .read_strongly_consistent()
1168 .await?;
1169
1170 let reporter = issue_reporter
1174 .to_resolved()
1175 .strongly_consistent()
1176 .await?
1177 .into_trait_ref()
1178 .await?;
1179 let has_fatal = reporter
1180 .report_issues(issues, source_raw, min_failing_severity)
1181 .await?;
1182
1183 if has_fatal {
1184 let mut message = "Fatal issue(s) occurred".to_owned();
1185 if let Some(path) = path.as_ref() {
1186 message += &format!(" in {path}");
1187 };
1188 if let Some(operation) = operation.as_ref() {
1189 message += &format!(" ({operation})");
1190 };
1191
1192 bail!(message)
1193 } else {
1194 Ok(())
1195 }
1196}
1197
1198fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1199 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1200 Ok(i) => SourcePos {
1201 line: i as u32,
1202 column: 0,
1203 },
1204 Err(i) => {
1205 if i == 0 {
1206 SourcePos {
1207 line: 0,
1208 column: offset,
1209 }
1210 } else {
1211 let line = &lines[i - 1];
1212 SourcePos {
1213 line: (i - 1) as u32,
1214 column: min(line.content.len() as u32, offset - line.bytes_offset),
1215 }
1216 }
1217 }
1218 }
1219}
1220
1221fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1222 let line = &lines[pos.line as usize];
1223 line.bytes_offset + pos.column
1224}