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