Skip to main content

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