Skip to content

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.

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.

TierMeaning
nativeIts own generator class, written against the language directly.
dotnetA .NET-family generator plus a Java→target translator.
braceJava’s line placement plus a syntax profile, with a bespoke generator subclass.
profileJava’s line placement plus a syntax profile only — no subclass needed.
sfcSingle-file component: only the <script> block, generated as its inner JS/TS.

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.

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.

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.

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 nested def gave 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.

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.

LanguageTierExtensionsNotes
Javanative.java
Pythonnative.py .pyw
JavaScriptnative.js .jsx .mjs .cjs .es .es6.jsx keeps its own token — JSX-aware placement keys on it.
TypeScriptnative.ts .tsx .mts .cts.tsx as above.
C#dotnet.cs .cshtml .razor .aspx .ascx .master .ashx .asax .xaml .axamlMarkup instruments only embedded code blocks; a plain view yields zero logs. Code-behind routes on its own .cs/.vb.
VB.NETdotnet.vb .vbhtml
F#dotnet.fs .fsx .fsi
Gobrace.go
Rustbrace.rs
PHPbrace.php .php4 .php5 .php7 .php8
Kotlinbrace.kt .kts
Scalabrace.scala .sc
Swiftbrace.swift
Rubybrace.rb .rake .gemspec .ru
Cbrace.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
Dartprofile.dart
Groovyprofile.groovyBuild scripts (.gradle/.gvy) are deliberately not collected — instrumenting a build script would break the build.
Zigprofile.zig
Objective-Cprofile.m .mm
Perlprofile.pl .pm
PowerShellprofile.ps1 .psm1
Rprofile.r
Luaprofile.lua
Elixirprofile.ex .exs
Shellprofile.sh .bash
Vuesfc.vue<script> block only.
Sveltesfc.svelte<script> block only.

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.

Terminal window
# 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 languages

Verification — 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:

beforeafter
checker rows satisfiable7 of 2525 of 25
languages verified13 of 2827 of 28 (all but Swift)
Gogofmt -e, parse onlygopls check, a type-check
C / C++gcc / g++ fallbackclang / 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.

LanguageGateSees
Rubyruby -csyntax
PHPphp -lsyntax
Perlperl -csyntax
Lualuac -psyntax
JS / TS / JSX / TSXesbuild (JsSyntaxGate)syntax
Pythonpy_compile (PySyntaxGate)syntax + indentation

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:

LanguageGate
Javajavac -Xlint:all
Gogopls check (falls back to gofmt -e)
Rustrustc --emit=metadata -D warnings
C / C++ / Objective-Cclang -fsyntax-only + targeted -Werror=
Swiftswiftc -emit-sil -warnings-as-errors
Zigzig ast-check
Shellshellcheck
Kotlinkotlinc -Wextra
Scalascalac -Wunused:locals
Elixirelixirc
Dartdart 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 logging
logger = 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 return compiles clean. No Scala gate can catch that shape.
  • Kotlin K2 emits neither UnreachableCode nor UnusedVariable by default — -Wextra is 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 / .svelte are not in the JS gate’s extension list, so single-file components are unverified even though only their <script> block is instrumented.

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.

ColumnMeans
In repoFiles of that language the whole-project sweep considered instrumentable
Cannot hold a logCollected files that could never hold one — markup/data, no generator, empty, DTO, interface, entry point. Removed from the denominator
EligibleIn repo − cannot hold a log. The coverage denominator
InstrumentedFiles this run actually changed, printed as N of eligible
CoverageInstrumented ÷ eligible
Nothing to logEligible 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 placedEligible files the analyzer DID recommend logs for, that gained none
Reverted by gateFiles 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.

ColumnMeans
RecommendedStatements the analyzer asked for. Already excludes duplicates — only one copy of a statement can be placed, so a second is not an opportunity
PlacedStatements the file actually gained
Not placedRecommended − 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.

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.

ReasonMeans
NO_INSERTION_KEYThe analyzer named a line the pass could not resolve to a legal position, so the statement never entered the insertion map
REKEY_COLLAPSEDLost 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_TIMERDropped 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_WRITESkipped 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_SOURCEThe file already contained that exact line before the run. Not a gap in your logging
ALREADY_PLACED_THIS_RUNAn identical line had already been placed in the same file by this run
TIMER_ALREADY_IN_METHODA duration start-timer the method already declares
WOULD_NOT_COMPILERefused by a compile guard — the proposed text was a fragment of a signature, not a statement
INSIDE_BLOCK_COMMENTThe 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_COLLIDESThe 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_CAPTUREAn 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_WRITERefused 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 |

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 / methodShape
TokenServiceImpl.getAllClaimsFromTokenthe same exception log recommended at 5 separate catch lines
CacheConfig.cacheManagerclass_variable_tracking (lines 24, 25) and method_variable_tracking (line 23) both emitting cacheManager → {}
AccountServiceImpl.createAccountsame 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.

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 1
print("no") # reachable — hoisting above `return 1` would be wrong twice over

A 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:

passbeforeafter
1466466
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.

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.

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 statement
if (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.

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.