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