use serde::{Deserialize, Serialize};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to parse source: {0}")]
ParseFailed(String),
#[error("Invalid syntax at position {position}: {message}")]
SyntaxError {
position: usize,
message: String,
},
#[error("Unsupported language: {0}")]
UnsupportedLanguage(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("UTF-8 error: {0}")]
Utf8(#[from] std::str::Utf8Error),
}
pub fn parse_tree(
parser: &mut tree_sitter::Parser,
language: &tree_sitter::Language,
source: &[u8],
language_name: &str,
) -> Result<tree_sitter::Tree> {
parser
.set_language(language)
.map_err(|error| Error::ParseFailed(error.to_string()))?;
parser
.parse(source, None)
.ok_or_else(|| Error::ParseFailed(format!("Failed to parse {language_name} source")))
}
pub fn find_node_by_id<'tree>(
root: &tree_sitter::Node<'tree>,
id: usize,
) -> Option<tree_sitter::Node<'tree>> {
let mut queue = std::collections::VecDeque::from([*root]);
while let Some(node) = queue.pop_front() {
if node.id() == id {
return Some(node);
}
let mut cursor = node.walk();
queue.extend(node.children(&mut cursor));
}
None
}
thread_local! {
static LITE_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
pub struct LiteGuard {
_thread_bound: std::marker::PhantomData<*const ()>,
}
impl LiteGuard {
pub fn enter() -> Self {
LITE_DEPTH.with(|d| d.set(d.get() + 1));
LiteGuard {
_thread_bound: std::marker::PhantomData,
}
}
}
impl Drop for LiteGuard {
fn drop(&mut self) {
LITE_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
}
#[inline]
pub fn lite() -> bool {
LITE_DEPTH.with(|d| d.get() > 0)
}
pub fn calculate_complexity(
node: &tree_sitter::Node<'_>,
metrics: &mut ComplexityMetrics,
depth: usize,
decision_kinds: &[&str],
) {
metrics.nesting_depth = metrics.nesting_depth.max(depth);
if depth == 0 {
metrics.line_count = node.end_position().row - node.start_position().row + 1;
}
if decision_kinds.contains(&node.kind()) {
metrics.cyclomatic += 1;
}
metrics.token_count += node.child_count();
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
calculate_complexity(&child, metrics, depth + 1, decision_kinds);
}
}
pub fn clean_call_text(raw: &str) -> String {
raw.split('(').next().unwrap_or(raw).trim().to_string()
}
#[cfg(test)]
mod tests {
use super::{find_node_by_id, parse_tree};
#[test]
fn test_parse_tree_finds_descendant_and_returns_none_for_missing_id() {
let source = b"fn outer() { let answer = 42; }";
let language = tree_sitter_rust::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
let tree = parse_tree(&mut parser, &language, source, "Rust").expect("Rust source parses");
let root = tree.root_node();
let function = root.named_child(0).expect("function descendant");
assert_eq!(
find_node_by_id(&root, function.id())
.expect("finds descendant")
.id(),
function.id()
);
assert!(find_node_by_id(&root, usize::MAX).is_none());
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImportInfo {
pub path: String,
pub alias: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SignatureInfo {
pub name: String,
pub qualified_name: String,
pub parameters: Vec<Parameter>,
pub return_type: Option<String>,
pub visibility: Visibility,
pub is_async: bool,
pub is_method: bool,
pub docstring: Option<String>,
pub calls: Vec<String>,
pub imports: Vec<ImportInfo>,
pub byte_range: (usize, usize),
#[serde(default)]
pub cyclomatic_complexity: u32,
#[serde(default)]
pub flow_facts: Vec<FlowFact>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FlowChannel {
Argument,
ReturnValue,
StateRead,
StateWrite,
CommandArgument,
Environment,
Stdin,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FlowFact {
pub channel: FlowChannel,
pub source: String,
pub target: String,
pub position: Option<usize>,
pub byte_range: (usize, usize),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Parameter {
pub name: String,
pub type_annotation: Option<String>,
pub default_value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Visibility {
Public,
Private,
Protected,
Internal,
Package,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityMetrics {
pub cyclomatic: usize,
pub nesting_depth: usize,
pub line_count: usize,
pub token_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
pub from: usize,
pub to: usize,
pub edge_type: EdgeType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EdgeType {
Unconditional,
TrueBranch,
FalseBranch,
Loop,
Exception,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
pub id: usize,
pub statements: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Graph<Block, Edge> {
pub blocks: Vec<Block>,
pub edges: Vec<Edge>,
pub entry_block: usize,
pub exit_blocks: Vec<usize>,
}
pub trait CodeIntelligence {
fn get_signatures(&self, source: &[u8]) -> Result<Vec<SignatureInfo>>;
fn get_signatures_with_parser(
&self,
source: &[u8],
_parser: &mut tree_sitter::Parser,
) -> Result<Vec<SignatureInfo>> {
self.get_signatures(source)
}
fn get_signatures_lite(
&self,
source: &[u8],
parser: &mut tree_sitter::Parser,
) -> Result<Vec<SignatureInfo>> {
let _guard = LiteGuard::enter();
self.get_signatures_with_parser(source, parser)
}
fn compute_cfg(&self, source: &[u8], node_id: usize) -> Result<Graph<Block, Edge>>;
fn extract_complexity(&self, node: &tree_sitter::Node<'_>) -> ComplexityMetrics;
}
#[derive(Debug, Clone)]
pub struct LanguageConfig {
pub name: String,
pub extensions: Vec<String>,
pub queries: QueryPatterns,
}
#[derive(Debug, Clone)]
pub struct QueryPatterns {
pub function_definition: String,
pub class_definition: String,
pub method_definition: String,
pub import_statement: String,
}
impl LanguageConfig {
pub fn from_extension(ext: &str) -> Option<&'static LanguageConfig> {
crate::parse::grammar::LanguageId::from_extension(ext).map(|id| id.config())
}
const fn default_queries() -> QueryPatterns {
QueryPatterns {
function_definition: String::new(),
class_definition: String::new(),
method_definition: String::new(),
import_statement: String::new(),
}
}
}
pub mod languages {
use crate::parse::traits::LanguageConfig;
use tree_sitter::Language;
pub mod python {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "Python".to_string(),
extensions: vec!["py".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_python::LANGUAGE.into()
}
}
pub mod javascript {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "JavaScript".to_string(),
extensions: vec![
"js".to_string(),
"jsx".to_string(),
"mjs".to_string(),
"cjs".to_string(),
],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_javascript::LANGUAGE.into()
}
}
pub mod typescript {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "TypeScript".to_string(),
extensions: vec![
"ts".to_string(),
"tsx".to_string(),
"mts".to_string(),
"cts".to_string(),
],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()
}
}
pub mod go {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "Go".to_string(),
extensions: vec!["go".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_go::LANGUAGE.into()
}
}
pub mod rust {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "Rust".to_string(),
extensions: vec!["rs".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_rust::LANGUAGE.into()
}
}
pub mod java {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "Java".to_string(),
extensions: vec!["java".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_java::LANGUAGE.into()
}
}
pub mod cpp {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "C++".to_string(),
extensions: vec![
"cpp".to_string(),
"cc".to_string(),
"cxx".to_string(),
"hpp".to_string(),
"h".to_string(),
],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_cpp::LANGUAGE.into()
}
}
pub mod csharp {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "C#".to_string(),
extensions: vec!["cs".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_c_sharp::LANGUAGE.into()
}
}
pub mod ruby {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "Ruby".to_string(),
extensions: vec!["rb".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_ruby::LANGUAGE.into()
}
}
pub mod php {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "PHP".to_string(),
extensions: vec!["php".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_php::LANGUAGE_PHP.into()
}
}
macro_rules! tier0_language {
($modname:ident, $display:expr, $exts:expr, $loader:expr) => {
pub mod $modname {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: $display.to_string(),
extensions: $exts.iter().map(|e| e.to_string()).collect(),
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
$loader.into()
}
}
};
}
tier0_language!(swift, "Swift", ["swift"], tree_sitter_swift::LANGUAGE);
tier0_language!(
kotlin,
"Kotlin",
["kt", "kts"],
tree_sitter_kotlin_ng::LANGUAGE
);
tier0_language!(dart, "Dart", ["dart"], tree_sitter_dart::LANGUAGE);
tier0_language!(html, "HTML", ["html", "htm"], tree_sitter_html::LANGUAGE);
tier0_language!(css, "CSS", ["css"], tree_sitter_css::LANGUAGE);
tier0_language!(scss, "SCSS", ["scss"], tree_sitter_scss::language());
tier0_language!(yaml, "YAML", ["yaml", "yml"], tree_sitter_yaml::LANGUAGE);
tier0_language!(cmake, "CMake", ["cmake"], tree_sitter_cmake::LANGUAGE);
tier0_language!(
elixir,
"Elixir",
["ex", "exs"],
tree_sitter_elixir::LANGUAGE
);
tier0_language!(
erlang,
"Erlang",
["erl", "hrl"],
tree_sitter_erlang::LANGUAGE
);
tier0_language!(haskell, "Haskell", ["hs"], tree_sitter_haskell::LANGUAGE);
tier0_language!(perl, "Perl", ["pl", "pm"], tree_sitter_perl::LANGUAGE);
tier0_language!(r, "R", ["r", "R"], tree_sitter_r::LANGUAGE);
tier0_language!(zig, "Zig", ["zig"], tree_sitter_zig::LANGUAGE);
tier0_language!(
graphql,
"GraphQL",
["graphql", "gql"],
tree_sitter_graphql::LANGUAGE
);
tier0_language!(
hcl,
"HCL",
["hcl", "tf", "tfvars"],
tree_sitter_hcl::LANGUAGE
);
tier0_language!(
make,
"Make",
["makefile", "mak", "mk"],
tree_sitter_make::LANGUAGE
);
tier0_language!(elisp, "Emacs Lisp", ["el"], tree_sitter_elisp::LANGUAGE);
tier0_language!(julia, "Julia", ["jl"], tree_sitter_julia::LANGUAGE);
tier0_language!(d, "D", ["d", "di"], tree_sitter_d::LANGUAGE);
tier0_language!(
glsl,
"GLSL",
["glsl", "vert", "frag", "comp"],
tree_sitter_glsl::LANGUAGE_GLSL
);
tier0_language!(
embedded_template,
"Embedded Template",
["ejs", "erb", "liquid"],
tree_sitter_embedded_template::LANGUAGE
);
tier0_language!(
markdown,
"Markdown",
["md", "markdown"],
tree_sitter_json::LANGUAGE
);
tier0_language!(rst, "reStructuredText", ["rst"], tree_sitter_json::LANGUAGE);
tier0_language!(
adoc,
"AsciiDoc",
["adoc", "asciidoc"],
tree_sitter_json::LANGUAGE
);
tier0_language!(plaintext, "Plain Text", ["txt"], tree_sitter_json::LANGUAGE);
pub fn language_by_name(name: &str) -> Language {
match name {
"swift" => swift::language(),
"kotlin" => kotlin::language(),
"dart" => dart::language(),
"html" => html::language(),
"css" => css::language(),
"scss" => scss::language(),
"yaml" => yaml::language(),
"cmake" => cmake::language(),
"elixir" => elixir::language(),
"erlang" => erlang::language(),
"haskell" => haskell::language(),
"perl" => perl::language(),
"r" => r::language(),
"zig" => zig::language(),
"graphql" => graphql::language(),
"hcl" => hcl::language(),
"make" => make::language(),
"elisp" => elisp::language(),
"julia" => julia::language(),
"d" => d::language(),
"glsl" => glsl::language(),
"embedded_template" => embedded_template::language(),
_ => panic!("language_by_name: unknown language {name}"),
}
}
pub fn language_by_name_is_registered(name: &str) -> bool {
matches!(
name,
"swift"
| "kotlin"
| "dart"
| "html"
| "css"
| "scss"
| "yaml"
| "cmake"
| "elixir"
| "erlang"
| "haskell"
| "perl"
| "r"
| "zig"
| "graphql"
| "hcl"
| "make"
| "elisp"
| "julia"
| "d"
| "glsl"
| "embedded_template"
)
}
pub mod lua {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "Lua".to_string(),
extensions: vec!["lua".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_lua::LANGUAGE.into()
}
}
pub mod scala {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "Scala".to_string(),
extensions: vec!["scala".to_string(), "sc".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_scala::LANGUAGE.into()
}
}
pub mod c {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "C".to_string(),
extensions: vec!["c".to_string(), "h".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_c::LANGUAGE.into()
}
}
pub mod bash {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "Bash".to_string(),
extensions: vec!["sh".to_string(), "bash".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_bash::LANGUAGE.into()
}
}
pub mod json {
use super::{Language, LanguageConfig};
use once_cell::sync::Lazy;
pub static CONFIG: Lazy<LanguageConfig> = Lazy::new(|| LanguageConfig {
name: "JSON".to_string(),
extensions: vec!["json".to_string()],
queries: LanguageConfig::default_queries(),
});
pub fn language() -> Language {
tree_sitter_json::LANGUAGE.into()
}
}
}