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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
//! `Loc` implementation for Perl.
#![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::perl::{PerlDataSectionRows, perl_data_section_rows};
/// Bills the rows after `__END__` / `__DATA__`, which the grammar holds
/// as one leaf (#1608): a POD block's rows as comment rows, and as
/// documentation unless the block is only `comment` regions, like POD
/// before the marker; every other non-blank row as code. The text is
/// neither, but MI then charges it rather than exempting it.
fn add_data_section_lines(section: &Node, code: &[u8], stats: &mut Stats) {
let bytes = code
.get(section.start_byte()..section.end_byte())
.unwrap_or_default();
perl_data_section_rows(bytes, section.start_row(), |rows| match rows {
PerlDataSectionRows::Pod {
first,
last,
documentation,
} => add_comment_rows(stats, first, last, documentation),
PerlDataSectionRows::Data(row) => {
stats.ploc.lines.insert(row);
}
});
}
impl Loc for PerlCode {
fn compute<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
is_func_space: bool,
) {
use Perl as P;
let start = init(node, stats, is_func_space);
match node.kind_id().into() {
P::SourceFile | P::Block | P::StandaloneBlock | P::PodContent => {}
// Internal string tokens — already accounted for by the
// parent string node's start row.
P::SQUOTE
| P::DQUOTE
| P::StringContent
| P::StringSingleQuotedContent
| P::StringSingleQQuotedContent
| P::StringQqQuotedContent
| P::StringDoubleQuotedContent
| P::EscapeSequence
| P::EscapeSequenceToken1
| P::Interpolation
| P::HASH => {}
// Multi-line-capable string literals: their interior rows are
// real code, not blank lines, so credit every spanned row to
// PLOC to match Python's #415 decision (#778). `HeredocBodyStatement`
// / `HeredocContent` are the body of a `<<EOT … EOT` heredoc; the
// quoted forms span rows when their literal text contains newlines.
P::HeredocBodyStatement
| P::HeredocContent
| P::StringSingleQuoted
| P::StringDoubleQuoted
| P::StringQQuoted
| P::StringQqQuoted
| P::BacktickQuoted
| P::CommandQxQuoted => {
add_multiline_string_ploc(node, ancestors, stats, start);
}
P::Comments | P::PodStatement => {
add_comment_lines::<Self>(node, code, ancestors, stats);
}
// The marker's own row is billed by the `__END__` / `__DATA__`
// token through the catch-all.
P::DataNotForCompiler => add_data_section_lines(node, code, stats),
P::SingleLineStatement
| P::IfStatement
| P::UnlessStatement
| P::WhileStatement
| P::UntilStatement
| P::ForStatement1
| P::ForStatement2
| P::LoopControlStatement
| P::PackageStatement
| P::RequireStatement
| P::UseNoStatement
| P::UseNoFeatureStatement
| P::UseNoIfStatement
| P::UseNoSubsStatement
| P::UseConstantStatement
| P::UseParentStatement
| P::UseNoVersion
| P::EllipsisStatement => {
stats.lloc.count_logical_line();
}
P::SEMI => {
// A `;` at top of `source_file` / a function `block` ends a
// statement (Perl wraps simple expressions in semicolons
// rather than emitting a dedicated statement kind), so it
// contributes one LLOC. Then fall through to the same PLOC
// bookkeeping the catch-all arm does.
if let Some(parent) = ancestors.parent(node)
&& matches!(parent.kind_id().into(), P::SourceFile | P::Block)
{
stats.lloc.count_logical_line();
}
check_comment_ends_on_code_line(stats, start);
stats.ploc.lines.insert(start);
}
_ => {
check_comment_ends_on_code_line(stats, start);
stats.ploc.lines.insert(start);
}
}
}
}
#[cfg(all(test, feature = "perl"))]
mod tests {
use crate::LANG;
use crate::Metric;
use crate::MetricsOptions;
use crate::test_support::metrics_verbatim;
/// `[sloc, ploc, cloc, dloc, blank]` of a whole Perl file.
fn loc_of(source: &str) -> [u64; 5] {
let options = MetricsOptions::default().with_only(&[Metric::Loc]);
let loc = metrics_verbatim(LANG::Perl, source.as_bytes(), options).loc;
[loc.sloc(), loc.ploc(), loc.cloc(), loc.dloc(), loc.blank()]
}
/// The grammar holds everything after `__END__` / `__DATA__` as one
/// leaf (#1608). A POD block there is billed as POD before the marker
/// is: every row from its command to its `=cut` is a comment row, and
/// a documentation row unless the block is only `comment` regions.
/// Any other non-blank row is code, other blank rows stay blank, and
/// the marker's row is code.
#[test]
fn perl_data_section_rows() {
// (source, [sloc, ploc, cloc, dloc, blank])
let cases: &[(&str, [u64; 5])] = &[
// ploc: rows 1-4; POD: rows 6-10; blank: row 5.
(
"package P;\nsub f { 1 }\n1;\n__END__\n\n=head1 NAME\n\nP - thing\n\n=cut\n",
[10, 4, 5, 5, 1],
),
// ploc: rows 1-3; POD: rows 5-9; blank: row 4.
(
"package P;\n1;\n__DATA__\n\n=pod\n\nText.\n\n=cut\n",
[9, 3, 5, 5, 1],
),
// Two blocks of `comment` regions, rows 3-9 and 11-14: comment
// rows, not documentation. Row 10 is blank between them.
(
"1;\n__END__\n=begin comment\n\nold();\n\n=end comment\n\n=cut\n\n=for comment\nx\n\n=cut\n",
[14, 2, 11, 0, 1],
),
// Each block is judged on its own text: a `comment` block
// (rows 3-6) then a documenting one (rows 8-11, dloc).
(
"1;\n__END__\n=for comment\nx\n\n=cut\n\n=head1 A\n\na\n=cut\n",
[11, 2, 8, 4, 1],
),
// No POD: rows 1-3 and 5-6 are code, row 4 blank.
("1;\n__DATA__\na b\n\nc d\ne\n", [6, 5, 0, 0, 1]),
// An unterminated block runs to its last text row, 6; row 7
// after it is blank, as after an unterminated inline block.
("1;\n__END__\nd\n=head1 NAME\n\nP\n\n", [7, 3, 3, 3, 1]),
// POD (rows 3-6), data (7), blank (8), POD (9-11).
(
"1;\n__END__\n=head1 A\n\na\n=cut\nrow\n\n=head1 B\nb\n=cut\n",
[11, 3, 7, 7, 1],
),
// `=1` and `=head-x` open nothing, and `=cut` outside a block
// is data.
("1;\n__DATA__\n=1\nx\n=head-x\n=cut\n", [6, 6, 0, 0, 0]),
// The section starts mid-row, after the marker: text there is
// not a line start, so it opens nothing and leaves the later
// `=cut` stray. Every row is code.
("1;\n__END__=head1 A\nx\n=cut\n", [4, 4, 0, 0, 0]),
// Any line starting `=cut` closes, trailing text and all: POD
// rows 3-5 and 7-8, data rows 6 and 9.
(
"1;\n__END__\n=head1 A\nx\n=cut foo\ny\n=pod\n=cutting\nz\n",
[9, 4, 5, 5, 0],
),
// `=cut` ends the block while its `=begin comment` is still
// open, so rows 3-5 are a comment block; the `=end comment`
// after it opens a new, documenting block, rows 6-7. Judged on
// text past its `=cut`, the first block would document too.
(
"1;\n__END__\n=begin comment\nx\n=cut\n=end comment\nDoc\n",
[7, 2, 5, 2, 0],
),
// CRLF line ends: `=head1\r` opens, `=cut\r` closes.
(
"1;\r\n__END__\r\n=head1\r\nx\r\n=cut\r\ny\r\n",
[6, 3, 3, 3, 0],
),
// Outside a block `=cutting` is a command that opens one, and
// the `^=cut` test closes it on the same row: POD row 3, data
// rows 4-5.
("1;\n__END__\n=cutting\ndata1\ndata2\n", [5, 4, 1, 1, 0]),
// Perl's `\s` after the command takes a vertical tab, so
// `=head1<VT>NAME` opens a block that runs to the end of the
// file: POD rows 3-4.
("1;\n__END__\n=head1\x0bNAME\nx\n", [4, 2, 2, 2, 0]),
];
for (source, expected) in cases {
assert_eq!(loc_of(source), *expected, "{source}");
}
}
}