tidy/
fluent_alphabetical.rs1use 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}