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
//! `Getter` implementation for Python.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
use crate::lang_helpers::python::{python_is_bare_string, python_is_bare_string_parenthesis};
/// 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);
}
// The parentheses around a bare string are discarded with it,
// as CPython drops them (#1610).
if kind == LPAREN && python_is_bare_string_parenthesis(node, ancestors) {
return TokenRole::Unknown;
}
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 => {
// A bare string statement's string — its sole value, or a
// fragment of an implicitly concatenated one — is discarded,
// so it is not an operand (#695). The LOC walk bills the same
// strings as comment rows through this predicate, so the two
// agree on which strings are discarded; whether one is also
// documentation is `python_documentation_string_run`'s
// question, which Halstead deliberately does not ask (#1597).
if python_is_bare_string(node, ancestors) {
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);
}