I love removing unused code. There’s nothing better than being able to delete an entire complicated branch of logic when cleaning up a feature flag. Oftentimes though, you don’t quite get all of it, and remnants of unused code can build up throughout your codebase. If it’s not referenced anywhere, it’s technically not that big of a deal to your application. However, you can easily get into situations where groups of files, tests, and even dependencies, are sitting around in your project for absolutely no reason.

Recently, I found a tool called Knip, which has significantly improved my ability to find not only unused code, but also unused dependencies and tests. I was a former user of the now discontinued tool ts-prune, and found it extremely helpful when cleaning up a codebase, however Knip takes it to the next level. I’ve been able to clean up significant numbers of entirely unused files, tests, dependencies, and functions across both work and personal projects, all thanks to this tool.

Why I prefer it over ts-prune

Using Knip’s --production mode I was able to inspect workspaces in repos, and determine what code wasn’t actually in use in production outputs. This made it easy to not only find unused code, but also find unused code that had unit tests attached. In a work codebase, I found that not only were there a large number of test files that were testing entirely dead code, two of the slowest running test files were in that list. I’d finally found a situation where I could justifiably delete test files to speed up test runners 😅.

Due to Knip detecting usage of various tools within the codebase, I also found it significantly reduced the number of false positive cases. I feel if tuned correctly, Knip would make a very useful CI check for PRs due to this.

The Caveat

The one issue I did run into however, is not with the Knip tool itself, and more an issue with the way code is written in general. In larger codebases, it can be common to work on components and business logic outside of the production app before hooking it all together. This work-in-progress code is usually used through a tool such as Storybook, or just tested via unit tests.

Knip correctly identifies that this code is unused, but it doesn’t know that it’s actively being developed. This makes it a false positive that’s relatively hard for Knip to identify, because as far as it’s aware it’s not a false positive, the code is actually unused. As with all false positives, this makes it hard to use within automated checks such as CI, and adds extra mental overhead when manually using the tool to identify files to cleanup. Having to check that every file I go to delete hasn’t been modified recently significantly slows down the process, especially if it hasn’t been run in a while and the number of reported unused files is high.

Using preprocessors to filter reporting output

Recently a feature was added to Knip that allows for custom preprocessors to filter entries out of the report. To solve the above-mentioned problem, we can write a preprocessor that filters out any files that have been modified by git recently. This actually ends up being remarkably simple;

import { promisify } from 'node:util';
import { exec as execSync } from 'node:child_process';

const exec = promisify(execSync);

async function isRecentlyModified(path) {
    // Ask git when this file was "changed"
    const { stdout } = await exec(`git log -1 --format="%ad" -- ${path}`);
    const lastModified = new Date(stdout);

    // Check if it's been modified within the past week
    return Date.now() - lastModified.getTime() >= 1000 * 60 * 60 * 24 * 7;
}

const gitPreprocessor = (report) => {
    const { files } = report.issues;

    // Little messy but this is significantly faster via Promise.all
    const fileFilterResults = await Promise.all([...files].map(async file => [file, await isRecentlyModified(file)]));
    const filteredFiles = fileFilterResults.filter(([_, isRecentlyModified]) => isRecentlyModified).map(([file, _]) => file);

    return {
        ...report,
        issues: {
            ...report.issues,
            files: new Set(filteredFiles)
        }
    }
}

export default gitPreprocessor;

Running this preprocessor with Knip, via knip --preprocessor ./preprocessor.js will not include any files modified within 7 days in the "unused files" section of the report. This makes it much easier to identify files that are actually unused, and not ones that are in active development. This could be extended to include the other report types too relatively easily, but for the sake of example I’ve only included the unused files report. I've published the example preprocessor I wrote on GitHub and do plan on improving it over time, as I believe this sort of preprocessor has a fairly wide use case.

Conclusion

While this preprocessor is extremely useful for me, I'm sure there are other cases that make sense across various codebases that would make this tool even more useful for you too. Overall, Knip preprocessors are a great way to make this already powerful tool even more useful for your specific use case. I highly recommend checking it out if you haven't already.

About the Author

Maddy Miller

Hi, I'm Maddy Miller, a Senior Software Engineer at Clipchamp at Microsoft. In my spare time I love writing articles, and I also develop the Minecraft mods WorldEdit, WorldGuard, and CraftBook. My opinions are my own and do not represent those of my employer in any capacity.