Sloc Cloc and Code 4.0 (scc) - Het vinden van de bestanden die de meeste aandacht nodig hebben

In dit bericht loop ik een aantal van deze functies langs, in de hoop lezers aan te moedigen de nieuwste versie uit te proberen.

Hotspots

Meer dan tien jaar geleden schreef ik over de bug-voorspelling van Google, die bestanden rangschikte op basis van commit-geschiedenis versus bugfixes om te bepalen waar problematische bestanden zich bevonden. Het was interessant, maar werd stopgezet omdat ontwikkelaars het simpelweg niet nuttig vonden. Soms wisten ze dat de code een 'hotspot' was, soms niet, maar het weten dat iets een hotspot was, bood hen geen middelen om tot verbetering over te gaan.

Onlangs vroeg ik me af of we de complexiteitsschatting van scc kunnen gebruiken om de ruis te verminderen. Het weten dat er veel fixes zijn toegepast op een configuratiebestand is niet erg nuttig; echter, weten dat er veel wijzigingen zijn aangebracht in een bestand met veel logica, is dat wel. Dit is dezelfde aanpak die ik gebruikte voor de rangschikking in codespelunker.

Complexe bestanden hebben de meeste aandacht nodig!

Voor zover ik weet is dit een heruitvinding van een idee van Adam Tornhill in "Your Code as a Crime Scene" (een boek dat ik momenteel lees), wat uiteindelijk leidde tot de oprichting van het bedrijf CodeScene. Er zit duidelijk waarde in deze metriek.

Hier is een voorbeeld van de output wanneer scc op zijn eigen codebase wordt gedraaid:

$ scc --hotspots
───────────────────────────────────────────────────────────────────────────────
Hotspots · last 1000 commits · 2019-07-21 → 2026-06-26
───────────────────────────────────────────────────────────────────────────────
File                            Lang   Cmplx  Commits   Lines±  Authrs  Hotspot
───────────────────────────────────────────────────────────────────────────────
processor/processor.go            Go     156      156    1,651      13    100.0
processor/workers.go              Go     244       92    3,617      15     92.2
test-all.sh                    Shell      56      181    3,287      15     41.7
~ocessor/formatters_test.go       Go     183       51    2,459       9     38.4
processor/workers_test.go         Go     408       21    1,189       8     35.2
processor/formatters.go           Go      44      135    5,848      17     24.4
main_test.go                      Go     261       18      995       6     19.3
processor/detector_test.go        Go     133       32    1,175       6     17.5
main.go                           Go      40      101    1,545      17     16.6
processor/file.go                 Go      50       73    2,074      12     15.0
processor/detector.go             Go      70       45      948       5     12.9
processor/file_test.go            Go      75       33      826       5     10.2
cmd/badges/main.go                Go      73       31    1,111       5      9.3
processor/history.go              Go     173        9      941       4      6.4
processor/structs.go              Go      25       42      247       10      4.3
config_test.go                    Go     199        4      850       3      3.3
~workers_regression_test.go       Go      50       13      276       6      2.7
~rocessor/processor_test.go       Go      51       12      289       4      2.5
~ocessor/history_authors.go       Go     111        5      666       3      2.3
mcp.go                            Go      71        7      527       4      2.0
───────────────────────────────────────────────────────────────────────────────
complexity × change-frequency, normalised · 20 of 90 files shown
───────────────────────────────────────────────────────────────────────────────

Zoals je kunt zien, heeft de output correct geïdentificeerd dat processor/processor.go en processor/workers.go de hotspots in de codebase zijn.

Hoe werkt het?

Kort samengevat: hotspot = complexiteit × aantal commits, genormaliseerd op een schaal van 0-100. We berekenen de complexiteit voor het huidige HEAD-bestand en lopen vervolgens terug om te zien hoe vaak elk bestand is gewijzigd. Let op: alleen bestanden in HEAD worden geteld; bestanden met veel wijzigingen die inmiddels zijn verwijderd, tellen niet mee.

Ter vergelijking met een eenvoudige telling:

$ scc --by-file -i go -s complexity
───────────────────────────────────────────────────────────────────────────────
Language            Files       Lines    Blanks  Comments       Code Complexity
───────────────────────────────────────────────────────────────────────────────
Go                     69      40,137     3,049     2,131     34,957      4,478
───────────────────────────────────────────────────────────────────────────────
processor/workers_test.go       2,156       374        69      1,713        408
main_test.go                      992        80        26        886        261
processor/workers.go              966       146       102        718        244
processor/report_test.go          971        98       109        764        237
config_test.go                    786        50        85        651        199

Bij een eenvoudige telling, beperkt tot Go-bestanden en gesorteerd op complexiteit, scoren workerstest.go, maintest.go en config_test.go hoog. Deze bestanden zijn technisch complex, maar ze vormen niet de plek waar het zware ontwikkelwerk plaatsvindt. Complexiteit op zichzelf vertelt je waar grote bestanden met veel if-condities zijn; vaak zijn dit testbestanden.

Draai het om en rangschik op churn (het aantal commits). Nu is test-all.sh nummer één met 181 commits, en structs.go stijgt met 42. Beiden veranderen constant, maar geen van beide bevat de kernlogica of de bugs. Churn op zichzelf vertelt je wat er veel verandert (vaak configuraties, scripts en boilerplate), maar is geen goede proxy voor bugs of logica.

Een redelijke proxy is echter de overlap tussen churn en complexiteit: welke bestanden zijn zowel gecompliceerd als onderhevig aan veel wijzigingen? Dit is geen aanwijzing voor "historisch buggy" code, maar een indicator voor "moeilijk om mee te werken", wat suggereert dat code mogelijk opgesplitst moet worden.

Complexe code die niemand bewerkt, is waarschijnlijk geen probleem. Simpele bestanden die je constant aanpast, zijn dat meestal ook niet. Echter, een bestand dat complex is én veel verandert, is waar problemen meestal liggen: merge-conflicten, brekende tests en frustratie bij wijzigingen.

Waar Google faalde omdat een hotspot-vlag geen directe actie suggereerde, wordt een soortgelijk idee een "onboarding map" wanneer je een onbekende codebase probeert te begrijpen.

Git-diepte en beperkingen

Je kunt de diepte van de git-commits specificeren voor deze berekening. Hiermee kun je zien hoe hotspots zijn verschoven door verder of korter terug in de tijd te kijken.

Voorbeeld van 50 commits versus 10 commits:

$ scc --hotspots --depth 50
───────────────────────────────────────────────────────────────────────────────
Hotspots · last 50 commits · 2026-04-13 → 2026-06-26
───────────────────────────────────────────────────────────────────────────────
File                            Lang   Cmplx  Commits   Lines±  Authrs  Hotspot
───────────────────────────────────────────────────────────────────────────────
processor/processor.go            Go     156       14      415       4    100.0
main_test.go                      Go     261        8      295       4     95.6
processor/history.go              Go     173        9      941       4     71.3
processor/workers.go              Go     244        6      130       5     67.0
processor/workers_test.go         Go     408        3      157       3     56.0
...
$ scc --hotspots --depth 10
───────────────────────────────────────────────────────────────────────────────
Hotspots · last 10 commits · 2026-06-25 → 2026-06-26
───────────────────────────────────────────────────────────────────────────────
File                            Lang   Cmplx  Commits   Lines±  Authrs  Hotspot
───────────────────────────────────────────────────────────────────────────────
processor/workers.go              Go     244        2       15       1    100.0
processor/processor.go            Go     156        3       20       1     95.9
config_test.go                    Go     199        2        5       2     81.6
processor/history.go              Go     173        1       19       1     35.5
regression_test.go                Go     152        1        6       1     31.1

Houd er rekening mee dat dit niet betekent dat er per definitie iets mis is met de codebase; het is enkel een indicator van waar je zou kunnen kijken.

Dit vereist geen installatie van git op het systeem. Hoewel de repository een .git-map nodig heeft, wordt scc geleverd met github.com/go-git/go-git, waardoor het een enkelvoudig binary-installatiebestand blijft.

Prestatiekosten

Omdat scc nu terugloopt door de git-geschiedenis (standaard 1000 commits), is het proces trager dan een standaard scc-run.

$ hyperfine 'scc' 'scc --hotspots'
Benchmark 1: scc
Time (mean ± σ):      11.2 ms ±   0.4 ms    [User: 15.2 ms, System: 7.6 ms]
Range (min … max):    10.6 ms …  13.5 ms    194 runs
Benchmark 2: scc --hotspots
Time (mean ± σ):      4.739 s ±  0.068 s    [User: 3.341 s, System: 1.611 s]
Range (min … max):    4.707 s …  4.930 s    10 runs

Summary
scc ran
421.57 ± 14.52 times faster than scc --hotspots

(Gemeten op een Macbook Air 2020 M1 tegen de scc codebase).

Change Coupling

Geïnspireerd door CodeScene heb ik ook de change coupling functie toegevoegd. Het idee is dat bestanden afhankelijk van elkaar zijn als ze constant in dezelfde commit verschijnen, ongeacht of er een door de compiler afgedwongen afhankelijkheid bestaat.

$ scc --coupling
───────────────────────────────────────────────────────────────────────────────
Change Coupling · last 1000 commits · 2019-07-25 → 2026-07-20
───────────────────────────────────────────────────────────────────────────────
File A                      File B                      Shared Commits Coupling
───────────────────────────────────────────────────────────────────────────────
languages.json              processor/constants.go                 198    67.8%
LANGUAGES.md                languages.json                         167    61.2%
LANGUAGES.md                processor/constants.go                 153    58.2%
SCC-OUTPUT-REPORT.html      processor/constants.go                 149    37.7%

Dit is nog nuttiger wanneer het per bestand wordt toegepast:

$ scc --coupling-for ./processor/detector.go
───────────────────────────────────────────────────────────────────────────────
Change Coupling · last 1000 commits · 2019-07-25 → 2026-07-20
───────────────────────────────────────────────────────────────────────────────
Related File                                          Shared Commits   Coupling
───────────────────────────────────────────────────────────────────────────────
processor/detector_test.go                                        26      49.1%
processor/workers.go                                              15      12.1%
processor/structs.go                                               9      11.1%
processor/file_test.go                                             7       9.7%
processor/workers_test.go                                          6       9.7%
processor/processor_test.go                                        5       9.4%

Om te voorkomen dat bestanden die geen code bevatten (zoals configuraties) hoog scoren, kan de hotspots-truc (weging op basis van complexiteit) worden toegepast via --coupling-weighted. Hierdoor worden bestanden met weinig logica naar beneden gezet.

Git Metrics

Naast hotspots en coupling zijn er andere git-outputs beschikbaar:

Tijdlijn van code

Met --timeline kun je de trend van code over tijd volgen, wat handig is bij bijvoorbeeld een migratie van JS naar TS.

$ scc --timeline
───────────────────────────────────────────────────────────────────────────────
Languages · last 1000 commits · 2019-07-21 → 2026-06-26
───────────────────────────────────────────────────────────────────────────────
Language             Trend                             Code    Share     Change
───────────────────────────────────────────────────────────────────────────────
Go                   ▂▂▂▂▂▂▂▂▂▃▃▃▃▃▃▃▃▃▃▃▃▄▆▆▆▇      37,868    65.2%    +33,595
JSON                 ▄▄▄▄▄▄▄▄▄▄<0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85>▆▆▆▆▆▆▆▇      12,944    22.3%     +6,236
HTML                   ▂▂▆▆▆▆▆▆▆▆▆▆▆▆▆▇<0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85>▆       3,160     5.4%     +3,160
Markdown             ▃▃▃▃▄▄▄▄▄<0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85>▆▆▆▆▆▇       1,884     3.2%     +1,386
Shell                ▂▄▄▄<0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85>▆▆▆▆▆▆▆▆▇▆<0xE2><0x96><0x85>▄       1,200     2.1%       +993
Go Template                               ▃▆▆▆▇         598     1.0%       +598
Python                              <0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85>▇         225     0.4%       +225
YAML                 ▃▃▃▃▃▃▃▃▃▃▃▃▆▆▆▆▆▇▇▇▇▇▇▇▇▇          52     0.1%        +33
Powershell           ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇          46     0.1%         +0
gitignore            <0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85>▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▆▇▇▇          28     0.0%         +9
License              ▇▇▇▇▇▇<0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85><0xE2><0x96><0x85>          25     0.0%        -12
Plain Text           ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇          24     0.0%         +0
───────────────────────────────────────────────────────────────────────────────

Bus Factor

Met --by-author kun je de "bus factor" van je applicatie berekenen.

$ scc --by-author
───────────────────────────────────────────────────────────────────────────────
Authors · last 1000 commits · 2019-07-24 → 2026-07-08
───────────────────────────────────────────────────────────────────────────────
Author                               Code     Cmplx   Files     Owns  Last seen
───────────────────────────────────────────────────────────────────────────────
Ben Boyter (github.com)            18,901     1,839      59    37.9% 2026-07-06
apocelipes                         16,740       299      11    33.6% 2026-06-26
Ben Boyter (boyter.org)             6,013       469      19    12.1% 2026-07-04
...
───────────────────────────────────────────────────────────────────────────────
Bus factor 2 · Ben Boyter (github.com) + apocelipes
last-touched 79% of in-window code
───────────────────────────────────────────────────────────────────────────────

Je kunt deze data ook combineren met de tijdlijn: scc --by-author --timeline.

Infographic

Je kunt nu een infographic genereren met scc --report. Dit produceert een HTML-bestand (scc-report.html) dat alle metrieken bevat. Dit is ideaal om aan het management te presenteren, aangezien zij waarschijnlijk geen voorkeur hebben voor een vi-geïnspireerd slide-deck.

Cognitieve Complexiteit

Scc bevat al complexiteitsberekeningen, maar cyclomatische complexiteit (uitgevonden in 1976) is niet de enige methode. Ik heb gekeken naar de benadering van Sonar, die cognitieve complexiteit berekent.

Sonar gebruikt een volledige AST (Abstract Syntax Tree), wat in scc te duur zou zijn qua runtime. Echter, de vorm van de code (inspringing/indentatie) is een goede proxy voor complexiteit. In linted code en Python is dit zeer betrouwbaar.

In scc benaderen we cognitieve complexiteit door simpelweg de hoeveelheid witruimte (spaties en tabs) aan het begin van de regel te tellen en dit te gebruiken als vermenigvuldiger voor de complexiteitsscore wanneer we op een vertakkingsconditie stuiten.

De prestatiekosten hiervan zijn verwaarloosbaar (< 1%):

$ hyperfine 'scc' 'scc --cognitive'
Benchmark 1: scc
Time (mean ± σ):      4.573 s ±  0.047 s
Benchmark 2: scc --cognitive
Time (mean ± σ):      4.583 s ±  0.086 s
Summary
scc ran 1.00 ± 0.02 times faster than scc --cognitive

Het effect is merkbaar; voor het bestand main.go in scc schiet de complexiteit omhoog:

  • Standaard: Complexity 40
  • Met --cognitive: Complexity 103

Dit is momenteel een opt-in functie en nog niet de standaard.

Config/DotFile Ondersteuning

Vanaf v4.0.0 ondersteunt scc globale configuratiebestanden. Je kunt een .sccconfig bestand toevoegen aan je projectroot of het pad definiëren via de omgevingsvariabele SCCCONFIGPATH.

Voorbeeld van een .sccconfig bestand:

# count the way I like it
--no-cocomo
--exclude-dir node_modules
--format wide

Let op: dit configuratiebestand staat scc niet toe om bestanden te schrijven; dit kan alleen via CLI-argumenten om veiligheidsredenen.

MCP Ondersteuning

Scc heeft nu ingebouwde MCP-ondersteuning. Door dit te koppelen aan een LLM, kan de LLM de complexiteit van je codebase vinden, wat helpt om tokens te besparen.

LOCOMO

Ik heb eerder over LOCOMO geschreven: scc kan nu de kosten voorspellen om je codebase in zijn huidige staat opnieuw op te bouwen met behulp van een LLM. Dit is een zelfbedachte metriek, maar ik wilde in ieder geval het proces in gang zetten.

Overige wijzigingen

  • Duplicate flags: De laatste duplicate flag wint nu (bijv. scc -i java -i go telt alleen Go).
  • Taaldetectie: Linguist-geïnspireerde detectie voor betere herkenning van C++/ObjectiveC/C header-bestanden.
  • JSON-output: Percentage-outputs zijn nu beschikbaar in JSON voor gebruikers van jq.
  • Externe ignore-bestanden: Ondersteuning via --ignore-file ~/.config/git/ignore.
  • Bugfixes & Performance: Talrijke kleine bugfixes en prestatieverbeteringen, grotendeels geleverd door apocelipes.

Conclusie

Tien jaar geleden schreef ik over Google's poging om hotspots te identificeren, wat ze stopzetten omdat men het niet nuttig vond. Ik denk dat ze niet helemaal ongelijk hadden, maar dat ze iets misten om de ruis te verminderen en het op de verkeerde plek implementeerden. Wanneer je het richt op een codebase die je probeert te doorgronden, is het veel effectiever.

Download scc 4.0.0 via GitHub, richt --hotspots op iets dat je nog nooit gezien hebt, en ontdek of het je naar de juiste bestanden stuurt.