Skip to content

Commit 6a86e55

Browse files
author
Marlon Costa
committed
feat(kotlin): Implement complete metrics support for Kotlin language
- Implement Checker trait for KotlinCode with AST node recognition - Implement Getter trait with space kind and Halstead classification - Implement all 11 metric modules: ABC, Cognitive, Cyclomatic, Exit, Halstead, LOC, NArgs, NOM, NPA, NPM, WMC - Add traverse_children helper method to Node for multi-level traversal
1 parent 30bb1c2 commit 6a86e55

File tree

13 files changed

+568
-31
lines changed

13 files changed

+568
-31
lines changed

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
@@ -576,4 +576,52 @@ impl Getter for JavaCode {
576576
}
577577
}
578578

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

src/metrics/abc.rs

Lines changed: 232 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,7 @@ implement_metric_trait!(
352352
RustCode,
353353
CppCode,
354354
PreprocCode,
355-
CcommentCode,
356-
KotlinCode
355+
CcommentCode
357356
);
358357

359358
// Fitzpatrick, Jerry (1997). "Applying the ABC metric to C, C++ and Java". C++ Report.
@@ -546,6 +545,237 @@ impl Abc for JavaCode {
546545
}
547546
}
548547

548+
// Inspects the content of Kotlin parenthesized expressions
549+
// and `Not` operators to find unary conditional expressions
550+
fn kotlin_inspect_container(container_node: &Node, conditions: &mut f64) {
551+
use Kotlin::*;
552+
553+
let mut node = *container_node;
554+
let mut node_kind = node.kind_id().into();
555+
556+
// Initializes the flag to true if the container is known to contain a boolean value
557+
let mut has_boolean_content = matches!(
558+
node.parent().unwrap().kind_id().into(),
559+
BinaryExpression | IfExpression | WhileStatement | DoWhileStatement | ForStatement
560+
);
561+
562+
// Looks inside parenthesized expressions and `Not` operators to find what they contain
563+
loop {
564+
// Checks if the node is a parenthesized expression or a `Not` operator
565+
// The child node of index 0 contains the unary expression operator (we look for the `!` operator)
566+
let is_parenthesised_exp = matches!(node_kind, ParenthesizedExpression);
567+
let is_not_operator = matches!(node_kind, UnaryExpression)
568+
&& matches!(node.child(0).unwrap().kind_id().into(), BANG);
569+
570+
// Stops the exploration if the node is neither
571+
// a parenthesized expression nor a `Not` operator
572+
if !is_parenthesised_exp && !is_not_operator {
573+
break;
574+
}
575+
576+
// Sets the flag to true if a `Not` operator is found
577+
// This is used to prove if a variable or a value returned by a method is actually boolean
578+
// e.g. `return (!x);`
579+
if !has_boolean_content && is_not_operator {
580+
has_boolean_content = true;
581+
}
582+
583+
// Parenthesized expressions and `Not` operators nodes
584+
// always store their expressions in the children nodes of index one
585+
// Reference: tree-sitter-kotlin grammar for parenthesized_expression and prefix_expression
586+
node = node.child(1).unwrap();
587+
node_kind = node.kind_id().into();
588+
589+
// Stops the exploration when the content is found
590+
if matches!(node_kind, CallExpression | Identifier | True | False) {
591+
if has_boolean_content {
592+
*conditions += 1.;
593+
}
594+
break;
595+
}
596+
}
597+
}
598+
599+
// Inspects a list of elements and counts any unary conditional expression found
600+
fn kotlin_count_unary_conditions(list_node: &Node, conditions: &mut f64) {
601+
use Kotlin::*;
602+
603+
let list_kind = list_node.kind_id().into();
604+
let mut cursor = list_node.cursor();
605+
606+
// Scans the immediate children nodes of the argument node
607+
if cursor.goto_first_child() {
608+
loop {
609+
// Gets the current child node and its kind
610+
let node = cursor.node();
611+
let node_kind = node.kind_id().into();
612+
613+
// Checks if the node is a unary condition
614+
if matches!(node_kind, CallExpression | Identifier | True | False)
615+
&& matches!(list_kind, BinaryExpression)
616+
{
617+
*conditions += 1.;
618+
} else {
619+
// Checks if the node is a unary condition container
620+
kotlin_inspect_container(&node, conditions);
621+
}
622+
623+
// Moves the cursor to the next sibling node of the current node
624+
// Exits the scan if there is no next sibling node
625+
if !cursor.goto_next_sibling() {
626+
break;
627+
}
628+
}
629+
}
630+
}
631+
632+
impl Abc for KotlinCode {
633+
fn compute(node: &Node, stats: &mut Stats) {
634+
use Kotlin::*;
635+
636+
match node.kind_id().into() {
637+
STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | PLUSPLUS | DASHDASH => {
638+
stats.assignments += 1.;
639+
}
640+
VariableDeclaration | PropertyDeclaration => {
641+
stats.declaration.push(DeclKind::Var);
642+
}
643+
Final => {
644+
if let Some(DeclKind::Var) = stats.declaration.last() {
645+
stats.declaration.push(DeclKind::Const);
646+
}
647+
}
648+
SEMI => {
649+
if let Some(DeclKind::Const | DeclKind::Var) = stats.declaration.last() {
650+
stats.declaration.clear();
651+
}
652+
}
653+
EQ => {
654+
// Excludes constant declarations
655+
stats
656+
.declaration
657+
.last()
658+
.map(|decl| {
659+
if matches!(decl, DeclKind::Var) {
660+
stats.assignments += 1.;
661+
}
662+
})
663+
.unwrap_or_else(|| {
664+
stats.assignments += 1.;
665+
});
666+
}
667+
CallExpression => {
668+
stats.branches += 1.;
669+
}
670+
GTEQ | LTEQ | EQEQ | BANGEQ | Else | Try | Catch => {
671+
stats.conditions += 1.;
672+
}
673+
GT | LT => {
674+
// Excludes `<` and `>` used for generic types
675+
if let Some(parent) = node.parent()
676+
&& !matches!(parent.kind_id().into(), TypeArguments)
677+
{
678+
stats.conditions += 1.;
679+
}
680+
}
681+
// Counts unary conditions in elements separated by `&&` or `||` boolean operators
682+
AMPAMP | PIPEPIPE => {
683+
if let Some(parent) = node.parent() {
684+
kotlin_count_unary_conditions(&parent, &mut stats.conditions);
685+
}
686+
}
687+
// Counts unary conditions inside assignments
688+
Assignment => {
689+
// The child node of index 2 contains the right operand of an assignment operation
690+
if let Some(right_operand) = node.child(2)
691+
&& matches!(
692+
right_operand.kind_id().into(),
693+
ParenthesizedExpression | UnaryExpression
694+
)
695+
{
696+
kotlin_inspect_container(&right_operand, &mut stats.conditions);
697+
}
698+
}
699+
// Counts unary conditions inside if and while statements
700+
IfExpression | WhileStatement => {
701+
// The child node of index 1 contains the condition
702+
if let Some(condition) = node.child(1)
703+
&& matches!(condition.kind_id().into(), ParenthesizedExpression)
704+
{
705+
kotlin_inspect_container(&condition, &mut stats.conditions);
706+
}
707+
}
708+
// Counts unary conditions inside do-while statements
709+
DoWhileStatement => {
710+
// The child node of index 3 contains the condition
711+
if let Some(condition) = node.child(3)
712+
&& matches!(condition.kind_id().into(), ParenthesizedExpression)
713+
{
714+
kotlin_inspect_container(&condition, &mut stats.conditions);
715+
}
716+
}
717+
// Counts unary conditions inside for statements
718+
ForStatement => {
719+
// The child node of index 3 contains the `condition` when
720+
// the initialization expression is a variable declaration
721+
// e.g. `for ( int i=0; `condition`; ... ) {}`
722+
if let Some(condition) = node.child(3) {
723+
match condition.kind_id().into() {
724+
SEMI => {
725+
// The child node of index 4 contains the `condition` when
726+
// the initialization expression is not a variable declaration
727+
// e.g. `for ( i=0; `condition`; ... ) {}`
728+
if let Some(cond) = node.child(4) {
729+
match cond.kind_id().into() {
730+
CallExpression | Identifier | True | False | SEMI | RPAREN => {
731+
stats.conditions += 1.;
732+
}
733+
ParenthesizedExpression | UnaryExpression => {
734+
kotlin_inspect_container(&cond, &mut stats.conditions);
735+
}
736+
_ => {}
737+
}
738+
}
739+
}
740+
CallExpression | Identifier | True | False => {
741+
stats.conditions += 1.;
742+
}
743+
ParenthesizedExpression | UnaryExpression => {
744+
kotlin_inspect_container(&condition, &mut stats.conditions);
745+
}
746+
_ => {}
747+
}
748+
}
749+
}
750+
// Counts unary conditions inside return statements
751+
Return => {
752+
// The child node of index 1 contains the return value
753+
if let Some(value) = node.child(1)
754+
&& matches!(
755+
value.kind_id().into(),
756+
ParenthesizedExpression | UnaryExpression
757+
)
758+
{
759+
kotlin_inspect_container(&value, &mut stats.conditions)
760+
}
761+
}
762+
// Counts unary conditions inside implicit return statements in lambda expressions
763+
AnnotatedLambda => {
764+
// The child node of index 2 contains the return value
765+
if let Some(value) = node.child(2)
766+
&& matches!(
767+
value.kind_id().into(),
768+
ParenthesizedExpression | UnaryExpression
769+
)
770+
{
771+
kotlin_inspect_container(&value, &mut stats.conditions)
772+
}
773+
}
774+
_ => {}
775+
}
776+
}
777+
}
778+
549779
#[cfg(test)]
550780
mod tests {
551781
use crate::tools::check_metrics;

0 commit comments

Comments
 (0)