code_moniker_workspace/
lang.rs1use std::path::Path;
2
3use thiserror::Error;
4
5use code_moniker_core::lang::Lang;
6
7#[derive(Debug, Error)]
8pub enum LangError {
9 #[error(
10 "unsupported file extension `.{0}` (known: ts/tsx/js/jsx/mjs/cjs, rs, java, py/pyi, go, c/h, cs, sql/sql.in/plpgsql)"
11 )]
12 UnknownExtension(String),
13 #[error("file has no extension; cannot infer language")]
14 NoExtension,
15}
16
17pub fn path_to_lang(path: &Path) -> Result<Lang, LangError> {
18 if path
19 .file_name()
20 .and_then(|name| name.to_str())
21 .is_some_and(|name| name.to_ascii_lowercase().ends_with(".sql.in"))
22 {
23 return Ok(Lang::Sql);
24 }
25 let ext = path
26 .extension()
27 .and_then(|s| s.to_str())
28 .map(|s| s.to_ascii_lowercase());
29 let ext = match ext.as_deref() {
30 Some("") | None => return Err(LangError::NoExtension),
31 Some(e) => e,
32 };
33 match ext {
34 "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs" => Ok(Lang::Ts),
35 "rs" => Ok(Lang::Rs),
36 "java" => Ok(Lang::Java),
37 "py" | "pyi" => Ok(Lang::Python),
38 "go" => Ok(Lang::Go),
39 "c" | "h" => Ok(Lang::C),
40 "cs" => Ok(Lang::Cs),
41 "sql" | "plpgsql" => Ok(Lang::Sql),
42 other => Err(LangError::UnknownExtension(other.to_string())),
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use std::path::PathBuf;
50
51 fn dispatch(s: &str) -> Result<Lang, LangError> {
52 path_to_lang(&PathBuf::from(s))
53 }
54
55 #[test]
56 fn ts_family_resolves_to_ts() {
57 for p in &[
58 "x.ts",
59 "x.tsx",
60 "x.js",
61 "x.jsx",
62 "x.mjs",
63 "x.cjs",
64 "a/b/c/x.TS",
65 ] {
66 assert_eq!(dispatch(p).unwrap(), Lang::Ts, "{p}");
67 }
68 }
69
70 #[test]
71 fn each_supported_extension_resolves() {
72 assert_eq!(dispatch("a.rs").unwrap(), Lang::Rs);
73 assert_eq!(dispatch("a.java").unwrap(), Lang::Java);
74 assert_eq!(dispatch("a.py").unwrap(), Lang::Python);
75 assert_eq!(dispatch("a.pyi").unwrap(), Lang::Python);
76 assert_eq!(dispatch("a.go").unwrap(), Lang::Go);
77 assert_eq!(dispatch("a.c").unwrap(), Lang::C);
78 assert_eq!(dispatch("a.h").unwrap(), Lang::C);
79 assert_eq!(dispatch("a.cs").unwrap(), Lang::Cs);
80 }
81
82 #[test]
83 fn unknown_extension_errors() {
84 match dispatch("a.txt") {
85 Err(LangError::UnknownExtension(s)) => assert_eq!(s, "txt"),
86 other => panic!("unexpected: {other:?}"),
87 }
88 }
89
90 #[test]
91 fn missing_extension_errors() {
92 match dispatch("Makefile") {
93 Err(LangError::NoExtension) => {}
94 other => panic!("unexpected: {other:?}"),
95 }
96 }
97
98 #[test]
99 fn case_is_insensitive() {
100 assert_eq!(dispatch("X.JAVA").unwrap(), Lang::Java);
101 assert_eq!(dispatch("X.RS").unwrap(), Lang::Rs);
102 assert_eq!(dispatch("X.H").unwrap(), Lang::C);
103 }
104
105 #[test]
106 fn sql_extension_resolves() {
107 assert_eq!(dispatch("a.sql").unwrap(), Lang::Sql);
108 assert_eq!(dispatch("a.plpgsql").unwrap(), Lang::Sql);
109 assert_eq!(dispatch("extension.SQL.IN").unwrap(), Lang::Sql);
110 }
111}