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
101impl StyledString {
102 pub fn to_unstyled_string(&self) -> String {
103 match self {
104 StyledString::Line(items) => items
105 .iter()
106 .map(|item| item.to_unstyled_string())
107 .collect::<Vec<_>>()
108 .join(""),
109 StyledString::Stack(items) => items
110 .iter()
111 .map(|item| item.to_unstyled_string())
112 .collect::<Vec<_>>()
113 .join("\n"),
114 StyledString::Text(s) | StyledString::Code(s) | StyledString::Strong(s) => {
115 s.to_string()
116 }
117 }
118 }
119}
120
121#[turbo_tasks::value_trait]
122pub trait Issue {
123 fn severity(&self) -> IssueSeverity {
126 IssueSeverity::Error
127 }
128
129 #[turbo_tasks::function]
132 fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
133
134 #[turbo_tasks::function]
137 fn stage(self: Vc<Self>) -> Vc<IssueStage>;
138
139 #[turbo_tasks::function]
144 fn title(self: Vc<Self>) -> Vc<StyledString>;
145
146 #[turbo_tasks::function]
150 fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
151 Vc::cell(None)
152 }
153
154 #[turbo_tasks::function]
158 fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
159 Vc::cell(None)
160 }
161
162 #[turbo_tasks::function]
165 fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
166 Vc::<RcStr>::default()
167 }
168
169 #[turbo_tasks::function]
173 fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
174 Vc::cell(None)
175 }
176}
177
178#[turbo_tasks::value_trait]
180pub trait ImportTracer {
181 #[turbo_tasks::function]
182 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
183}
184
185#[turbo_tasks::value]
186#[derive(Debug)]
187pub struct DelegatingImportTracer {
188 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
189}
190
191impl DelegatingImportTracer {
192 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
193 Ok(self
194 .delegates
195 .iter()
196 .map(|d| d.get_traces(path.clone()))
197 .try_join()
198 .await?
199 .iter()
200 .flat_map(|v| v.0.iter().cloned())
201 .collect())
202 }
203}
204
205pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
206
207#[turbo_tasks::value(shared)]
208pub struct ImportTraces(pub Vec<ImportTrace>);
209
210#[turbo_tasks::value_impl]
211impl ValueDefault for ImportTraces {
212 #[turbo_tasks::function]
213 fn value_default() -> Vc<Self> {
214 Self::cell(ImportTraces(vec![]))
215 }
216}
217
218pub trait IssueExt {
219 fn emit(self);
220}
221
222impl<T> IssueExt for ResolvedVc<T>
223where
224 T: Upcast<Box<dyn Issue>>,
225{
226 fn emit(self) {
227 emit(ResolvedVc::upcast_non_strict::<Box<dyn Issue>>(self));
228 }
229}
230
231#[turbo_tasks::value(transparent)]
232pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
233
234#[turbo_tasks::value(shared)]
237#[derive(Debug)]
238pub struct CapturedIssues {
239 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
240 tracer: ResolvedVc<DelegatingImportTracer>,
241}
242
243#[turbo_tasks::value_impl]
244impl CapturedIssues {
245 #[turbo_tasks::function]
246 pub fn is_empty(&self) -> Vc<bool> {
247 Vc::cell(self.is_empty_ref())
248 }
249}
250
251impl CapturedIssues {
252 pub fn is_empty_ref(&self) -> bool {
254 self.issues.is_empty()
255 }
256
257 #[allow(clippy::len_without_is_empty)]
259 pub fn len(&self) -> usize {
260 self.issues.len()
261 }
262
263 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
265 self.issues.iter().copied()
266 }
267
268 pub async fn get_plain_issues(&self) -> Result<Vec<ReadRef<PlainIssue>>> {
270 let mut list = self
271 .issues
272 .iter()
273 .map(|issue| async move { PlainIssue::from_issue(**issue, Some(*self.tracer)).await })
274 .try_join()
275 .await?;
276 list.sort();
277 Ok(list)
278 }
279}
280
281#[derive(
282 Clone,
283 Copy,
284 Debug,
285 PartialEq,
286 Eq,
287 Serialize,
288 Deserialize,
289 Hash,
290 TaskInput,
291 TraceRawVcs,
292 NonLocalValue,
293)]
294pub struct IssueSource {
295 source: ResolvedVc<Box<dyn Source>>,
296 range: Option<SourceRange>,
297}
298
299#[derive(
301 Clone,
302 Copy,
303 Debug,
304 PartialEq,
305 Eq,
306 Serialize,
307 Deserialize,
308 Hash,
309 TaskInput,
310 TraceRawVcs,
311 NonLocalValue,
312)]
313enum SourceRange {
314 LineColumn(SourcePos, SourcePos),
315 ByteOffset(u32, u32),
316}
317
318impl IssueSource {
319 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
322 IssueSource {
323 source,
324 range: None,
325 }
326 }
327
328 pub fn from_line_col(
329 source: ResolvedVc<Box<dyn Source>>,
330 start: SourcePos,
331 end: SourcePos,
332 ) -> Self {
333 IssueSource {
334 source,
335 range: Some(SourceRange::LineColumn(start, end)),
336 }
337 }
338
339 async fn into_plain(self) -> Result<PlainIssueSource> {
340 let Self { mut source, range } = self;
341
342 let range = if let Some(range) = range {
343 let mut range = match range {
344 SourceRange::LineColumn(start, end) => Some((start, end)),
345 SourceRange::ByteOffset(start, end) => {
346 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
347 let start = find_line_and_column(lines.as_ref(), start);
348 let end = find_line_and_column(lines.as_ref(), end);
349 Some((start, end))
350 } else {
351 None
352 }
353 }
354 };
355
356 if let Some((start, end)) = range {
358 let mapped = source_pos(source, start, end).await?;
359
360 if let Some((mapped_source, start, end)) = mapped {
361 range = Some((start, end));
362 source = mapped_source;
363 }
364 }
365 range
366 } else {
367 None
368 };
369 Ok(PlainIssueSource {
370 asset: PlainSource::from_source(*source).await?,
371 range,
372 })
373 }
374
375 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
384 IssueSource {
385 source,
386 range: match (start == 0, end == 0) {
387 (true, true) => None,
388 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
389 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
390 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
391 },
392 }
393 }
394
395 pub async fn from_byte_offset(
405 source: ResolvedVc<Box<dyn Source>>,
406 start: u32,
407 end: u32,
408 ) -> Result<Self> {
409 Ok(IssueSource {
410 source,
411 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
412 let start = find_line_and_column(lines.as_ref(), start);
413 let end = find_line_and_column(lines.as_ref(), end);
414 Some(SourceRange::LineColumn(start, end))
415 } else {
416 None
417 },
418 })
419 }
420
421 pub fn file_path(&self) -> Vc<FileSystemPath> {
423 self.source.ident().path()
424 }
425}
426
427impl IssueSource {
428 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
430 Ok(match &self.range {
431 Some(range) => match range {
432 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
433 SourceRange::LineColumn(start, end) => {
434 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
435 let start = find_offset(lines.as_ref(), *start) + 1;
436 let end = find_offset(lines.as_ref(), *end) + 1;
437 Some((start, end))
438 } else {
439 None
440 }
441 }
442 },
443 _ => None,
444 })
445 }
446}
447
448async fn source_pos(
449 source: ResolvedVc<Box<dyn Source>>,
450 start: SourcePos,
451 end: SourcePos,
452) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
453 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
454 return Ok(None);
455 };
456
457 let srcmap = generator.generate_source_map();
458 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
459 return Ok(None);
460 };
461
462 let find = async |line: u32, col: u32| {
463 let TokenWithSource {
464 token,
465 source_content,
466 } = &srcmap.lookup_token_and_source(line, col).await?;
467
468 match token {
469 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
470 SourcePos {
471 line: t.generated_line as _,
472 column: t.generated_column as _,
473 },
474 *source_content,
475 )),
476 crate::source_map::Token::Original(t) => anyhow::Ok((
477 SourcePos {
478 line: t.original_line as _,
479 column: t.original_column as _,
480 },
481 *source_content,
482 )),
483 }
484 };
485
486 let (start, content_1) = find(start.line, start.column).await?;
487 let (end, content_2) = find(end.line, end.column).await?;
488
489 let Some((content_1, content_2)) = content_1.zip(content_2) else {
490 return Ok(None);
491 };
492
493 if content_1 != content_2 {
494 return Ok(None);
495 }
496
497 Ok(Some((content_1, start, end)))
498}
499
500#[turbo_tasks::value(transparent)]
501pub struct OptionIssueSource(Option<IssueSource>);
502
503#[turbo_tasks::value(transparent)]
504pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
505
506#[derive(
508 Serialize,
509 PartialEq,
510 Eq,
511 PartialOrd,
512 Ord,
513 Clone,
514 Debug,
515 TraceRawVcs,
516 NonLocalValue,
517 DeterministicHash,
518)]
519#[serde(rename_all = "camelCase")]
520pub struct PlainTraceItem {
521 pub fs_name: RcStr,
523 pub root_path: RcStr,
525 pub path: RcStr,
527 pub layer: Option<RcStr>,
529}
530
531impl PlainTraceItem {
532 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
533 let fs_path = asset.path.clone();
536 let fs_name = fs_path.fs.to_string().owned().await?;
537 let root_path = fs_path.fs.root().await?.path.clone();
538 let path = fs_path.path.clone();
539 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
540 Ok(Self {
541 fs_name,
542 root_path,
543 path,
544 layer,
545 })
546 }
547}
548
549pub type PlainTrace = Vec<PlainTraceItem>;
550
551async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
553 let mut plain_traces = traces
554 .into_iter()
555 .map(|trace| async move {
556 let mut plain_trace = trace
557 .into_iter()
558 .filter(|asset| {
559 asset.assets.is_empty()
562 })
563 .map(PlainTraceItem::from_asset_ident)
564 .try_join()
565 .await?;
566
567 plain_trace.dedup();
584
585 Ok(plain_trace)
586 })
587 .try_join()
588 .await?;
589
590 plain_traces.retain(|t| t.len() > 1);
593 plain_traces.sort_by(|a, b| {
596 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
598 });
599
600 if plain_traces.len() > 1 {
608 let mut i = 0;
609 while i < plain_traces.len() - 1 {
610 let mut j = plain_traces.len() - 1;
611 while j > i {
612 if plain_traces[j].ends_with(&plain_traces[i]) {
613 plain_traces.remove(j);
619 }
620 j -= 1;
621 }
622 i += 1;
623 }
624 }
625
626 Ok(plain_traces)
627}
628
629#[turbo_tasks::value(shared)]
630#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash)]
631pub enum IssueStage {
632 Config,
633 AppStructure,
634 ProcessModule,
635 Load,
637 SourceTransform,
638 Parse,
639 Transform,
641 Analysis,
642 Resolve,
643 Bindings,
644 CodeGen,
645 Unsupported,
646 Misc,
647 Other(RcStr),
648}
649
650impl Display for IssueStage {
651 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
652 match self {
653 IssueStage::Config => write!(f, "config"),
654 IssueStage::Resolve => write!(f, "resolve"),
655 IssueStage::ProcessModule => write!(f, "process module"),
656 IssueStage::Load => write!(f, "load"),
657 IssueStage::SourceTransform => write!(f, "source transform"),
658 IssueStage::Parse => write!(f, "parse"),
659 IssueStage::Transform => write!(f, "transform"),
660 IssueStage::Analysis => write!(f, "analysis"),
661 IssueStage::Bindings => write!(f, "bindings"),
662 IssueStage::CodeGen => write!(f, "code gen"),
663 IssueStage::Unsupported => write!(f, "unsupported"),
664 IssueStage::AppStructure => write!(f, "app structure"),
665 IssueStage::Misc => write!(f, "misc"),
666 IssueStage::Other(s) => write!(f, "{s}"),
667 }
668 }
669}
670
671#[turbo_tasks::value(serialization = "none")]
672#[derive(Clone, Debug, PartialOrd, Ord)]
673pub struct PlainIssue {
674 pub severity: IssueSeverity,
675 pub stage: IssueStage,
676
677 pub title: StyledString,
678 pub file_path: RcStr,
679
680 pub description: Option<StyledString>,
681 pub detail: Option<StyledString>,
682 pub documentation_link: RcStr,
683
684 pub source: Option<PlainIssueSource>,
685 pub import_traces: Vec<PlainTrace>,
686}
687
688fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
689 hasher.write_ref(&issue.severity);
690 hasher.write_ref(&issue.file_path);
691 hasher.write_ref(&issue.stage);
692 hasher.write_ref(&issue.title);
693 hasher.write_ref(&issue.description);
694 hasher.write_ref(&issue.detail);
695 hasher.write_ref(&issue.documentation_link);
696
697 if let Some(source) = &issue.source {
698 hasher.write_value(1_u8);
699 hasher.write_ref(&source.range);
702 } else {
703 hasher.write_value(0_u8);
704 }
705
706 if full {
707 hasher.write_ref(&issue.import_traces);
708 }
709}
710
711impl PlainIssue {
712 pub fn internal_hash_ref(&self, full: bool) -> u64 {
721 let mut hasher = Xxh3Hash64Hasher::new();
722 hash_plain_issue(self, &mut hasher, full);
723 hasher.finish()
724 }
725}
726
727#[turbo_tasks::value_impl]
728impl PlainIssue {
729 #[turbo_tasks::function]
732 pub async fn from_issue(
733 issue: ResolvedVc<Box<dyn Issue>>,
734 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
735 ) -> Result<Vc<Self>> {
736 let description: Option<StyledString> = match *issue.description().await? {
737 Some(description) => Some(description.owned().await?),
738 None => None,
739 };
740 let detail = match *issue.detail().await? {
741 Some(detail) => Some(detail.owned().await?),
742 None => None,
743 };
744 let trait_ref = issue.into_trait_ref().await?;
745
746 let severity = trait_ref.severity();
747
748 Ok(Self::cell(Self {
749 severity,
750 file_path: issue.file_path().to_string().owned().await?,
751 stage: issue.stage().owned().await?,
752 title: issue.title().owned().await?,
753 description,
754 detail,
755 documentation_link: issue.documentation_link().owned().await?,
756 source: {
757 if let Some(s) = &*issue.source().await? {
758 Some(s.into_plain().await?)
759 } else {
760 None
761 }
762 },
763 import_traces: match import_tracer {
764 Some(tracer) => {
765 into_plain_trace(
766 tracer
767 .await?
768 .get_traces(issue.file_path().owned().await?)
769 .await?,
770 )
771 .await?
772 }
773 None => vec![],
774 },
775 }))
776 }
777}
778
779#[turbo_tasks::value(serialization = "none")]
780#[derive(Clone, Debug, PartialOrd, Ord)]
781pub struct PlainIssueSource {
782 pub asset: ReadRef<PlainSource>,
783 pub range: Option<(SourcePos, SourcePos)>,
784}
785
786#[turbo_tasks::value(serialization = "none")]
787#[derive(Clone, Debug, PartialOrd, Ord)]
788pub struct PlainSource {
789 pub ident: ReadRef<RcStr>,
790 #[turbo_tasks(debug_ignore)]
791 pub content: ReadRef<FileContent>,
792}
793
794#[turbo_tasks::value_impl]
795impl PlainSource {
796 #[turbo_tasks::function]
797 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
798 let asset_content = asset.content().await?;
799 let content = match *asset_content {
800 AssetContent::File(file_content) => file_content.await?,
801 AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
802 };
803
804 Ok(PlainSource {
805 ident: asset.ident().to_string().await?,
806 content,
807 }
808 .cell())
809 }
810}
811
812#[turbo_tasks::value_trait]
813pub trait IssueReporter {
814 #[turbo_tasks::function]
825 fn report_issues(
826 self: Vc<Self>,
827 source: TransientValue<RawVc>,
828 min_failing_severity: IssueSeverity,
829 ) -> Vc<bool>;
830}
831
832pub trait CollectibleIssuesExt
833where
834 Self: Sized,
835{
836 fn peek_issues(self) -> CapturedIssues;
840
841 fn drop_issues(self);
845}
846
847impl<T> CollectibleIssuesExt for T
848where
849 T: CollectiblesSource + Copy + Send,
850{
851 fn peek_issues(self) -> CapturedIssues {
852 CapturedIssues {
853 issues: self.peek_collectibles(),
854
855 tracer: DelegatingImportTracer {
856 delegates: self.peek_collectibles(),
857 }
858 .resolved_cell(),
859 }
860 }
861
862 fn drop_issues(self) {
863 self.drop_collectibles::<Box<dyn Issue>>();
864 }
865}
866
867pub async fn handle_issues<T: Send>(
871 source_op: OperationVc<T>,
872 issue_reporter: Vc<Box<dyn IssueReporter>>,
873 min_failing_severity: IssueSeverity,
874 path: Option<&str>,
875 operation: Option<&str>,
876) -> Result<()> {
877 let source_vc = source_op.connect();
878 let _ = source_op.resolve_strongly_consistent().await?;
879
880 let has_fatal = issue_reporter.report_issues(
881 TransientValue::new(Vc::into_raw(source_vc)),
882 min_failing_severity,
883 );
884
885 if *has_fatal.await? {
886 let mut message = "Fatal issue(s) occurred".to_owned();
887 if let Some(path) = path.as_ref() {
888 message += &format!(" in {path}");
889 };
890 if let Some(operation) = operation.as_ref() {
891 message += &format!(" ({operation})");
892 };
893
894 Err(anyhow!(message))
895 } else {
896 Ok(())
897 }
898}
899
900fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
901 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
902 Ok(i) => SourcePos {
903 line: i as u32,
904 column: 0,
905 },
906 Err(i) => {
907 if i == 0 {
908 SourcePos {
909 line: 0,
910 column: offset,
911 }
912 } else {
913 let line = &lines[i - 1];
914 SourcePos {
915 line: (i - 1) as u32,
916 column: min(line.content.len() as u32, offset - line.bytes_offset),
917 }
918 }
919 }
920 }
921}
922
923fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
924 let line = &lines[pos.line as usize];
925 line.bytes_offset + pos.column
926}