Skip to main content

turbopack_core/issue/
mod.rs

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/// Represents a section of structured styled text. This can be interpreted and
87/// rendered by various UIs as appropriate, e.g. HTML for display on the web,
88/// ANSI sequences in TTYs.
89#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
90#[turbo_tasks::value(shared)]
91pub enum StyledString {
92    /// Multiple [StyledString]s concatenated into a single line. Each item is
93    /// considered as inline element. Items might contain line breaks, which
94    /// would be considered as soft line breaks.
95    Line(Vec<StyledString>),
96    /// Multiple [StyledString]s stacked vertically. They are considered as
97    /// block elements, just like the top level [StyledString].
98    Stack(Vec<StyledString>),
99    /// Some prose text.
100    Text(RcStr),
101    /// Code snippet.
102    // TODO add language to support syntax highlighting
103    Code(RcStr),
104    /// Some important text.
105    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    /// Severity allows the user to filter out unimportant issues, with Bug
131    /// being the highest priority and Info being the lowest.
132    fn severity(&self) -> IssueSeverity {
133        IssueSeverity::Error
134    }
135
136    /// The file path that generated the issue, displayed to the user as message
137    /// header.
138    #[turbo_tasks::function]
139    fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
140
141    /// The stage of the compilation process at which the issue occurred. This
142    /// is used to sort issues.
143    #[turbo_tasks::function]
144    fn stage(self: Vc<Self>) -> Vc<IssueStage>;
145
146    /// The issue title should be descriptive of the issue, but should be a
147    /// single line. This is displayed to the user directly under the issue
148    /// header.
149    // TODO add Vc<StyledString>
150    #[turbo_tasks::function]
151    fn title(self: Vc<Self>) -> Vc<StyledString>;
152
153    /// A more verbose message of the issue, appropriate for providing multiline
154    /// information of the issue.
155    // TODO add Vc<StyledString>
156    #[turbo_tasks::function]
157    fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
158        Vc::cell(None)
159    }
160
161    /// Full details of the issue, appropriate for providing debug level
162    /// information. Only displayed if the user explicitly asks for detailed
163    /// messages (not to be confused with severity).
164    #[turbo_tasks::function]
165    fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
166        Vc::cell(None)
167    }
168
169    /// A link to relevant documentation of the issue. Only displayed in console
170    /// if the user explicitly asks for detailed messages.
171    #[turbo_tasks::function]
172    fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
173        Vc::<RcStr>::default()
174    }
175
176    /// The source location that caused the issue. Eg, for a parsing error it
177    /// should point at the offending character. Displayed to the user alongside
178    /// the title/description.
179    #[turbo_tasks::function]
180    fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
181        Vc::cell(None)
182    }
183}
184
185// A collectible trait that allows traces to be computed for a given module.
186#[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/// A pattern that can match by exact string, glob, or regex.
242#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
243pub enum IgnoreIssuePattern {
244    /// The value must exactly equal the pattern string.
245    ExactString(RcStr),
246    /// The pattern is treated as a glob (uses turbo-tasks-fs glob matching).
247    Glob(Glob),
248    /// The pattern is a regular expression (supports ES-style patterns via `EsRegex`).
249    Regex(EsRegex),
250}
251
252impl IgnoreIssuePattern {
253    /// Test whether the pattern matches the given value.
254    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/// A rule describing an issue to ignore. `path` is mandatory;
264/// `title` and `description` are optional additional filters.
265#[derive(Clone, Debug, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
266pub struct IgnoreIssue {
267    /// File-path pattern (mandatory).
268    pub path: IgnoreIssuePattern,
269    /// Title pattern (optional).
270    pub title: Option<IgnoreIssuePattern>,
271    /// Description pattern (optional).
272    pub description: Option<IgnoreIssuePattern>,
273}
274
275#[turbo_tasks::value(shared)]
276pub struct IssueFilter {
277    /// The minimum severity for issues
278    severity: IssueSeverity,
279    /// The minimum severity for issues in node_modules
280    foreign_severity: IssueSeverity,
281    /// Issues matching any of these rules are ignored (dropped from results).
282    ignore_rules: Vec<IgnoreIssue>,
283}
284
285#[turbo_tasks::value_impl]
286impl IssueFilter {
287    /// A filter that lets everything through.
288    #[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    /// Returns true if the issue is allowed by this filter.
299    #[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        // Fetch the file path once — it's used by both severity and ignore-rule
311        // checks.
312        let file_path = issue.file_path().await?;
313
314        // Check severity first — this is cheap and avoids fetching
315        // title/description for issues that would be filtered out anyway.
316        let severity = issue.into_trait_ref().await?.severity();
317        // NOTE: Lower severities are _more_ severe
318        let severity_allowed = if severity <= self.severity || severity <= self.foreign_severity {
319            // we need to check the path to see if it is foreign or not.  Only await the
320            // path if it might possibly matter
321            if severity <= self.severity && severity <= self.foreign_severity {
322                // it matches no matter where the path is
323                true
324            } else if ContextCondition::InNodeModules.matches(&file_path) {
325                severity <= self.foreign_severity
326            } else {
327                severity <= self.severity
328            }
329        } else {
330            // it is too low severity to match either way
331            false
332        };
333
334        if !severity_allowed {
335            return Ok(Vc::cell(false));
336        }
337
338        // Check ignore rules — if any rule matches, the issue is dropped.
339        // Title and description are fetched lazily: only when a rule's path
340        // matches and the rule also specifies a title/description pattern.
341        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                // All specified fields matched — ignore this issue.
372                return Ok(Vc::cell(false));
373            }
374        }
375
376        Ok(Vc::cell(true))
377    }
378}
379
380impl IssueFilter {
381    /// Construct a filter with the standard warning/foreign-error severities.
382    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    /// Set the ignore rules for this filter.
391    pub fn with_ignore_rules(mut self, rules: Vec<IgnoreIssue>) -> Self {
392        self.ignore_rules = rules;
393        self
394    }
395}
396
397/// A list of issues captured with [`Issue::peek_issues_with_path`] and
398/// [`Issue::take_issues`].
399#[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    /// Returns true if there are no issues.
416    pub fn is_empty_ref(&self) -> bool {
417        self.issues.is_empty()
418    }
419
420    /// Returns the number of issues.
421    #[allow(clippy::len_without_is_empty)]
422    pub fn len(&self) -> usize {
423        self.issues.len()
424    }
425
426    /// Returns an iterator over the issues.
427    pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
428        self.issues.iter().copied()
429    }
430
431    // Returns all the issues as formatted `PlainIssues`.
432    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/// The end position is the first character after the range
464#[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    // Sometimes we only have the source file that causes an issue, not the
474    // exact location, such as as in some generated code.
475    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 we have a source map, map the line/column to the original source.
511            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    /// Create a [`IssueSource`] from byte offsets given by an swc ast node
530    /// span.
531    ///
532    /// Arguments:
533    ///
534    /// * `source`: The source code in which to look up the byte offsets.
535    /// * `start`: The start index of the span. Must use **1-based** indexing.
536    /// * `end`: The end index of the span. Must use **1-based** indexing.
537    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    /// Returns an `IssueSource` representing a span of code in the `source`.
550    /// Positions are derived from byte offsets and stored as lines and columns.
551    /// Requires a binary search of the source text to perform this.
552    ///
553    /// Arguments:
554    ///
555    /// * `source`: The source code in which to look up the byte offsets.
556    /// * `start`: Byte offset into the source that the text begins. 0-based index and inclusive.
557    /// * `end`: Byte offset into the source that the text ends. 0-based index and exclusive.
558    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    /// Returns the file path for the source file.
576    pub fn file_path(&self) -> Vc<FileSystemPath> {
577        self.source.ident().path()
578    }
579}
580
581impl IssueSource {
582    /// Returns bytes offsets corresponding the source range in the format used by swc's Spans.
583    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// A structured reference to a file with module level details for displaying in an import trace
661#[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    // The name of the filesystem
676    pub fs_name: RcStr,
677    // The root path of the filesystem, for constructing links
678    pub root_path: RcStr,
679    // The path of the file, relative to the filesystem root
680    pub path: RcStr,
681    // An optional label attached to the module that clarifies where in the module graph it is.
682    pub layer: Option<RcStr>,
683}
684
685impl PlainTraceItem {
686    async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
687        // TODO(lukesandberg): How should we display paths? it would be good to display all paths
688        // relative to the cwd or the project root.
689        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
705// Flatten and simplify this set of import traces into a simpler format for formatting.
706async 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                    // If there are nested assets, this is a synthetic module which is likely to be
714                    // confusing/distracting.  Just skip it.
715                    asset.assets.is_empty()
716                })
717                .map(PlainTraceItem::from_asset_ident)
718                .try_join()
719                .await?;
720
721            // After simplifying the trace, we may end up with apparent duplicates.
722            // Consider this example:
723            // Import trace:
724            // ./[project]/app/global.scss.css [app-client] (css) [app-client]
725            // ./[project]/app/layout.js [app-client] (ecmascript) [app-client]
726            // ./[project]/app/layout.js [app-rsc] (client reference proxy) [app-rsc]
727            // ./[project]/app/layout.js [app-rsc] (ecmascript) [app-rsc]
728            // ./[project]/app/layout.js [app-rsc] (ecmascript, Next.js Server Component) [app-rsc]
729            //
730            // In that case, there are an number of 'shim modules' that are inserted by next with
731            // different `modifiers` that are used to model the server->client hand off.  The
732            // simplification performed by `PlainTraceItem::from_asset_ident` drops these
733            // 'modifiers' and so we would end up with 'app/layout.js' appearing to be duplicated
734            // several times.  These modules are implementation details of the application so we
735            // just deduplicate them here.
736
737            plain_trace.dedup();
738
739            Ok(plain_trace)
740        })
741        .try_join()
742        .await?;
743
744    // Trim any empty traces and traces that only contain 1 item.  Showing a trace that points to
745    // the file with the issue is not useful.
746    plain_traces.retain(|t| t.len() > 1);
747    // Sort so the shortest traces come first, and break ties by the trace itself to ensure
748    // stability
749    plain_traces.sort_by(|a, b| {
750        // Sort by length first, so that shorter traces come first.
751        a.len().cmp(&b.len()).then_with(|| a.cmp(b))
752    });
753
754    // Now see if there are any overlaps
755    // If two of the traces overlap that means one is a suffix of another one.  Because we are
756    // computing shortest paths in the same graph and the shortest path algorithm we use is
757    // deterministic.
758    // Technically this is a quadratic algorithm since we need to compare each trace with all
759    // subsequent traces, however there are rarely more than 3 traces and certainly never more
760    // than 10.
761    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                    // Remove the longer trace.
768                    // This typically happens due to things like server->client transitions where
769                    // the same file appears multiple times under different modules identifiers.
770                    // On the one hand the shorter trace is simpler, on the other hand the longer
771                    // trace might be more 'interesting' and even relevant.
772                    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    /// Read file.
790    Load,
791    SourceTransform,
792    Parse,
793    /// TODO: Add index of the transform
794    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        // I'm assuming we don't need to hash the contents. Not 100% correct, but
854        // probably 99%.
855        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    /// We need deduplicate issues that can come from unique paths, but
867    /// represent the same underlying problem. Eg, a parse error for a file
868    /// that is compiled in both client and server contexts.
869    ///
870    /// Passing [full] will also hash any sub-issues and processing paths. While
871    /// useful for generating exact matching hashes, it's possible for the
872    /// same issue to pass from multiple processing paths, making for overly
873    /// verbose logging.
874    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    /// Translate an [Issue] into a [PlainIssue]. A more regular structure suitable for printing and
884    /// serialization.
885    #[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    /// Reports issues to the user (e.g. to stdio). Returns whether fatal
969    /// (program-ending) issues were present.
970    ///
971    /// # Arguments:
972    ///
973    /// * `source` - The root [Vc] from which issues are traced. Can be used by implementers to
974    ///   determine which issues are new.  This must be derived from the OperationVc so issues can
975    ///   be collected.
976    /// * `min_failing_severity` - The minimum Vc<[IssueSeverity]>
977    ///  The minimum issue severity level considered to fatally end the program.
978    #[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    /// Returns all issues from `source`
991    ///
992    /// Must be called in a turbo-task as this constructs a `cell`
993    fn peek_issues(self) -> CapturedIssues;
994
995    /// Drops all issues from `source`
996    ///
997    /// This unemits the issues. They will not propagate up.
998    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
1021/// A helper function to print out issues to the console.
1022///
1023/// Must be called in a turbo-task as this constructs a `cell`
1024pub 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}