use super::{FileIndex, LanguageResolver};
use crate::analysis::parser::ImportStatement;
use crate::analysis::walker::Language;
pub struct HaskellResolver;
impl LanguageResolver for HaskellResolver {
fn resolve(
&self,
import: &ImportStatement,
_importing_file: &str,
file_index: &FileIndex,
) -> Option<String> {
resolve_haskell(&import.path, file_index)
}
fn language(&self) -> Language {
Language::Haskell
}
fn name(&self) -> &'static str {
"haskell"
}
}
fn resolve_haskell(module_path: &str, file_index: &FileIndex) -> Option<String> {
if is_haskell_stdlib(module_path) {
return None;
}
let rel = module_path.replace('.', "/");
let direct = format!("{rel}.hs");
if file_index.contains(&direct) {
return Some(direct);
}
let src = format!("src/{rel}.hs");
if file_index.contains(&src) {
return Some(src);
}
let app = format!("app/{rel}.hs");
if file_index.contains(&app) {
return Some(app);
}
let lhs = format!("{rel}.lhs");
if file_index.contains(&lhs) {
return Some(lhs);
}
let src_lhs = format!("src/{rel}.lhs");
if file_index.contains(&src_lhs) {
return Some(src_lhs);
}
None
}
fn is_haskell_stdlib(module: &str) -> bool {
let mut parts = module.splitn(3, '.');
let first = parts.next().unwrap_or("");
match first {
"GHC" | "Prelude" | "Foreign" | "Numeric" | "Debug" | "Unsafe" | "Type" => return true,
"Data" | "Control" | "System" | "Text" => {}
_ => return false,
}
let second = match parts.next() {
Some(s) => s,
None => return false,
};
match first {
"Data" => matches!(
second,
"Bifoldable"
| "Bifunctor"
| "Bitraversable"
| "Bits"
| "Bool"
| "Char"
| "Coerce"
| "Complex"
| "Data"
| "Dynamic"
| "Either"
| "Eq"
| "Fixed"
| "Foldable"
| "Function"
| "Functor"
| "IORef"
| "Int"
| "Ix"
| "Kind"
| "List"
| "Maybe"
| "Monoid"
| "Ord"
| "Proxy"
| "Ratio"
| "STRef"
| "Semigroup"
| "String"
| "Traversable"
| "Tuple"
| "Type"
| "Typeable"
| "Unique"
| "Void"
| "Version"
| "Word"
| "Map"
| "Set"
| "IntMap"
| "IntSet"
| "Sequence"
| "Tree"
| "Graph"
| "ByteString"
| "Text"
| "Array"
| "Time"
),
"Control" => matches!(
second,
"Applicative"
| "Arrow"
| "Category"
| "Concurrent"
| "Exception"
| "Monad"
| "DeepSeq"
),
"System" => matches!(
second,
"CPUTime"
| "Console"
| "Environment"
| "Exit"
| "IO"
| "Info"
| "Mem"
| "Posix"
| "Timeout"
| "FilePath"
| "Directory"
| "Process"
| "Random"
),
"Text" => matches!(
second,
"ParserCombinators" | "Printf" | "Read" | "Show"
| "PrettyPrint"
| "Parsec"
| "Regex"
),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::parser::import::ImportKind;
fn idx(paths: &[&str]) -> FileIndex {
FileIndex::new(paths.iter().map(|s| s.to_string()))
}
fn import(path: &str) -> ImportStatement {
ImportStatement::new(path, ImportKind::Normal, 1)
}
#[test]
fn stdlib_skipped() {
let file_index = idx(&["src/Main.hs"]);
assert_eq!(
HaskellResolver.resolve(&import("Data.List"), "src/Main.hs", &file_index),
None
);
assert_eq!(
HaskellResolver.resolve(&import("Control.Monad"), "src/Main.hs", &file_index),
None
);
assert_eq!(
HaskellResolver.resolve(&import("Prelude"), "src/Main.hs", &file_index),
None
);
}
#[test]
fn local_module_resolves_under_src() {
let file_index = idx(&["src/Main.hs", "src/MyLib/Utils.hs"]);
let result = HaskellResolver.resolve(&import("MyLib.Utils"), "src/Main.hs", &file_index);
assert_eq!(result, Some("src/MyLib/Utils.hs".into()));
}
#[test]
fn local_module_resolves_at_root() {
let file_index = idx(&["Main.hs", "Lib/Helper.hs"]);
let result = HaskellResolver.resolve(&import("Lib.Helper"), "Main.hs", &file_index);
assert_eq!(result, Some("Lib/Helper.hs".into()));
}
#[test]
fn literate_haskell_resolves() {
let file_index = idx(&["src/Main.hs", "src/MyLib/Doc.lhs"]);
let result = HaskellResolver.resolve(&import("MyLib.Doc"), "src/Main.hs", &file_index);
assert_eq!(result, Some("src/MyLib/Doc.lhs".into()));
}
#[test]
fn nonexistent_returns_none() {
let file_index = idx(&["src/Main.hs"]);
assert_eq!(
HaskellResolver.resolve(&import("Missing.Module"), "src/Main.hs", &file_index),
None
);
}
#[test]
fn data_list_is_stdlib() {
assert!(is_haskell_stdlib("Data.List"));
assert!(is_haskell_stdlib("Data.List.NonEmpty"));
}
#[test]
fn data_map_is_stdlib() {
assert!(is_haskell_stdlib("Data.Map"));
assert!(is_haskell_stdlib("Data.Map.Strict"));
}
#[test]
fn data_aeson_is_not_stdlib() {
assert!(!is_haskell_stdlib("Data.Aeson"));
assert!(!is_haskell_stdlib("Data.Aeson.Types"));
}
#[test]
fn data_aeson_types_is_not_stdlib() {
assert!(!is_haskell_stdlib("Data.Aeson.Types.Internal"));
assert!(!is_haskell_stdlib("Data.Aeson.Key"));
}
#[test]
fn control_monad_is_stdlib() {
assert!(is_haskell_stdlib("Control.Monad"));
assert!(is_haskell_stdlib("Control.Monad.IO.Class"));
assert!(is_haskell_stdlib("Control.Exception"));
}
#[test]
fn user_module_is_not_stdlib() {
assert!(!is_haskell_stdlib("MyApp.Foo"));
assert!(!is_haskell_stdlib("Lib.Internal.Utils"));
assert!(!is_haskell_stdlib("Network.HTTP"));
}
}