Language Support
Logrite instruments 28 languages. This page is the authoritative statement of which ones, what routes to them, and how far each is trusted.
Generation tiers
Section titled “Generation tiers”A language’s tier says how its log statements are produced, and therefore how much of the engine has to be right for it to work.
| Tier | Meaning |
|---|---|
| native | Its own generator class, written against the language directly. |
| dotnet | A .NET-family generator plus a Java→target translator. |
| brace | Java’s line placement plus a syntax profile, with a bespoke generator subclass. |
| profile | Java’s line placement plus a syntax profile only — no subclass needed. |
| sfc | Single-file component: only the <script> block, generated as its inner JS/TS. |
Where a log is allowed to go
Section titled “Where a log is allowed to go”A statement is inserted before line K, and K can be illegal for reasons that have nothing to
do with the language: the line above has not finished (a wrapped signature, a fluent chain), the
line above ended the path (return, throw, VB’s Exit Function), or the line above IS the
body’s value (Rust, Scala, Ruby, Elixir, Perl, R, Groovy and F# return their tail expression).
Placement is owned per language rather than centrally: returnSafePlacement covers C, Lua and
Go — the three whose compiler rejects a bad key outright — bodyKeyPlacement covers VB and
Kotlin, Rust has enforceRustStatementPlacement (which resolves placement against a type
environment it builds for the file), and the brace profiles carry a tailExpressionValued flag
that anchors the exit log above the tail expression for F# and Scala. Everything else relies on
the collision passes plus a bounds clamp, which can only DROP.
A method whose brace is on the next line (Allman)
Section titled “A method whose brace is on the next line (Allman)”Detection comes before placement, and a method the parser cannot see produces nothing at all —
no entry log, no exit log, no recommendation of any kind. Every concrete-method pattern in
parseJavaMethods requires a literal {, and the scan loop explicitly skips a line that is
only {, so a declaration written
public List<EmployerDTO> findAll(){matched nothing and the method did not exist.
assembleWrappedSignature exists to join a declaration to the lines below it, but it returned
early on depth <= 0 — the parameter list here is already closed, so there was nothing it
considered “left hanging open”. It now looks at the first non-comment line below and appends
it only when that line is exactly {, and only when the declaration already holds a complete
parameter list.
The one-slot body
Section titled “The one-slot body”FileUtil.addLine used to open with
if (lineToAdd.contains("<") && lineToAdd.contains(">")) return false;a guard against inserting a type-parameter fragment such as Function<Claims, T> claimsResolver.
Our own markers are >>> Entering and <<< Exiting, so either alone passed and the two
together were silently discarded. When a method body has one usable insertion key, the entry and
exit logs share it, the merged statement carries both markers, and the backend threw away both
logs.
It now strips the lifecycle arrows before looking for a type parameter, and tests each PHYSICAL line of a multi-line statement separately rather than the whole string at once.
Known gap: placement is not yet universal
Section titled “Known gap: placement is not yet universal”Extending one placement pass to all 28 was built and measured, and it does not ship yet. On the harness, with nothing else changed, routing every language through it moved the average composite 0.9014 → 0.8757: 15 languages down, 6 up. The dominant failure is idempotency falling 1.00 → 0.00 in nine languages (C, C++, C#, Elixir, Groovy, Kotlin, Ruby, TypeScript, Zig).
The cause is that a shared key is emitted as consecutive physical lines, and the cross-pass dedup that makes a re-scan add nothing matches a recorded statement WHOLE — so a group it did not record as a group is re-recommended in full on the next scan. Teaching the dedup about multi-line statements is the prerequisite, not more placement logic.
Indentation is syntax in four of them
Section titled “Indentation is syntax in four of them”F#, Python, Scala and Elixir are offside-rule languages: the column an injected line lands in
decides whether the file parses. FileUtil.offsideIndent skips blank anchors and walks up to the
HEAD of the statement above, because the two lines either side of the insertion point are not
enough on their own:
- Both anchors blank. A blank line between
def register_errorhandlers(app):and its nesteddefgave the entry log column 0 —IndentationError: expected an indented block after function definition. The first statement of a body must be deeper than its header. - The anchor above is a continuation. A comprehension wrapped onto a second line is indented
8 while the statement it belongs to starts at 4; taking the 8 puts the log in a block that is
at 4 —
IndentationError: unexpected indent.
Both were measured on a RealWorld Flask application, whose PR reported three Python files “instrumented, then rolled back because the result did not parse”. An indentation error is not attributable to one of our lines by the syntax gate’s look-back, so the gate cannot prune it and reverts the whole file instead.
Credentials are never logged, in any of them
Section titled “Credentials are never logged, in any of them”languages/shared/secretRedaction.ts runs over the final statements for all 28: a parameter
whose name looks like a secret (password, token, apiKey, ssn, cvv, …) is rendered
name=<redacted> and its VALUE is removed from the argument list.
Both halves together is the point. Removing the placeholder alone leaves an unused argument,
which is error: argument never used in Rust and prints %!(EXTRA …) in Go; removing the
argument alone leaves a hole with nothing to fill it. The pass counts the positional holes,
redacts the n-th, and drops exactly the n-th argument — or, for the interpolating languages,
rewrites ${password} in place and touches no argument list.
The catalog
Section titled “The catalog”Source of truth: src/core/languages/shared/languageCatalog.ts in logrite-analyzer.
Adding a language means adding its generator plus one entry there — the /analyze/auto
extension map and GET /languages are both derived from it, so there is no second list
to forget.
| Language | Tier | Extensions | Notes |
|---|---|---|---|
| Java | native | .java | |
| Python | native | .py .pyw | |
| JavaScript | native | .js .jsx .mjs .cjs .es .es6 | .jsx keeps its own token — JSX-aware placement keys on it. |
| TypeScript | native | .ts .tsx .mts .cts | .tsx as above. |
| C# | dotnet | .cs .cshtml .razor .aspx .ascx .master .ashx .asax .xaml .axaml | Markup instruments only embedded code blocks; a plain view yields zero logs. Code-behind routes on its own .cs/.vb. |
| VB.NET | dotnet | .vb .vbhtml | |
| F# | dotnet | .fs .fsx .fsi | |
| Go | brace | .go | |
| Rust | brace | .rs | |
| PHP | brace | .php .php4 .php5 .php7 .php8 | |
| Kotlin | brace | .kt .kts | |
| Scala | brace | .scala .sc | |
| Swift | brace | .swift | |
| Ruby | brace | .rb .rake .gemspec .ru | |
| C | brace | .c .h | .h is ambiguous (C or C++); routed as C, whose fprintf compiles in both. |
| C++ | brace | .cpp .cc .cxx .hpp .hxx .hh .ipp .tpp .cppm | |
| Dart | profile | .dart | |
| Groovy | profile | .groovy | Build scripts (.gradle/.gvy) are deliberately not collected — instrumenting a build script would break the build. |
| Zig | profile | .zig | |
| Objective-C | profile | .m .mm | |
| Perl | profile | .pl .pm | |
| PowerShell | profile | .ps1 .psm1 | |
| R | profile | .r | |
| Lua | profile | .lua | |
| Elixir | profile | .ex .exs | |
| Shell | profile | .sh .bash | |
| Vue | sfc | .vue | <script> block only. |
| Svelte | sfc | .svelte | <script> block only. |
The /languages contract
Section titled “The /languages contract”GET /languages on the analyzer serves the catalog, cross-checked against the generator
factory — a language listed without a real generator is reported, not assumed.
# on the VM: analyzer is host port 9001 on dev, 9000 on prod (ops/deploy/services.json)curl -s http://127.0.0.1:9001/languages | jq '{count, missingGenerators}'# { "count": 28, "missingGenerators": [] }The backend calls this at startup (AnalyzerCapabilityCheck) and compares it against its
own INSTRUMENTABLE_LANGUAGES. Agreement logs one line; divergence logs the specific
languages, at ERROR:
[capability] backend and analyzer agree on 28 languagesVerification — which languages we can actually check
Section titled “Verification — which languages we can actually check”A generator without a checker is a guess. LanguageSyntaxGate holds one Checker row per
language: the check-only command, a probe proving the tool exists, and the regex that reads a
line number out of its errors. Adding a language is a row, not a class.
Which image the checkers actually live in — changed 2026-08-20
Section titled “Which image the checkers actually live in — changed 2026-08-20”A row only checks anything if its tool is present, and until 2026-08-20 most of them were not
present in the product image. Replicating ToolResolver’s resolution — PATH, then
LOGRITE_*_HOME_* — inside the running dev backend:
| before | after | |
|---|---|---|
| checker rows satisfiable | 7 of 25 | 25 of 25 |
| languages verified | 13 of 28 | 27 of 28 (all but Swift) |
| Go | gofmt -e, parse only | gopls check, a type-check |
| C / C++ | gcc / g++ fallback | clang / clang++ |
When an unresolved name is OURS — corrected 2026-08-20
Section titled “When an unresolved name is OURS — corrected 2026-08-20”A type-checking row run on one file cannot resolve the project’s names, so the gate ignores any
diagnostic that only says a name could not be found. That is right about Java: the unresolved
name is LogRite’s own import org.slf4j.Logger meeting an empty classpath, and the real build
resolves it. Blaming it is what emptied a 30-file run to no pull request.
Parse-only rows
Section titled “Parse-only rows”| Language | Gate | Sees |
|---|---|---|
| Ruby | ruby -c | syntax |
| PHP | php -l | syntax |
| Perl | perl -c | syntax |
| Lua | luac -p | syntax |
| JS / TS / JSX / TSX | esbuild (JsSyntaxGate) | syntax |
| Python | py_compile (PySyntaxGate) | syntax + indentation |
Type-checking rows
Section titled “Type-checking rows”Parse-only is blind to the defects we actually produce — unreachable code, an unused timing
scaffold, a tail expression demoted to a statement. gofmt -e happily accepts a Go file with
__logStart := time.Now() sitting after return rows, which go build rejects as both dead
code and an unused variable. These rows compile instead:
| Language | Gate |
|---|---|
| Java | javac -Xlint:all |
| Go | gopls check (falls back to gofmt -e) |
| Rust | rustc --emit=metadata -D warnings |
| C / C++ / Objective-C | clang -fsyntax-only + targeted -Werror= |
| Swift | swiftc -emit-sil -warnings-as-errors |
| Zig | zig ast-check |
| Shell | shellcheck |
| Kotlin | kotlinc -Wextra |
| Scala | scalac -Wunused:locals |
| Elixir | elixirc |
| Dart | dart analyze |
Toolchains installed through mise are found via a LOGRITE_<LANG>_HOME_ environment prefix
rather than assumed to be on PATH. Where an extension is claimed by more than one row, the
strongest available tool wins — so Go degrades from gopls to gofmt rather than to nothing.
Where the logger declaration is anchored, and why a bracketed import is a special case
Section titled “Where the logger declaration is anchored, and why a bracketed import is a special case”LoggerInsertionLocator.findImportInsertLine places the synthesized logger declaration and its
import after the file’s last import. An import, however, can be one statement spread over
several physical lines:
from conduit.database import (Model, SurrogatePK, db, reference_col, relationship)Only the first line looks like an import — the continuation is a bare identifier list. Matching per physical line therefore returned an anchor inside the open bracket, and where such an import was the file’s last, generation split the statement in half:
from conduit.database import (Model, SurrogatePK, db,import logginglogger = logging.getLogger(__name__) reference_col, relationship)That is a hard SyntaxError, so the gate reverts the file whole and every correct log in it
is lost for a defect in none of them. The anchor now walks to the statement’s last physical line
by balancing (/[ (ignoring anything after a #), which is inert for a single-line import —
i.e. for every import in a brace language.
Known limits, recorded rather than implied away
Section titled “Known limits, recorded rather than implied away”- Scala 3 has no dead-code analysis. A log placed immediately after an explicit
returncompiles clean. No Scala gate can catch that shape. - Kotlin K2 emits neither
UnreachableCodenorUnusedVariableby default —-Wextrais what turns the checkers on. Promoting them without it promotes a diagnostic that is never produced, i.e. a gate that always passes. - Single-file compilation cannot type-check a real customer repository in any JVM or .NET language, because every file references sibling types. These rows are sound against the self-contained harness fixtures; enabling them for customer checkouts needs project context.
.vue/.svelteare not in the JS gate’s extension list, so single-file components are unverified even though only their<script>block is instrumented.
Reading the PR coverage table
Section titled “Reading the PR coverage table”Every scan that opens a PR puts two per-language tables in the PR body
(CoverageReportBuilder) — one counting FILES, one counting LOG STATEMENTS. They were a single
ten-column row until 2026-08-17, which is what made the report hard to read: “Files covered
56.4%” sat beside “Logs recommended 526” with nothing saying they measure different things, and
a reader comparing 526 against the 298 statements in the diff reasonably concluded that 228 had
been lost.
It is the only artefact an unattended, webhook-driven scan leaves behind, so it has to be able
to answer “what did I not get?” on its own.
Files — a file counts once here, whether it gained one log or forty.
| Column | Means |
|---|---|
| In repo | Files of that language the whole-project sweep considered instrumentable |
| Cannot hold a log | Collected files that could never hold one — markup/data, no generator, empty, DTO, interface, entry point. Removed from the denominator |
| Eligible | In repo − cannot hold a log. The coverage denominator |
| Instrumented | Files this run actually changed, printed as N of eligible |
| Coverage | Instrumented ÷ eligible |
| Nothing to log | Eligible files the analyzer returned no recommendation for. Both “genuinely nothing to log” and “the analyzer could not see it” land here — see the warning below |
| Recommended, none placed | Eligible files the analyzer DID recommend logs for, that gained none |
| Reverted by gate | Files that WERE instrumented and then rolled back because the result did not parse |
The files table balances: eligible = instrumented + nothing to log + recommended-none-placed + reverted by gate. Any remainder is printed as an explicit accounting-gap note rather than left for
the reader to find by adding the columns up.
Log statements — what the analyzer asked for, and what the diff contains.
| Column | Means |
|---|---|
| Recommended | Statements the analyzer asked for. Already excludes duplicates — only one copy of a statement can be placed, so a second is not an opportunity |
| Placed | Statements the file actually gained |
| Not placed | Recommended − placed, itemised by stage underneath the table |
Zeros print as 0, not as an em dash: a dash in a numeric column reads as “no data”, which is the
opposite of “nothing was excluded here”. Reverted by gate stays in the FILES table on purpose —
it is a file count, and putting it in the statements table would hide it whenever no log ledger is
supplied, which is exactly when a silently reverted language most needs surfacing.
Recommended vs placed is the answer to “how many logs am I missing?” File counts cannot give
it: a file counts as instrumented if it received one log, whether the analyzer asked for one or
forty. The two numbers are taken either side of buildInsertions, which is where the loss
happens — several recommendations can resolve to the same key and the backend’s map holds one
statement per key — so the difference is an exact count of what placement dropped. Files a gate
reverted are counted separately, in the last column.
Where recommendations are lost
Section titled “Where recommendations are lost”A run can recommend 526 statements and place 290. The PR body used to cover that whole gap with one sentence asserting it “is concentrated in short, dense method bodies” — a claim about distribution that nothing measured. It is now counted per stage.
| Reason | Means |
|---|---|
NO_INSERTION_KEY | The analyzer named a line the pass could not resolve to a legal position, so the statement never entered the insertion map |
REKEY_COLLAPSED | Lost when keys are re-derived against the file on disk — two statements resolved to one line, and the map holds one statement per line |
ORPHAN_TIMER | Dropped as half a timing pair. An orphan timer is a compile error in C/Go/Lua and a runtime error elsewhere, so its partner goes with it |
NEVER_OFFERED_TO_WRITE | Skipped by a placement guard BEFORE the write was attempted — the variable is not in scope yet, the position would be unreachable, or the line was never visited |
ALREADY_IN_SOURCE | The file already contained that exact line before the run. Not a gap in your logging |
ALREADY_PLACED_THIS_RUN | An identical line had already been placed in the same file by this run |
TIMER_ALREADY_IN_METHOD | A duration start-timer the method already declares |
WOULD_NOT_COMPILE | Refused by a compile guard — the proposed text was a fragment of a signature, not a statement |
INSIDE_BLOCK_COMMENT | The keyed position is inside a /* … */ block comment, i.e. commented-out code the analyzer read as live. Refused before the write. Not yet deployed (2026-09-15) |
TIMER_NAME_COLLIDES | The customer’s method already declares the timer’s name, so this run’s timer and its readers in that method were removed (Java, C#). Not yet deployed (2026-09-15) |
EXIT_UNSAFE_TO_CAPTURE | An exit log above a pass-through return that could not be captured into a local, removed rather than left to run before the work (Java). Not yet deployed (2026-09-15) |
REFUSED_AT_WRITE | Refused at write time for a reason the stage counters could not classify. A large number here is a placement bug worth reporting |
| UNATTRIBUTED | Whatever the counters could not place |
No statement is recommended twice for one file
Section titled “No statement is recommended twice for one file”Only one copy of a given statement can ever be placed: FileUtil.addLine refuses an identical
second, because a duplicate log makes every re-scan grow the repository. A duplicate
recommendation is therefore not an opportunity, and counting it as one measured coverage
against a surface that could not be reached.
Measured on BankJava_TestRepo scan 291 — 46 of 526 recommendations were the same statement
twice for one file, in 26 groups, 24 of them inside a single method:
| File / method | Shape |
|---|---|
TokenServiceImpl.getAllClaimsFromToken | the same exception log recommended at 5 separate catch lines |
CacheConfig.cacheManager | class_variable_tracking (lines 24, 25) and method_variable_tracking (line 23) both emitting cacheManager → {} |
AccountServiceImpl.createAccount | same shape, 4 copies |
The pre-existing dedup in Util.convertToLevelTwoDtos catches neither. It keys on
(method, lineNumber, statement) — the lineNumber is exactly what lets five catch lines through
— and it runs once per rule, so it never sees across two rules. Deduplication is now
whole-file and cross-rule, applied before the counts, so *ExpectedCnt and the *Total
columns describe recommendations that can actually be placed.
Counting what was placed
Section titled “Counting what was placed”Two defects made the reported numbers disagree with the diff. Both are fixed; both are worth knowing about because both were silent.
A wrapped throw shipped a PR that does not compile
Section titled “A wrapped throw shipped a PR that does not compile”indexOfLastTerminator reads ONE physical line, so a terminator whose statement wraps is invisible
to it:
throw new OtpRetryLimitExceededException( String.format(ApiMessages.OTP_GENERATION_LIMIT_EXCEEDED.getMessage(), waitingMinutes));log.warn("Throwing OtpRetryLimitExceededException from validateOtpWithinRetryLimit(otpInfo=<redacted>)");The last line begins with neither keyword, so no hoist ran and the log landed below the throw.
BankJava_TestRepo PR #175 was published with OtpServiceImpl.java:[90,33] unreachable statement.
The terminator search now consults returnThrowStmtStart to find the statement’s FIRST line, gated
on SourceLineText.closesUnopenedBracket so that only a genuine continuation looks back. A balanced
last line behaves exactly as before, and that gate is load-bearing: in an offside-rule language a
line merely preceded by a return is reachable code.
if x: return 1print("no") # reachable — hoisting above `return 1` would be wrong twice overA re-scan used to duplicate a log every time
Section titled “A re-scan used to duplicate a log every time”TerminalHoister.hoistAboveTerminal bypasses addLine, and its own javadoc flags the consequence:
“Dedup is re-checked here rather than assumed … the one thing that must not be lost with it is
‘do not add a statement this file already has’.” It re-checked the wrong set — updatedLines, the
output built so far — so a copy sitting below the insertion point had not been copied in yet
and was invisible. That is the identical blind spot addLine closed for itself with
existingTrimmedLines. The hoist path now receives the same set.
Convergence on BankJava_TestRepo, measured on the repository rather than a fixture:
| pass | before | after |
|---|---|---|
| 1 | 466 | 466 |
| 2 | +10 | +3 |
| 3 | — | 0 ← fixed point |
The remaining +3 are logs pass one did not place and pass two finds an anchor for; they are not duplicates, and pass three adds nothing. No duplicate log line is introduced at all — the only two repeated log lines in the final tree are the customer’s own, present twice in the pristine base.
A transient GitHub failure no longer loses the PR
Section titled “A transient GitHub failure no longer loses the PR”A scan pushed its branch and then got 503 No server is currently available … please try resubmitting your request. The single attempt failed and the run ended END null /
pr_creation_failed with no PR, while the API was answering again minutes later. PR creation now
retries four times over ~12s on 429 and 5xx. A non-429 4xx is not retried — a bad token fails
identically every time. The PR is the only durable artefact a scan produces.
A failed build no longer looks mergeable
Section titled “A failed build no longer looks mergeable”The build verdict used to only label the run: result="Failed" and a “Build Failed. Please
review” websocket frame. On a webhook-driven scan the session is null, so nobody saw the frame and
the PR was opened normal and mergeable, titled “adds comprehensive logging”. GitHub PRs whose build
failed now open as draft with a warning banner in the body — both properties of the artefact, so
they survive an unattended run. Draft is refused with 422 on some repositories, so that falls back
to a normal PR: losing the flag beats losing the PR. The branch is still pushed; the work is marked
not-ready, not discarded.
The push refuses to delete source
Section titled “The push refuses to delete source”buildProjectCollectAllFailures narrows a failure by renaming each blamed file to
<name>.logrite_skip and restoring it in a finally, across up to 50 Maven builds. If the JVM dies
inside that loop the finally never runs, and git add --update stages the rename as a deletion
of customer source plus an untracked twin. The push now refuses any staged deletion or
.logrite_skip file. LogRite adds and modifies; it never deletes.
A log that redacts a secret was thrown away
Section titled “A log that redacts a secret was thrown away”carriesTypeParameterFragment refuses a line that looks like a signature fragment —
Function<Claims, T> claimsResolver — which does not compile in any position. Its test was:
strip our own >>>/<<< markers, then reject anything containing both < and >.
A redacted parameter renders as pin=<redacted>, inside a string literal, so it tripped the
guard and the whole log was refused.
…and a SECOND < guard was doing the same thing, in a different method
Section titled “…and a SECOND < guard was doing the same thing, in a different method”Fixing carriesTypeParameterFragment moved PR #174’s placed count from 222 to 321, and yet the
diff contained exactly one redacted log out of ~130 recommended. A second, unrelated guard in
skipAddLine was discarding the rest:
// if there is a single "<" in the log statement then skip it.Matcher m = lessThanSignPattern.matcher(methodLog.getLogStatement()); // the RAW statementif (m.find()) { addLine = false; }Same defect, same cause, different site — and because it runs in the scope guard rather than at the
write, the loss did not even present as a refusal. It presented as NEVER_OFFERED_TO_WRITE.
Both guards now share one definition of what counts as code:
SourceLineText.carriesBareLessThanInCode sits directly beside
SourceLineText.carriesTypeParameterFragment, and the scope guard calls it. That adjacency is the
fix, not a tidy-up — the first correction was applied to one copy of the test while the other
survived in a different class, and nothing connected them.
The placed-count iterated a map the write had already consumed
Section titled “The placed-count iterated a map the write had already consumed”The return-handler places a method’s logs and then removes their keys from insertions, so the
main per-line loop cannot emit them a second time. That is correct for placement.
countPlacedLogStatements ran at the END of the method and iterated whatever was LEFT — so every
log the return-handler successfully placed was unclaimable. Exit logs live in methods that end in
return, which is the return-handler’s entire job, so the loss fell almost entirely on them.
Measured quality per language
Section titled “Measured quality per language”Support is not a boolean. Every language above is scored end to end — detection,
instrumentation, rule coverage, insertion, validity, consistency, false positives,
idempotency — by the harness described in
Quality Harness. Per-language grades live in
docs/quality/ in LogriteMasterControl
and are deliberately not duplicated here, because a copied table drifts and a linked
one cannot.