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 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 without_range(self) -> Self {
459 IssueSource {
460 range: None,
461 ..self
462 }
463 }
464
465 pub fn from_line_col(
466 source: ResolvedVc<Box<dyn Source>>,
467 start: SourcePos,
468 end: SourcePos,
469 ) -> Self {
470 IssueSource {
471 source,
472 range: Some(SourceRange::LineColumn(start, end)),
473 }
474 }
475
476 pub fn from_single_line_col(source: ResolvedVc<Box<dyn Source>>, pos: SourcePos) -> Self {
477 IssueSource {
478 source,
479 range: Some(SourceRange::LineColumn(
480 pos,
481 SourcePos {
482 line: pos.line,
483 column: pos.column + 1,
485 },
486 )),
487 }
488 }
489
490 async fn into_plain(self) -> Result<PlainIssueSource> {
491 let Self { mut source, range } = self;
492
493 let range = if let Some(range) = range {
494 let mut range = match range {
495 SourceRange::LineColumn(start, end) => Some((start, end)),
496 SourceRange::ByteOffset(start, end) => {
497 if let Ok(content) = self.source.content().lines().await
501 && let FileLinesContent::Lines(lines) = &*content
502 {
503 let start = find_line_and_column(lines.as_ref(), start);
504 let end = find_line_and_column(lines.as_ref(), end);
505 Some((start, end))
506 } else {
507 None
508 }
509 }
510 };
511
512 if let Some((start, end)) = range {
514 let mapped = source_pos(source, start, end).await?;
515
516 if let Some((mapped_source, start, end)) = mapped {
517 range = Some((start, end));
518 source = mapped_source;
519 }
520 }
521 range
522 } else {
523 None
524 };
525 Ok(PlainIssueSource {
526 asset: PlainSource::from_source(*source).await?,
527 range,
528 })
529 }
530
531 pub fn from_unparsable_json(
534 source: ResolvedVc<Box<dyn Source>>,
535 error: &UnparsableJson,
536 ) -> Self {
537 match (error.start_location, error.end_location) {
538 (None, None) => Self::from_source_only(source),
539 (Some((line, column)), None) | (None, Some((line, column))) => Self::from_line_col(
540 source,
541 SourcePos { line, column },
542 SourcePos { line, column },
543 ),
544 (Some((start_line, start_column)), Some((end_line, end_column))) => {
545 Self::from_line_col(
546 source,
547 SourcePos {
548 line: start_line,
549 column: start_column,
550 },
551 SourcePos {
552 line: end_line,
553 column: end_column,
554 },
555 )
556 }
557 }
558 }
559
560 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
569 IssueSource {
570 source,
571 range: match (start == 0, end == 0) {
572 (true, true) => None,
573 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
574 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
575 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
576 },
577 }
578 }
579
580 pub async fn from_byte_offset(
590 source: ResolvedVc<Box<dyn Source>>,
591 start: u32,
592 end: u32,
593 ) -> Result<Self> {
594 Ok(IssueSource {
595 source,
596 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
597 let start = find_line_and_column(lines.as_ref(), start);
598 let end = find_line_and_column(lines.as_ref(), end);
599 Some(SourceRange::LineColumn(start, end))
600 } else {
601 None
602 },
603 })
604 }
605
606 pub async fn file_path(&self) -> Result<FileSystemPath> {
608 Ok(self.source.ident().await?.path.clone())
609 }
610
611 pub async fn to_generated_code_source(&self) -> Result<Option<AdditionalIssueSource>> {
616 if ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source).is_some() {
617 let description = self.source.description().await?;
618 let generated = Vc::upcast::<Box<dyn Source>>(GeneratedCodeSource::new(*self.source))
619 .to_resolved()
620 .await?;
621 return Ok(Some(AdditionalIssueSource {
622 description: format!("Generated code of {}", description).into(),
623 source: IssueSource {
624 source: generated,
625 range: self.range,
630 },
631 }));
632 }
633 Ok(None)
634 }
635}
636
637impl IssueSource {
638 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
640 Ok(match &self.range {
641 Some(range) => match range {
642 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
643 SourceRange::LineColumn(start, end) => {
644 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
645 let start = find_offset(lines.as_ref(), *start) + 1;
646 let end = find_offset(lines.as_ref(), *end) + 1;
647 Some((start, end))
648 } else {
649 None
650 }
651 }
652 },
653 _ => None,
654 })
655 }
656}
657
658async fn source_pos(
659 source: ResolvedVc<Box<dyn Source>>,
660 start: SourcePos,
661 end: SourcePos,
662) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
663 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
664 return Ok(None);
665 };
666
667 let srcmap = generator.generate_source_map();
668 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
669 return Ok(None);
670 };
671
672 let find = async |line: u32, col: u32| {
673 let TokenWithSource {
674 token,
675 source_content,
676 } = &srcmap.lookup_token_and_source(line, col).await?;
677
678 match token {
679 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
680 SourcePos {
681 line: t.generated_line as _,
682 column: t.generated_column as _,
683 },
684 *source_content,
685 )),
686 crate::source_map::Token::Original(t) => anyhow::Ok((
687 SourcePos {
688 line: t.original_line as _,
689 column: t.original_column as _,
690 },
691 *source_content,
692 )),
693 }
694 };
695
696 let (start, content_1) = find(start.line, start.column).await?;
697 let (end, content_2) = find(end.line, end.column).await?;
698
699 let Some((content_1, content_2)) = content_1.zip(content_2) else {
700 return Ok(None);
701 };
702
703 if content_1 != content_2 {
704 return Ok(None);
705 }
706
707 Ok(Some((content_1, start, end)))
708}
709
710#[turbo_tasks::value(shared)]
714pub struct AdditionalIssueSource {
715 pub description: RcStr,
716 pub source: IssueSource,
717}
718
719#[turbo_tasks::value(shared, transparent)]
720pub struct AdditionalIssueSources(Vec<AdditionalIssueSource>);
721
722#[turbo_tasks::value_impl]
723impl AdditionalIssueSources {
724 #[turbo_tasks::function]
725 pub fn empty() -> Vc<Self> {
726 Vc::cell(Vec::new())
727 }
728}
729
730#[derive(
732 Serialize,
733 PartialEq,
734 Eq,
735 PartialOrd,
736 Ord,
737 Clone,
738 Debug,
739 TraceRawVcs,
740 NonLocalValue,
741 DeterministicHash,
742)]
743#[serde(rename_all = "camelCase")]
744pub struct PlainTraceItem {
745 pub fs_name: RcStr,
747 pub root_path: RcStr,
749 pub path: RcStr,
751 pub layer: Option<RcStr>,
753}
754
755impl PlainTraceItem {
756 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
757 let fs_path = asset.path.clone();
760 let fs_name = fs_path.fs.to_string().owned().await?;
761 let root_path = fs_path.fs.root().await?.path.clone();
762 let path = fs_path.path.clone();
763 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
764 Ok(Self {
765 fs_name,
766 root_path,
767 path,
768 layer,
769 })
770 }
771}
772
773pub type PlainTrace = Vec<PlainTraceItem>;
774
775async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
777 let mut plain_traces = traces
778 .into_iter()
779 .map(async |trace| {
780 let mut plain_trace = trace
781 .into_iter()
782 .filter(|asset| {
783 asset.assets.is_empty()
786 })
787 .map(PlainTraceItem::from_asset_ident)
788 .try_join()
789 .await?;
790
791 plain_trace.dedup();
808
809 Ok(plain_trace)
810 })
811 .try_join()
812 .await?;
813
814 plain_traces.retain(|t| t.len() > 1);
817 plain_traces.sort_by(|a, b| {
820 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
822 });
823
824 if plain_traces.len() > 1 {
832 let mut i = 0;
833 while i < plain_traces.len() - 1 {
834 let mut j = plain_traces.len() - 1;
835 while j > i {
836 if plain_traces[j].ends_with(&plain_traces[i]) {
837 plain_traces.remove(j);
843 }
844 j -= 1;
845 }
846 i += 1;
847 }
848 }
849
850 Ok(plain_traces)
851}
852
853#[turbo_tasks::value(shared)]
854#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
855pub enum IssueStage {
856 Config,
857 AppStructure,
858 ProcessModule,
859 Load,
861 SourceTransform,
862 Parse,
863 Transform,
865 Analysis,
866 Resolve,
867 Bindings,
868 CodeGen,
869 Emit,
870 Unsupported,
871 Misc,
872 Other(RcStr),
873}
874
875impl Display for IssueStage {
876 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
877 match self {
878 IssueStage::Config => write!(f, "config"),
879 IssueStage::Resolve => write!(f, "resolve"),
880 IssueStage::ProcessModule => write!(f, "process module"),
881 IssueStage::Load => write!(f, "load"),
882 IssueStage::SourceTransform => write!(f, "source transform"),
883 IssueStage::Parse => write!(f, "parse"),
884 IssueStage::Transform => write!(f, "transform"),
885 IssueStage::Analysis => write!(f, "analysis"),
886 IssueStage::Bindings => write!(f, "bindings"),
887 IssueStage::CodeGen => write!(f, "code gen"),
888 IssueStage::Emit => write!(f, "emit"),
889 IssueStage::Unsupported => write!(f, "unsupported"),
890 IssueStage::AppStructure => write!(f, "app structure"),
891 IssueStage::Misc => write!(f, "misc"),
892 IssueStage::Other(s) => write!(f, "{s}"),
893 }
894 }
895}
896
897#[turbo_tasks::value(serialization = "skip")]
898#[derive(Clone, Debug, PartialOrd, Ord)]
899pub struct PlainIssue {
900 pub severity: IssueSeverity,
901 pub stage: IssueStage,
902
903 pub title: StyledString,
904 pub file_path: RcStr,
905
906 pub description: Option<StyledString>,
907 pub detail: Option<StyledString>,
908 pub documentation_link: RcStr,
909
910 pub source: Option<PlainIssueSource>,
911 pub additional_sources: Vec<PlainAdditionalIssueSource>,
912 pub import_traces: Vec<PlainTrace>,
913}
914
915#[turbo_tasks::value(serialization = "skip")]
920#[derive(Debug)]
921pub struct PlainIssues(pub Vec<ReadRef<PlainIssue>>);
922
923#[turbo_tasks::value(serialization = "skip")]
924#[derive(Clone, Debug, PartialOrd, Ord)]
925pub struct PlainAdditionalIssueSource {
926 pub description: RcStr,
927 pub source: PlainIssueSource,
928}
929
930fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
931 hasher.write_ref(&issue.severity);
932 hasher.write_ref(&issue.file_path);
933 hasher.write_ref(&issue.stage);
934 hasher.write_ref(&issue.title);
935 hasher.write_ref(&issue.description);
936 hasher.write_ref(&issue.detail);
937 hasher.write_ref(&issue.documentation_link);
938
939 if let Some(source) = &issue.source {
940 hasher.write_value(1_u8);
941 hasher.write_ref(&source.range);
944 } else {
945 hasher.write_value(0_u8);
946 }
947
948 if full {
955 hasher.write_ref(&issue.import_traces);
956 }
957}
958
959impl PlainIssue {
960 pub fn internal_hash_ref(&self, full: bool) -> u64 {
968 let mut hasher = Xxh3Hash64Hasher::new();
969 hash_plain_issue(self, &mut hasher, full);
970 hasher.finish()
971 }
972}
973
974#[turbo_tasks::value_impl]
975impl PlainIssue {
976 #[turbo_tasks::function]
979 pub async fn from_issue(
980 issue: ResolvedVc<Box<dyn Issue>>,
981 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
982 ) -> Result<Vc<Self>> {
983 Ok(
984 Self::from_issue_ref(&*issue.into_trait_ref().await?, import_tracer)
985 .await?
986 .cell(),
987 )
988 }
989}
990
991impl PlainIssue {
992 pub async fn from_issue_ref(
993 trait_ref: &dyn Issue,
994 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
995 ) -> Result<Self> {
996 let severity = trait_ref.severity();
997 let file_path = trait_ref.file_path().await?;
998 let file_path_str = file_path.to_string_ref().await?;
999
1000 Ok(Self {
1001 severity,
1002 file_path: file_path_str,
1003 stage: trait_ref.stage(),
1004 title: trait_ref.title().await?,
1005 description: trait_ref.description().await?,
1006 detail: trait_ref.detail().await?,
1007 documentation_link: trait_ref.documentation_link(),
1008 source: {
1009 if let Some(s) = trait_ref.source() {
1010 Some(s.into_plain().await?)
1011 } else {
1012 None
1013 }
1014 },
1015 additional_sources: {
1016 trait_ref
1017 .additional_sources()
1018 .await?
1019 .into_iter()
1020 .map(async |s| {
1021 Ok(PlainAdditionalIssueSource {
1022 source: s.source.into_plain().await?,
1023 description: s.description,
1024 })
1025 })
1026 .try_join()
1027 .await?
1028 },
1029 import_traces: match import_tracer {
1030 Some(tracer) => {
1031 into_plain_trace(tracer.await?.get_traces(file_path).await?).await?
1032 }
1033 None => vec![],
1034 },
1035 })
1036 }
1037}
1038
1039#[turbo_tasks::value(serialization = "skip")]
1040#[derive(Clone, Debug, PartialOrd, Ord)]
1041pub struct PlainIssueSource {
1042 pub asset: ReadRef<PlainSource>,
1043 pub range: Option<(SourcePos, SourcePos)>,
1044}
1045
1046#[turbo_tasks::value(serialization = "skip")]
1047#[derive(Clone, Debug, PartialOrd, Ord)]
1048pub struct PlainSource {
1049 pub ident: RcStr,
1050 pub file_path: RcStr,
1051 #[turbo_tasks(debug_ignore)]
1052 pub content: ReadRef<FileContent>,
1053}
1054
1055#[turbo_tasks::value_impl]
1056impl PlainSource {
1057 #[turbo_tasks::function]
1058 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
1059 let content = if let Ok(asset_content) = asset.content().await
1063 && let AssetContent::File(file_content) = &*asset_content
1064 && let Ok(file_content) = file_content.await
1065 {
1066 file_content
1067 } else {
1068 ReadRef::new_owned(FileContent::NotFound)
1069 };
1070 let ident = asset.ident();
1071
1072 Ok(PlainSource {
1073 ident: ident.to_string().owned().await?,
1074 file_path: ident.await?.path.to_string_ref().await?,
1075 content,
1076 }
1077 .cell())
1078 }
1079}
1080
1081#[async_trait]
1082#[turbo_tasks::value_trait]
1083pub trait IssueReporter {
1084 async fn report_issues(
1100 &self,
1101 issues: ReadRef<PlainIssues>,
1102 source: RawVc,
1103 min_failing_severity: IssueSeverity,
1104 ) -> Result<bool>;
1105}
1106
1107pub trait CollectibleIssuesExt
1108where
1109 Self: Sized,
1110{
1111 fn peek_issues(self) -> CapturedIssues;
1115
1116 fn drop_issues(self);
1120}
1121
1122impl<T> CollectibleIssuesExt for T
1123where
1124 T: CollectiblesSource + Copy + Send,
1125{
1126 fn peek_issues(self) -> CapturedIssues {
1127 CapturedIssues {
1128 issues: self.peek_collectibles(),
1129
1130 tracer: DelegatingImportTracer {
1131 delegates: self.peek_collectibles(),
1132 }
1133 .resolved_cell(),
1134 }
1135 }
1136
1137 fn drop_issues(self) {
1138 self.drop_collectibles::<Box<dyn Issue>>();
1139 }
1140}
1141
1142#[turbo_tasks::function(operation, root)]
1149async fn collect_issues(source: OperationVc<()>) -> Result<Vc<PlainIssues>> {
1150 let plain = source
1151 .peek_issues()
1152 .get_plain_issues(&IssueFilter::everything())
1153 .await?;
1154 Ok(PlainIssues(plain).cell())
1155}
1156
1157pub async fn handle_issues<T: Send>(
1161 source_op: OperationVc<T>,
1162 issue_reporter: Vc<Box<dyn IssueReporter>>,
1163 min_failing_severity: IssueSeverity,
1164 path: Option<&str>,
1165 operation: Option<&str>,
1166) -> Result<()> {
1167 let source_vc = source_op.connect();
1168 let _ = source_op.resolve().strongly_consistent().await?;
1169 let source_raw = Vc::into_raw(source_vc);
1170
1171 let erased_source = OperationVc::<()>::try_from(source_raw)?;
1176 let issues = collect_issues(erased_source)
1177 .read_strongly_consistent()
1178 .await?;
1179
1180 let reporter = issue_reporter
1184 .to_resolved()
1185 .strongly_consistent()
1186 .await?
1187 .into_trait_ref()
1188 .await?;
1189 let has_fatal = reporter
1190 .report_issues(issues, source_raw, min_failing_severity)
1191 .await?;
1192
1193 if has_fatal {
1194 let mut message = "Fatal issue(s) occurred".to_owned();
1195 if let Some(path) = path.as_ref() {
1196 message += &format!(" in {path}");
1197 };
1198 if let Some(operation) = operation.as_ref() {
1199 message += &format!(" ({operation})");
1200 };
1201
1202 bail!(message)
1203 } else {
1204 Ok(())
1205 }
1206}
1207
1208fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1209 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1210 Ok(i) => SourcePos {
1211 line: i as u32,
1212 column: 0,
1213 },
1214 Err(i) => {
1215 if i == 0 {
1216 SourcePos {
1217 line: 0,
1218 column: offset,
1219 }
1220 } else {
1221 let line = &lines[i - 1];
1222 SourcePos {
1223 line: (i - 1) as u32,
1224 column: min(line.content.len() as u32, offset - line.bytes_offset),
1225 }
1226 }
1227 }
1228 }
1229}
1230
1231fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1232 let line = &lines[pos.line as usize];
1233 line.bytes_offset + pos.column
1234}