use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
pub enum Language {
C,
Cpp,
CSharp,
Css,
JavaScript,
Python,
TypeScript,
Rust,
Go,
Java,
Ruby,
Php,
Swift,
Kotlin,
Scala,
Sql,
Dart,
Lua,
Perl,
Shell,
Groovy,
Elixir,
R,
Haskell,
Html,
Svelte,
Vue,
Zig,
Terraform,
Puppet,
Pulumi,
Http,
Plsql,
Apex,
Abap,
ServiceNow,
Json,
}
impl fmt::Display for Language {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.short_name())
}
}
impl Language {
pub const ALL: &'static [Self] = &[
Self::C,
Self::Cpp,
Self::CSharp,
Self::Css,
Self::JavaScript,
Self::Python,
Self::TypeScript,
Self::Rust,
Self::Go,
Self::Java,
Self::Ruby,
Self::Php,
Self::Swift,
Self::Kotlin,
Self::Scala,
Self::Sql,
Self::Dart,
Self::Lua,
Self::Perl,
Self::Shell,
Self::Groovy,
Self::Elixir,
Self::R,
Self::Haskell,
Self::Html,
Self::Svelte,
Self::Vue,
Self::Zig,
Self::Terraform,
Self::Puppet,
Self::Pulumi,
Self::Http,
Self::Plsql,
Self::Apex,
Self::Abap,
Self::ServiceNow,
Self::Json,
];
#[must_use]
pub const fn canonical_name(self) -> &'static str {
match self {
Self::C => "c",
Self::Cpp => "cpp",
Self::CSharp => "csharp",
Self::Css => "css",
Self::JavaScript => "javascript",
Self::Python => "python",
Self::TypeScript => "typescript",
Self::Rust => "rust",
Self::Go => "go",
Self::Java => "java",
Self::Ruby => "ruby",
Self::Php => "php",
Self::Swift => "swift",
Self::Kotlin => "kotlin",
Self::Scala => "scala",
Self::Sql => "sql",
Self::Dart => "dart",
Self::Lua => "lua",
Self::Perl => "perl",
Self::Shell => "shell",
Self::Groovy => "groovy",
Self::Elixir => "elixir",
Self::R => "r",
Self::Haskell => "haskell",
Self::Html => "html",
Self::Svelte => "svelte",
Self::Vue => "vue",
Self::Zig => "zig",
Self::Terraform => "terraform",
Self::Puppet => "puppet",
Self::Pulumi => "pulumi",
Self::Http => "http",
Self::Plsql => "plsql",
Self::Apex => "apex",
Self::Abap => "abap",
Self::ServiceNow => "servicenow",
Self::Json => "json",
}
}
#[must_use]
pub const fn short_name(self) -> &'static str {
match self {
Self::C => "c",
Self::Cpp => "cpp",
Self::CSharp => "csharp",
Self::Css => "css",
Self::JavaScript => "js",
Self::Python => "py",
Self::TypeScript => "ts",
Self::Rust => "rust",
Self::Go => "go",
Self::Java => "java",
Self::Ruby => "ruby",
Self::Php => "php",
Self::Swift => "swift",
Self::Kotlin => "kotlin",
Self::Scala => "scala",
Self::Sql => "sql",
Self::Dart => "dart",
Self::Lua => "lua",
Self::Perl => "perl",
Self::Shell => "shell",
Self::Groovy => "groovy",
Self::Elixir => "elixir",
Self::R => "r",
Self::Haskell => "haskell",
Self::Html => "html",
Self::Svelte => "svelte",
Self::Vue => "vue",
Self::Zig => "zig",
Self::Terraform => "terraform",
Self::Puppet => "puppet",
Self::Pulumi => "pulumi",
Self::Http => "http",
Self::Plsql => "plsql",
Self::Apex => "apex",
Self::Abap => "abap",
Self::ServiceNow => "servicenow",
Self::Json => "json",
}
}
#[must_use]
pub const fn aliases(self) -> &'static [&'static str] {
match self {
Self::C => &[],
Self::Cpp => &["c++", "cplusplus", "cxx"],
Self::CSharp => &["c#", "cs"],
Self::Css => &[],
Self::JavaScript => &[],
Self::Python => &[],
Self::TypeScript => &[],
Self::Rust => &["rs"],
Self::Go => &["golang"],
Self::Java => &[],
Self::Ruby => &["rb"],
Self::Php => &[],
Self::Swift => &[],
Self::Kotlin => &["kt"],
Self::Scala => &[],
Self::Sql => &[],
Self::Dart => &[],
Self::Lua => &[],
Self::Perl => &["pl"],
Self::Shell => &["bash", "sh"],
Self::Groovy => &[],
Self::Elixir => &["ex", "exs"],
Self::R => &[],
Self::Haskell => &["hs"],
Self::Html => &["html5"],
Self::Svelte => &[],
Self::Vue => &[],
Self::Zig => &[],
Self::Terraform => &["hcl", "tf"],
Self::Puppet => &[],
Self::Pulumi => &[],
Self::Http => &[],
Self::Plsql => &["pl/sql", "oracle"],
Self::Apex => &["salesforce"],
Self::Abap => &[],
Self::ServiceNow => &["xanadu"],
Self::Json => &[],
}
}
#[must_use]
pub fn accepted_names(self) -> Vec<&'static str> {
let mut names = vec![self.canonical_name()];
if self.short_name() != self.canonical_name() {
names.push(self.short_name());
}
names.extend_from_slice(self.aliases());
names
}
#[must_use]
pub fn canonical_names() -> Vec<&'static str> {
Self::ALL.iter().map(|lang| lang.canonical_name()).collect()
}
#[must_use]
pub fn from_id(value: &str) -> Option<Self> {
let needle = value.trim().to_ascii_lowercase();
Self::ALL.iter().copied().find(|lang| {
lang.canonical_name() == needle
|| lang.short_name() == needle
|| lang.aliases().contains(&needle.as_str())
})
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct NodeId {
pub language: Language,
pub file: Arc<str>,
pub qualified_name: Arc<str>,
}
impl NodeId {
pub fn new(language: Language, file: impl AsRef<str>, qualified_name: impl AsRef<str>) -> Self {
Self {
language,
file: Arc::from(file.as_ref()),
qualified_name: Arc::from(qualified_name.as_ref()),
}
}
#[must_use]
pub fn symbol_name(&self) -> &str {
if let Some(name) = self.qualified_name.rsplit("::").next()
&& name != self.qualified_name.as_ref()
{
return name;
}
if let Some(name) = self.qualified_name.rsplit('.').next() {
return name;
}
&self.qualified_name
}
}
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}:{}", self.language, self.file, self.qualified_name)
}
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct Span {
pub start: Position,
pub end: Position,
}
impl Span {
#[must_use]
pub fn new(start: Position, end: Position) -> Self {
Self { start, end }
}
#[must_use]
pub fn from_node(node: &tree_sitter::Node<'_>) -> Self {
let start = node.start_position();
let end = node.end_position();
Self {
start: Position {
line: start.row,
column: start.column,
},
end: Position {
line: end.row,
column: end.column,
},
}
}
#[must_use]
#[deprecated(
since = "31.0.0",
note = "builds a line-1 span with the byte offset in the column; use Span::from_node, or LineIndex::span when only offsets are available"
)]
pub fn from_bytes(start: usize, end: usize) -> Self {
Self {
start: Position {
line: 0,
column: start,
},
end: Position {
line: 0,
column: end,
},
}
}
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
pub struct Position {
pub line: usize,
pub column: usize,
}
impl Position {
#[must_use]
pub fn new(line: usize, column: usize) -> Self {
Self { line, column }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum NodeKind {
Function {
params: Vec<Param>,
return_type: Option<Type>,
is_async: bool,
},
Class {
bases: Vec<NodeId>,
interfaces: Vec<NodeId>,
},
Module {
exports: Vec<NodeId>,
},
Variable {
var_type: Option<Type>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Param {
pub name: String,
pub param_type: Option<Type>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Type {
pub name: String,
}
#[derive(Debug, Clone, Default)]
pub struct NodeMetadata {
pub visibility: Option<String>,
pub doc_comment: Option<String>,
pub attributes: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CodeNode {
pub id: NodeId,
pub kind: NodeKind,
pub span: Span,
pub metadata: NodeMetadata,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_id_creation() {
let id = NodeId::new(Language::Cpp, "src/main.cpp", "main");
assert_eq!(id.language, Language::Cpp);
assert_eq!(id.file.as_ref(), "src/main.cpp");
assert_eq!(id.qualified_name.as_ref(), "main");
}
#[test]
fn test_node_id_display() {
let id = NodeId::new(Language::Python, "api.py", "User.authenticate");
assert_eq!(id.to_string(), "py:api.py:User.authenticate");
}
#[test]
fn test_node_id_hash() {
use std::collections::HashSet;
let id1 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
let id2 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
let id3 = NodeId::new(Language::JavaScript, "api.js", "createUser");
let mut set = HashSet::new();
set.insert(id1.clone());
set.insert(id2.clone());
set.insert(id3.clone());
assert_eq!(set.len(), 2); }
#[test]
fn test_node_id_clone_cheap() {
let id1 = NodeId::new(Language::Cpp, "src/utils.cpp", "std::vector::push_back");
let id2 = id1.clone();
assert_eq!(Arc::as_ptr(&id1.file), Arc::as_ptr(&id2.file));
assert_eq!(
Arc::as_ptr(&id1.qualified_name),
Arc::as_ptr(&id2.qualified_name)
);
}
#[test]
fn test_symbol_name_extraction() {
let id1 = NodeId::new(Language::Cpp, "main.cpp", "std::vector::push_back");
assert_eq!(id1.symbol_name(), "push_back");
let id2 = NodeId::new(Language::Python, "api.py", "User.authenticate");
assert_eq!(id2.symbol_name(), "authenticate");
let id3 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
assert_eq!(id3.symbol_name(), "fetchUsers");
}
#[test]
fn test_span_creation() {
let span = Span::new(Position::new(10, 0), Position::new(20, 1));
assert_eq!(span.start.line, 10);
assert_eq!(span.end.line, 20);
}
#[test]
fn every_variant_round_trips_through_from_id() {
for &lang in Language::ALL {
assert_eq!(
Language::from_id(lang.canonical_name()),
Some(lang),
"{} canonical name does not round-trip",
lang.canonical_name()
);
assert_eq!(
Language::from_id(lang.short_name()),
Some(lang),
"{} short name does not round-trip",
lang.short_name()
);
for alias in lang.aliases() {
assert_eq!(
Language::from_id(alias),
Some(lang),
"alias {alias} does not round-trip"
);
}
assert_eq!(
Language::from_id(&format!(" {} ", lang.canonical_name().to_uppercase())),
Some(lang)
);
}
}
#[test]
fn language_all_is_complete_and_unique() {
assert_eq!(Language::ALL.len(), 37);
let names = Language::canonical_names();
let unique: std::collections::HashSet<_> = names.iter().collect();
assert_eq!(unique.len(), names.len(), "canonical names must be unique");
}
#[test]
fn canonical_and_short_names_are_pinned() {
assert_eq!(Language::TypeScript.canonical_name(), "typescript");
assert_eq!(Language::TypeScript.short_name(), "ts");
assert_eq!(Language::JavaScript.canonical_name(), "javascript");
assert_eq!(Language::JavaScript.short_name(), "js");
assert_eq!(Language::Python.canonical_name(), "python");
assert_eq!(Language::Python.short_name(), "py");
assert_eq!(Language::TypeScript.to_string(), "ts");
assert_eq!(Language::JavaScript.to_string(), "js");
assert_eq!(Language::Python.to_string(), "py");
}
#[test]
fn unknown_is_not_a_language() {
assert_eq!(Language::from_id("unknown"), None);
assert_eq!(Language::from_id("bogus"), None);
assert_eq!(Language::from_id(""), None);
assert_eq!(Language::from_id("servicenow-xanadu"), None);
assert_eq!(Language::from_id("servicenow-xml"), None);
}
#[test]
fn test_language_display() {
assert_eq!(Language::Cpp.to_string(), "cpp");
assert_eq!(Language::JavaScript.to_string(), "js");
assert_eq!(Language::Python.to_string(), "py");
assert_eq!(Language::Ruby.to_string(), "ruby");
assert_eq!(Language::Php.to_string(), "php");
assert_eq!(Language::Swift.to_string(), "swift");
assert_eq!(Language::Kotlin.to_string(), "kotlin");
assert_eq!(Language::Scala.to_string(), "scala");
assert_eq!(Language::Http.to_string(), "http");
}
#[test]
fn test_language_from_id() {
assert_eq!(Language::from_id("javascript"), Some(Language::JavaScript));
assert_eq!(Language::from_id("js"), Some(Language::JavaScript));
assert_eq!(Language::from_id("c#"), Some(Language::CSharp));
assert_eq!(Language::from_id("rb"), Some(Language::Ruby));
assert_eq!(Language::from_id("json"), Some(Language::Json));
assert_eq!(Language::from_id("unknown"), None);
}
}