Burying your dead code
eng.grandrounds.com
eng.grandrounds.com
Here's an example in PHP from a quick google:
https://github.com/scheb/tombstone
I once had to lead a large refactor of a project written in an foreign dynamically typed language and tombstoning was a necessity for how large and poorly designed the projet was.
There's lots of ways that code could become dead, even if it's still accessible via a test suite.
[0] https://github.com/colszowka/simplecov [1] https://github.com/danmayer/coverband
If I were to refactor some function to no longer be used, one would hope/expect that code coverage for that function would be missing, and thus we'd realize (or at least be able to notice more easily) that it was dead code.
However, unless your code is all tested via end-to-end tests of features and capabilities, it's _very_ possible that I (or a teammate) wrote some test of our dead helper function, verifying that it handles its edge cases correctly, as a unit test. In that case, we'd still see our code as "covered".
I think we could work around that by making the function deliberately raise an exception, and see which tests break, or commenting out the unit test of that function and see if it's still used. If the code is being exercised in any way other than its own unit tests, it ought to raise an error (or show coverage, depending on tactic). However, this requires us to _already_ suspect that function is dead.
Example: If I have a "dead" add() function with a unit test that asserts add(2,2) == 4, then code coverage would report that function being covered even if no other part of the codebase uses it anymore.
I still _want_ that kind of test coverage, esp when there's weird edge cases, but when looking for dead code, it's probably good to look at only the acceptance / integration tests (or whatever you want to call the tests that do the holistic "I click here and everything works" tests).
Python's cold acceptance is already enough to make big projects impractical on it, because every large codebase will gather some undecidable metaprogramming spread through it. But Ruby gets it everywhere.
Metaprogramming: the cause of, and solution to, all of life's problems.
No matter how many times you say it, that doesn't make it true.
It doesn't help with potentially reachable but unused cpdepaths, which in my experience make up for the most of 'dead' code.
Think of the functionality in an application that nobody uses or of obsolete API version when all the clients use a more recent version.
30-15 to static analysis, I think? ;)
return new $className(); $method = 'get' . $command;
$this->$method;
are marked in grey. Which means some methods that you need to keep are grey. Not necessarily helpful. And other methods you want to deleted are not grey.(The solution to that is "write code that doesn't suck", but alas, dead code tends to be a bigger problem in code that you didn't write.)
But as others have said, the unused code that keeps me busy is the stuff that says:
if($this->option->isTurnedOn()) {
$this->doStuff();
} else {
$this->doThat();
}
Sometimes it turns out that everyone uses it in the non-default state. Then there's the cases where everyone wants to use it in the non-default state, they just don't know the option can be toggled.A unit test does not prove without a doubt a class/method are not in use. Static analysis/good IDEs are bound to the immediate code base, they can't determine usage when your code is shared across multiple workspaces or when the class/method are only called externally.
Tombstoning isn't perfect, but its safer than what I typically do given a sufficient amount of time to collect logs along side a bit of analysis on a case by case basis (e.g. method AddTwoNumbers() not being called for 6 months maybe okay to remove, whereas EndOfYearReportGenerator() not being called for 6 months shouldn't blindly be removed.)
Yes, static analysis is useful for finding internally orphaned code (though a simple search and seeing if the references outside of tests are 1 is just as effective) but that's not all dead code. The article is just using that as a simple example.
If you have a feature that has non-orphaned methods your testing may cover it and your static analysis will say it's good but if it is never used then the code supporting it is effectively dead.
This code is dead weight to your code base. It's costing you time and effort to maintain or work around. Having the ability to track it down and remove it is important.
This is where other methods come in.
Or to put it another way, when you're designing a system, please think about the benefit static analysis will have in identifying dead code three years hence. But when you're trying to find dead code in that ten year old PHP code no amount of woulda's and coulda's will get rid of it.
The intelligent person solves the problem they've got. The unintelligent person shoulda's coulda's and woulda's you into oblivion.
(NB. I would probably choose a tool with a good type system, and I would try to write to its strengths, in any system I was designing today. But that still doesn't help me identify the dead code I've got.)
I've run that through many projects over the last year and it's been pretty instrumental in finding the low-hanging stuff easy to pick off.
The underlying premise is that, by leveraging ctags and the ability to search for the presence of tokens within a directory, it can estimate what's used based on occurrence frequency and location. Because of this, it's language-agnostic.
Biggest removal I've worked on is ~1500 LOC (a large chunk being JSON); however, I've seen the results from some of our client work at thoughtbot, which have surpassed 3k-4kLOC.
Bisect is great to find the commit that caused a bug, because the commit narrows down the code to inspect to fix the bug.
Maybe I'm missing something here, but git bisect doesn't seem to apply in this case.
[1]https://en.wikipedia.org/wiki/Wikipedia:Chesterton%27s_fence
git log -p -G regexThe tricky part shouldn't be the second step. The second step shouldn't even exist. How is it that in this day and age, your compiler can't tell you definitively if a block of code is dead?
You get a nice structured logging format set up, log every request, and feed that into something you can graph and query. Leave that on long enough (long enough being a length of time that depends on what the endpoint does and how critical it is, as well as how much traffic you see regularly) and you can figure out what's being used pretty quickly.
And I'd imagine that in any form of client-server system, especially if there are multiple client implementations, it would be hard to tell if specific server endpoints are definitively "dead".
If you don't have that detail in your logs, that's step 0.
Of course in a dynamic language, or one with reflection, then the distinctions are blurry