Some bugs survive everything. The build is green, the tests pass, the reviewer approves, and the user still sees something broken. I spent the last weeks teaching a tool to find a few of them, and tested every rule against 19 public Angular and AngularJS codebases pinned at fixed commits. Here is what came out, and how to check your own app in a minute.
1. The translation key that doesn't exist
message: this.translateService.instant('walletconnect.error'),
The English bundle has walletconnect.connection_error, not walletconnect.error. When a lookup fails, ngx-translate (and Transloco) render the key itself, so the user reads walletconnect.error on the error page. The compiler can't see it, the key is just a string. A test that renders the component passes too.
Checking this by hand stops working the moment an app loads more than one bundle. Feature bundles usually declare the same top-level namespace with different children, so the bundles have to be merged deeply per language before any lookup means something. A shallow merge makes keys look missing that are not.
The same app had four more: a key no language has, and three that only exist in English.
2. The label that points nowhere
<label class="text-sm text-gray-600" for="currentPassword">Current password</label>
<input class="input-text" [(ngModel)]="currentPassword" name="currentPassword" type="password" required>
It looks fine and it renders fine. But for points at an id the input doesn't have, so the label is attached to nothing. Clicking it doesn't focus the field, and a screen reader announces the field without its name. That fails WCAG 1.3.1 and 4.1.2, level A, which since June 2025 the European Accessibility Act asks a lot of businesses to meet.
Across the 19 codebases, every repository but one had at least one defect of this kind. The most common were (click) on a div with no keyboard handler, and images without alt.
3. The test that checks nothing
it('should replay the last emitted value', () => {
testScheduler.run(({ cold, expectObservable }) => {});
});
The name promises a check, the body has none. It passes on every run, shows up green in every report, and counts towards coverage. Empty tests usually start as a placeholder or lose their body in a refactor, and nobody notices because nothing ever fails.
The same check also finds the subtler kind: an expect() inside a subscribe() on an asynchronous source, in a test that takes no done, returns no promise and flushes no timers. The test returns before the value arrives, so the assertion never runs inside it.
Why these three
They share one property: nothing in the normal toolchain looks at them. TypeScript sees strings and objects. The test runner sees a test that returned. The reviewer sees code that looks right. That's the gap I built ng-checkup for. It reas an Angular codebase as text and syntax trees, never builds or runs it, and reports what blocks the next upgrade and what nobody else notices.
I've sent fixes for all three: vendure storefront #97, airgap-wallet #350 and rx-angular #1972.
Check your own app
npx ng-checkup-cli
No install, no config. It needs Node 22, works on a fresh clone without node_modules, and reads your code without running any of it. For the full report as one page:
npx ng-checkup-cli --report checkup.html
To keep it from getting worse without fixing everything first, record today's numbers and fail CI only when one goes up:
npx ng-checkup-cli baseline
npx ng-checkup-cli --fail-on regression
On a pull request, --since origin/main keeps only the findings in the files your branch changed.
The translation and accessibility checks never change the score. They're findings and baseline measures, so adopting them on a large app doesn't turn the dashboard red on day one, but CI fails the day a new one lands.
It also ships as a GitHub Action, a GitLab CI/CD component, a VS Code extension and an MCP server for Claude Code, Cursor and Copilot. It's MIT licensed. If a rule gets something wrong on your code, I'd like to hear about it: every rule is tested against those 19 codebases, and a false positive is a bug.
Top comments (0)