summaryrefslogtreecommitdiffstats
path: root/third_party/rust/clap_builder/src/parser/features/suggestions.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--third_party/rust/clap_builder/src/parser/features/suggestions.rs167
1 files changed, 167 insertions, 0 deletions
diff --git a/third_party/rust/clap_builder/src/parser/features/suggestions.rs b/third_party/rust/clap_builder/src/parser/features/suggestions.rs
new file mode 100644
index 0000000000..b8bb7adfa1
--- /dev/null
+++ b/third_party/rust/clap_builder/src/parser/features/suggestions.rs
@@ -0,0 +1,167 @@
+#[cfg(feature = "suggestions")]
+use std::cmp::Ordering;
+
+// Internal
+use crate::builder::Command;
+
+/// Find strings from an iterable of `possible_values` similar to a given value `v`
+/// Returns a Vec of all possible values that exceed a similarity threshold
+/// sorted by ascending similarity, most similar comes last
+#[cfg(feature = "suggestions")]
+pub(crate) fn did_you_mean<T, I>(v: &str, possible_values: I) -> Vec<String>
+where
+ T: AsRef<str>,
+ I: IntoIterator<Item = T>,
+{
+ let mut candidates: Vec<(f64, String)> = possible_values
+ .into_iter()
+ // GH #4660: using `jaro` because `jaro_winkler` implementation in `strsim-rs` is wrong
+ // causing strings with common prefix >=10 to be considered perfectly similar
+ .map(|pv| (strsim::jaro(v, pv.as_ref()), pv.as_ref().to_owned()))
+ // Confidence of 0.7 so that bar -> baz is suggested
+ .filter(|(confidence, _)| *confidence > 0.7)
+ .collect();
+ candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
+ candidates.into_iter().map(|(_, pv)| pv).collect()
+}
+
+#[cfg(not(feature = "suggestions"))]
+pub(crate) fn did_you_mean<T, I>(_: &str, _: I) -> Vec<String>
+where
+ T: AsRef<str>,
+ I: IntoIterator<Item = T>,
+{
+ Vec::new()
+}
+
+/// Returns a suffix that can be empty, or is the standard 'did you mean' phrase
+pub(crate) fn did_you_mean_flag<'a, 'help, I, T>(
+ arg: &str,
+ remaining_args: &[&std::ffi::OsStr],
+ longs: I,
+ subcommands: impl IntoIterator<Item = &'a mut Command>,
+) -> Option<(String, Option<String>)>
+where
+ 'help: 'a,
+ T: AsRef<str>,
+ I: IntoIterator<Item = T>,
+{
+ use crate::mkeymap::KeyType;
+
+ match did_you_mean(arg, longs).pop() {
+ Some(candidate) => Some((candidate, None)),
+ None => subcommands
+ .into_iter()
+ .filter_map(|subcommand| {
+ subcommand._build_self(false);
+
+ let longs = subcommand.get_keymap().keys().filter_map(|a| {
+ if let KeyType::Long(v) = a {
+ Some(v.to_string_lossy().into_owned())
+ } else {
+ None
+ }
+ });
+
+ let subcommand_name = subcommand.get_name();
+
+ let candidate = some!(did_you_mean(arg, longs).pop());
+ let score = some!(remaining_args.iter().position(|x| subcommand_name == *x));
+ Some((score, (candidate, Some(subcommand_name.to_string()))))
+ })
+ .min_by_key(|(x, _)| *x)
+ .map(|(_, suggestion)| suggestion),
+ }
+}
+
+#[cfg(all(test, feature = "suggestions"))]
+mod test {
+ use super::*;
+
+ #[test]
+ fn missing_letter() {
+ let p_vals = ["test", "possible", "values"];
+ assert_eq!(did_you_mean("tst", p_vals.iter()), vec!["test"]);
+ }
+
+ #[test]
+ fn ambiguous() {
+ let p_vals = ["test", "temp", "possible", "values"];
+ assert_eq!(did_you_mean("te", p_vals.iter()), vec!["test", "temp"]);
+ }
+
+ #[test]
+ fn unrelated() {
+ let p_vals = ["test", "possible", "values"];
+ assert_eq!(
+ did_you_mean("hahaahahah", p_vals.iter()),
+ Vec::<String>::new()
+ );
+ }
+
+ #[test]
+ fn best_fit() {
+ let p_vals = [
+ "test",
+ "possible",
+ "values",
+ "alignmentStart",
+ "alignmentScore",
+ ];
+ assert_eq!(
+ did_you_mean("alignmentScorr", p_vals.iter()),
+ vec!["alignmentStart", "alignmentScore"]
+ );
+ }
+
+ #[test]
+ fn best_fit_long_common_prefix_issue_4660() {
+ let p_vals = ["alignmentScore", "alignmentStart"];
+ assert_eq!(
+ did_you_mean("alignmentScorr", p_vals.iter()),
+ vec!["alignmentStart", "alignmentScore"]
+ );
+ }
+
+ #[test]
+ fn flag_missing_letter() {
+ let p_vals = ["test", "possible", "values"];
+ assert_eq!(
+ did_you_mean_flag("tst", &[], p_vals.iter(), []),
+ Some(("test".to_owned(), None))
+ );
+ }
+
+ #[test]
+ fn flag_ambiguous() {
+ let p_vals = ["test", "temp", "possible", "values"];
+ assert_eq!(
+ did_you_mean_flag("te", &[], p_vals.iter(), []),
+ Some(("temp".to_owned(), None))
+ );
+ }
+
+ #[test]
+ fn flag_unrelated() {
+ let p_vals = ["test", "possible", "values"];
+ assert_eq!(
+ did_you_mean_flag("hahaahahah", &[], p_vals.iter(), []),
+ None
+ );
+ }
+
+ #[test]
+ fn flag_best_fit() {
+ let p_vals = [
+ "test",
+ "possible",
+ "values",
+ "alignmentStart",
+ "alignmentScore",
+ ];
+ assert_eq!(
+ did_you_mean_flag("alignmentScorr", &[], p_vals.iter(), []),
+ Some(("alignmentScore".to_owned(), None))
+ );
+ }
+}