use crate::snippets::session::ValidationSession;
use std::path::{Path, PathBuf};
pub(super) fn missing_typescript_declaration(session: &ValidationSession) -> Vec<PathBuf> {
let Some(manifest) = session.manifest.as_deref() else {
return Vec::new();
};
let Ok(bytes) = std::fs::read(manifest) else {
return Vec::new();
};
let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes) else {
return Vec::new();
};
if value.get("compilerOptions").is_some() {
return Vec::new();
}
let Some(entry) = value
.get("types")
.or_else(|| value.get("typings"))
.and_then(serde_json::Value::as_str)
else {
return Vec::new();
};
let declaration = manifest.parent().unwrap_or_else(|| Path::new(".")).join(entry);
if declaration.exists() {
Vec::new()
} else {
vec![declaration]
}
}
pub(super) fn missing_zig_ffi_library(session: &ValidationSession) -> Vec<PathBuf> {
let Some(manifest) = session.manifest.as_deref() else {
return Vec::new();
};
super::zig::manifest::unresolvable_ffi_library(manifest)
.ok()
.flatten()
.into_iter()
.collect()
}
pub(super) fn missing_go_library_directories(session: &ValidationSession) -> Vec<PathBuf> {
let project = session
.manifest
.as_deref()
.and_then(Path::parent)
.unwrap_or(&session.working_directory);
let Ok(entries) = std::fs::read_dir(project) else {
return Vec::new();
};
let mut missing = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_none_or(|extension| extension != "go") {
continue;
}
let Ok(source) = std::fs::read_to_string(&path) else {
continue;
};
for directory in cgo_source_relative_library_directories(&source, project) {
if !directory.is_dir() && !missing.contains(&directory) {
missing.push(directory);
}
}
}
missing.sort();
missing
}
fn cgo_source_relative_library_directories(source: &str, project: &Path) -> Vec<PathBuf> {
const SOURCE_DIRECTORY: &str = "${SRCDIR}/";
let mut directories = Vec::new();
for line in source.lines() {
let trimmed = line.trim();
let Some(directive) = trimmed.strip_prefix("//") else {
continue;
};
let directive = directive.trim();
if !directive.starts_with("#cgo ") || !directive.contains("LDFLAGS:") {
continue;
}
for argument in directive.split_whitespace() {
let Some(path) = argument.strip_prefix("-L") else {
continue;
};
let Some(relative) = path.strip_prefix(SOURCE_DIRECTORY) else {
continue;
};
directories.push(normalized(&project.join(relative)));
}
}
directories
}
fn normalized(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
if !out.pop() {
out.push("..");
}
}
std::path::Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snippets::types::Language;
use std::collections::BTreeMap;
fn session(language: Language, working_directory: PathBuf, manifest: Option<PathBuf>) -> ValidationSession {
ValidationSession {
language,
working_directory,
manifest,
fingerprint: "fingerprint".into(),
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
}
}
#[test]
fn a_declared_typescript_declaration_file_that_was_never_emitted_is_reported_missing() {
let package = tempfile::tempdir().expect("package directory");
let manifest = package.path().join("package.json");
std::fs::write(&manifest, r#"{"name":"sample-binding","types":"dist/index.d.ts"}"#).expect("manifest");
let missing = missing_typescript_declaration(&session(
Language::TypeScript,
package.path().to_path_buf(),
Some(manifest),
));
assert_eq!(missing, vec![package.path().join("dist/index.d.ts")]);
}
#[test]
fn a_typescript_package_whose_declaration_exists_reports_nothing_missing() {
let package = tempfile::tempdir().expect("package directory");
let manifest = package.path().join("package.json");
std::fs::write(&manifest, r#"{"name":"sample-binding","types":"index.d.ts"}"#).expect("manifest");
std::fs::write(package.path().join("index.d.ts"), "export {};").expect("declaration");
let missing = missing_typescript_declaration(&session(
Language::TypeScript,
package.path().to_path_buf(),
Some(manifest),
));
assert!(missing.is_empty(), "{missing:?}");
}
#[test]
fn a_tsconfig_session_manifest_claims_no_artifact() {
let project = tempfile::tempdir().expect("project directory");
let manifest = project.path().join("tsconfig.json");
std::fs::write(&manifest, r#"{"compilerOptions":{"strict":true}}"#).expect("manifest");
let missing = missing_typescript_declaration(&session(
Language::TypeScript,
project.path().to_path_buf(),
Some(manifest),
));
assert!(missing.is_empty(), "{missing:?}");
}
#[test]
fn a_cgo_search_directory_the_build_never_produced_is_reported_missing() {
let root = tempfile::tempdir().expect("root directory");
let project = root.path().join("packages/go");
std::fs::create_dir_all(&project).expect("project directory");
std::fs::write(
project.join("bindings.go"),
"package sample\n\n// #cgo LDFLAGS: -L${SRCDIR}/../../target/release -lsample_ffi -lm\nimport \"C\"\n",
)
.expect("go source");
let missing =
missing_go_library_directories(&session(Language::Go, project.clone(), Some(project.join("go.mod"))));
assert_eq!(missing, vec![root.path().join("target/release")]);
}
#[test]
fn a_cgo_directive_whose_search_directory_exists_reports_nothing_missing() {
let root = tempfile::tempdir().expect("root directory");
let project = root.path().join("packages/go");
std::fs::create_dir_all(&project).expect("project directory");
std::fs::create_dir_all(root.path().join("target/release")).expect("build directory");
std::fs::write(
project.join("bindings.go"),
"package sample\n\n// #cgo LDFLAGS: -L${SRCDIR}/../../target/release -lsample_ffi -lm\nimport \"C\"\n",
)
.expect("go source");
let missing =
missing_go_library_directories(&session(Language::Go, project.clone(), Some(project.join("go.mod"))));
assert!(missing.is_empty(), "{missing:?}");
}
#[test]
fn a_cgo_search_directory_outside_the_package_tree_is_never_probed() {
let project = tempfile::tempdir().expect("project directory");
std::fs::write(
project.path().join("bindings.go"),
"package sample\n\n// #cgo LDFLAGS: -L/opt/vendor/lib -Lrelative/lib -lsample_ffi\nimport \"C\"\n",
)
.expect("go source");
let missing = missing_go_library_directories(&session(
Language::Go,
project.path().to_path_buf(),
Some(project.path().join("go.mod")),
));
assert!(missing.is_empty(), "{missing:?}");
}
#[test]
fn a_reported_directory_carries_no_parent_components_an_operator_cannot_act_on() {
let root = tempfile::tempdir().expect("root directory");
let project = root.path().join("packages/go");
let directories = cgo_source_relative_library_directories(
"// #cgo LDFLAGS: -L${SRCDIR}/../../target/release -lsample_ffi",
&project,
);
assert_eq!(directories, vec![root.path().join("target/release")]);
}
}