turbopack_core/issue/
mod.rs

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