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                    // Defensively read the content, an error there should not prevent all issue
501                    // formatting.  Best practice is for `content` to return `NotFound` instead of
502                    // an error.
503                    if let Ok(content) = self.source.content().lines().await
504                        && let FileLinesContent::Lines(lines) = &*content
505                    {
506                        let start = find_line_and_column(lines.as_ref(), start);
507                        let end = find_line_and_column(lines.as_ref(), end);
508                        Some((start, end))
509                    } else {
510                        None
511                    }
512                }
513            };
514
515            // If we have a source map, map the line/column to the original source.
516            if let Some((start, end)) = range {
517                let mapped = source_pos(source, start, end).await?;
518
519                if let Some((mapped_source, start, end)) = mapped {
520                    range = Some((start, end));
521                    source = mapped_source;
522                }
523            }
524            range
525        } else {
526            None
527        };
528        Ok(PlainIssueSource {
529            asset: PlainSource::from_source(*source).await?,
530            range,
531        })
532    }
533
534    /// Create an [`IssueSource`] from an [`UnparsableJson`] error, using its
535    /// start/end location if available.
536    pub fn from_unparsable_json(
537        source: ResolvedVc<Box<dyn Source>>,
538        error: &UnparsableJson,
539    ) -> Self {
540        match (error.start_location, error.end_location) {
541            (None, None) => Self::from_source_only(source),
542            (Some((line, column)), None) | (None, Some((line, column))) => Self::from_line_col(
543                source,
544                SourcePos { line, column },
545                SourcePos { line, column },
546            ),
547            (Some((start_line, start_column)), Some((end_line, end_column))) => {
548                Self::from_line_col(
549                    source,
550                    SourcePos {
551                        line: start_line,
552                        column: start_column,
553                    },
554                    SourcePos {
555                        line: end_line,
556                        column: end_column,
557                    },
558                )
559            }
560        }
561    }
562
563    /// Create a [`IssueSource`] from byte offsets given by an swc ast node
564    /// span.
565    ///
566    /// Arguments:
567    ///
568    /// * `source`: The source code in which to look up the byte offsets.
569    /// * `start`: The start index of the span. Must use **1-based** indexing.
570    /// * `end`: The end index of the span. Must use **1-based** indexing.
571    pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
572        IssueSource {
573            source,
574            range: match (start == 0, end == 0) {
575                (true, true) => None,
576                (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
577                (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
578                (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
579            },
580        }
581    }
582
583    /// Returns an `IssueSource` representing a span of code in the `source`.
584    /// Positions are derived from byte offsets and stored as lines and columns.
585    /// Requires a binary search of the source text to perform this.
586    ///
587    /// Arguments:
588    ///
589    /// * `source`: The source code in which to look up the byte offsets.
590    /// * `start`: Byte offset into the source that the text begins. 0-based index and inclusive.
591    /// * `end`: Byte offset into the source that the text ends. 0-based index and exclusive.
592    pub async fn from_byte_offset(
593        source: ResolvedVc<Box<dyn Source>>,
594        start: u32,
595        end: u32,
596    ) -> Result<Self> {
597        Ok(IssueSource {
598            source,
599            range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
600                let start = find_line_and_column(lines.as_ref(), start);
601                let end = find_line_and_column(lines.as_ref(), end);
602                Some(SourceRange::LineColumn(start, end))
603            } else {
604                None
605            },
606        })
607    }
608
609    /// Returns the file path for the source file.
610    pub async fn file_path(&self) -> Result<FileSystemPath> {
611        Ok(self.source.ident().await?.path.clone())
612    }
613
614    /// If this source implements `GenerateSourceMap`, returns an
615    /// `AdditionalIssueSource` that wraps the source in a `GeneratedCodeSource`
616    /// (stripping source-map support) so the generated code is shown alongside
617    /// the original in error messages. Returns `None` otherwise.
618    pub async fn to_generated_code_source(&self) -> Result<Option<AdditionalIssueSource>> {
619        if ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(self.source).is_some() {
620            let description = self.source.description().await?;
621            let generated = Vc::upcast::<Box<dyn Source>>(GeneratedCodeSource::new(*self.source))
622                .to_resolved()
623                .await?;
624            return Ok(Some(AdditionalIssueSource {
625                description: format!("Generated code of {}", description).into(),
626                source: IssueSource {
627                    source: generated,
628                    // The range is intentionally copied verbatim: the offsets
629                    // are already in generated-source coordinates (they came
630                    // from parsing the loader output), so no remapping is
631                    // needed here.
632                    range: self.range,
633                },
634            }));
635        }
636        Ok(None)
637    }
638}
639
640impl IssueSource {
641    /// Returns bytes offsets corresponding the source range in the format used by swc's Spans.
642    pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
643        Ok(match &self.range {
644            Some(range) => match range {
645                SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
646                SourceRange::LineColumn(start, end) => {
647                    if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
648                        let start = find_offset(lines.as_ref(), *start) + 1;
649                        let end = find_offset(lines.as_ref(), *end) + 1;
650                        Some((start, end))
651                    } else {
652                        None
653                    }
654                }
655            },
656            _ => None,
657        })
658    }
659}
660
661async fn source_pos(
662    source: ResolvedVc<Box<dyn Source>>,
663    start: SourcePos,
664    end: SourcePos,
665) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
666    let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
667        return Ok(None);
668    };
669
670    let srcmap = generator.generate_source_map();
671    let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
672        return Ok(None);
673    };
674
675    let find = async |line: u32, col: u32| {
676        let TokenWithSource {
677            token,
678            source_content,
679        } = &srcmap.lookup_token_and_source(line, col).await?;
680
681        match token {
682            crate::source_map::Token::Synthetic(t) => anyhow::Ok((
683                SourcePos {
684                    line: t.generated_line as _,
685                    column: t.generated_column as _,
686                },
687                *source_content,
688            )),
689            crate::source_map::Token::Original(t) => anyhow::Ok((
690                SourcePos {
691                    line: t.original_line as _,
692                    column: t.original_column as _,
693                },
694                *source_content,
695            )),
696        }
697    };
698
699    let (start, content_1) = find(start.line, start.column).await?;
700    let (end, content_2) = find(end.line, end.column).await?;
701
702    let Some((content_1, content_2)) = content_1.zip(content_2) else {
703        return Ok(None);
704    };
705
706    if content_1 != content_2 {
707        return Ok(None);
708    }
709
710    Ok(Some((content_1, start, end)))
711}
712
713/// A labeled issue source used to provide additional context in error messages.
714/// For example, when a webpack loader produces broken code, the primary source
715/// shows the original file, while an additional source shows the generated code.
716#[turbo_tasks::value(shared)]
717pub struct AdditionalIssueSource {
718    pub description: RcStr,
719    pub source: IssueSource,
720}
721
722#[turbo_tasks::value(shared, transparent)]
723pub struct AdditionalIssueSources(Vec<AdditionalIssueSource>);
724
725#[turbo_tasks::value_impl]
726impl AdditionalIssueSources {
727    #[turbo_tasks::function]
728    pub fn empty() -> Vc<Self> {
729        Vc::cell(Vec::new())
730    }
731}
732
733// A structured reference to a file with module level details for displaying in an import trace
734#[derive(
735    Serialize,
736    PartialEq,
737    Eq,
738    PartialOrd,
739    Ord,
740    Clone,
741    Debug,
742    TraceRawVcs,
743    NonLocalValue,
744    DeterministicHash,
745)]
746#[serde(rename_all = "camelCase")]
747pub struct PlainTraceItem {
748    // The name of the filesystem
749    pub fs_name: RcStr,
750    // The root path of the filesystem, for constructing links
751    pub root_path: RcStr,
752    // The path of the file, relative to the filesystem root
753    pub path: RcStr,
754    // An optional label attached to the module that clarifies where in the module graph it is.
755    pub layer: Option<RcStr>,
756}
757
758impl PlainTraceItem {
759    async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
760        // TODO(lukesandberg): How should we display paths? it would be good to display all paths
761        // relative to the cwd or the project root.
762        let fs_path = asset.path.clone();
763        let fs_name = fs_path.fs.to_string().owned().await?;
764        let root_path = fs_path.fs.root().await?.path.clone();
765        let path = fs_path.path.clone();
766        let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
767        Ok(Self {
768            fs_name,
769            root_path,
770            path,
771            layer,
772        })
773    }
774}
775
776pub type PlainTrace = Vec<PlainTraceItem>;
777
778// Flatten and simplify this set of import traces into a simpler format for formatting.
779async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
780    let mut plain_traces = traces
781        .into_iter()
782        .map(|trace| async move {
783            let mut plain_trace = trace
784                .into_iter()
785                .filter(|asset| {
786                    // If there are nested assets, this is a synthetic module which is likely to be
787                    // confusing/distracting.  Just skip it.
788                    asset.assets.is_empty()
789                })
790                .map(PlainTraceItem::from_asset_ident)
791                .try_join()
792                .await?;
793
794            // After simplifying the trace, we may end up with apparent duplicates.
795            // Consider this example:
796            // Import trace:
797            // ./[project]/app/global.scss.css [app-client] (css) [app-client]
798            // ./[project]/app/layout.js [app-client] (ecmascript) [app-client]
799            // ./[project]/app/layout.js [app-rsc] (client reference proxy) [app-rsc]
800            // ./[project]/app/layout.js [app-rsc] (ecmascript) [app-rsc]
801            // ./[project]/app/layout.js [app-rsc] (ecmascript, Next.js Server Component) [app-rsc]
802            //
803            // In that case, there are an number of 'shim modules' that are inserted by next with
804            // different `modifiers` that are used to model the server->client hand off.  The
805            // simplification performed by `PlainTraceItem::from_asset_ident` drops these
806            // 'modifiers' and so we would end up with 'app/layout.js' appearing to be duplicated
807            // several times.  These modules are implementation details of the application so we
808            // just deduplicate them here.
809
810            plain_trace.dedup();
811
812            Ok(plain_trace)
813        })
814        .try_join()
815        .await?;
816
817    // Trim any empty traces and traces that only contain 1 item.  Showing a trace that points to
818    // the file with the issue is not useful.
819    plain_traces.retain(|t| t.len() > 1);
820    // Sort so the shortest traces come first, and break ties by the trace itself to ensure
821    // stability
822    plain_traces.sort_by(|a, b| {
823        // Sort by length first, so that shorter traces come first.
824        a.len().cmp(&b.len()).then_with(|| a.cmp(b))
825    });
826
827    // Now see if there are any overlaps
828    // If two of the traces overlap that means one is a suffix of another one.  Because we are
829    // computing shortest paths in the same graph and the shortest path algorithm we use is
830    // deterministic.
831    // Technically this is a quadratic algorithm since we need to compare each trace with all
832    // subsequent traces, however there are rarely more than 3 traces and certainly never more
833    // than 10.
834    if plain_traces.len() > 1 {
835        let mut i = 0;
836        while i < plain_traces.len() - 1 {
837            let mut j = plain_traces.len() - 1;
838            while j > i {
839                if plain_traces[j].ends_with(&plain_traces[i]) {
840                    // Remove the longer trace.
841                    // This typically happens due to things like server->client transitions where
842                    // the same file appears multiple times under different modules identifiers.
843                    // On the one hand the shorter trace is simpler, on the other hand the longer
844                    // trace might be more 'interesting' and even relevant.
845                    plain_traces.remove(j);
846                }
847                j -= 1;
848            }
849            i += 1;
850        }
851    }
852
853    Ok(plain_traces)
854}
855
856#[turbo_tasks::value(shared)]
857#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
858pub enum IssueStage {
859    Config,
860    AppStructure,
861    ProcessModule,
862    /// Read file.
863    Load,
864    SourceTransform,
865    Parse,
866    /// TODO: Add index of the transform
867    Transform,
868    Analysis,
869    Resolve,
870    Bindings,
871    CodeGen,
872    Emit,
873    Unsupported,
874    Misc,
875    Other(RcStr),
876}
877
878impl Display for IssueStage {
879    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
880        match self {
881            IssueStage::Config => write!(f, "config"),
882            IssueStage::Resolve => write!(f, "resolve"),
883            IssueStage::ProcessModule => write!(f, "process module"),
884            IssueStage::Load => write!(f, "load"),
885            IssueStage::SourceTransform => write!(f, "source transform"),
886            IssueStage::Parse => write!(f, "parse"),
887            IssueStage::Transform => write!(f, "transform"),
888            IssueStage::Analysis => write!(f, "analysis"),
889            IssueStage::Bindings => write!(f, "bindings"),
890            IssueStage::CodeGen => write!(f, "code gen"),
891            IssueStage::Emit => write!(f, "emit"),
892            IssueStage::Unsupported => write!(f, "unsupported"),
893            IssueStage::AppStructure => write!(f, "app structure"),
894            IssueStage::Misc => write!(f, "misc"),
895            IssueStage::Other(s) => write!(f, "{s}"),
896        }
897    }
898}
899
900#[turbo_tasks::value(serialization = "skip")]
901#[derive(Clone, Debug, PartialOrd, Ord)]
902pub struct PlainIssue {
903    pub severity: IssueSeverity,
904    pub stage: IssueStage,
905
906    pub title: StyledString,
907    pub file_path: RcStr,
908
909    pub description: Option<StyledString>,
910    pub detail: Option<StyledString>,
911    pub documentation_link: RcStr,
912
913    pub source: Option<PlainIssueSource>,
914    pub additional_sources: Vec<PlainAdditionalIssueSource>,
915    pub import_traces: Vec<PlainTrace>,
916}
917
918#[turbo_tasks::value(serialization = "skip")]
919#[derive(Clone, Debug, PartialOrd, Ord)]
920pub struct PlainAdditionalIssueSource {
921    pub description: RcStr,
922    pub source: PlainIssueSource,
923}
924
925fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
926    hasher.write_ref(&issue.severity);
927    hasher.write_ref(&issue.file_path);
928    hasher.write_ref(&issue.stage);
929    hasher.write_ref(&issue.title);
930    hasher.write_ref(&issue.description);
931    hasher.write_ref(&issue.detail);
932    hasher.write_ref(&issue.documentation_link);
933
934    if let Some(source) = &issue.source {
935        hasher.write_value(1_u8);
936        // I'm assuming we don't need to hash the contents. Not 100% correct, but
937        // probably 99%.
938        hasher.write_ref(&source.range);
939    } else {
940        hasher.write_value(0_u8);
941    }
942
943    // `additional_sources` is intentionally not hashed: it carries supplementary
944    // display info (e.g. generated code from a loader) that does not change the
945    // identity of the underlying problem.  Two issues that differ only in their
946    // generated-code snippet still represent the same root cause and should be
947    // deduplicated.
948
949    if full {
950        hasher.write_ref(&issue.import_traces);
951    }
952}
953
954impl PlainIssue {
955    /// We need deduplicate issues that can come from unique paths, but represent the same
956    /// underlying problem. E.g., a parse error for a file that is compiled in both client and
957    /// server contexts.
958    ///
959    /// Passing `full` will also hash any sub-issues and processing paths. While useful for
960    /// generating exact matching hashes, it's possible for the same issue to pass from multiple
961    /// processing paths, making for overly verbose logging.
962    pub fn internal_hash_ref(&self, full: bool) -> u64 {
963        let mut hasher = Xxh3Hash64Hasher::new();
964        hash_plain_issue(self, &mut hasher, full);
965        hasher.finish()
966    }
967}
968
969#[turbo_tasks::value_impl]
970impl PlainIssue {
971    /// Translate an [Issue] into a [PlainIssue]. A more regular structure suitable for printing and
972    /// serialization.
973    #[turbo_tasks::function]
974    pub async fn from_issue(
975        issue: ResolvedVc<Box<dyn Issue>>,
976        import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
977    ) -> Result<Vc<Self>> {
978        let trait_ref = issue.into_trait_ref().await?;
979        let severity = trait_ref.severity();
980        let file_path = trait_ref.file_path().await?;
981        let file_path_str = file_path.to_string_ref().await?;
982
983        Ok(Self::cell(Self {
984            severity,
985            file_path: file_path_str,
986            stage: trait_ref.stage(),
987            title: trait_ref.title().await?,
988            description: trait_ref.description().await?,
989            detail: trait_ref.detail().await?,
990            documentation_link: trait_ref.documentation_link(),
991            source: {
992                if let Some(s) = trait_ref.source() {
993                    Some(s.into_plain().await?)
994                } else {
995                    None
996                }
997            },
998            additional_sources: {
999                trait_ref
1000                    .additional_sources()
1001                    .await?
1002                    .into_iter()
1003                    .map(async |s| {
1004                        Ok(PlainAdditionalIssueSource {
1005                            source: s.source.into_plain().await?,
1006                            description: s.description,
1007                        })
1008                    })
1009                    .try_join()
1010                    .await?
1011            },
1012            import_traces: match import_tracer {
1013                Some(tracer) => {
1014                    into_plain_trace(tracer.await?.get_traces(file_path).await?).await?
1015                }
1016                None => vec![],
1017            },
1018        }))
1019    }
1020}
1021
1022#[turbo_tasks::value(serialization = "skip")]
1023#[derive(Clone, Debug, PartialOrd, Ord)]
1024pub struct PlainIssueSource {
1025    pub asset: ReadRef<PlainSource>,
1026    pub range: Option<(SourcePos, SourcePos)>,
1027}
1028
1029#[turbo_tasks::value(serialization = "skip")]
1030#[derive(Clone, Debug, PartialOrd, Ord)]
1031pub struct PlainSource {
1032    pub ident: RcStr,
1033    pub file_path: RcStr,
1034    #[turbo_tasks(debug_ignore)]
1035    pub content: ReadRef<FileContent>,
1036}
1037
1038#[turbo_tasks::value_impl]
1039impl PlainSource {
1040    #[turbo_tasks::function]
1041    pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
1042        // Defensively read the content, an error there should not prevent all issue
1043        // formatting.  Best practice is for `content` to return `NotFound` instead of
1044        // an error.
1045        let content = if let Ok(asset_content) = asset.content().await
1046            && let AssetContent::File(file_content) = &*asset_content
1047            && let Ok(file_content) = file_content.await
1048        {
1049            file_content
1050        } else {
1051            ReadRef::new_owned(FileContent::NotFound)
1052        };
1053        let ident = asset.ident();
1054
1055        Ok(PlainSource {
1056            ident: ident.to_string().owned().await?,
1057            file_path: ident.await?.path.to_string_ref().await?,
1058            content,
1059        }
1060        .cell())
1061    }
1062}
1063
1064#[turbo_tasks::value_trait]
1065pub trait IssueReporter {
1066    /// Reports issues to the user (e.g. to stdio). Returns whether fatal
1067    /// (program-ending) issues were present.
1068    ///
1069    /// # Arguments:
1070    ///
1071    /// * `source` - The root [Vc] from which issues are traced. Can be used by implementers to
1072    ///   determine which issues are new.  This must be derived from the OperationVc so issues can
1073    ///   be collected.
1074    /// * `min_failing_severity` - The minimum Vc<[IssueSeverity]>
1075    ///  The minimum issue severity level considered to fatally end the program.
1076    #[turbo_tasks::function]
1077    fn report_issues(
1078        self: Vc<Self>,
1079        source: TransientValue<RawVc>,
1080        min_failing_severity: IssueSeverity,
1081    ) -> Vc<bool>;
1082}
1083
1084pub trait CollectibleIssuesExt
1085where
1086    Self: Sized,
1087{
1088    /// Returns all issues from `source`
1089    ///
1090    /// Must be called in a turbo-task as this constructs a `cell`
1091    fn peek_issues(self) -> CapturedIssues;
1092
1093    /// Drops all issues from `source`
1094    ///
1095    /// This unemits the issues. They will not propagate up.
1096    fn drop_issues(self);
1097}
1098
1099impl<T> CollectibleIssuesExt for T
1100where
1101    T: CollectiblesSource + Copy + Send,
1102{
1103    fn peek_issues(self) -> CapturedIssues {
1104        CapturedIssues {
1105            issues: self.peek_collectibles(),
1106
1107            tracer: DelegatingImportTracer {
1108                delegates: self.peek_collectibles(),
1109            }
1110            .resolved_cell(),
1111        }
1112    }
1113
1114    fn drop_issues(self) {
1115        self.drop_collectibles::<Box<dyn Issue>>();
1116    }
1117}
1118
1119/// A helper function to print out issues to the console.
1120///
1121/// Must be called in a turbo-task as this constructs a `cell`
1122pub async fn handle_issues<T: Send>(
1123    source_op: OperationVc<T>,
1124    issue_reporter: Vc<Box<dyn IssueReporter>>,
1125    min_failing_severity: IssueSeverity,
1126    path: Option<&str>,
1127    operation: Option<&str>,
1128) -> Result<()> {
1129    let source_vc = source_op.connect();
1130    let _ = source_op.resolve().strongly_consistent().await?;
1131
1132    let has_fatal = issue_reporter.report_issues(
1133        TransientValue::new(Vc::into_raw(source_vc)),
1134        min_failing_severity,
1135    );
1136
1137    if *has_fatal.await? {
1138        let mut message = "Fatal issue(s) occurred".to_owned();
1139        if let Some(path) = path.as_ref() {
1140            message += &format!(" in {path}");
1141        };
1142        if let Some(operation) = operation.as_ref() {
1143            message += &format!(" ({operation})");
1144        };
1145
1146        bail!(message)
1147    } else {
1148        Ok(())
1149    }
1150}
1151
1152fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1153    match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1154        Ok(i) => SourcePos {
1155            line: i as u32,
1156            column: 0,
1157        },
1158        Err(i) => {
1159            if i == 0 {
1160                SourcePos {
1161                    line: 0,
1162                    column: offset,
1163                }
1164            } else {
1165                let line = &lines[i - 1];
1166                SourcePos {
1167                    line: (i - 1) as u32,
1168                    column: min(line.content.len() as u32, offset - line.bytes_offset),
1169                }
1170            }
1171        }
1172    }
1173}
1174
1175fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1176    let line = &lines[pos.line as usize];
1177    line.bytes_offset + pos.column
1178}