Skip to content

fix(loc): count Perl POD placed after __END__ #1608

Closed

Description

@dekobon

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.rs routes P::Comments | P::PodStatement to the comment helpers; data_not_for_compiler has no arm.
  • .claude/rules/grammar-dispatch.md rule 6 applies: dump an __END__ file and a __DATA__ file with bca dump before 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

  1. dekobon commented on Oct 19, 2026

    @dekobon
    OwnerAuthor

    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).

  2. added 6 commits that reference this issue on Oct 19, 2026
    c957d73
    4642446
    d394a2b
    fa00db6
    396c314
    97474b5
  3. dekobon commented on Oct 20, 2026

    @dekobon
    OwnerAuthor

    Pull request: #1650.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions