Skip to content

Commit 1a7cbe0

Browse files
author
Marlon Costa
committed
feat(kotlin): Implement complete metrics support for Kotlin language
1 parent 010c4d2 commit 1a7cbe0

File tree

17 files changed

+1371
-581
lines changed

17 files changed

+1371
-581
lines changed

src/asttools.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
//! AST traversal and analysis utilities.
2+
//!
3+
//! This module provides helper functions and macros for traversing
4+
//! and analyzing the AST tree structure.
5+
6+
use crate::node::Node;
7+
8+
/// Gets an ancestor at a specific level above the current node.
9+
///
10+
/// # Arguments
11+
/// * `node` - The starting node
12+
/// * `level` - How many levels up to traverse (0 returns the node itself)
13+
///
14+
/// # Returns
15+
/// The ancestor node at the specified level, or None if the tree isn't deep enough.
16+
///
17+
/// # Example
18+
/// ```ignore
19+
/// // Get the grandparent (2 levels up)
20+
/// if let Some(grandparent) = get_parent(&node, 2) {
21+
/// println!("Grandparent kind: {}", grandparent.kind());
22+
/// }
23+
/// ```
24+
pub fn get_parent<'a>(node: &Node<'a>, level: usize) -> Option<Node<'a>> {
25+
let mut level = level;
26+
let mut current = *node;
27+
while level != 0 {
28+
current = current.parent()?;
29+
level -= 1;
30+
}
31+
Some(current)
32+
}
33+
34+
/// Traverses a tree passing from children to children in search of a specific
35+
/// token or series of tokens.
36+
///
37+
/// # Arguments
38+
/// * `node` - The starting node
39+
/// * `token_list` - A slice of predicates, each matching a level of descent
40+
///
41+
/// # Returns
42+
/// The final node after following the token path, or None if any token wasn't found.
43+
///
44+
/// # Example
45+
/// ```ignore
46+
/// // Find: node -> child matching pred1 -> grandchild matching pred2
47+
/// let result = traverse_children(&node, &[
48+
/// |id| id == SomeToken::Foo as u16,
49+
/// |id| id == SomeToken::Bar as u16,
50+
/// ]);
51+
/// ```
52+
pub fn traverse_children<'a, F>(node: &Node<'a>, token_list: &[F]) -> Option<Node<'a>>
53+
where
54+
F: Fn(u16) -> bool,
55+
{
56+
let mut current = *node;
57+
'outer: for token in token_list {
58+
for child in current.children() {
59+
if token(child.kind_id()) {
60+
current = child;
61+
continue 'outer;
62+
}
63+
}
64+
// Token not found at this level
65+
return None;
66+
}
67+
Some(current)
68+
}
69+
70+
/// Checks if a node has specific ancestors in sequence.
71+
///
72+
/// This macro checks if the node's ancestors match a specific pattern,
73+
/// where the first pattern(s) are immediate ancestors and the last pattern
74+
/// is the final ancestor to match.
75+
///
76+
/// # Example
77+
/// ```ignore
78+
/// // Check if node is inside a function inside a class
79+
/// let is_method = has_ancestors!(node, Class | Struct, Function);
80+
/// ```
81+
#[macro_export]
82+
macro_rules! has_ancestors {
83+
($node:expr, $( $typs:pat_param )|*, $( $typ:pat_param ),+) => {{
84+
let mut res = false;
85+
loop {
86+
let mut node = *$node;
87+
$(
88+
if let Some(parent) = node.parent() {
89+
match parent.kind_id().into() {
90+
$typ => {
91+
node = parent;
92+
},
93+
_ => {
94+
break;
95+
}
96+
}
97+
} else {
98+
break;
99+
}
100+
)*
101+
if let Some(parent) = node.parent() {
102+
match parent.kind_id().into() {
103+
$( $typs )|+ => {
104+
res = true;
105+
},
106+
_ => {}
107+
}
108+
}
109+
break;
110+
}
111+
res
112+
}};
113+
}
114+
115+
/// Counts specific ancestors matching a pattern until a stop condition.
116+
///
117+
/// This macro traverses up the tree counting ancestors that match the given
118+
/// patterns, stopping when it encounters an ancestor matching the stop pattern.
119+
///
120+
/// # Example
121+
/// ```ignore
122+
/// // Count nested if statements until we hit a function boundary
123+
/// let nesting = count_specific_ancestors!(node, If | ElseIf, Function | Method);
124+
/// ```
125+
#[macro_export]
126+
macro_rules! count_specific_ancestors {
127+
($node:expr, $checker:ty, $( $typs:pat_param )|*, $( $stops:pat_param )|*) => {{
128+
let mut count = 0;
129+
let mut node = *$node;
130+
while let Some(parent) = node.parent() {
131+
match parent.kind_id().into() {
132+
$( $typs )|* => {
133+
if !<$checker>::is_else_if(&parent) {
134+
count += 1;
135+
}
136+
},
137+
$( $stops )|* => break,
138+
_ => {}
139+
}
140+
node = parent;
141+
}
142+
count
143+
}};
144+
}
145+
146+
#[cfg(test)]
147+
mod tests {
148+
#[test]
149+
fn test_get_parent_level_zero() {
150+
// Level 0 should return the same node
151+
// (actual test would need a real node)
152+
}
153+
}

src/checker.rs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -663,39 +663,64 @@ impl Checker for RustCode {
663663
}
664664

665665
impl Checker for KotlinCode {
666-
fn is_comment(_: &Node) -> bool {
667-
false
666+
fn is_comment(node: &Node) -> bool {
667+
matches!(
668+
node.kind_id().into(),
669+
Kotlin::LineComment | Kotlin::MultilineComment
670+
)
668671
}
669672

670673
fn is_useful_comment(_: &Node, _: &[u8]) -> bool {
671674
false
672675
}
673676

674-
fn is_func_space(_: &Node) -> bool {
675-
false
677+
fn is_func_space(node: &Node) -> bool {
678+
matches!(
679+
node.kind_id().into(),
680+
Kotlin::SourceFile | Kotlin::ClassDeclaration
681+
)
676682
}
677683

678-
fn is_func(_: &Node) -> bool {
679-
false
684+
fn is_func(node: &Node) -> bool {
685+
node.kind_id() == Kotlin::FunctionDeclaration
680686
}
681687

682-
fn is_closure(_: &Node) -> bool {
683-
false
688+
fn is_closure(node: &Node) -> bool {
689+
node.kind_id() == Kotlin::LambdaLiteral
684690
}
685691

686-
fn is_call(_: &Node) -> bool {
687-
false
692+
fn is_call(node: &Node) -> bool {
693+
node.kind_id() == Kotlin::CallExpression
688694
}
689695

690-
fn is_non_arg(_: &Node) -> bool {
691-
false
696+
fn is_non_arg(node: &Node) -> bool {
697+
matches!(
698+
node.kind_id().into(),
699+
Kotlin::LPAREN
700+
| Kotlin::COMMA
701+
| Kotlin::RPAREN
702+
| Kotlin::PIPEPIPE
703+
| Kotlin::UnaryExpression
704+
)
692705
}
693706

694-
fn is_string(_: &Node) -> bool {
695-
false
707+
fn is_string(node: &Node) -> bool {
708+
// StringLiteral covers both single-line and multi-line strings in this grammar
709+
// StringContent captures the text content within strings
710+
matches!(
711+
node.kind_id().into(),
712+
Kotlin::StringLiteral | Kotlin::StringContent
713+
)
696714
}
697715

698-
fn is_else_if(_: &Node) -> bool {
716+
fn is_else_if(node: &Node) -> bool {
717+
if node.kind_id() != Kotlin::IfExpression {
718+
return false;
719+
}
720+
if let Some(parent) = node.parent() {
721+
return parent.kind_id() == Kotlin::Else;
722+
}
723+
699724
false
700725
}
701726

src/getter.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,4 +575,52 @@ impl Getter for JavaCode {
575575
}
576576
}
577577

578-
impl Getter for KotlinCode {}
578+
impl Getter for KotlinCode {
579+
fn get_space_kind(node: &Node) -> SpaceKind {
580+
use Kotlin::*;
581+
582+
let typ = node.kind_id().into();
583+
match typ {
584+
ClassDeclaration => SpaceKind::Class,
585+
FunctionDeclaration | Constructor | AnnotatedLambda => SpaceKind::Function,
586+
SourceFile => SpaceKind::Unit,
587+
_ => SpaceKind::Unknown,
588+
}
589+
}
590+
591+
fn get_op_type(node: &Node) -> HalsteadType {
592+
use Kotlin::*;
593+
594+
let typ = node.kind_id();
595+
596+
match typ.into() {
597+
// Operator: function calls
598+
CallExpression
599+
// Operator: control flow
600+
| If | Else | When | Try | Catch | Throw | For | While | Continue | Break | Do | Finally
601+
// Operator: keywords
602+
| Return | Abstract | Final | Super | This
603+
// Operator: brackets and comma and terminators (separators)
604+
| SEMI | COMMA | COLONCOLON | LBRACE | LBRACK | LPAREN | RBRACE | RBRACK | RPAREN | DOTDOT | DOT
605+
// Operator: operators
606+
| EQ | LT | GT | BANG | QMARKCOLON | AsQMARK | COLON // no grammar for lambda operator ->
607+
| EQEQ | LTEQ | GTEQ | BANGEQ | AMPAMP | PIPEPIPE | PLUSPLUS | DASHDASH
608+
| PLUS | DASH | STAR | SLASH | PERCENT
609+
| PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ => {
610+
HalsteadType::Operator
611+
}
612+
// Operands: variables, constants, literals
613+
// StringLiteral covers both line strings and multi-line strings in this grammar
614+
RealLiteral | IntegerLiteral | HexLiteral | BinLiteral | CharacterLiteralToken1 | UniCharacterLiteralToken1
615+
| LiteralConstant | StringLiteral | StringContent | LambdaLiteral | FunctionLiteral
616+
| ObjectLiteral | UnsignedLiteral | LongLiteral | BooleanLiteral | CharacterLiteral => {
617+
HalsteadType::Operand
618+
},
619+
_ => {
620+
HalsteadType::Unknown
621+
},
622+
}
623+
}
624+
625+
get_operator!(Kotlin);
626+
}

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ pub use crate::function::*;
8484
mod ast;
8585
pub use crate::ast::*;
8686

87+
/// AST traversal and analysis utilities.
88+
pub mod asttools;
89+
pub use crate::asttools::{get_parent, traverse_children};
90+
8791
mod count;
8892
pub use crate::count::*;
8993

0 commit comments

Comments
 (0)