Skip to content

Commit 428a7df

Browse files
authored
Better dotenv error messages (vercel/turborepo#4464)
The issue message emitted by `TryDotenvProcessEnv` was pretty bad: `Execution of TryDotenvProcessEnv::read_all failed`. This error message comes from #3550, which attaches the calling function as the context for every failed turbo function. This PR accomplishes 2 main things: 1. Expose an explicit method determining whether a the `DotenvEnvProcess`'s prior or current env failed 2. Detects a current env failure and uses the error's root cause for the issue message. <img width="961" alt="Screen Shot 2023-04-04 at 7 29 52 PM" src="https://user-images.githubusercontent.com/112982/229944525-d39dbe87-778a-4421-9bc8-632924cd3782.png"> Fixes WEB-851 Tests: #47937
1 parent ebe4dae commit 428a7df

File tree

11 files changed

+126
-83
lines changed

11 files changed

+126
-83
lines changed

crates/turbo-tasks-env/src/dotenv.rs

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,22 @@ impl DotenvProcessEnvVc {
2222
pub fn new(prior: Option<ProcessEnvVc>, path: FileSystemPathVc) -> Self {
2323
DotenvProcessEnv { prior, path }.cell()
2424
}
25-
}
2625

27-
#[turbo_tasks::value_impl]
28-
impl ProcessEnv for DotenvProcessEnv {
2926
#[turbo_tasks::function]
30-
async fn read_all(&self) -> Result<EnvMapVc> {
31-
let prior = if let Some(p) = self.prior {
32-
Some(p.read_all().await?)
33-
} else {
34-
None
35-
};
36-
let empty = IndexMap::new();
37-
let prior = prior.as_deref().unwrap_or(&empty);
27+
pub async fn read_prior(self) -> Result<EnvMapVc> {
28+
let this = self.await?;
29+
match this.prior {
30+
None => Ok(EnvMapVc::empty()),
31+
Some(p) => Ok(p.read_all()),
32+
}
33+
}
34+
35+
#[turbo_tasks::function]
36+
pub async fn read_all_with_prior(self, prior: EnvMapVc) -> Result<EnvMapVc> {
37+
let this = self.await?;
38+
let prior = prior.await?;
3839

39-
let file = self.path.read().await?;
40+
let file = this.path.read().await?;
4041
if let FileContent::Content(f) = &*file {
4142
let res;
4243
let vars;
@@ -48,7 +49,7 @@ impl ProcessEnv for DotenvProcessEnv {
4849
// state.
4950
let initial = env::vars().collect();
5051

51-
restore_env(&initial, prior, &lock);
52+
restore_env(&initial, &prior, &lock);
5253

5354
// from_read will load parse and evalute the Read, and set variables
5455
// into the global env. If a later dotenv defines an already defined
@@ -59,20 +60,29 @@ impl ProcessEnv for DotenvProcessEnv {
5960
restore_env(&vars, &initial, &lock);
6061
}
6162

62-
if res.is_err() {
63-
res.context(anyhow!(
63+
if let Err(e) = res {
64+
return Err(e).context(anyhow!(
6465
"unable to read {} for env vars",
65-
self.path.to_string().await?
66-
))?;
66+
this.path.to_string().await?
67+
));
6768
}
6869

6970
Ok(EnvMapVc::cell(vars))
7071
} else {
71-
Ok(EnvMapVc::cell(prior.clone()))
72+
Ok(EnvMapVc::cell(prior.clone_value()))
7273
}
7374
}
7475
}
7576

77+
#[turbo_tasks::value_impl]
78+
impl ProcessEnv for DotenvProcessEnv {
79+
#[turbo_tasks::function]
80+
async fn read_all(self_vc: DotenvProcessEnvVc) -> Result<EnvMapVc> {
81+
let prior = self_vc.read_prior();
82+
Ok(self_vc.read_all_with_prior(prior))
83+
}
84+
}
85+
7686
/// Restores the global env variables to mirror `to`.
7787
fn restore_env(
7888
from: &IndexMap<String, String>,

crates/turbo-tasks/src/util.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ impl<'de> Deserialize<'de> for SharedError {
8484
}
8585
}
8686

87+
impl Deref for SharedError {
88+
type Target = Arc<Error>;
89+
fn deref(&self) -> &Self::Target {
90+
&self.inner
91+
}
92+
}
93+
8794
pub struct FormatDuration(pub Duration);
8895

8996
impl Display for FormatDuration {

crates/turbopack-cli-utils/src/issue.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ impl IssueReporter for ConsoleUi {
369369
.iter_with_shortest_path()
370370
.map(|(issue, path)| async move {
371371
let plain_issue = issue.into_plain(path);
372-
let id = plain_issue.internal_hash().await?;
372+
let id = plain_issue.internal_hash(false).await?;
373373
Ok((plain_issue.await?, *id))
374374
})
375375
.try_join()

crates/turbopack-core/src/issue/mod.rs

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -503,42 +503,67 @@ pub struct PlainIssue {
503503
pub processing_path: PlainIssueProcessingPathReadRef,
504504
}
505505

506+
fn hash_plain_issue(issue: &PlainIssue, hasher: &mut Xxh3Hash64Hasher, full: bool) {
507+
hasher.write_ref(&issue.severity);
508+
hasher.write_ref(&issue.context);
509+
hasher.write_ref(&issue.category);
510+
hasher.write_ref(&issue.title);
511+
hasher.write_ref(
512+
// Normalize syspaths from Windows. These appear in stack traces.
513+
&issue.description.replace('\\', "/"),
514+
);
515+
hasher.write_ref(&issue.detail);
516+
hasher.write_ref(&issue.documentation_link);
517+
518+
if let Some(source) = &issue.source {
519+
hasher.write_value(1_u8);
520+
// I'm assuming we don't need to hash the contents. Not 100% correct, but
521+
// probably 99%.
522+
hasher.write_ref(&source.start);
523+
hasher.write_ref(&source.end);
524+
} else {
525+
hasher.write_value(0_u8);
526+
}
527+
528+
if full {
529+
hasher.write_value(issue.sub_issues.len());
530+
for i in &issue.sub_issues {
531+
hash_plain_issue(i, hasher, full);
532+
}
533+
534+
hasher.write_ref(&issue.processing_path);
535+
}
536+
}
537+
506538
impl PlainIssue {
507539
/// We need deduplicate issues that can come from unique paths, but
508540
/// represent the same underlying problem. Eg, a parse error for a file
509541
/// that is compiled in both client and server contexts.
510-
pub fn internal_hash(&self) -> u64 {
542+
///
543+
/// Passing [full] will also hash any sub-issues and processing paths. While
544+
/// useful for generating exact matching hashes, it's possible for the
545+
/// same issue to pass from multiple processing paths, making for overly
546+
/// verbose logging.
547+
pub fn internal_hash(&self, full: bool) -> u64 {
511548
let mut hasher = Xxh3Hash64Hasher::new();
512-
hasher.write_ref(&self.severity);
513-
hasher.write_ref(&self.context);
514-
hasher.write_ref(&self.category);
515-
hasher.write_ref(&self.title);
516-
hasher.write_ref(
517-
// Normalize syspaths from Windows. These appear in stack traces.
518-
&self.description.replace('\\', "/"),
519-
);
520-
hasher.write_ref(&self.detail);
521-
hasher.write_ref(&self.documentation_link);
522-
523-
if let Some(source) = &self.source {
524-
hasher.write_value(1_u8);
525-
// I'm assuming we don't need to hash the contents. Not 100% correct, but
526-
// probably 99%.
527-
hasher.write_ref(&source.start);
528-
hasher.write_ref(&source.end);
529-
} else {
530-
hasher.write_value(0_u8);
531-
}
532-
549+
hash_plain_issue(self, &mut hasher, full);
533550
hasher.finish()
534551
}
535552
}
536553

537554
#[turbo_tasks::value_impl]
538555
impl PlainIssueVc {
556+
/// We need deduplicate issues that can come from unique paths, but
557+
/// represent the same underlying problem. Eg, a parse error for a file
558+
/// that is compiled in both client and server contexts.
559+
///
560+
/// Passing [full] will also hash any sub-issues and processing paths. While
561+
/// useful for generating exact matching hashes, it's possible for the
562+
/// same issue to pass from multiple processing paths, making for overly
563+
/// verbose logging.
539564
#[turbo_tasks::function]
540-
pub async fn internal_hash(self) -> Result<U64Vc> {
541-
Ok(U64Vc::cell(self.await?.internal_hash()))
565+
pub async fn internal_hash(self, full: bool) -> Result<U64Vc> {
566+
Ok(U64Vc::cell(self.await?.internal_hash(full)))
542567
}
543568
}
544569

@@ -631,11 +656,11 @@ impl PlainAssetVc {
631656
}
632657

633658
#[turbo_tasks::value(transparent, serialization = "none")]
634-
#[derive(Clone, Debug)]
659+
#[derive(Clone, Debug, DeterministicHash)]
635660
pub struct PlainIssueProcessingPath(Option<Vec<PlainIssueProcessingPathItemReadRef>>);
636661

637662
#[turbo_tasks::value(serialization = "none")]
638-
#[derive(Clone, Debug)]
663+
#[derive(Clone, Debug, DeterministicHash)]
639664
pub struct PlainIssueProcessingPathItem {
640665
pub context: Option<StringReadRef>,
641666
pub description: StringReadRef,
Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,61 @@
1-
use std::future::IntoFuture;
2-
31
use anyhow::Result;
4-
use turbo_tasks::primitives::{OptionStringVc, StringVc};
2+
use turbo_tasks::primitives::StringVc;
53
use turbo_tasks_env::{DotenvProcessEnvVc, EnvMapVc, ProcessEnv, ProcessEnvVc};
64
use turbo_tasks_fs::FileSystemPathVc;
75

86
use crate::ProcessEnvIssue;
97

108
#[turbo_tasks::value]
119
pub struct TryDotenvProcessEnv {
10+
dotenv: DotenvProcessEnvVc,
1211
prior: ProcessEnvVc,
1312
path: FileSystemPathVc,
1413
}
1514

16-
impl TryDotenvProcessEnv {
17-
async fn with_issue<T, V: Copy + IntoFuture<Output = Result<T>>>(
18-
&self,
19-
op: impl Fn(ProcessEnvVc) -> V,
20-
) -> Result<V> {
21-
let r = op(DotenvProcessEnvVc::new(Some(self.prior), self.path).as_process_env());
22-
match r.await {
23-
Ok(_) => Ok(r),
24-
Err(e) => {
25-
let r = op(self.prior);
26-
// If the prior process env also reports an error we don't want to report our
27-
// issue
28-
r.await?;
29-
30-
ProcessEnvIssue {
31-
path: self.path,
32-
description: StringVc::cell(e.to_string()),
33-
}
34-
.cell()
35-
.as_issue()
36-
.emit();
37-
Ok(r)
38-
}
39-
}
40-
}
41-
}
42-
4315
#[turbo_tasks::value_impl]
4416
impl TryDotenvProcessEnvVc {
4517
#[turbo_tasks::function]
4618
pub fn new(prior: ProcessEnvVc, path: FileSystemPathVc) -> Self {
47-
TryDotenvProcessEnv { prior, path }.cell()
19+
let dotenv = DotenvProcessEnvVc::new(Some(prior), path);
20+
TryDotenvProcessEnv {
21+
dotenv,
22+
prior,
23+
path,
24+
}
25+
.cell()
4826
}
4927
}
5028

5129
#[turbo_tasks::value_impl]
5230
impl ProcessEnv for TryDotenvProcessEnv {
5331
#[turbo_tasks::function]
5432
async fn read_all(&self) -> Result<EnvMapVc> {
55-
self.with_issue(|e| e.read_all()).await
56-
}
33+
let dotenv = self.dotenv;
34+
let prior = dotenv.read_prior();
5735

58-
#[turbo_tasks::function]
59-
async fn read(&self, name: &str) -> Result<OptionStringVc> {
60-
self.with_issue(|e| e.read(name)).await
36+
// Ensure prior succeeds. If it doesn't, then we don't want to attempt to read
37+
// the dotenv file (and potentially emit an Issue), just trust that the prior
38+
// will have emitted its own.
39+
prior.await?;
40+
41+
let vars = dotenv.read_all_with_prior(prior);
42+
match vars.await {
43+
Ok(_) => Ok(vars),
44+
Err(e) => {
45+
// If parsing the dotenv file fails (but getting the prior value didn't), then
46+
// we want to emit an Issue and fall back to the prior's read.
47+
ProcessEnvIssue {
48+
path: self.path,
49+
// read_all_with_prior will wrap a current error with a context containing the
50+
// failing file, which we don't really care about (we report the filepath as the
51+
// Issue context, not the description). So extract the real error.
52+
description: StringVc::cell(e.root_cause().to_string()),
53+
}
54+
.cell()
55+
.as_issue()
56+
.emit();
57+
Ok(prior)
58+
}
59+
}
6160
}
6261
}

crates/turbopack-test-utils/src/snapshot.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub async fn snapshot_issues<
3131
let expected_issues = expected(issues_path).await?;
3232
let mut seen = HashSet::new();
3333
for (plain_issue, debug_string) in captured_issues.into_iter() {
34-
let hash = encode_hex(plain_issue.internal_hash());
34+
let hash = encode_hex(plain_issue.internal_hash(true));
3535

3636
let path = issues_path.join(&format!(
3737
"{}-{}.txt",
@@ -44,7 +44,9 @@ pub async fn snapshot_issues<
4444
.replace('?', "__q__"),
4545
&hash[0..6]
4646
));
47-
seen.insert(path);
47+
if !seen.insert(path) {
48+
continue;
49+
}
4850

4951
// Annoyingly, the PlainIssue.source -> PlainIssueSource.asset ->
5052
// PlainAsset.path -> FileSystemPath.fs -> DiskFileSystem.root changes

0 commit comments

Comments
 (0)