use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::config::Settings;
use crate::project_resolver::{
ResolutionResult, Sha256Hash,
helpers::{compute_config_shas, extract_language_config_paths, is_language_enabled},
memo::ResolutionMemo,
persist::{ResolutionIndex, ResolutionPersistence, ResolutionRules},
provider::ProjectResolutionProvider,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GoModPath(PathBuf);
impl GoModPath {
pub fn new(path: PathBuf) -> Self {
Self(path)
}
pub fn as_path(&self) -> &PathBuf {
&self.0
}
}
#[derive(Debug, Clone, Default)]
pub struct GoModInfo {
pub module_name: Option<String>,
pub go_version: Option<String>,
}
pub struct GoProvider {
#[allow(dead_code)]
memo: ResolutionMemo<HashMap<GoModPath, Sha256Hash>>,
}
impl Default for GoProvider {
fn default() -> Self {
Self::new()
}
}
impl GoProvider {
pub fn new() -> Self {
Self {
memo: ResolutionMemo::new(),
}
}
pub fn module_path_for_file(&self, file_path: &Path) -> Option<String> {
let codanna_dir = Path::new(crate::init::local_dir_name());
let persistence = ResolutionPersistence::new(codanna_dir);
let index = persistence.load("go").ok()?;
let canon_file = file_path.canonicalize().ok()?;
let config_path = index.get_config_for_file(&canon_file)?;
let rules = index.rules.get(config_path)?;
let module_name = rules.base_url.as_ref()?;
for root_path in rules.paths.keys() {
let root = Path::new(root_path);
let canon_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
if let Ok(relative) = canon_file.strip_prefix(&canon_root) {
let dir_path = relative.parent()?.to_string_lossy();
if dir_path.is_empty() {
return Some(module_name.clone());
} else {
return Some(format!("{module_name}/{dir_path}"));
}
}
}
None
}
fn parse_go_mod(&self, go_mod_path: &Path) -> ResolutionResult<GoModInfo> {
use std::fs;
let content = fs::read_to_string(go_mod_path).map_err(|e| {
crate::project_resolver::ResolutionError::IoError {
path: go_mod_path.to_path_buf(),
cause: e.to_string(),
}
})?;
let mut info = GoModInfo::default();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("//") {
continue;
}
if let Some(module_name) = line.strip_prefix("module ") {
info.module_name = Some(module_name.trim().to_string());
}
else if let Some(go_version) = line.strip_prefix("go ") {
info.go_version = Some(go_version.trim().to_string());
}
}
Ok(info)
}
fn build_rules_for_config(&self, config_path: &Path) -> ResolutionResult<ResolutionRules> {
let go_mod_info = self.parse_go_mod(config_path)?;
let project_root = config_path.parent().unwrap_or(Path::new(".")).to_path_buf();
let mut paths = HashMap::new();
paths.insert(project_root.to_string_lossy().to_string(), Vec::new());
Ok(ResolutionRules {
base_url: go_mod_info.module_name,
paths,
})
}
}
impl ProjectResolutionProvider for GoProvider {
fn language_id(&self) -> &'static str {
"go"
}
fn is_enabled(&self, settings: &Settings) -> bool {
is_language_enabled(settings, "go")
}
fn config_paths(&self, settings: &Settings) -> Vec<PathBuf> {
extract_language_config_paths(settings, "go")
}
fn compute_shas(&self, configs: &[PathBuf]) -> ResolutionResult<HashMap<PathBuf, Sha256Hash>> {
compute_config_shas(configs)
}
fn rebuild_cache(&self, settings: &Settings) -> ResolutionResult<()> {
let config_paths = self.config_paths(settings);
if config_paths.is_empty() {
return Ok(());
}
let persistence = ResolutionPersistence::new(Path::new(crate::init::local_dir_name()));
let mut index = ResolutionIndex::new();
for config_path in &config_paths {
if !config_path.exists() {
continue;
}
let rules = self.build_rules_for_config(config_path)?;
let project_dir = config_path.parent().unwrap_or(Path::new("."));
let pattern = format!("{}/**/*.go", project_dir.display());
index.mappings.insert(pattern, config_path.clone());
index.rules.insert(config_path.clone(), rules);
}
let shas = self.compute_shas(&config_paths)?;
for (path, sha) in shas {
index.hashes.insert(path, sha.0);
}
persistence.save("go", &index)?;
Ok(())
}
fn select_affected_files(&self, _settings: &Settings) -> Vec<PathBuf> {
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_parse_go_mod_extracts_module_name() {
let temp_dir = TempDir::new().unwrap();
let go_mod_path = temp_dir.path().join("go.mod");
let go_mod_content = r#"module github.com/gin-gonic/gin
go 1.21
require (
github.com/bytedance/sonic v1.9.1
github.com/go-playground/validator/v10 v10.14.0
)
"#;
fs::write(&go_mod_path, go_mod_content).unwrap();
let provider = GoProvider::new();
let info = provider.parse_go_mod(&go_mod_path).unwrap();
assert_eq!(
info.module_name,
Some("github.com/gin-gonic/gin".to_string())
);
assert_eq!(info.go_version, Some("1.21".to_string()));
}
#[test]
fn test_parse_go_mod_handles_minimal_file() {
let temp_dir = TempDir::new().unwrap();
let go_mod_path = temp_dir.path().join("go.mod");
let go_mod_content = "module example.com/myproject\n";
fs::write(&go_mod_path, go_mod_content).unwrap();
let provider = GoProvider::new();
let info = provider.parse_go_mod(&go_mod_path).unwrap();
assert_eq!(info.module_name, Some("example.com/myproject".to_string()));
assert_eq!(info.go_version, None);
}
#[test]
fn test_build_rules_sets_base_url_to_module_name() {
let temp_dir = TempDir::new().unwrap();
let go_mod_path = temp_dir.path().join("go.mod");
let go_mod_content = "module github.com/user/repo\ngo 1.21\n";
fs::write(&go_mod_path, go_mod_content).unwrap();
let provider = GoProvider::new();
let rules = provider.build_rules_for_config(&go_mod_path).unwrap();
assert_eq!(rules.base_url, Some("github.com/user/repo".to_string()));
assert_eq!(rules.paths.len(), 1);
assert!(
rules
.paths
.keys()
.any(|k| k.contains(temp_dir.path().to_str().unwrap()))
);
}
#[test]
#[ignore = "Requires filesystem isolation (changes cwd, conflicts with parallel tests)"]
fn test_rebuild_cache_creates_resolution_json() {
let temp_dir = TempDir::new().unwrap();
let go_mod_path = temp_dir.path().join("go.mod");
let codanna_dir = temp_dir.path().join(crate::init::local_dir_name());
let go_mod_content = "module github.com/test/project\ngo 1.21\n";
fs::write(&go_mod_path, go_mod_content).unwrap();
let settings_content = format!(
r#"
[languages.go]
enabled = true
config_files = ["{}"]
"#,
go_mod_path.display()
);
let settings: Settings = toml::from_str(&settings_content).unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&temp_dir).unwrap();
fs::create_dir_all(&codanna_dir).unwrap();
let provider = GoProvider::new();
provider.rebuild_cache(&settings).unwrap();
std::env::set_current_dir(&original_dir).unwrap();
let cache_path = codanna_dir.join("index/resolvers/go_resolution.json");
assert!(
cache_path.exists(),
"Cache file should exist at {}",
cache_path.display()
);
let cache_content = fs::read_to_string(&cache_path).unwrap();
assert!(
cache_content.contains("github.com/test/project"),
"Cache should contain module name"
);
}
#[test]
fn test_provider_language_id() {
let provider = GoProvider::new();
assert_eq!(provider.language_id(), "go");
}
#[test]
fn test_provider_uses_helpers_for_settings() {
let provider = GoProvider::new();
let settings = Settings::default();
assert!(provider.is_enabled(&settings)); assert!(provider.config_paths(&settings).is_empty()); }
}