Strengths and Limitations of Knip for Unused Code Detection in Angular
As the codebase grows and evolves, the cost of maintaining it becomes higher. In extreme cases, leading to situations where upkeep takes significantly more time and effort than new developments. While you cannot prevent balancing out of those proportions overtime, there are ways to mitigate the risk of ending up in an endless maintenance loop. One of such is identification and removal of unused or overhead code to decrease complexity.
This article will delve into the capabilities of Knip tool that identifies unused code in Angular based applications. It advertises itself as a tool that “finds unused files, dependencies and exports in your JavaScript and TypeScript projects. Less code and dependencies lead to improved performance, less maintenance and easier refactorings”. Gaining popularity in the JavaScript ecosystem based on NPM weekly downloads.
To fully understand Knip’s effectiveness, we will comprehensively evaluate it in real-world application. In the upcoming section, we will evaluate its performance in identifying unused code in the Adscore project, which we are developing and maintaining at Iterative. Additionally, we will analyze Knip’s ability to navigate complex code within a sandbox Angular project in a controlled environment. This two-pronged approach will check Knip’s versatility and provide insights into its capabilities in Angular codebases.
Real world project
Adscore allows users to categorize web traffic into distinct buckets like organic, paid, bot-generated, and low-quality. The app acts as a shield for ad budgets. The detailed comprehension guarantees that resources are directed towards the intended audience - real users - thereby maximizing campaign efficiency and return on investment.
Projects Statistics
The following statistics provide an idea of the size of the Adscore system.
Language | Files | Lines of code |
---|---|---|
TypeScript | 248 | 26995 |
SCSS | 112 | 16535 |
HTML | 78 | 12822 |
The total count of Components, Pipes, and Directives is 74.
Ignore File
After several iterations due to Knip incorrectly detecting in-use packages, following ignore file was created:
{
"angular": {
"config": ["angular.json"]
},
"ignore": [
// both files used in angular.json file
"Karma.conf.js",
"src/polyfills.ts",
],
"ignoreDependencies": [
// used in angular.json
"@angular-eslint/schematics",
"@angular-eslint/template-parser",
"eslint-config-prettier",
"jasmine-core",
"Karma-chrome-launcher", // used by Karma, several more plugins enlisted but removed for brevity
// imported in global styles.scss, Knip lacks Sass plugin at the time of writing
"loaders.css",
"husky", // used in package.json
"pretty-quick", // used by husky
"simple-line-icons" // used in custom script
],
}
Some dependencies are used in the angular.json
file, some in .sass
files. This is something that can be improved in the plugins used by Knip.
Knip Scan Results Summary
Knip created following report after creation of ignore file (Knip provides list of unused components for each section, those details are removed for brevity):
Unused files (11)
Unused dependencies (3)
Unused devDependencies (5)
Unused exports (10)
Unused exported types (1)
Configuration issues (1)
In total, Knip allowed us to detect and remove 435 lines of code (which is 1.61% of total LOC) and 16 files.
While integrating Knip with our Angular project, optimizing the knip.jsonc file required multiple iterations to ensure accurate detection of unused code. However, this configuration effort was a one-time investment; moving forward, we can seamlessly integrate Knip into our post-refactoring workflow for recurring analysis. While inclusion in the automated pipeline is pending further evaluation, Knip has already proven its potential as a valuable tool for our codebase optimization. Despite Knip’s detection capabilities, we identified additional unused assets that require further investigation. We’ll be exploring this scenario within a controlled sandbox environment to gain deeper insights.
Sandbox Project
A project containing unused code has been prepared. The objective is to identify all unused cases, which are:
- 7 class properties
- 10 class methods
- 2 interface properties
- 2 variables
- 1 constant
- 5 classes (Angular’s: Component, Directive, Module, Pipe, Service)
- 1 dependency (
@angular/forms
- installed by default but unused in the project, andexpress
) - 1 devDependency (
express
) - 1 unused export (variable used locally)
- 2 NgRx Actions (functions)
- 1 NgRx Effect (class)
- 2 functions - one is unit tested
- 1 asset
Total: 36
Code is available at: https://github.com/IterativeEngineering/blog.angular-knip-unused-code-article
Intentional Challenges
Some of the parts of the code are directly unused, which should be easy to detect by the tool. Some are trickier like for example: an interface has a property, then the object implementing the interface, has the property defined, but property itself is unused as in the following snippet:
export interface InUseFeatureState {
definedButUnusedState: string;
}
const initialState: InUseFeatureState = {
definedButUnusedState: 'unused',
}
Ideally, an unused interface property should be detected on a first run. But iteratively also will do the job:
- Run the tool
- Identify and remove the detected code
- Run the tool again
- Repeat until no more unused code is detected
Analyzing Knip Results
Tested with following versions:
- Angular - v17.0.5
- Knip - v5.0.0
Command used:
npx knip --include classMembers
Since Knip version 4, class members are opted out by default, but given Angular’s heavy class usage, enabling it seemed beneficial.
Result of the first Knip scan (details of each section removed for brevity):
Unused files (6)
Unused dependencies (3)
Unused devDependencies (8)
Unlisted binaries (2)
Unused exports (4)
Unused exported class members (16)
Let’s review each section’s outcome individually.
Unused files
No | File | Detection | Remarks |
---|---|---|---|
1 | unused-function.ts | Correct | Even when in barrel file (index.ts) Knip was able to detect this file correctly. |
2 | unused.service.ts | Correct | Service is exported but it is unused |
3 | unused-type.ts | Correct | Types are exported but not used anywhere in the code |
4 | unused.util.ts | Correct | Util is exported but not used anywhere in the code |
5 | unused-feature.module.ts | Correct | Module is exported but not used anywhere in the cod |
6 | .eslintrc.js | Incorrect | Used by ES Lint |
Detected: 5 files of 6 were detected correctly.
Unused dependencies
No | Dependency | Detection | Remarks |
---|---|---|---|
1 | @angular/compiler | Correct | Not directly used in the project, but for example @angular-devkit/build-angular depends on it, so it will be installed anyway in package-lock.json |
2 | @angular/forms | Correct | Forms are installed by Angular by default, but this test project does not use them. |
3 | @angular/platform-browser-dynamic | Correct | Not directly used in the project but other Angular liblaries depends on it, will be installed anyway in package-lock.json |
4 | unused.util.ts | Correct | Util is exported but not used anywhere in the code |
5 | unused-feature.module.ts | Correct | Module is exported but not used anywhere in the code |
6 | .eslintrc.js | Incorrect | Used by ES Lint |
Detected: 1 of 1 – 2 other dependencies have been detected as unused, but are used by Angular, won’t be included in the final score.
Unused devDependencies
No | Dependency | Detection | Remarks |
---|---|---|---|
1 | express | Correct | Unused in the project |
2 | husky | Incorrect | Used in package.json |
3 | jasmine-core | Incorrect | Installed by Angular and used during testing, but can be removed because it will be installed as a sub-dependency |
4 | karma-chrome-launcher | Incorrect | Installed by Angular, cannot run tests without it |
5 | karma-coverage | Incorrect | Can be used by Karma to generate coverage report, installed by default by Angular |
6 | karma-jasmine | Incorrect | Used by Karma |
7 | karma-jasmine-html-reporter | Incorrect | Used by Karma |
8 | lint-staged | Incorrect | Used by Husky |
Detected: 1 of 1 - All detected but with false positives.
Unlisted binaries
No | Binary | Detection | Remarks |
---|---|---|---|
1 | eslint | Incorrect | Listed as script in package.json and run by lint-staged |
2 | prettier | Incorrect | Listed in lint-staged script |
Detected: 0 of 0 - Did not expect to find any binaries, as the code was not prepared for this case. Anyway all were false positives.
Unused exports
No | Export | File | Detection | Remarks |
---|---|---|---|---|
1 | UNUSED_CONSTANT | constants.ts | Correct | Constant is exported but unused |
2 | unusedAction1 | in-use-feature.actions.ts | Correct | Action is exported but unused |
3 | unusedSelector1 | in-use-feature.selectors.ts | Correct | Selector is exported but unused |
4 | rootState | in-use-feature.selectors.ts | Correct | Selector is exported but only used locally |
Detected: 4 of 4 - Found one expected export and 3 unused Actions and Selectors.
Unused exported class members
The biggest part of the Knip result, and most likely the biggest part of any Angular application.
No | Class Member | File | Detection | Remarks |
---|---|---|---|---|
1 | unusedEffect$ | InUseFeatureEffects | Correct | |
2 | unusedEmitAction | InUseFeatureStateFacade | Correct | |
3 | unusedProperty | InUseFeatureStateFacade | Correct | |
4 | unusedProperty | InUseComponent | Correct | |
5 | unusedMethod | InUseFeatureStateFacade | Correct | |
6 | unusedPublicMethod | InUseComponent | Correct | |
7 | unusedProperty | UnusedComponent | Correct | |
8 | inUseMethod | UnusedComponent | Correct | Partially correct, because component is unused, but this variable is used in HTML (which was not recognized) |
9 | unusedPublicMethod | UnusedComponent | Correct | |
10 | inUseProperty | UnusedComponent | Correct | Partially correct, because component is unused, but this variable is used in HTML (which was not recognized) |
11 | unusedMethod | InUseService | Correct | |
12 | inUseMethod | AppComponent | Incorrect | Used in HTML file |
13 | inUseAction1Effect$ | InUseFeatureEffects | Incorrect | Used by NgRx |
14 | inUseProperty | InUseComponent | Incorrect | Used in HTML file |
15 | inUseStoreProperty$ | InUseComponent | Incorrect | Used in HTML file |
16 | inUseMethod | InUseComponent | Incorrect | Used in HTML file |
Detected: 11 of 17
Summary of first run
In its initial run, Knip successfully identified 22 out of 36 cases, demonstrating a promising detection rate. However, it’s important to note that 14 of the findings were classified as false positives, highlighting the need for further refinement.
Code removed after the first run can be found in clean-up-after-knip-v5-detection
branch.
Subsequent launches
Knip’s second run yielded an additional 2 detections:
- inUseActionInUnusedEffect
- unusedSelector2
Total identified cases: 24 out of 36. Subsequent launches did not detect any additional code.
Missed Parts
Let’s check which scenarios were not detected:
- UnusedPipe - most probably not found due to being defined in Angular Module which is in use
- UnusedPath - same as above
- UnusedComponent - same as above
- UnusedDirective - same as above
- unusedUtilButTestedFunction - function is unused, but unit tested - most probably because Knip does not have (unlike Jest) plugin for Jasmine it recognized this function as in use
- unusedPrivateProperty - Haven’t been detected, but ESLint or IDE’s like Visual Studio Code are capable to mark this case as unused
- unusedPrivateProperty2 - same as above
- unusedPrivateMethod - same as above
- definedButUnusedState - same as above
- unusedState - same as above
- unused.svg - looks like assets files were not checked at all
A Summary of Knip’s Efficiency with Angular
Understanding Knip’s capabilities and limitations is crucial for leveraging its power in Angular. Let’s explore its current performance, areas for improvement, and practical tips to optimize your code analysis process.
Strengths:
- Detects unused functions and exports efficiently
- Can help reduce code complexity and improve maintainability
Weaknesses:
- May require multiple iterations for complex projects
- In the context of Angular:
- Cannot find unused Components, Pipes etc if defined in Module because, and currently does not analyze HTML templates
- Can detect unused Actions/Selectors (NgRx) but not Effects/Reducers
- Detects class properties used in HTML templates as unused
- Doesn’t fully support Angular’s default test framework (Jasmine)
- May misinterpret certain devDependencies as unused
- Dependencies used only in Sass files are falsely detected as unused
- Assets are not checked
- Package.json scripts: limited detection in scripts, it didn’t detect
Husky
andlint-staged
- Unit testing: May not detect unused code if tested with unsupported test frameworks
Tips for Efficient Usage:
- Review and remove unused Angular’s router paths before running Knip to improve Module detection.
- Consider alternative test frameworks supported by Knip for broader detection - Since Angular 16, Jest has experimental support
- Be aware of limitations specific to your technology stack and adjust workflow accordingly.
Additional Notes:
- Knip’s Angular support is evolving, so future updates might address some limitations
- Explore alternative code analysis tools that might complement Knip and address its shortcomings