skunk multiplies code complexity by the cost of the code you never tested
A SkunkScore Calculator for Ruby Code -- Find the most complicated code without test coverage!
At a glance
- What is it?
- skunk is a RubyCritic extension that produces a SkunkScore for a file or a project by combining code complexity, code smells and code coverage, so the modules that are both hard to read and untested float to the top of the list. It reads SimpleCov's coverage JSON and can compare a feature branch against another one.
- Who is it for?
- Adopt skunk if you have a Ruby codebase where complexity and test coverage have drifted apart and you need to decide what to refactor first, because the score multiplies the two so the worst offenders surface as a ranked list rather than as two separate reports you have to reconcile. Do not adopt it as a quality gate in CI without reading how it scores, because the ranking depends on churn and on a coverage file that has to be regenerated after every test run.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the score is actually measuring
Skunk is a RubyCritic extension that calculates a SkunkScore for a file or project, and the SkunkScore is described as a value that assesses the technical debt of a module.
The stated goal of the score is to serve as a compass in your next refactoring adventure, and the README lists the questions it is meant to answer: what can I do to pay off technical debt, what are the most complicated files with the least code coverage, what are good candidates for my next test-writing efforts, and what are good candidates for my next refactoring efforts.
Three inputs go into it: code complexity, code smells, and code coverage. Two of those come from RubyCritic, which is the analyser skunk extends, and the third comes from SimpleCov. So the score is not a new static analysis, it is a combination of existing measurements.
The combination is the interesting part, and the example output in the README shows why. The report is a sorted table with the columns file, skunk_score, churn_times_cost, churn, cost and coverage. Two rows make the point:
One is samples/rubycritic/analysed_module.rb with a score of 258.88, a cost of 2.59 and a coverage of 0.0. A file with essentially no test coverage at all.
The other is lib/skunk/commands/status_reporter.rb, with a much lower score of 11.33 but a churn of 18 and a cost of 3.78, and coverage of 97.5. It is a more complex and more frequently changed file than many others, but it is tested, so it does not climb the list.
That contrast is the design. A complexity metric on its own tells you the code is hard to read; a coverage metric on its own tells you the code is untested. Neither tells you whether you should care. A module that is complex and untested is where a bug will be expensive, and that is what the multiplication surfaces.
The cost column is worth understanding separately. It comes from churn, how many times the file changed, combined with its complexity, which is the standard RubyCritic cost formula. So the churn_times_cost column tells you what the change burden has been, and the coverage column tells you how much of that churn was unguarded by tests.
The setup, and the coverage file the whole thing depends on
Installation is a Gemfile line and a bundle:
gem 'skunk'Then run bundle, or install it yourself with gem install skunk.
The important part comes next, and it is the step people miss. Skunk needs a code coverage report, and the README is explicit that SimpleCov is what generates it: use SimpleCov to generate a report of how many statements are covered by your test suite, and that is how skunk knows the status of your test suite and code coverage.
The success signal for SimpleCov is concrete: it generated a JSON file under coverage/.resultset.json in your application directory. The README warns that skunk runs rubycritic, which loads code coverage data from your coverage/.resultset.json file, and tells you to make sure you have this file for the best SkunkScore results.
There is a second requirement hidden in the first sentence of that section: SimpleCov has to be installed and set up in your project already. The README points at SimpleCov's own getting started guide for that.
And there is a third requirement about freshness. After adding more tests to your application, run SimpleCov again to generate an updated code coverage. This way, skunk will generate a new SkunkScore.
That last point is the operational trap in this tool. The score is only as current as your last coverage run, and coverage runs happen as part of your test suite. If you run the test suite, then run skunk, you get a score reflecting the current tests. If you write tests, do not re-run SimpleCov, then run skunk, you are looking at the previous coverage numbers with the current code, and the file that looks worst may be one you have just finished testing.
So the correct order is always: run the tests, then run skunk. And since the tool's whole purpose is to tell you what to test next, that ordering matters more here than for most static analysis tools.
Running it, and what the report looks like
The help output is short:
Usage: skunk [options] [paths]
-b, --branch BRANCH Set branch to compare
-o, --out PATH Output directory for reports
-v, --version Show gem's version
-h, --help Show this messageFour options, and the paths argument means you can point it at a subset of the tree rather than always analysing everything.
On the project you want to analyse, you run skunk with no arguments and the report goes to the console. The README shows the full example table, and the header row is worth reading carefully because the column names explain the model: file, skunk_score, churn_times_cost, churn, cost and coverage.
The rows are sorted by skunk_score, descending, and the bottom of the example gives the summary lines: SkunkScore Total 428.21, Modules Analysed 19, SkunkScore Average 22.54, and Worst SkunkScore 258.88 for samples/rubycritic/analysed_module.rb.
Three of those summary numbers are useful on their own. The average tells you the baseline quality of the codebase, so a project sitting at 22.54 and one at 60 are in different states even if their worst file is the same. The worst score tells you the ceiling. And the total is a number you can track over time, which is what the sharing feature is for.
The example also shows how the columns relate in practice. lib/skunk/commands/compare.rb has a score of 85.41 with a churn of 14, a cost of 2.31 and coverage of 63.64. Its cost is unremarkable; it scores highly because it changes often and a third of it is untested. lib/skunk/commands/version.rb has a score of 2.64 with a cost of 0.09 and coverage of 70.0, and lib/skunk/version.rb sits at 0.0 with coverage of 0.0, which shows that a file with no coverage and no complexity is not a problem worth a refactoring budget.
Branch comparison and sharing, the two features beyond the report
The command list in the README covers more than the default report, and two entries are worth describing.
The first is comparing feature branches, which the help output exposes as the --branch option. The repository includes a lib/skunk/commands/compare.rb file, and the example table includes lib/skunk/commands/compare_score.rb as well, so the comparison is implemented as its own command with its own scoring. The idea is to check a branch before merging it: a branch that adds a lot of churn to poorly covered files should be visible in the comparison rather than discovered in review.
The second is sharing your SkunkScore, which has its own command file, lib/skunk/commands/status_sharer.rb, and the README's table of contents has a Sharing your SkunkScore section. The file name in the example output also tells you the tool is designed around a notion of status, since there is a lib/skunk/commands/status_reporter.rb among the highest-churn files in its own report, and a lib/skunk/commands/status.rb.
The output option, --out PATH, sets the output directory for reports, so the console table is one form of output and the report can also be written somewhere for a dashboard or a CI artifact.
The repository layout supports the picture of a tool with several commands behind one CLI. Under lib/skunk/ there is a cli/ directory with an application.rb and an options/ directory containing argv.rb, plus a commands/ directory holding base.rb, compare.rb, compare_score.rb, default.rb, help.rb, output.rb, shareable.rb, status_reporter.rb, status_sharer.rb and version.rb. The rubycritic/ subdirectory holds the analysis glue, including analysed_module.rb and analysed_modules_collection.rb.
The samples/ directory at the top level contains samples/engines/ and samples/rubycritic/, and the sample files are used in the example report, so the project ships fixtures it analyses against itself. The repository also carries a .reek.yml and a .rubocop.yml with a .rubocop_todo.yml, so the tool lints and smell-checks itself with the same ecosystem it analyses.
Where skunk is the wrong tool
Three limits, and the first is about what the score does not see.
The first is that the coverage term is only as good as the coverage report, and coverage reports are frequently misleading in exactly the projects where a complexity score would matter. A test that executes a line without asserting anything raises coverage without raising confidence, and a suite that skips the hard branches still reports high numbers. Skunk multiplies a real measurement by a noisy one, and the README does not discuss how to read the result when the coverage figure is inflated by assertion-free tests.
The second is that churn is a git-dependent input, and it means the score is not reproducible across clones. Churn is how many times a file has changed, which comes from the git history. A shallow clone, a fresh branch, a repository that was imported rather than developed in place, and a file that was rewritten once in a large commit all produce different churn numbers for the same code. That means a SkunkScore is a property of a checkout rather than of a codebase, and comparing scores between two machines requires the same history on both.
The third is the RubyCritic dependency and what it brings with it. skunk does not analyse Ruby itself; it runs rubycritic and combines its output with coverage data. So you inherit RubyCritic's analysis model, its configuration, its own notion of complexity and smell, and any mismatch between what RubyCritic thinks and what your team thinks is complicated. The .reek.yml and .rubocop.yml in the repository are RubyCritic's configuration files, which makes that dependency concrete.
There is also the honest question of whether a single number should drive a refactoring queue at all. The README frames the SkunkScore as a compass, and a compass is a good metaphor for that, since it points rather than decides. The risk is a team that turns it into a target, since a score can be lowered by adding tests, which raises coverage, without the complexity ever falling, so the metric can be satisfied without the code being better.
The alternative approach is to use the underlying tools directly, run RubyCritic for complexity and smells and SimpleCov for coverage, and keep the two reports side by side rather than multiplying them. The difference in approach is that the multiplication is what surfaces the files that are both complex and untested, which is the case worth acting on, and it is also what produces a number that can be gamed and a ranking that can be over-trusted. A team that wants to argue about which files to fix will get more out of the two separate reports; a team that wants a sorted shortlist will get it from skunk.
Editorial conclusion
Adopt skunk if you have a Ruby codebase where complexity and test coverage have drifted apart and you need to decide what to refactor first, because the score multiplies the two so the worst offenders surface as a ranked list rather than as two separate reports you have to reconcile. Do not adopt it as a quality gate in CI without reading how it scores, because the ranking depends on churn and on a coverage file that has to be regenerated after every test run. Verify first that SimpleCov has produced coverage/.resultset.json in your project, since without it the coverage term is missing and the score is not the one the tool describes, that you re-run SimpleCov after adding tests so the score reflects the current suite, and that the RubyCritic and SimpleCov versions you run produce the churn and coverage figures the tool expects.
Frequently asked questions
What does the SkunkScore measure?
It assesses the technical debt of a module by taking three things into account: code complexity, code smells and code coverage. Skunk is a RubyCritic extension, so complexity and smells come from RubyCritic and coverage comes from SimpleCov.
How do I install skunk?
Add gem 'skunk' to your Gemfile and run bundle, or install it yourself with gem install skunk. You also need SimpleCov set up in your project, because skunk reads the coverage data from your coverage/.resultset.json file.
What files does the skunk report show?
A sorted table with the columns file, skunk_score, churn_times_cost, churn, cost and coverage, followed by a summary with the SkunkScore Total, Modules Analysed, SkunkScore Average and Worst SkunkScore. In the README example the worst file scores 258.88 with zero coverage.
Why does my skunk score look wrong after adding tests?
The score uses the coverage file SimpleCov produced, so you need to run SimpleCov again after adding tests to generate an updated code coverage, and then run skunk again to get a new SkunkScore.
What command line options does skunk support?
Four: -b or --branch to set the branch to compare, -o or --out for the output directory for reports, -v or --version to show the gem's version, and -h or --help to show the usage message. The tool also takes paths, so you can analyse part of a tree.
Can skunk compare a feature branch against another one?
Yes, through the --branch option, and the implementation lives in lib/skunk/commands/compare.rb with its own compare_score.rb alongside it, so a branch comparison has a separate score from the default project report.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fastruby-skunk)Was this useful?