Dode code met groene tests: wat coverage je niet vertelt
7m leestijd

Dode code met groene tests: wat coverage je niet vertelt

Ik scande 29 van mijn eigen repository's opnieuw op dode code. Van de 296 dode symbolen worden er 138 alleen nog door tests aangeroepen. Coverage telt ze gewoon mee, en een agent die de repo leest, ziet een geteste functie die er veilig uitziet om te hergebruiken.

bewijs138 dode symbolen die alleen tests nog aanroepenknossos 0.44.0, vitest 5.0.02 bronnen11 november 2026
eigen meting
Knossos-scan van 29 eigen repository's, 327 kandidaten nagelopen, op 11 november 2026
getest op
knossos 0.44.0, vitest 5.0.0
gecontroleerd
11 november 2026

In mcpobservatory, het project achter MCP Observatory, staat een functie die parseSemgrepJson heet. Hij zet de JSON-output van Semgrep om in findings. Valt die JSON niet te parsen, dan geeft hij een lege lijst terug.

Dat laatste was een bug. Een rapport dat halverwege was afgekapt omdat de buffer vol zat, werd zo "Semgrep heeft gekeken en niets gevonden", en de scan ging als schoon de database in. Op 11 augustus heb ik dat gerepareerd. Productie roept sindsdien parseSemgrepJsonStrict aan, die null teruggeeft als het rapport niet te lezen is.

De oude functie bleef staan. Hij is nog steeds geëxporteerd en doet nog steeds precies wat de bug deed. Negen tests roepen hem aan, 15 keer per run, en coverage telt hem netjes mee in de 95,1% van de functies die de suite raakt. Niets in productie gebruikt hem.

Voor een agent die "de Semgrep-parser" zoekt, is dit de beste kandidaat in de repository: de kortste naam, een docblock, en tests die laten zien hoe je hem aanroept. Het is ook de enige versie die de bug terugbrengt.

Alleen de test roept parseSemgrepJson nog aan, productie gebruikt parseSemgrepJsonStrict

Wat ik telde ​

In september liet ik Knossos, de architectuurscanner die ik bouw, los op al mijn repository's. Vandaag heb ik dat overgedaan met Knossos 0.44.0, op de 29 repository's van mezelf die nog op deze machine staan. Code die ik voor werk schrijf, deed niet mee.

Knossos meldt dode code als kandidaat en zegt erbij hoe zeker hij is. Ik nam alleen de probable-kandidaten. Bij 166 daarvan gaf de simpelste controle die er is al uitsluitsel: de naam komt nergens anders in de repository voor, of bij test-only code nergens buiten de tests. De andere 161 zijn met de hand in de broncode nagelopen, eerst door drie agents en daarna door mij: welk type de receiver heeft, of een aanroep in een commentaar staat, of een script met de hand wordt gedraaid.

Testhelpers waarvan de naam dat al zegt, zoals __resetStatusRegionForTests, heb ik vooraf weggelaten. Die zijn met opzet alleen voor tests.

WatAantal
probable-kandidaten327
bevestigd dood296
daarvan alleen aangeroepen in tests138
false positives31 (9,5%)
repository's met dode code18 van 29

De false positives waren bijna allemaal code waarvan de scanner niet ziet dat hij geladen wordt: scripts die ik met de hand draai, fixture-bestanden die Semgrep als data leest, callbacks die je aan een library meegeeft. Eén repository heb ik helemaal weggelaten. Knossos 0.44.0 zag daar de Python-imports aan voor externe modules, waardoor elke service dood leek. Een scanner die zijn maker vertelt dat diens hele backend dood is, heeft in elk geval zelfvertrouwen. De bug staat inmiddels op de lijst van Knossos zelf.

Waarom geteste dode code erger is ​

Ongeteste dode code valt op. Coverage kleurt hem rood, en wie hem tegenkomt, vraagt zich af of hij nog nodig is. Geteste dode code ziet eruit als een onderdeel van het systeem: hij heeft een test, hij telt mee in de coverage, en de test laat zien hoe je hem aanroept.

De makers van knip, een tool die ongebruikte exports in JavaScript en TypeScript opspoort, schrijven het in hun documentatie over production mode zonder omwegen op: "Test files usually import production files. This prevents production files or their exports from being reported as unused, while sometimes both of them can be deleted."

Voor een mens is dat rommel. Voor een agent is het documentatie. Een coding agent leest de repository om te bepalen wat hij kan hergebruiken, en een geëxporteerde functie met een groene test is het sterkste signaal dat hij daarbij krijgt. Dat de functie in augustus is vervangen, staat nergens. Een dependency-graph ziet het wel: buiten de tests roept niemand hem aan.

GitClear meet hetzelfde patroon van de andere kant. In hun rapport over 2026 staat dat "long-term legacy maintenance is down 74% vs 2022 levels", en volgens hen wordt de gangbare workflow met AI beloond voor "a happy-path, a passing test, a closed ticket". Een nieuwe functie met een test sluit een ticket. De oude opruimen sluit er geen.

Zeven weken na de lijst ​

Het pijnlijkste getal komt van mezelf. Op 24 september had ik de volledige lijst, per repository en per symbool. In dezelfde repository's stonden toen 369 dode stukken productiecode. Vandaag zijn er daarvan 168 nog steeds aanwezig en nog steeds dood: 46%.

Er zijn 133 verwijderd. De rest wordt inmiddels gebruikt, of Knossos rekent ze niet meer mee na de fixes voor false positives van de afgelopen weken. Tussen code met en zonder tests zat nauwelijks verschil: van de test-only symbolen staat 48% er nog, van de volledig ongebruikte 43%.

Een lijst met dode code ruimt dus niets op. Het is een voornemen, en mijn voornemens houden het ongeveer net zo lang vol als die van iedereen in januari.

Maakt het uit wanneer de repository begon? ​

Ik verwachtte dat repository's die ik begon nadat Knossos er was, beter zouden scoren, en dat nieuwere modellen minder rommel achterlaten. De data zegt minder dan ik hoopte.

Eerste commitRepository'sDode symbolen per 1.000
voor 18 juli 2026 (start Knossos)1311,3
vanaf 18 juli162,3
vanaf 18 juli, zonder Knossos158,1

Het verschil zit bijna helemaal in Knossos zelf: 15.113 symbolen, nul dood. Het is ook de repository waarvan de CI faalt zodra het aantal ongebruikte kandidaten boven een budget komt. Dat het gereedschap hier zijn eigen huiswerk nakijkt, is me niet ontgaan. Zonder Knossos hangt het verschil af van hoe je telt. Alles bij elkaar opgeteld is het 8,1 tegen 11,3. Per repository ligt het gemiddelde in beide groepen rond de 13 per 1.000, en de nieuwere repository's zijn jonger en kleiner, dus ze hebben simpelweg minder tijd gehad om dode code te verzamelen. Wat ik wel durf te zeggen: de repository met een gate heeft nul, de rest heeft een lijst.

Over modellen kan ik niets zeggen. Mijn transcripts gaan terug tot september, en daarin draait vrijwel alles op Opus 5.5. Een effect van een modelupdate kan ik met deze data niet loskoppelen van leeftijd en omvang.

Wat je eraan doet ​

  • Draai je controle op dode code zonder de tests. Een symbool dat alleen tests aanroepen, telt dan als dood. knip doet dat met knip --production, Knossos meldt het als test_only. Bij een tool als vulture geef je alleen je broncode mee, niet je testmap.
  • Verwijder een functie samen met zijn tests. Een test die alleen dode code test, beschermt niets. Verhuis het gedrag dat er nog toe doet naar de functie die wel draait, en gooi de rest weg.
  • Maak het weghalen van de oude versie onderdeel van de vervanging. Wie een functie vervangt, haalt de oude in dezelfde commit weg. Een regel in je CLAUDE.md helpt soms. Een controle die kan falen helpt altijd: laat de gate rood worden als het aantal test-only symbolen groeit.

Een groene suite zegt al weinig over of je tests een bug zouden vangen. Over de vraag of de geteste code nog ergens voor dient, zegt hij helemaal niets.

parseSemgrepJson gaat er vandaag uit. Dit is de prompt die de agent in mcpobservatory daarvoor kreeg:

text
parseSemgrepJson in src/enrich/semgrep.ts has had no production caller since
6ec7481b (11 Aug): runSemgrep uses parseSemgrepJsonStrict. The wrapper still
turns a parse failure into [], which is the S2 bug, so nothing may reuse it.

1. Find every caller with `git grep -w parseSemgrepJson`. Expect only
   tests/enrich/semgrep.test.ts.
2. Move each test that still checks real behaviour onto
   parseSemgrepJsonStrict: [] for a clean report, null for an unparseable one.
   Delete tests that only exercise the wrapper.
3. Delete parseSemgrepJson and update the comments that name it in
   src/enrich/semgrep.ts. Leave migration 064 alone, it is history.
4. Run the full suite with the coverage floors. Report the diff stat and the
   test count before and after.

Do not change the behaviour of parseSemgrepJsonStrict.

Zakt de coverage daarna een tikje, dan is dat de eerlijkste daling die ik dit jaar heb gezien.

(11 van 11)