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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! `Getter` implementation for Python.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
/// The role of a `{`, `:` or `=`, which Python spells both as syntax
/// and inside an f-string's replacement field.
///
/// As syntax each is an operator (#1486). The interpolation opener
/// (`f"{x}"`, and a nested `{w}` inside a format spec) is spelling, not
/// an operation — the six interpolating languages settled that in #1314
/// and Python follows them. The `:` that opens a format spec
/// (`f"{x:>10}"`) introduces the format mini-language, whose text is
/// string content and counts as nothing, so its delimiter does not
/// count either. The self-documenting `=` (`f"{x=}"`) is a directive of
/// the same field: it asks for the expression's text to be printed and
/// assigns nothing, like the `!r` conversion beside it, which was
/// already unbilled (#1551).
fn replacement_field_role(kind: Python, parent: Option<Python>) -> TokenRole {
use Python::*;
match (kind, parent) {
(LBRACE | EQ, Some(Interpolation | FormatExpression)) | (COLON, Some(FormatSpecifier)) => {
TokenRole::Unknown
}
_ => TokenRole::Operator,
}
}
impl Getter for PythonCode {
fn get_space_kind(node: &Node) -> SpaceKind {
match node.kind_id().into() {
Python::FunctionDefinition => SpaceKind::Function,
Python::ClassDefinition => SpaceKind::Class,
Python::Module => SpaceKind::Unit,
_ => SpaceKind::Unknown,
}
}
fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
use Python::*;
let kind: Python = node.kind_id().into();
// Bracket openers, `;`, `:` and `=` are vocabulary like every
// other getter's (#1395, #1486); only the f-string
// replacement-field spellings are withdrawn. An early return
// rather than an arm, because the match below is one rustfmt
// declines to format.
if matches!(kind, LBRACE | COLON | EQ) {
let parent = ancestors.parent(node).map(|p| p.kind_id().into());
return replacement_field_role(kind, parent);
}
match kind {
// The `not` / `in` / `is` leaf tokens are operators on their own
// (`not x`, `a in b`, `a is b`, `for x in y`), but the grammar
// also nests them inside the compound `not in` (Notin) and
// `is not` (Isnot) nodes. When the leaf's parent is one of those
// compounds, the compound itself is classified as the single
// operator below, so the leaf must yield Unknown — otherwise
// `a not in b` would count `not` + `in` as two operators (#413).
Not | In | Is => match ancestors.parent(node).map(|p| p.kind_id().into()) {
Some(Notin | Isnot) => TokenRole::Unknown,
_ => TokenRole::Operator,
},
Import | DOT | From | COMMA | As | STAR | GTGT | Assert | COLONEQ | Return | Def
| Del | Raise | Pass | Break | Continue | If | Elif | Else | Async | For
| While | Try | Except | Finally | With | DASHGT | Global | Nonlocal | Exec
| AT | And | Or | PLUS | DASH | SLASH | PERCENT | SLASHSLASH | STARSTAR | PIPE
| AMP | CARET | LTLT | TILDE | LT | LTEQ | EQEQ | BANGEQ | GTEQ | GT | LTGT
| PLUSEQ | DASHEQ | STAREQ | SLASHEQ | ATEQ | SLASHSLASHEQ | PERCENTEQ | STARSTAREQ
| GTGTEQ | LTLTEQ | AMPEQ | CARETEQ | PIPEEQ | Yield | Print | LPAREN | LBRACK
| SEMI
// `not in` / `is not` compounds count as one operator each; the
// inner Not/In/Is leaves are suppressed by the parent-guard arm
// above (#413).
| Notin | Isnot
// `match` / `case` keyword tokens (Match=26, Case=27), mirroring
// the cyclomatic metric which already counts each `case` clause
// and the Rust Halstead which counts `match` (#413).
| Match | Case
// `nonlocal` keyword token, for parity with `global` which was
// already classified (#413).
// `await`: count only the await-expression node (Await=237);
// Await2 (keyword token 95) is the nested keyword and was being
// double-counted, mirroring how `yield` counts only the Yield
// node and not its keyword leaf (#413).
| Await
// `lambda`: count only the keyword token (Lambda3=73), not the
// Lambda/Lambda2 expression nodes that wrap it, to avoid the same
// node+keyword double count fixed for await (#413).
| Lambda3
// `class`, the declaration keyword beside `def`. Only the leaf:
// its `ClassDefinition` wrapper is not listed, so the pair
// cannot double-count (#1550).
| Class => {
TokenRole::Operator
}
Identifier | Integer | Float | True | False | None => TokenRole::Operand,
String => {
// Docstring / module-level string statement: an `ExpressionStatement`
// whose only child is the string. Skip those.
let mut climb = ancestors.iter(node);
let Some((parent, _)) = climb.next() else {
return TokenRole::Unknown;
};
if parent.kind_id() == ExpressionStatement && parent.child_count() == 1 {
return TokenRole::Unknown;
}
// Implicit-concatenation docstring (`"""doc""" "more"`): the
// adjacent literals are wrapped in a `concatenated_string`,
// which is itself the sole child of the docstring
// `ExpressionStatement`. Without this arm each fragment would
// count as a separate operand, making the docstring's N2
// contribution depend on how many literals it was split into
// (#695). Suppress every fragment of such a docstring.
if parent.kind_id() == ConcatenatedString
&& climb.next().is_some_and(|(grandparent, _)| {
grandparent.kind_id() == ExpressionStatement
&& grandparent.child_count() == 1
})
{
return TokenRole::Unknown;
}
// Regression #191: an f-string wraps `Interpolation` children
// whose inner expressions are walked and counted separately.
// Skip the wrapping literal to avoid double-counting (same
// pattern as #180 for Bash/Elixir and #184 for PHP).
Self::string_operand_type(node, &[Interpolation as u16])
}
_ => TokenRole::Unknown,
}
}
get_operator!(Python);
}