Compatibility
Published
If you ship a command line tool, a linter, a migration assistant or an SDK that reads code, then somewhere inside it a parser hands you a tree of objects you did not create. Every node in that tree is a dictionary whose key names belong to a specification, and every visitor you pass to a traversal library is a dictionary whose key names belong to that library. Two external contracts, both made of property names, in the one kind of program most likely to be protected before shipping.
What was measured
One file built on the real installed parser and traversal library rather than stand-ins. It parses a small function, checks the shape of the program, reads the function name and parameter names, walks the tree collecting identifiers, slices the source using a node's recorded position, runs a codemod that renames one identifier, runs a lint rule that counts declarations, tokenises the same source, and prints the summary a build would log.
The base column is clean on all five protection profiles. The parse, the walk, the codemod and the token list all reproduced exactly. Protecting a tool that reads code does not stop it reading code.
The member column matches names on the two contracts: the node shapes the parser writes, and the visitor keys the traversal library reads. Both were measured on both member-renaming profiles.
A linter that reports your file is clean
Renaming the node type key is the arm to understand first, because everything a tool does with a tree begins by asking what kind of node it is looking at.
The walk still ran. The traversal library visited every node, and the visitor's test for the node kind never matched anything, so the identifier count went from seven to zero. The lint rule that counts variable declarations counted none, and its verdict went from recommending a change to reporting the file clean. The codemod's report of whether it renamed anything went from true to false. The summary printed at the end still said the run was fine.
Nothing threw. No node was skipped by accident, no file failed to parse, no exit code changed. A tool in this state answers every question with the most reassuring possible answer, which is the worst property a tool can have. A linter that says clean, a migration that says nothing to migrate, and a scanner that says no findings are indistinguishable from a healthy run of the same commands.
That matters most where these tools are usually pointed: at somebody else's codebase, during an upgrade, as evidence that a change is safe.
The visitor is the same failure from the other side
A traversal library reads the visitor object you hand it, looking for the entry and exit callbacks by name. That makes your object the dictionary and the library the reader, which is the mirror of the node contract.
Renaming those two names produced exactly the same output as renaming the node kind: zero identifiers, a clean verdict, a codemod that reports no changes, and a summary that says the run was fine. The library walked the entire tree and called nothing, because it looked for the callbacks under names your build had rewritten.
Two different contracts, opposite directions, indistinguishable symptom. Neither one produces an error message that would send anybody to look at the build step.
Where it throws instead, and why that is the easy case
Not every arm was silent. Renaming the container names on a node, the list of statements and the parameter list, threw a TypeError on the first read of a length. Renaming the recorded source positions threw when the code sliced the original text using them, because an index read off an undefined range has nothing to read.
Both are the good failure mode, and both follow the pattern this series keeps measuring: a container read without a guard is loud, a leaf read is silent. It is the leaves that carry the meaning here. Renaming the identifier name key produced a walk that collected seven identifiers and printed them as seven empty strings, and a codemod that matched nothing because the name it compares against was undefined on every node. Renaming the token value key emptied every token in the same way.
So a tool built on this stack fails loudly when it touches structure and quietly when it touches content, and content is where all the decisions are made.
The report your pipeline reads is a contract as well
The last arm matched the keys of the summary the tool prints at the end of a run, which is a self-owned object and was included as a control. It behaved exactly as the output-side rule predicts: the emitted document came out with generated keys in place of the file, identifier count, declaration count and status fields.
Everything inside the process is still consistent, because both ends of every in-file read moved together. Only the emitted document changed, and the reader of that document is a pipeline step, a dashboard, or a script that gates a merge. The most likely outcome is that the gate reads a missing status field and treats it as a pass, which is a presence check failing exactly the way presence checks fail throughout this series.
If your tool writes machine-readable output, that output is an external contract with the same rules as any other. It deserves the same exclusion and the same verification.
What to do about it
Keep the parser's node vocabulary and the traversal library's visitor keys out of the member pattern. In practice that means node kind, name, value, the statement and parameter containers, the declaration list, the recorded positions and the entry and exit callbacks. As always, anchoring the member pattern to a private prefix does this without an exclusion list that has to be maintained against every library upgrade.
Then verify the way this file does. Print a value the tool extracted, such as the function name and the identifier list, and compare it against text you typed. Print a count, such as the number of declarations found, and compare that against a number you typed. Both checks are one line, and both caught arms in this measurement that an exit code and a successful parse did not.
The general rule for anybody shipping developer tooling is worth stating plainly, because tooling is disproportionately affected: a program whose entire job is to read other people's data structures is a program made almost entirely of external contracts. That is not an argument against protecting it. It is an argument for a narrow, prefix-anchored member pattern and a smoke test that reads one real answer out of a real run.
Frequently asked questions
Does obfuscation break AST tools and codemods?
Not in the default configuration. A tool built on the real installed parser and traversal library, doing shape checks, a walk, a codemod, a lint rule and tokenisation, reproduced its output exactly on all five protection profiles measured. It becomes a surface only when member renaming is on and the pattern matches the node vocabulary or the visitor keys.
Why does my linter report a clean file after protection?
Because the test for what kind of node it is looking at never matches. In the measured run the walk visited every node, matched nothing, counted zero identifiers and zero declarations, and reported the file clean with a summary marked ok. Nothing was thrown and the exit code did not change.
Can renaming the visitor object cause the same thing?
Yes, and it is indistinguishable. The traversal library looks for the entry and exit callbacks by name, so renaming them made it walk the whole tree and call nothing, producing the same zero counts and the same clean verdict as renaming the node kind. Two contracts, opposite directions, one symptom.
Which parts of an AST tool fail loudly?
The structural ones. Renaming the statement or parameter containers threw a TypeError on the first length read, and renaming the recorded source positions threw when the code sliced the original text with them. Leaf reads such as identifier names and token values stay silent and simply come back empty.
Does this affect the report my CI reads?
It can. The arm that matched the summary keys emitted the report with generated key names, while everything inside the process stayed consistent. A gate that checks whether a status field is present, rather than comparing a value, will read a missing field and is likely to treat the run as a pass.
Should I protect a tool that parses JavaScript at all?
There is no reason not to, and plenty of vendors have to. The point is that a program whose job is reading other people's data structures is made almost entirely of external contracts, so it needs a narrow member pattern anchored to a private prefix and a smoke test that reads one real answer out of a real run.
How do I smoke-test a codemod against a protected build?
Run it over a fixed input and compare two things against literals you typed: a value it extracted, such as the list of identifiers, and a count, such as the number of declarations. A successful parse and a zero exit code proved nothing in every silent arm measured here.
Related reading