turbopack_core/issue/
mod.rs

1pub mod analyze;
2pub mod code_gen;
3pub mod module;
4pub mod resolve;
5
6use std::{
7    cmp::{Ordering, min},
8    fmt::{Display, Formatter},
9};
10
11use anyhow::{Result, anyhow};
12use async_trait::async_trait;
13use auto_hash_map::AutoSet;
14use serde::{Deserialize, Serialize};
15use turbo_rcstr::RcStr;
16use turbo_tasks::{
17    CollectiblesSource, IntoTraitRef, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc,
18    TaskInput, TransientInstance, TransientValue, TryJoinIterExt, Upcast, ValueDefault,
19    ValueToString, Vc, emit, 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(PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput)]
34#[serde(rename_all = "camelCase")]
35pub enum IssueSeverity {
36    Bug,
37    Fatal,
38    Error,
39    Warning,
40    Hint,
41    Note,
42    Suggestion,
43    Info,
44}
45
46impl IssueSeverity {
47    pub fn as_str(&self) -> &'static str {
48        match self {
49            IssueSeverity::Bug => "bug",
50            IssueSeverity::Fatal => "fatal",
51            IssueSeverity::Error => "error",
52            IssueSeverity::Warning => "warning",
53            IssueSeverity::Hint => "hint",
54            IssueSeverity::Note => "note",
55            IssueSeverity::Suggestion => "suggestion",
56            IssueSeverity::Info => "info",
57        }
58    }
59
60    pub fn as_help_str(&self) -> &'static str {
61        match self {
62            IssueSeverity::Bug => "bug in implementation",
63            IssueSeverity::Fatal => "unrecoverable problem",
64            IssueSeverity::Error => "problem that cause a broken result",
65            IssueSeverity::Warning => "problem should be addressed in short term",
66            IssueSeverity::Hint => "idea for improvement",
67            IssueSeverity::Note => "detail that is worth mentioning",
68            IssueSeverity::Suggestion => "change proposal for improvement",
69            IssueSeverity::Info => "detail that is worth telling",
70        }
71    }
72}
73
74impl Display for IssueSeverity {
75    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
76        f.write_str(self.as_str())
77    }
78}
79
80/// Represents a section of structured styled text. This can be interpreted and
81/// rendered by various UIs as appropriate, e.g. HTML for display on the web,
82/// ANSI sequences in TTYs.
83#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash)]
84#[turbo_tasks::value(shared)]
85pub enum StyledString {
86    /// Multiple [StyledString]s concatenated into a single line. Each item is
87    /// considered as inline element. Items might contain line breaks, which
88    /// would be considered as soft line breaks.
89    Line(Vec<StyledString>),
90    /// Multiple [StyledString]s stacked vertically. They are considered as
91    /// block elements, just like the top level [StyledString].
92    Stack(Vec<StyledString>),
93    /// Some prose text.
94    Text(RcStr),
95    /// Code snippet.
96    // TODO add language to support syntax highlighting
97    Code(RcStr),
98    /// Some important text.
99    Strong(RcStr),
100}
101
102#[turbo_tasks::value_trait]
103pub trait Issue {
104    /// Severity allows the user to filter out unimportant issues, with Bug
105    /// being the highest priority and Info being the lowest.
106    fn severity(&self) -> IssueSeverity {
107        IssueSeverity::Error
108    }
109
110    /// The file path that generated the issue, displayed to the user as message
111    /// header.
112    #[turbo_tasks::function]
113    fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
114
115    /// The stage of the compilation process at which the issue occurred. This
116    /// is used to sort issues.
117    #[turbo_tasks::function]
118    fn stage(self: Vc<Self>) -> Vc<IssueStage>;
119
120    /// The issue title should be descriptive of the issue, but should be a
121    /// single line. This is displayed to the user directly under the issue
122    /// header.
123    // TODO add Vc<StyledString>
124    #[turbo_tasks::function]
125    fn title(self: Vc<Self>) -> Vc<StyledString>;
126
127    /// A more verbose message of the issue, appropriate for providing multiline
128    /// information of the issue.
129    // TODO add Vc<StyledString>
130    #[turbo_tasks::function]
131    fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
132        Vc::cell(None)
133    }
134
135    /// Full details of the issue, appropriate for providing debug level
136    /// information. Only displayed if the user explicitly asks for detailed
137    /// messages (not to be confused with severity).
138    #[turbo_tasks::function]
139    fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
140        Vc::cell(None)
141    }
142
143    /// A link to relevant documentation of the issue. Only displayed in console
144    /// if the user explicitly asks for detailed messages.
145    #[turbo_tasks::function]
146    fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
147        Vc::<RcStr>::default()
148    }
149
150    /// The source location that caused the issue. Eg, for a parsing error it
151    /// should point at the offending character. Displayed to the user alongside
152    /// the title/description.
153    #[turbo_tasks::function]
154    fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
155        Vc::cell(None)
156    }
157}
158
159// A collectible trait that allows traces to be computed for a given module.
160#[turbo_tasks::value_trait]
161pub trait ImportTracer {
162    #[turbo_tasks::function]
163    fn get_traces(self: Vc<Self>, path: FileSystemPath) -> Vc<ImportTraces>;
164}
165
166#[turbo_tasks::value(shared)]
167#[derive(Debug)]
168pub struct DelegatingImportTracer {
169    delegates: AutoSet<ResolvedVc<Box<dyn ImportTracer>>>,
170}
171
172impl DelegatingImportTracer {
173    async fn get_traces(&self, path: FileSystemPath) -> Result<Vec<ImportTrace>> {
174        Ok(self
175            .delegates
176            .iter()
177            .map(|d| d.get_traces(path.clone()))
178            .try_join()
179            .await?
180            .iter()
181            .flat_map(|v| v.0.iter().cloned())
182            .collect())
183    }
184}
185
186pub type ImportTrace = Vec<ReadRef<AssetIdent>>;
187
188#[turbo_tasks::value(shared)]
189pub struct ImportTraces(pub Vec<ImportTrace>);
190
191#[turbo_tasks::value_impl]
192impl ValueDefault for ImportTraces {
193    #[turbo_tasks::function]
194    fn value_default() -> Vc<Self> {
195        Self::cell(ImportTraces(vec![]))
196    }
197}
198
199#[turbo_tasks::value_trait]
200trait IssueProcessingPath {
201    #[turbo_tasks::function]
202    fn shortest_path(
203        self: Vc<Self>,
204        issue: Vc<Box<dyn Issue>>,
205    ) -> Vc<OptionIssueProcessingPathItems>;
206}
207
208#[turbo_tasks::value]
209pub struct IssueProcessingPathItem {
210    pub file_path: Option<FileSystemPath>,
211    pub description: ResolvedVc<RcStr>,
212}
213
214#[turbo_tasks::value_impl]
215impl ValueToString for IssueProcessingPathItem {
216    #[turbo_tasks::function]
217    async fn to_string(&self) -> Result<Vc<RcStr>> {
218        if let Some(context) = &self.file_path {
219            let description_str = self.description.await?;
220            Ok(Vc::cell(
221                format!("{} ({})", context.value_to_string().await?, description_str).into(),
222            ))
223        } else {
224            Ok(*self.description)
225        }
226    }
227}
228
229#[turbo_tasks::value_impl]
230impl IssueProcessingPathItem {
231    #[turbo_tasks::function]
232    pub async fn into_plain(&self) -> Result<Vc<PlainIssueProcessingPathItem>> {
233        Ok(PlainIssueProcessingPathItem {
234            file_path: if let Some(context) = &self.file_path {
235                Some(context.value_to_string().await?)
236            } else {
237                None
238            },
239            description: self.description.await?,
240        }
241        .cell())
242    }
243}
244
245#[turbo_tasks::value(transparent)]
246pub struct OptionIssueProcessingPathItems(Option<Vec<ResolvedVc<IssueProcessingPathItem>>>);
247
248#[turbo_tasks::value_impl]
249impl OptionIssueProcessingPathItems {
250    #[turbo_tasks::function]
251    pub fn none() -> Vc<Self> {
252        Vc::cell(None)
253    }
254
255    #[turbo_tasks::function]
256    pub async fn into_plain(self: Vc<Self>) -> Result<Vc<PlainIssueProcessingPath>> {
257        Ok(Vc::cell(if let Some(items) = &*self.await? {
258            Some(
259                items
260                    .iter()
261                    .map(|item| item.into_plain())
262                    .try_join()
263                    .await?,
264            )
265        } else {
266            None
267        }))
268    }
269}
270
271#[turbo_tasks::value]
272struct RootIssueProcessingPath(ResolvedVc<Box<dyn Issue>>);
273
274#[turbo_tasks::value_impl]
275impl IssueProcessingPath for RootIssueProcessingPath {
276    #[turbo_tasks::function]
277    fn shortest_path(
278        &self,
279        issue: ResolvedVc<Box<dyn Issue>>,
280    ) -> Vc<OptionIssueProcessingPathItems> {
281        if self.0 == issue {
282            Vc::cell(Some(Vec::new()))
283        } else {
284            Vc::cell(None)
285        }
286    }
287}
288
289#[turbo_tasks::value]
290struct ItemIssueProcessingPath(
291    Option<ResolvedVc<IssueProcessingPathItem>>,
292    AutoSet<ResolvedVc<Box<dyn IssueProcessingPath>>>,
293);
294
295#[turbo_tasks::value_impl]
296impl IssueProcessingPath for ItemIssueProcessingPath {
297    /// Returns the shortest path from the root issue to the given issue.
298    #[turbo_tasks::function]
299    async fn shortest_path(
300        &self,
301        issue: Vc<Box<dyn Issue>>,
302    ) -> Result<Vc<OptionIssueProcessingPathItems>> {
303        assert!(!self.1.is_empty());
304        let paths = self
305            .1
306            .iter()
307            .map(|child| child.shortest_path(issue))
308            .collect::<Vec<_>>();
309        let paths = paths.iter().try_join().await?;
310        let mut shortest: Option<&Vec<_>> = None;
311        for path in paths.iter().filter_map(|p| p.as_ref()) {
312            if let Some(old) = shortest {
313                match old.len().cmp(&path.len()) {
314                    Ordering::Greater => {
315                        shortest = Some(path);
316                    }
317                    Ordering::Equal => {
318                        let (mut a, mut b) = (old.iter(), path.iter());
319                        while let (Some(a), Some(b)) = (a.next(), b.next()) {
320                            let (a, b) = (a.to_string().await?, b.to_string().await?);
321                            match RcStr::cmp(&*a, &*b) {
322                                Ordering::Less => break,
323                                Ordering::Greater => {
324                                    shortest = Some(path);
325                                    break;
326                                }
327                                Ordering::Equal => {}
328                            }
329                        }
330                    }
331                    Ordering::Less => {}
332                }
333            } else {
334                shortest = Some(path);
335            }
336        }
337        Ok(Vc::cell(shortest.map(|path| {
338            if let Some(item) = self.0 {
339                std::iter::once(item).chain(path.iter().copied()).collect()
340            } else {
341                path.clone()
342            }
343        })))
344    }
345}
346
347pub trait IssueExt {
348    fn emit(self);
349}
350
351impl<T> IssueExt for ResolvedVc<T>
352where
353    T: Upcast<Box<dyn Issue>>,
354{
355    fn emit(self) {
356        let issue = ResolvedVc::upcast::<Box<dyn Issue>>(self);
357        emit(issue);
358        emit(ResolvedVc::upcast::<Box<dyn IssueProcessingPath>>(
359            RootIssueProcessingPath::resolved_cell(RootIssueProcessingPath(issue)),
360        ))
361    }
362}
363
364#[turbo_tasks::value(transparent)]
365pub struct Issues(Vec<ResolvedVc<Box<dyn Issue>>>);
366
367/// A list of issues captured with [`Issue::peek_issues_with_path`] and
368/// [`Issue::take_issues_with_path`].
369#[turbo_tasks::value(shared)]
370#[derive(Debug)]
371pub struct CapturedIssues {
372    issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
373    #[cfg(feature = "issue_path")]
374    processing_path: ResolvedVc<ItemIssueProcessingPath>,
375    tracer: ResolvedVc<DelegatingImportTracer>,
376}
377
378#[turbo_tasks::value_impl]
379impl CapturedIssues {
380    #[turbo_tasks::function]
381    pub fn is_empty(&self) -> Vc<bool> {
382        Vc::cell(self.is_empty_ref())
383    }
384}
385
386impl CapturedIssues {
387    /// Returns true if there are no issues.
388    pub fn is_empty_ref(&self) -> bool {
389        self.issues.is_empty()
390    }
391
392    /// Returns the number of issues.
393    #[allow(clippy::len_without_is_empty)]
394    pub fn len(&self) -> usize {
395        self.issues.len()
396    }
397
398    /// Returns an iterator over the issues.
399    pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
400        self.issues.iter().copied()
401    }
402
403    // Returns all the issues as formatted `PlainIssues`.
404    pub async fn get_plain_issues(&self) -> Result<Vec<ReadRef<PlainIssue>>> {
405        let mut list = self
406            .issues
407            .iter()
408            .map(|issue| async move {
409                #[cfg(feature = "issue_path")]
410                let processing_path = self.processing_path.shortest_path(**issue);
411                #[cfg(not(feature = "issue_path"))]
412                let processing_path = OptionIssueProcessingPathItems::none();
413                PlainIssue::from_issue(**issue, Some(*self.tracer), processing_path).await
414            })
415            .try_join()
416            .await?;
417        list.sort();
418        Ok(list)
419    }
420}
421
422#[derive(
423    Clone,
424    Copy,
425    Debug,
426    PartialEq,
427    Eq,
428    Serialize,
429    Deserialize,
430    Hash,
431    TaskInput,
432    TraceRawVcs,
433    NonLocalValue,
434)]
435pub struct IssueSource {
436    source: ResolvedVc<Box<dyn Source>>,
437    range: Option<SourceRange>,
438}
439
440/// The end position is the first character after the range
441#[derive(
442    Clone,
443    Copy,
444    Debug,
445    PartialEq,
446    Eq,
447    Serialize,
448    Deserialize,
449    Hash,
450    TaskInput,
451    TraceRawVcs,
452    NonLocalValue,
453)]
454enum SourceRange {
455    LineColumn(SourcePos, SourcePos),
456    ByteOffset(u32, u32),
457}
458
459impl IssueSource {
460    // Sometimes we only have the source file that causes an issue, not the
461    // exact location, such as as in some generated code.
462    pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
463        IssueSource {
464            source,
465            range: None,
466        }
467    }
468
469    pub fn from_line_col(
470        source: ResolvedVc<Box<dyn Source>>,
471        start: SourcePos,
472        end: SourcePos,
473    ) -> Self {
474        IssueSource {
475            source,
476            range: Some(SourceRange::LineColumn(start, end)),
477        }
478    }
479
480    async fn into_plain(self) -> Result<PlainIssueSource> {
481        let Self { mut source, range } = self;
482
483        let range = if let Some(range) = range {
484            let mut range = match range {
485                SourceRange::LineColumn(start, end) => Some((start, end)),
486                SourceRange::ByteOffset(start, end) => {
487                    if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
488                        let start = find_line_and_column(lines.as_ref(), start);
489                        let end = find_line_and_column(lines.as_ref(), end);
490                        Some((start, end))
491                    } else {
492                        None
493                    }
494                }
495            };
496
497            // If we have a source map, map the line/column to the original source.
498            if let Some((start, end)) = range {
499                let mapped = source_pos(source, start, end).await?;
500
501                if let Some((mapped_source, start, end)) = mapped {
502                    range = Some((start, end));
503                    source = mapped_source;
504                }
505            }
506            range
507        } else {
508            None
509        };
510        Ok(PlainIssueSource {
511            asset: PlainSource::from_source(*source).await?,
512            range,
513        })
514    }
515
516    /// Create a [`IssueSource`] from byte offsets given by an swc ast node
517    /// span.
518    ///
519    /// Arguments:
520    ///
521    /// * `source`: The source code in which to look up the byte offsets.
522    /// * `start`: The start index of the span. Must use **1-based** indexing.
523    /// * `end`: The end index of the span. Must use **1-based** indexing.
524    pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
525        IssueSource {
526            source,
527            range: match (start == 0, end == 0) {
528                (true, true) => None,
529                (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
530                (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
531                (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
532            },
533        }
534    }
535
536    /// Returns an `IssueSource` representing a span of code in the `source`.
537    /// Positions are derived from byte offsets and stored as lines and columns.
538    /// Requires a binary search of the source text to perform this.
539    ///
540    /// Arguments:
541    ///
542    /// * `source`: The source code in which to look up the byte offsets.
543    /// * `start`: Byte offset into the source that the text begins. 0-based index and inclusive.
544    /// * `end`: Byte offset into the source that the text ends. 0-based index and exclusive.
545    pub async fn from_byte_offset(
546        source: ResolvedVc<Box<dyn Source>>,
547        start: u32,
548        end: u32,
549    ) -> Result<Self> {
550        Ok(IssueSource {
551            source,
552            range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
553                let start = find_line_and_column(lines.as_ref(), start);
554                let end = find_line_and_column(lines.as_ref(), end);
555                Some(SourceRange::LineColumn(start, end))
556            } else {
557                None
558            },
559        })
560    }
561
562    /// Returns the file path for the source file.
563    pub fn file_path(&self) -> Vc<FileSystemPath> {
564        self.source.ident().path()
565    }
566}
567
568impl IssueSource {
569    /// Returns bytes offsets corresponding the source range in the format used by swc's Spans.
570    pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
571        Ok(match &self.range {
572            Some(range) => match range {
573                SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
574                SourceRange::LineColumn(start, end) => {
575                    if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
576                        let start = find_offset(lines.as_ref(), *start) + 1;
577                        let end = find_offset(lines.as_ref(), *end) + 1;
578                        Some((start, end))
579                    } else {
580                        None
581                    }
582                }
583            },
584            _ => None,
585        })
586    }
587}
588
589async fn source_pos(
590    source: ResolvedVc<Box<dyn Source>>,
591    start: SourcePos,
592    end: SourcePos,
593) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
594    let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
595        return Ok(None);
596    };
597
598    let srcmap = generator.generate_source_map();
599    let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
600        return Ok(None);
601    };
602
603    let find = async |line: u32, col: u32| {
604        let TokenWithSource {
605            token,
606            source_content,
607        } = &srcmap.lookup_token_and_source(line, col).await?;
608
609        match token {
610            crate::source_map::Token::Synthetic(t) => anyhow::Ok((
611                SourcePos {
612                    line: t.generated_line as _,
613                    column: t.generated_column as _,
614                },
615                *source_content,
616            )),
617            crate::source_map::Token::Original(t) => anyhow::Ok((
618                SourcePos {
619                    line: t.original_line as _,
620                    column: t.original_column as _,
621                },
622                *source_content,
623            )),
624        }
625    };
626
627    let (start, content_1) = find(start.line, start.column).await?;
628    let (end, content_2) = find(end.line, end.column).await?;
629
630    let Some((content_1, content_2)) = content_1.zip(content_2) else {
631        return Ok(None);
632    };
633
634    if content_1 != content_2 {
635        return Ok(None);
636    }
637
638    Ok(Some((content_1, start, end)))
639}
640
641#[turbo_tasks::value(transparent)]
642pub struct OptionIssueSource(Option<IssueSource>);
643
644#[turbo_tasks::value(transparent)]
645pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
646
647// A structured reference to a file with module level details for displaying in an import trace
648#[derive(
649    Serialize,
650    PartialEq,
651    Eq,
652    PartialOrd,
653    Ord,
654    Clone,
655    Debug,
656    TraceRawVcs,
657    NonLocalValue,
658    DeterministicHash,
659)]
660#[serde(rename_all = "camelCase")]
661pub struct PlainTraceItem {
662    // The name of the filesystem
663    pub fs_name: RcStr,
664    // The root path of the filesystem, for constructing links
665    pub root_path: RcStr,
666    // The path of the file, relative to the filesystem root
667    pub path: RcStr,
668    // An optional label attached to the module that clarifies where in the module graph it is.
669    pub layer: Option<RcStr>,
670}
671
672impl PlainTraceItem {
673    async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
674        // TODO(lukesandberg): How should we display paths? it would be good to display all paths
675        // relative to the cwd or the project root.
676        let fs_path = asset.path.clone();
677        let fs_name = fs_path.fs.to_string().owned().await?;
678        let root_path = fs_path.fs.root().await?.path.clone();
679        let path = fs_path.path.clone();
680        let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
681        Ok(Self {
682            fs_name,
683            root_path,
684            path,
685            layer,
686        })
687    }
688}
689
690pub type PlainTrace = Vec<PlainTraceItem>;
691
692// Flatten and simplify this set of import traces into a simpler format for formatting.
693async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
694    let mut plain_traces = traces
695        .into_iter()
696        .map(|trace| async move {
697            let mut plain_trace = trace
698                .into_iter()
699                .filter(|asset| {
700                    // If there are nested assets, this is a synthetic module which is likely to be
701                    // confusing/distracting.  Just skip it.
702                    asset.assets.is_empty()
703                })
704                .map(PlainTraceItem::from_asset_ident)
705                .try_join()
706                .await?;
707
708            // After simplifying the trace, we may end up with apparent duplicates.
709            // Consider this example:
710            // Import trace:
711            // ./[project]/app/global.scss.css [app-client] (css) [app-client]
712            // ./[project]/app/layout.js [app-client] (ecmascript) [app-client]
713            // ./[project]/app/layout.js [app-rsc] (client reference proxy) [app-rsc]
714            // ./[project]/app/layout.js [app-rsc] (ecmascript) [app-rsc]
715            // ./[project]/app/layout.js [app-rsc] (ecmascript, Next.js Server Component) [app-rsc]
716            //
717            // In that case, there are an number of 'shim modules' that are inserted by next with
718            // different `modifiers` that are used to model the server->client hand off.  The
719            // simplification performed by `PlainTraceItem::from_asset_ident` drops these
720            // 'modifiers' and so we would end up with 'app/layout.js' appearing to be duplicated
721            // several times.  These modules are implementation details of the application so we
722            // just deduplicate them here.
723
724            plain_trace.dedup();
725
726            Ok(plain_trace)
727        })
728        .try_join()
729        .await?;
730
731    // Trim any empty traces and traces that only contain 1 item.  Showing a trace that points to
732    // the file with the issue is not useful.
733    plain_traces.retain(|t| t.len() > 1);
734    // Sort so the shortest traces come first, and break ties by the trace itself to ensure
735    // stability
736    plain_traces.sort_by(|a, b| {
737        // Sort by length first, so that shorter traces come first.
738        a.len().cmp(&b.len()).then_with(|| a.cmp(b))
739    });
740
741    // Now see if there are any overlaps
742    // If two of the traces overlap that means one is a suffix of another one.  Because we are
743    // computing shortest paths in the same graph and the shortest path algorithm we use is
744    // deterministic.
745    // Technically this is a quadratic algorithm since we need to compare each trace with all
746    // subsequent traces, however there are rarely more than 3 traces and certainly never more
747    // than 10.
748    if plain_traces.len() > 1 {
749        let mut i = 0;
750        while i < plain_traces.len() - 1 {
751            let mut j = plain_traces.len() - 1;
752            while j > i {
753                if plain_traces[j].ends_with(&plain_traces[i]) {
754                    // Remove the longer trace.
755                    // This typically happens due to things like server->client transitions where
756                    // the same file appears multiple times under different modules identifiers.
757                    // On the one hand the shorter trace is simpler, on the other hand the longer
758                    // trace might be more 'interesting' and even relevant.
759                    plain_traces.remove(j);
760                }
761                j -= 1;
762            }
763            i += 1;
764        }
765    }
766
767    Ok(plain_traces)
768}
769
770#[turbo_tasks::value(shared, serialization = "none")]
771#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
772pub enum IssueStage {
773    Config,
774    AppStructure,
775    ProcessModule,
776    /// Read file.
777    Load,
778    SourceTransform,
779    Parse,
780    /// TODO: Add index of the transform
781    Transform,
782    Analysis,
783    Resolve,
784    Bindings,
785    CodeGen,
786    Unsupported,
787    Misc,
788    Other(String),
789}
790
791impl Display for IssueStage {
792    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
793        match self {
794            IssueStage::Config => write!(f, "config"),
795            IssueStage::Resolve => write!(f, "resolve"),
796            IssueStage::ProcessModule => write!(f, "process module"),
797            IssueStage::Load => write!(f, "load"),
798            IssueStage::SourceTransform => write!(f, "source transform"),
799            IssueStage::Parse => write!(f, "parse"),
800            IssueStage::Transform => write!(f, "transform"),
801            IssueStage::Analysis => write!(f, "analysis"),
802            IssueStage::Bindings => write!(f, "bindings"),
803            IssueStage::CodeGen => write!(f, "code gen"),
804            IssueStage::Unsupported => write!(f, "unsupported"),
805            IssueStage::AppStructure => write!(f, "app structure"),
806            IssueStage::Misc => write!(f, "misc"),
807            IssueStage::Other(s) => write!(f, "{s}"),
808        }
809    }
810}
811
812#[turbo_tasks::value(serialization = "none")]
813#[derive(Clone, Debug, PartialOrd, Ord)]
814pub struct PlainIssue {
815    pub severity: IssueSeverity,
816    pub stage: IssueStage,
817
818    pub title: StyledString,
819    pub file_path: RcStr,
820
821    pub description: Option<StyledString>,
822    pub detail: Option<StyledString>,
823    pub documentation_link: RcStr,
824
825    pub source: Option<PlainIssueSource>,
826    pub processing_path: ReadRef<PlainIssueProcessingPath>,
827    pub import_traces: Vec<PlainTrace>,
828}
829
830fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
831    hasher.write_ref(&issue.severity);
832    hasher.write_ref(&issue.file_path);
833    hasher.write_ref(&issue.stage);
834    hasher.write_ref(&issue.title);
835    hasher.write_ref(&issue.description);
836    hasher.write_ref(&issue.detail);
837    hasher.write_ref(&issue.documentation_link);
838
839    if let Some(source) = &issue.source {
840        hasher.write_value(1_u8);
841        // I'm assuming we don't need to hash the contents. Not 100% correct, but
842        // probably 99%.
843        hasher.write_ref(&source.range);
844    } else {
845        hasher.write_value(0_u8);
846    }
847
848    if full {
849        hasher.write_ref(&issue.processing_path);
850        hasher.write_ref(&issue.import_traces);
851    }
852}
853
854impl PlainIssue {
855    /// We need deduplicate issues that can come from unique paths, but
856    /// represent the same underlying problem. Eg, a parse error for a file
857    /// that is compiled in both client and server contexts.
858    ///
859    /// Passing [full] will also hash any sub-issues and processing paths. While
860    /// useful for generating exact matching hashes, it's possible for the
861    /// same issue to pass from multiple processing paths, making for overly
862    /// verbose logging.
863    pub fn internal_hash_ref(&self, full: bool) -> u64 {
864        let mut hasher = Xxh3Hash64Hasher::new();
865        hash_plain_issue(self, &mut hasher, full);
866        hasher.finish()
867    }
868}
869
870#[turbo_tasks::value_impl]
871impl PlainIssue {
872    /// Translate an [Issue] into a [PlainIssue]. A more regular structure suitable for printing and
873    /// serialization.
874    #[turbo_tasks::function]
875    pub async fn from_issue(
876        issue: ResolvedVc<Box<dyn Issue>>,
877        import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
878        processing_path: Vc<OptionIssueProcessingPathItems>,
879    ) -> Result<Vc<Self>> {
880        let description: Option<StyledString> = match *issue.description().await? {
881            Some(description) => Some((*description.await?).clone()),
882            None => None,
883        };
884        let detail = match *issue.detail().await? {
885            Some(detail) => Some((*detail.await?).clone()),
886            None => None,
887        };
888        let trait_ref = issue.into_trait_ref().await?;
889
890        let severity = trait_ref.severity();
891
892        Ok(Self::cell(Self {
893            severity,
894            file_path: issue.file_path().to_string().owned().await?,
895            stage: issue.stage().owned().await?,
896            title: issue.title().owned().await?,
897            description,
898            detail,
899            documentation_link: issue.documentation_link().owned().await?,
900            source: {
901                if let Some(s) = &*issue.source().await? {
902                    Some(s.into_plain().await?)
903                } else {
904                    None
905                }
906            },
907            processing_path: processing_path.into_plain().await?,
908            import_traces: match import_tracer {
909                Some(tracer) => {
910                    into_plain_trace(
911                        tracer
912                            .await?
913                            .get_traces(issue.file_path().await?.clone_value())
914                            .await?,
915                    )
916                    .await?
917                }
918                None => vec![],
919            },
920        }))
921    }
922}
923
924#[turbo_tasks::value(serialization = "none")]
925#[derive(Clone, Debug, PartialOrd, Ord)]
926pub struct PlainIssueSource {
927    pub asset: ReadRef<PlainSource>,
928    pub range: Option<(SourcePos, SourcePos)>,
929}
930
931#[turbo_tasks::value(serialization = "none")]
932#[derive(Clone, Debug, PartialOrd, Ord)]
933pub struct PlainSource {
934    pub ident: ReadRef<RcStr>,
935    #[turbo_tasks(debug_ignore)]
936    pub content: ReadRef<FileContent>,
937}
938
939#[turbo_tasks::value_impl]
940impl PlainSource {
941    #[turbo_tasks::function]
942    pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
943        let asset_content = asset.content().await?;
944        let content = match *asset_content {
945            AssetContent::File(file_content) => file_content.await?,
946            AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
947        };
948
949        Ok(PlainSource {
950            ident: asset.ident().to_string().await?,
951            content,
952        }
953        .cell())
954    }
955}
956
957#[turbo_tasks::value(transparent, serialization = "none")]
958#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
959pub struct PlainIssueProcessingPath(Option<Vec<ReadRef<PlainIssueProcessingPathItem>>>);
960
961#[turbo_tasks::value(serialization = "none")]
962#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
963pub struct PlainIssueProcessingPathItem {
964    pub file_path: Option<ReadRef<RcStr>>,
965    pub description: ReadRef<RcStr>,
966}
967
968#[turbo_tasks::value_trait]
969pub trait IssueReporter {
970    /// Reports issues to the user (e.g. to stdio). Returns whether fatal
971    /// (program-ending) issues were present.
972    ///
973    /// # Arguments:
974    ///
975    /// * `issues` - A [ReadRef] of [CapturedIssues]. Typically obtained with
976    ///   `source.peek_issues_with_path()`.
977    /// * `source` - The root [Vc] from which issues are traced. Can be used by implementers to
978    ///   determine which issues are new.
979    /// * `min_failing_severity` - The minimum Vc<[IssueSeverity]>
980    ///  The minimum issue severity level considered to fatally end the program.
981    #[turbo_tasks::function]
982    fn report_issues(
983        self: Vc<Self>,
984        issues: TransientInstance<CapturedIssues>,
985        source: TransientValue<RawVc>,
986        min_failing_severity: IssueSeverity,
987    ) -> Vc<bool>;
988}
989
990#[async_trait]
991pub trait IssueDescriptionExt
992where
993    Self: Sized,
994{
995    #[allow(unused_variables, reason = "behind feature flag")]
996    async fn attach_file_path(
997        self,
998        file_path: impl Into<Option<FileSystemPath>> + Send,
999        description: impl Into<String> + Send,
1000    ) -> Result<Self>;
1001
1002    #[allow(unused_variables, reason = "behind feature flag")]
1003    async fn attach_description(self, description: impl Into<String> + Send) -> Result<Self>;
1004
1005    async fn issue_file_path(
1006        self,
1007        file_path: impl Into<Option<FileSystemPath>> + Send,
1008        description: impl Into<String> + Send,
1009    ) -> Result<Self>;
1010    async fn issue_description(self, description: impl Into<String> + Send) -> Result<Self>;
1011
1012    /// Returns all issues from `source` in a list with their associated
1013    /// processing path.
1014    async fn peek_issues_with_path(self) -> Result<CapturedIssues>;
1015
1016    /// Returns all issues from `source` in a list with their associated
1017    /// processing path.
1018    ///
1019    /// This unemits the issues. They will not propagate up.
1020    async fn take_issues_with_path(self) -> Result<CapturedIssues>;
1021}
1022
1023#[async_trait]
1024impl<T> IssueDescriptionExt for T
1025where
1026    T: CollectiblesSource + Copy + Send,
1027{
1028    #[allow(unused_variables, reason = "behind feature flag")]
1029    async fn attach_file_path(
1030        self,
1031        file_path: impl Into<Option<FileSystemPath>> + Send,
1032        description: impl Into<String> + Send,
1033    ) -> Result<Self> {
1034        #[cfg(feature = "issue_path")]
1035        {
1036            let children = self.take_collectibles();
1037            if !children.is_empty() {
1038                emit(ResolvedVc::upcast::<Box<dyn IssueProcessingPath>>(
1039                    ItemIssueProcessingPath::resolved_cell(ItemIssueProcessingPath(
1040                        Some(IssueProcessingPathItem::resolved_cell(
1041                            IssueProcessingPathItem {
1042                                file_path: file_path.into(),
1043                                description: ResolvedVc::cell(RcStr::from(description.into())),
1044                            },
1045                        )),
1046                        children,
1047                    )),
1048                ));
1049            }
1050        }
1051        Ok(self)
1052    }
1053
1054    #[allow(unused_variables, reason = "behind feature flag")]
1055    async fn attach_description(self, description: impl Into<String> + Send) -> Result<T> {
1056        self.attach_file_path(None, description).await
1057    }
1058
1059    async fn issue_file_path(
1060        self,
1061        file_path: impl Into<Option<FileSystemPath>> + Send,
1062        description: impl Into<String> + Send,
1063    ) -> Result<Self> {
1064        #[cfg(feature = "issue_path")]
1065        {
1066            let children = self.take_collectibles();
1067            if !children.is_empty() {
1068                emit(ResolvedVc::upcast::<Box<dyn IssueProcessingPath>>(
1069                    ItemIssueProcessingPath::resolved_cell(ItemIssueProcessingPath(
1070                        Some(IssueProcessingPathItem::resolved_cell(
1071                            IssueProcessingPathItem {
1072                                file_path: file_path.into(),
1073                                description: ResolvedVc::cell(RcStr::from(description.into())),
1074                            },
1075                        )),
1076                        children,
1077                    )),
1078                ));
1079            }
1080        }
1081        #[cfg(not(feature = "issue_path"))]
1082        {
1083            let _ = (file_path, description);
1084        }
1085        Ok(self)
1086    }
1087
1088    async fn issue_description(self, description: impl Into<String> + Send) -> Result<Self> {
1089        self.issue_file_path(None, description).await
1090    }
1091
1092    async fn peek_issues_with_path(self) -> Result<CapturedIssues> {
1093        Ok(CapturedIssues {
1094            issues: self.peek_collectibles(),
1095            #[cfg(feature = "issue_path")]
1096            processing_path: ItemIssueProcessingPath::resolved_cell(ItemIssueProcessingPath(
1097                None,
1098                self.peek_collectibles(),
1099            )),
1100            tracer: DelegatingImportTracer::resolved_cell(DelegatingImportTracer {
1101                delegates: self.peek_collectibles(),
1102            }),
1103        })
1104    }
1105
1106    async fn take_issues_with_path(self) -> Result<CapturedIssues> {
1107        Ok(CapturedIssues {
1108            issues: self.take_collectibles(),
1109            #[cfg(feature = "issue_path")]
1110            processing_path: ItemIssueProcessingPath::resolved_cell(ItemIssueProcessingPath(
1111                None,
1112                self.take_collectibles(),
1113            )),
1114            tracer: DelegatingImportTracer::resolved_cell(DelegatingImportTracer {
1115                delegates: self.take_collectibles(),
1116            }),
1117        })
1118    }
1119}
1120
1121pub async fn handle_issues<T: Send>(
1122    source_op: OperationVc<T>,
1123    issue_reporter: Vc<Box<dyn IssueReporter>>,
1124    min_failing_severity: IssueSeverity,
1125    path: Option<&str>,
1126    operation: Option<&str>,
1127) -> Result<()> {
1128    let source_vc = source_op.connect();
1129    let _ = source_op.resolve_strongly_consistent().await?;
1130    let issues = source_op.peek_issues_with_path().await?;
1131
1132    let has_fatal = issue_reporter.report_issues(
1133        TransientInstance::new(issues),
1134        TransientValue::new(Vc::into_raw(source_vc)),
1135        min_failing_severity,
1136    );
1137
1138    if *has_fatal.await? {
1139        let mut message = "Fatal issue(s) occurred".to_owned();
1140        if let Some(path) = path.as_ref() {
1141            message += &format!(" in {path}");
1142        };
1143        if let Some(operation) = operation.as_ref() {
1144            message += &format!(" ({operation})");
1145        };
1146
1147        Err(anyhow!(message))
1148    } else {
1149        Ok(())
1150    }
1151}
1152
1153fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1154    match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1155        Ok(i) => SourcePos {
1156            line: i as u32,
1157            column: 0,
1158        },
1159        Err(i) => {
1160            if i == 0 {
1161                SourcePos {
1162                    line: 0,
1163                    column: offset,
1164                }
1165            } else {
1166                let line = &lines[i - 1];
1167                SourcePos {
1168                    line: (i - 1) as u32,
1169                    column: min(line.content.len() as u32, offset - line.bytes_offset),
1170                }
1171            }
1172        }
1173    }
1174}
1175
1176fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1177    let line = &lines[pos.line as usize];
1178    line.bytes_offset + pos.column
1179}