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_esregex::EsRegex;
16use turbo_rcstr::RcStr;
17use turbo_tasks::{
18 CollectiblesSource, IntoTraitRef, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc,
19 TaskInput, TransientValue, TryFlatJoinIterExt, TryJoinIterExt, Upcast, ValueDefault,
20 ValueToString, Vc, emit, trace::TraceRawVcs,
21};
22use turbo_tasks_fs::{
23 FileContent, FileLine, FileLinesContent, FileSystem, FileSystemPath, glob::Glob,
24};
25use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
26
27use crate::{
28 asset::{Asset, AssetContent},
29 condition::ContextCondition,
30 ident::{AssetIdent, Layer},
31 source::Source,
32 source_map::{GenerateSourceMap, SourceMap, TokenWithSource},
33 source_pos::SourcePos,
34};
35
36#[turbo_tasks::value(shared)]
37#[derive(
38 PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput, Serialize, Deserialize,
39)]
40#[serde(rename_all = "camelCase")]
41pub enum IssueSeverity {
42 Bug,
43 Fatal,
44 Error,
45 Warning,
46 Hint,
47 Note,
48 Suggestion,
49 Info,
50}
51
52impl IssueSeverity {
53 pub fn as_str(&self) -> &'static str {
54 match self {
55 IssueSeverity::Bug => "bug",
56 IssueSeverity::Fatal => "fatal",
57 IssueSeverity::Error => "error",
58 IssueSeverity::Warning => "warning",
59 IssueSeverity::Hint => "hint",
60 IssueSeverity::Note => "note",
61 IssueSeverity::Suggestion => "suggestion",
62 IssueSeverity::Info => "info",
63 }
64 }
65
66 pub fn as_help_str(&self) -> &'static str {
67 match self {
68 IssueSeverity::Bug => "bug in implementation",
69 IssueSeverity::Fatal => "unrecoverable problem",
70 IssueSeverity::Error => "problem that cause a broken result",
71 IssueSeverity::Warning => "problem should be addressed in short term",
72 IssueSeverity::Hint => "idea for improvement",
73 IssueSeverity::Note => "detail that is worth mentioning",
74 IssueSeverity::Suggestion => "change proposal for improvement",
75 IssueSeverity::Info => "detail that is worth telling",
76 }
77 }
78}
79
80impl Display for IssueSeverity {
81 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82 f.write_str(self.as_str())
83 }
84}
85
86#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
90#[turbo_tasks::value(shared)]
91pub enum StyledString {
92 Line(Vec<StyledString>),
96 Stack(Vec<StyledString>),
99 Text(RcStr),
101 Code(RcStr),
104 Strong(RcStr),
106}
107
108impl StyledString {
109 pub fn to_unstyled_string(&self) -> String {
110 match self {
111 StyledString::Line(items) => items
112 .iter()
113 .map(|item| item.to_unstyled_string())
114 .collect::<Vec<_>>()
115 .join(""),
116 StyledString::Stack(items) => items
117 .iter()
118 .map(|item| item.to_unstyled_string())
119 .collect::<Vec<_>>()
120 .join("\n"),
121 StyledString::Text(s) | StyledString::Code(s) | StyledString::Strong(s) => {
122 s.to_string()
123 }
124 }
125 }
126}
127
128#[turbo_tasks::value_trait]
129pub trait Issue {
130 fn severity(&self) -> IssueSeverity {
133 IssueSeverity::Error
134 }
135
136 #[turbo_tasks::function]
139 fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
140
141 #[turbo_tasks::function]
144 fn stage(self: Vc<Self>) -> Vc<IssueStage>;
145
146 #[turbo_tasks::function]
151 fn title(self: Vc<Self>) -> Vc<StyledString>;
152
153 #[turbo_tasks::function]
157 fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
158 Vc::cell(None)
159 }
160
161 #[turbo_tasks::function]
165 fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
166 Vc::cell(None)
167 }
168
169 #[turbo_tasks::function]
172 fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
173 Vc::<RcStr>::default()
174 }
175
176 #[turbo_tasks::function]
180 fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
181 Vc::cell(None)
182 }
183}
184
185#[turbo_tasks::value_trait]
187pub trait ImportTracer {
188 #[turbo_tasks::function]
189 fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
190}
191
192#[turbo_tasks::value]
193#[derive(Debug)]
194pub struct DelegatingImportTracer {
195 delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
196}
197
198impl DelegatingImportTracer {
199 async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
200 Ok(self
201 .delegates
202 .iter()
203 .map(|d| d.get_traces(path.clone()))
204 .try_join()
205 .await?
206 .iter()
207 .flat_map(|v| v.0.iter().cloned())
208 .collect())
209 }
210}
211
212pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
213
214#[turbo_tasks::value(shared)]
215pub struct ImportTraces(pub Vec<ImportTrace>);
216
217#[turbo_tasks::value_impl]
218impl ValueDefault for ImportTraces {
219 #[turbo_tasks::function]
220 fn value_default() -> Vc<Self> {
221 Self::cell(ImportTraces(vec![]))
222 }
223}
224
225pub trait IssueExt {
226 fn emit(self);
227}
228
229impl<T> IssueExt for ResolvedVc<T>
230where
231 T: Upcast<Box<dyn Issue>>,
232{
233 fn emit(self) {
234 emit(ResolvedVc::upcast_non_strict::<Box<dyn Issue>>(self));
235 }
236}
237
238#[turbo_tasks::value(transparent)]
239pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
240
241#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
243pub enum IgnoreIssuePattern {
244 ExactString(RcStr),
246 Glob(Glob),
248 Regex(EsRegex),
250}
251
252impl IgnoreIssuePattern {
253 pub fn matches(&self, value: &str) -> bool {
255 match self {
256 IgnoreIssuePattern::ExactString(s) => value == s.as_str(),
257 IgnoreIssuePattern::Glob(glob) => glob.matches(value),
258 IgnoreIssuePattern::Regex(regex) => regex.is_match(value),
259 }
260 }
261}
262
263#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
266pub struct IgnoreIssue {
267 pub path: IgnoreIssuePattern,
269 pub title: Option<IgnoreIssuePattern>,
271 pub description: Option<IgnoreIssuePattern>,
273}
274
275#[turbo_tasks::value(shared)]
276pub struct IssueFilter {
277 severity: IssueSeverity,
279 foreign_severity: IssueSeverity,
281 ignore_rules: Vec<IgnoreIssue>,
283}
284
285#[turbo_tasks::value_impl]
286impl IssueFilter {
287 #[turbo_tasks::function]
289 pub fn everything() -> Vc<Self> {
290 IssueFilter {
291 severity: IssueSeverity::Info,
292 foreign_severity: IssueSeverity::Info,
293 ignore_rules: Vec::new(),
294 }
295 .cell()
296 }
297
298 #[turbo_tasks::function]
300 pub async fn matches(&self, issue: ResolvedVc<Box<dyn Issue>>) -> Result<Vc<bool>> {
301 let has_no_ignore_rules = self.ignore_rules.is_empty();
302 let is_everything = self.severity == IssueSeverity::Info
303 && self.foreign_severity == IssueSeverity::Info
304 && has_no_ignore_rules;
305
306 if is_everything {
307 return Ok(Vc::cell(true));
308 }
309
310 let file_path = issue.file_path().await?;
313
314 let severity = issue.into_trait_ref().await?.severity();
317 let severity_allowed = if severity <= self.severity || severity <= self.foreign_severity {
319 if severity <= self.severity && severity <= self.foreign_severity {
322 true
324 } else if ContextCondition::InNodeModules.matches(&file_path) {
325 severity <= self.foreign_severity
326 } else {
327 severity <= self.severity
328 }
329 } else {
330 false
332 };
333
334 if !severity_allowed {
335 return Ok(Vc::cell(false));
336 }
337
338 if !has_no_ignore_rules {
342 let file_path_str = file_path.to_string();
343 let mut title_str: Option<String> = None;
344 let mut description_text: Option<Option<String>> = None;
345
346 for rule in &self.ignore_rules {
347 if !rule.path.matches(&file_path_str) {
348 continue;
349 }
350 if let Some(ref title_pat) = rule.title {
351 if title_str.is_none() {
352 title_str = Some(issue.title().await?.to_unstyled_string());
353 }
354 if !title_pat.matches(title_str.as_deref().unwrap()) {
355 continue;
356 }
357 }
358 if let Some(ref desc_pat) = rule.description {
359 if description_text.is_none() {
360 let desc_opt = issue.description().await?;
361 description_text = Some(match desc_opt.as_ref() {
362 Some(desc_vc) => Some(desc_vc.await?.to_unstyled_string()),
363 None => None,
364 });
365 }
366 match description_text.as_ref().unwrap().as_deref() {
367 Some(desc) if desc_pat.matches(desc) => {}
368 _ => continue,
369 }
370 }
371 return Ok(Vc::cell(false));
373 }
374 }
375
376 Ok(Vc::cell(true))
377 }
378}
379
380impl IssueFilter {
381 pub fn warnings_and_foreign_errors() -> Self {
383 IssueFilter {
384 severity: IssueSeverity::Warning,
385 foreign_severity: IssueSeverity::Error,
386 ignore_rules: Vec::new(),
387 }
388 }
389
390 pub fn with_ignore_rules(mut self, rules: Vec<IgnoreIssue>) -> Self {
392 self.ignore_rules = rules;
393 self
394 }
395}
396
397#[turbo_tasks::value(shared)]
400#[derive(Debug)]
401pub struct CapturedIssues {
402 issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
403 tracer: ResolvedVc<DelegatingImportTracer>,
404}
405
406#[turbo_tasks::value_impl]
407impl CapturedIssues {
408 #[turbo_tasks::function]
409 pub fn is_empty(&self) -> Vc<bool> {
410 Vc::cell(self.is_empty_ref())
411 }
412}
413
414impl CapturedIssues {
415 pub fn is_empty_ref(&self) -> bool {
417 self.issues.is_empty()
418 }
419
420 #[allow(clippy::len_without_is_empty)]
422 pub fn len(&self) -> usize {
423 self.issues.len()
424 }
425
426 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
428 self.issues.iter().copied()
429 }
430
431 pub async fn get_plain_issues(
433 &self,
434 filter: Vc<IssueFilter>,
435 ) -> Result<Vec<ReadRef<PlainIssue>>> {
436 let mut list = self
437 .issues
438 .iter()
439 .map(async |issue| {
440 if *filter.matches(**issue).await? {
441 Ok(Some(
442 PlainIssue::from_issue(**issue, Some(*self.tracer)).await?,
443 ))
444 } else {
445 Ok(None)
446 }
447 })
448 .try_flat_join()
449 .await?;
450 list.sort();
451 Ok(list)
452 }
453}
454
455#[derive(
456 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
457)]
458pub struct IssueSource {
459 source: ResolvedVc<Box<dyn Source>>,
460 range: Option<SourceRange>,
461}
462
463#[derive(
465 Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
466)]
467enum SourceRange {
468 LineColumn(SourcePos, SourcePos),
469 ByteOffset(u32, u32),
470}
471
472impl IssueSource {
473 pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
476 IssueSource {
477 source,
478 range: None,
479 }
480 }
481
482 pub fn from_line_col(
483 source: ResolvedVc<Box<dyn Source>>,
484 start: SourcePos,
485 end: SourcePos,
486 ) -> Self {
487 IssueSource {
488 source,
489 range: Some(SourceRange::LineColumn(start, end)),
490 }
491 }
492
493 async fn into_plain(self) -> Result<PlainIssueSource> {
494 let Self { mut source, range } = self;
495
496 let range = if let Some(range) = range {
497 let mut range = match range {
498 SourceRange::LineColumn(start, end) => Some((start, end)),
499 SourceRange::ByteOffset(start, end) => {
500 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
501 let start = find_line_and_column(lines.as_ref(), start);
502 let end = find_line_and_column(lines.as_ref(), end);
503 Some((start, end))
504 } else {
505 None
506 }
507 }
508 };
509
510 if let Some((start, end)) = range {
512 let mapped = source_pos(source, start, end).await?;
513
514 if let Some((mapped_source, start, end)) = mapped {
515 range = Some((start, end));
516 source = mapped_source;
517 }
518 }
519 range
520 } else {
521 None
522 };
523 Ok(PlainIssueSource {
524 asset: PlainSource::from_source(*source).await?,
525 range,
526 })
527 }
528
529 pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
538 IssueSource {
539 source,
540 range: match (start == 0, end == 0) {
541 (true, true) => None,
542 (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
543 (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
544 (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
545 },
546 }
547 }
548
549 pub async fn from_byte_offset(
559 source: ResolvedVc<Box<dyn Source>>,
560 start: u32,
561 end: u32,
562 ) -> Result<Self> {
563 Ok(IssueSource {
564 source,
565 range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
566 let start = find_line_and_column(lines.as_ref(), start);
567 let end = find_line_and_column(lines.as_ref(), end);
568 Some(SourceRange::LineColumn(start, end))
569 } else {
570 None
571 },
572 })
573 }
574
575 pub fn file_path(&self) -> Vc<FileSystemPath> {
577 self.source.ident().path()
578 }
579}
580
581impl IssueSource {
582 pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
584 Ok(match &self.range {
585 Some(range) => match range {
586 SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
587 SourceRange::LineColumn(start, end) => {
588 if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
589 let start = find_offset(lines.as_ref(), *start) + 1;
590 let end = find_offset(lines.as_ref(), *end) + 1;
591 Some((start, end))
592 } else {
593 None
594 }
595 }
596 },
597 _ => None,
598 })
599 }
600}
601
602async fn source_pos(
603 source: ResolvedVc<Box<dyn Source>>,
604 start: SourcePos,
605 end: SourcePos,
606) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
607 let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
608 return Ok(None);
609 };
610
611 let srcmap = generator.generate_source_map();
612 let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
613 return Ok(None);
614 };
615
616 let find = async |line: u32, col: u32| {
617 let TokenWithSource {
618 token,
619 source_content,
620 } = &srcmap.lookup_token_and_source(line, col).await?;
621
622 match token {
623 crate::source_map::Token::Synthetic(t) => anyhow::Ok((
624 SourcePos {
625 line: t.generated_line as _,
626 column: t.generated_column as _,
627 },
628 *source_content,
629 )),
630 crate::source_map::Token::Original(t) => anyhow::Ok((
631 SourcePos {
632 line: t.original_line as _,
633 column: t.original_column as _,
634 },
635 *source_content,
636 )),
637 }
638 };
639
640 let (start, content_1) = find(start.line, start.column).await?;
641 let (end, content_2) = find(end.line, end.column).await?;
642
643 let Some((content_1, content_2)) = content_1.zip(content_2) else {
644 return Ok(None);
645 };
646
647 if content_1 != content_2 {
648 return Ok(None);
649 }
650
651 Ok(Some((content_1, start, end)))
652}
653
654#[turbo_tasks::value(transparent)]
655pub struct OptionIssueSource(Option<IssueSource>);
656
657#[turbo_tasks::value(transparent)]
658pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
659
660#[derive(
662 Serialize,
663 PartialEq,
664 Eq,
665 PartialOrd,
666 Ord,
667 Clone,
668 Debug,
669 TraceRawVcs,
670 NonLocalValue,
671 DeterministicHash,
672)]
673#[serde(rename_all = "camelCase")]
674pub struct PlainTraceItem {
675 pub fs_name: RcStr,
677 pub root_path: RcStr,
679 pub path: RcStr,
681 pub layer: Option<RcStr>,
683}
684
685impl PlainTraceItem {
686 async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
687 let fs_path = asset.path.clone();
690 let fs_name = fs_path.fs.to_string().owned().await?;
691 let root_path = fs_path.fs.root().await?.path.clone();
692 let path = fs_path.path.clone();
693 let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
694 Ok(Self {
695 fs_name,
696 root_path,
697 path,
698 layer,
699 })
700 }
701}
702
703pub type PlainTrace = Vec<PlainTraceItem>;
704
705async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
707 let mut plain_traces = traces
708 .into_iter()
709 .map(|trace| async move {
710 let mut plain_trace = trace
711 .into_iter()
712 .filter(|asset| {
713 asset.assets.is_empty()
716 })
717 .map(PlainTraceItem::from_asset_ident)
718 .try_join()
719 .await?;
720
721 plain_trace.dedup();
738
739 Ok(plain_trace)
740 })
741 .try_join()
742 .await?;
743
744 plain_traces.retain(|t| t.len() > 1);
747 plain_traces.sort_by(|a, b| {
750 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
752 });
753
754 if plain_traces.len() > 1 {
762 let mut i = 0;
763 while i < plain_traces.len() - 1 {
764 let mut j = plain_traces.len() - 1;
765 while j > i {
766 if plain_traces[j].ends_with(&plain_traces[i]) {
767 plain_traces.remove(j);
773 }
774 j -= 1;
775 }
776 i += 1;
777 }
778 }
779
780 Ok(plain_traces)
781}
782
783#[turbo_tasks::value(shared)]
784#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
785pub enum IssueStage {
786 Config,
787 AppStructure,
788 ProcessModule,
789 Load,
791 SourceTransform,
792 Parse,
793 Transform,
795 Analysis,
796 Resolve,
797 Bindings,
798 CodeGen,
799 Unsupported,
800 Misc,
801 Other(RcStr),
802}
803
804impl Display for IssueStage {
805 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
806 match self {
807 IssueStage::Config => write!(f, "config"),
808 IssueStage::Resolve => write!(f, "resolve"),
809 IssueStage::ProcessModule => write!(f, "process module"),
810 IssueStage::Load => write!(f, "load"),
811 IssueStage::SourceTransform => write!(f, "source transform"),
812 IssueStage::Parse => write!(f, "parse"),
813 IssueStage::Transform => write!(f, "transform"),
814 IssueStage::Analysis => write!(f, "analysis"),
815 IssueStage::Bindings => write!(f, "bindings"),
816 IssueStage::CodeGen => write!(f, "code gen"),
817 IssueStage::Unsupported => write!(f, "unsupported"),
818 IssueStage::AppStructure => write!(f, "app structure"),
819 IssueStage::Misc => write!(f, "misc"),
820 IssueStage::Other(s) => write!(f, "{s}"),
821 }
822 }
823}
824
825#[turbo_tasks::value(serialization = "none")]
826#[derive(Clone, Debug, PartialOrd, Ord)]
827pub struct PlainIssue {
828 pub severity: IssueSeverity,
829 pub stage: IssueStage,
830
831 pub title: StyledString,
832 pub file_path: RcStr,
833
834 pub description: Option<StyledString>,
835 pub detail: Option<StyledString>,
836 pub documentation_link: RcStr,
837
838 pub source: Option<PlainIssueSource>,
839 pub import_traces: Vec<PlainTrace>,
840}
841
842fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
843 hasher.write_ref(&issue.severity);
844 hasher.write_ref(&issue.file_path);
845 hasher.write_ref(&issue.stage);
846 hasher.write_ref(&issue.title);
847 hasher.write_ref(&issue.description);
848 hasher.write_ref(&issue.detail);
849 hasher.write_ref(&issue.documentation_link);
850
851 if let Some(source) = &issue.source {
852 hasher.write_value(1_u8);
853 hasher.write_ref(&source.range);
856 } else {
857 hasher.write_value(0_u8);
858 }
859
860 if full {
861 hasher.write_ref(&issue.import_traces);
862 }
863}
864
865impl PlainIssue {
866 pub fn internal_hash_ref(&self, full: bool) -> u64 {
875 let mut hasher = Xxh3Hash64Hasher::new();
876 hash_plain_issue(self, &mut hasher, full);
877 hasher.finish()
878 }
879}
880
881#[turbo_tasks::value_impl]
882impl PlainIssue {
883 #[turbo_tasks::function]
886 pub async fn from_issue(
887 issue: ResolvedVc<Box<dyn Issue>>,
888 import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
889 ) -> Result<Vc<Self>> {
890 let description: Option<StyledString> = match *issue.description().await? {
891 Some(description) => Some(description.owned().await?),
892 None => None,
893 };
894 let detail = match *issue.detail().await? {
895 Some(detail) => Some(detail.owned().await?),
896 None => None,
897 };
898 let trait_ref = issue.into_trait_ref().await?;
899
900 let severity = trait_ref.severity();
901
902 Ok(Self::cell(Self {
903 severity,
904 file_path: issue.file_path().to_string().owned().await?,
905 stage: issue.stage().owned().await?,
906 title: issue.title().owned().await?,
907 description,
908 detail,
909 documentation_link: issue.documentation_link().owned().await?,
910 source: {
911 if let Some(s) = &*issue.source().await? {
912 Some(s.into_plain().await?)
913 } else {
914 None
915 }
916 },
917 import_traces: match import_tracer {
918 Some(tracer) => {
919 into_plain_trace(
920 tracer
921 .await?
922 .get_traces(issue.file_path().owned().await?)
923 .await?,
924 )
925 .await?
926 }
927 None => vec![],
928 },
929 }))
930 }
931}
932
933#[turbo_tasks::value(serialization = "none")]
934#[derive(Clone, Debug, PartialOrd, Ord)]
935pub struct PlainIssueSource {
936 pub asset: ReadRef<PlainSource>,
937 pub range: Option<(SourcePos, SourcePos)>,
938}
939
940#[turbo_tasks::value(serialization = "none")]
941#[derive(Clone, Debug, PartialOrd, Ord)]
942pub struct PlainSource {
943 pub ident: ReadRef<RcStr>,
944 #[turbo_tasks(debug_ignore)]
945 pub content: ReadRef<FileContent>,
946}
947
948#[turbo_tasks::value_impl]
949impl PlainSource {
950 #[turbo_tasks::function]
951 pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
952 let asset_content = asset.content().await?;
953 let content = match *asset_content {
954 AssetContent::File(file_content) => file_content.await?,
955 AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
956 };
957
958 Ok(PlainSource {
959 ident: asset.ident().to_string().await?,
960 content,
961 }
962 .cell())
963 }
964}
965
966#[turbo_tasks::value_trait]
967pub trait IssueReporter {
968 #[turbo_tasks::function]
979 fn report_issues(
980 self: Vc<Self>,
981 source: TransientValue<RawVc>,
982 min_failing_severity: IssueSeverity,
983 ) -> Vc<bool>;
984}
985
986pub trait CollectibleIssuesExt
987where
988 Self: Sized,
989{
990 fn peek_issues(self) -> CapturedIssues;
994
995 fn drop_issues(self);
999}
1000
1001impl<T> CollectibleIssuesExt for T
1002where
1003 T: CollectiblesSource + Copy + Send,
1004{
1005 fn peek_issues(self) -> CapturedIssues {
1006 CapturedIssues {
1007 issues: self.peek_collectibles(),
1008
1009 tracer: DelegatingImportTracer {
1010 delegates: self.peek_collectibles(),
1011 }
1012 .resolved_cell(),
1013 }
1014 }
1015
1016 fn drop_issues(self) {
1017 self.drop_collectibles::<Box<dyn Issue>>();
1018 }
1019}
1020
1021pub async fn handle_issues<T: Send>(
1025 source_op: OperationVc<T>,
1026 issue_reporter: Vc<Box<dyn IssueReporter>>,
1027 min_failing_severity: IssueSeverity,
1028 path: Option<&str>,
1029 operation: Option<&str>,
1030) -> Result<()> {
1031 let source_vc = source_op.connect();
1032 let _ = source_op.resolve_strongly_consistent().await?;
1033
1034 let has_fatal = issue_reporter.report_issues(
1035 TransientValue::new(Vc::into_raw(source_vc)),
1036 min_failing_severity,
1037 );
1038
1039 if *has_fatal.await? {
1040 let mut message = "Fatal issue(s) occurred".to_owned();
1041 if let Some(path) = path.as_ref() {
1042 message += &format!(" in {path}");
1043 };
1044 if let Some(operation) = operation.as_ref() {
1045 message += &format!(" ({operation})");
1046 };
1047
1048 Err(anyhow!(message))
1049 } else {
1050 Ok(())
1051 }
1052}
1053
1054fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1055 match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1056 Ok(i) => SourcePos {
1057 line: i as u32,
1058 column: 0,
1059 },
1060 Err(i) => {
1061 if i == 0 {
1062 SourcePos {
1063 line: 0,
1064 column: offset,
1065 }
1066 } else {
1067 let line = &lines[i - 1];
1068 SourcePos {
1069 line: (i - 1) as u32,
1070 column: min(line.content.len() as u32, offset - line.bytes_offset),
1071 }
1072 }
1073 }
1074 }
1075}
1076
1077fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1078 let line = &lines[pos.line as usize];
1079 line.bytes_offset + pos.column
1080}