use once_cell::sync::Lazy;
use regex::Regex;
use scribe_core::{Language, Result, ScribeError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use tree_sitter::{Language as TsLanguage, Node, Parser};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DetectionStrategy {
ExtensionOnly,
ExtensionWithContent,
FullAnalysis,
Custom(CustomDetectionRules),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomDetectionRules {
pub extension_overrides: HashMap<String, Language>,
pub filename_patterns: HashMap<String, Language>,
pub content_signatures: Vec<ContentSignatureConfig>,
pub priority_languages: Vec<Language>,
}
#[derive(Debug, Clone)]
pub struct ContentSignature {
pub language: Language,
pub patterns: Vec<regex::Regex>,
pub weight: f32,
pub required_matches: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentSignatureConfig {
pub language: Language,
pub patterns: Vec<String>,
pub weight: f32,
pub required_matches: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageHints {
pub project_type: Option<ProjectType>,
pub build_files: Vec<String>,
pub directory_structure: Vec<String>,
pub dominant_languages: Vec<Language>,
pub framework_indicators: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProjectType {
WebFrontend,
WebBackend,
MobileApp,
DesktopApp,
SystemsProgram,
DataScience,
GameDevelopment,
EmbeddedSystem,
Library,
Documentation,
Configuration,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectionResult {
pub language: Language,
pub confidence: f32,
pub detection_method: DetectionMethod,
pub alternatives: Vec<(Language, f32)>,
pub evidence: Vec<DetectionEvidence>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DetectionMethod {
FileExtension,
Filename,
Shebang,
ContentSignature,
StatisticalAnalysis,
Hybrid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectionEvidence {
pub evidence_type: EvidenceType,
pub description: String,
pub weight: f32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EvidenceType {
Extension,
Filename,
Shebang,
Keyword,
Syntax,
Import,
Framework,
BuildSystem,
}
pub struct LanguageDetector {
strategy: DetectionStrategy,
extension_map: HashMap<String, Vec<(Language, f32)>>, filename_patterns: HashMap<String, Language>,
content_signatures: HashMap<Language, Vec<ContentSignature>>,
shebang_patterns: HashMap<String, Language>,
ast_parsers: HashMap<Language, Parser>,
syntax_analyzers: HashMap<Language, SyntaxAnalyzer>,
}
#[derive(Debug, Clone)]
struct SyntaxAnalyzer {
language: Language,
keywords: Vec<String>,
structural_patterns: Vec<String>, confidence_weights: HashMap<String, f32>,
}
static TS_LANGUAGES: Lazy<HashMap<Language, fn() -> TsLanguage>> = Lazy::new(|| {
let mut languages = HashMap::new();
languages.insert(
Language::Python,
tree_sitter_python::language as fn() -> TsLanguage,
);
languages.insert(
Language::JavaScript,
tree_sitter_javascript::language as fn() -> TsLanguage,
);
languages.insert(
Language::TypeScript,
tree_sitter_typescript::language_typescript as fn() -> TsLanguage,
);
languages.insert(
Language::Rust,
tree_sitter_rust::language as fn() -> TsLanguage,
);
languages.insert(Language::Go, tree_sitter_go::language as fn() -> TsLanguage);
languages
});
impl Default for DetectionStrategy {
fn default() -> Self {
DetectionStrategy::ExtensionWithContent
}
}
impl Default for LanguageHints {
fn default() -> Self {
Self {
project_type: None,
build_files: Vec::new(),
directory_structure: Vec::new(),
dominant_languages: Vec::new(),
framework_indicators: Vec::new(),
}
}
}
impl LanguageDetector {
pub fn new() -> Self {
let mut detector = Self {
strategy: DetectionStrategy::default(),
extension_map: HashMap::new(),
filename_patterns: HashMap::new(),
content_signatures: HashMap::new(),
shebang_patterns: HashMap::new(),
ast_parsers: HashMap::new(),
syntax_analyzers: HashMap::new(),
};
detector.initialize_detection_rules();
detector
}
pub fn with_strategy(strategy: DetectionStrategy) -> Self {
let mut detector = Self::new();
detector.strategy = strategy;
detector
}
pub fn detect_language(&self, path: &Path) -> Language {
match self.strategy {
DetectionStrategy::ExtensionOnly => self.detect_by_extension(path),
_ => {
self.detect_by_extension_and_filename(path)
}
}
}
pub fn detect_language_with_content(&mut self, path: &Path, content: &str) -> DetectionResult {
match self.strategy {
DetectionStrategy::ExtensionOnly => {
let language = self.detect_by_extension(path);
DetectionResult {
language: language.clone(),
confidence: if language == Language::Unknown {
0.1
} else {
0.9
},
detection_method: DetectionMethod::FileExtension,
alternatives: vec![],
evidence: vec![DetectionEvidence {
evidence_type: EvidenceType::Extension,
description: format!("File extension: {:?}", path.extension()),
weight: 0.9,
}],
}
}
DetectionStrategy::ExtensionWithContent => {
self.detect_with_content_analysis(path, content)
}
DetectionStrategy::FullAnalysis => self.detect_with_full_analysis(path, content),
DetectionStrategy::Custom(ref rules) => {
let rules = rules.clone();
self.detect_with_custom_rules(path, content, &rules)
}
}
}
pub fn detect_with_hints(
&mut self,
path: &Path,
content: &str,
hints: &LanguageHints,
) -> DetectionResult {
let mut base_result = self.detect_language_with_content(path, content);
if let Some(project_type) = &hints.project_type {
base_result = self.apply_project_type_bias(base_result, project_type);
}
if !hints.dominant_languages.is_empty() {
base_result = self.apply_dominant_language_bias(base_result, &hints.dominant_languages);
}
if !hints.framework_indicators.is_empty() {
base_result = self.apply_framework_bias(base_result, &hints.framework_indicators);
}
base_result
}
fn initialize_detection_rules(&mut self) {
self.initialize_extension_map();
self.initialize_filename_patterns();
self.initialize_shebang_patterns();
self.initialize_content_signatures();
self.initialize_ast_parsers();
self.initialize_syntax_analyzers();
}
fn initialize_extension_map(&mut self) {
let extensions = vec![
("rs", vec![(Language::Rust, 1.0)]),
("py", vec![(Language::Python, 0.95)]),
("pyw", vec![(Language::Python, 1.0)]),
("pyi", vec![(Language::Python, 1.0)]),
("js", vec![(Language::JavaScript, 0.9)]),
("jsx", vec![(Language::JavaScript, 1.0)]),
("mjs", vec![(Language::JavaScript, 1.0)]),
("ts", vec![(Language::TypeScript, 1.0)]),
("tsx", vec![(Language::TypeScript, 1.0)]),
("java", vec![(Language::Java, 1.0)]),
("kt", vec![(Language::Kotlin, 1.0)]),
("kts", vec![(Language::Kotlin, 1.0)]),
("scala", vec![(Language::Scala, 1.0)]),
("sc", vec![(Language::Scala, 0.8)]),
("c", vec![(Language::C, 0.9)]),
("h", vec![(Language::C, 0.7), (Language::Cpp, 0.3)]),
("cpp", vec![(Language::Cpp, 1.0)]),
("cxx", vec![(Language::Cpp, 1.0)]),
("cc", vec![(Language::Cpp, 1.0)]),
("hpp", vec![(Language::Cpp, 1.0)]),
("hxx", vec![(Language::Cpp, 1.0)]),
("cs", vec![(Language::CSharp, 1.0)]),
("go", vec![(Language::Go, 1.0)]),
("rb", vec![(Language::Ruby, 1.0)]),
("rbw", vec![(Language::Ruby, 1.0)]),
("php", vec![(Language::PHP, 1.0)]),
("phtml", vec![(Language::PHP, 1.0)]),
("swift", vec![(Language::Swift, 1.0)]),
("dart", vec![(Language::Dart, 1.0)]),
("sh", vec![(Language::Bash, 1.0)]),
("bash", vec![(Language::Bash, 1.0)]),
("zsh", vec![(Language::Bash, 1.0)]),
("fish", vec![(Language::Bash, 1.0)]),
("html", vec![(Language::HTML, 1.0)]),
("htm", vec![(Language::HTML, 1.0)]),
("css", vec![(Language::CSS, 1.0)]),
("scss", vec![(Language::SCSS, 1.0)]),
("sass", vec![(Language::SASS, 1.0)]),
("md", vec![(Language::Markdown, 1.0)]),
("markdown", vec![(Language::Markdown, 1.0)]),
("xml", vec![(Language::XML, 1.0)]),
("json", vec![(Language::JSON, 1.0)]),
("yaml", vec![(Language::YAML, 1.0)]),
("yml", vec![(Language::YAML, 1.0)]),
("toml", vec![(Language::TOML, 1.0)]),
("ini", vec![(Language::Unknown, 1.0)]),
("cfg", vec![(Language::Unknown, 0.8)]),
("conf", vec![(Language::Unknown, 0.7)]),
("sql", vec![(Language::SQL, 1.0)]),
("rst", vec![(Language::Unknown, 1.0)]),
("tex", vec![(Language::Unknown, 1.0)]),
("r", vec![(Language::R, 1.0)]),
("R", vec![(Language::R, 1.0)]),
(
"m",
vec![(Language::ObjectiveC, 0.6), (Language::Matlab, 0.4)],
),
("mm", vec![(Language::ObjectiveC, 1.0)]),
("pl", vec![(Language::Unknown, 0.8)]),
("pm", vec![(Language::Unknown, 1.0)]),
("lua", vec![(Language::Unknown, 1.0)]),
("vim", vec![(Language::Unknown, 1.0)]),
("hs", vec![(Language::Haskell, 1.0)]),
("lhs", vec![(Language::Haskell, 1.0)]),
];
for (ext, languages) in extensions {
self.extension_map.insert(ext.to_string(), languages);
}
}
fn initialize_filename_patterns(&mut self) {
let patterns = vec![
("Makefile", Language::Unknown),
("makefile", Language::Unknown),
("Dockerfile", Language::Unknown),
("dockerfile", Language::Unknown),
("Cargo.toml", Language::TOML),
("Cargo.lock", Language::TOML),
("package.json", Language::JSON),
("tsconfig.json", Language::JSON),
("pyproject.toml", Language::TOML),
("setup.py", Language::Python),
("requirements.txt", Language::Unknown),
("README", Language::Unknown),
("LICENSE", Language::Unknown),
("CHANGELOG", Language::Unknown),
("CMakeLists.txt", Language::Unknown),
(".gitignore", Language::Unknown),
(".dockerignore", Language::Unknown),
("Jenkinsfile", Language::Unknown),
("build.gradle", Language::Unknown),
("pom.xml", Language::XML),
];
for (filename, language) in patterns {
self.filename_patterns
.insert(filename.to_string(), language);
}
}
fn initialize_shebang_patterns(&mut self) {
let patterns = vec![
("python", Language::Python),
("python3", Language::Python),
("python2", Language::Python),
("node", Language::JavaScript),
("bash", Language::Bash),
("sh", Language::Bash),
("zsh", Language::Bash),
("fish", Language::Bash),
("ruby", Language::Ruby),
("php", Language::PHP),
("env python", Language::Python),
("env node", Language::JavaScript),
("env bash", Language::Bash),
("env ruby", Language::Ruby),
];
for (pattern, language) in patterns {
self.shebang_patterns.insert(pattern.to_string(), language);
}
}
fn initialize_content_signatures(&mut self) {
let python_patterns = vec![
r"def\s+\w+\s*\(",
r"import\s+\w+",
r"from\s+\w+\s+import",
r"class\s+\w+\s*\(",
r"__\w+__",
];
if let Ok(compiled_patterns) = self.compile_patterns(python_patterns) {
let python_sigs = vec![ContentSignature {
language: Language::Python,
patterns: compiled_patterns,
weight: 0.9,
required_matches: 2,
}];
self.content_signatures
.insert(Language::Python, python_sigs);
}
let js_patterns = vec![
r"function\s+\w+\s*\(",
r"const\s+\w+\s*=",
r"let\s+\w+\s*=",
r"=>\s*\{",
r"require\s*\(",
r"console\.log\s*\(",
];
if let Ok(compiled_patterns) = self.compile_patterns(js_patterns) {
let js_sigs = vec![ContentSignature {
language: Language::JavaScript,
patterns: compiled_patterns,
weight: 0.8,
required_matches: 2,
}];
self.content_signatures
.insert(Language::JavaScript, js_sigs);
}
let rust_patterns = vec![
r"fn\s+\w+\s*\(",
r"use\s+[\w:]+",
r"struct\s+\w+",
r"impl\s+[\w<>]+",
r"let\s+mut\s+\w+",
r"match\s+\w+\s*\{",
];
if let Ok(compiled_patterns) = self.compile_patterns(rust_patterns) {
let rust_sigs = vec![ContentSignature {
language: Language::Rust,
patterns: compiled_patterns,
weight: 0.95,
required_matches: 2,
}];
self.content_signatures.insert(Language::Rust, rust_sigs);
}
}
fn compile_patterns(&self, patterns: Vec<&str>) -> Result<Vec<regex::Regex>> {
let mut compiled = Vec::new();
for pattern in patterns {
match regex::Regex::new(pattern) {
Ok(regex) => compiled.push(regex),
Err(e) => {
log::warn!("Failed to compile regex pattern '{}': {}", pattern, e);
return Err(ScribeError::pattern(
format!("Failed to compile regex pattern: {}", e),
pattern.to_string(),
));
}
}
}
Ok(compiled)
}
fn initialize_ast_parsers(&mut self) {
for (language, ts_lang_fn) in TS_LANGUAGES.iter() {
let mut parser = Parser::new();
if parser.set_language(ts_lang_fn()).is_ok() {
self.ast_parsers.insert(language.clone(), parser);
}
}
}
fn initialize_syntax_analyzers(&mut self) {
let python_analyzer = SyntaxAnalyzer {
language: Language::Python,
keywords: vec![
"def".to_string(),
"class".to_string(),
"import".to_string(),
"from".to_string(),
"if".to_string(),
"elif".to_string(),
],
structural_patterns: vec![
"function_definition".to_string(),
"class_definition".to_string(),
"import_statement".to_string(),
"import_from_statement".to_string(),
],
confidence_weights: HashMap::from([
("function_definition".to_string(), 0.9),
("class_definition".to_string(), 0.9),
("import_statement".to_string(), 0.8),
]),
};
self.syntax_analyzers
.insert(Language::Python, python_analyzer);
let js_analyzer = SyntaxAnalyzer {
language: Language::JavaScript,
keywords: vec![
"function".to_string(),
"class".to_string(),
"import".to_string(),
"const".to_string(),
"let".to_string(),
"var".to_string(),
],
structural_patterns: vec![
"function_declaration".to_string(),
"class_declaration".to_string(),
"import_statement".to_string(),
"variable_declaration".to_string(),
],
confidence_weights: HashMap::from([
("function_declaration".to_string(), 0.9),
("class_declaration".to_string(), 0.9),
("import_statement".to_string(), 0.8),
]),
};
self.syntax_analyzers
.insert(Language::JavaScript, js_analyzer);
let rust_analyzer = SyntaxAnalyzer {
language: Language::Rust,
keywords: vec![
"fn".to_string(),
"struct".to_string(),
"enum".to_string(),
"impl".to_string(),
"use".to_string(),
"mod".to_string(),
],
structural_patterns: vec![
"function_item".to_string(),
"struct_item".to_string(),
"enum_item".to_string(),
"use_declaration".to_string(),
],
confidence_weights: HashMap::from([
("function_item".to_string(), 0.9),
("struct_item".to_string(), 0.9),
("use_declaration".to_string(), 0.8),
]),
};
self.syntax_analyzers.insert(Language::Rust, rust_analyzer);
}
fn detect_by_extension(&self, path: &Path) -> Language {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
if let Some(languages) = self.extension_map.get(&extension.to_lowercase()) {
return languages[0].0.clone();
}
}
Language::Unknown
}
fn detect_by_extension_and_filename(&self, path: &Path) -> Language {
if let Some(filename) = path.file_name().and_then(|name| name.to_str()) {
if let Some(language) = self.filename_patterns.get(filename) {
return language.clone();
}
}
self.detect_by_extension(path)
}
fn detect_with_content_analysis(&mut self, path: &Path, content: &str) -> DetectionResult {
let mut candidates = Vec::new();
let mut evidence = Vec::new();
let extension_lang = self.detect_by_extension_and_filename(path);
if extension_lang != Language::Unknown {
candidates.push((extension_lang.clone(), 0.8));
evidence.push(DetectionEvidence {
evidence_type: EvidenceType::Extension,
description: format!("File extension suggests: {:?}", extension_lang),
weight: 0.8,
});
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
let confident_extensions = ["rs", "py", "js", "ts", "go", "java", "cpp", "c"];
if confident_extensions.contains(&ext) {
if self.quick_content_validation(&extension_lang, content) {
return DetectionResult {
language: extension_lang,
confidence: 0.95,
detection_method: DetectionMethod::FileExtension,
alternatives: vec![],
evidence,
};
}
}
}
}
if let Some(shebang_lang) = self.detect_by_shebang(content) {
candidates.push((shebang_lang.clone(), 0.95));
evidence.push(DetectionEvidence {
evidence_type: EvidenceType::Shebang,
description: format!("Shebang indicates: {:?}", shebang_lang),
weight: 0.95,
});
}
let signature_results = self.analyze_content_signatures_optimized(content, &extension_lang);
for (lang, confidence) in signature_results {
candidates.push((lang.clone(), confidence));
evidence.push(DetectionEvidence {
evidence_type: EvidenceType::Syntax,
description: format!("Content signatures match: {:?}", lang),
weight: confidence,
});
}
let max_confidence = candidates.iter().map(|(_, c)| *c).fold(0.0f32, f32::max);
if max_confidence < 0.8 {
let import_results = self.analyze_import_patterns(content);
for (lang, confidence) in import_results {
candidates.push((lang.clone(), confidence));
evidence.push(DetectionEvidence {
evidence_type: EvidenceType::Import,
description: format!("Import patterns match: {:?}", lang),
weight: confidence,
});
}
}
self.aggregate_detection_results(candidates, evidence)
}
fn quick_content_validation(&self, language: &Language, content: &str) -> bool {
match language {
Language::Rust => {
content.contains("fn ") || content.contains("use ") || content.contains("struct ")
}
Language::Python => {
content.contains("def ")
|| content.contains("import ")
|| content.contains("class ")
}
Language::JavaScript => {
content.contains("function ")
|| content.contains("const ")
|| content.contains("var ")
}
Language::TypeScript => {
content.contains("interface ")
|| content.contains("type ")
|| content.contains(": ")
}
Language::Go => {
content.contains("func ")
|| content.contains("package ")
|| content.contains("import ")
}
Language::Java => {
content.contains("class ")
|| content.contains("public ")
|| content.contains("import ")
}
Language::C => {
content.contains("#include")
|| content.contains("int main")
|| content.contains("void ")
}
Language::Cpp => {
content.contains("#include")
|| content.contains("class ")
|| content.contains("namespace ")
}
_ => true, }
}
fn analyze_content_signatures_optimized(
&self,
content: &str,
extension_lang: &Language,
) -> Vec<(Language, f32)> {
let mut results = Vec::new();
if *extension_lang != Language::Unknown {
if let Some(signatures) = self.content_signatures.get(extension_lang) {
for signature in signatures {
let matches = self.count_signature_matches(signature, content);
if matches >= signature.required_matches {
let confidence =
(matches as f32 / signature.patterns.len() as f32) * signature.weight;
results.push((extension_lang.clone(), confidence));
if confidence > 0.7 {
return results;
}
}
}
}
}
for (language, signatures) in &self.content_signatures {
if *language == *extension_lang {
continue; }
for signature in signatures {
let matches = self.count_signature_matches(signature, content);
if matches >= signature.required_matches {
let confidence =
(matches as f32 / signature.patterns.len() as f32) * signature.weight;
results.push((language.clone(), confidence));
}
}
}
results
}
fn count_signature_matches(&self, signature: &ContentSignature, content: &str) -> usize {
signature
.patterns
.iter()
.map(|regex| regex.find_iter(content).count())
.sum::<usize>()
}
fn detect_with_full_analysis(&mut self, path: &Path, content: &str) -> DetectionResult {
let mut base_result = self.detect_with_content_analysis(path, content);
let statistical_results = self.statistical_analysis(content);
for (lang, confidence) in statistical_results {
base_result.alternatives.push((lang, confidence));
}
base_result
.alternatives
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
base_result
}
fn detect_with_custom_rules(
&mut self,
path: &Path,
content: &str,
rules: &CustomDetectionRules,
) -> DetectionResult {
let mut candidates = Vec::new();
let mut evidence = Vec::new();
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
if let Some(language) = rules.extension_overrides.get(&extension.to_lowercase()) {
candidates.push((language.clone(), 1.0));
evidence.push(DetectionEvidence {
evidence_type: EvidenceType::Extension,
description: format!("Custom extension rule: {} -> {:?}", extension, language),
weight: 1.0,
});
}
}
if let Some(filename) = path.file_name().and_then(|name| name.to_str()) {
if let Some(language) = rules.filename_patterns.get(filename) {
candidates.push((language.clone(), 1.0));
evidence.push(DetectionEvidence {
evidence_type: EvidenceType::Filename,
description: format!("Custom filename rule: {} -> {:?}", filename, language),
weight: 1.0,
});
}
}
for signature_config in &rules.content_signatures {
let matches = signature_config
.patterns
.iter()
.map(|pattern| {
match regex::Regex::new(pattern) {
Ok(regex) => regex.find_iter(content).count(),
Err(_) => content.matches(pattern).count(),
}
})
.sum::<usize>();
if matches >= signature_config.required_matches {
candidates.push((signature_config.language.clone(), signature_config.weight));
evidence.push(DetectionEvidence {
evidence_type: EvidenceType::Syntax,
description: format!(
"Custom signature matches for {:?}: {}",
signature_config.language, matches
),
weight: signature_config.weight,
});
}
}
if candidates.is_empty() {
return self.detect_with_content_analysis(path, content);
}
self.aggregate_detection_results(candidates, evidence)
}
fn detect_by_shebang(&self, content: &str) -> Option<Language> {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() {
return None;
}
let first_line = lines[0];
if first_line.starts_with("#!") {
let shebang_path = &first_line[2..].trim();
for (pattern, language) in &self.shebang_patterns {
if shebang_path.contains(pattern) {
return Some(language.clone());
}
}
}
None
}
fn analyze_content_signatures(&self, content: &str) -> Vec<(Language, f32)> {
let mut results = Vec::new();
for (language, signatures) in &self.content_signatures {
for signature in signatures {
let matches = signature
.patterns
.iter()
.map(|pattern| {
pattern.find_iter(content).count()
})
.sum::<usize>();
if matches >= signature.required_matches {
let confidence =
(matches as f32 / signature.patterns.len() as f32) * signature.weight;
results.push((language.clone(), confidence));
}
}
}
results
}
fn analyze_import_patterns(&mut self, content: &str) -> Vec<(Language, f32)> {
let mut results = Vec::new();
let likely_languages = self.get_likely_languages_from_content(content);
for language in likely_languages {
if let Some(parser) = self.ast_parsers.get_mut(&language) {
if let Some(tree) = parser.parse(content, None) {
let root_node = tree.root_node();
let import_count = self.count_import_nodes(&root_node, &language);
if import_count > 0 {
let confidence = (import_count as f32 / 10.0).min(0.9);
results.push((language, confidence));
if confidence > 0.7 {
break;
}
}
}
}
}
results
}
fn get_likely_languages_from_content(&self, content: &str) -> Vec<Language> {
let mut likely_languages = Vec::new();
if content.contains("def ") || content.contains("import ") || content.contains("from ") {
likely_languages.push(Language::Python);
}
if content.contains("fn ") || content.contains("use ") || content.contains("struct ") {
likely_languages.push(Language::Rust);
}
if content.contains("function ") || content.contains("const ") || content.contains("let ") {
likely_languages.push(Language::JavaScript);
}
if content.contains("interface ")
|| content.contains("type ")
|| content.contains(": string")
{
likely_languages.push(Language::TypeScript);
}
if content.contains("func ") || content.contains("package ") {
likely_languages.push(Language::Go);
}
if likely_languages.is_empty() {
likely_languages = vec![
Language::JavaScript,
Language::Python,
Language::TypeScript,
Language::Rust,
Language::Go,
];
}
likely_languages
}
fn statistical_analysis(&mut self, content: &str) -> Vec<(Language, f32)> {
let mut results = Vec::new();
let likely_languages = self.get_likely_languages_from_content(content);
for language in likely_languages {
if let Some(analyzer) = self.syntax_analyzers.get(&language) {
if let Some(parser) = self.ast_parsers.get_mut(&language) {
if let Some(tree) = parser.parse(content, None) {
let root_node = tree.root_node();
let structural_score =
self.calculate_structural_score(&root_node, analyzer);
if structural_score > 0.0 {
results.push((language, structural_score));
if structural_score > 0.8 {
break;
}
}
}
}
}
}
results
}
fn count_import_nodes(&self, node: &Node, language: &Language) -> usize {
let mut count = 0;
let import_types: &[&str] = match language {
Language::Python => &["import_statement", "import_from_statement"],
Language::JavaScript | Language::TypeScript => {
&["import_statement", "import_declaration"]
}
Language::Rust => &["use_declaration"],
Language::Go => &["import_spec", "import_declaration"],
Language::Java => &["import_declaration"],
_ => &[],
};
self.count_nodes_recursive(node, import_types, &mut count);
count
}
fn calculate_structural_score(&self, node: &Node, analyzer: &SyntaxAnalyzer) -> f32 {
let mut score = 0.0;
for pattern in &analyzer.structural_patterns {
let count = self.count_specific_nodes(node, pattern);
if count > 0 {
let weight = analyzer.confidence_weights.get(pattern).unwrap_or(&0.5);
score += (count as f32) * weight;
}
}
(score / 10.0).min(1.0)
}
fn count_nodes_recursive(&self, node: &Node, target_types: &[&str], count: &mut usize) {
if target_types.contains(&node.kind()) {
*count += 1;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.count_nodes_recursive(&child, target_types, count);
}
}
}
fn count_specific_nodes(&self, node: &Node, target_type: &str) -> usize {
let mut count = 0;
self.count_nodes_recursive(node, &[target_type], &mut count);
count
}
fn aggregate_detection_results(
&self,
candidates: Vec<(Language, f32)>,
evidence: Vec<DetectionEvidence>,
) -> DetectionResult {
if candidates.is_empty() {
return DetectionResult {
language: Language::Unknown,
confidence: 0.0,
detection_method: DetectionMethod::FileExtension,
alternatives: vec![],
evidence,
};
}
let mut language_scores: HashMap<Language, f32> = HashMap::new();
let mut methods_used: Vec<DetectionMethod> = Vec::new();
for (lang, confidence) in &candidates {
*language_scores.entry(lang.clone()).or_insert(0.0) += confidence;
}
let (best_language, best_confidence) = language_scores
.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(lang, conf)| (lang.clone(), *conf))
.unwrap_or((Language::Unknown, 0.0));
let normalized_confidence = best_confidence.min(1.0);
let mut alternatives: Vec<(Language, f32)> = language_scores
.into_iter()
.filter(|(lang, _)| *lang != best_language)
.collect();
alternatives.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
let detection_method = if evidence
.iter()
.any(|e| e.evidence_type == EvidenceType::Shebang)
{
DetectionMethod::Shebang
} else if evidence
.iter()
.any(|e| e.evidence_type == EvidenceType::Syntax)
{
DetectionMethod::ContentSignature
} else if evidence
.iter()
.any(|e| e.evidence_type == EvidenceType::Extension)
{
DetectionMethod::FileExtension
} else {
DetectionMethod::Hybrid
};
DetectionResult {
language: best_language,
confidence: normalized_confidence,
detection_method,
alternatives,
evidence,
}
}
fn apply_project_type_bias(
&self,
mut result: DetectionResult,
project_type: &ProjectType,
) -> DetectionResult {
let bias_factor = 0.25;
match project_type {
ProjectType::WebFrontend => {
if matches!(
result.language,
Language::JavaScript | Language::TypeScript | Language::HTML | Language::CSS
) {
result.confidence += bias_factor;
}
}
ProjectType::WebBackend => {
if matches!(
result.language,
Language::Python
| Language::JavaScript
| Language::TypeScript
| Language::Java
| Language::Go
| Language::Rust
) {
result.confidence += bias_factor;
}
}
ProjectType::SystemsProgram => {
if matches!(
result.language,
Language::Rust | Language::C | Language::Cpp | Language::Go
) {
result.confidence += bias_factor;
}
}
ProjectType::DataScience => {
if matches!(
result.language,
Language::Python | Language::R | Language::SQL
) {
result.confidence += bias_factor;
}
}
_ => {}
}
result.confidence = result.confidence.min(1.0);
result
}
fn apply_dominant_language_bias(
&self,
mut result: DetectionResult,
dominant_languages: &[Language],
) -> DetectionResult {
if dominant_languages.contains(&result.language) {
result.confidence += 0.15;
result.confidence = result.confidence.min(1.0);
}
result
}
fn apply_framework_bias(
&self,
mut result: DetectionResult,
framework_indicators: &[String],
) -> DetectionResult {
for indicator in framework_indicators {
match indicator.as_str() {
"package.json" | "node_modules" => {
if matches!(result.language, Language::JavaScript | Language::TypeScript) {
result.confidence += 0.1;
}
}
"Cargo.toml" | "Cargo.lock" => {
if result.language == Language::Rust {
result.confidence += 0.1;
}
}
"requirements.txt" | "__pycache__" | ".pyc" => {
if result.language == Language::Python {
result.confidence += 0.1;
}
}
_ => {}
}
}
result.confidence = result.confidence.min(1.0);
result
}
}
impl Default for LanguageDetector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_extension_detection() {
let detector = LanguageDetector::new();
assert_eq!(
detector.detect_language(Path::new("test.rs")),
Language::Rust
);
assert_eq!(
detector.detect_language(Path::new("test.py")),
Language::Python
);
assert_eq!(
detector.detect_language(Path::new("test.js")),
Language::JavaScript
);
assert_eq!(
detector.detect_language(Path::new("test.ts")),
Language::TypeScript
);
assert_eq!(
detector.detect_language(Path::new("test.java")),
Language::Java
);
assert_eq!(detector.detect_language(Path::new("test.go")), Language::Go);
assert_eq!(
detector.detect_language(Path::new("test.cpp")),
Language::Cpp
);
assert_eq!(detector.detect_language(Path::new("test.c")), Language::C);
}
#[test]
fn test_rust_files_are_programming() {
let detector = LanguageDetector::new();
let rust_files = [
"src/lib.rs",
"scribe-rs/src/lib.rs",
"scribe-rs/scribe-core/src/lib.rs",
"main.rs",
"mod.rs",
];
for file_path in &rust_files {
let language = detector.detect_language(Path::new(file_path));
assert_eq!(language, Language::Rust, "Failed for file: {}", file_path);
assert!(
language.is_programming(),
"Rust should be programming language for file: {}",
file_path
);
}
}
#[test]
fn test_filename_patterns() {
let mut detector = LanguageDetector::new();
assert_eq!(
detector.detect_language(Path::new("Makefile")),
Language::Unknown
);
assert_eq!(
detector.detect_language(Path::new("Dockerfile")),
Language::Unknown
);
assert_eq!(
detector.detect_language(Path::new("Cargo.toml")),
Language::TOML
);
assert_eq!(
detector.detect_language(Path::new("package.json")),
Language::JSON
);
}
#[test]
fn test_shebang_detection() {
let mut detector = LanguageDetector::new();
let python_script = "#!/usr/bin/env python3\nprint('Hello, world!')";
let result = detector.detect_language_with_content(Path::new("script"), python_script);
assert_eq!(result.language, Language::Python);
assert!(result.confidence > 0.9);
assert_eq!(result.detection_method, DetectionMethod::Shebang);
let bash_script = "#!/bin/bash\necho 'Hello, world!'";
let result = detector.detect_language_with_content(Path::new("script"), bash_script);
assert_eq!(result.language, Language::Bash);
assert!(result.confidence > 0.9);
}
#[test]
fn test_content_signature_detection() {
let mut detector = LanguageDetector::new();
let python_code = r#"
def hello_world():
print("Hello, world!")
class MyClass:
def __init__(self):
pass
import sys
from collections import defaultdict
"#;
let result = detector.detect_language_with_content(Path::new("unknown"), python_code);
assert_eq!(result.language, Language::Python);
assert!(result.confidence > 0.5);
let rust_code = r#"
fn main() {
println!("Hello, world!");
}
struct MyStruct {
field: i32,
}
impl MyStruct {
fn new() -> Self {
MyStruct { field: 0 }
}
}
use std::collections::HashMap;
"#;
let result = detector.detect_language_with_content(Path::new("unknown"), rust_code);
assert_eq!(result.language, Language::Rust);
assert!(result.confidence > 0.5);
}
#[test]
fn test_import_pattern_detection() {
let mut detector = LanguageDetector::new();
let js_code = r#"
import React from 'react';
import { useState } from 'react';
const fs = require('fs');
"#;
let result = detector.detect_language_with_content(Path::new("unknown"), js_code);
assert_eq!(result.language, Language::JavaScript);
let python_code = r#"
import os
import sys
from collections import defaultdict, Counter
"#;
let result = detector.detect_language_with_content(Path::new("unknown"), python_code);
assert_eq!(result.language, Language::Python);
}
#[test]
fn test_hybrid_detection() {
let mut detector = LanguageDetector::new();
let python_code = "#!/usr/bin/env python\nprint('Hello')\n# Python comment";
let result = detector.detect_language_with_content(Path::new("test.py"), python_code);
assert_eq!(result.language, Language::Python);
assert!(result.confidence > 0.6); assert!(result.evidence.len() > 1);
let python_code = "def hello(): print('Hello')";
let result = detector.detect_language_with_content(Path::new("test.js"), python_code);
assert!(result.language == Language::Python || result.language == Language::JavaScript);
}
#[test]
fn test_detection_with_hints() {
let mut detector = LanguageDetector::new();
let hints = LanguageHints {
project_type: Some(ProjectType::WebFrontend),
dominant_languages: vec![Language::TypeScript],
framework_indicators: vec!["package.json".to_string()],
..Default::default()
};
let ts_code = "const hello = () => console.log('Hello');";
let result = detector.detect_with_hints(Path::new("unknown"), ts_code, &hints);
assert_eq!(result.language, Language::JavaScript); assert!(result.confidence > 0.5);
}
#[test]
fn test_custom_detection_rules() {
let mut custom_rules = CustomDetectionRules {
extension_overrides: HashMap::new(),
filename_patterns: HashMap::new(),
content_signatures: vec![],
priority_languages: vec![],
};
custom_rules
.extension_overrides
.insert("myext".to_string(), Language::Rust);
let mut detector = LanguageDetector::with_strategy(DetectionStrategy::Custom(custom_rules));
let result = detector.detect_language_with_content(Path::new("test.myext"), "some content");
assert_eq!(result.language, Language::Rust);
assert_eq!(result.confidence, 1.0);
}
#[test]
fn test_detection_evidence() {
let mut detector = LanguageDetector::new();
let python_code = "#!/usr/bin/env python\nprint('Hello World')";
let result = detector.detect_language_with_content(Path::new("test.py"), python_code);
assert!(result.evidence.len() >= 2);
assert!(result
.evidence
.iter()
.any(|e| e.evidence_type == EvidenceType::Shebang));
assert!(result
.evidence
.iter()
.any(|e| e.evidence_type == EvidenceType::Extension));
}
#[test]
fn test_confidence_scoring() {
let mut detector = LanguageDetector::new();
let strong_python = "#!/usr/bin/env python3\nimport os\ndef main(): pass\nclass Test: pass";
let result = detector.detect_language_with_content(Path::new("test.py"), strong_python);
assert!(result.confidence > 0.8);
let weak_indicators = "hello world";
let result = detector.detect_language_with_content(Path::new("test.txt"), weak_indicators);
assert!(result.confidence < 0.8);
}
#[test]
fn test_alternatives_ranking() {
let mut detector = LanguageDetector::new();
let ambiguous_code = "print hello"; let result = detector.detect_language_with_content(Path::new("unknown"), ambiguous_code);
if result.alternatives.len() > 1 {
assert!(result.alternatives[0].1 >= result.alternatives[1].1);
}
}
}