repowise-dev / repowise-dev/repowise
[Bug] A file with zero coverable lines (LF:0) is ingested as 0% coverage, so coverage_gradient imputes what it documents it never will
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 6.7k
- Forks
- 711
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 439
Description
Describe the Bug
A file with zero coverable lines (LF:0) is ingested as line_coverage_pct = 0.0 instead of "not applicable". Downstream that is indistinguishable from a file that has coverable lines of which none were hit, so coverage_gradient and untested_hotspot both fire on a file that contains no executable code at all.
This bypasses the guarantee coverage_gradient documents for itself:
Absent coverage ≠ zero coverage. When no coverage report was ingested (
line_coverage_pct is None) the biomarker is silent - it never imputes uncovered for missing data.
The guard is if cov is None: return []. It holds when no report was ingested, but a 0/0 record hands the detector a hard 0.0, so the biomarker imputes exactly what the docstring promises it never will.
The same function already handles the analogous case correctly one branch over — branch_coverage_pct becomes None when there are no branches, while line_coverage_pct becomes 0.0 when there are no lines:
# core/analysis/health/coverage/lcov.py:46-51
line_pct = (hit / total * 100.0) if total else 0.0 # 0 lines -> "0% covered"
branch_pct: float | None
if has_branches and branches_found:
branch_pct = branches_hit / branches_found * 100.0
else:
branch_pct = None # 0 branches -> "not applicable"
Steps to Reproduce
Minimal lcov with one type-only file (LF:0) and one genuinely uncovered file:
TN:
SF:src/types.ts
FNF:0
FNH:0
LF:0
LH:0
BRF:0
BRH:0
end_of_record
TN:
SF:src/real.ts
FNF:2
FNH:0
DA:1,0
DA:2,0
LF:2
LH:0
BRF:0
BRH:0
end_of_record
from repowise.core.analysis.health.coverage.lcov import parse_lcov
for f in parse_lcov(open("repro.lcov").read()).files:
print(f"{f.file_path:16} coverable_lines={f.total_coverable_lines} "
f"line_pct={f.line_coverage_pct!r:8} branch_pct={f.branch_coverage_pct!r}")
Expected Behavior
The two records are not the same fact and should not collapse to the same value. A file with nothing to cover should be reported as unknown/not-applicable (None), the way branch_coverage_pct already is, so the coverage biomarkers stay silent on it.
Actual Behavior
src/types.ts coverable_lines=0 line_pct=0.0 branch_pct=None
src/real.ts coverable_lines=2 line_pct=0.0 branch_pct=None
Both report 0.0. total_coverable_lines does carry the distinction, but nothing downstream consults it before applying the coverage deductions.
On a real repo. A TypeScript barrel of 28 export interface / export type declarations and no executable statement (LF:0 FNF:0 BRF:0, emitted by @vitest/coverage-v8) scored:
coverage_gradient |
−2.00 · "100% of lines uncovered (0% line coverage)" |
untested_hotspot |
−2.00 · critical · "Hotspot with 0% line coverage and 21 dependents" |
| score | 2.5 |
weighted_deficit |
2079 — 15.8% of the whole repo gap, ranked #2 in high_leverage_files |
So fix_first's runner-up was a file with no code to fix. Excluding the module from the coverage report moved the repo from 2 alert files to 1 and the average from 8.04 to 8.12, which is the same bug seen from the other side.
Root cause (and why it is not a one-line fix)
FileCoverage cannot express the distinction for lines, but can for branches:
# core/analysis/health/coverage/model.py:19-21
file_path: str
line_coverage_pct: float # no None
branch_coverage_pct: float | None # has None
The three if total else 0.0 sites are a consequence of that asymmetry, and all four parsers share it:
coverage/lcov.py:46—(hit / total * 100.0) if total else 0.0coverage/cobertura.py:96— same expressioncoverage/clover.py:77—(len(covered) / total_n * 100.0) if total_n else 0.0coverage/repowise_json.py:111— clampspctwith no zero-coverable guard
A fix presumably widens the field to float | None and returns None when there is nothing coverable, then lets the existing cov is None guards in coverage_gradient / coverage_gap / untested_hotspot do their job unchanged.
Worth deciding explicitly whether "no coverable lines" should be None (unknown) or 100.0 (vacuously covered). None seems right and matches the branch behaviour, but it is a product call — an lcov record whose producer genuinely instrumented the file and found nothing to instrument is arguably a different statement from no record at all.
Environment
- OS: macOS 27.0.0 (Darwin, arm64)
- Python version: 3.14.6
- Repowise version: 0.49.0 (also present in 0.48.0)
- Installation method: pipx
- Coverage producer:
@vitest/coverage-v84.x,reporter: ['text', 'lcov']
Additional Context
The distinction is already available at the point of the bug — total_coverable_lines is set in the same flush() that computes line_pct — so the parsers have everything they need to answer "not applicable" without consulting the source tree.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with core/analysis/health/coverage/model.py and the parser logic in coverage/lcov.py, coverage/cobertura.py, coverage/clover.py, and coverage/repowise_json.py. Decide how zero coverable lines should be represented, update the shared model and parser paths consistently, then verify that the existing coverage biomarkers remain silent for those records while genuinely uncovered files still report 0% coverage.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- testing-qa, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100