1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
//! Checks that no Fluent messages or attributes end in periods (except ellipses)

use std::path::Path;

use fluent_syntax::ast::{Entry, PatternElement};

use crate::walk::{filter_dirs, walk};

fn filter_fluent(path: &Path) -> bool {
    if let Some(ext) = path.extension() { ext.to_str() != Some("ftl") } else { true }
}

/// Messages allowed to have `.` at their end.
///
/// These should probably be reworked eventually.
const ALLOWLIST: &[&str] = &[
    "const_eval_long_running",
    "const_eval_validation_failure_note",
    "driver_impl_ice",
    "incremental_corrupt_file",
    "mir_build_pointer_pattern",
];

fn check_period(filename: &str, contents: &str, bad: &mut bool) {
    if filename.contains("codegen") {
        // FIXME: Too many codegen messages have periods right now...
        return;
    }

    let (Ok(parse) | Err((parse, _))) = fluent_syntax::parser::parse(contents);
    for entry in &parse.body {
        if let Entry::Message(m) = entry {
            if ALLOWLIST.contains(&m.id.name) {
                continue;
            }

            if let Some(pat) = &m.value {
                if let Some(PatternElement::TextElement { value }) = pat.elements.last() {
                    // We don't care about ellipses.
                    if value.ends_with(".") && !value.ends_with("...") {
                        let ll = find_line(contents, *value);
                        let name = m.id.name;
                        tidy_error!(bad, "{filename}:{ll}: message `{name}` ends in a period");
                    }
                }
            }

            for attr in &m.attributes {
                // Teach notes usually have long messages.
                if attr.id.name == "teach_note" {
                    continue;
                }

                if let Some(PatternElement::TextElement { value }) = attr.value.elements.last() {
                    if value.ends_with(".") && !value.ends_with("...") {
                        let ll = find_line(contents, *value);
                        let name = attr.id.name;
                        tidy_error!(bad, "{filename}:{ll}: attr `{name}` ends in a period");
                    }
                }
            }
        }
    }
}

/// Evil cursed bad hack. Requires that `value` be a substr (in memory) of `contents`.
fn find_line(haystack: &str, needle: &str) -> usize {
    for (ll, line) in haystack.lines().enumerate() {
        if line.as_ptr() > needle.as_ptr() {
            return ll;
        }
    }

    1
}

pub fn check(path: &Path, bad: &mut bool) {
    walk(
        path,
        |path, is_dir| filter_dirs(path) || (!is_dir && filter_fluent(path)),
        &mut |ent, contents| {
            check_period(ent.path().to_str().unwrap(), contents, bad);
        },
    );
}