use std::collections::HashMap;
use std::fs;
use std::path::Path;
use crate::extract;
use crate::graph::{self, AnyFragment, Fragment};
use crate::manifest::Value;
use crate::markup;
use crate::parse;
fn is_csharp(rel: &str) -> bool {
rel.ends_with(".cs")
}
pub(super) fn is_graph_source(rel: &str) -> bool {
is_csharp(rel)
|| markup::is_markup(rel)
|| parse::ts_grammar_for(rel).is_some()
|| parse::is_rust(rel)
}
fn read_source_lossy(path: &Path) -> Option<String> {
fs::read(path)
.ok()
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
}
pub(super) fn carries_best_source(entry: &Value, rel: &str) -> bool {
let source = entry
.get("source")
.and_then(Value::as_str)
.unwrap_or("heuristic");
if source == "agent" {
return true;
}
if parse::is_ts_js(rel) || parse::is_rust(rel) {
return source == "ast" || source == "ast-none";
}
source == if is_csharp(rel) { "ast" } else { "heuristic" }
}
pub(super) enum ParsedFile {
CSharp(String, extract::Extraction),
TsJs(String, Option<String>, extract::TsFragment),
Markup(String, Fragment),
Rust(String, Option<String>, extract::RsFragment),
}
pub(super) fn reparse(root: &Path, rel: &str) -> Option<ParsedFile> {
if is_csharp(rel) {
return read_source_lossy(&root.join(rel))
.map(|src| ParsedFile::CSharp(rel.to_string(), extract::extract(&src)));
}
if markup::is_markup(rel) {
return graph::markup_fragment(root, rel).map(|f| ParsedFile::Markup(rel.to_string(), f));
}
if parse::is_rust(rel) {
let rs = extract::extract_rs_file(&read_source_lossy(&root.join(rel))?);
return Some(ParsedFile::Rust(rel.to_string(), rs.purpose, rs.fragment));
}
let grammar = parse::ts_grammar_for(rel)?;
let src = read_source_lossy(&root.join(rel))?;
let ts = extract::extract_ts_file(root, rel, &src, grammar)?;
Some(ParsedFile::TsJs(rel.to_string(), ts.purpose, ts.fragment))
}
pub(super) fn fold(
per_file: Vec<Option<ParsedFile>>,
) -> (HashMap<String, String>, HashMap<String, AnyFragment>) {
let mut purposes: HashMap<String, String> = HashMap::new();
let mut fresh_fragments: HashMap<String, AnyFragment> = HashMap::new();
for parsed in per_file.into_iter().flatten() {
match parsed {
ParsedFile::CSharp(rel, extraction) => {
if let Some(p) = &extraction.purpose {
purposes.insert(rel.clone(), p.clone());
}
fresh_fragments.insert(
rel,
AnyFragment::Cs(graph::fragment_from_extraction(&extraction)),
);
}
ParsedFile::Markup(rel, fragment) => {
fresh_fragments.insert(rel, AnyFragment::Cs(fragment));
}
ParsedFile::TsJs(rel, purpose, fragment) => {
if let Some(p) = purpose {
purposes.insert(rel.clone(), p);
}
fresh_fragments.insert(rel, AnyFragment::Ts(fragment));
}
ParsedFile::Rust(rel, purpose, fragment) => {
if let Some(p) = purpose {
purposes.insert(rel.clone(), p);
}
fresh_fragments.insert(rel, AnyFragment::Rs(fragment));
}
}
}
(purposes, fresh_fragments)
}
pub(super) fn fallback_source(rel: &str) -> &'static str {
if parse::is_ts_js(rel) || parse::is_rust(rel) {
"ast-none"
} else {
"heuristic"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_source_always_carries() {
let e = Value::object(vec![("source", Value::string("agent"))]);
assert!(carries_best_source(&e, "a.cs"));
assert!(carries_best_source(&e, "a.ts"));
}
#[test]
fn ast_source_carries_for_csharp_and_ts_js() {
let e = Value::object(vec![("source", Value::string("ast"))]);
assert!(carries_best_source(&e, "a.cs"));
assert!(carries_best_source(&e, "a.ts"));
assert!(!carries_best_source(&e, "a.json"));
}
#[test]
fn heuristic_source_carries_only_for_non_ast_capable_extensions() {
let e = Value::object(vec![("source", Value::string("heuristic"))]);
assert!(!carries_best_source(&e, "a.cs"));
assert!(!carries_best_source(&e, "a.ts"));
assert!(carries_best_source(&e, "a.json"));
}
#[test]
fn missing_source_defaults_to_heuristic() {
let e = Value::object(vec![("purpose", Value::string("x"))]);
assert!(carries_best_source(&e, "a.json"));
assert!(!carries_best_source(&e, "a.cs"));
assert!(!carries_best_source(&e, "a.ts"));
}
#[test]
fn ast_none_source_carries_for_ts_js_but_not_for_csharp_or_plain_files() {
let e = Value::object(vec![("source", Value::string("ast-none"))]);
assert!(carries_best_source(&e, "a.ts"));
assert!(carries_best_source(&e, "a.tsx"));
assert!(carries_best_source(&e, "a.js"));
assert!(
!carries_best_source(&e, "a.cs"),
"C# never produces ast-none; an entry claiming it must still be reparsed"
);
assert!(!carries_best_source(&e, "a.json"));
}
#[test]
fn a_rust_file_is_a_graph_source() {
assert!(is_graph_source("src/lib.rs"));
assert!(!is_graph_source("src/lib.rs.bak"));
assert!(!is_graph_source("Cargo.toml"));
}
#[test]
fn a_rust_file_expects_ast_or_ast_none_and_never_heuristic() {
for (source, best) in [
("ast", true),
("ast-none", true),
("heuristic", false),
("agent", true),
] {
let e = Value::object(vec![("source", Value::string(source))]);
assert_eq!(carries_best_source(&e, "src/lib.rs"), best, "{source}");
}
assert_eq!(fallback_source("src/lib.rs"), "ast-none");
assert_eq!(fallback_source("src/A.cs"), "heuristic");
}
}