use crate::error::{CodegraphError, Result};
use crate::graph::{FileFacts, FileFactsValidationContext, validate_file_facts_with_context};
use crate::lang::Language;
#[cfg(feature = "c")]
use super::CExtractor;
#[cfg(feature = "csharp")]
use super::CSharpExtractor;
#[cfg(feature = "cpp")]
use super::CppExtractor;
#[cfg(feature = "dart")]
use super::DartExtractor;
#[cfg(feature = "go")]
use super::GoExtractor;
#[cfg(feature = "hcl")]
use super::HclExtractor;
#[cfg(feature = "java")]
use super::JavaExtractor;
#[cfg(feature = "typescript")]
use super::JavaScriptExtractor;
#[cfg(feature = "kotlin")]
use super::KotlinExtractor;
#[cfg(feature = "lua")]
use super::LuaExtractor;
#[cfg(feature = "luau")]
use super::LuauExtractor;
#[cfg(feature = "pascal")]
use super::PascalExtractor;
#[cfg(feature = "php")]
use super::PhpExtractor;
#[cfg(feature = "python")]
use super::PythonExtractor;
#[cfg(feature = "ruby")]
use super::RubyExtractor;
#[cfg(feature = "rust")]
use super::RustExtractor;
#[cfg(feature = "scala")]
use super::ScalaExtractor;
#[cfg(feature = "shell")]
use super::ShellExtractor;
#[cfg(feature = "solidity")]
use super::SolidityExtractor;
#[cfg(feature = "sql")]
use super::SqlExtractor;
#[cfg(feature = "svelte")]
use super::SvelteExtractor;
#[cfg(feature = "swift")]
use super::SwiftExtractor;
#[cfg(feature = "typescript")]
use super::TypeScriptExtractor;
pub trait Extractor {
fn lang(&self) -> Language;
fn extract_facts(&self, source: &str, file: &str) -> Result<FileFacts>;
fn extract_facts_with_bindings(
&self,
source: &str,
file: &str,
rules: &crate::extract::BindingRules,
) -> Result<FileFacts> {
let _ = rules;
self.extract_facts(source, file)
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let facts = self.extract_facts(source, file)?;
normalize_and_validate(facts, self.lang(), source, file)
}
fn extract_with_bindings(
&self,
source: &str,
file: &str,
rules: &crate::extract::BindingRules,
) -> Result<FileFacts> {
let facts = self.extract_facts_with_bindings(source, file, rules)?;
normalize_and_validate(facts, self.lang(), source, file)
}
}
fn normalize_and_validate(
mut facts: FileFacts,
lang: Language,
source: &str,
file: &str,
) -> Result<FileFacts> {
dedupe_symbol_identities(&mut facts);
validate_file_facts_with_context(
&facts,
FileFactsValidationContext {
expected_file: file,
expected_language: lang,
source_len: source.len(),
},
)?;
Ok(facts)
}
pub fn extract_file(lang: Language, source: &str, file: &str) -> Result<FileFacts> {
extract_file_with_bindings(lang, source, file, &super::BindingRules::empty())
}
#[cfg_attr(not(feature = "_extractors"), allow(unused_variables))]
pub fn extract_file_with_bindings(
lang: Language,
source: &str,
file: &str,
rules: &super::BindingRules,
) -> Result<FileFacts> {
#[allow(unreachable_patterns)]
let facts = match lang {
#[cfg(feature = "c")]
Language::C => CExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "csharp")]
Language::CSharp => CSharpExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "cpp")]
Language::Cpp => CppExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "go")]
Language::Go => GoExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "java")]
Language::Java => JavaExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "typescript")]
Language::JavaScript => JavaScriptExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "php")]
Language::Php => PhpExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "python")]
Language::Python => PythonExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "ruby")]
Language::Ruby => RubyExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "rust")]
Language::Rust => RustExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "shell")]
Language::Shell => ShellExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "swift")]
Language::Swift => SwiftExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "kotlin")]
Language::Kotlin => KotlinExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "solidity")]
Language::Solidity => SolidityExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "sql")]
Language::Sql => SqlExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "hcl")]
Language::Hcl => HclExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "typescript")]
Language::TypeScript => TypeScriptExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "scala")]
Language::Scala => ScalaExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "dart")]
Language::Dart => DartExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "lua")]
Language::Lua => LuaExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "luau")]
Language::Luau => LuauExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "pascal")]
Language::Pascal => PascalExtractor.extract_with_bindings(source, file, rules),
#[cfg(feature = "svelte")]
Language::Svelte => SvelteExtractor.extract_with_bindings(source, file, rules),
_ => Err(CodegraphError::UnsupportedLanguage(format!(
"{} (grammar feature disabled)",
lang.as_str()
))),
}?;
Ok(facts)
}
fn dedupe_symbol_identities(facts: &mut FileFacts) {
let mut seen = std::collections::HashSet::with_capacity(facts.symbols.len());
facts
.symbols
.retain(|symbol| seen.insert(symbol.id.clone()));
}
pub fn extract_path(file: &str, source: &str) -> Result<FileFacts> {
extract_path_with_bindings(file, source, &super::BindingRules::empty())
}
pub fn extract_path_with_bindings(
file: &str,
source: &str,
rules: &super::BindingRules,
) -> Result<FileFacts> {
let lang = Language::from_path(file)
.ok_or_else(|| CodegraphError::UnsupportedLanguage(file.to_owned()))?;
extract_file_with_bindings(lang, source, file, rules)
}
#[cfg(all(test, feature = "rust"))]
mod tests {
use super::*;
#[test]
fn cfg_gated_duplicate_definitions_collapse_to_one_symbol() {
let src = r#"
#[cfg(unix)]
fn identity() -> u32 { 1 }
#[cfg(not(unix))]
fn identity() -> u32 { 2 }
"#;
let facts = extract_path("src/lib.rs", src).unwrap();
let matches: Vec<_> = facts
.symbols
.iter()
.filter(|s| s.name == "identity")
.collect();
assert_eq!(
matches.len(),
1,
"cfg-gated duplicate definitions must dedupe to a single symbol, got {matches:?}"
);
}
#[test]
fn a_contract_violating_pass_fails_at_the_extractor_seam() {
struct ViolatingExtractor;
impl Extractor for ViolatingExtractor {
fn lang(&self) -> Language {
Language::Rust
}
fn extract_facts(&self, _source: &str, file: &str) -> Result<FileFacts> {
Ok(FileFacts {
file: file.to_owned(),
lang: "rust".to_owned(),
symbols: Vec::new(),
references: vec![crate::graph::Reference {
name: "helper".to_owned(),
occ: crate::graph::Occurrence {
file: file.to_owned(),
line: 1,
col: 1,
byte: 0,
},
role: crate::graph::RefRole::Call,
source_module: Some("module".to_owned()),
from_path: None,
imported_name: None,
is_reexport: false,
qualifier: None,
scope: None,
type_ref_ctx: None,
cross_artifact: false,
self_receiver: false,
}],
scopes: Vec::new(),
bindings: Vec::new(),
ffi_exports: Vec::new(),
})
}
}
let source = "fn caller() { helper() }\n";
assert!(
ViolatingExtractor
.extract_facts(source, "src/lib.rs")
.is_ok(),
"the raw pass itself is unguarded — the seam is what enforces the contract"
);
let error = ViolatingExtractor
.extract(source, "src/lib.rs")
.expect_err("a contract-violating pass must not escape its extractor");
assert!(
error.to_string().contains("source_module"),
"the failure must name the rule that fired, got {error}"
);
}
#[test]
fn dispatch_returns_context_valid_member_read_facts() {
let source = "struct Entry { extra: i64 }\nfn read(entry: &Entry) -> i64 { entry.extra }\n";
let facts = extract_path("src/entry.rs", source).expect("dispatch extraction");
assert!(facts.references.iter().any(|reference| {
reference.role == crate::graph::RefRole::Read
&& reference.name == "extra"
&& reference.qualifier.as_deref() == Some("entry")
}));
validate_file_facts_with_context(
&facts,
FileFactsValidationContext {
expected_file: "src/entry.rs",
expected_language: Language::Rust,
source_len: source.len(),
},
)
.expect("public dispatch output must satisfy its context contract");
}
#[cfg(feature = "typescript")]
#[test]
fn dispatch_returns_context_valid_typescript_and_javascript_property_reads() {
for (file, language, source) in [
(
"src/entry.ts",
Language::TypeScript,
"interface Entry { extra: number }\nfunction read(entry: Entry) { return entry.extra; }\n",
),
(
"src/entry.js",
Language::JavaScript,
"function read(entry) { return entry.extra; }\n",
),
] {
let facts = extract_path(file, source).expect("dispatch extraction");
assert!(facts.references.iter().any(|reference| {
reference.role == crate::graph::RefRole::Read
&& reference.name == "extra"
&& reference.qualifier.as_deref() == Some("entry")
}));
validate_file_facts_with_context(
&facts,
FileFactsValidationContext {
expected_file: file,
expected_language: language,
source_len: source.len(),
},
)
.expect("public dispatch output must satisfy its context contract");
}
}
}