nimbus_fml/editing/
error_converter.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2* License, v. 2.0. If a copy of the MPL was not distributed with this
3* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use super::{values_finder::ValuesFinder, ErrorKind, FeatureValidationError};
6#[cfg(feature = "client-lib")]
7use super::{CorrectionCandidate, FmlEditorError};
8use crate::{
9    error::FMLError,
10    intermediate_representation::{EnumDef, FeatureDef, ObjectDef},
11};
12use serde_json::Value;
13use std::collections::{BTreeMap, BTreeSet};
14
15#[allow(dead_code)]
16pub(crate) struct ErrorConverter<'a> {
17    pub(crate) enum_defs: &'a BTreeMap<String, EnumDef>,
18    pub(crate) object_defs: &'a BTreeMap<String, ObjectDef>,
19}
20
21impl<'a> ErrorConverter<'a> {
22    pub(crate) fn new(
23        enum_defs: &'a BTreeMap<String, EnumDef>,
24        object_defs: &'a BTreeMap<String, ObjectDef>,
25    ) -> Self {
26        Self {
27            enum_defs,
28            object_defs,
29        }
30    }
31
32    pub(crate) fn convert_feature_error(
33        &self,
34        feature_def: &FeatureDef,
35        feature_value: &Value,
36        error: FeatureValidationError,
37    ) -> FMLError {
38        let values = ValuesFinder::new(self.enum_defs, feature_def, feature_value);
39        let long_message = self.long_message(&values, &error);
40        FMLError::ValidationError(error.path.path, long_message)
41    }
42
43    #[allow(dead_code)]
44    #[cfg(feature = "client-lib")]
45    pub(crate) fn convert_into_editor_errors(
46        &self,
47        feature_def: &FeatureDef,
48        feature_value: &Value,
49        src: &str,
50        errors: &Vec<FeatureValidationError>,
51    ) -> Vec<FmlEditorError> {
52        let mut editor_errors: Vec<_> = Default::default();
53        let values = ValuesFinder::new(self.enum_defs, feature_def, feature_value);
54        for error in errors {
55            // While experimenter is not known to be using the corrections, we should continue to use
56            // the long message which includes the did_you_mean and corrections.
57            let message = self.long_message(&values, error);
58            // After experimenter is using the corrections, we can switch to
59            // let message = self.message(error);
60
61            let highlight = error.path.first_error_token().map(String::from);
62            // TODO: derive the highlighted token from the error span.
63            let error_span = error.path.error_span(src);
64
65            let corrections = self.correction_candidates(&values, src, error);
66
67            let error = FmlEditorError {
68                message,
69
70                highlight,
71                corrections,
72
73                // deprecated, can be removed once it's removed in experimenter.
74                line: error_span.from.line,
75                col: error_span.from.col,
76
77                error_span,
78            };
79            editor_errors.push(error);
80        }
81        editor_errors
82    }
83
84    pub(crate) fn convert_object_error(&self, error: FeatureValidationError) -> FMLError {
85        FMLError::ValidationError(error.path.path.to_owned(), self.message(&error))
86    }
87}
88
89impl ErrorConverter<'_> {
90    fn long_message(&self, values: &ValuesFinder, error: &FeatureValidationError) -> String {
91        let message = self.message(error);
92        let mut suggestions = self.string_replacements(error, values);
93        let dym = did_you_mean(&mut suggestions);
94        format!("{message}{dym}")
95    }
96
97    fn message(&self, error: &FeatureValidationError) -> String {
98        let token = error.path.error_token_abbr();
99        error.kind.message(&token)
100    }
101
102    #[allow(dead_code)]
103    #[cfg(feature = "client-lib")]
104    fn correction_candidates(
105        &self,
106        values: &ValuesFinder,
107        _src: &str,
108        error: &FeatureValidationError,
109    ) -> Vec<CorrectionCandidate> {
110        let strings = self.string_replacements(error, values);
111        let placeholders = self.placeholder_replacements(error, values);
112
113        let mut candidates = Vec::with_capacity(strings.len() + placeholders.len());
114        for s in &strings {
115            candidates.push(CorrectionCandidate::string_replacement(s));
116        }
117        for s in &placeholders {
118            candidates.push(CorrectionCandidate::literal_replacement(s));
119        }
120        candidates
121    }
122}
123
124/// The following methods are for unpacking errors coming out of the DefaultsValidator, to be used
125/// for correction candidates (like Quick Fix in VSCode) and autocomplete.
126impl ErrorConverter<'_> {
127    #[allow(dead_code)]
128    #[cfg(feature = "client-lib")]
129    fn placeholder_replacements(
130        &self,
131        error: &FeatureValidationError,
132        values: &ValuesFinder,
133    ) -> BTreeSet<String> {
134        match &error.kind {
135            ErrorKind::InvalidValue { value_type: t, .. }
136            | ErrorKind::TypeMismatch { value_type: t }
137            | ErrorKind::InvalidNestedValue { prop_type: t, .. } => values.all_placeholders(t),
138            _ => Default::default(),
139        }
140    }
141
142    fn string_replacements(
143        &self,
144        error: &FeatureValidationError,
145        values: &ValuesFinder,
146    ) -> BTreeSet<String> {
147        let complete = match &error.kind {
148            ErrorKind::InvalidKey { key_type: t, .. }
149            | ErrorKind::InvalidValue { value_type: t, .. }
150            | ErrorKind::TypeMismatch { value_type: t } => values.all_specific_strings(t),
151            // For property keys that we don't want to suggest to the user, but we _do_ want them involved in
152            // validation or code generation, we make them never/difficult to be overridden by an experiment,
153            // by filtering them here.
154            ErrorKind::InvalidPropKey { valid, .. } => valid
155                .iter()
156                .filter(|s| s.starts_with(char::is_alphanumeric))
157                .map(ToOwned::to_owned)
158                .collect(),
159            ErrorKind::InvalidNestedValue { .. } => Default::default(),
160        };
161
162        // We don't want to suggest any tokens that the user has already used correctly, so
163        // we can filter out the ones in use.
164        match &error.kind {
165            ErrorKind::InvalidKey { in_use, .. } | ErrorKind::InvalidPropKey { in_use, .. }
166                // This last check is an optimization:
167                // if none of the in_use are valid,
168                // then we can skip cloning.
169                if !complete.is_disjoint(in_use) =>
170            {
171                complete.difference(in_use).cloned().collect()
172            }
173            _ => complete,
174        }
175    }
176}
177
178fn did_you_mean(words: &mut BTreeSet<String>) -> String {
179    let mut words = words.iter();
180    match words.len() {
181        0 => String::from(""),
182        1 => format!("; did you mean \"{}\"?", words.next().unwrap()),
183        2 => format!(
184            "; did you mean \"{}\" or \"{}\"?",
185            words.next().unwrap(),
186            words.next().unwrap(),
187        ),
188        _ => {
189            let last = words.next_back().unwrap();
190            format!(
191                "; did you mean one of \"{}\" or \"{last}\"?",
192                itertools::join(words, "\", \"")
193            )
194        }
195    }
196}