1use std::path::Path;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FileKind {
11 Markdown,
12 Json,
13 Jsonc,
14 Yaml,
15 Toml,
16 Orphan,
18}
19
20pub fn classify(path: &Path) -> Option<FileKind> {
24 let name = path.file_name()?.to_str()?;
25
26 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
28 match ext.to_ascii_lowercase().as_str() {
29 "md" | "markdown" => return Some(FileKind::Markdown),
30 "json" => return Some(FileKind::Json),
31 "jsonc" => return Some(FileKind::Jsonc),
32 "yaml" | "yml" => return Some(FileKind::Yaml),
33 "toml" => return Some(FileKind::Toml),
34 "txt" | "text" => return Some(FileKind::Orphan),
35 _ => {}
36 }
37 }
38
39 is_orphan(name).then_some(FileKind::Orphan)
40}
41
42fn is_orphan(name: &str) -> bool {
46 const EXACT: &[&str] = &[
47 ".gitignore",
48 ".gitattributes",
49 ".dockerignore",
50 ".npmignore",
51 ".eslintignore",
52 ".prettierignore",
53 ".primignore",
54 ".helmignore",
55 ".editorconfig",
56 ".containerignore",
57 ".mailmap",
58 "CODEOWNERS",
59 "Dockerfile",
60 "Containerfile",
61 "AUTHORS",
62 "CONTRIBUTORS",
63 "NOTICE",
64 "COPYING",
65 ];
66
67 EXACT.contains(&name)
68 || name.starts_with("Dockerfile.") || name.starts_with("LICENSE") }
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 fn k(p: &str) -> Option<FileKind> {
77 classify(Path::new(p))
78 }
79
80 #[test]
81 fn parsed_formats_by_extension() {
82 assert_eq!(k("a.md"), Some(FileKind::Markdown));
83 assert_eq!(k("a.markdown"), Some(FileKind::Markdown));
84 assert_eq!(k("a.json"), Some(FileKind::Json));
85 assert_eq!(k("a.jsonc"), Some(FileKind::Jsonc));
86 assert_eq!(k("a.yaml"), Some(FileKind::Yaml));
87 assert_eq!(k("a.yml"), Some(FileKind::Yaml));
88 assert_eq!(k("a.toml"), Some(FileKind::Toml));
89 }
90
91 #[test]
92 fn orphan_allowlist_dotfiles() {
93 for name in [
94 ".gitignore",
95 ".gitattributes",
96 ".dockerignore",
97 ".npmignore",
98 ".eslintignore",
99 ".prettierignore",
100 ".primignore",
101 ".helmignore",
102 ".editorconfig",
103 ".containerignore",
104 ".mailmap",
105 ] {
106 assert_eq!(k(name), Some(FileKind::Orphan), "{name}");
107 }
108 }
109
110 #[test]
111 fn orphan_allowlist_patterns_and_names() {
112 assert_eq!(k("Dockerfile"), Some(FileKind::Orphan));
113 assert_eq!(k("Dockerfile.dev"), Some(FileKind::Orphan));
114 assert_eq!(k("Containerfile"), Some(FileKind::Orphan));
115 assert_eq!(k("CODEOWNERS"), Some(FileKind::Orphan));
116 assert_eq!(k("LICENSE"), Some(FileKind::Orphan));
117 assert_eq!(k("LICENSE.txt"), Some(FileKind::Orphan));
118 assert_eq!(k("AUTHORS"), Some(FileKind::Orphan));
119 assert_eq!(k("CONTRIBUTORS"), Some(FileKind::Orphan));
120 assert_eq!(k("NOTICE"), Some(FileKind::Orphan));
121 assert_eq!(k("COPYING"), Some(FileKind::Orphan));
122 assert_eq!(k("notes.txt"), Some(FileKind::Orphan));
123 assert_eq!(k("readme.text"), Some(FileKind::Orphan));
124 }
125
126 #[test]
127 fn non_owned_returns_none() {
128 assert_eq!(k("main.rs"), None);
129 assert_eq!(k("script.py"), None);
130 assert_eq!(k("logo.png"), None);
131 assert_eq!(k(".env"), None); assert_eq!(k(".env.local"), None);
133 assert_eq!(k("Makefile"), None); assert_eq!(k("run.sh"), None); assert_eq!(k("noext"), None);
136 }
137
138 #[test]
139 fn classifies_by_final_component_of_a_path() {
140 assert_eq!(k("src/docs/guide.md"), Some(FileKind::Markdown));
141 assert_eq!(k("/etc/project/.gitignore"), Some(FileKind::Orphan));
142 }
143}