#[derive(Debug, Default, Clone)]
pub(crate) struct GoModules {
modules: Vec<(String, String)>,
}
impl GoModules {
pub(crate) fn from_manifests<'a, I>(manifests: I) -> Self
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
let mut modules: Vec<(String, String)> = Vec::new();
for (path, contents) in manifests {
let Some(module_path) = declared_module_path(contents) else {
continue;
};
let dir = path
.rsplit_once('/')
.map(|(head, _)| head.to_string())
.unwrap_or_default();
modules.push((module_path, dir));
}
modules.sort_by(|a, b| b.0.len().cmp(&a.0.len()).then_with(|| a.0.cmp(&b.0)));
GoModules { modules }
}
pub(crate) fn directory_for(&self, import_path: &str) -> Option<String> {
for (module_path, dir) in &self.modules {
if import_path == module_path {
return Some(dir.clone());
}
if let Some(rest) = import_path.strip_prefix(module_path) {
if let Some(rest) = rest.strip_prefix('/') {
return Some(if dir.is_empty() {
rest.to_string()
} else {
format!("{dir}/{rest}")
});
}
}
}
None
}
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.modules.is_empty()
}
}
fn declared_module_path(contents: &str) -> Option<String> {
for line in contents.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("module ") {
let path = rest.trim().trim_matches('"');
if !path.is_empty() {
return Some(path.to_string());
}
}
}
None
}
pub(crate) fn extract_go_imports(source: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut in_block = false;
for line in source.lines() {
let trimmed = line.trim();
if in_block {
if trimmed.starts_with(')') {
in_block = false;
continue;
}
if let Some(path) = quoted_path(trimmed) {
out.push(path);
}
continue;
}
if trimmed == "import (" || trimmed.starts_with("import (") {
in_block = true;
continue;
}
if let Some(rest) = trimmed.strip_prefix("import ") {
if let Some(path) = quoted_path(rest) {
out.push(path);
}
}
}
out
}
fn quoted_path(text: &str) -> Option<String> {
let (_, rest) = text.split_once('"')?;
let (path, _) = rest.split_once('"')?;
if path.is_empty() {
return None;
}
Some(path.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
const MANIFEST: &str = "module github.com/acme/tool\n\ngo 1.26.1\n";
#[test]
fn block_imports_are_extracted_one_path_per_entry() {
let source = "package runtime\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/acme/tool/internal/store\"\n)\n\nfunc F() {}\n";
assert_eq!(
extract_go_imports(source),
vec![
"fmt".to_string(),
"github.com/acme/tool/internal/store".to_string()
]
);
}
#[test]
fn alias_blank_and_dot_imports_all_yield_their_path() {
let source = "import (\n\tm \"github.com/acme/tool/a\"\n\t_ \"github.com/acme/tool/b\"\n\t. \"github.com/acme/tool/c\"\n)\n";
assert_eq!(
extract_go_imports(source),
vec![
"github.com/acme/tool/a".to_string(),
"github.com/acme/tool/b".to_string(),
"github.com/acme/tool/c".to_string()
]
);
}
#[test]
fn single_line_import_is_extracted_too() {
assert_eq!(extract_go_imports("import \"fmt\"\n"), vec!["fmt"]);
}
#[test]
fn code_after_the_block_is_not_mistaken_for_an_import() {
let source = "import (\n\t\"fmt\"\n)\n\nvar s = \"github.com/acme/tool/nope\"\n";
assert_eq!(extract_go_imports(source), vec!["fmt"]);
}
#[test]
fn import_path_maps_onto_the_declaring_modules_directory() {
let mods = GoModules::from_manifests([("go.mod", MANIFEST)]);
assert_eq!(
mods.directory_for("github.com/acme/tool/internal/store")
.as_deref(),
Some("internal/store")
);
assert_eq!(
mods.directory_for("github.com/acme/tool").as_deref(),
Some("")
);
}
#[test]
fn a_module_in_a_subdirectory_anchors_on_that_subdirectory() {
let mods = GoModules::from_manifests([("sdks/go/go.mod", MANIFEST)]);
assert_eq!(
mods.directory_for("github.com/acme/tool/client").as_deref(),
Some("sdks/go/client")
);
}
#[test]
fn a_nested_module_wins_over_the_one_containing_it() {
let mods = GoModules::from_manifests([
("go.mod", "module github.com/acme/tool\n"),
("plugin/go.mod", "module github.com/acme/tool/plugin\n"),
]);
assert_eq!(
mods.directory_for("github.com/acme/tool/plugin/x")
.as_deref(),
Some("plugin/x")
);
assert_eq!(
mods.directory_for("github.com/acme/tool/internal")
.as_deref(),
Some("internal")
);
}
#[test]
fn an_external_or_stdlib_import_names_no_workspace_directory() {
let mods = GoModules::from_manifests([("go.mod", MANIFEST)]);
assert_eq!(mods.directory_for("fmt"), None);
assert_eq!(mods.directory_for("github.com/other/dep"), None);
assert_eq!(mods.directory_for("github.com/acme/toolkit/x"), None);
}
#[test]
fn a_workspace_with_no_manifest_declares_no_modules() {
let mods = GoModules::from_manifests([("go.mod", "go 1.26.1\n")]);
assert!(mods.is_empty());
assert_eq!(mods.directory_for("github.com/acme/tool/x"), None);
}
}