pub mod c;
pub mod cpp;
pub mod csharp;
pub mod go;
pub mod java;
pub mod kotlin;
pub mod php;
pub mod preview;
pub mod python;
pub mod ruby;
pub mod rust;
pub mod svelte;
pub mod tsconfig;
pub mod typescript;
pub mod vue;
pub mod zig;
use crate::models::{Language, SearchResult};
use anyhow::{Result, anyhow};
pub struct ParserFactory;
#[derive(Debug, Clone)]
pub struct ImportInfo {
pub imported_path: String,
pub import_type: crate::models::ImportType,
pub line_number: usize,
pub imported_symbols: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct ExportInfo {
pub exported_symbol: Option<String>,
pub source_path: String,
pub line_number: usize,
}
pub trait DependencyExtractor {
fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>>;
}
impl ParserFactory {
pub fn get_language_grammar(language: Language) -> Result<tree_sitter::Language> {
match language {
Language::Rust => Ok(tree_sitter_rust::LANGUAGE.into()),
Language::Python => Ok(tree_sitter_python::LANGUAGE.into()),
Language::TypeScript => Ok(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
Language::JavaScript => Ok(tree_sitter_typescript::LANGUAGE_TSX.into()),
Language::Go => Ok(tree_sitter_go::LANGUAGE.into()),
Language::Java => Ok(tree_sitter_java::LANGUAGE.into()),
Language::C => Ok(tree_sitter_c::LANGUAGE.into()),
Language::Cpp => Ok(tree_sitter_cpp::LANGUAGE.into()),
Language::CSharp => Ok(tree_sitter_c_sharp::LANGUAGE.into()),
Language::PHP => Ok(tree_sitter_php::LANGUAGE_PHP.into()),
Language::Ruby => Ok(tree_sitter_ruby::LANGUAGE.into()),
Language::Kotlin => Ok(tree_sitter_kotlin_ng::LANGUAGE.into()),
Language::Zig => Ok(tree_sitter_zig::LANGUAGE.into()),
Language::Swift => Err(anyhow!(
"Swift support temporarily disabled (parser queries out of date with tree-sitter-swift 0.7.x grammar)"
)),
Language::Vue => Err(anyhow!(
"Vue uses line-based parsing, not tree-sitter (tree-sitter-vue incompatible with tree-sitter 0.24+)"
)),
Language::Svelte => Err(anyhow!(
"Svelte uses line-based parsing, not tree-sitter (tree-sitter-svelte incompatible with tree-sitter 0.24+)"
)),
Language::Text => Err(anyhow!(
"The text tier (markdown, YAML, JSON, TOML, HTML, shell, proto) is \
trigram-indexed only and has no grammar. Use full-text or regex \
search on these files, not --symbols or --ast."
)),
Language::Unknown => Err(anyhow!("Unknown language")),
}
}
pub fn get_keywords(language: Language) -> &'static [&'static str] {
match language {
Language::Rust => &[
"fn", "struct", "enum", "trait", "impl", "mod", "const", "static", "type", "macro",
],
Language::PHP => &["class", "function", "trait", "interface", "enum"],
Language::Python => &["class", "def", "async"],
Language::TypeScript | Language::JavaScript => &[
"class",
"function",
"interface",
"type",
"enum",
"const",
"let",
"var",
],
Language::Go => &["func", "struct", "interface", "type", "const", "var"],
Language::Java => &["class", "interface", "enum", "@interface"],
Language::C => &["struct", "enum", "union", "typedef"],
Language::Cpp => &[
"class",
"struct",
"enum",
"union",
"typedef",
"namespace",
"template",
],
Language::CSharp => &[
"class",
"struct",
"interface",
"enum",
"delegate",
"record",
"namespace",
],
Language::Ruby => &["class", "module", "def"],
Language::Kotlin => &["class", "fun", "interface", "object", "enum", "annotation"],
Language::Zig => &["fn", "struct", "enum", "const", "var", "type"],
Language::Swift => &["class", "struct", "enum", "protocol", "func", "var", "let"],
Language::Vue | Language::Svelte => &["function", "const", "let", "var"],
Language::Text => &[],
Language::Unknown => &[],
}
}
pub fn get_all_keywords() -> &'static [&'static str] {
&[
"fn",
"function",
"def",
"func",
"class",
"struct",
"enum",
"interface",
"trait",
"type",
"record",
"mod",
"module",
"namespace",
"const",
"static",
"let",
"var",
"impl",
"async",
"object",
"annotation",
"protocol",
"union",
"typedef",
"delegate",
"template",
"@interface",
]
}
pub fn parse(path: &str, source: &str, language: Language) -> Result<Vec<SearchResult>> {
if is_minified(source) {
log::info!(
"Skipping symbol extraction for {} — looks minified ({} bytes over {} lines). \
The file is still searchable with full-text and regex queries.",
path,
source.len(),
count_lines(source)
);
return Ok(Vec::new());
}
match language {
Language::Rust => rust::parse(path, source),
Language::TypeScript => typescript::parse(path, source, language),
Language::JavaScript => typescript::parse(path, source, language),
Language::Vue => vue::parse(path, source),
Language::Svelte => svelte::parse(path, source),
Language::Python => python::parse(path, source),
Language::Go => go::parse(path, source),
Language::Java => java::parse(path, source),
Language::PHP => php::parse(path, source),
Language::C => c::parse(path, source),
Language::Cpp => cpp::parse(path, source),
Language::CSharp => csharp::parse(path, source),
Language::Ruby => ruby::parse(path, source),
Language::Kotlin => kotlin::parse(path, source),
Language::Swift => {
log::warn!(
"Swift support temporarily disabled (parser queries out of date with tree-sitter-swift 0.7.x grammar): {}",
path
);
Ok(vec![])
}
Language::Zig => zig::parse(path, source),
Language::Text => {
log::debug!("No symbol extraction for text-tier file: {}", path);
Ok(vec![])
}
Language::Unknown => {
log::warn!("Unknown language for file: {}", path);
Ok(vec![])
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parser_factory() {
let _factory = ParserFactory;
}
}
pub const MAX_BYTES_PER_LINE: usize = 1024;
pub const MINIFIED_SIZE_FLOOR: usize = 16 * 1024;
fn count_lines(source: &str) -> usize {
memchr::memchr_iter(b'\n', source.as_bytes()).count()
}
pub fn is_minified(source: &str) -> bool {
if source.len() < MINIFIED_SIZE_FLOOR {
return false;
}
let enough = source.len() / MAX_BYTES_PER_LINE;
let mut seen = 0usize;
for _ in memchr::memchr_iter(b'\n', source.as_bytes()) {
seen += 1;
if seen > enough {
return false;
}
}
true
}
#[cfg(test)]
mod minified_tests {
use super::*;
#[test]
fn real_source_is_never_minified() {
let src = "pub fn some_function_name(a: u32) -> u32 {\n".repeat(2000);
assert!(!is_minified(&src));
}
#[test]
fn a_one_line_bundle_is_minified() {
assert!(is_minified(&"function f(a,b){return a+b}".repeat(60_000)));
}
#[test]
fn a_small_file_is_never_minified_however_long_its_line() {
assert!(!is_minified(&"x".repeat(MINIFIED_SIZE_FLOOR - 1)));
}
#[test]
fn one_long_line_among_normal_ones_is_not_minified() {
let src = format!("{}\n{}", "z".repeat(12_282), "const a = 1;\n".repeat(2000));
assert!(
!is_minified(&src),
"generated protobuf must keep its symbols"
);
}
#[test]
fn cjk_source_is_not_minified() {
let line = format!("// {}\n", "日".repeat(200));
assert!(!is_minified(&line.repeat(200)));
}
#[test]
fn a_minified_file_yields_no_symbols_but_does_not_error() {
let src = "function f(a,b){return a+b}".repeat(60_000);
let out = ParserFactory::parse("bundle.js", &src, Language::JavaScript).unwrap();
assert!(out.is_empty(), "got {} symbols", out.len());
}
#[test]
fn a_normal_file_still_yields_symbols() {
let src = "export function realOne(a: number) {\n return a;\n}\n".repeat(50);
let out = ParserFactory::parse("real.ts", &src, Language::TypeScript).unwrap();
assert!(!out.is_empty(), "normal source must still parse");
}
}