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#[derive(Clone, Debug, PartialOrd, Ord, DeterministicHash)]
84#[turbo_tasks::value(shared)]
85pub enum StyledString {
86 Line(Vec<StyledString>),
90 Stack(Vec<StyledString>),
93 Text(RcStr),
95 Code(RcStr),
98 Strong(RcStr),
100}
101
102#[turbo_tasks::value_trait]
103pub trait Issue {
104 fn severity(self: Vc<Self>) -> Vc<IssueSeverity> {
107 IssueSeverity::Error.into()
108 }
109
110 fn file_path(self: Vc<Self>) -> Vc<FileSystemPath>;
113
114 fn stage(self: Vc<Self>) -> Vc<IssueStage>;
117
118 fn title(self: Vc<Self>) -> Vc<StyledString>;
123
124 fn description(self: Vc<Self>) -> Vc<OptionStyledString> {
128 Vc::cell(None)
129 }
130
131 fn detail(self: Vc<Self>) -> Vc<OptionStyledString> {
135 Vc::cell(None)
136 }
137
138 fn documentation_link(self: Vc<Self>) -> Vc<RcStr> {
141 Vc::<RcStr>::default()
142 }
143
144 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 #[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#[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 pub fn is_empty_ref(&self) -> bool {
386 self.issues.is_empty()
387 }
388
389 #[allow(clippy::len_without_is_empty)]
391 pub fn len(&self) -> usize {
392 self.issues.len()
393 }
394
395 pub fn iter(&self) -> impl Iterator<Item = ResolvedVc<Box<dyn Issue>>> + '_ {
397 self.issues.iter().copied()
398 }
399
400 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#[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 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 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 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 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 pub fn file_path(&self) -> Vc<FileSystemPath> {
555 self.source.ident().path()
556 }
557}
558
559impl IssueSource {
560 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 Load,
646 SourceTransform,
647 Parse,
648 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 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 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 #[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 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 async fn peek_issues_with_path(self) -> Result<CapturedIssues>;
873
874 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}