fun-refactor

Design

Every answer carries
how it was reached.

A refactoring tool that resolves a reference wrongly edits the wrong code, and the result usually compiles. This one attaches the resolution rule to every answer and rewrites only the two strongest tiers.

The alternatives

Find-and-replace covers every language and parses none. It renames the string in a comment, a substring inside another identifier, and the same-named field on an unrelated type.

A language server covers one language and stops at its edge: not Helm charts, Terraform modules or stylesheets, and not a file that does not currently compile.

This tool

It builds one index across nineteen languages out of syntax alone. A named rule resolves each reference: the scope chain, an import, the enclosing package. Every answer carries which rule answered it.

It rewrites the references it resolved exactly or through an import, and lists the rest: weakly resolved sites, names appearing in strings, files that did not parse. It prints the diff, then the list.

fr capabilities prints the live language support matrix.

Defects found by running it on real repositories

Each of these lived in this tool, produced output that parsed, and turned up when somebody ran it over an existing codebase instead of a fixture.

A guard clause that moved code out from under its guard

Go's grammar puts a statement_list between a block and its statements. Counting it as a statement made every block look like a block of one. The check "is this if the last thing here?" then passed for an if with code after it, and the rewrite hoisted that code out from under the condition guarding it.

The result parses. No syntax check catches it. Over 250 files of helm's pkg/services, 1,258 of 1,498 applications were wrong, or 84%. The fix refuses those and applies the remaining 240.

A de Morgan rewrite that dropped its own brackets

!(a && b) is one operand; !a || !b is two, and the brackets that held it together left with the negation. Inside another operator, x && !(a && b) became x && !a || !b: valid, and a different program.

References that were never found at all

A Go package is a directory: code in one file calls a function in the next with no import and no qualifier. The index treated only Terraform that way, so fr refs returned zero for symbols helm calls from the file next door. Dead-code analysis reported 238 internal Go symbols as dead where 47 are. A rename rewrote the definition and filed the call sites it could not see under "unresolved".

That last one reported the gap instead of rewriting wrongly. It was still a bug, and it is fixed.

Where it fits

Keep rust-analyzer and gopls. They read types; this reads syntax, and syntax stops where a receiver's type would tell you which method you meant. At that point the tool says field-based and leaves the line alone.

What it has that they do not:

  • One index over the whole repository, whatever the file is written in.
  • Configuration as a first-class language: a Helm values key knows which templates read it, and a rename moves them.
  • Answers that survive a file that does not compile, because it never needed the compiler.
  • An explicit account of what it did not do, every time.