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