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
6use crate::lang_helpers::python::{python_is_bare_string, python_is_bare_string_parenthesis};
7
8/// The role of a `{`, `:` or `=`, which Python spells both as syntax
9/// and inside an f-string's replacement field.
10///
11/// As syntax each is an operator (#1486). The interpolation opener
12/// (`f"{x}"`, and a nested `{w}` inside a format spec) is spelling, not
13/// an operation — the six interpolating languages settled that in #1314
14/// and Python follows them. The `:` that opens a format spec
15/// (`f"{x:>10}"`) introduces the format mini-language, whose text is
16/// string content and counts as nothing, so its delimiter does not
17/// count either. The self-documenting `=` (`f"{x=}"`) is a directive of
18/// the same field: it asks for the expression's text to be printed and
19/// assigns nothing, like the `!r` conversion beside it, which was
20/// already unbilled (#1551).
21fn replacement_field_role(kind: Python, parent: Option<Python>) -> TokenRole {
22 use Python::*;
23
24 match (kind, parent) {
25 (LBRACE | EQ, Some(Interpolation | FormatExpression)) | (COLON, Some(FormatSpecifier)) => {
26 TokenRole::Unknown
27 }
28 _ => TokenRole::Operator,
29 }
30}
31
32impl Getter for PythonCode {
33 fn get_space_kind(node: &Node) -> SpaceKind {
34 match node.kind_id().into() {
35 Python::FunctionDefinition => SpaceKind::Function,
36 Python::ClassDefinition => SpaceKind::Class,
37 Python::Module => SpaceKind::Unit,
38 _ => SpaceKind::Unknown,
39 }
40 }
41
42 fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
43 use Python::*;
44
45 let kind: Python = node.kind_id().into();
46 // Bracket openers, `;`, `:` and `=` are vocabulary like every
47 // other getter's (#1395, #1486); only the f-string
48 // replacement-field spellings are withdrawn. An early return
49 // rather than an arm, because the match below is one rustfmt
50 // declines to format.
51 if matches!(kind, LBRACE | COLON | EQ) {
52 let parent = ancestors.parent(node).map(|p| p.kind_id().into());
53 return replacement_field_role(kind, parent);
54 }
55 // The parentheses around a bare string are discarded with it,
56 // as CPython drops them (#1610).
57 if kind == LPAREN && python_is_bare_string_parenthesis(node, ancestors) {
58 return TokenRole::Unknown;
59 }
60
61 match kind {
62 // The `not` / `in` / `is` leaf tokens are operators on their own
63 // (`not x`, `a in b`, `a is b`, `for x in y`), but the grammar
64 // also nests them inside the compound `not in` (Notin) and
65 // `is not` (Isnot) nodes. When the leaf's parent is one of those
66 // compounds, the compound itself is classified as the single
67 // operator below, so the leaf must yield Unknown — otherwise
68 // `a not in b` would count `not` + `in` as two operators (#413).
69 Not | In | Is => match ancestors.parent(node).map(|p| p.kind_id().into()) {
70 Some(Notin | Isnot) => TokenRole::Unknown,
71 _ => TokenRole::Operator,
72 },
73 Import | DOT | From | COMMA | As | STAR | GTGT | Assert | COLONEQ | Return | Def
74 | Del | Raise | Pass | Break | Continue | If | Elif | Else | Async | For
75 | While | Try | Except | Finally | With | DASHGT | Global | Nonlocal | Exec
76 | AT | And | Or | PLUS | DASH | SLASH | PERCENT | SLASHSLASH | STARSTAR | PIPE
77 | AMP | CARET | LTLT | TILDE | LT | LTEQ | EQEQ | BANGEQ | GTEQ | GT | LTGT
78 | PLUSEQ | DASHEQ | STAREQ | SLASHEQ | ATEQ | SLASHSLASHEQ | PERCENTEQ | STARSTAREQ
79 | GTGTEQ | LTLTEQ | AMPEQ | CARETEQ | PIPEEQ | Yield | Print | LPAREN | LBRACK
80 | SEMI
81 // `not in` / `is not` compounds count as one operator each; the
82 // inner Not/In/Is leaves are suppressed by the parent-guard arm
83 // above (#413).
84 | Notin | Isnot
85 // `match` / `case` keyword tokens (Match=26, Case=27), mirroring
86 // the cyclomatic metric which already counts each `case` clause
87 // and the Rust Halstead which counts `match` (#413).
88 | Match | Case
89 // `nonlocal` keyword token, for parity with `global` which was
90 // already classified (#413).
91 // `await`: count only the await-expression node (Await=237);
92 // Await2 (keyword token 95) is the nested keyword and was being
93 // double-counted, mirroring how `yield` counts only the Yield
94 // node and not its keyword leaf (#413).
95 | Await
96 // `lambda`: count only the keyword token (Lambda3=73), not the
97 // Lambda/Lambda2 expression nodes that wrap it, to avoid the same
98 // node+keyword double count fixed for await (#413).
99 | Lambda3
100 // `class`, the declaration keyword beside `def`. Only the leaf:
101 // its `ClassDefinition` wrapper is not listed, so the pair
102 // cannot double-count (#1550).
103 | Class => {
104 TokenRole::Operator
105 }
106 Identifier | Integer | Float | True | False | None => TokenRole::Operand,
107 String => {
108 // A bare string statement's string — its sole value, or a
109 // fragment of an implicitly concatenated one — is discarded,
110 // so it is not an operand (#695). The LOC walk bills the same
111 // strings as comment rows through this predicate, so the two
112 // agree on which strings are discarded; whether one is also
113 // documentation is `python_documentation_string_run`'s
114 // question, which Halstead deliberately does not ask (#1597).
115 if python_is_bare_string(node, ancestors) {
116 return TokenRole::Unknown;
117 }
118 // Regression #191: an f-string wraps `Interpolation` children
119 // whose inner expressions are walked and counted separately.
120 // Skip the wrapping literal to avoid double-counting (same
121 // pattern as #180 for Bash/Elixir and #184 for PHP).
122 Self::string_operand_type(node, &[Interpolation as u16])
123 }
124 _ => TokenRole::Unknown,
125 }
126 }
127
128 get_operator!(Python);
129}