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