use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use super::TESTIFY_INDIRECT_DEPS;
fn parse_go_mod_requires(go_mod_text: &str) -> BTreeMap<String, String> {
let mut requires = BTreeMap::new();
let mut in_require_block = false;
for raw_line in go_mod_text.lines() {
let line = raw_line.split("//").next().unwrap_or(raw_line).trim();
if line.is_empty() {
continue;
}
if in_require_block {
if line == ")" {
in_require_block = false;
continue;
}
if let Some((module, version)) = parse_module_version_pair(line) {
requires.insert(module, version);
}
continue;
}
if line == "require (" {
in_require_block = true;
continue;
}
if let Some(rest) = line.strip_prefix("require ")
&& let Some((module, version)) = parse_module_version_pair(rest)
{
requires.insert(module, version);
}
}
requires
}
fn parse_module_version_pair(text: &str) -> Option<(String, String)> {
let mut parts = text.split_whitespace();
let module = parts.next()?;
let version = parts.next()?;
Some((module.to_string(), version.to_string()))
}
pub(super) fn resolve_indirect_requires(
dependency_go_mod_dir: Option<&Path>,
direct_modules: &BTreeSet<String>,
) -> Vec<(String, String)> {
let mut merged: BTreeMap<String, String> = TESTIFY_INDIRECT_DEPS
.iter()
.map(|(module, version)| ((*module).to_string(), (*version).to_string()))
.collect();
if let Some(dir) = dependency_go_mod_dir {
let go_mod_path = dir.join("go.mod");
match std::fs::read_to_string(&go_mod_path) {
Ok(text) => {
for (module, version) in parse_go_mod_requires(&text) {
merged.insert(module, version);
}
}
Err(error) => {
tracing::debug!(
path = %go_mod_path.display(),
%error,
"could not read dependency go.mod for indirect requires; falling back to testify-only set"
);
}
}
}
merged.retain(|module, _| !direct_modules.contains(module));
merged.into_iter().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_go_mod_requires_reads_single_line_and_block_forms() {
let text = "module example.com/foo\n\ngo 1.26\n\nrequire github.com/tree-sitter/go-tree-sitter v0.25.0\n\nrequire (\n\tgithub.com/mattn/go-pointer v0.0.1 // indirect\n)\n";
let requires = parse_go_mod_requires(text);
assert_eq!(
requires
.get("github.com/tree-sitter/go-tree-sitter")
.map(String::as_str),
Some("v0.25.0")
);
assert_eq!(
requires.get("github.com/mattn/go-pointer").map(String::as_str),
Some("v0.0.1")
);
assert_eq!(requires.len(), 2, "unexpected requires: {requires:?}");
}
#[test]
fn parse_go_mod_requires_returns_empty_for_bare_module() {
let text = "module example.com/bare\n\ngo 1.26\n";
assert!(parse_go_mod_requires(text).is_empty());
}
fn direct_modules(paths: &[&str]) -> BTreeSet<String> {
paths.iter().map(|p| (*p).to_string()).collect()
}
#[test]
fn resolve_indirect_requires_falls_back_to_testify_only_when_dir_is_none() {
let resolved = resolve_indirect_requires(None, &direct_modules(&["github.com/example/mylib"]));
let expected: Vec<(String, String)> = TESTIFY_INDIRECT_DEPS
.iter()
.map(|(m, v)| ((*m).to_string(), (*v).to_string()))
.collect();
assert_eq!(resolved, expected);
}
#[test]
fn resolve_indirect_requires_falls_back_to_testify_only_when_go_mod_missing() {
let dir = tempfile::tempdir().expect("create temp dir");
let resolved = resolve_indirect_requires(Some(dir.path()), &direct_modules(&["github.com/example/mylib"]));
let expected: Vec<(String, String)> = TESTIFY_INDIRECT_DEPS
.iter()
.map(|(m, v)| ((*m).to_string(), (*v).to_string()))
.collect();
assert_eq!(resolved, expected);
}
#[test]
fn resolve_indirect_requires_merges_dependency_go_mod_requires() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("go.mod"),
"module github.com/xberg-io/tree-sitter-language-pack/packages/go\n\n\
go 1.26\n\n\
require github.com/tree-sitter/go-tree-sitter v0.25.0\n\n\
require github.com/mattn/go-pointer v0.0.1 // indirect\n",
)
.expect("write fixture go.mod");
let resolved = resolve_indirect_requires(
Some(dir.path()),
&direct_modules(&[
"github.com/xberg-io/tree-sitter-language-pack/packages/go",
"github.com/stretchr/testify",
]),
);
assert_eq!(
resolved,
vec![
("github.com/davecgh/go-spew".to_string(), "v1.1.1".to_string()),
("github.com/mattn/go-pointer".to_string(), "v0.0.1".to_string()),
("github.com/pmezard/go-difflib".to_string(), "v1.0.0".to_string()),
(
"github.com/tree-sitter/go-tree-sitter".to_string(),
"v0.25.0".to_string()
),
("gopkg.in/yaml.v3".to_string(), "v3.0.1".to_string()),
],
"expected testify indirects merged alphabetically with the dependency's own requires"
);
}
#[test]
fn resolve_indirect_requires_excludes_self_reference() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("go.mod"),
"module github.com/example/mylib\n\ngo 1.26\n\nrequire github.com/example/mylib v1.0.0\n",
)
.expect("write fixture go.mod");
let resolved = resolve_indirect_requires(Some(dir.path()), &direct_modules(&["github.com/example/mylib"]));
assert!(
!resolved.iter().any(|(module, _)| module == "github.com/example/mylib"),
"a module must not appear as its own indirect require, got: {resolved:?}"
);
}
#[test]
fn resolve_indirect_requires_excludes_modules_already_required_directly() {
let dir = tempfile::tempdir().expect("create temp dir");
std::fs::write(
dir.path().join("go.mod"),
"module github.com/xberg-io/tree-sitter-language-pack/packages/go\n\n\
go 1.26\n\n\
require github.com/tree-sitter/go-tree-sitter v0.25.0\n\n\
require github.com/mattn/go-pointer v0.0.1 // indirect\n",
)
.expect("write fixture go.mod");
let resolved = resolve_indirect_requires(
Some(dir.path()),
&direct_modules(&[
"github.com/xberg-io/tree-sitter-language-pack/packages/go",
"github.com/stretchr/testify",
"github.com/tree-sitter/go-tree-sitter",
]),
);
assert!(
!resolved
.iter()
.any(|(module, _)| module == "github.com/tree-sitter/go-tree-sitter"),
"a module already required directly must not also appear as `// indirect`, got: {resolved:?}"
);
assert!(
resolved
.iter()
.any(|(module, _)| module == "github.com/mattn/go-pointer"),
"go-pointer (only indirect) should still be included, got: {resolved:?}"
);
}
}