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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! `Loc` implementation for Python.
#![allow(
clippy::enum_glob_use,
clippy::match_same_arms,
clippy::struct_field_names,
clippy::wildcard_imports
)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use super::*;
use crate::lang_helpers::python::{
python_documentation_string_run, python_is_bare_statement_value, python_is_bare_string,
python_is_bare_string_parenthesis, python_sphinx_doc_comment,
};
impl Loc for PythonCode {
fn compute<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
is_func_space: bool,
) {
// bca: suppress(cyclomatic, halstead) — per-kind LOC dispatch for Python, one arm per node kind
use Python::*;
let start = init(node, stats, is_func_space);
match node.kind_id().into() {
// `block` (both aliased kind_ids — `Block` 135, the dead
// hidden supertype, and `Block2` 160, the only one emitted
// today) is a structural wrapper that contributes no LOC of
// its own: its opening row is always shared with the first
// statement it wraps, which inserts that row via its own
// arm. Route through `python_is_block` so both aliases
// no-op identically at one normalization point — previously
// `Block2` fell through to the `_` arm and re-inserted an
// already-covered row (a HashSet duplicate); making the
// no-op explicit is behaviour-preserving (issue #419).
_ if python_is_block(node) => {}
// `escape_sequence` and `escape_interpolation` are leaves inside
// a string's content that spell no code of their own; their row
// is billed by the string's arm. Left to the catch-all they
// credited a docstring row to PLOC (#1597).
StringStart | StringEnd | StringContent | EscapeSequence | EscapeInterpolation
| Module => {}
Comment => {
add_comment_lines::<Self>(node, code, ancestors, stats);
add_sphinx_doc_comment(node, code, ancestors, stats);
}
// An implicitly concatenated bare string (`"""doc""" "more"`)
// is one string to CPython and to Halstead (#695): bill the
// whole literal here, once; its fragments are skipped below. A
// concatenated value falls to the catch-all.
ConcatenatedString if python_is_bare_statement_value(node, ancestors) => {
add_bare_string_lines(node, code, ancestors, stats);
}
// CPython drops the parentheses around a bare string, so they
// are part of it (#1610): their rows are its comment rows, and
// documentation rows when it is, which the string's arm credits.
// The `parenthesized_expression` itself bills nothing.
LPAREN | RPAREN if python_is_bare_string_parenthesis(node, ancestors) => {
add_comment_rows(stats, node.start_row(), node.end_row(), false);
}
ParenthesizedExpression if python_is_bare_string_parenthesis(node, ancestors) => {}
String => {
if python_is_bare_string(node, ancestors) {
// A bare string statement is counted as comment lines
// (see issue #415 for the related classification caveat,
// intentionally left unchanged), and as documentation
// too when it sits where Python reads documentation. A
// fragment of a concatenated string was billed by the
// arm above.
if !ancestors.parent_has_kind(node, ConcatenatedString as u16) {
add_bare_string_lines(node, code, ancestors, stats);
}
} else if ancestors.parent_has_kind(node, ExpressionStatement as u16) {
// A bare statement's literal that is not a docstring, such
// as an element of the discarded tuple `"""a\nb""", x`.
// The statement's arm credits no row, so the literal
// credits its own opening row instead of trusting
// `add_multiline_string_ploc`'s parent gate (#1597).
check_comment_ends_on_code_line(stats, start);
stats.ploc.lines.insert(start);
add_string_interior_ploc(node, stats, start);
} else {
// A non-docstring string literal (assigned or passed as an
// argument). Its interior physical rows are real code, not
// blank lines (#415). The opening row is inserted only when
// the parent statement begins on an earlier row, otherwise
// that row is already attributed to the enclosing statement.
//
// This arm is the rule `add_multiline_string_ploc` was
// extracted from (#778), and stayed open-coded until #1423
// swept the last copies of the pattern onto the helper. The
// parent gate is unchanged — a `string` always has a parent
// here, so the helper's `is_none_or` reduces to the same
// test — and the interior bound moves from the raw end row
// to `Node::end_line`. That is a no-op in Python today: a
// `string` node always closes on a `string_end` quote, so
// its end column is never 0, and an unterminated literal
// parses to a bare `string_start` under an `ERROR` rather
// than to a `string` node at all.
add_multiline_string_ploc(node, ancestors, stats, start);
}
}
// `match` (PEP 634) and `type` aliases (PEP 695) are real
// statements that parse cleanly and previously fell through
// to the `_` arm, contributing 0 logical lines (#462). Count
// `MatchStatement` like `IfStatement`/`TryStatement` — the
// construct itself adds one lloc; its `case_clause` children
// are deliberately omitted, mirroring how `elif_clause`,
// `else_clause`, `except_clause`, and `finally_clause` are
// absent here (clauses do not add lloc; the statements they
// contain do via their own arms). `TypeAliasStatement` is a
// leaf statement, counted like an assignment expression.
Statement
| SimpleStatements
| ImportStatement
| FutureImportStatement
| ImportFromStatement
| PrintStatement
| AssertStatement
| ReturnStatement
| DeleteStatement
| RaiseStatement
| PassStatement
| BreakStatement
| ContinueStatement
| IfStatement
| ForStatement
| WhileStatement
| TryStatement
| MatchStatement
| TypeAliasStatement
| WithStatement
| GlobalStatement
| NonlocalStatement
| ExecStatement
| ExpressionStatement => {
stats.lloc.count_logical_line();
}
_ => {
check_comment_ends_on_code_line(stats, start);
stats.ploc.lines.insert(start);
}
}
}
}
/// Bills a bare string statement's rows: as comment rows always (#415),
/// and as documentation rows (`dloc`) only in a docstring or attribute
/// docstring position (#1597) — so a triple-quoted string wrapping
/// commented-out code is charged by MI like a `#` comment.
fn add_bare_string_lines<'a>(
value: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) {
add_comment_rows(stats, value.start_row(), value.end_row(), false);
python_documentation_string_run(value, code, ancestors, |first, last| {
stats.dloc.lines.insert_range(first, last);
});
}
/// Credits a Sphinx `#:` doc comment, which documents the assignment
/// beside or below it, as `dloc`. Its rows are already comment rows, so
/// `dloc <= cloc` holds (#1597).
fn add_sphinx_doc_comment<'a>(
comment: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) {
python_sphinx_doc_comment(comment, code, ancestors, |first, last| {
stats.dloc.lines.insert_range(first, last);
});
}