Skip to content

Escape variable declarations named after C# keywords (#133) - #156

Merged
paulirwin merged 3 commits into
masterfrom
fix/133-escape-variable-declarations
Aug 14, 2026
Merged

paulirwin merged 3 commits into
masterfrom
fix/133-escape-variable-declarations

Conversation

@paulirwin

Copy link
Copy Markdown
Owner

Fixes #133.

Problem

TypeHelper.EscapeIdentifier was applied at identifier usagesNameExpressionVisitor, FieldAccessExpressionVisitor — and at parameters, but never at any declaration site. So a variable named after a C# keyword produced output that referenced an identifier it never declared:

using (InputStream in = new FileInputStream(dictionaryFile))  // not escaped
{
    Init(@in);                                                 // escaped
}

That C# does not compile. Seven sites were affected, all of the same shape.

Fix

Applied EscapeIdentifier at every declaration site:

Site File
Local variables ExpressionStatementVisitor.cs
Fields FieldDeclarationVisitor.cs
try-with-resources declarations TryStatementVisitor.cs
try (existingVar) resources (Java 9+) TryStatementVisitor.cs
Catch parameters TryStatementVisitor.cs
foreach variables ForEachStatementVisitor.cs
for-loop init ForStatementVisitor.cs

Two needed more than an added call:

ForEachStatementVisitor built a VariableDeclarator only to read Identifier.ValueText back off it, which strips the @ prefix. It now constructs the identifier token directly, dropping the pointless round trip.

ForStatementVisitor passed VariableDeclarator.toString() into SyntaxFactory.VariableDeclarator. That returns the name and its initializer ("i = 0"), while that overload treats the whole string as an identifier name — it happened to round-trip through the printer, which is why it worked. Escaping it blindly corrupts the output, so the declarator is now built from its parts: the escaped name plus the initializer visited through ExpressionVisitor, matching how every other declarator site works.

Testing

Written TDD — each test was added and confirmed failing before the corresponding fix. Eight new tests in EscapeIdentifierTests.cs cover local variables, fields, try-with-resources (both forms), catch parameters, foreach, for-loop init, and a multi-declarator for loop (int @base = 0, i = 1) asserting that only the keyword is escaped.

The ForStatementVisitor rewrite is a behavior change on a hot path, so coverage there matters beyond the unit tests: Resources/*.java contains dozens of classic for loops, and FullIntegrationTests Roslyn-compiles the generated C# and executes it. Those pass, which is what establishes the rewrite is faithful rather than merely printer-identical.

Before After
New escaping tests (8) fail pass
Full suite 256 pass 264 pass

Scope note

Only the first commit is #133 as filed. The for-loop, catch-clause, and try (existingVar) sites are the same class of bug, found while fixing it, and are split into their own commits — they drop cleanly onto a separate branch if you'd rather keep #133 narrow.

🤖 Generated with Claude Code

paulirwin and others added 3 commits August 14, 2026 14:04
Usages of a variable were escaped via NameExpressionVisitor, but the
declarations themselves were not, producing uncompilable output such as
`InputStream in = ...` alongside a correctly escaped `Init(@in)`.

Apply TypeHelper.EscapeIdentifier at the four declarator sites: local
variables, fields, try-with-resources resources, and foreach variables.

The foreach visitor previously round-tripped the name through a
VariableDeclarator and read back Identifier.ValueText, which strips the
`@` prefix; it now builds the identifier token directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the previous commit, covering the two remaining declaration
sites that left keyword identifiers unescaped.

The for-loop initializer passed VariableDeclarator.toString() — the name
*and* its initializer, e.g. "i = 0" — into SyntaxFactory.VariableDeclarator,
which treats the whole string as an identifier. Escaping that blindly
corrupts the output, so build the declarator from its parts instead: the
escaped name plus the initializer visited as an expression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Java 9+ form `try (existingVar)` references an already-declared
effectively-final variable rather than declaring one, and was the last
identifier site left unescaped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@paulirwin
paulirwin merged commit 47c4206 into master Aug 14, 2026
5 checks passed
@paulirwin
paulirwin deleted the fix/133-escape-variable-declarations branch August 14, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Variable declarations with reserved keywords are not escaped

1 participant