Skip to content

Commit 600a67b

Browse files
authored
Generate option value derivations in the rust parser. (#20535)
We use these to show the source of option values in help output. The derivations can be a lot of data, so we only produce them if asked to. Note that the derivations currently only show the source type (args, env, config, default) but not, for example, which config file a value came from. A future change may add this, but it will require some changes to the Source enum which are out of scope for this PR. Also: use maplit for easy hashmap literals in the test.
1 parent e37ecb7 commit 600a67b

File tree

9 files changed

+364
-73
lines changed

9 files changed

+364
-73
lines changed

src/rust/engine/Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/engine/client/src/client_tests.rs

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ async fn test_client_fingerprint_mismatch() {
4040
Env::new(HashMap::new()),
4141
None,
4242
true,
43+
false,
4344
)
4445
.unwrap();
4546
let error = pantsd::find_pantsd(&build_root, &options_parser)

src/rust/engine/client/src/main.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async fn execute(start: SystemTime) -> Result<i32, String> {
3232
let (env, dropped) = Env::capture_lossy();
3333
let env_items = (&env).into();
3434
let argv = env::args().collect::<Vec<_>>();
35-
let options_parser = OptionParser::new(Args::argv(), env, None, true)?;
35+
let options_parser = OptionParser::new(Args::argv(), env, None, true, false)?;
3636

3737
let use_pantsd = options_parser.parse_bool(&option_id!("pantsd"), true)?;
3838
if !use_pantsd.value {

src/rust/engine/options/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ publish = false
88
[dependencies]
99
lazy_static = { workspace = true }
1010
log = { workspace = true }
11+
maplit = { workspace = true }
1112
peg = { workspace = true }
1213
shellexpand = { workspace = true }
1314
toml = { workspace = true }

src/rust/engine/options/src/lib.rs

+120-38
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ mod types;
3333
use std::collections::{BTreeMap, HashMap, HashSet};
3434
use std::fmt::Debug;
3535
use std::hash::Hash;
36-
use std::ops::Deref;
3736
use std::os::unix::ffi::OsStrExt;
3837
use std::path;
3938
use std::path::Path;
@@ -56,7 +55,7 @@ pub use types::OptionType;
5655
// We only use this for parsing values in dicts, as in other cases we know that the type must
5756
// be some scalar or string, or a uniform list of one type of scalar or string, so we can
5857
// parse as such.
59-
#[derive(Debug, PartialEq)]
58+
#[derive(Clone, Debug, PartialEq)]
6059
pub enum Val {
6160
Bool(bool),
6261
Int(i64),
@@ -67,26 +66,26 @@ pub enum Val {
6766
}
6867

6968
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
70-
pub(crate) enum ListEditAction {
69+
pub enum ListEditAction {
7170
Replace,
7271
Add,
7372
Remove,
7473
}
7574

76-
#[derive(Debug, Eq, PartialEq)]
77-
pub(crate) struct ListEdit<T> {
75+
#[derive(Clone, Debug, Eq, PartialEq)]
76+
pub struct ListEdit<T> {
7877
pub action: ListEditAction,
7978
pub items: Vec<T>,
8079
}
8180

8281
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
83-
pub(crate) enum DictEditAction {
82+
pub enum DictEditAction {
8483
Replace,
8584
Add,
8685
}
8786

88-
#[derive(Debug, PartialEq)]
89-
pub(crate) struct DictEdit {
87+
#[derive(Clone, Debug, PartialEq)]
88+
pub struct DictEdit {
9089
pub action: DictEditAction,
9190
pub items: HashMap<String, Val>,
9291
}
@@ -200,20 +199,28 @@ pub enum Source {
200199

201200
#[derive(Debug)]
202201
pub struct OptionValue<T> {
202+
pub derivation: Option<Vec<(Source, T)>>,
203+
// Scalar options are always set from a single source, so we provide that
204+
// here, as it can be useful in user-facing messages.
203205
pub source: Source,
204206
pub value: T,
205207
}
206208

207-
impl<T> Deref for OptionValue<T> {
208-
type Target = T;
209+
#[derive(Debug)]
210+
pub struct ListOptionValue<T> {
211+
pub derivation: Option<Vec<(Source, Vec<ListEdit<T>>)>>,
212+
pub value: Vec<T>,
213+
}
209214

210-
fn deref(&self) -> &Self::Target {
211-
&self.value
212-
}
215+
#[derive(Debug)]
216+
pub struct DictOptionValue {
217+
pub derivation: Option<Vec<(Source, Vec<DictEdit>)>>,
218+
pub value: HashMap<String, Val>,
213219
}
214220

215221
pub struct OptionParser {
216222
sources: BTreeMap<Source, Rc<dyn OptionsSource>>,
223+
include_derivation: bool,
217224
}
218225

219226
impl OptionParser {
@@ -224,6 +231,7 @@ impl OptionParser {
224231
env: Env,
225232
config_paths: Option<Vec<&str>>,
226233
allow_pantsrc: bool,
234+
include_derivation: bool,
227235
) -> Result<OptionParser, String> {
228236
let buildroot = BuildRoot::find()?;
229237
let buildroot_string = String::from_utf8(buildroot.as_os_str().as_bytes().to_vec())
@@ -246,6 +254,7 @@ impl OptionParser {
246254
sources.insert(Source::Flag, Rc::new(args));
247255
let mut parser = OptionParser {
248256
sources: sources.clone(),
257+
include_derivation: false,
249258
};
250259

251260
fn path_join(a: &str, b: &str) -> String {
@@ -256,10 +265,12 @@ impl OptionParser {
256265
Some(paths) => paths.iter().map(|s| s.to_string()).collect(),
257266
None => {
258267
let default_config_path = path_join(&buildroot_string, "pants.toml");
259-
parser.parse_string_list(
260-
&option_id!("pants", "config", "files"),
261-
&[&default_config_path],
262-
)?
268+
parser
269+
.parse_string_list(
270+
&option_id!("pants", "config", "files"),
271+
&[&default_config_path],
272+
)?
273+
.value
263274
}
264275
};
265276

@@ -285,17 +296,21 @@ impl OptionParser {
285296
sources.insert(Source::Config, Rc::new(config.clone()));
286297
parser = OptionParser {
287298
sources: sources.clone(),
299+
include_derivation: false,
288300
};
289301

290-
if allow_pantsrc && *parser.parse_bool(&option_id!("pantsrc"), true)? {
291-
for rcfile in parser.parse_string_list(
292-
&option_id!("pantsrc", "files"),
293-
&[
294-
"/etc/pantsrc",
295-
shellexpand::tilde("~/.pants.rc").as_ref(),
296-
".pants.rc",
297-
],
298-
)? {
302+
if allow_pantsrc && parser.parse_bool(&option_id!("pantsrc"), true)?.value {
303+
for rcfile in parser
304+
.parse_string_list(
305+
&option_id!("pantsrc", "files"),
306+
&[
307+
"/etc/pantsrc",
308+
shellexpand::tilde("~/.pants.rc").as_ref(),
309+
".pants.rc",
310+
],
311+
)?
312+
.value
313+
{
299314
let rcfile_path = Path::new(&rcfile);
300315
if rcfile_path.exists() {
301316
let rc_config = Config::parse(&[rcfile_path], &seed_values)?;
@@ -304,7 +319,10 @@ impl OptionParser {
304319
}
305320
}
306321
sources.insert(Source::Config, Rc::new(config));
307-
Ok(OptionParser { sources })
322+
Ok(OptionParser {
323+
sources,
324+
include_derivation,
325+
})
308326
}
309327

310328
#[allow(clippy::type_complexity)]
@@ -314,15 +332,27 @@ impl OptionParser {
314332
default: &T,
315333
getter: fn(&Rc<dyn OptionsSource>, &OptionId) -> Result<Option<T::Owned>, String>,
316334
) -> Result<OptionValue<T::Owned>, String> {
335+
let mut derivation = None;
336+
if self.include_derivation {
337+
let mut derivations = vec![(Source::Default, default.to_owned())];
338+
for (source_type, source) in self.sources.iter().rev() {
339+
if let Some(val) = getter(source, id)? {
340+
derivations.push((*source_type, val));
341+
}
342+
}
343+
derivation = Some(derivations);
344+
}
317345
for (source_type, source) in self.sources.iter() {
318346
if let Some(value) = getter(source, id)? {
319347
return Ok(OptionValue {
348+
derivation,
320349
source: *source_type,
321350
value,
322351
});
323352
}
324353
}
325354
Ok(OptionValue {
355+
derivation,
326356
source: Source::Default,
327357
value: default.to_owned(),
328358
})
@@ -349,14 +379,32 @@ impl OptionParser {
349379
}
350380

351381
#[allow(clippy::type_complexity)]
352-
fn parse_list<T>(
382+
fn parse_list<T: Clone>(
353383
&self,
354384
id: &OptionId,
355385
default: Vec<T>,
356386
getter: fn(&Rc<dyn OptionsSource>, &OptionId) -> Result<Option<Vec<ListEdit<T>>>, String>,
357387
remover: fn(&mut Vec<T>, &Vec<T>),
358-
) -> Result<Vec<T>, String> {
388+
) -> Result<ListOptionValue<T>, String> {
359389
let mut list = default;
390+
let mut derivation = None;
391+
if self.include_derivation {
392+
let mut derivations = vec![(
393+
Source::Default,
394+
vec![ListEdit {
395+
action: ListEditAction::Replace,
396+
items: list.clone(),
397+
}],
398+
)];
399+
for (source_type, source) in self.sources.iter().rev() {
400+
if let Some(list_edits) = getter(source, id)? {
401+
if !list_edits.is_empty() {
402+
derivations.push((*source_type, list_edits));
403+
}
404+
}
405+
}
406+
derivation = Some(derivations);
407+
}
360408
for (_source_type, source) in self.sources.iter().rev() {
361409
if let Some(list_edits) = getter(source, id)? {
362410
for list_edit in list_edits {
@@ -368,7 +416,10 @@ impl OptionParser {
368416
}
369417
}
370418
}
371-
Ok(list)
419+
Ok(ListOptionValue {
420+
derivation,
421+
value: list,
422+
})
372423
}
373424

374425
// For Eq+Hash types we can use a HashSet when computing removals, which will be avg O(N+M).
@@ -378,28 +429,40 @@ impl OptionParser {
378429
// However this is still more than fast enough, and inoculates us against a very unlikely
379430
// pathological case of a very large removal set.
380431
#[allow(clippy::type_complexity)]
381-
fn parse_list_hashable<T: Eq + Hash>(
432+
fn parse_list_hashable<T: Clone + Eq + Hash>(
382433
&self,
383434
id: &OptionId,
384435
default: Vec<T>,
385436
getter: fn(&Rc<dyn OptionsSource>, &OptionId) -> Result<Option<Vec<ListEdit<T>>>, String>,
386-
) -> Result<Vec<T>, String> {
437+
) -> Result<ListOptionValue<T>, String> {
387438
self.parse_list(id, default, getter, |list, remove| {
388439
let to_remove = remove.iter().collect::<HashSet<_>>();
389440
list.retain(|item| !to_remove.contains(item));
390441
})
391442
}
392443

393-
pub fn parse_bool_list(&self, id: &OptionId, default: &[bool]) -> Result<Vec<bool>, String> {
444+
pub fn parse_bool_list(
445+
&self,
446+
id: &OptionId,
447+
default: &[bool],
448+
) -> Result<ListOptionValue<bool>, String> {
394449
self.parse_list_hashable(id, default.to_vec(), |source, id| source.get_bool_list(id))
395450
}
396451

397-
pub fn parse_int_list(&self, id: &OptionId, default: &[i64]) -> Result<Vec<i64>, String> {
452+
pub fn parse_int_list(
453+
&self,
454+
id: &OptionId,
455+
default: &[i64],
456+
) -> Result<ListOptionValue<i64>, String> {
398457
self.parse_list_hashable(id, default.to_vec(), |source, id| source.get_int_list(id))
399458
}
400459

401460
// Floats are not Eq or Hash, so we fall back to the brute-force O(N*M) lookups.
402-
pub fn parse_float_list(&self, id: &OptionId, default: &[f64]) -> Result<Vec<f64>, String> {
461+
pub fn parse_float_list(
462+
&self,
463+
id: &OptionId,
464+
default: &[f64],
465+
) -> Result<ListOptionValue<f64>, String> {
403466
self.parse_list(
404467
id,
405468
default.to_vec(),
@@ -414,7 +477,7 @@ impl OptionParser {
414477
&self,
415478
id: &OptionId,
416479
default: &[&str],
417-
) -> Result<Vec<String>, String> {
480+
) -> Result<ListOptionValue<String>, String> {
418481
self.parse_list_hashable::<String>(
419482
id,
420483
default.iter().map(|s| s.to_string()).collect(),
@@ -426,8 +489,24 @@ impl OptionParser {
426489
&self,
427490
id: &OptionId,
428491
default: HashMap<String, Val>,
429-
) -> Result<HashMap<String, Val>, String> {
492+
) -> Result<DictOptionValue, String> {
430493
let mut dict = default;
494+
let mut derivation = None;
495+
if self.include_derivation {
496+
let mut derivations = vec![(
497+
Source::Default,
498+
vec![DictEdit {
499+
action: DictEditAction::Replace,
500+
items: dict.clone(),
501+
}],
502+
)];
503+
for (source_type, source) in self.sources.iter().rev() {
504+
if let Some(dict_edits) = source.get_dict(id)? {
505+
derivations.push((*source_type, dict_edits));
506+
}
507+
}
508+
derivation = Some(derivations);
509+
}
431510
for (_, source) in self.sources.iter().rev() {
432511
if let Some(dict_edits) = source.get_dict(id)? {
433512
for dict_edit in dict_edits {
@@ -438,7 +517,10 @@ impl OptionParser {
438517
}
439518
}
440519
}
441-
Ok(dict)
520+
Ok(DictOptionValue {
521+
derivation,
522+
value: dict,
523+
})
442524
}
443525
}
444526

0 commit comments

Comments
 (0)