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