use std::collections::HashMap;
use std::path::Path;
fn parse_toml_value(rest: &str) -> Option<String> {
let rest = rest.trim();
let rest = rest.strip_prefix('=')?;
let rest = rest.trim();
if let Some(rest) = rest.strip_prefix('"') {
return rest.strip_suffix('"').map(|s| s.to_string());
}
if let Some(rest) = rest.strip_prefix('\'') {
return rest.strip_suffix('\'').map(|s| s.to_string());
}
Some(rest.to_string())
}
pub struct SourceContext<'a> {
pub file_path: &'a Path,
pub rel_path: &'a str,
pub project_root: &'a Path,
}
pub trait RuleSource: Send + Sync {
fn namespace(&self) -> &str;
fn evaluate(&self, ctx: &SourceContext) -> Option<HashMap<String, String>>;
}
#[derive(Default)]
pub struct SourceRegistry {
sources: Vec<Box<dyn RuleSource>>,
}
impl SourceRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, source: Box<dyn RuleSource>) {
self.sources.push(source);
}
pub fn get(&self, ctx: &SourceContext, key: &str) -> Option<String> {
let (ns, field) = key.split_once('.')?;
for source in &self.sources {
if source.namespace() == ns
&& let Some(values) = source.evaluate(ctx)
{
return values.get(field).cloned();
}
}
None
}
}
pub struct EnvSource;
impl RuleSource for EnvSource {
fn namespace(&self) -> &str {
"env"
}
fn evaluate(&self, _ctx: &SourceContext) -> Option<HashMap<String, String>> {
Some(std::env::vars().collect())
}
}
pub struct PathSource;
impl RuleSource for PathSource {
fn namespace(&self) -> &str {
"path"
}
fn evaluate(&self, ctx: &SourceContext) -> Option<HashMap<String, String>> {
let mut result = HashMap::new();
result.insert("rel".to_string(), ctx.rel_path.to_string());
result.insert(
"abs".to_string(),
ctx.file_path.to_string_lossy().to_string(),
);
if let Some(ext) = ctx.file_path.extension() {
result.insert("ext".to_string(), ext.to_string_lossy().to_string());
}
if let Some(name) = ctx.file_path.file_name() {
result.insert("filename".to_string(), name.to_string_lossy().to_string());
}
Some(result)
}
}
pub struct GitSource;
impl RuleSource for GitSource {
fn namespace(&self) -> &str {
"git"
}
fn evaluate(&self, ctx: &SourceContext) -> Option<HashMap<String, String>> {
let mut result = HashMap::new();
if let Some(branch) = gix::discover(ctx.project_root).ok().and_then(|repo| {
repo.head().ok().and_then(|head| {
head.referent_name().map(|name| {
let b = name.as_bstr();
b.strip_prefix(b"refs/heads/")
.map(|s| String::from_utf8_lossy(s).into_owned())
.unwrap_or_else(|| String::from_utf8_lossy(b).into_owned())
})
})
}) {
result.insert("branch".to_string(), branch);
}
if let Ok(repo) = gix::discover(ctx.project_root) {
let is_dirty = repo.is_dirty().unwrap_or(false);
result.insert("dirty".to_string(), is_dirty.to_string());
let is_staged = (|| -> Option<bool> {
let head_id = repo.head_id().ok()?;
let head_commit = head_id.object().ok()?.into_commit();
let head_tree = head_commit.tree().ok()?;
let entry_in_head = head_tree.lookup_entry_by_path(ctx.rel_path).ok().flatten();
let head_blob_id = entry_in_head.map(|e| e.id().detach());
let index = repo.index_or_empty().ok()?;
let rel_bstr: &gix::bstr::BStr = ctx.rel_path.as_bytes().into();
let index_blob_id = index.entry_by_path(rel_bstr).map(|e| e.id);
Some(index_blob_id != head_blob_id)
})()
.unwrap_or(false);
result.insert("staged".to_string(), is_staged.to_string());
}
Some(result)
}
}
pub struct RustSource;
impl RustSource {
fn find_cargo_toml(file_path: &Path) -> Option<std::path::PathBuf> {
let mut current = file_path.parent()?;
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
return Some(cargo_toml);
}
current = current.parent()?;
}
}
fn find_workspace_root(start: &Path) -> Option<std::path::PathBuf> {
let mut current = start.parent()?;
loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists()
&& let Ok(content) = std::fs::read_to_string(&cargo_toml)
&& let Ok(parsed) = content.parse::<toml::Table>()
&& parsed.contains_key("workspace")
{
return Some(cargo_toml);
}
current = current.parent()?;
}
}
fn parse_cargo_toml(cargo_toml_path: &Path) -> HashMap<String, String> {
let mut result = HashMap::new();
let content = match std::fs::read_to_string(cargo_toml_path) {
Ok(c) => c,
Err(_) => return result,
};
let parsed: toml::Table = match content.parse() {
Ok(t) => t,
Err(_) => return result,
};
let package = match parsed.get("package").and_then(|v| v.as_table()) {
Some(p) => p,
None => return result,
};
let keys = ["edition", "resolver", "name", "version"];
let mut workspace_package: Option<&toml::Table> = None;
let mut workspace_parsed: Option<toml::Table> = None;
for key in keys {
if let Some(value) = package.get(key) {
if let Some(table) = value.as_table()
&& table.get("workspace").and_then(|v| v.as_bool()) == Some(true)
{
if workspace_package.is_none() {
if let Some(ws_path) = Self::find_workspace_root(cargo_toml_path)
&& let Ok(ws_content) = std::fs::read_to_string(&ws_path)
&& let Ok(ws_parsed) = ws_content.parse::<toml::Table>()
{
workspace_parsed = Some(ws_parsed);
}
workspace_package = workspace_parsed
.as_ref()
.and_then(|ws| ws.get("workspace"))
.and_then(|w| w.as_table())
.and_then(|w| w.get("package"))
.and_then(|p| p.as_table());
}
if let Some(ws_pkg) = workspace_package
&& let Some(ws_value) = ws_pkg.get(key)
{
if let Some(s) = ws_value.as_str() {
result.insert(key.to_string(), s.to_string());
} else if let Some(i) = ws_value.as_integer() {
result.insert(key.to_string(), i.to_string());
}
}
continue;
}
if let Some(s) = value.as_str() {
result.insert(key.to_string(), s.to_string());
} else if let Some(i) = value.as_integer() {
result.insert(key.to_string(), i.to_string());
}
}
}
result
}
fn is_test_file(ctx: &SourceContext) -> bool {
if normalize_languages::is_test_path(ctx.file_path) {
return true;
}
if let Ok(content) = std::fs::read_to_string(ctx.file_path) {
for line in content.lines().take(50) {
if line.trim().starts_with("#[cfg(test)]") {
return true;
}
}
}
false
}
}
impl RuleSource for RustSource {
fn namespace(&self) -> &str {
"rust"
}
fn evaluate(&self, ctx: &SourceContext) -> Option<HashMap<String, String>> {
let ext = ctx.file_path.extension()?;
if ext != "rs" {
return None;
}
let cargo_toml = Self::find_cargo_toml(ctx.file_path);
let mut result = cargo_toml
.map(|p| Self::parse_cargo_toml(&p))
.unwrap_or_default();
result.insert(
"is_test_file".to_string(),
Self::is_test_file(ctx).to_string(),
);
Some(result)
}
}
pub struct TypeScriptSource;
impl TypeScriptSource {
fn find_tsconfig(file_path: &Path) -> Option<std::path::PathBuf> {
let mut current = file_path.parent()?;
loop {
let tsconfig = current.join("tsconfig.json");
if tsconfig.exists() {
return Some(tsconfig);
}
current = current.parent()?;
}
}
fn find_package_json(file_path: &Path) -> Option<std::path::PathBuf> {
let mut current = file_path.parent()?;
loop {
let pkg = current.join("package.json");
if pkg.exists() {
return Some(pkg);
}
current = current.parent()?;
}
}
fn parse_tsconfig(content: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
for line in content.lines() {
let line = line.trim();
if let Some(value) = Self::extract_json_string(line, "target") {
result.insert("target".to_string(), value);
} else if let Some(value) = Self::extract_json_string(line, "module") {
result.insert("module".to_string(), value);
} else if let Some(value) = Self::extract_json_string(line, "moduleResolution") {
result.insert("moduleResolution".to_string(), value);
} else if line.contains("\"strict\"") {
if line.contains("true") {
result.insert("strict".to_string(), "true".to_string());
} else if line.contains("false") {
result.insert("strict".to_string(), "false".to_string());
}
}
}
result
}
fn parse_package_json(content: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
let mut in_engines = false;
for line in content.lines() {
let line = line.trim();
if line.contains("\"engines\"") {
in_engines = true;
} else if in_engines {
if line.starts_with('}') {
in_engines = false;
} else if let Some(value) = Self::extract_json_string(line, "node") {
result.insert("node_version".to_string(), value);
}
}
if let Some(value) = Self::extract_json_string(line, "name")
&& !result.contains_key("name")
{
result.insert("name".to_string(), value);
}
if let Some(value) = Self::extract_json_string(line, "version")
&& !result.contains_key("version")
{
result.insert("version".to_string(), value);
}
}
result
}
fn extract_json_string(line: &str, key: &str) -> Option<String> {
let pattern = format!("\"{}\"", key);
if !line.contains(&pattern) {
return None;
}
let colon_pos = line.find(':')?;
let after_colon = line[colon_pos + 1..].trim();
if let Some(rest) = after_colon.strip_prefix('"') {
let end = rest.find('"')?;
return Some(rest[..end].to_string());
}
None
}
}
impl RuleSource for TypeScriptSource {
fn namespace(&self) -> &str {
"typescript"
}
fn evaluate(&self, ctx: &SourceContext) -> Option<HashMap<String, String>> {
let ext = ctx.file_path.extension()?.to_string_lossy();
if !matches!(ext.as_ref(), "ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs") {
return None;
}
let mut result = HashMap::new();
if let Some(tsconfig) = Self::find_tsconfig(ctx.file_path)
&& let Ok(content) = std::fs::read_to_string(&tsconfig)
{
result.extend(Self::parse_tsconfig(&content));
}
if let Some(pkg_json) = Self::find_package_json(ctx.file_path)
&& let Ok(content) = std::fs::read_to_string(&pkg_json)
{
result.extend(Self::parse_package_json(&content));
}
result.insert(
"is_test_file".to_string(),
normalize_languages::is_test_path(ctx.file_path).to_string(),
);
Some(result)
}
}
pub struct PythonSource;
impl PythonSource {
fn find_pyproject(file_path: &Path) -> Option<std::path::PathBuf> {
let mut current = file_path.parent()?;
loop {
let pyproject = current.join("pyproject.toml");
if pyproject.exists() {
return Some(pyproject);
}
current = current.parent()?;
}
}
fn parse_pyproject(content: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
for line in content.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("requires-python")
&& let Some(value) = parse_toml_value(rest)
{
let version = value
.trim_start_matches(">=")
.trim_start_matches("<=")
.trim_start_matches("==")
.trim_start_matches('^')
.trim_start_matches('~');
result.insert("requires_python".to_string(), version.to_string());
} else if let Some(rest) = line.strip_prefix("name")
&& let Some(value) = parse_toml_value(rest)
{
result.insert("name".to_string(), value);
} else if let Some(rest) = line.strip_prefix("version")
&& let Some(value) = parse_toml_value(rest)
{
result.insert("version".to_string(), value);
}
}
result
}
}
impl RuleSource for PythonSource {
fn namespace(&self) -> &str {
"python"
}
fn evaluate(&self, ctx: &SourceContext) -> Option<HashMap<String, String>> {
let ext = ctx.file_path.extension()?;
if ext != "py" {
return None;
}
let mut result = HashMap::new();
if let Some(pyproject) = Self::find_pyproject(ctx.file_path)
&& let Ok(content) = std::fs::read_to_string(&pyproject)
{
result.extend(Self::parse_pyproject(&content));
}
result.insert(
"is_test_file".to_string(),
normalize_languages::is_test_path(ctx.file_path).to_string(),
);
Some(result)
}
}
pub struct GoSource;
impl GoSource {
fn find_go_mod(file_path: &Path) -> Option<std::path::PathBuf> {
let mut current = file_path.parent()?;
loop {
let go_mod = current.join("go.mod");
if go_mod.exists() {
return Some(go_mod);
}
current = current.parent()?;
}
}
fn parse_go_mod(content: &str) -> HashMap<String, String> {
let mut result = HashMap::new();
for line in content.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("module ") {
result.insert("module".to_string(), rest.trim().to_string());
}
else if let Some(rest) = line.strip_prefix("go ") {
result.insert("version".to_string(), rest.trim().to_string());
}
}
result
}
}
impl RuleSource for GoSource {
fn namespace(&self) -> &str {
"go"
}
fn evaluate(&self, ctx: &SourceContext) -> Option<HashMap<String, String>> {
let ext = ctx.file_path.extension()?;
if ext != "go" {
return None;
}
let mut result = HashMap::new();
if let Some(go_mod) = Self::find_go_mod(ctx.file_path)
&& let Ok(content) = std::fs::read_to_string(&go_mod)
{
result.extend(Self::parse_go_mod(&content));
}
result.insert(
"is_test_file".to_string(),
normalize_languages::is_test_path(ctx.file_path).to_string(),
);
Some(result)
}
}
pub fn builtin_registry() -> SourceRegistry {
let mut registry = SourceRegistry::new();
registry.register(Box::new(EnvSource));
registry.register(Box::new(PathSource));
registry.register(Box::new(GitSource));
registry.register(Box::new(RustSource));
registry.register(Box::new(TypeScriptSource));
registry.register(Box::new(PythonSource));
registry.register(Box::new(GoSource));
registry
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_env_source() {
unsafe {
std::env::set_var("MOSS_TEST_VAR", "hello");
}
let ctx = SourceContext {
file_path: Path::new("/tmp/test.rs"),
rel_path: "test.rs",
project_root: Path::new("/tmp"),
};
let registry = builtin_registry();
let value = registry.get(&ctx, "env.MOSS_TEST_VAR");
assert_eq!(value, Some("hello".to_string()));
unsafe {
std::env::remove_var("MOSS_TEST_VAR");
}
}
#[test]
fn test_path_source() {
let ctx = SourceContext {
file_path: Path::new("/project/src/lib.rs"),
rel_path: "src/lib.rs",
project_root: Path::new("/project"),
};
let registry = builtin_registry();
assert_eq!(
registry.get(&ctx, "path.rel"),
Some("src/lib.rs".to_string())
);
assert_eq!(registry.get(&ctx, "path.ext"), Some("rs".to_string()));
assert_eq!(
registry.get(&ctx, "path.filename"),
Some("lib.rs".to_string())
);
}
#[test]
fn test_rust_source_parse_cargo_toml() {
let temp_dir = std::env::temp_dir().join("moss_test_cargo_toml");
std::fs::create_dir_all(&temp_dir).unwrap();
let cargo_path = temp_dir.join("Cargo.toml");
let content = r#"
[package]
name = "my-crate"
version = "0.1.0"
edition = "2024"
resolver = "2"
"#;
std::fs::write(&cargo_path, content).unwrap();
let result = RustSource::parse_cargo_toml(&cargo_path);
assert_eq!(result.get("name"), Some(&"my-crate".to_string()));
assert_eq!(result.get("version"), Some(&"0.1.0".to_string()));
assert_eq!(result.get("edition"), Some(&"2024".to_string()));
assert_eq!(result.get("resolver"), Some(&"2".to_string()));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_rust_source_real_file() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let file_path = manifest_dir.join("src/lib.rs");
let ctx = SourceContext {
file_path: &file_path,
rel_path: "src/lib.rs",
project_root: manifest_dir,
};
let registry = builtin_registry();
let edition = registry.get(&ctx, "rust.edition");
assert!(edition.is_some(), "Should find rust.edition");
}
#[test]
fn test_typescript_source_parse_tsconfig() {
let content = r#"{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"moduleResolution": "bundler"
}
}"#;
let result = TypeScriptSource::parse_tsconfig(content);
assert_eq!(result.get("target"), Some(&"ES2020".to_string()));
assert_eq!(result.get("module"), Some(&"ESNext".to_string()));
assert_eq!(result.get("strict"), Some(&"true".to_string()));
assert_eq!(result.get("moduleResolution"), Some(&"bundler".to_string()));
}
#[test]
fn test_typescript_source_parse_package_json() {
let content = r#"{
"name": "my-app",
"version": "1.0.0",
"engines": {
"node": ">=18.0.0"
}
}"#;
let result = TypeScriptSource::parse_package_json(content);
assert_eq!(result.get("name"), Some(&"my-app".to_string()));
assert_eq!(result.get("version"), Some(&"1.0.0".to_string()));
assert_eq!(result.get("node_version"), Some(&">=18.0.0".to_string()));
}
#[test]
fn test_python_source_parse_pyproject() {
let content = r#"
[project]
name = "my-package"
version = "0.1.0"
requires-python = ">=3.10"
"#;
let result = PythonSource::parse_pyproject(content);
assert_eq!(result.get("name"), Some(&"my-package".to_string()));
assert_eq!(result.get("version"), Some(&"0.1.0".to_string()));
assert_eq!(result.get("requires_python"), Some(&"3.10".to_string()));
}
#[test]
fn test_go_source_parse_go_mod() {
let content = r#"module github.com/user/repo
go 1.21
require (
golang.org/x/text v0.3.0
)"#;
let result = GoSource::parse_go_mod(content);
assert_eq!(
result.get("module"),
Some(&"github.com/user/repo".to_string())
);
assert_eq!(result.get("version"), Some(&"1.21".to_string()));
}
#[test]
fn test_rust_is_test_file() {
let ctx = SourceContext {
file_path: Path::new("/project/tests/integration.rs"),
rel_path: "tests/integration.rs",
project_root: Path::new("/project"),
};
assert!(RustSource::is_test_file(&ctx));
let ctx = SourceContext {
file_path: Path::new("/project/src/foo_test.rs"),
rel_path: "src/foo_test.rs",
project_root: Path::new("/project"),
};
assert!(RustSource::is_test_file(&ctx));
let ctx = SourceContext {
file_path: Path::new("/project/src/test_bar.rs"),
rel_path: "src/test_bar.rs",
project_root: Path::new("/project"),
};
assert!(RustSource::is_test_file(&ctx));
let ctx = SourceContext {
file_path: Path::new("/project/src/lib.rs"),
rel_path: "src/lib.rs",
project_root: Path::new("/project"),
};
assert!(!RustSource::is_test_file(&ctx));
}
}