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 Ok(content) = self.source.content().lines().await
504 && let FileLinesContent::Lines(lines) = &*content
505 {
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 async fn file_path(&self) -> Result<FileSystemPath> {
611 Ok(self.source.ident().await?.path.clone())
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(shared)]
717pub struct AdditionalIssueSource {
718 pub description: RcStr,
719 pub source: IssueSource,
720}
721
722#[turbo_tasks::value(shared, transparent)]
723pub struct AdditionalIssueSources(Vec<AdditionalIssueSource>);
724
725#[turbo_tasks::value_impl]
726impl AdditionalIssueSources {
727 #[turbo_tasks::function]
728 pub fn empty() -> Vc<Self> {
729 Vc::cell(Vec::new())
730 }
731}
732
733#[derive(
735 Serialize,
736 PartialEq,
737 Eq,
738 PartialOrd,
739 Ord,
740 Clone,
741 Debug,
742 TraceRawVcs,
743 NonLocalValue,
744 DeterministicHash,
745)]
746#[serde(rename_all = "camelCase")]
747pub struct PlainTraceItem {
748 pub fs_name: RcStr,
750 pub root_path: RcStr,
752 pub path: RcStr,
754 pub layer: Option<RcStr>,
756}
757
758impl PlainTraceItem {
759 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
760 let fs_path = asset.path.clone();
763 let fs_name = fs_path.fs.to_string().owned().await?;
764 let root_path = fs_path.fs.root().await?.path.clone();
765 let path = fs_path.path.clone();
766 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
767 Ok(Self {
768 fs_name,
769 root_path,
770 path,
771 layer,
772 })
773 }
774}
775
776pub type PlainTrace = Vec<PlainTraceItem>;
777
778async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
780 let mut plain_traces = traces
781 .into_iter()
782 .map(|trace| async move {
783 let mut plain_trace = trace
784 .into_iter()
785 .filter(|asset| {
786 asset.assets.is_empty()
789 })
790 .map(PlainTraceItem::from_asset_ident)
791 .try_join()
792 .await?;
793
794 plain_trace.dedup();
811
812 Ok(plain_trace)
813 })
814 .try_join()
815 .await?;
816
817 plain_traces.retain(|t| t.len() > 1);
820 plain_traces.sort_by(|a, b| {
823 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
825 });
826
827 if plain_traces.len() > 1 {
835 let mut i = 0;
836 while i < plain_traces.len() - 1 {
837 let mut j = plain_traces.len() - 1;
838 while j > i {
839 if plain_traces[j].ends_with(&plain_traces[i]) {
840 plain_traces.remove(j);
846 }
847 j -= 1;
848 }
849 i += 1;
850 }
851 }
852
853 Ok(plain_traces)
854}
855
856#[turbo_tasks::value(shared)]
857#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
858pub enum IssueStage {
859 Config,
860 AppStructure,
861 ProcessModule,
862 Load,
864 SourceTransform,
865 Parse,
866 Transform,
868 Analysis,
869 Resolve,
870 Bindings,
871 CodeGen,
872 Emit,
873 Unsupported,
874 Misc,
875 Other(RcStr),
876}
877
878impl Display for IssueStage {
879 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
880 match self {
881 IssueStage::Config => write!(f, "config"),
882 IssueStage::Resolve => write!(f, "resolve"),
883 IssueStage::ProcessModule => write!(f, "process module"),
884 IssueStage::Load => write!(f, "load"),
885 IssueStage::SourceTransform => write!(f, "source transform"),
886 IssueStage::Parse => write!(f, "parse"),
887 IssueStage::Transform => write!(f, "transform"),
888 IssueStage::Analysis => write!(f, "analysis"),
889 IssueStage::Bindings => write!(f, "bindings"),
890 IssueStage::CodeGen => write!(f, "code gen"),
891 IssueStage::Emit => write!(f, "emit"),
892 IssueStage::Unsupported => write!(f, "unsupported"),
893 IssueStage::AppStructure => write!(f, "app structure"),
894 IssueStage::Misc => write!(f, "misc"),
895 IssueStage::Other(s) => write!(f, "{s}"),
896 }
897 }
898}
899
900#[turbo_tasks::value(serialization = "skip")]
901#[derive(Clone, Debug, PartialOrd, Ord)]
902pub struct PlainIssue {
903 pub severity: IssueSeverity,
904 pub stage: IssueStage,
905
906 pub title: StyledString,
907 pub file_path: RcStr,
908
909 pub description: Option<StyledString>,
910 pub detail: Option<StyledString>,
911 pub documentation_link: RcStr,
912
913 pub source: Option<PlainIssueSource>,
914 pub additional_sources: Vec<PlainAdditionalIssueSource>,
915 pub import_traces: Vec<PlainTrace>,
916}
917
918#[turbo_tasks::value(serialization = "skip")]
919#[derive(Clone, Debug, PartialOrd, Ord)]
920pub struct PlainAdditionalIssueSource {
921 pub description: RcStr,
922 pub source: PlainIssueSource,
923}
924
925fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
926 hasher.write_ref(&issue.severity);
927 hasher.write_ref(&issue.file_path);
928 hasher.write_ref(&issue.stage);
929 hasher.write_ref(&issue.title);
930 hasher.write_ref(&issue.description);
931 hasher.write_ref(&issue.detail);
932 hasher.write_ref(&issue.documentation_link);
933
934 if let Some(source) = &issue.source {
935 hasher.write_value(1_u8);
936 hasher.write_ref(&source.range);
939 } else {
940 hasher.write_value(0_u8);
941 }
942
943 if full {
950 hasher.write_ref(&issue.import_traces);
951 }
952}
953
954impl PlainIssue {
955 pub fn internal_hash_ref(&self, full: bool) -> u64 {
963 let mut hasher = Xxh3Hash64Hasher::new();
964 hash_plain_issue(self, &mut hasher, full);
965 hasher.finish()
966 }
967}
968
969#[turbo_tasks::value_impl]
970impl PlainIssue {
971 #[turbo_tasks::function]
974 pub async fn from_issue(
975 issue: ResolvedVc<Box<dyn Issue>>,
976 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
977 ) -> Result<Vc<Self>> {
978 let trait_ref = issue.into_trait_ref().await?;
979 let severity = trait_ref.severity();
980 let file_path = trait_ref.file_path().await?;
981 let file_path_str = file_path.to_string_ref().await?;
982
983 Ok(Self::cell(Self {
984 severity,
985 file_path: file_path_str,
986 stage: trait_ref.stage(),
987 title: trait_ref.title().await?,
988 description: trait_ref.description().await?,
989 detail: trait_ref.detail().await?,
990 documentation_link: trait_ref.documentation_link(),
991 source: {
992 if let Some(s) = trait_ref.source() {
993 Some(s.into_plain().await?)
994 } else {
995 None
996 }
997 },
998 additional_sources: {
999 trait_ref
1000 .additional_sources()
1001 .await?
1002 .into_iter()
1003 .map(async |s| {
1004 Ok(PlainAdditionalIssueSource {
1005 source: s.source.into_plain().await?,
1006 description: s.description,
1007 })
1008 })
1009 .try_join()
1010 .await?
1011 },
1012 import_traces: match import_tracer {
1013 Some(tracer) => {
1014 into_plain_trace(tracer.await?.get_traces(file_path).await?).await?
1015 }
1016 None => vec![],
1017 },
1018 }))
1019 }
1020}
1021
1022#[turbo_tasks::value(serialization = "skip")]
1023#[derive(Clone, Debug, PartialOrd, Ord)]
1024pub struct PlainIssueSource {
1025 pub asset: ReadRef<PlainSource>,
1026 pub range: Option<(SourcePos, SourcePos)>,
1027}
1028
1029#[turbo_tasks::value(serialization = "skip")]
1030#[derive(Clone, Debug, PartialOrd, Ord)]
1031pub struct PlainSource {
1032 pub ident: RcStr,
1033 pub file_path: RcStr,
1034 #[turbo_tasks(debug_ignore)]
1035 pub content: ReadRef<FileContent>,
1036}
1037
1038#[turbo_tasks::value_impl]
1039impl PlainSource {
1040 #[turbo_tasks::function]
1041 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
1042 let content = if let Ok(asset_content) = asset.content().await
1046 && let AssetContent::File(file_content) = &*asset_content
1047 && let Ok(file_content) = file_content.await
1048 {
1049 file_content
1050 } else {
1051 ReadRef::new_owned(FileContent::NotFound)
1052 };
1053 let ident = asset.ident();
1054
1055 Ok(PlainSource {
1056 ident: ident.to_string().owned().await?,
1057 file_path: ident.await?.path.to_string_ref().await?,
1058 content,
1059 }
1060 .cell())
1061 }
1062}
1063
1064#[turbo_tasks::value_trait]
1065pub trait IssueReporter {
1066 #[turbo_tasks::function]
1077 fn report_issues(
1078 self: Vc<Self>,
1079 source: TransientValue<RawVc>,
1080 min_failing_severity: IssueSeverity,
1081 ) -> Vc<bool>;
1082}
1083
1084pub trait CollectibleIssuesExt
1085where
1086 Self: Sized,
1087{
1088 fn peek_issues(self) -> CapturedIssues;
1092
1093 fn drop_issues(self);
1097}
1098
1099impl<T> CollectibleIssuesExt for T
1100where
1101 T: CollectiblesSource + Copy + Send,
1102{
1103 fn peek_issues(self) -> CapturedIssues {
1104 CapturedIssues {
1105 issues: self.peek_collectibles(),
1106
1107 tracer: DelegatingImportTracer {
1108 delegates: self.peek_collectibles(),
1109 }
1110 .resolved_cell(),
1111 }
1112 }
1113
1114 fn drop_issues(self) {
1115 self.drop_collectibles::<Box<dyn Issue>>();
1116 }
1117}
1118
1119pub async fn handle_issues<T: Send>(
1123 source_op: OperationVc<T>,
1124 issue_reporter: Vc<Box<dyn IssueReporter>>,
1125 min_failing_severity: IssueSeverity,
1126 path: Option<&str>,
1127 operation: Option<&str>,
1128) -> Result<()> {
1129 let source_vc = source_op.connect();
1130 let _ = source_op.resolve().strongly_consistent().await?;
1131
1132 let has_fatal = issue_reporter.report_issues(
1133 TransientValue::new(Vc::into_raw(source_vc)),
1134 min_failing_severity,
1135 );
1136
1137 if *has_fatal.await? {
1138 let mut message = "Fatal issue(s) occurred".to_owned();
1139 if let Some(path) = path.as_ref() {
1140 message += &format!(" in {path}");
1141 };
1142 if let Some(operation) = operation.as_ref() {
1143 message += &format!(" ({operation})");
1144 };
1145
1146 bail!(message)
1147 } else {
1148 Ok(())
1149 }
1150}
1151
1152fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1153 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1154 Ok(i) => SourcePos {
1155 line: i as u32,
1156 column: 0,
1157 },
1158 Err(i) => {
1159 if i == 0 {
1160 SourcePos {
1161 line: 0,
1162 column: offset,
1163 }
1164 } else {
1165 let line = &lines[i - 1];
1166 SourcePos {
1167 line: (i - 1) as u32,
1168 column: min(line.content.len() as u32, offset - line.bytes_offset),
1169 }
1170 }
1171 }
1172 }
1173}
1174
1175fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1176 let line = &lines[pos.line as usize];
1177 line.bytes_offset + pos.column
1178}