Skip to main content

turbopack_core/issue/
mod.rs

1pub mod analyze;
2pub mod code_gen;
3pub mod module;
4pub mod resolve;
5
6use std::{
7    cmp::min,
8    fmt::{Display, Formatter},
9};
10
11use anyhow::{Result, anyhow};
12use auto_hash_map::AutoSet;
13use bincode::{Decode, Encode};
14use serde::{Deserialize, Serialize};
15use turbo_esregex::EsRegex;
16use turbo_rcstr::RcStr;
17use turbo_tasks::{
18    CollectiblesSource, IntoTraitRef, NonLocalValue, OperationVc, RawVc, ReadRef, ResolvedVc,
19    TaskInput, TransientValue, TryFlatJoinIterExt, TryJoinIterExt, Upcast, ValueDefault,
20    ValueToString, Vc, emit, trace::TraceRawVcs,
21};
22use turbo_tasks_fs::{
23    FileContent, FileLine, FileLinesContent, FileSystem, FileSystemPath, glob::Glob,
24    json::UnparsableJson,
25};
26use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
27
28use crate::{
29    asset::{Asset, AssetContent},
30    condition::ContextCondition,
31    ident::{AssetIdent, Layer},
32    source::Source,
33    source_map::{GenerateSourceMap, SourceMap, TokenWithSource},
34    source_pos::SourcePos,
35};
36
37#[turbo_tasks::value(shared)]
38#[derive(
39    PartialOrd, Ord, Copy, Clone, Hash, Debug, DeterministicHash, TaskInput, Serialize, Deserialize,
40)]
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#[turbo_tasks::value_trait]
130pub trait Issue {
131    /// Severity allows the user to filter out unimportant issues, with Bug
132    /// being the highest priority and Info being the lowest.
133    fn severity(&self) -> IssueSeverity {
134        IssueSeverity::Error
135    }
136
137    /// The file path that generated the issue, displayed to the user as message
138    /// header.
139    #[turbo_tasks::function]
140    fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
141
142    /// The stage of the compilation process at which the issue occurred. This
143    /// is used to sort issues.
144    #[turbo_tasks::function]
145    fn stage(self: Vc<Self>) -> Vc<IssueStage>;
146
147    /// The issue title should be descriptive of the issue, but should be a
148    /// single line. This is displayed to the user directly under the issue
149    /// header.
150    // TODO add Vc<StyledString>
151    #[turbo_tasks::function]
152    fn title(self: Vc<Self>) -> Vc<StyledString>;
153
154    /// A more verbose message of the issue, appropriate for providing multiline
155    /// information of the issue.
156    // TODO add Vc<StyledString>
157    #[turbo_tasks::function]
158    fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
159        Vc::cell(None)
160    }
161
162    /// Full details of the issue, appropriate for providing debug level
163    /// information. Only displayed if the user explicitly asks for detailed
164    /// messages (not to be confused with severity).
165    #[turbo_tasks::function]
166    fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
167        Vc::cell(None)
168    }
169
170    /// A link to relevant documentation of the issue. Only displayed in console
171    /// if the user explicitly asks for detailed messages.
172    #[turbo_tasks::function]
173    fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
174        Vc::<RcStr>::default()
175    }
176
177    /// The source location that caused the issue. Eg, for a parsing error it
178    /// should point at the offending character. Displayed to the user alongside
179    /// the title/description.
180    #[turbo_tasks::function]
181    fn source(self: Vc<Self>) -> Vc<OptionIssueSource> {
182        Vc::cell(None)
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: Vec<IgnoreIssue>,
284}
285
286#[turbo_tasks::value_impl]
287impl IssueFilter {
288    /// A filter that lets everything through.
289    #[turbo_tasks::function]
290    pub fn everything() -> Vc<Self> {
291        IssueFilter {
292            severity: IssueSeverity::Info,
293            foreign_severity: IssueSeverity::Info,
294            ignore_rules: Vec::new(),
295        }
296        .cell()
297    }
298
299    /// Returns true if the issue is allowed by this filter.
300    #[turbo_tasks::function]
301    pub async fn matches(&self, issue: ResolvedVc<Box<dyn Issue>>) -> Result<Vc<bool>> {
302        let has_no_ignore_rules = self.ignore_rules.is_empty();
303        let is_everything = self.severity == IssueSeverity::Info
304            && self.foreign_severity == IssueSeverity::Info
305            && has_no_ignore_rules;
306
307        if is_everything {
308            return Ok(Vc::cell(true));
309        }
310
311        // Fetch the file path once — it's used by both severity and ignore-rule
312        // checks.
313        let file_path = issue.file_path().await?;
314
315        // Check severity first — this is cheap and avoids fetching
316        // title/description for issues that would be filtered out anyway.
317        let severity = issue.into_trait_ref().await?.severity();
318        // NOTE: Lower severities are _more_ severe
319        let severity_allowed = if severity <= self.severity || severity <= self.foreign_severity {
320            // we need to check the path to see if it is foreign or not.  Only await the
321            // path if it might possibly matter
322            if severity <= self.severity && severity <= self.foreign_severity {
323                // it matches no matter where the path is
324                true
325            } else if ContextCondition::InNodeModules.matches(&file_path) {
326                severity <= self.foreign_severity
327            } else {
328                severity <= self.severity
329            }
330        } else {
331            // it is too low severity to match either way
332            false
333        };
334
335        if !severity_allowed {
336            return Ok(Vc::cell(false));
337        }
338
339        // Check ignore rules — if any rule matches, the issue is dropped.
340        // Title and description are fetched lazily: only when a rule's path
341        // matches and the rule also specifies a title/description pattern.
342        if !has_no_ignore_rules {
343            let file_path_str = file_path.to_string();
344            let mut title_str: Option<String> = None;
345            let mut description_text: Option<Option<String>> = None;
346
347            for rule in &self.ignore_rules {
348                if !rule.path.matches(&file_path_str) {
349                    continue;
350                }
351                if let Some(ref title_pat) = rule.title {
352                    if title_str.is_none() {
353                        title_str = Some(issue.title().await?.to_unstyled_string());
354                    }
355                    if !title_pat.matches(title_str.as_deref().unwrap()) {
356                        continue;
357                    }
358                }
359                if let Some(ref desc_pat) = rule.description {
360                    if description_text.is_none() {
361                        let desc_opt = issue.description().await?;
362                        description_text = Some(match desc_opt.as_ref() {
363                            Some(desc_vc) => Some(desc_vc.await?.to_unstyled_string()),
364                            None => None,
365                        });
366                    }
367                    match description_text.as_ref().unwrap().as_deref() {
368                        Some(desc) if desc_pat.matches(desc) => {}
369                        _ => continue,
370                    }
371                }
372                // All specified fields matched — ignore this issue.
373                return Ok(Vc::cell(false));
374            }
375        }
376
377        Ok(Vc::cell(true))
378    }
379}
380
381impl IssueFilter {
382    /// Construct a filter with the standard warning/foreign-error severities.
383    pub fn warnings_and_foreign_errors() -> Self {
384        IssueFilter {
385            severity: IssueSeverity::Warning,
386            foreign_severity: IssueSeverity::Error,
387            ignore_rules: Vec::new(),
388        }
389    }
390
391    /// Set the ignore rules for this filter.
392    pub fn with_ignore_rules(mut self, rules: Vec<IgnoreIssue>) -> Self {
393        self.ignore_rules = rules;
394        self
395    }
396}
397
398/// A list of issues captured with [`Issue::peek_issues_with_path`] and
399/// [`Issue::take_issues`].
400#[turbo_tasks::value(shared)]
401#[derive(Debug)]
402pub struct CapturedIssues {
403    issues: AutoSet<ResolvedVc<Box<dyn Issue>>>,
404    tracer: ResolvedVc<DelegatingImportTracer>,
405}
406
407#[turbo_tasks::value_impl]
408impl CapturedIssues {
409    #[turbo_tasks::function]
410    pub fn is_empty(&self) -> Vc<bool> {
411        Vc::cell(self.is_empty_ref())
412    }
413}
414
415impl CapturedIssues {
416    /// Returns true if there are no issues.
417    pub fn is_empty_ref(&self) -> bool {
418        self.issues.is_empty()
419    }
420
421    /// Returns the number of issues.
422    #[allow(clippy::len_without_is_empty)]
423    pub fn len(&self) -> usize {
424        self.issues.len()
425    }
426
427    /// Returns an iterator over the issues.
428    pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
429        self.issues.iter().copied()
430    }
431
432    // Returns all the issues as formatted `PlainIssues`.
433    pub async fn get_plain_issues(
434        &self,
435        filter: Vc<IssueFilter>,
436    ) -> Result<Vec<ReadRef<PlainIssue>>> {
437        let mut list = self
438            .issues
439            .iter()
440            .map(async |issue| {
441                if *filter.matches(**issue).await? {
442                    Ok(Some(
443                        PlainIssue::from_issue(**issue, Some(*self.tracer)).await?,
444                    ))
445                } else {
446                    Ok(None)
447                }
448            })
449            .try_flat_join()
450            .await?;
451        list.sort();
452        Ok(list)
453    }
454}
455
456#[derive(
457    Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
458)]
459pub struct IssueSource {
460    source: ResolvedVc<Box<dyn Source>>,
461    range: Option<SourceRange>,
462}
463
464/// The end position is the first character after the range
465#[derive(
466    Clone, Copy, Debug, PartialEq, Eq, Hash, TaskInput, TraceRawVcs, NonLocalValue, Encode, Decode,
467)]
468enum SourceRange {
469    LineColumn(SourcePos, SourcePos),
470    ByteOffset(u32, u32),
471}
472
473impl IssueSource {
474    // Sometimes we only have the source file that causes an issue, not the
475    // exact location, such as as in some generated code.
476    pub fn from_source_only(source: ResolvedVc<Box<dyn Source>>) -> Self {
477        IssueSource {
478            source,
479            range: None,
480        }
481    }
482
483    pub fn from_line_col(
484        source: ResolvedVc<Box<dyn Source>>,
485        start: SourcePos,
486        end: SourcePos,
487    ) -> Self {
488        IssueSource {
489            source,
490            range: Some(SourceRange::LineColumn(start, end)),
491        }
492    }
493
494    pub fn from_single_line_col(source: ResolvedVc<Box<dyn Source>>, pos: SourcePos) -> Self {
495        IssueSource {
496            source,
497            range: Some(SourceRange::LineColumn(
498                pos,
499                SourcePos {
500                    line: pos.line,
501                    // The end position is the first character after the range
502                    column: pos.column + 1,
503                },
504            )),
505        }
506    }
507
508    async fn into_plain(self) -> Result<PlainIssueSource> {
509        let Self { mut source, range } = self;
510
511        let range = if let Some(range) = range {
512            let mut range = match range {
513                SourceRange::LineColumn(start, end) => Some((start, end)),
514                SourceRange::ByteOffset(start, end) => {
515                    if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
516                        let start = find_line_and_column(lines.as_ref(), start);
517                        let end = find_line_and_column(lines.as_ref(), end);
518                        Some((start, end))
519                    } else {
520                        None
521                    }
522                }
523            };
524
525            // If we have a source map, map the line/column to the original source.
526            if let Some((start, end)) = range {
527                let mapped = source_pos(source, start, end).await?;
528
529                if let Some((mapped_source, start, end)) = mapped {
530                    range = Some((start, end));
531                    source = mapped_source;
532                }
533            }
534            range
535        } else {
536            None
537        };
538        Ok(PlainIssueSource {
539            asset: PlainSource::from_source(*source).await?,
540            range,
541        })
542    }
543
544    /// Create an [`IssueSource`] from an [`UnparsableJson`] error, using its
545    /// start/end location if available.
546    pub fn from_unparsable_json(
547        source: ResolvedVc<Box<dyn Source>>,
548        error: &UnparsableJson,
549    ) -> Self {
550        match (error.start_location, error.end_location) {
551            (None, None) => Self::from_source_only(source),
552            (Some((line, column)), None) | (None, Some((line, column))) => Self::from_line_col(
553                source,
554                SourcePos { line, column },
555                SourcePos { line, column },
556            ),
557            (Some((start_line, start_column)), Some((end_line, end_column))) => {
558                Self::from_line_col(
559                    source,
560                    SourcePos {
561                        line: start_line,
562                        column: start_column,
563                    },
564                    SourcePos {
565                        line: end_line,
566                        column: end_column,
567                    },
568                )
569            }
570        }
571    }
572
573    /// Create a [`IssueSource`] from byte offsets given by an swc ast node
574    /// span.
575    ///
576    /// Arguments:
577    ///
578    /// * `source`: The source code in which to look up the byte offsets.
579    /// * `start`: The start index of the span. Must use **1-based** indexing.
580    /// * `end`: The end index of the span. Must use **1-based** indexing.
581    pub fn from_swc_offsets(source: ResolvedVc<Box<dyn Source>>, start: u32, end: u32) -> Self {
582        IssueSource {
583            source,
584            range: match (start == 0, end == 0) {
585                (true, true) => None,
586                (false, false) => Some(SourceRange::ByteOffset(start - 1, end - 1)),
587                (false, true) => Some(SourceRange::ByteOffset(start - 1, start - 1)),
588                (true, false) => Some(SourceRange::ByteOffset(end - 1, end - 1)),
589            },
590        }
591    }
592
593    /// Returns an `IssueSource` representing a span of code in the `source`.
594    /// Positions are derived from byte offsets and stored as lines and columns.
595    /// Requires a binary search of the source text to perform this.
596    ///
597    /// Arguments:
598    ///
599    /// * `source`: The source code in which to look up the byte offsets.
600    /// * `start`: Byte offset into the source that the text begins. 0-based index and inclusive.
601    /// * `end`: Byte offset into the source that the text ends. 0-based index and exclusive.
602    pub async fn from_byte_offset(
603        source: ResolvedVc<Box<dyn Source>>,
604        start: u32,
605        end: u32,
606    ) -> Result<Self> {
607        Ok(IssueSource {
608            source,
609            range: if let FileLinesContent::Lines(lines) = &*source.content().lines().await? {
610                let start = find_line_and_column(lines.as_ref(), start);
611                let end = find_line_and_column(lines.as_ref(), end);
612                Some(SourceRange::LineColumn(start, end))
613            } else {
614                None
615            },
616        })
617    }
618
619    /// Returns the file path for the source file.
620    pub fn file_path(&self) -> Vc<FileSystemPath> {
621        self.source.ident().path()
622    }
623}
624
625impl IssueSource {
626    /// Returns bytes offsets corresponding the source range in the format used by swc's Spans.
627    pub async fn to_swc_offsets(&self) -> Result<Option<(u32, u32)>> {
628        Ok(match &self.range {
629            Some(range) => match range {
630                SourceRange::ByteOffset(start, end) => Some((*start + 1, *end + 1)),
631                SourceRange::LineColumn(start, end) => {
632                    if let FileLinesContent::Lines(lines) = &*self.source.content().lines().await? {
633                        let start = find_offset(lines.as_ref(), *start) + 1;
634                        let end = find_offset(lines.as_ref(), *end) + 1;
635                        Some((start, end))
636                    } else {
637                        None
638                    }
639                }
640            },
641            _ => None,
642        })
643    }
644}
645
646async fn source_pos(
647    source: ResolvedVc<Box<dyn Source>>,
648    start: SourcePos,
649    end: SourcePos,
650) -> Result<Option<(ResolvedVc<Box<dyn Source>>, SourcePos, SourcePos)>> {
651    let Some(generator) = ResolvedVc::try_sidecast::<Box<dyn GenerateSourceMap>>(source) else {
652        return Ok(None);
653    };
654
655    let srcmap = generator.generate_source_map();
656    let Some(srcmap) = &*SourceMap::new_from_rope_cached(srcmap).await? else {
657        return Ok(None);
658    };
659
660    let find = async |line: u32, col: u32| {
661        let TokenWithSource {
662            token,
663            source_content,
664        } = &srcmap.lookup_token_and_source(line, col).await?;
665
666        match token {
667            crate::source_map::Token::Synthetic(t) => anyhow::Ok((
668                SourcePos {
669                    line: t.generated_line as _,
670                    column: t.generated_column as _,
671                },
672                *source_content,
673            )),
674            crate::source_map::Token::Original(t) => anyhow::Ok((
675                SourcePos {
676                    line: t.original_line as _,
677                    column: t.original_column as _,
678                },
679                *source_content,
680            )),
681        }
682    };
683
684    let (start, content_1) = find(start.line, start.column).await?;
685    let (end, content_2) = find(end.line, end.column).await?;
686
687    let Some((content_1, content_2)) = content_1.zip(content_2) else {
688        return Ok(None);
689    };
690
691    if content_1 != content_2 {
692        return Ok(None);
693    }
694
695    Ok(Some((content_1, start, end)))
696}
697
698#[turbo_tasks::value(transparent)]
699pub struct OptionIssueSource(Option<IssueSource>);
700
701#[turbo_tasks::value(transparent)]
702pub struct OptionStyledString(Option<ResolvedVc<StyledString>>);
703
704// A structured reference to a file with module level details for displaying in an import trace
705#[derive(
706    Serialize,
707    PartialEq,
708    Eq,
709    PartialOrd,
710    Ord,
711    Clone,
712    Debug,
713    TraceRawVcs,
714    NonLocalValue,
715    DeterministicHash,
716)]
717#[serde(rename_all = "camelCase")]
718pub struct PlainTraceItem {
719    // The name of the filesystem
720    pub fs_name: RcStr,
721    // The root path of the filesystem, for constructing links
722    pub root_path: RcStr,
723    // The path of the file, relative to the filesystem root
724    pub path: RcStr,
725    // An optional label attached to the module that clarifies where in the module graph it is.
726    pub layer: Option<RcStr>,
727}
728
729impl PlainTraceItem {
730    async fn from_asset_ident(asset: ReadRef<AssetIdent>) -> Result<Self> {
731        // TODO(lukesandberg): How should we display paths? it would be good to display all paths
732        // relative to the cwd or the project root.
733        let fs_path = asset.path.clone();
734        let fs_name = fs_path.fs.to_string().owned().await?;
735        let root_path = fs_path.fs.root().await?.path.clone();
736        let path = fs_path.path.clone();
737        let layer = asset.layer.as_ref().map(Layer::user_friendly_name).cloned();
738        Ok(Self {
739            fs_name,
740            root_path,
741            path,
742            layer,
743        })
744    }
745}
746
747pub type PlainTrace = Vec<PlainTraceItem>;
748
749// Flatten and simplify this set of import traces into a simpler format for formatting.
750async fn into_plain_trace(traces: Vec<Vec<ReadRef<AssetIdent>>>) -> Result<Vec<PlainTrace>> {
751    let mut plain_traces = traces
752        .into_iter()
753        .map(|trace| async move {
754            let mut plain_trace = trace
755                .into_iter()
756                .filter(|asset| {
757                    // If there are nested assets, this is a synthetic module which is likely to be
758                    // confusing/distracting.  Just skip it.
759                    asset.assets.is_empty()
760                })
761                .map(PlainTraceItem::from_asset_ident)
762                .try_join()
763                .await?;
764
765            // After simplifying the trace, we may end up with apparent duplicates.
766            // Consider this example:
767            // Import trace:
768            // ./[project]/app/global.scss.css [app-client] (css) [app-client]
769            // ./[project]/app/layout.js [app-client] (ecmascript) [app-client]
770            // ./[project]/app/layout.js [app-rsc] (client reference proxy) [app-rsc]
771            // ./[project]/app/layout.js [app-rsc] (ecmascript) [app-rsc]
772            // ./[project]/app/layout.js [app-rsc] (ecmascript, Next.js Server Component) [app-rsc]
773            //
774            // In that case, there are an number of 'shim modules' that are inserted by next with
775            // different `modifiers` that are used to model the server->client hand off.  The
776            // simplification performed by `PlainTraceItem::from_asset_ident` drops these
777            // 'modifiers' and so we would end up with 'app/layout.js' appearing to be duplicated
778            // several times.  These modules are implementation details of the application so we
779            // just deduplicate them here.
780
781            plain_trace.dedup();
782
783            Ok(plain_trace)
784        })
785        .try_join()
786        .await?;
787
788    // Trim any empty traces and traces that only contain 1 item.  Showing a trace that points to
789    // the file with the issue is not useful.
790    plain_traces.retain(|t| t.len() > 1);
791    // Sort so the shortest traces come first, and break ties by the trace itself to ensure
792    // stability
793    plain_traces.sort_by(|a, b| {
794        // Sort by length first, so that shorter traces come first.
795        a.len().cmp(&b.len()).then_with(|| a.cmp(b))
796    });
797
798    // Now see if there are any overlaps
799    // If two of the traces overlap that means one is a suffix of another one.  Because we are
800    // computing shortest paths in the same graph and the shortest path algorithm we use is
801    // deterministic.
802    // Technically this is a quadratic algorithm since we need to compare each trace with all
803    // subsequent traces, however there are rarely more than 3 traces and certainly never more
804    // than 10.
805    if plain_traces.len() > 1 {
806        let mut i = 0;
807        while i < plain_traces.len() - 1 {
808            let mut j = plain_traces.len() - 1;
809            while j > i {
810                if plain_traces[j].ends_with(&plain_traces[i]) {
811                    // Remove the longer trace.
812                    // This typically happens due to things like server->client transitions where
813                    // the same file appears multiple times under different modules identifiers.
814                    // On the one hand the shorter trace is simpler, on the other hand the longer
815                    // trace might be more 'interesting' and even relevant.
816                    plain_traces.remove(j);
817                }
818                j -= 1;
819            }
820            i += 1;
821        }
822    }
823
824    Ok(plain_traces)
825}
826
827#[turbo_tasks::value(shared)]
828#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash, Serialize)]
829pub enum IssueStage {
830    Config,
831    AppStructure,
832    ProcessModule,
833    /// Read file.
834    Load,
835    SourceTransform,
836    Parse,
837    /// TODO: Add index of the transform
838    Transform,
839    Analysis,
840    Resolve,
841    Bindings,
842    CodeGen,
843    Unsupported,
844    Misc,
845    Other(RcStr),
846}
847
848impl Display for IssueStage {
849    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
850        match self {
851            IssueStage::Config => write!(f, "config"),
852            IssueStage::Resolve => write!(f, "resolve"),
853            IssueStage::ProcessModule => write!(f, "process module"),
854            IssueStage::Load => write!(f, "load"),
855            IssueStage::SourceTransform => write!(f, "source transform"),
856            IssueStage::Parse => write!(f, "parse"),
857            IssueStage::Transform => write!(f, "transform"),
858            IssueStage::Analysis => write!(f, "analysis"),
859            IssueStage::Bindings => write!(f, "bindings"),
860            IssueStage::CodeGen => write!(f, "code gen"),
861            IssueStage::Unsupported => write!(f, "unsupported"),
862            IssueStage::AppStructure => write!(f, "app structure"),
863            IssueStage::Misc => write!(f, "misc"),
864            IssueStage::Other(s) => write!(f, "{s}"),
865        }
866    }
867}
868
869#[turbo_tasks::value(serialization = "none")]
870#[derive(Clone, Debug, PartialOrd, Ord)]
871pub struct PlainIssue {
872    pub severity: IssueSeverity,
873    pub stage: IssueStage,
874
875    pub title: StyledString,
876    pub file_path: RcStr,
877
878    pub description: Option<StyledString>,
879    pub detail: Option<StyledString>,
880    pub documentation_link: RcStr,
881
882    pub source: Option<PlainIssueSource>,
883    pub import_traces: Vec<PlainTrace>,
884}
885
886fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
887    hasher.write_ref(&issue.severity);
888    hasher.write_ref(&issue.file_path);
889    hasher.write_ref(&issue.stage);
890    hasher.write_ref(&issue.title);
891    hasher.write_ref(&issue.description);
892    hasher.write_ref(&issue.detail);
893    hasher.write_ref(&issue.documentation_link);
894
895    if let Some(source) = &issue.source {
896        hasher.write_value(1_u8);
897        // I'm assuming we don't need to hash the contents. Not 100% correct, but
898        // probably 99%.
899        hasher.write_ref(&source.range);
900    } else {
901        hasher.write_value(0_u8);
902    }
903
904    if full {
905        hasher.write_ref(&issue.import_traces);
906    }
907}
908
909impl PlainIssue {
910    /// We need deduplicate issues that can come from unique paths, but
911    /// represent the same underlying problem. Eg, a parse error for a file
912    /// that is compiled in both client and server contexts.
913    ///
914    /// Passing [full] will also hash any sub-issues and processing paths. While
915    /// useful for generating exact matching hashes, it's possible for the
916    /// same issue to pass from multiple processing paths, making for overly
917    /// verbose logging.
918    pub fn internal_hash_ref(&self, full: bool) -> u64 {
919        let mut hasher = Xxh3Hash64Hasher::new();
920        hash_plain_issue(self, &mut hasher, full);
921        hasher.finish()
922    }
923}
924
925#[turbo_tasks::value_impl]
926impl PlainIssue {
927    /// Translate an [Issue] into a [PlainIssue]. A more regular structure suitable for printing and
928    /// serialization.
929    #[turbo_tasks::function]
930    pub async fn from_issue(
931        issue: ResolvedVc<Box<dyn Issue>>,
932        import_tracer: Option<ResolvedVc<DelegatingImportTracer>>,
933    ) -> Result<Vc<Self>> {
934        let description: Option<StyledString> = match *issue.description().await? {
935            Some(description) => Some(description.owned().await?),
936            None => None,
937        };
938        let detail = match *issue.detail().await? {
939            Some(detail) => Some(detail.owned().await?),
940            None => None,
941        };
942        let trait_ref = issue.into_trait_ref().await?;
943
944        let severity = trait_ref.severity();
945
946        Ok(Self::cell(Self {
947            severity,
948            file_path: issue.file_path().to_string().owned().await?,
949            stage: issue.stage().owned().await?,
950            title: issue.title().owned().await?,
951            description,
952            detail,
953            documentation_link: issue.documentation_link().owned().await?,
954            source: {
955                if let Some(s) = &*issue.source().await? {
956                    Some(s.into_plain().await?)
957                } else {
958                    None
959                }
960            },
961            import_traces: match import_tracer {
962                Some(tracer) => {
963                    into_plain_trace(
964                        tracer
965                            .await?
966                            .get_traces(issue.file_path().owned().await?)
967                            .await?,
968                    )
969                    .await?
970                }
971                None => vec![],
972            },
973        }))
974    }
975}
976
977#[turbo_tasks::value(serialization = "none")]
978#[derive(Clone, Debug, PartialOrd, Ord)]
979pub struct PlainIssueSource {
980    pub asset: ReadRef<PlainSource>,
981    pub range: Option<(SourcePos, SourcePos)>,
982}
983
984#[turbo_tasks::value(serialization = "none")]
985#[derive(Clone, Debug, PartialOrd, Ord)]
986pub struct PlainSource {
987    pub ident: ReadRef<RcStr>,
988    #[turbo_tasks(debug_ignore)]
989    pub content: ReadRef<FileContent>,
990}
991
992#[turbo_tasks::value_impl]
993impl PlainSource {
994    #[turbo_tasks::function]
995    pub async fn from_source(asset: ResolvedVc<Box<dyn Source>>) -> Result<Vc<PlainSource>> {
996        let asset_content = asset.content().await?;
997        let content = match *asset_content {
998            AssetContent::File(file_content) => file_content.await?,
999            AssetContent::Redirect { .. } => ReadRef::new_owned(FileContent::NotFound),
1000        };
1001
1002        Ok(PlainSource {
1003            ident: asset.ident().to_string().await?,
1004            content,
1005        }
1006        .cell())
1007    }
1008}
1009
1010#[turbo_tasks::value_trait]
1011pub trait IssueReporter {
1012    /// Reports issues to the user (e.g. to stdio). Returns whether fatal
1013    /// (program-ending) issues were present.
1014    ///
1015    /// # Arguments:
1016    ///
1017    /// * `source` - The root [Vc] from which issues are traced. Can be used by implementers to
1018    ///   determine which issues are new.  This must be derived from the OperationVc so issues can
1019    ///   be collected.
1020    /// * `min_failing_severity` - The minimum Vc<[IssueSeverity]>
1021    ///  The minimum issue severity level considered to fatally end the program.
1022    #[turbo_tasks::function]
1023    fn report_issues(
1024        self: Vc<Self>,
1025        source: TransientValue<RawVc>,
1026        min_failing_severity: IssueSeverity,
1027    ) -> Vc<bool>;
1028}
1029
1030pub trait CollectibleIssuesExt
1031where
1032    Self: Sized,
1033{
1034    /// Returns all issues from `source`
1035    ///
1036    /// Must be called in a turbo-task as this constructs a `cell`
1037    fn peek_issues(self) -> CapturedIssues;
1038
1039    /// Drops all issues from `source`
1040    ///
1041    /// This unemits the issues. They will not propagate up.
1042    fn drop_issues(self);
1043}
1044
1045impl<T> CollectibleIssuesExt for T
1046where
1047    T: CollectiblesSource + Copy + Send,
1048{
1049    fn peek_issues(self) -> CapturedIssues {
1050        CapturedIssues {
1051            issues: self.peek_collectibles(),
1052
1053            tracer: DelegatingImportTracer {
1054                delegates: self.peek_collectibles(),
1055            }
1056            .resolved_cell(),
1057        }
1058    }
1059
1060    fn drop_issues(self) {
1061        self.drop_collectibles::<Box<dyn Issue>>();
1062    }
1063}
1064
1065/// A helper function to print out issues to the console.
1066///
1067/// Must be called in a turbo-task as this constructs a `cell`
1068pub async fn handle_issues<T: Send>(
1069    source_op: OperationVc<T>,
1070    issue_reporter: Vc<Box<dyn IssueReporter>>,
1071    min_failing_severity: IssueSeverity,
1072    path: Option<&str>,
1073    operation: Option<&str>,
1074) -> Result<()> {
1075    let source_vc = source_op.connect();
1076    let _ = source_op.resolve_strongly_consistent().await?;
1077
1078    let has_fatal = issue_reporter.report_issues(
1079        TransientValue::new(Vc::into_raw(source_vc)),
1080        min_failing_severity,
1081    );
1082
1083    if *has_fatal.await? {
1084        let mut message = "Fatal issue(s) occurred".to_owned();
1085        if let Some(path) = path.as_ref() {
1086            message += &format!(" in {path}");
1087        };
1088        if let Some(operation) = operation.as_ref() {
1089            message += &format!(" ({operation})");
1090        };
1091
1092        Err(anyhow!(message))
1093    } else {
1094        Ok(())
1095    }
1096}
1097
1098fn find_line_and_column(lines: &[FileLine], offset: u32) -> SourcePos {
1099    match lines.binary_search_by(|line| line.bytes_offset.cmp(&offset)) {
1100        Ok(i) => SourcePos {
1101            line: i as u32,
1102            column: 0,
1103        },
1104        Err(i) => {
1105            if i == 0 {
1106                SourcePos {
1107                    line: 0,
1108                    column: offset,
1109                }
1110            } else {
1111                let line = &lines[i - 1];
1112                SourcePos {
1113                    line: (i - 1) as u32,
1114                    column: min(line.content.len() as u32, offset - line.bytes_offset),
1115                }
1116            }
1117        }
1118    }
1119}
1120
1121fn find_offset(lines: &[FileLine], pos: SourcePos) -> u32 {
1122    let line = &lines[pos.line as usize];
1123    line.bytes_offset + pos.column
1124}