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