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