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