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, anyhow};
12use auto_hash_map::AutoSet;
13use serde::{Deserialize, Serialize};
14use turbo_rcstr::RcStr;
15use turbo_tasks::{
16 CollectiblesSource, IntoTraitRef, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc,
17 TaskInput, TransientValue, TryJoinIterExt, Upcast, ValueDefault, ValueToString, Vc, emit,
18 trace::TraceRawVcs,
19};
20use turbo_tasks_fs::{FileContent, FileLine, FileLinesContent, FileSystem, FileSystemPath};
21use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
22
23use crate::{
24 asset::{Asset, AssetContent},
25 ident::{AssetIdent, Layer},
26 source::Source,
27 source_map::{GenerateSourceMap, SourceMap, TokenWithSource},
28 source_pos::SourcePos,
29};
30
31#[turbo_tasks::value(shared)]
32#[derive(PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput)]
33#[serde(rename_all = "camelCase")]
34pub enum IssueSeverity {
35 Bug,
36 Fatal,
37 Error,
38 Warning,
39 Hint,
40 Note,
41 Suggestion,
42 Info,
43}
44
45impl IssueSeverity {
46 pub fn as_str(&self) -> &'static str {
47 match self {
48 IssueSeverity::Bug => "bug",
49 IssueSeverity::Fatal => "fatal",
50 IssueSeverity::Error => "error",
51 IssueSeverity::Warning => "warning",
52 IssueSeverity::Hint => "hint",
53 IssueSeverity::Note => "note",
54 IssueSeverity::Suggestion => "suggestion",
55 IssueSeverity::Info => "info",
56 }
57 }
58
59 pub fn as_help_str(&self) -> &'static str {
60 match self {
61 IssueSeverity::Bug => "bug in implementation",
62 IssueSeverity::Fatal => "unrecoverable problem",
63 IssueSeverity::Error => "problem that cause a broken result",
64 IssueSeverity::Warning => "problem should be addressed in short term",
65 IssueSeverity::Hint => "idea for improvement",
66 IssueSeverity::Note => "detail that is worth mentioning",
67 IssueSeverity::Suggestion => "change proposal for improvement",
68 IssueSeverity::Info => "detail that is worth telling",
69 }
70 }
71}
72
73impl Display for IssueSeverity {
74 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
75 f.write_str(self.as_str())
76 }
77}
78
79#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash)]
83#[turbo_tasks::value(shared)]
84pub enum StyledString {
85 Line(Vec<StyledString>),
89 Stack(Vec<StyledString>),
92 Text(RcStr),
94 Code(RcStr),
97 Strong(RcStr),
99}
100
101#[turbo_tasks::value_trait]
102pub trait Issue {
103 fn severity(&self) -> IssueSeverity {
106 IssueSeverity::Error
107 }
108
109 #[turbo_tasks::function]
112 fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
113
114 #[turbo_tasks::function]
117 fn stage(self: Vc<Self>) -> Vc<IssueStage>;
118
119 #[turbo_tasks::function]
124 fn title(self: Vc<Self>) -> Vc<StyledString>;
125
126 #[turbo_tasks::function]
130 fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
131 Vc::cell(None)
132 }
133
134 #[turbo_tasks::function]
138 fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
139 Vc::cell(None)
140 }
141
142 #[turbo_tasks::function]
145 fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
146 Vc::<RcStr>::default()
147 }
148
149 #[turbo_tasks::function]
153 fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
154 Vc::cell(None)
155 }
156}
157
158#[turbo_tasks::value_trait]
160pub trait ImportTracer {
161 #[turbo_tasks::function]
162 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
163}
164
165#[turbo_tasks::value]
166#[derive(Debug)]
167pub struct DelegatingImportTracer {
168 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
169}
170
171impl DelegatingImportTracer {
172 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
173 Ok(self
174 .delegates
175 .iter()
176 .map(|d| d.get_traces(path.clone()))
177 .try_join()
178 .await?
179 .iter()
180 .flat_map(|v| v.0.iter().cloned())
181 .collect())
182 }
183}
184
185pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
186
187#[turbo_tasks::value(shared)]
188pub struct ImportTraces(pub Vec<ImportTrace>);
189
190#[turbo_tasks::value_impl]
191impl ValueDefault for ImportTraces {
192 #[turbo_tasks::function]
193 fn value_default() -> Vc<Self> {
194 Self::cell(ImportTraces(vec![]))
195 }
196}
197
198pub trait IssueExt {
199 fn emit(self);
200}
201
202impl<T> IssueExt for ResolvedVc<T>
203where
204 T: Upcast<Box<dyn Issue>>,
205{
206 fn emit(self) {
207 emit(ResolvedVc::upcast_non_strict::<Box<dyn Issue>>(self));
208 }
209}
210
211#[turbo_tasks::value(transparent)]
212pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
213
214#[turbo_tasks::value(shared)]
217#[derive(Debug)]
218pub struct CapturedIssues {
219 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
220 tracer: ResolvedVc<DelegatingImportTracer>,
221}
222
223#[turbo_tasks::value_impl]
224impl CapturedIssues {
225 #[turbo_tasks::function]
226 pub fn is_empty(&self) -> Vc<bool> {
227 Vc::cell(self.is_empty_ref())
228 }
229}
230
231impl CapturedIssues {
232 pub fn is_empty_ref(&self) -> bool {
234 self.issues.is_empty()
235 }
236
237 #[allow(clippy::len_without_is_empty)]
239 pub fn len(&self) -> usize {
240 self.issues.len()
241 }
242
243 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
245 self.issues.iter().copied()
246 }
247
248 pub async fn get_plain_issues(&self) -> Result<Vec<ReadRef<PlainIssue>>> {
250 let mut list = self
251 .issues
252 .iter()
253 .map(|issue| async move { PlainIssue::from_issue(**issue, Some(*self.tracer)).await })
254 .try_join()
255 .await?;
256 list.sort();
257 Ok(list)
258 }
259}
260
261#[derive(
262 Clone,
263 Copy,
264 Debug,
265 PartialEq,
266 Eq,
267 Serialize,
268 Deserialize,
269 Hash,
270 TaskInput,
271 TraceRawVcs,
272 NonLocalValue,
273)]
274pub struct IssueSource {
275 source: ResolvedVc<Box<dyn Source>>,
276 range: Option<SourceRange>,
277}
278
279#[derive(
281 Clone,
282 Copy,
283 Debug,
284 PartialEq,
285 Eq,
286 Serialize,
287 Deserialize,
288 Hash,
289 TaskInput,
290 TraceRawVcs,
291 NonLocalValue,
292)]
293enum SourceRange {
294 LineColumn(SourcePos, SourcePos),
295 ByteOffset(u32, u32),
296}
297
298impl IssueSource {
299 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
302 IssueSource {
303 source,
304 range: None,
305 }
306 }
307
308 pub fn from_line_col(
309 source: ResolvedVc<Box<dyn Source>>,
310 start: SourcePos,
311 end: SourcePos,
312 ) -> Self {
313 IssueSource {
314 source,
315 range: Some(SourceRange::LineColumn(start, end)),
316 }
317 }
318
319 async fn into_plain(self) -> Result<PlainIssueSource> {
320 let Self { mut source, range } = self;
321
322 let range = if let Some(range) = range {
323 let mut range = match range {
324 SourceRange::LineColumn(start, end) => Some((start, end)),
325 SourceRange::ByteOffset(start, end) => {
326 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
327 let start = find_line_and_column(lines.as_ref(), start);
328 let end = find_line_and_column(lines.as_ref(), end);
329 Some((start, end))
330 } else {
331 None
332 }
333 }
334 };
335
336 if let Some((start, end)) = range {
338 let mapped = source_pos(source, start, end).await?;
339
340 if let Some((mapped_source, start, end)) = mapped {
341 range = Some((start, end));
342 source = mapped_source;
343 }
344 }
345 range
346 } else {
347 None
348 };
349 Ok(PlainIssueSource {
350 asset: PlainSource::from_source(*source).await?,
351 range,
352 })
353 }
354
355 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
364 IssueSource {
365 source,
366 range: match (start == 0, end == 0) {
367 (true, true) => None,
368 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
369 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
370 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
371 },
372 }
373 }
374
375 pub async fn from_byte_offset(
385 source: ResolvedVc<Box<dyn Source>>,
386 start: u32,
387 end: u32,
388 ) -> Result<Self> {
389 Ok(IssueSource {
390 source,
391 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
392 let start = find_line_and_column(lines.as_ref(), start);
393 let end = find_line_and_column(lines.as_ref(), end);
394 Some(SourceRange::LineColumn(start, end))
395 } else {
396 None
397 },
398 })
399 }
400
401 pub fn file_path(&self) -> Vc<FileSystemPath> {
403 self.source.ident().path()
404 }
405}
406
407impl IssueSource {
408 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
410 Ok(match &self.range {
411 Some(range) => match range {
412 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
413 SourceRange::LineColumn(start, end) => {
414 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
415 let start = find_offset(lines.as_ref(), *start) + 1;
416 let end = find_offset(lines.as_ref(), *end) + 1;
417 Some((start, end))
418 } else {
419 None
420 }
421 }
422 },
423 _ => None,
424 })
425 }
426}
427
428async fn source_pos(
429 source: ResolvedVc<Box<dyn Source>>,
430 start: SourcePos,
431 end: SourcePos,
432) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
433 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
434 return Ok(None);
435 };
436
437 let srcmap = generator.generate_source_map();
438 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
439 return Ok(None);
440 };
441
442 let find = async |line: u32, col: u32| {
443 let TokenWithSource {
444 token,
445 source_content,
446 } = &srcmap.lookup_token_and_source(line, col).await?;
447
448 match token {
449 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
450 SourcePos {
451 line: t.generated_line as _,
452 column: t.generated_column as _,
453 },
454 *source_content,
455 )),
456 crate::source_map::Token::Original(t) => anyhow::Ok((
457 SourcePos {
458 line: t.original_line as _,
459 column: t.original_column as _,
460 },
461 *source_content,
462 )),
463 }
464 };
465
466 let (start, content_1) = find(start.line, start.column).await?;
467 let (end, content_2) = find(end.line, end.column).await?;
468
469 let Some((content_1, content_2)) = content_1.zip(content_2) else {
470 return Ok(None);
471 };
472
473 if content_1 != content_2 {
474 return Ok(None);
475 }
476
477 Ok(Some((content_1, start, end)))
478}
479
480#[turbo_tasks::value(transparent)]
481pub struct OptionIssueSource(Option<IssueSource>);
482
483#[turbo_tasks::value(transparent)]
484pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
485
486#[derive(
488 Serialize,
489 PartialEq,
490 Eq,
491 PartialOrd,
492 Ord,
493 Clone,
494 Debug,
495 TraceRawVcs,
496 NonLocalValue,
497 DeterministicHash,
498)]
499#[serde(rename_all = "camelCase")]
500pub struct PlainTraceItem {
501 pub fs_name: RcStr,
503 pub root_path: RcStr,
505 pub path: RcStr,
507 pub layer: Option<RcStr>,
509}
510
511impl PlainTraceItem {
512 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
513 let fs_path = asset.path.clone();
516 let fs_name = fs_path.fs.to_string().owned().await?;
517 let root_path = fs_path.fs.root().await?.path.clone();
518 let path = fs_path.path.clone();
519 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
520 Ok(Self {
521 fs_name,
522 root_path,
523 path,
524 layer,
525 })
526 }
527}
528
529pub type PlainTrace = Vec<PlainTraceItem>;
530
531async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
533 let mut plain_traces = traces
534 .into_iter()
535 .map(|trace| async move {
536 let mut plain_trace = trace
537 .into_iter()
538 .filter(|asset| {
539 asset.assets.is_empty()
542 })
543 .map(PlainTraceItem::from_asset_ident)
544 .try_join()
545 .await?;
546
547 plain_trace.dedup();
564
565 Ok(plain_trace)
566 })
567 .try_join()
568 .await?;
569
570 plain_traces.retain(|t| t.len() > 1);
573 plain_traces.sort_by(|a, b| {
576 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
578 });
579
580 if plain_traces.len() > 1 {
588 let mut i = 0;
589 while i < plain_traces.len() - 1 {
590 let mut j = plain_traces.len() - 1;
591 while j > i {
592 if plain_traces[j].ends_with(&plain_traces[i]) {
593 plain_traces.remove(j);
599 }
600 j -= 1;
601 }
602 i += 1;
603 }
604 }
605
606 Ok(plain_traces)
607}
608
609#[turbo_tasks::value(shared)]
610#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash)]
611pub enum IssueStage {
612 Config,
613 AppStructure,
614 ProcessModule,
615 Load,
617 SourceTransform,
618 Parse,
619 Transform,
621 Analysis,
622 Resolve,
623 Bindings,
624 CodeGen,
625 Unsupported,
626 Misc,
627 Other(RcStr),
628}
629
630impl Display for IssueStage {
631 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
632 match self {
633 IssueStage::Config => write!(f, "config"),
634 IssueStage::Resolve => write!(f, "resolve"),
635 IssueStage::ProcessModule => write!(f, "process module"),
636 IssueStage::Load => write!(f, "load"),
637 IssueStage::SourceTransform => write!(f, "source transform"),
638 IssueStage::Parse => write!(f, "parse"),
639 IssueStage::Transform => write!(f, "transform"),
640 IssueStage::Analysis => write!(f, "analysis"),
641 IssueStage::Bindings => write!(f, "bindings"),
642 IssueStage::CodeGen => write!(f, "code gen"),
643 IssueStage::Unsupported => write!(f, "unsupported"),
644 IssueStage::AppStructure => write!(f, "app structure"),
645 IssueStage::Misc => write!(f, "misc"),
646 IssueStage::Other(s) => write!(f, "{s}"),
647 }
648 }
649}
650
651#[turbo_tasks::value(serialization = "none")]
652#[derive(Clone, Debug, PartialOrd, Ord)]
653pub struct PlainIssue {
654 pub severity: IssueSeverity,
655 pub stage: IssueStage,
656
657 pub title: StyledString,
658 pub file_path: RcStr,
659
660 pub description: Option<StyledString>,
661 pub detail: Option<StyledString>,
662 pub documentation_link: RcStr,
663
664 pub source: Option<PlainIssueSource>,
665 pub import_traces: Vec<PlainTrace>,
666}
667
668fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
669 hasher.write_ref(&issue.severity);
670 hasher.write_ref(&issue.file_path);
671 hasher.write_ref(&issue.stage);
672 hasher.write_ref(&issue.title);
673 hasher.write_ref(&issue.description);
674 hasher.write_ref(&issue.detail);
675 hasher.write_ref(&issue.documentation_link);
676
677 if let Some(source) = &issue.source {
678 hasher.write_value(1_u8);
679 hasher.write_ref(&source.range);
682 } else {
683 hasher.write_value(0_u8);
684 }
685
686 if full {
687 hasher.write_ref(&issue.import_traces);
688 }
689}
690
691impl PlainIssue {
692 pub fn internal_hash_ref(&self, full: bool) -> u64 {
701 let mut hasher = Xxh3Hash64Hasher::new();
702 hash_plain_issue(self, &mut hasher, full);
703 hasher.finish()
704 }
705}
706
707#[turbo_tasks::value_impl]
708impl PlainIssue {
709 #[turbo_tasks::function]
712 pub async fn from_issue(
713 issue: ResolvedVc<Box<dyn Issue>>,
714 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
715 ) -> Result<Vc<Self>> {
716 let description: Option<StyledString> = match *issue.description().await? {
717 Some(description) => Some(description.owned().await?),
718 None => None,
719 };
720 let detail = match *issue.detail().await? {
721 Some(detail) => Some(detail.owned().await?),
722 None => None,
723 };
724 let trait_ref = issue.into_trait_ref().await?;
725
726 let severity = trait_ref.severity();
727
728 Ok(Self::cell(Self {
729 severity,
730 file_path: issue.file_path().to_string().owned().await?,
731 stage: issue.stage().owned().await?,
732 title: issue.title().owned().await?,
733 description,
734 detail,
735 documentation_link: issue.documentation_link().owned().await?,
736 source: {
737 if let Some(s) = &*issue.source().await? {
738 Some(s.into_plain().await?)
739 } else {
740 None
741 }
742 },
743 import_traces: match import_tracer {
744 Some(tracer) => {
745 into_plain_trace(
746 tracer
747 .await?
748 .get_traces(issue.file_path().owned().await?)
749 .await?,
750 )
751 .await?
752 }
753 None => vec![],
754 },
755 }))
756 }
757}
758
759#[turbo_tasks::value(serialization = "none")]
760#[derive(Clone, Debug, PartialOrd, Ord)]
761pub struct PlainIssueSource {
762 pub asset: ReadRef<PlainSource>,
763 pub range: Option<(SourcePos, SourcePos)>,
764}
765
766#[turbo_tasks::value(serialization = "none")]
767#[derive(Clone, Debug, PartialOrd, Ord)]
768pub struct PlainSource {
769 pub ident: ReadRef<RcStr>,
770 #[turbo_tasks(debug_ignore)]
771 pub content: ReadRef<FileContent>,
772}
773
774#[turbo_tasks::value_impl]
775impl PlainSource {
776 #[turbo_tasks::function]
777 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
778 let asset_content = asset.content().await?;
779 let content = match *asset_content {
780 AssetContent::File(file_content) => file_content.await?,
781 AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
782 };
783
784 Ok(PlainSource {
785 ident: asset.ident().to_string().await?,
786 content,
787 }
788 .cell())
789 }
790}
791
792#[turbo_tasks::value_trait]
793pub trait IssueReporter {
794 #[turbo_tasks::function]
805 fn report_issues(
806 self: Vc<Self>,
807 source: TransientValue<RawVc>,
808 min_failing_severity: IssueSeverity,
809 ) -> Vc<bool>;
810}
811
812pub trait CollectibleIssuesExt
813where
814 Self: Sized,
815{
816 fn peek_issues(self) -> CapturedIssues;
820
821 fn drop_issues(self);
825}
826
827impl<T> CollectibleIssuesExt for T
828where
829 T: CollectiblesSource + Copy + Send,
830{
831 fn peek_issues(self) -> CapturedIssues {
832 CapturedIssues {
833 issues: self.peek_collectibles(),
834
835 tracer: DelegatingImportTracer {
836 delegates: self.peek_collectibles(),
837 }
838 .resolved_cell(),
839 }
840 }
841
842 fn drop_issues(self) {
843 self.drop_collectibles::<Box<dyn Issue>>();
844 }
845}
846
847pub async fn handle_issues<T: Send>(
851 source_op: OperationVc<T>,
852 issue_reporter: Vc<Box<dyn IssueReporter>>,
853 min_failing_severity: IssueSeverity,
854 path: Option<&str>,
855 operation: Option<&str>,
856) -> Result<()> {
857 let source_vc = source_op.connect();
858 let _ = source_op.resolve_strongly_consistent().await?;
859
860 let has_fatal = issue_reporter.report_issues(
861 TransientValue::new(Vc::into_raw(source_vc)),
862 min_failing_severity,
863 );
864
865 if *has_fatal.await? {
866 let mut message = "Fatal issue(s) occurred".to_owned();
867 if let Some(path) = path.as_ref() {
868 message += &format!(" in {path}");
869 };
870 if let Some(operation) = operation.as_ref() {
871 message += &format!(" ({operation})");
872 };
873
874 Err(anyhow!(message))
875 } else {
876 Ok(())
877 }
878}
879
880fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
881 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
882 Ok(i) => SourcePos {
883 line: i as u32,
884 column: 0,
885 },
886 Err(i) => {
887 if i == 0 {
888 SourcePos {
889 line: 0,
890 column: offset,
891 }
892 } else {
893 let line = &lines[i - 1];
894 SourcePos {
895 line: (i - 1) as u32,
896 column: min(line.content.len() as u32, offset - line.bytes_offset),
897 }
898 }
899 }
900 }
901}
902
903fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
904 let line = &lines[pos.line as usize];
905 line.bytes_offset + pos.column
906}