1use std::collections::HashSet;
4use std::path::Path;
5use std::sync::LazyLock;
6
7use toml::Value;
8
9use crate::diagnostics::TidyCtx;
10
11static SUBMODULES: LazyLock<Vec<&'static Path>> = LazyLock::new(|| {
12 crate::deps::WORKSPACES
14 .iter()
15 .map(|ws| ws.submodules.iter())
16 .flatten()
17 .map(|p| Path::new(p))
18 .collect()
19});
20
21pub fn check(path: &Path, tidy_ctx: TidyCtx) {
22 let mut check = tidy_ctx.start_check("triagebot");
23 let triagebot_path = path.join("triagebot.toml");
24
25 if !triagebot_path.exists() {
29 return;
30 }
31
32 let contents = std::fs::read_to_string(&triagebot_path).unwrap();
33 let config: Value = toml::from_str(&contents).unwrap();
34
35 if let Some(Value::Table(mentions)) = config.get("mentions") {
37 let mut builder = globset::GlobSetBuilder::new();
38 let mut glob_entries = Vec::new();
39
40 for (entry_key, entry_val) in mentions.iter() {
41 if entry_val.get("type").is_some_and(|t| t.as_str().unwrap_or_default() != "filename") {
43 continue;
44 }
45 let path_str = entry_key;
46 let clean_path = path_str.trim_matches('"');
48 let full_path = path.join(clean_path);
49
50 if !full_path.exists() {
51 let trimmed_path = clean_path.trim_end_matches('/');
54 builder.add(globset::Glob::new(&format!("{trimmed_path}{{,/*}}")).unwrap());
55 glob_entries.push(clean_path.to_string());
56 } else if is_in_submodule(Path::new(clean_path)) {
57 check.error(format!(
58 "triagebot.toml [mentions.*] '{clean_path}' cannot match inside a submodule"
59 ));
60 }
61 }
62
63 let gs = builder.build().unwrap();
64
65 let mut found = HashSet::new();
66 let mut matches = Vec::new();
67
68 let cloned_path = path.to_path_buf();
69
70 for entry in ignore::WalkBuilder::new(&path)
72 .filter_entry(move |entry| {
73 let entry_path = entry.path().strip_prefix(&cloned_path).unwrap();
75 is_not_in_submodule(entry_path)
76 })
77 .build()
78 .flatten()
79 {
80 let entry_path = entry.path().strip_prefix(path).unwrap();
82
83 gs.matches_into(entry_path, &mut matches);
85 found.extend(matches.iter().copied());
86
87 if found.len() == glob_entries.len() {
89 break;
90 }
91 }
92
93 for (i, clean_path) in glob_entries.iter().enumerate() {
94 if !found.contains(&i) {
95 check.error(format!(
96 "triagebot.toml [mentions.*] contains '{clean_path}' which doesn't match any file or directory in the repository"
97 ));
98 }
99 }
100 } else {
101 check.error(
102 "triagebot.toml missing [mentions.*] section, this wrong for rust-lang/rust repo.",
103 );
104 }
105
106 if let Some(Value::Table(assign)) = config.get("assign") {
110 if let Some(Value::Table(owners)) = assign.get("owners") {
111 for path_str in owners.keys() {
112 let clean_path = path_str.trim_matches('"').trim_start_matches('/');
114 let full_path = path.join(clean_path);
115
116 if !full_path.exists() {
117 check.error(format!(
118 "triagebot.toml [assign.owners] contains path '{clean_path}' which doesn't exist"
119 ));
120 }
121 }
122 } else {
123 check.error(
124 "triagebot.toml missing [assign.owners] section, this wrong for rust-lang/rust repo."
125 );
126 }
127 }
128
129 if let Some(Value::Table(autolabels)) = config.get("autolabel") {
137 for (label, content) in autolabels {
138 if let Some(trigger_files) = content.get("trigger_files").and_then(|v| v.as_array()) {
139 for file in trigger_files {
140 if let Some(file_str) = file.as_str() {
141 let full_path = path.join(file_str);
142
143 if !full_path.exists() {
145 check.error(format!(
146 "triagebot.toml [autolabel.{label}] contains trigger_files path '{file_str}' which doesn't exist",
147 ));
148 }
149 }
150 }
151 }
152 }
153 }
154}
155
156fn is_not_in_submodule(path: &Path) -> bool {
157 SUBMODULES.contains(&path) || !SUBMODULES.iter().any(|p| path.starts_with(*p))
158}
159
160fn is_in_submodule(path: &Path) -> bool {
161 !SUBMODULES.contains(&path) && SUBMODULES.iter().any(|p| path.starts_with(*p))
162}