Repository navigation
feat(mi): stop charging documentation rows in MI - #1609
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1609 +/- ##
==========================================
+ Coverage 98.20% 98.22% +0.02%
==========================================
Files 365 367 +2
Lines 101068 104922 +3854
Branches 100637 104487 +3850
==========================================
+ Hits 99250 103064 +3814
- Misses 1173 1197 +24
- Partials 645 661 +16
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
This was referenced Oct 17, 2026
dekobon
force-pushed
the
fix/1597-loc-docstring
branch
from
October 17, 2026 19:38
c9e449e to
5126ec4
Compare
dekobon
added a commit
that referenced
this pull request
Oct 17, 2026
Codecov listed 68 uncovered patch lines on PR #1609. The production ones a valid input can reach now have tests, each confirmed by a perturbation that fails it: - fold_down agrees across the memoised cache, a known chain with no cache, and the chainless climb, on the state and on the parent each step's `outer` chain names. ancestors.rs goes from 23 missed lines to 2: an assertion-message argument and an older test's untaken match arm. - A typed `self` in __init__, a `#:` on the last line, a Ruby `foo.()` call, a Go directive whose value starts with a digit, and the frozen-string Emacs modeline. - mi_mode_warning with no run mode, against both baseline modes. What stays uncovered is test-message formatting and `?` on lookups the grammar guarantees, which codecov.yml documents as uncoverable. Refs #1597
Every other per-node metric receives the source; Loc::compute did not, so no LOC rule could read a comment's or a string's bytes. Pass `code` through the trait, the walker's call site and the no-op macro arm. The 23 impls ignore it for now, and no metric moves. Refs #1597
A Python docstring has counted as comment rows since #415, so cloc could not be read as "comments" the way a C or Rust reader expects, and nothing said how many rows were documentation. Add loc.dloc with dloc_average, dloc_min and dloc_max: the rows a language treats as documentation, a subset of cloc kept true by one writer. Python follows PEP 257 and Sphinx: the first statement of a module, class or function, successive strings after a docstring, an attribute docstring after an assignment to a name or a `type` alias at module or class level or to `self.<name>` at the top of `__init__`, and `#:` comments on or above such an assignment. Only a str literal counts; any other bare string stays a comment row. LOC and Halstead share one bare-string predicate, which fixes three shapes: an implicitly concatenated docstring, a bare tuple of strings, and an escaped string whose row was also billed as code. The field reaches every output format, the text tree, the CSV row (as trailing columns), list-metrics, the loc.dloc threshold, SARIF and the Python LocDict. Every other language reports 0. Refs #1597
MI charged documentation as code: a function with a docstring scored lower than the same function without one. Size every mi.* variant on sloc less the documentation rows that carry no code, and form mi.sei's comment percentage as cloc − dloc over that size. A docstring sharing its row with code (`def f(): """doc"""`) keeps the row. Working comments and blank rows are still charged; #1607 tracks the rest. The book's claim that MI is sized on PLOC was wrong; it has always used SLOC, and now says so. Refs #1597
Count a comment as dloc when the language's own documentation tool would: the grammar's mark where there is one (Rust's doc_comment, Groovydoc, Perl POD but not comment regions), and otherwise the opening marker as Doxygen, javac (with JEP 467 `///`), Kotlin, JSDoc, tsc, PHP's scanner and Roslyn read it, edges included (`/***`, `////`, `/**/`). Placement follows each tool too. A doc comment in a function body is an ordinary comment where rustc, Roslyn, javadoc, Dokka and PHPDoc ignore it; C, C++ and Objective-C keep it as Doxygen's in-body description, the JS family as JSDoc inner symbols, and Perl POD anywhere. Signature placements and Rust's inner doc opening a body count. Checker gains is_doc_comment, DOC_COMMENTS_IN_FUNCTION_BODIES and holds_doc_comment_in_body, and every comment arm routes through add_comment_lines. The in-body test folds down the ancestry once per walk through Ancestors::fold_down and a FoldCache the walker lends, so it stays linear in nesting depth (loc/rust-nested-doc-mods). Corpus snapshots move dloc and mi.* only. Refs #1597
Go, Ruby and Lua have no doc marker, so a comment is documentation by where it sits, decided once per run of consecutive comment rows at its last member: - Go: go/doc's lead comment above a declaration, spec, field or interface method outside function bodies; directives are dropped. - Ruby: YARD's block above a def, class, module, constant, alias, attr call or visibility-wrapped def, in a namespace body, plus RDoc `##` and YARD `@!` directive blocks; shebang and magic lines drop. - Lua: an LDoc block that has text and is the file's first, carries a tag, or sits above a function, assignment, named field or a return of a function or table. The Ruby then/do wrappers no longer credit their start row to PLOC, which had turned a parked comment row into code. Scope tests fold once per walk; probes pin each run walk as linear. Refs #1597
Make the documentation-inclusive formula one setting away: MetricsOptions::with_mi_charges_docs, the --mi-charges-docs flag, the mi_charges_docs manifest key, a Python keyword and a web embedder option. It moves nothing but mi.*. --write-baseline stamps the mode into [provenance], and bca check warns when an mi.* offender meets an mi.* entry written under the other mode. A baseline with no stamp reads as charging documentation, the only mode an earlier release had, so the upgrade warns. Refs #1597
`bca metrics --metrics loc`, the flag's own help example, was rejected: the selector reused `bca diff --metric`'s bucket vocabulary, where the loc family appears only as its sub-metrics. A selection names what to compute, so a family name now selects the whole family. diff keeps its validator.
Lesson 98: an emulated tool's specification is its source, not its manual or a summary of it (Doxygen in-body descriptions, tsc's isJSDocLikeText, LDoc's parse.lua, YARD's processor). Lesson 81 gains the predicate helpers that recursed once per nesting level. Refs #1597
AGENTS.md: regenerate .bca-baseline.toml after each commit of a split change, since the refresh measures the working tree. A new history-rewrite rule: a scripted rebase that names a superseded hash replays the old commit silently, so compare trees after any rewrite. Refs #1597
dekobon
force-pushed
the
fix/1597-loc-docstring
branch
from
October 17, 2026 22:03
3338c96 to
e1e3256
Compare
dekobon
added a commit
that referenced
this pull request
Oct 19, 2026
C, C++, Mozilla C++ and Objective-C counted a Doxygen block as dloc wherever it sat outside a function body, so a block above the end of a file, a closing brace or a forward declaration inflated dloc and the MI comment share. Each now counts what Doxygen 1.13.2 documents, read from commentscan.l and confirmed by running it with EXTRACT_ALL=NO over ~200 fixtures: - Doxygen keeps every doc block before an entry and concatenates them: a declaration, definition, macro, typedef, alias, namespace, template, enumerator, macro call, or Objective-C container, property or method. It reads past plain comments, #include, #import, #pragma, an empty `;` and an empty conditional, and into a conditional that declares something. - A block above a forward declaration, `}`, `@end`, `@synthesize`, an explicit instantiation or the end of the file documents nothing. - A structural block (\file, \defgroup, \page, \name, or a naming command with a name: \class X, \def M) stands alone and counts wherever it sits; the doc blocks above a \file block count with it. - Member-after /**< blocks and in-body blocks keep counting (#1609). A new DocBinding::gathers models the \file case for a stretch that ends at no target. The rule lives in lang_helpers/doxygen.rs and one macro the four Checker impls share. Probes loc/c-dangling-docs and loc/cpp-structural-docs hold it at O(k). No committed corpus snapshot moves: every C++ doc block in DeepSpeech native_client is attached. Submodule unchanged at 4d7a7b65. Fixes #1613
dekobon
added a commit
that referenced
this pull request
Oct 20, 2026
C, C++, Mozilla C++ and Objective-C counted a Doxygen block as dloc wherever it sat outside a function body, so a block above the end of a file, a closing brace or a forward declaration inflated dloc and the MI comment share. Each now counts what Doxygen 1.13.2 documents, read from commentscan.l and confirmed by running it with EXTRACT_ALL=NO over ~200 fixtures: - Doxygen keeps every doc block before an entry and concatenates them: a declaration, definition, macro, typedef, alias, namespace, template, enumerator, macro call, or Objective-C container, property or method. It reads past plain comments, #include, #import, #pragma, an empty `;` and an empty conditional, and into a conditional that declares something. - A block above a forward declaration, `}`, `@end`, `@synthesize`, an explicit instantiation or the end of the file documents nothing. - A structural block (\file, \defgroup, \page, \name, or a naming command with a name: \class X, \def M) stands alone and counts wherever it sits; the doc blocks above a \file block count with it. - Member-after /**< blocks and in-body blocks keep counting (#1609). A new DocBinding::gathers models the \file case for a stretch that ends at no target. The rule lives in lang_helpers/doxygen.rs and one macro the four Checker impls share. Probes loc/c-dangling-docs and loc/cpp-structural-docs hold it at O(k). Two rules the other stretch bindings share change with it. A stretch reads past a parse error that is no doc target and holds none, as Doxygen's binding already does, so the error tree-sitter-rust leaves for a Rust 2024 `safe fn` no longer drops its doc comment; an error holding a declaration, such as a Kotlin class-body context(...), stands in for it. And KeptDocs::Leading asks whether a doc comment after code on its row still documents: tsc's leading-comment scan starts at the line break after the token before, so in TS and TSX one does only before a parameter, while Roslyn, JSDoc and PHP-Parser read it. No committed corpus snapshot moves: every C++ doc block in DeepSpeech native_client is attached. Submodule unchanged at 9076c554. Fixes #1613
dekobon
added a commit
that referenced
this pull request
Oct 20, 2026
C, C++, Mozilla C++ and Objective-C counted a Doxygen block as dloc wherever it sat outside a function body, so a block above the end of a file, a closing brace or a forward declaration inflated dloc and the MI comment share. Each now counts what Doxygen 1.13.2 documents, read from commentscan.l and confirmed by running it with EXTRACT_ALL=NO over ~200 fixtures: - Doxygen keeps every doc block before an entry and concatenates them: a declaration, definition, macro, typedef, alias, namespace, template, enumerator, macro call, or Objective-C container, property or method. It reads past plain comments, #include, #import, #pragma, an empty `;` and an empty conditional, and into a conditional that declares something. - A block above a forward declaration, `}`, `@end`, `@synthesize`, an explicit instantiation or the end of the file documents nothing. - A structural block (\file, \defgroup, \page, \name, or a naming command with a name: \class X, \def M) stands alone and counts wherever it sits; the doc blocks above a \file block count with it. - Member-after /**< blocks and in-body blocks keep counting (#1609). A new DocBinding::gathers models the \file case for a stretch that ends at no target. The rule lives in lang_helpers/doxygen.rs and one macro the four Checker impls share. Probes loc/c-dangling-docs and loc/cpp-structural-docs hold it at O(k). Two rules the other stretch bindings share change with it. A stretch reads past a parse error that is no doc target and holds none, as Doxygen's binding already does, so the error tree-sitter-rust leaves for a Rust 2024 `safe fn` no longer drops its doc comment; an error holding a declaration, such as a Kotlin class-body context(...), stands in for it. And KeptDocs::Leading asks whether a doc comment after code on its row still documents: tsc's leading-comment scan starts at the line break after the token before, so in TS and TSX one does only before a parameter, while Roslyn, JSDoc and PHP-Parser read it. No committed corpus snapshot moves: every C++ doc block in DeepSpeech native_client is attached. Submodule unchanged at 9076c554. Fixes #1613
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1597.
The maintainability index charged documentation as code. A function with a doc comment scored lower than the same function without one. This PR measures documentation rows as a new
loc.dlocmetric and drops them from MI's size term and from both sides of its comment ratio. The previous formula is still available behindmi_charges_docs.What changes
loc.dloc(Documentation Lines Of Code), withdloc_average,dloc_minanddloc_max. It is a subset ofcloc, kept true by a single writer. Each language follows the tool that reads its documentation:///,//!), Javadoc (incl. JEP 467///), KDoc, JSDoc, tsc (which takes/***), PHPDoc, C# XML docs, Doxygen for C/C++/ObjC, Groovydoc and Perl POD.#:comments, including ontypealiases.@docstays code (fix(metrics/loc): multi-line strings and heredocs read as blank in Bash, Elixir, Tcl, iRules #1260).code_lines = sloc − dloc_without_codeandcomment_lines = cloc − dloc. A row that documentation shares with code (def f(): """doc""") stays in the size term.mi_charges_docs: restores the previous formula. It is available as aMetricsOptionsbuilder, a--mi-charges-docsflag, abca.tomlkey, a web option and a Python keyword.--write-baselinestamps the mode into the baseline, andbca checkwarns when the run's mode and the baseline's disagree onmi.*entries.dlocappears in every output format, the CSV columns (appended at the end of the row),list-metricsandbca check --threshold loc.dloc. The new fields are SemVer-additive, with#[serde(default)].bca metrics --metrics loc, the flag's own help example, used to be rejected. A family name now selects the whole family.bca diff --metric locstays rejected, because a diff filters buckets.Snapshots
docstring→dlocrename moved all 1,611 corpus snapshots.dlocandmi.*.dekobon/big-code-analysis-output@22e5a762, which is on itsmain.Validation
make pre-commitpasses at the tip (BCA_GATE: pass).make bench-scaling: all 58 probes stay within their complexity bound. Seven probes are new:loc/go-comment-runloc/rust-nested-doc-modsloc/go-nested-doc-fieldsloc/ruby-nested-doc-constantsloc/lua-tagless-blocksloc/rust-inner-doc-runandloc/python-additional-docstrings.--mi-charges-docsreproducesmain's MI on every space.Upgrading with an existing baseline
A baseline written by a released
bcahas nomi_charges_docskey. It reads astrue, because every earlier release charged documentation. The firstbca checkafter upgrading therefore warns, but only when anmi.*offender meets anmi.*entry. The warning says the baseline "records no mode, so it predates the stamp", and names both remedies:--write-baseline;--mi-charges-docsto keep the old formula.I verified this end to end with a baseline written by the
mainbinary.Known gaps and follow-ups
__END__./** */licence abovepackageand a///above a Rust macro call. A fix moves DeepSpeech corpus snapshots.main:mi.originalormi.seicannot be baselined.metrics=accepts only family names, while the CLI's--metricsalso takesdloc,slocand dotted ids.Merging
The history is nine signed commits, one concern each, every one passing
make pre-commiton its own, ready for a fast-forward merge:refactor(loc): handLoc::computethe source bytes.feat(loc): report Python docstrings asloc.dloc.feat(mi): leave documentation rows out of MI.feat(loc): count doc comments by their marker. This commit movesthe corpus snapshots.
feat(loc): read Go, Ruby and Lua docs by position.feat(mi): addmi_charges_docsto charge docs.fix(cli): accept a family name in--metrics.docs(lessons): add lesson 98 and extend lesson 81.docs: add per-commit baseline and rewrite rules.