{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"refactoring-and-codemods","title":"The task it is best at","url":"https://addaly.com/learn/how-software-changed/refactoring-and-codemods","position":44,"of":87,"minutes":9,"answer":"Mechanical change is the safest use of generation because the old version is a perfect oracle.","keyIdea":"Mechanical change is the safest use of generation because the old version is a perfect oracle, so keep behaviour-preserving work in its own verified commit and prefer a deterministic pattern tool wherever the change is a pattern."},"body":{"format":"markdown","text":"## Why refactoring is the ideal case\n\nEvery difficulty this course has described comes from the same root: you cannot cheaply tell whether the output is right. Refactoring removes that difficulty entirely. The requirement is *behave identically*, the reference implementation is in git, and the test suite is the oracle.\n\nSo mechanical change is where generated code is safest, most useful, and least discussed. Renaming across a repository. Extracting a module. Converting callbacks to promises to async. Migrating a test framework. Replacing a deprecated API across four hundred call sites. Splitting a 3,000-line file. Introducing a type where there was none.\n\nAll of these were previously bounded by typing speed and tedium, which is exactly the constraint that lifted.\n\n## Deterministic tools first\n\nBefore reaching for generation, check whether the change is expressible as a pattern. If it is, a syntax-aware rewriting tool will do it perfectly, on every occurrence, reviewably, and in seconds.\n\n```bash\n# ast-grep: structural search and replace, language-aware\nsg --pattern 'logger.warn($MSG)' --rewrite 'logger.warning($MSG)' -l py -U\n\n# comby: works across many languages\ncomby 'requests.get(:[url])' 'http.get(:[url])' .py -i\n```\n\nOther free options in the same family: `jscodeshift` for JavaScript and TypeScript, `libcst` and `bowler` for Python, OpenRewrite for Java, `gofmt -r` for Go, `rustfmt` alongside `cargo fix` for Rust.\n\nThe advantage is not speed. It is that a deterministic tool applies exactly one transformation and cannot invent a variation on occurrence 217. When you review a codemod diff you check the transformation once and the count; when you review a generated diff you check every occurrence, because they may differ.\n\nThe rule of thumb: **if it is a pattern, use a pattern tool; if it needs judgement, use a model; if it needs judgement in four hundred places, do it in four hundred small steps or reconsider.**\n\n## The discipline that makes it safe\n\nOne commit, behaviour-preserving, verified.\n\n1. **Confirm the tests are green and meaningful before you start.** A refactor verified by a weak suite is not verified. If the mutation score is low, this is the moment to add a few real tests, before the refactor, so they test the old behaviour.\n2. **Make the change with no behaviour modification at all.** Not one small improvement while you are in there. Not a fixed bug. Nothing.\n3. **Run the suite. Run a differential check if you have one.**\n4. **Commit alone, with a message saying it is behaviour-preserving.**\n5. **Only then** make the behaviour change, in its own tiny commit.\n\nThe payoff for the discipline arrives at review. A reviewer can read \"pure refactor, tests unchanged and green\" and skim, then read twenty lines of real change carefully. If the two are mixed, they must read everything carefully, which means in practice they will read nothing carefully.\n\nThe payoff arrives again when something breaks in a fortnight and `git bisect` lands on a commit that either changed behaviour or did not. That is a much better position than landing on a commit that did both.\n\n## Large migrations without a large diff\n\nFor changes too big for one review (a framework upgrade, an API replacement across a monolith) the strangler pattern still applies and works well here.\n\n- Add the new implementation beside the old one.\n- Route a small share of calls to it behind a flag.\n- Compare outputs, in shadow mode if the operation is read-only.\n- Move the share up.\n- Delete the old one, and delete the flag.\n\nEach step is small, reviewable and reversible. The whole migration might be twenty pull requests over two months, and every one of them is safe. This is slower than a single enormous change and it is the only approach with a good outcome distribution.\n\n## What to be careful about\n\n**A refactor that quietly changes behaviour** is the failure mode, and it is subtle by definition. Common instances: changing the order of side effects, converting a stable sort to an unstable one, altering exception types, changing when something is evaluated from eager to lazy, and normalising a value \"harmlessly\" on the way through.\n\nThe defence is a differential test rather than a careful read, because a careful read is exactly what these defeat.\n\n**A refactor is not free even when it is correct.** It rewrites history for `git blame`, invalidates people's mental maps, and conflicts with every open branch touching those files. Doing it because it is now cheap, rather than because something needed it, is a real cost imposed on everyone else. Cheap to produce is not the same as free to absorb, and that sentence is most of what this course has been about."},"check":{"question":"A single pull request converts a module to async and fixes a long-standing off-by-one in the same commit. Tests pass. Why is this hard to review even though the change is correct?","options":[{"text":"Async conversions frequently introduce concurrency bugs, so combining them with any other change compounds risk unnecessarily.","correct":false,"tempting":"Names a real hazard of the conversion itself, but the reviewability problem would exist even for a completely safe transformation."},{"text":"The tests passing proves only that the old behaviour was preserved, not that the off-by-one fix is correct.","correct":false,"tempting":"Partly right, but the fix should have had its own new test; the deeper problem is that no reviewer can locate it in the diff."},{"text":"Async code is harder to read than synchronous code, so the reviewer's capacity is consumed before reaching the fix.","correct":false,"tempting":"Attributes the problem to the style of the code rather than to the mixing of a mechanical change with a semantic one."},{"text":"There is no way to tell which lines are the mechanical conversion and which are the behaviour change.","correct":true}]},"prev":{"slug":"debugging-did-not-change","title":"Debugging still works the old way","url":"https://addaly.com/learn/how-software-changed/debugging-did-not-change"},"next":{"slug":"unfamiliar-language","title":"Shipping in a language you do not know","url":"https://addaly.com/learn/how-software-changed/unfamiliar-language"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'The task it is best at', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/refactoring-and-codemods (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"The task it is best at","courseTitle":"How Software Development Changed","module":{"index":5,"slug":"the-working-loop","title":"The working loop"},"answer":null,"keyIdea":"Mechanical change is the safest use of generation because the old version is a perfect oracle, so keep behaviour-preserving work in its own verified commit and prefer a deterministic pattern tool wherever the change is a pattern.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"why-refactoring-is-the-ideal-case","runs":[{"text":"Why refactoring is the ideal case"}]},{"t":"p","runs":[{"text":"Every difficulty this course has described comes from the same root: you cannot cheaply tell whether the output is right. Refactoring removes that difficulty entirely. The requirement is "},{"text":"behave identically","i":true},{"text":", the reference implementation is in git, and the test suite is the oracle."}]},{"t":"p","runs":[{"text":"So mechanical change is where generated code is safest, most useful, and least discussed. Renaming across a repository. Extracting a module. Converting callbacks to promises to async. Migrating a test framework. Replacing a deprecated API across four hundred call sites. Splitting a 3,000-line file. Introducing a type where there was none."}]},{"t":"p","runs":[{"text":"All of these were previously bounded by typing speed and tedium, which is exactly the constraint that lifted."}]},{"t":"h2","id":"deterministic-tools-first","runs":[{"text":"Deterministic tools first"}]},{"t":"p","runs":[{"text":"Before reaching for generation, check whether the change is expressible as a pattern. If it is, a syntax-aware rewriting tool will do it perfectly, on every occurrence, reviewably, and in seconds."}]},{"t":"code","lang":"bash","label":"Bash","text":"# ast-grep: structural search and replace, language-aware\nsg --pattern 'logger.warn($MSG)' --rewrite 'logger.warning($MSG)' -l py -U\n\n# comby: works across many languages\ncomby 'requests.get(:[url])' 'http.get(:[url])' .py -i"},{"t":"p","runs":[{"text":"Other free options in the same family: "},{"text":"jscodeshift","code":true},{"text":" for JavaScript and TypeScript, "},{"text":"libcst","code":true},{"text":" and "},{"text":"bowler","code":true},{"text":" for Python, OpenRewrite for Java, "},{"text":"gofmt -r","code":true},{"text":" for Go, "},{"text":"rustfmt","code":true},{"text":" alongside "},{"text":"cargo fix","code":true},{"text":" for Rust."}]},{"t":"p","runs":[{"text":"The advantage is not speed. It is that a deterministic tool applies exactly one transformation and cannot invent a variation on occurrence 217. When you review a codemod diff you check the transformation once and the count; when you review a generated diff you check every occurrence, because they may differ."}]},{"t":"p","runs":[{"text":"The rule of thumb: "},{"text":"if it is a pattern, use a pattern tool; if it needs judgement, use a model; if it needs judgement in four hundred places, do it in four hundred small steps or reconsider.","b":true}]},{"t":"h2","id":"the-discipline-that-makes-it-safe","runs":[{"text":"The discipline that makes it safe"}]},{"t":"p","runs":[{"text":"One commit, behaviour-preserving, verified."}]},{"t":"ol","start":1,"items":[[{"text":"Confirm the tests are green and meaningful before you start.","b":true},{"text":" A refactor verified by a weak suite is not verified. If the mutation score is low, this is the moment to add a few real tests, before the refactor, so they test the old behaviour."}],[{"text":"Make the change with no behaviour modification at all.","b":true},{"text":" Not one small improvement while you are in there. Not a fixed bug. Nothing."}],[{"text":"Run the suite. Run a differential check if you have one.","b":true}],[{"text":"Commit alone, with a message saying it is behaviour-preserving.","b":true}],[{"text":"Only then","b":true},{"text":" make the behaviour change, in its own tiny commit."}]]},{"t":"p","runs":[{"text":"The payoff for the discipline arrives at review. A reviewer can read “pure refactor, tests unchanged and green” and skim, then read twenty lines of real change carefully. If the two are mixed, they must read everything carefully, which means in practice they will read nothing carefully."}]},{"t":"p","runs":[{"text":"The payoff arrives again when something breaks in a fortnight and "},{"text":"git bisect","code":true},{"text":" lands on a commit that either changed behaviour or did not. That is a much better position than landing on a commit that did both."}]},{"t":"h2","id":"large-migrations-without-a-large-diff","runs":[{"text":"Large migrations without a large diff"}]},{"t":"p","runs":[{"text":"For changes too big for one review (a framework upgrade, an API replacement across a monolith) the strangler pattern still applies and works well here."}]},{"t":"ul","items":[[{"text":"Add the new implementation beside the old one."}],[{"text":"Route a small share of calls to it behind a flag."}],[{"text":"Compare outputs, in shadow mode if the operation is read-only."}],[{"text":"Move the share up."}],[{"text":"Delete the old one, and delete the flag."}]]},{"t":"p","runs":[{"text":"Each step is small, reviewable and reversible. The whole migration might be twenty pull requests over two months, and every one of them is safe. This is slower than a single enormous change and it is the only approach with a good outcome distribution."}]},{"t":"h2","id":"what-to-be-careful-about","runs":[{"text":"What to be careful about"}]},{"t":"p","runs":[{"text":"A refactor that quietly changes behaviour","b":true},{"text":" is the failure mode, and it is subtle by definition. Common instances: changing the order of side effects, converting a stable sort to an unstable one, altering exception types, changing when something is evaluated from eager to lazy, and normalising a value “harmlessly” on the way through."}]},{"t":"p","runs":[{"text":"The defence is a differential test rather than a careful read, because a careful read is exactly what these defeat."}]},{"t":"p","runs":[{"text":"A refactor is not free even when it is correct.","b":true},{"text":" It rewrites history for "},{"text":"git blame","code":true},{"text":", invalidates people’s mental maps, and conflicts with every open branch touching those files. Doing it because it is now cheap, rather than because something needed it, is a real cost imposed on everyone else. Cheap to produce is not the same as free to absorb, and that sentence is most of what this course has been about."}]}],"check":{"question":[{"text":"A single pull request converts a module to async and fixes a long-standing off-by-one in the same commit. Tests pass. Why is this hard to review even though the change is correct?"}],"options":[{"text":[{"text":"Async conversions frequently introduce concurrency bugs, so combining them with any other change compounds risk unnecessarily."}],"correct":false,"tempting":[{"text":"Names a real hazard of the conversion itself, but the reviewability problem would exist even for a completely safe transformation."}]},{"text":[{"text":"The tests passing proves only that the old behaviour was preserved, not that the off-by-one fix is correct."}],"correct":false,"tempting":[{"text":"Partly right, but the fix should have had its own new test; the deeper problem is that no reviewer can locate it in the diff."}]},{"text":[{"text":"Async code is harder to read than synchronous code, so the reviewer’s capacity is consumed before reaching the fix."}],"correct":false,"tempting":[{"text":"Attributes the problem to the style of the code rather than to the mixing of a mechanical change with a semantic one."}]},{"text":[{"text":"There is no way to tell which lines are the mechanical conversion and which are the behaviour change."}],"correct":true,"tempting":null}],"order":[2,3,0,1],"why":null},"prev":{"slug":"debugging-did-not-change","title":"Debugging still works the old way","minutes":9},"next":{"slug":"unfamiliar-language","title":"Shipping in a language you do not know","minutes":9}}}