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]
482 #[deprecated(
483 since = "31.0.0",
484 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"
485 )]
486 pub fn from_bytes(start: usize, end: usize) -> Self {
487 Self {
488 start: Position {
489 line: 0,
490 column: start,
491 },
492 end: Position {
493 line: 0,
494 column: end,
495 },
496 }
497 }
498}
499
500#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
502pub struct Position {
503 pub line: usize,
505 pub column: usize,
507}
508
509impl Position {
510 #[must_use]
512 pub fn new(line: usize, column: usize) -> Self {
513 Self { line, column }
514 }
515}
516
517#[derive(Debug, Clone, PartialEq)]
519pub enum NodeKind {
520 Function {
522 params: Vec<Param>,
524 return_type: Option<Type>,
526 is_async: bool,
528 },
529 Class {
531 bases: Vec<NodeId>,
533 interfaces: Vec<NodeId>,
535 },
536 Module {
538 exports: Vec<NodeId>,
540 },
541 Variable {
543 var_type: Option<Type>,
545 },
546}
547
548#[derive(Debug, Clone, PartialEq)]
550pub struct Param {
551 pub name: String,
553 pub param_type: Option<Type>,
555}
556
557#[derive(Debug, Clone, PartialEq)]
559pub struct Type {
560 pub name: String,
562}
563
564#[derive(Debug, Clone, Default)]
566pub struct NodeMetadata {
567 pub visibility: Option<String>,
569 pub doc_comment: Option<String>,
571 pub attributes: Vec<String>,
573}
574
575#[derive(Debug, Clone)]
577pub struct CodeNode {
578 pub id: NodeId,
580 pub kind: NodeKind,
582 pub span: Span,
584 pub metadata: NodeMetadata,
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 #[test]
593 fn test_node_id_creation() {
594 let id = NodeId::new(Language::Cpp, "src/main.cpp", "main");
595 assert_eq!(id.language, Language::Cpp);
596 assert_eq!(id.file.as_ref(), "src/main.cpp");
597 assert_eq!(id.qualified_name.as_ref(), "main");
598 }
599
600 #[test]
601 fn test_node_id_display() {
602 let id = NodeId::new(Language::Python, "api.py", "User.authenticate");
603 assert_eq!(id.to_string(), "py:api.py:User.authenticate");
604 }
605
606 #[test]
607 fn test_node_id_hash() {
608 use std::collections::HashSet;
609
610 let id1 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
611 let id2 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
612 let id3 = NodeId::new(Language::JavaScript, "api.js", "createUser");
613
614 let mut set = HashSet::new();
615 set.insert(id1.clone());
616 set.insert(id2.clone());
617 set.insert(id3.clone());
618
619 assert_eq!(set.len(), 2); }
621
622 #[test]
623 fn test_node_id_clone_cheap() {
624 let id1 = NodeId::new(Language::Cpp, "src/utils.cpp", "std::vector::push_back");
625 let id2 = id1.clone();
626
627 assert_eq!(Arc::as_ptr(&id1.file), Arc::as_ptr(&id2.file));
629 assert_eq!(
630 Arc::as_ptr(&id1.qualified_name),
631 Arc::as_ptr(&id2.qualified_name)
632 );
633 }
634
635 #[test]
636 fn test_symbol_name_extraction() {
637 let id1 = NodeId::new(Language::Cpp, "main.cpp", "std::vector::push_back");
638 assert_eq!(id1.symbol_name(), "push_back");
639
640 let id2 = NodeId::new(Language::Python, "api.py", "User.authenticate");
641 assert_eq!(id2.symbol_name(), "authenticate");
642
643 let id3 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
644 assert_eq!(id3.symbol_name(), "fetchUsers");
645 }
646
647 #[test]
648 fn test_span_creation() {
649 let span = Span::new(Position::new(10, 0), Position::new(20, 1));
650
651 assert_eq!(span.start.line, 10);
652 assert_eq!(span.end.line, 20);
653 }
654
655 #[test]
656 fn every_variant_round_trips_through_from_id() {
657 for &lang in Language::ALL {
660 assert_eq!(
661 Language::from_id(lang.canonical_name()),
662 Some(lang),
663 "{} canonical name does not round-trip",
664 lang.canonical_name()
665 );
666 assert_eq!(
667 Language::from_id(lang.short_name()),
668 Some(lang),
669 "{} short name does not round-trip",
670 lang.short_name()
671 );
672 for alias in lang.aliases() {
673 assert_eq!(
674 Language::from_id(alias),
675 Some(lang),
676 "alias {alias} does not round-trip"
677 );
678 }
679 assert_eq!(
681 Language::from_id(&format!(" {} ", lang.canonical_name().to_uppercase())),
682 Some(lang)
683 );
684 }
685 }
686
687 #[test]
688 fn language_all_is_complete_and_unique() {
689 assert_eq!(Language::ALL.len(), 37);
690 let names = Language::canonical_names();
691 let unique: std::collections::HashSet<_> = names.iter().collect();
692 assert_eq!(unique.len(), names.len(), "canonical names must be unique");
693 }
694
695 #[test]
696 fn canonical_and_short_names_are_pinned() {
697 assert_eq!(Language::TypeScript.canonical_name(), "typescript");
702 assert_eq!(Language::TypeScript.short_name(), "ts");
703 assert_eq!(Language::JavaScript.canonical_name(), "javascript");
704 assert_eq!(Language::JavaScript.short_name(), "js");
705 assert_eq!(Language::Python.canonical_name(), "python");
706 assert_eq!(Language::Python.short_name(), "py");
707 assert_eq!(Language::TypeScript.to_string(), "ts");
709 assert_eq!(Language::JavaScript.to_string(), "js");
710 assert_eq!(Language::Python.to_string(), "py");
711 }
712
713 #[test]
714 fn unknown_is_not_a_language() {
715 assert_eq!(Language::from_id("unknown"), None);
719 assert_eq!(Language::from_id("bogus"), None);
720 assert_eq!(Language::from_id(""), None);
721 assert_eq!(Language::from_id("servicenow-xanadu"), None);
723 assert_eq!(Language::from_id("servicenow-xml"), None);
724 }
725
726 #[test]
727 fn test_language_display() {
728 assert_eq!(Language::Cpp.to_string(), "cpp");
729 assert_eq!(Language::JavaScript.to_string(), "js");
730 assert_eq!(Language::Python.to_string(), "py");
731 assert_eq!(Language::Ruby.to_string(), "ruby");
732 assert_eq!(Language::Php.to_string(), "php");
733 assert_eq!(Language::Swift.to_string(), "swift");
734 assert_eq!(Language::Kotlin.to_string(), "kotlin");
735 assert_eq!(Language::Scala.to_string(), "scala");
736 assert_eq!(Language::Http.to_string(), "http");
737 }
738
739 #[test]
740 fn test_language_from_id() {
741 assert_eq!(Language::from_id("javascript"), Some(Language::JavaScript));
742 assert_eq!(Language::from_id("js"), Some(Language::JavaScript));
743 assert_eq!(Language::from_id("c#"), Some(Language::CSharp));
744 assert_eq!(Language::from_id("rb"), Some(Language::Ruby));
745 assert_eq!(Language::from_id("json"), Some(Language::Json));
746 assert_eq!(Language::from_id("unknown"), None);
747 }
748}