1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//! `Cognitive` implementation for Kotlin.
#![allow(
clippy::enum_glob_use,
clippy::match_same_arms,
clippy::needless_pass_by_value,
clippy::wildcard_imports
)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use super::*;
/// Whether `node` is a labeled *jump* — `break@label` / `continue@label`.
///
/// tree-sitter-kotlin-ng has no break/continue/jump statement kind: it
/// models a labeled jump as a `labeled_expression` whose first child is a
/// `label` holding the *fused* jump keyword `break@` / `continue@` (the
/// loop-target identifier is a sibling). A labeled *non-jump*
/// (`lbl@ run { … }`, `lbl@ if (…) {…}`) is also a `labeled_expression`,
/// but its label is an ordinary `name@`, so gating on the label token
/// starting with `break@` / `continue@` excludes it (#450). `return@label`
/// is a distinct `return_expression` and never reaches here; a bare
/// `break` / `continue` parses as a plain identifier, never a
/// `labeled_expression`.
fn kotlin_is_labeled_jump(node: &Node, code: &[u8]) -> bool {
node.child(0)
.filter(|c| c.kind_id() == Kotlin::Label as u16)
.and_then(|c| c.utf8_text(code))
.is_some_and(|t| t.starts_with("break@") || t.starts_with("continue@"))
}
impl Cognitive for KotlinCode {
fn compute<'a>(
node: &Node<'a>,
code: &'a [u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
nesting_map: &mut NestingMap,
) {
use Kotlin::*;
let mut nesting = get_nesting_from_map(node, nesting_map);
match node.kind_id().into() {
IfExpression if !Self::is_else_if(node, ancestors) => {
increase_nesting(stats, &mut nesting);
}
ForStatement | WhileStatement | DoWhileStatement | WhenExpression | CatchBlock => {
increase_nesting(stats, &mut nesting);
}
Else => {
// Per the SonarSource spec, `else ->` inside a `when`
// expression is the default arm of a switch-like construct
// and should be +0, not +1.
//
// Read off the walker's chain rather than `Node::parent`,
// which is `O(depth)` — one climb per `else` keeps the
// metric quadratic on a deeply nested `if`/`else` chain,
// the shape #1062 exists to make linear.
let in_when = ancestors.parent_has_kind(node, WhenEntry as u16);
if !in_when {
increment_by_one(stats);
}
}
// SonarSource §B2: labeled `break@outer` / `continue@outer`
// each add +1 for breaking structured control flow; bare
// `break` / `continue` are +0. See `kotlin_is_labeled_jump`
// for how the grammar models the labeled-jump shape (#450).
LabeledExpression if kotlin_is_labeled_jump(node, code) => {
increment_by_one(stats);
}
BinaryExpression => {
// Kotlin's Elvis operator `?:` (token `QMARKCOLON`) is a
// short-circuit nullish operator analogous to JS `??` and
// forms boolean sequences alongside `&&` / `||` per
// SonarSource Cognitive Complexity B1.
compute_booleans_with(node, stats, |id| {
matches!(id.into(), AMPAMP | PIPEPIPE | QMARKCOLON)
});
}
// Property accessors and `init { … }` are function
// boundaries too (#1184). They open a `FuncSpace`, so without
// this arm an accessor written inside an `if` inherited that
// `if`'s nesting and scored more than the byte-identical
// accessor at top level — the same defect #1160 fixed for
// Java's compact constructor. They are `is_func_space`
// without being `is_func`, which does not matter here: this
// arm matches kinds directly.
FunctionDeclaration | SecondaryConstructor | Getter | Setter | AnonymousInitializer => {
enter_function_boundary(
&mut nesting,
node,
ancestors,
&[
FunctionDeclaration,
SecondaryConstructor,
Getter,
Setter,
AnonymousInitializer,
],
);
}
LambdaLiteral | AnonymousFunction => {
nesting.lambda += 1;
}
_ => {}
}
nesting_map.insert(node.id(), nesting);
}
}