tidy/
fluent_alphabetical.rs

1//! Checks that all Flunt files have messages in alphabetical order
2
3use std::collections::HashMap;
4use std::fs::OpenOptions;
5use std::io::Write;
6use std::path::Path;
7
8use regex::Regex;
9
10use crate::walk::{filter_dirs, walk};
11
12fn message() -> &'static Regex {
13    static_regex!(r#"(?m)^([a-zA-Z0-9_]+)\s*=\s*"#)
14}
15
16fn filter_fluent(path: &Path) -> bool {
17    if let Some(ext) = path.extension() { ext.to_str() != Some("ftl") } else { true }
18}
19
20fn check_alphabetic(
21    filename: &str,
22    fluent: &str,
23    bad: &mut bool,
24    all_defined_msgs: &mut HashMap<String, String>,
25) {
26    let mut matches = message().captures_iter(fluent).peekable();
27    while let Some(m) = matches.next() {
28        let name = m.get(1).unwrap();
29        if let Some(defined_filename) = all_defined_msgs.get(name.as_str()) {
30            tidy_error!(
31                bad,
32                "{filename}: message `{}` is already defined in {}",
33                name.as_str(),
34                defined_filename,
35            );
36        }
37
38        all_defined_msgs.insert(name.as_str().to_owned(), filename.to_owned());
39
40        if let Some(next) = matches.peek() {
41            let next = next.get(1).unwrap();
42            if name.as_str() > next.as_str() {
43                tidy_error!(
44                    bad,
45                    "{filename}: message `{}` appears before `{}`, but is alphabetically later than it
46run `./x.py test tidy --bless` to sort the file correctly",
47                    name.as_str(),
48                    next.as_str()
49                );
50            }
51        } else {
52            break;
53        }
54    }
55}
56
57fn sort_messages(
58    filename: &str,
59    fluent: &str,
60    bad: &mut bool,
61    all_defined_msgs: &mut HashMap<String, String>,
62) -> String {
63    let mut chunks = vec![];
64    let mut cur = String::new();
65    for line in fluent.lines() {
66        if let Some(name) = message().find(line) {
67            if let Some(defined_filename) = all_defined_msgs.get(name.as_str()) {
68                tidy_error!(
69                    bad,
70                    "{filename}: message `{}` is already defined in {}",
71                    name.as_str(),
72                    defined_filename,
73                );
74            }
75
76            all_defined_msgs.insert(name.as_str().to_owned(), filename.to_owned());
77            chunks.push(std::mem::take(&mut cur));
78        }
79
80        cur += line;
81        cur.push('\n');
82    }
83    chunks.push(cur);
84    chunks.sort();
85    let mut out = chunks.join("");
86    out = out.trim().to_string();
87    out.push('\n');
88    out
89}
90
91pub fn check(path: &Path, bless: bool, bad: &mut bool) {
92    let mut all_defined_msgs = HashMap::new();
93    walk(
94        path,
95        |path, is_dir| filter_dirs(path) || (!is_dir && filter_fluent(path)),
96        &mut |ent, contents| {
97            if bless {
98                let sorted = sort_messages(
99                    ent.path().to_str().unwrap(),
100                    contents,
101                    bad,
102                    &mut all_defined_msgs,
103                );
104                if sorted != contents {
105                    let mut f =
106                        OpenOptions::new().write(true).truncate(true).open(ent.path()).unwrap();
107                    f.write(sorted.as_bytes()).unwrap();
108                }
109            } else {
110                check_alphabetic(
111                    ent.path().to_str().unwrap(),
112                    contents,
113                    bad,
114                    &mut all_defined_msgs,
115                );
116            }
117        },
118    );
119
120    crate::fluent_used::check(path, all_defined_msgs, bad);
121}