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 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")]
912#[derive(Debug)]
913pub struct PlainIssues(pub Vec<ReadRef<PlainIssue>>);
914
915#[turbo_tasks::value(serialization = "skip")]
916#[derive(Clone, Debug, PartialOrd, Ord)]
917pub struct PlainAdditionalIssueSource {
918 pub description: RcStr,
919 pub source: PlainIssueSource,
920}
921
922fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
923 hasher.write_ref(&issue.severity);
924 hasher.write_ref(&issue.file_path);
925 hasher.write_ref(&issue.stage);
926 hasher.write_ref(&issue.title);
927 hasher.write_ref(&issue.description);
928 hasher.write_ref(&issue.detail);
929 hasher.write_ref(&issue.documentation_link);
930
931 if let Some(source) = &issue.source {
932 hasher.write_value(1_u8);
933 hasher.write_ref(&source.range);
936 } else {
937 hasher.write_value(0_u8);
938 }
939
940 if full {
947 hasher.write_ref(&issue.import_traces);
948 }
949}
950
951impl PlainIssue {
952 pub fn internal_hash_ref(&self, full: bool) -> u64 {
960 let mut hasher = Xxh3Hash64Hasher::new();
961 hash_plain_issue(self, &mut hasher, full);
962 hasher.finish()
963 }
964}
965
966#[turbo_tasks::value_impl]
967impl PlainIssue {
968 #[turbo_tasks::function]
971 pub async fn from_issue(
972 issue: ResolvedVc<Box<dyn Issue>>,
973 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
974 ) -> Result<Vc<Self>> {
975 Ok(
976 Self::from_issue_ref(&*issue.into_trait_ref().await?, import_tracer)
977 .await?
978 .cell(),
979 )
980 }
981}
982
983impl PlainIssue {
984 pub async fn from_issue_ref(
985 trait_ref: &dyn Issue,
986 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
987 ) -> Result<Self> {
988 let severity = trait_ref.severity();
989 let file_path = trait_ref.file_path().await?;
990 let file_path_str = file_path.to_string_ref().await?;
991
992 Ok(Self {
993 severity,
994 file_path: file_path_str,
995 stage: trait_ref.stage(),
996 title: trait_ref.title().await?,
997 description: trait_ref.description().await?,
998 detail: trait_ref.detail().await?,
999 documentation_link: trait_ref.documentation_link(),
1000 source: {
1001 if let Some(s) = trait_ref.source() {
1002 Some(s.into_plain().await?)
1003 } else {
1004 None
1005 }
1006 },
1007 additional_sources: {
1008 trait_ref
1009 .additional_sources()
1010 .await?
1011 .into_iter()
1012 .map(async |s| {
1013 Ok(PlainAdditionalIssueSource {
1014 source: s.source.into_plain().await?,
1015 description: s.description,
1016 })
1017 })
1018 .try_join()
1019 .await?
1020 },
1021 import_traces: match import_tracer {
1022 Some(tracer) => {
1023 into_plain_trace(tracer.await?.get_traces(file_path).await?).await?
1024 }
1025 None => vec![],
1026 },
1027 })
1028 }
1029}
1030
1031#[turbo_tasks::value(serialization = "skip")]
1032#[derive(Clone, Debug, PartialOrd, Ord)]
1033pub struct PlainIssueSource {
1034 pub asset: ReadRef<PlainSource>,
1035 pub range: Option<(SourcePos, SourcePos)>,
1036}
1037
1038#[turbo_tasks::value(serialization = "skip")]
1039#[derive(Clone, Debug, PartialOrd, Ord)]
1040pub struct PlainSource {
1041 pub ident: RcStr,
1042 pub file_path: RcStr,
1043 #[turbo_tasks(debug_ignore)]
1044 pub content: ReadRef<FileContent>,
1045}
1046
1047#[turbo_tasks::value_impl]
1048impl PlainSource {
1049 #[turbo_tasks::function]
1050 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
1051 let content = if let Ok(asset_content) = asset.content().await
1055 && let AssetContent::File(file_content) = &*asset_content
1056 && let Ok(file_content) = file_content.await
1057 {
1058 file_content
1059 } else {
1060 ReadRef::new_owned(FileContent::NotFound)
1061 };
1062 let ident = asset.ident();
1063
1064 Ok(PlainSource {
1065 ident: ident.to_string().owned().await?,
1066 file_path: ident.await?.path.to_string_ref().await?,
1067 content,
1068 }
1069 .cell())
1070 }
1071}
1072
1073#[async_trait]
1074#[turbo_tasks::value_trait]
1075pub trait IssueReporter {
1076 async fn report_issues(
1092 &self,
1093 issues: ReadRef<PlainIssues>,
1094 source: RawVc,
1095 min_failing_severity: IssueSeverity,
1096 ) -> Result<bool>;
1097}
1098
1099pub trait CollectibleIssuesExt
1100where
1101 Self: Sized,
1102{
1103 fn peek_issues(self) -> CapturedIssues;
1107
1108 fn drop_issues(self);
1112}
1113
1114impl<T> CollectibleIssuesExt for T
1115where
1116 T: CollectiblesSource + Copy + Send,
1117{
1118 fn peek_issues(self) -> CapturedIssues {
1119 CapturedIssues {
1120 issues: self.peek_collectibles(),
1121
1122 tracer: DelegatingImportTracer {
1123 delegates: self.peek_collectibles(),
1124 }
1125 .resolved_cell(),
1126 }
1127 }
1128
1129 fn drop_issues(self) {
1130 self.drop_collectibles::<Box<dyn Issue>>();
1131 }
1132}
1133
1134#[turbo_tasks::function(operation, root)]
1141async fn collect_issues(source: OperationVc<()>) -> Result<Vc<PlainIssues>> {
1142 let plain = source
1143 .peek_issues()
1144 .get_plain_issues(&IssueFilter::everything())
1145 .await?;
1146 Ok(PlainIssues(plain).cell())
1147}
1148
1149pub async fn handle_issues<T: Send>(
1153 source_op: OperationVc<T>,
1154 issue_reporter: Vc<Box<dyn IssueReporter>>,
1155 min_failing_severity: IssueSeverity,
1156 path: Option<&str>,
1157 operation: Option<&str>,
1158) -> Result<()> {
1159 let source_vc = source_op.connect();
1160 let _ = source_op.resolve().strongly_consistent().await?;
1161 let source_raw = Vc::into_raw(source_vc);
1162
1163 let erased_source = OperationVc::<()>::try_from(source_raw)?;
1168 let issues = collect_issues(erased_source)
1169 .read_strongly_consistent()
1170 .await?;
1171
1172 let reporter = issue_reporter
1176 .to_resolved()
1177 .strongly_consistent()
1178 .await?
1179 .into_trait_ref()
1180 .await?;
1181 let has_fatal = reporter
1182 .report_issues(issues, source_raw, min_failing_severity)
1183 .await?;
1184
1185 if has_fatal {
1186 let mut message = "Fatal issue(s) occurred".to_owned();
1187 if let Some(path) = path.as_ref() {
1188 message += &format!(" in {path}");
1189 };
1190 if let Some(operation) = operation.as_ref() {
1191 message += &format!(" ({operation})");
1192 };
1193
1194 bail!(message)
1195 } else {
1196 Ok(())
1197 }
1198}
1199
1200fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1201 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1202 Ok(i) => SourcePos {
1203 line: i as u32,
1204 column: 0,
1205 },
1206 Err(i) => {
1207 if i == 0 {
1208 SourcePos {
1209 line: 0,
1210 column: offset,
1211 }
1212 } else {
1213 let line = &lines[i - 1];
1214 SourcePos {
1215 line: (i - 1) as u32,
1216 column: min(line.content.len() as u32, offset - line.bytes_offset),
1217 }
1218 }
1219 }
1220 }
1221}
1222
1223fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1224 let line = &lines[pos.line as usize];
1225 line.bytes_offset + pos.column
1226}