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 fluent_syntax::ast::Entry;
9use fluent_syntax::parser;
10use regex::Regex;
11
12use crate::diagnostics::{CheckId, DiagCtx, RunningCheck};
13use crate::walk::{filter_dirs, walk};
14
15fn message() -> &'static Regex {
16    static_regex!(r#"(?m)^([a-zA-Z0-9_]+)\s*=\s*"#)
17}
18
19fn is_fluent(path: &Path) -> bool {
20    path.extension().is_some_and(|ext| ext == "ftl")
21}
22
23fn check_alphabetic(
24    filename: &str,
25    fluent: &str,
26    check: &mut RunningCheck,
27    all_defined_msgs: &mut HashMap<String, String>,
28) {
29    let Ok(resource) = parser::parse(fluent) else {
30        panic!("Errors encountered while parsing fluent file `{filename}`");
31    };
32
33    let mut prev: Option<&str> = None;
34
35    for entry in &resource.body {
36        if let Entry::Message(msg) = entry {
37            let name: &str = msg.id.name;
38            if let Some(defined_filename) = all_defined_msgs.get(name) {
39                check.error(format!(
40                    "{filename}: message `{name}` is already defined in {defined_filename}",
41                ));
42            } else {
43                all_defined_msgs.insert(name.to_string(), filename.to_owned());
44            }
45            if let Some(prev) = prev
46                && prev > name
47            {
48                check.error(format!(
49                    "{filename}: message `{prev}` appears before `{name}`, but is alphabetically \
50later than it. Run `./x.py test tidy --bless` to sort the file correctly",
51                ));
52            }
53            prev = Some(name);
54        }
55    }
56}
57
58fn sort_messages(
59    filename: &str,
60    fluent: &str,
61    check: &mut RunningCheck,
62    all_defined_msgs: &mut HashMap<String, String>,
63) -> String {
64    let mut chunks = vec![];
65    let mut cur = String::new();
66    for line in fluent.lines() {
67        if let Some(name) = message().find(line) {
68            if let Some(defined_filename) = all_defined_msgs.get(name.as_str()) {
69                check.error(format!(
70                    "{filename}: message `{}` is already defined in {defined_filename}",
71                    name.as_str(),
72                ));
73            }
74
75            all_defined_msgs.insert(name.as_str().to_owned(), filename.to_owned());
76            chunks.push(std::mem::take(&mut cur));
77        }
78
79        cur += line;
80        cur.push('\n');
81    }
82    chunks.push(cur);
83    chunks.sort();
84    let mut out = chunks.join("");
85    out = out.trim().to_string();
86    out.push('\n');
87    out
88}
89
90pub fn check(path: &Path, bless: bool, diag_ctx: DiagCtx) {
91    let mut check = diag_ctx.start_check(CheckId::new("fluent_alphabetical").path(path));
92
93    let mut all_defined_msgs = HashMap::new();
94    walk(
95        path,
96        |path, is_dir| filter_dirs(path) || (!is_dir && !is_fluent(path)),
97        &mut |ent, contents| {
98            if bless {
99                let sorted = sort_messages(
100                    ent.path().to_str().unwrap(),
101                    contents,
102                    &mut check,
103                    &mut all_defined_msgs,
104                );
105                if sorted != contents {
106                    let mut f =
107                        OpenOptions::new().write(true).truncate(true).open(ent.path()).unwrap();
108                    f.write_all(sorted.as_bytes()).unwrap();
109                }
110            } else {
111                check_alphabetic(
112                    ent.path().to_str().unwrap(),
113                    contents,
114                    &mut check,
115                    &mut all_defined_msgs,
116                );
117            }
118        },
119    );
120
121    assert!(!all_defined_msgs.is_empty());
122
123    crate::fluent_used::check(path, all_defined_msgs, diag_ctx);
124}