Repository navigation
fix(loc): count Perl POD placed after __END__ #1608
Description
Describe the bug
Perl POD written after __END__ or __DATA__ — the layout h2xs generates, used by about a third of sampled system .pm files — is not counted as comment or documentation lines. The grammar parses everything after the marker as one opaque data_not_for_compiler node, which the Perl LOC walk does not bill, so the POD's text rows fall through to blank.
Reproducer (bca metrics --no-config --paths p.pm -O json, file level, on the fix/1597-loc-docstring branch):
package P;
sub f { 1 }
1;
__END__
=head1 NAME
P - thing
=cut| sloc | ploc | cloc | dloc | blank |
|---|---|---|---|---|
| 10 | 4 | 0 | 0 | 6 |
Three of the six "blank" rows carry text. The same POD placed before __END__ is cloc and, since #1597, dloc, so the maintainability index charges post-__END__ POD while exempting the identical inline POD. The cloc and blank half of this already exists on main; #1597 added the dloc half. Text/ParseWords.pm is an example: 127 POD rows after __END__, reported as dloc 0 and blank 171.
Describe the solution you'd like
Scan the data_not_for_compiler node's bytes for POD paragraphs (a line starting =word through the next =cut or the end of the file, as perlpod defines them) and bill those rows as comment and documentation rows, applying the same =begin comment / =for comment exclusion the inline POD arm uses.
That leaves one decision this issue should settle first: what the non-POD rows of a __DATA__ section are. They are not code, not comments, and not blank. Today they read as blank, which is wrong for blank, but PLOC would be wrong too. Options: leave them uncounted (they then stay blank, as now), or count them as PLOC, the conservative choice for MI.
Additional context
src/metrics/loc/perl.rsroutesP::Comments | P::PodStatementto the comment helpers;data_not_for_compilerhas no arm..claude/rules/grammar-dispatch.mdrule 6 applies: dump an__END__file and a__DATA__file withbca dumpbefore deciding which node keeps the classification.- Found by the max-effort review of feat(mi): stop charging documentation in the maintainability index #1597.
Resolution
Commit: 34fd788a on fix/batch-2026-10-17 (fix(loc): count Perl POD placed after __END__). No corpus file holds __END__/__DATA__, so no snapshot moved.
Decision applied (maintainer): non-POD, non-blank data-section rows are ploc; blank rows stay blank; the marker row stays code; POD rows are cloc + dloc (cloc only for a block that is nothing but =begin comment / =for comment regions).
Shape (bca dump): special_literal > { __END__ token, data_not_for_compiler } — one opaque leaf from the marker row to EOF. A new public byte scanner lang_helpers::perl::perl_data_section_rows classifies it in one linear pass; src/metrics/loc/perl.rs gains a DataNotForCompiler arm that credits POD rows through the shared add_comment_rows path (so dloc ≤ cloc by construction) and data rows as ploc. The block rule follows Pod::Simple::BlackBox.pm (what perldoc runs, lesson 98): opens on ^=[a-zA-Z][a-zA-Z0-9]*(\s|$) — so =1 and =head-x open nothing — closes on ^=cut (=cut foo, =cutting included); a stray =cut opens nothing; an unterminated block ends at its last non-blank row.
Before → after [sloc, ploc, cloc, dloc, blank]: the issue's __END__ + POD 10,4,0,0,6 → 10,4,5,5,1; __DATA__ + data + POD 11,3,0,0,8 → 11,6,4,4,1; system Text/ParseWords.pm 303,122,15,0,171 → 303,122,141,126,45.
Review findings folded: the marker row's own text was a candidate ^= line, so __END__=head1 A opened a POD block — fixed (the scanner consumes the marker row first); =cut foo / =cutting and an unterminated =begin comment cut by =cut were untested — three cases added (13 total, 14 mutations all caught). A rustfmt-bail in loc/perl.rs (comment inside a match pattern) was hoisted per formatting.md, clearing its 6 baselined bails.
Also found, filed, not fixed: #1644 — Halstead counts the whole data section as one operand (__END__ adds 1 unique / 1 total).
Probe: loc/perl-data-pod exponent 1.00 (bound 1.50); all 63 probes pass. Baseline: PerlCode::compute halstead.effort 59.3k → 62.5k (one new arm); perl_data_section_rows enters at 49.5k (hard 50k) — one linear loop with a five-arm match, baselined rather than split.
Gate: BCA_GATE: pass (gate=pre-commit).
Activity
dekobon commentedon Oct 19, 2026
Resolution
Commit:
34fd788aonfix/batch-2026-10-17(fix(loc): count Perl POD placed after __END__). No corpus file holds__END__/__DATA__, so no snapshot moved.Decision applied (maintainer): non-POD, non-blank data-section rows are
ploc; blank rows stayblank; the marker row stays code; POD rows arecloc+dloc(cloc only for a block that is nothing but=begin comment/=for commentregions).Shape (
bca dump):special_literal > { __END__ token, data_not_for_compiler }— one opaque leaf from the marker row to EOF. A new public byte scannerlang_helpers::perl::perl_data_section_rowsclassifies it in one linear pass;src/metrics/loc/perl.rsgains aDataNotForCompilerarm that credits POD rows through the sharedadd_comment_rowspath (sodloc ≤ clocby construction) and data rows as ploc. The block rule followsPod::Simple::BlackBox.pm(whatperldocruns, lesson 98): opens on^=[a-zA-Z][a-zA-Z0-9]*(\s|$)— so=1and=head-xopen nothing — closes on^=cut(=cut foo,=cuttingincluded); a stray=cutopens nothing; an unterminated block ends at its last non-blank row.Before → after
[sloc, ploc, cloc, dloc, blank]: the issue's__END__+ POD 10,4,0,0,6 → 10,4,5,5,1;__DATA__+ data + POD 11,3,0,0,8 → 11,6,4,4,1; systemText/ParseWords.pm303,122,15,0,171 → 303,122,141,126,45.Review findings folded: the marker row's own text was a candidate
^=line, so__END__=head1 Aopened a POD block — fixed (the scanner consumes the marker row first);=cut foo/=cuttingand an unterminated=begin commentcut by=cutwere untested — three cases added (13 total, 14 mutations all caught). A rustfmt-bail inloc/perl.rs(comment inside a match pattern) was hoisted performatting.md, clearing its 6 baselined bails.Also found, filed, not fixed: #1644 — Halstead counts the whole data section as one operand (
__END__adds 1 unique / 1 total).Probe:
loc/perl-data-podexponent 1.00 (bound 1.50); all 63 probes pass. Baseline:PerlCode::computehalstead.effort59.3k → 62.5k (one new arm);perl_data_section_rowsenters at 49.5k (hard 50k) — one linear loop with a five-arm match, baselined rather than split.Gate:
BCA_GATE: pass (gate=pre-commit).fix(loc): count Perl POD placed after __END__
docs(changelog): record the LOC and doc-comment fixes
fix(loc): count Perl POD placed after __END__
docs(changelog): record the LOC and doc-comment fixes
fix(loc): count Perl POD placed after __END__
docs(changelog): record the LOC and doc-comment fixes
dekobon commentedon Oct 20, 2026
Pull request: #1650.