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(
55 globset::GlobBuilder::new(&format!("{trimmed_path}{{,/*}}"))
56 .empty_alternates(true)
57 .build()
58 .unwrap(),
59 );
60 glob_entries.push(clean_path.to_string());
61 } else if is_in_submodule(Path::new(clean_path)) {
62 check.error(format!(
63 "triagebot.toml [mentions.*] '{clean_path}' cannot match inside a submodule"
64 ));
65 }
66 }
67
68 let gs = builder.build().unwrap();
69
70 let mut found = HashSet::new();
71 let mut matches = Vec::new();
72
73 let cloned_path = path.to_path_buf();
74
75 for entry in ignore::WalkBuilder::new(&path)
77 .filter_entry(move |entry| {
78 let entry_path = entry.path().strip_prefix(&cloned_path).unwrap();
80 is_not_in_submodule(entry_path)
81 })
82 .build()
83 .flatten()
84 {
85 let entry_path = entry.path().strip_prefix(path).unwrap();
87
88 gs.matches_into(entry_path, &mut matches);
90 found.extend(matches.iter().copied());
91
92 if found.len() == glob_entries.len() {
94 break;
95 }
96 }
97
98 for (i, clean_path) in glob_entries.iter().enumerate() {
99 if !found.contains(&i) {
100 check.error(format!(
101 "triagebot.toml [mentions.*] contains '{clean_path}' which doesn't match any file or directory in the repository"
102 ));
103 }
104 }
105 } else {
106 check.error(
107 "triagebot.toml missing [mentions.*] section, this wrong for rust-lang/rust repo.",
108 );
109 }
110
111 if let Some(Value::Table(assign)) = config.get("assign") {
115 if let Some(Value::Table(owners)) = assign.get("owners") {
116 for path_str in owners.keys() {
117 let clean_path = path_str.trim_matches('"').trim_start_matches('/');
119 let full_path = path.join(clean_path);
120
121 if !full_path.exists() {
122 check.error(format!(
123 "triagebot.toml [assign.owners] contains path '{clean_path}' which doesn't exist"
124 ));
125 }
126 }
127 } else {
128 check.error(
129 "triagebot.toml missing [assign.owners] section, this wrong for rust-lang/rust repo."
130 );
131 }
132 }
133
134 if let Some(Value::Table(autolabels)) = config.get("autolabel") {
142 for (label, content) in autolabels {
143 if let Some(trigger_files) = content.get("trigger_files").and_then(|v| v.as_array()) {
144 for file in trigger_files {
145 if let Some(file_str) = file.as_str() {
146 let full_path = path.join(file_str);
147
148 if !full_path.exists() {
150 check.error(format!(
151 "triagebot.toml [autolabel.{label}] contains trigger_files path '{file_str}' which doesn't exist",
152 ));
153 }
154 }
155 }
156 }
157 }
158 }
159}
160
161fn is_not_in_submodule(path: &Path) -> bool {
162 SUBMODULES.contains(&path) || !SUBMODULES.iter().any(|p| path.starts_with(*p))
163}
164
165fn is_in_submodule(path: &Path) -> bool {
166 !SUBMODULES.contains(&path) && SUBMODULES.iter().any(|p| path.starts_with(*p))
167}