1use crate::data_structures::Content;
4use crate::heuristic::{
5 guess_by_extensions, guess_by_filename, guess_by_heuristic, guess_by_interpreter,
6 guess_by_modeline,
7};
8use std::fmt::{Display, Formatter};
9use std::path::Path;
10
11#[derive(Debug, Eq, PartialEq)]
12pub enum Guess {
13 Kind(String),
14 Unknown,
15}
16
17#[cfg(not(tarpaulin_include))]
18impl Display for Guess {
19 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
20 match self {
21 Guess::Kind(lang) => f.write_str(lang),
22 Guess::Unknown => f.write_str("Unknown file"),
23 }
24 }
25}
26
27pub fn guess(path: &Path) -> Result<Guess, std::io::Error> {
31 let metadata = path.metadata()?;
32 if metadata.is_dir() {
33 return Ok(Guess::Kind("Directory".to_string()));
34 }
35
36 if let Some(lang) = guess_by_filename(path) {
37 return Ok(Guess::Kind(lang.to_string()));
38 }
39 let optional_extensions = extensions(path);
40 let content = Content::new(path)?;
41 match content {
42 Content::Binary(_) => return Ok(Guess::Kind("Binary".to_string())),
43 Content::Empty => {
44 if let Some(ext_vec) = optional_extensions {
45 for ext in ext_vec {
46 if let Some(lang) = guess_by_extensions(&ext) {
47 return Ok(Guess::Kind(lang.to_string()));
48 }
49 }
50 }
51 return Ok(Guess::Kind("Unknown file".to_string()));
52 }
53 Content::Text { modelines, body } => {
54 if let Some(interpreter) = guess_by_interpreter(&body) {
55 return Ok(Guess::Kind(interpreter.to_string()));
56 }
57 if let Some(lang) = guess_by_modeline(&modelines) {
58 return Ok(Guess::Kind(lang.to_string()));
59 }
60
61 if let Some(ext_vec) = optional_extensions {
62 for ext in ext_vec {
63 if let Some(lang) = guess_by_heuristic(&ext, &body) {
64 return Ok(Guess::Kind(lang.to_string()));
65 }
66
67 if let Some(lang) = guess_by_extensions(&ext) {
68 return Ok(Guess::Kind(lang.to_string()));
69 }
70 }
71 }
72 }
73 }
74 Ok(Guess::Unknown)
75}
76
77fn extensions(path: &Path) -> Option<Vec<String>> {
78 if let Some(os_str) = path.file_name() {
79 if let Some(filename) = os_str.to_str() {
80 let mut result = Vec::with_capacity(2);
81 let mut ext1 = String::new();
82 for c in filename.chars().rev() {
83 ext1.insert(0, c);
84 if c == '.' {
85 break;
86 }
87 }
88 if ext1.len() == filename.len() {
89 return None;
90 }
91
92 let mut ext2 = ext1.clone();
93 let new_end = filename.len() - ext1.len();
94 for c in filename[0..new_end].chars().rev() {
95 ext2.insert(0, c);
96 if c == '.' {
97 break;
98 }
99 }
100
101 if !ext2.is_empty() && ext2.len() != filename.len() {
102 ext2 = ext2.to_lowercase();
103 result.push(ext2);
104 }
105 ext1 = ext1.to_lowercase();
106 result.push(ext1);
107 return Some(result);
108 }
109 }
110 None
111}
112
113#[cfg(test)]
114#[cfg(not(tarpaulin_include))]
115#[test]
116fn test_extensions() {
117 let path = Path::new("foo/bar.js");
118 let expected = ".js";
119 let actual = &extensions(&path).unwrap()[0];
120 assert_eq!(expected, actual);
121
122 let path = Path::new("foo/bar.Js");
123 let expected = ".js";
124 let actual = &extensions(&path).unwrap()[0];
125 assert_eq!(expected, actual);
126
127 let path = Path::new(".gitignore");
128 let expected: Option<Vec<String>> = None;
129 let actual = extensions(&path);
130 assert_eq!(expected, actual);
131
132 let path = Path::new(".gitignore.js");
133 let expected = ".js";
134 let actual = &extensions(&path).unwrap()[0];
135 assert_eq!(expected, actual);
136
137 let path = Path::new("libfoo.dll.config");
138 let expected = vec![".dll.config".to_string(), ".config".to_string()];
139 let actual = &extensions(&path).unwrap();
140 assert_eq!(expected[0], actual[0]);
141 assert_eq!(expected[1], actual[1]);
142}