1use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::sync::Arc;
9
10#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
12pub enum Language {
13 C,
15 Cpp,
17 CSharp,
19 Css,
21 JavaScript,
23 Python,
25 TypeScript,
27 Rust,
29 Go,
31 Java,
33 Ruby,
35 Php,
37 Swift,
39 Kotlin,
41 Scala,
43 Sql,
45 Dart,
47 Lua,
49 Perl,
51 Shell,
53 Groovy,
55 Elixir,
57 R,
59 Haskell,
61 Html,
63 Svelte,
65 Vue,
67 Zig,
69 Terraform,
71 Puppet,
73 Pulumi,
75 Http,
77 Plsql,
79 Apex,
81 Abap,
83 ServiceNow,
85 Json,
87}
88
89impl fmt::Display for Language {
90 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
91 f.write_str(self.short_name())
96 }
97}
98
99impl Language {
100 pub const ALL: &'static [Self] = &[
107 Self::C,
108 Self::Cpp,
109 Self::CSharp,
110 Self::Css,
111 Self::JavaScript,
112 Self::Python,
113 Self::TypeScript,
114 Self::Rust,
115 Self::Go,
116 Self::Java,
117 Self::Ruby,
118 Self::Php,
119 Self::Swift,
120 Self::Kotlin,
121 Self::Scala,
122 Self::Sql,
123 Self::Dart,
124 Self::Lua,
125 Self::Perl,
126 Self::Shell,
127 Self::Groovy,
128 Self::Elixir,
129 Self::R,
130 Self::Haskell,
131 Self::Html,
132 Self::Svelte,
133 Self::Vue,
134 Self::Zig,
135 Self::Terraform,
136 Self::Puppet,
137 Self::Pulumi,
138 Self::Http,
139 Self::Plsql,
140 Self::Apex,
141 Self::Abap,
142 Self::ServiceNow,
143 Self::Json,
144 ];
145
146 #[must_use]
152 pub const fn canonical_name(self) -> &'static str {
153 match self {
154 Self::C => "c",
155 Self::Cpp => "cpp",
156 Self::CSharp => "csharp",
157 Self::Css => "css",
158 Self::JavaScript => "javascript",
159 Self::Python => "python",
160 Self::TypeScript => "typescript",
161 Self::Rust => "rust",
162 Self::Go => "go",
163 Self::Java => "java",
164 Self::Ruby => "ruby",
165 Self::Php => "php",
166 Self::Swift => "swift",
167 Self::Kotlin => "kotlin",
168 Self::Scala => "scala",
169 Self::Sql => "sql",
170 Self::Dart => "dart",
171 Self::Lua => "lua",
172 Self::Perl => "perl",
173 Self::Shell => "shell",
174 Self::Groovy => "groovy",
175 Self::Elixir => "elixir",
176 Self::R => "r",
177 Self::Haskell => "haskell",
178 Self::Html => "html",
179 Self::Svelte => "svelte",
180 Self::Vue => "vue",
181 Self::Zig => "zig",
182 Self::Terraform => "terraform",
183 Self::Puppet => "puppet",
184 Self::Pulumi => "pulumi",
185 Self::Http => "http",
186 Self::Plsql => "plsql",
187 Self::Apex => "apex",
188 Self::Abap => "abap",
189 Self::ServiceNow => "servicenow",
190 Self::Json => "json",
191 }
192 }
193
194 #[must_use]
201 pub const fn short_name(self) -> &'static str {
202 match self {
203 Self::C => "c",
204 Self::Cpp => "cpp",
205 Self::CSharp => "csharp",
206 Self::Css => "css",
207 Self::JavaScript => "js",
208 Self::Python => "py",
209 Self::TypeScript => "ts",
210 Self::Rust => "rust",
211 Self::Go => "go",
212 Self::Java => "java",
213 Self::Ruby => "ruby",
214 Self::Php => "php",
215 Self::Swift => "swift",
216 Self::Kotlin => "kotlin",
217 Self::Scala => "scala",
218 Self::Sql => "sql",
219 Self::Dart => "dart",
220 Self::Lua => "lua",
221 Self::Perl => "perl",
222 Self::Shell => "shell",
223 Self::Groovy => "groovy",
224 Self::Elixir => "elixir",
225 Self::R => "r",
226 Self::Haskell => "haskell",
227 Self::Html => "html",
228 Self::Svelte => "svelte",
229 Self::Vue => "vue",
230 Self::Zig => "zig",
231 Self::Terraform => "terraform",
232 Self::Puppet => "puppet",
233 Self::Pulumi => "pulumi",
234 Self::Http => "http",
235 Self::Plsql => "plsql",
236 Self::Apex => "apex",
237 Self::Abap => "abap",
238 Self::ServiceNow => "servicenow",
239 Self::Json => "json",
240 }
241 }
242
243 #[must_use]
249 pub const fn aliases(self) -> &'static [&'static str] {
250 match self {
251 Self::C => &[],
252 Self::Cpp => &["c++", "cplusplus", "cxx"],
253 Self::CSharp => &["c#", "cs"],
254 Self::Css => &[],
255 Self::JavaScript => &[],
256 Self::Python => &[],
257 Self::TypeScript => &[],
258 Self::Rust => &["rs"],
259 Self::Go => &["golang"],
260 Self::Java => &[],
261 Self::Ruby => &["rb"],
262 Self::Php => &[],
263 Self::Swift => &[],
264 Self::Kotlin => &["kt"],
265 Self::Scala => &[],
266 Self::Sql => &[],
267 Self::Dart => &[],
268 Self::Lua => &[],
269 Self::Perl => &["pl"],
270 Self::Shell => &["bash", "sh"],
271 Self::Groovy => &[],
272 Self::Elixir => &["ex", "exs"],
273 Self::R => &[],
274 Self::Haskell => &["hs"],
275 Self::Html => &["html5"],
276 Self::Svelte => &[],
277 Self::Vue => &[],
278 Self::Zig => &[],
279 Self::Terraform => &["hcl", "tf"],
280 Self::Puppet => &[],
281 Self::Pulumi => &[],
282 Self::Http => &[],
283 Self::Plsql => &["pl/sql", "oracle"],
284 Self::Apex => &["salesforce"],
285 Self::Abap => &[],
286 Self::ServiceNow => &["xanadu"],
287 Self::Json => &[],
288 }
289 }
290
291 #[must_use]
293 pub fn accepted_names(self) -> Vec<&'static str> {
294 let mut names = vec![self.canonical_name()];
295 if self.short_name() != self.canonical_name() {
296 names.push(self.short_name());
297 }
298 names.extend_from_slice(self.aliases());
299 names
300 }
301
302 #[must_use]
307 pub fn canonical_names() -> Vec<&'static str> {
308 Self::ALL.iter().map(|lang| lang.canonical_name()).collect()
309 }
310
311 #[must_use]
319 pub fn from_id(value: &str) -> Option<Self> {
320 let needle = value.trim().to_ascii_lowercase();
321 Self::ALL.iter().copied().find(|lang| {
322 lang.canonical_name() == needle
323 || lang.short_name() == needle
324 || lang.aliases().contains(&needle.as_str())
325 })
326 }
327}
328
329#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
351pub struct NodeId {
352 pub language: Language,
354 pub file: Arc<str>,
356 pub qualified_name: Arc<str>,
359}
360
361impl NodeId {
362 pub fn new(language: Language, file: impl AsRef<str>, qualified_name: impl AsRef<str>) -> Self {
375 Self {
376 language,
377 file: Arc::from(file.as_ref()),
378 qualified_name: Arc::from(qualified_name.as_ref()),
379 }
380 }
381
382 #[must_use]
393 pub fn symbol_name(&self) -> &str {
394 if let Some(name) = self.qualified_name.rsplit("::").next()
396 && name != self.qualified_name.as_ref()
397 {
398 return name;
399 }
400
401 if let Some(name) = self.qualified_name.rsplit('.').next() {
402 return name;
403 }
404
405 &self.qualified_name
406 }
407}
408
409impl fmt::Display for NodeId {
410 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
411 write!(f, "{}:{}:{}", self.language, self.file, self.qualified_name)
412 }
413}
414
415#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
417pub struct Span {
418 pub start: Position,
420 pub end: Position,
422}
423
424impl Span {
425 #[must_use]
427 pub fn new(start: Position, end: Position) -> Self {
428 Self { start, end }
429 }
430
431 #[must_use]
444 pub fn from_node(node: &tree_sitter::Node<'_>) -> Self {
445 let start = node.start_position();
446 let end = node.end_position();
447 Self {
448 start: Position {
449 line: start.row,
450 column: start.column,
451 },
452 end: Position {
453 line: end.row,
454 column: end.column,
455 },
456 }
457 }
458
459 #[must_use]
474 pub fn from_bytes(start: usize, end: usize) -> Self {
475 Self {
476 start: Position {
477 line: 0,
478 column: start,
479 },
480 end: Position {
481 line: 0,
482 column: end,
483 },
484 }
485 }
486}
487
488#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
490pub struct Position {
491 pub line: usize,
493 pub column: usize,
495}
496
497impl Position {
498 #[must_use]
500 pub fn new(line: usize, column: usize) -> Self {
501 Self { line, column }
502 }
503}
504
505#[derive(Debug, Clone, PartialEq)]
507pub enum NodeKind {
508 Function {
510 params: Vec<Param>,
512 return_type: Option<Type>,
514 is_async: bool,
516 },
517 Class {
519 bases: Vec<NodeId>,
521 interfaces: Vec<NodeId>,
523 },
524 Module {
526 exports: Vec<NodeId>,
528 },
529 Variable {
531 var_type: Option<Type>,
533 },
534}
535
536#[derive(Debug, Clone, PartialEq)]
538pub struct Param {
539 pub name: String,
541 pub param_type: Option<Type>,
543}
544
545#[derive(Debug, Clone, PartialEq)]
547pub struct Type {
548 pub name: String,
550}
551
552#[derive(Debug, Clone, Default)]
554pub struct NodeMetadata {
555 pub visibility: Option<String>,
557 pub doc_comment: Option<String>,
559 pub attributes: Vec<String>,
561}
562
563#[derive(Debug, Clone)]
565pub struct CodeNode {
566 pub id: NodeId,
568 pub kind: NodeKind,
570 pub span: Span,
572 pub metadata: NodeMetadata,
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 #[test]
581 fn test_node_id_creation() {
582 let id = NodeId::new(Language::Cpp, "src/main.cpp", "main");
583 assert_eq!(id.language, Language::Cpp);
584 assert_eq!(id.file.as_ref(), "src/main.cpp");
585 assert_eq!(id.qualified_name.as_ref(), "main");
586 }
587
588 #[test]
589 fn test_node_id_display() {
590 let id = NodeId::new(Language::Python, "api.py", "User.authenticate");
591 assert_eq!(id.to_string(), "py:api.py:User.authenticate");
592 }
593
594 #[test]
595 fn test_node_id_hash() {
596 use std::collections::HashSet;
597
598 let id1 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
599 let id2 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
600 let id3 = NodeId::new(Language::JavaScript, "api.js", "createUser");
601
602 let mut set = HashSet::new();
603 set.insert(id1.clone());
604 set.insert(id2.clone());
605 set.insert(id3.clone());
606
607 assert_eq!(set.len(), 2); }
609
610 #[test]
611 fn test_node_id_clone_cheap() {
612 let id1 = NodeId::new(Language::Cpp, "src/utils.cpp", "std::vector::push_back");
613 let id2 = id1.clone();
614
615 assert_eq!(Arc::as_ptr(&id1.file), Arc::as_ptr(&id2.file));
617 assert_eq!(
618 Arc::as_ptr(&id1.qualified_name),
619 Arc::as_ptr(&id2.qualified_name)
620 );
621 }
622
623 #[test]
624 fn test_symbol_name_extraction() {
625 let id1 = NodeId::new(Language::Cpp, "main.cpp", "std::vector::push_back");
626 assert_eq!(id1.symbol_name(), "push_back");
627
628 let id2 = NodeId::new(Language::Python, "api.py", "User.authenticate");
629 assert_eq!(id2.symbol_name(), "authenticate");
630
631 let id3 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
632 assert_eq!(id3.symbol_name(), "fetchUsers");
633 }
634
635 #[test]
636 fn test_span_creation() {
637 let span = Span::new(Position::new(10, 0), Position::new(20, 1));
638
639 assert_eq!(span.start.line, 10);
640 assert_eq!(span.end.line, 20);
641 }
642
643 #[test]
644 fn every_variant_round_trips_through_from_id() {
645 for &lang in Language::ALL {
648 assert_eq!(
649 Language::from_id(lang.canonical_name()),
650 Some(lang),
651 "{} canonical name does not round-trip",
652 lang.canonical_name()
653 );
654 assert_eq!(
655 Language::from_id(lang.short_name()),
656 Some(lang),
657 "{} short name does not round-trip",
658 lang.short_name()
659 );
660 for alias in lang.aliases() {
661 assert_eq!(
662 Language::from_id(alias),
663 Some(lang),
664 "alias {alias} does not round-trip"
665 );
666 }
667 assert_eq!(
669 Language::from_id(&format!(" {} ", lang.canonical_name().to_uppercase())),
670 Some(lang)
671 );
672 }
673 }
674
675 #[test]
676 fn language_all_is_complete_and_unique() {
677 assert_eq!(Language::ALL.len(), 37);
678 let names = Language::canonical_names();
679 let unique: std::collections::HashSet<_> = names.iter().collect();
680 assert_eq!(unique.len(), names.len(), "canonical names must be unique");
681 }
682
683 #[test]
684 fn canonical_and_short_names_are_pinned() {
685 assert_eq!(Language::TypeScript.canonical_name(), "typescript");
690 assert_eq!(Language::TypeScript.short_name(), "ts");
691 assert_eq!(Language::JavaScript.canonical_name(), "javascript");
692 assert_eq!(Language::JavaScript.short_name(), "js");
693 assert_eq!(Language::Python.canonical_name(), "python");
694 assert_eq!(Language::Python.short_name(), "py");
695 assert_eq!(Language::TypeScript.to_string(), "ts");
697 assert_eq!(Language::JavaScript.to_string(), "js");
698 assert_eq!(Language::Python.to_string(), "py");
699 }
700
701 #[test]
702 fn unknown_is_not_a_language() {
703 assert_eq!(Language::from_id("unknown"), None);
707 assert_eq!(Language::from_id("bogus"), None);
708 assert_eq!(Language::from_id(""), None);
709 assert_eq!(Language::from_id("servicenow-xanadu"), None);
711 assert_eq!(Language::from_id("servicenow-xml"), None);
712 }
713
714 #[test]
715 fn test_language_display() {
716 assert_eq!(Language::Cpp.to_string(), "cpp");
717 assert_eq!(Language::JavaScript.to_string(), "js");
718 assert_eq!(Language::Python.to_string(), "py");
719 assert_eq!(Language::Ruby.to_string(), "ruby");
720 assert_eq!(Language::Php.to_string(), "php");
721 assert_eq!(Language::Swift.to_string(), "swift");
722 assert_eq!(Language::Kotlin.to_string(), "kotlin");
723 assert_eq!(Language::Scala.to_string(), "scala");
724 assert_eq!(Language::Http.to_string(), "http");
725 }
726
727 #[test]
728 fn test_language_from_id() {
729 assert_eq!(Language::from_id("javascript"), Some(Language::JavaScript));
730 assert_eq!(Language::from_id("js"), Some(Language::JavaScript));
731 assert_eq!(Language::from_id("c#"), Some(Language::CSharp));
732 assert_eq!(Language::from_id("rb"), Some(Language::Ruby));
733 assert_eq!(Language::from_id("json"), Some(Language::Json));
734 assert_eq!(Language::from_id("unknown"), None);
735 }
736}