A few weeks ago I used the golang tool deadcode
to find and remove
unreachable code from the DNSControl project. I also fixed some unrelated
unused parameter
warnings. This was my first experience with the utility and
it was a good experience.
What is deadcode?
deadcode
is a utility that detects unreachable Go code. Read this excellent
blog post for more info.
What is DNSControl
DNSControl is “infrastructure as code” for DNS. It supports 40+ DNS providers. More info on https://dnscontrol.org/ or Github.
Installation
Installing deadcode
is easy. Just do:
|
|
Starting state
This is the “before” picture. Deadcode found many, many, functions that are not used.
|
|
Which editor?
I switch between vim
and VSCode. I use vim
when I need to serious work
done, and VSCode when I want the editor to do work for me.
I like vim because it is snappy and responsive. Because I have decades of experience with it, my fingers just know what to do.
I like VSCode because it reports warnings from a variety of tools, it has a great “rename symbol” command, an amazing “refactor” feature, and it has a VIM-emulator plug-in that isn’t perfect, but makes my fingers happy.
Even when I’m using vim I still keep VSCode running on a separate monitor because the warnings and other messages it displays in the “problems” tab are extremely useful.
Another thing I like about VSCode is the -g
command line option.
|
|
opens the file
at that line and column. The filename:row:col
format is used by deadcode
and many other tools.
I used the -g
option to quickly move to each of deadcode
’s findings.
FYI: You can do something similar with vim: vim +361 providers/cloudflare/rest.go
opens the file and scrolls down to that line, but this is more work because it requires 2 copy-and-paste operations instead of one.
Unused Parameters reveals a bug
I started up VSCode which reported a number of unused parameter issues:
This warning does not come from deadcode
but I’m glad I went on this
detour from my original plan because I found a bug!
The warning about targetSpec
indicated that the integration tests for
IGNORE()
ignored the 2nd parameter! It always tested “all types” instead
of the one type you’ve specified. Oops!
The fix looked like this:
|
|
After commiting this change I ran the full integration test suite via GitHub Actions (GHA).
Those tests verify some algorithms related to deleting data. I feared that once the tests were fixed I’d learn that the code was not deleting what it was supposed or (my real fear) it was deleting things that shouldn’t be deleted!
I breathed a sigh of relief when the tests completed without error.
This has nothing to do with deadcode
but I thought I’d mention it.
Unused parameters trick
“And the second trick will amaze you!”
In most cases an “unused parameter” could just be removed from the definition and all uses of the function. I found it was best to remove it from the implementation or interface, then let VSCode tell you all the (now) invalid uses of that function. It was then easy to click on each VSCode “problem” and delete the (now unneeded) parameter.
However some situations I could not change the function signature. The parameter was needed to meet an interface, even though the paramter would go unused.
One way to fix this is to contrive a use for the variable:
|
|
However a better way is to use _
as the name in the function signature.
|
|
Da da!
Removing dead code
Next I went through the deadcode
output and looked into each complaint.
Rather than deleting the dead code, I would rename the variable by adding an “x” to the start of the name and see if the code no longer compiled.
If it did compile, I would run unit and integration tests. If all that succeeded I would delete the function. Or, in some cases, I would comment it out because the function was for debugging and I might want to bring it back some day.
I committed all the changes individually (or in groups) so that I could revert any one individually in the future if a problem was found. (Then I had to remind myself not to use the “squash” function in Github when I merged the final PR.
Test functions
One interesting case was RRstoRCs
. This function was unused by the main code, but the unit tests need it.
Therefore, I moved it to the appropriate _test.go
file.
deadcode
normally doesn’t examine the _test.go
files. Add the -test
flag to test those. It was interesting to see what warnings appeared/disappeared with that flag.
justMsgString
and possibly others fell into this category.
The difficult cases
Most of the deadcode
warnings were easy to fix. Some were more difficult. Two were impossible.
This was was interesting:
|
|
After studying the code and exprimenting with various potential fixes I had to give up. It turns out these two functions are layering violations that exists to facilitate integration testing. The layering violations are needed because of a bad design decision early on in DNSControl history.
I tried refactoring the functions themselves with no luck. It just doesn’t seem possible.
Is it a bug that deadcode
lists those with and without -test
? I’m not sure. I would expect the warning to go away with the -test
flag.
The remaining problems are all related:
|
|
The code related to those items is super complex. I poked around but decided it was too risky to remove them. I don’t have a test account on Loopia to verify my changes didn’t break anything.
I do have an awesome volunteer that maintains the Loopia plug-in, and they have a test account. I’ll be filing a bug asking them to look into it.
The .String()
function
I got a number of warnings about .String()
functions that were unused. I’m
very nervous about removing them, because while nobody is using them outright,
there might be fmt.Printf
statements that does.
I’m not sure if deadcode
is smart about printf’s so I left some of
them in. Others I commented out so I can bring them back easily.
Results:
Here are all the changes I made. This list includes both deadcode
and “unused
parameter” issues that a different utility raised.
|
|
What was the final reduction?
|
|
Congrats! The new version is 4k smaller!
Ok, that wasn’t the huge savings I was hoping for. At least the code is cleaner now. Plus, I found and fixed some bugs.
Final thoughts
I’m impressed by deadcode
. It is very fast and impressively accurate.
In this case, the size reduction was tiny. However the bugs that I happened to find while removing the dead code was a good thing.
Some functions were impossible to remove. As the help page says, “In any case, just because a function is reported as dead does not mean it is unconditionally safe to delete it.” You have to use your best judgement.
I’m unsure if .String()
functions are safe to remove. It would be nice if printf format strings were analyzed. I’m also not sure if it is a bug that PrepareCloudflareTestWorkers
is listed as dead when the -test
flag is included.
There’s no obvious place to file bug reports about deadcode
. The -h
page doesn’t tell you, nor does the the documentation.
It would be nice if there was a //nolint:
equivalent for deadcode
so that I don’t have to see warnings about the “impossible” functions that I don’t want to remove. Of course, I’d like a flag that ignores such comments.
If I ever do get to the point where deadcode
finds no issues, I’d add this to my CI/CD pipeline to warn me if new dead code appears. However I can’t do that until some kind of //nolint
option appears.
Bottom line: Dead code is bad. deadcode
is good.