big_code_analysis_ast/getter/python.rs
1//! `Getter` implementation for Python.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6impl Getter for PythonCode {
7 fn get_space_kind(node: &Node) -> SpaceKind {
8 match node.kind_id().into() {
9 Python::FunctionDefinition => SpaceKind::Function,
10 Python::ClassDefinition => SpaceKind::Class,
11 Python::Module => SpaceKind::Unit,
12 _ => SpaceKind::Unknown,
13 }
14 }
15
16 fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
17 use Python::*;
18
19 match node.kind_id().into() {
20 // The `not` / `in` / `is` leaf tokens are operators on their own
21 // (`not x`, `a in b`, `a is b`, `for x in y`), but the grammar
22 // also nests them inside the compound `not in` (Notin) and
23 // `is not` (Isnot) nodes. When the leaf's parent is one of those
24 // compounds, the compound itself is classified as the single
25 // operator below, so the leaf must yield Unknown — otherwise
26 // `a not in b` would count `not` + `in` as two operators (#413).
27 Not | In | Is => match ancestors.parent(node).map(|p| p.kind_id().into()) {
28 Some(Notin | Isnot) => TokenRole::Unknown,
29 _ => TokenRole::Operator,
30 },
31 Import | DOT | From | COMMA | As | STAR | GTGT | Assert | COLONEQ | Return | Def
32 | Del | Raise | Pass | Break | Continue | If | Elif | Else | Async | For
33 | While | Try | Except | Finally | With | DASHGT | EQ | Global | Nonlocal | Exec
34 | AT | And | Or | PLUS | DASH | SLASH | PERCENT | SLASHSLASH | STARSTAR | PIPE
35 | AMP | CARET | LTLT | TILDE | LT | LTEQ | EQEQ | BANGEQ | GTEQ | GT | LTGT
36 | PLUSEQ | DASHEQ | STAREQ | SLASHEQ | ATEQ | SLASHSLASHEQ | PERCENTEQ | STARSTAREQ
37 | GTGTEQ | LTLTEQ | AMPEQ | CARETEQ | PIPEEQ | Yield | Print
38 // `not in` / `is not` compounds count as one operator each; the
39 // inner Not/In/Is leaves are suppressed by the parent-guard arm
40 // above (#413).
41 | Notin | Isnot
42 // `match` / `case` keyword tokens (Match=26, Case=27), mirroring
43 // the cyclomatic metric which already counts each `case` clause
44 // and the Rust Halstead which counts `match` (#413).
45 | Match | Case
46 // `nonlocal` keyword token, for parity with `global` which was
47 // already classified (#413).
48 // `await`: count only the await-expression node (Await=237);
49 // Await2 (keyword token 95) is the nested keyword and was being
50 // double-counted, mirroring how `yield` counts only the Yield
51 // node and not its keyword leaf (#413).
52 | Await
53 // `lambda`: count only the keyword token (Lambda3=73), not the
54 // Lambda/Lambda2 expression nodes that wrap it, to avoid the same
55 // node+keyword double count fixed for await (#413).
56 | Lambda3 => {
57 TokenRole::Operator
58 }
59 Identifier | Integer | Float | True | False | None => TokenRole::Operand,
60 String => {
61 // Docstring / module-level string statement: an `ExpressionStatement`
62 // whose only child is the string. Skip those.
63 let mut climb = ancestors.iter(node);
64 let Some((parent, _)) = climb.next() else {
65 return TokenRole::Unknown;
66 };
67 if parent.kind_id() == ExpressionStatement && parent.child_count() == 1 {
68 return TokenRole::Unknown;
69 }
70 // Implicit-concatenation docstring (`"""doc""" "more"`): the
71 // adjacent literals are wrapped in a `concatenated_string`,
72 // which is itself the sole child of the docstring
73 // `ExpressionStatement`. Without this arm each fragment would
74 // count as a separate operand, making the docstring's N2
75 // contribution depend on how many literals it was split into
76 // (#695). Suppress every fragment of such a docstring.
77 if parent.kind_id() == ConcatenatedString
78 && climb.next().is_some_and(|(grandparent, _)| {
79 grandparent.kind_id() == ExpressionStatement
80 && grandparent.child_count() == 1
81 })
82 {
83 return TokenRole::Unknown;
84 }
85 // Regression #191: an f-string wraps `Interpolation` children
86 // whose inner expressions are walked and counted separately.
87 // Skip the wrapping literal to avoid double-counting (same
88 // pattern as #180 for Bash/Elixir and #184 for PHP).
89 Self::string_operand_type(node, &[Interpolation as u16])
90 }
91 _ => TokenRole::Unknown,
92 }
93 }
94
95 fn get_operator_id_as_str(id: u16) -> &'static str {
96 Into::<Python>::into(id).into()
97 }
98}