sway_error/
formatting.rs

1//! This module contains various helper functions for easier formatting and creation of user-friendly messages.
2
3use std::{
4    borrow::Cow,
5    cmp::{self, Ordering},
6    fmt::{self, Display},
7};
8
9use sway_types::{SourceEngine, SourceId, Span};
10
11use crate::diagnostic::Hint;
12
13/// Returns the file name (with extension) for the provided `source_id`,
14/// or `None` if the `source_id` is `None` or the file name cannot be
15/// obtained.
16pub fn get_file_name(source_engine: &SourceEngine, source_id: Option<&SourceId>) -> Option<String> {
17    match source_id {
18        Some(source_id) => source_engine.get_file_name(source_id),
19        None => None,
20    }
21}
22
23/// Returns reading-friendly textual representation for `num` smaller than or equal to 10
24/// or its numeric representation if it is greater than 10.
25pub fn num_to_str(num: usize) -> String {
26    match num {
27        0 => "zero".to_string(),
28        1 => "one".to_string(),
29        2 => "two".to_string(),
30        3 => "three".to_string(),
31        4 => "four".to_string(),
32        5 => "five".to_string(),
33        6 => "six".to_string(),
34        7 => "seven".to_string(),
35        8 => "eight".to_string(),
36        9 => "nine".to_string(),
37        10 => "ten".to_string(),
38        _ => format!("{num}"),
39    }
40}
41
42/// Returns reading-friendly textual representation for `num` smaller than or equal to 10
43/// or its numeric representation if it is greater than 10.
44///
45/// Zero is returned as "none".
46pub fn num_to_str_or_none(num: usize) -> String {
47    if num == 0 {
48        "none".to_string()
49    } else {
50        num_to_str(num)
51    }
52}
53
54pub enum Enclosing {
55    #[allow(dead_code)]
56    None,
57    DoubleQuote,
58}
59
60impl Display for Enclosing {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(
63            f,
64            "{}",
65            match self {
66                Self::None => "",
67                Self::DoubleQuote => "\"",
68            },
69        )
70    }
71}
72
73pub enum Indent {
74    #[allow(dead_code)]
75    None,
76    Single,
77    Double,
78}
79
80impl Display for Indent {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(
83            f,
84            "{}",
85            match self {
86                Self::None => "",
87                Self::Single => "  ",
88                Self::Double => "    ",
89            },
90        )
91    }
92}
93
94/// Returns reading-friendly textual representation of the `sequence`, with comma-separated
95/// items and each item optionally enclosed in the specified `enclosing`.
96/// If the sequence has more than `max_items` the remaining items are replaced
97/// with the text "and <number> more".
98///
99/// E.g.:
100/// - \[a\] => "a"
101/// - \[a, b\] => "a" and "b"
102/// - \[a, b, c\] => "a", "b" and "c"
103/// - \[a, b, c, d\] => "a", "b", "c" and one more
104/// - \[a, b, c, d, e\] => "a", "b", "c" and two more
105///
106/// Panics if the `sequence` is empty, or `max_items` is zero.
107pub fn sequence_to_str<T>(sequence: &[T], enclosing: Enclosing, max_items: usize) -> String
108where
109    T: Display,
110{
111    sequence_to_str_impl(sequence, enclosing, max_items, "and")
112}
113
114/// Returns reading-friendly textual representation of the `sequence`, with comma-separated
115/// items and each item optionally enclosed in the specified `enclosing`.
116/// If the sequence has more than `max_items` the remaining items are replaced
117/// with the text "or <number> more".
118///
119/// E.g.:
120/// - \[a\] => "a"
121/// - \[a, b\] => "a" or "b"
122/// - \[a, b, c\] => "a", "b" or "c"
123/// - \[a, b, c, d\] => "a", "b", "c" or one more
124/// - \[a, b, c, d, e\] => "a", "b", "c" or two more
125///
126/// Panics if the `sequence` is empty, or `max_items` is zero.
127pub fn sequence_to_str_or<T>(sequence: &[T], enclosing: Enclosing, max_items: usize) -> String
128where
129    T: Display,
130{
131    sequence_to_str_impl(sequence, enclosing, max_items, "or")
132}
133
134fn sequence_to_str_impl<T>(
135    sequence: &[T],
136    enclosing: Enclosing,
137    max_items: usize,
138    and_or: &str,
139) -> String
140where
141    T: Display,
142{
143    assert!(
144        !sequence.is_empty(),
145        "Sequence to display must not be empty."
146    );
147    assert!(
148        max_items > 0,
149        "Maximum number of items to display must be greater than zero."
150    );
151
152    let max_items = cmp::min(max_items, sequence.len());
153
154    let (to_display, remaining) = sequence.split_at(max_items);
155
156    let fmt_item = |item: &T| format!("{enclosing}{item}{enclosing}");
157
158    if !remaining.is_empty() {
159        format!(
160            "{}, {} {} more",
161            to_display
162                .iter()
163                .map(fmt_item)
164                .collect::<Vec<_>>()
165                .join(", "),
166            and_or,
167            num_to_str(remaining.len())
168        )
169    } else {
170        match to_display {
171            [] => unreachable!("There must be at least one item in the sequence."),
172            [item] => fmt_item(item),
173            [first_item, second_item] => {
174                format!(
175                    "{} {} {}",
176                    fmt_item(first_item),
177                    and_or,
178                    fmt_item(second_item)
179                )
180            }
181            _ => format!(
182                "{}, {} {}",
183                to_display
184                    .split_last()
185                    .unwrap()
186                    .1
187                    .iter()
188                    .map(fmt_item)
189                    .collect::<Vec::<_>>()
190                    .join(", "),
191                and_or,
192                fmt_item(to_display.last().unwrap())
193            ),
194        }
195    }
196}
197
198/// Returns reading-friendly textual representation of the `sequence`, with vertically
199/// listed items and each item indented for the `indent` and preceded with the dash (-).
200/// If the sequence has more than `max_items` the remaining items are replaced
201/// with the text "and <number> more".
202///
203/// E.g.:
204/// * \[a\] =>
205///     - a
206/// * \[a, b\] =>
207///     - a
208///     - b
209/// * \[a, b, c, d, e\] =>
210///     - a
211///     - b
212///     - and three more
213///
214/// Panics if the `sequence` is empty, or `max_items` is zero.
215pub fn sequence_to_list<T>(sequence: &[T], indent: Indent, max_items: usize) -> Vec<String>
216where
217    T: Display,
218{
219    assert!(
220        !sequence.is_empty(),
221        "Sequence to display must not be empty."
222    );
223    assert!(
224        max_items > 0,
225        "Maximum number of items to display must be greater than zero."
226    );
227
228    let mut result = vec![];
229
230    let max_items = cmp::min(max_items, sequence.len());
231    let (to_display, remaining) = sequence.split_at(max_items);
232    for item in to_display {
233        result.push(format!("{indent}- {item}"));
234    }
235    if !remaining.is_empty() {
236        result.push(format!(
237            "{indent}- and {} more",
238            num_to_str(remaining.len())
239        ));
240    }
241
242    result
243}
244
245/// Returns "s" if `count` is different than 1, otherwise empty string.
246/// Convenient for building simple plural of words.
247pub fn plural_s(count: usize) -> &'static str {
248    if count == 1 {
249        ""
250    } else {
251        "s"
252    }
253}
254
255/// Returns "is" if `count` is 1, otherwise "are".
256pub fn is_are(count: usize) -> &'static str {
257    if count == 1 {
258        "is"
259    } else {
260        "are"
261    }
262}
263
264/// Returns `singular` if `count` is 1, otherwise `plural`.
265pub fn singular_plural<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str {
266    if count == 1 {
267        singular
268    } else {
269        plural
270    }
271}
272
273/// Returns the short name of a type or function represented by the `full_name`.
274/// Convenient for subsequent showing only the short name of a full name that was
275/// already shown.
276///
277/// The `full_name` is expected to be a call path with or without generic parameters,
278/// eventually prefixed with `&`s or `&mut`s for types.
279///
280/// E.g.:
281/// - `SomeType` -> `SomeType`
282/// - `SomeType<T>` -> `SomeType`
283/// - `std::ops::Eq` -> `Eq`
284/// - `some_lib::Struct<A, B>` -> `Struct`
285/// - `some_lib::Struct<some::other::lib::A, some::other::lib::B>` -> `Struct`
286/// - `&mut some_lib::Struct<&some::other::lib::A, &mut some::other::lib::B>` -> `&mut Struct`
287/// - `&&&mut some_lib::Struct<&some::other::lib::A, &mut some::other::lib::B>` -> `&&&mut Struct`
288/// - `some_lib::fns::some_function<A, B>` -> `some_function`
289pub fn short_name(full_name: &str) -> String {
290    // Preserve leading references, `&`s and `&mut`s.
291    let mut name_start_index = 0;
292    loop {
293        let reminder = &full_name[name_start_index..];
294        if reminder.starts_with('&') {
295            name_start_index += 1;
296        } else if reminder.starts_with("mut ") {
297            name_start_index += 4;
298        } else {
299            break;
300        }
301    }
302    let full_name_without_refs = &full_name[name_start_index..];
303    let full_name_without_generics = match full_name_without_refs.find('<') {
304        Some(index) => &full_name_without_refs[..index],
305        None => full_name_without_refs,
306    };
307    let short_name = match full_name_without_generics.rfind(':') {
308        Some(index) if index < full_name_without_generics.len() - 1 => {
309            full_name_without_generics.split_at(index + 1).1.to_string()
310        }
311        _ => full_name_without_generics.to_string(),
312    };
313    format!("{}{short_name}", &full_name[..name_start_index])
314}
315
316/// Returns indefinite article "a" or "an" that corresponds to the `word`,
317/// or an empty string if the indefinite article do not fit to the word.
318///
319/// Note that the function does not recognize plurals and assumes that the
320/// `word` is in singular.
321///
322/// If an article is returned, it is followed by a space, e.g. "a ".
323pub fn a_or_an<S: AsRef<str> + ?Sized>(word: &S) -> &'static str {
324    let is_a = in_definite::is_an(word.as_ref());
325    match is_a {
326        in_definite::Is::An => "an ",
327        in_definite::Is::A => "a ",
328        in_definite::Is::None => "",
329    }
330}
331
332/// Returns the ordinal suffix for the given `num`.
333/// Convenient for building ordinal numbers like "1st", "2nd", "3rd", "4th", etc.
334pub fn ord_num_suffix(num: usize) -> &'static str {
335    match num % 100 {
336        11..=13 => "th",
337        _ => match num % 10 {
338            1 => "st",
339            2 => "nd", // typos:ignore
340            3 => "rd",
341            _ => "th",
342        },
343    }
344}
345
346/// Returns `text` with the first character turned into ASCII uppercase.
347pub fn ascii_sentence_case(text: &String) -> Cow<String> {
348    if text.is_empty() || text.chars().next().unwrap().is_uppercase() {
349        Cow::Borrowed(text)
350    } else {
351        let mut result = text.clone();
352        result[0..1].make_ascii_uppercase();
353        Cow::Owned(result.to_owned())
354    }
355}
356
357/// Returns the first line in `text`, up to the first `\n` if the `text` contains
358/// multiple lines, and optionally adds ellipses "..." to the end of the line
359/// if `with_ellipses` is true.
360///
361/// If the `text` is a single-line string, returns the original `text`.
362///
363/// Suitable for showing just the first line of a piece of code.
364/// E.g., if `text` is:
365///   if x {
366///     0
367///   } else {
368///     1
369///   }
370///  the returned value, with ellipses, will be:
371///   if x {...
372pub fn first_line(text: &str, with_ellipses: bool) -> Cow<str> {
373    if !text.contains('\n') {
374        Cow::Borrowed(text)
375    } else {
376        let index_of_new_line = text.find('\n').unwrap();
377        Cow::Owned(text[..index_of_new_line].to_string() + if with_ellipses { "..." } else { "" })
378    }
379}
380
381/// Finds strings from an iterable of `possible_values` similar to a given value `v`.
382/// Returns a vector of all possible values that exceed a similarity threshold,
383/// sorted by similarity (most similar comes first). The returned vector will have
384/// at most `max_num_of_suggestions` elements.
385///
386/// The implementation is taken and adapted from the [Clap project](https://github.com/clap-rs/clap/blob/50f7646cf72dd7d4e76d9284d76bdcdaceb7c049/clap_builder/src/parser/features/suggestions.rs#L11).
387pub fn did_you_mean<T, I>(v: &str, possible_values: I, max_num_of_suggestions: usize) -> Vec<String>
388where
389    T: AsRef<str>,
390    I: IntoIterator<Item = T>,
391{
392    let mut candidates: Vec<_> = possible_values
393        .into_iter()
394        .map(|pv| (strsim::jaro(v, pv.as_ref()), pv.as_ref().to_owned()))
395        // Confidence of 0.7 so that bar -> baz is suggested.
396        .filter(|(confidence, _)| *confidence > 0.7)
397        .collect();
398    candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
399    candidates
400        .into_iter()
401        .take(max_num_of_suggestions)
402        .map(|(_, pv)| pv)
403        .collect()
404}
405
406/// Returns a single line "Did you mean" [Hint::help]. E.g.: Did you mean "this" or "that"?
407///
408/// The input value is taken from the `span` and the help hint is positioned at that `span`.
409/// Each suggestion are enclosed in `enclosing`.
410pub fn did_you_mean_help<T, I>(
411    source_engine: &SourceEngine,
412    span: Span,
413    possible_values: I,
414    max_num_of_suggestions: usize,
415    enclosing: Enclosing,
416) -> Hint
417where
418    T: AsRef<str>,
419    I: IntoIterator<Item = T>,
420{
421    let suggestions = &did_you_mean(span.as_str(), possible_values, max_num_of_suggestions);
422    if suggestions.is_empty() {
423        Hint::none()
424    } else {
425        Hint::help(
426            source_engine,
427            span,
428            format!(
429                "Did you mean {}?",
430                sequence_to_str_or(suggestions, enclosing, max_num_of_suggestions)
431            ),
432        )
433    }
434}
435
436mod test {
437    #[test]
438    fn test_short_name() {
439        use super::short_name;
440
441        let test = |full_name: &str, expected: &str| {
442            let short_name = short_name(full_name);
443            assert_eq!(short_name, expected, "Full name: {full_name}.");
444        };
445
446        test("SomeType", "SomeType");
447        test("&SomeType", "&SomeType");
448        test("&&&SomeType", "&&&SomeType");
449        test("&mut &&mut SomeType", "&mut &&mut SomeType");
450        test("&&&mut &mut SomeType", "&&&mut &mut SomeType");
451        test("SomeType<T>", "SomeType");
452        test("&SomeType<&T>", "&SomeType");
453        test("&&&SomeType<&&&T>", "&&&SomeType");
454        test("&mut &&mut SomeType<&mut &&mut T>", "&mut &&mut SomeType");
455        test(
456            "&&&mut &mut SomeType<&&&mut &mut T>",
457            "&&&mut &mut SomeType",
458        );
459        test("std::ops::Eq", "Eq");
460        test("some_lib::Struct<A, B>", "Struct");
461        test("&&mut some_lib::Struct<&A, &mut B>", "&&mut Struct");
462        test(
463            "some_lib::Struct<some::other::lib::A, some::other::lib::B>",
464            "Struct",
465        );
466        test(
467            "&&&mut some_lib::Struct<some::other::lib::A, some::other::lib::B>",
468            "&&&mut Struct",
469        );
470        test(
471            "some_lib::fn::function<some::other::lib::A<T1, T2>, some::other::lib::B<T3>>",
472            "function",
473        );
474    }
475}