tidy/
fluent_period.rs

1//! Checks that no Fluent messages or attributes end in periods (except ellipses)
2
3use std::path::Path;
4
5use fluent_syntax::ast::{Entry, PatternElement};
6
7use crate::walk::{filter_dirs, walk};
8
9fn filter_fluent(path: &Path) -> bool {
10    if let Some(ext) = path.extension() { ext.to_str() != Some("ftl") } else { true }
11}
12
13/// Messages allowed to have `.` at their end.
14///
15/// These should probably be reworked eventually.
16const ALLOWLIST: &[&str] = &[
17    "const_eval_long_running",
18    "const_eval_validation_failure_note",
19    "driver_impl_ice",
20    "incremental_corrupt_file",
21];
22
23fn check_period(filename: &str, contents: &str, bad: &mut bool) {
24    if filename.contains("codegen") {
25        // FIXME: Too many codegen messages have periods right now...
26        return;
27    }
28
29    let (Ok(parse) | Err((parse, _))) = fluent_syntax::parser::parse(contents);
30    for entry in &parse.body {
31        if let Entry::Message(m) = entry {
32            if ALLOWLIST.contains(&m.id.name) {
33                continue;
34            }
35
36            if let Some(pat) = &m.value {
37                if let Some(PatternElement::TextElement { value }) = pat.elements.last() {
38                    // We don't care about ellipses.
39                    if value.ends_with(".") && !value.ends_with("...") {
40                        let ll = find_line(contents, *value);
41                        let name = m.id.name;
42                        tidy_error!(bad, "{filename}:{ll}: message `{name}` ends in a period");
43                    }
44                }
45            }
46
47            for attr in &m.attributes {
48                // Teach notes usually have long messages.
49                if attr.id.name == "teach_note" {
50                    continue;
51                }
52
53                if let Some(PatternElement::TextElement { value }) = attr.value.elements.last() {
54                    if value.ends_with(".") && !value.ends_with("...") {
55                        let ll = find_line(contents, *value);
56                        let name = attr.id.name;
57                        tidy_error!(bad, "{filename}:{ll}: attr `{name}` ends in a period");
58                    }
59                }
60            }
61        }
62    }
63}
64
65/// Evil cursed bad hack. Requires that `value` be a substr (in memory) of `contents`.
66fn find_line(haystack: &str, needle: &str) -> usize {
67    for (ll, line) in haystack.lines().enumerate() {
68        if line.as_ptr() > needle.as_ptr() {
69            return ll;
70        }
71    }
72
73    1
74}
75
76pub fn check(path: &Path, bad: &mut bool) {
77    walk(
78        path,
79        |path, is_dir| filter_dirs(path) || (!is_dir && filter_fluent(path)),
80        &mut |ent, contents| {
81            check_period(ent.path().to_str().unwrap(), contents, bad);
82        },
83    );
84}