Skip to main content

code_moniker_workspace/
lang.rs

1use 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}