Mechanical vs structural
Some code changes can be verified by reading the diff. Others require understanding the code. The split matters.
Partway through a backlog of static analysis findings, I noticed the list was not one pile but two. Some fixes could be described once and applied everywhere. Others needed understanding of the surrounding code before I could touch them.
Classify first, fix second
The instinct with a large backlog is to start fixing, and that's what I did. However, when the split became clear I backed out and started categorising before touching anything else.
The distribution was heavily skewed. A small number of rules accounted for most of the findings. Most were mechanical: API renames, operator modernisation, duplicate imports, redundant fragments.
Once I started grouping, I stopped thinking in terms of individual rules and started thinking in terms of risk. Mechanical fixes were the ones I could describe once, apply everywhere, and verify from the diff. The rest changed control flow or moved assumptions around, and a passing glance at the diff was not enough to confirm correctness.
What makes a change mechanical
A mechanical fix is one you can describe as a rule that applies to all instances, without needing to understand the surrounding code.
Replace foo && foo.bar with foo?.bar. Merge two import statements from the same module into one. Update a deprecated API call to its modern equivalent. These are text transformations. They can be verified by reading the diff line by line. Because they don't change logic, they don't introduce logic bugs on their own.
That does not make them risk-free. Renaming a deprecated import is a textbook mechanical fix, but if the file is loaded by a build plugin that matches on the old name, the build breaks. The transformation was correct, but the build configuration did not expect it. Mechanical changes can have environmental side effects even when the code change itself is sound.
What makes a change structural
A structural change requires understanding context to apply correctly. The change itself is usually necessary. The complexity comes from the consequences.
React requires hooks to be called in the same order on every render(opens in a new tab). A component that calls hooks after an early return is a bug, even if it happens to work today. The fix is straightforward: move the hooks above the return. But after the move, every hook runs unconditionally, and dependencies that were safe behind a guard now need null checks throughout. The rule violation has to be fixed. The question is what else changes when you fix it.
Extracting helpers from complex functions is structural for a different reason. A function that has grown too large to reason about is a real problem, and breaking it into smaller pieces is the right response. But closure variables become parameters. Implicit dependencies become explicit. The complexity moves rather than shrinks. Tests for the extracted helpers are cheap to write (they are pure functions) and will flag regressions, but those tests only exist because the extraction created a new contract that did not exist before.
Why the split matters
Working through the backlog, this reminded me of reading Fowler's Refactoring(opens in a new tab) years ago. His split is between behaviour-preserving transformations and the rest. The cut here is a little finer: not whether behaviour is preserved, but whether correctness can be verified from the diff alone. "Extract function" preserves behaviour in Fowler's sense, but the diff alone doesn't prove the extracted helpers closed over the right variables.
The mechanical phase builds confidence. It touches many files, produces a large diff, and passes CI on the first try. If a bulk mechanical pass goes clean, that is evidence the codebase is well-enough tested to support the structural phase.
The structural phase consumes that confidence. Moving hooks, extracting functions, and restructuring conditionals all carry the risk of subtle behaviour changes. Without the mechanical phase proving the safety net is solid, the structural work would need more manual testing and slower iteration.
Mechanical changes should not introduce logic bugs. Structural changes can. Review effort should be proportional to the structural content of the diff, not the total line count. A 2,000-line diff that is 80% mechanical and 20% structural needs most review attention on the 20%.
Where the line blurs
Some fixes sit between the two categories. A static analysis tool might flag if (!x) { A } else { B } and suggest flipping it to if (x) { B } else { A }. The transformation is textually mechanical, but a reviewer still has to verify that A and B were swapped correctly. And sometimes the negated form reads more naturally than the positive one. A finding that is mechanically simple to apply but debatable whether to apply at all is not really mechanical.
The categories are a planning tool, not a taxonomy. The point is to separate the work you can do quickly and safely from the work that requires thought.
Beyond cleanup
The mechanical/structural split is not specific to static analysis cleanup. Feature work has it too. Adding a new prop to a component is mechanical if the component just passes it through. It is structural if the component makes decisions based on it.
The discipline is in drawing the line before starting, not after. Once you are in the middle of a change, the temptation is to fix "one more thing" that crosses the boundary.