Clang Biome
A lint and format extension for C written against Renesas RL78 / CC-RL conventions. It began as a port
of the desktop application rl78-biome — the same 10 lint rules and the same 20 formatter rules, moved
out of a Tauri window and onto VSCode's own diagnostic and formatting surfaces. The lint rules are still
that port verbatim; several formatter rule bodies have since been changed deliberately, and the
resulting behaviour is described under Formatting behaviour below.
Parsing is done with tree-sitter, so the rules act on a syntax tree rather than on regular
expressions. Where the tree cannot be trusted — an RL78 SFR bit access such as P1.0 = 1; is not
valid C and parses as an error node — the affected region is marked protected and left byte for byte
as it was written.
Status
Pre-release. Not yet verified inside a VSCode extension host. Every rule and both surfaces are
exercised by npm run probe, a plain-Node harness that runs the built dist/ bundle, but nobody has
yet pressed F5 and confirmed the behaviour in a real editor window. Treat the settings below as
implemented-and-unit-proven, not as field-tested.
What it does
Lint runs automatically. Editing a document schedules an analysis 500 ms after you stop typing;
switching editors, changing a clangBiome.* setting, and activation each trigger one too. Results
appear in the Problems panel. There is no command to run it manually yet.
Format is a whole-document formatter, so Shift+Alt+F and format-on-save both reach it. Range
formatting is deliberately not offered: indent, align-comment, align-declarations and
align-array-elements all reason about the whole document, and a selected fragment parsed on its own
becomes error nodes that the RL78 guard would then refuse to touch.
The formatter declines to act, returning no edits at all, when the document is excluded, larger than
200 KB, empty or whitespace-only, or fails to parse. That size ceiling is lower than the 500 KB the
linter uses, because formatting costs roughly 3.4 microseconds per byte and format-on-save is enabled
by default: at 200 KB a save already pauses the editor for about three quarters of a second, and the
pause is what the user feels, not the missing reformat. The reason is written to the Clang Biome
output channel — a document that silently does not change is otherwise indistinguishable from one
that had nothing to fix.
Lint rules
constant-upper-snake, func-name-uppercase-start, global-var-uppercase-start,
if-else-requires-else, no-assign-in-condition, no-magic-number, require-comment-empty-block,
switch-requires-default, switch-sequence-break — all default to warning.
var-prefix defaults to off, matching the source it was ported from.
Each rule is one setting taking off, warning or error; severity and enablement are not split
across two keys.
Twenty rules, each individually switchable: brace-style, break-if-conditions,
collapse-blank-lines, indent, align-declarations, control-keyword-spacing,
condition-paren-spacing, function-call-spacing, space-after-keyword, space-around-operator,
pointer-asterisk-style, unary-tightness, array-subscript-tightness, space-after-comma,
space-after-semicolon, hex-literal-case, align-array-elements, align-comment,
trim-trailing-space, final-newline.
Indentation is tabs by default — one tab per level, and format.indent.width is then not
consulted at all. Set format.indent.style to space to indent with that many spaces per level
instead. A line the RL78 guard protects, such as one containing P1.0, keeps its original leading
whitespace under either style, so an SFR line can look out of step with its neighbours; that is the
guard working, not an indent bug.
The editor's own tabSize and insertSpaces are not consulted. Indentation has one owner here, and
two settings surfaces competing over one output is worse than an inconsistency you can see.
Four rules carry an option of their own: format.indent.style (default tab) and
format.indent.width (1–16, default 4), format.braceStyle (kr, allman, linux, default
linux), format.alignComment.column (1–500, default 100) and
format.breakIfConditions.minConditions (1–32, default 2).
Three rules are off by default: align-declarations and align-array-elements, as in rl78-biome,
plus collapse-blank-lines, which rl78-biome runs but this extension does not — a run of blank
lines you left between two functions is yours to keep.
The points below are fixed behaviour, not settings — there is no key to turn any of them off.
- A declaration's leading type, storage-class or qualifier keyword is separated from the next token
by exactly one space:
extern int x; becomes extern int x;, and a tab after a return type
(void<TAB>aaa()) becomes void aaa(). The payload of a #define is never touched.
- A parenthesized condition of exactly one token loses its interior padding —
while( 1 ) becomes
while(1), and if(flag) / switch(mode) stay tight. A condition of two tokens or more keeps the
padding, so if( a == 1 ) is left as written.
else and else if start on a new line after the closing } (Stroustrup), in every brace style.
Where the opening { goes is still format.braceStyle's decision.
break-if-conditions splits a condition of minConditions clauses or more across lines, indents
each continuation one level deeper than the if, and no longer forces the body's { onto a line of
its own — where that brace goes is format.braceStyle's decision like any other.
- Under the default
linux brace style a function definition's { goes on its own line while a
control statement's { stays at the end of its line. kr puts both at the end of the line;
allman puts both on their own line.
align-comment pads a trailing comment out to column 100 on the first pass and then leaves the
line alone. Formatting an already-formatted document is a no-op, including for a comment trailing a
preprocessor directive such as #define MAX 10 /* max */, which earlier grew by a few bytes on
every save.
Notable settings
clangBiome.lint.excludeFiles defaults to ["iodefine.h"]. Removing that entry makes the Renesas
SFR definition header report a wall of naming and magic-number diagnostics, because SFR-name
exclusion is still a stub in this version.
An invalid value never disables the extension. Each setting falls back on its own, and the reason is
written to the output channel; one mistyped value does not drag the rest of the configuration back
to defaults with it.
Building
npm install
npm run compile # webpack -> dist/extension.js, plus the tree-sitter wasm
npm run probe # plain-Node evidence harness over the built dist/
The two .wasm files are read from disk as byte arrays and handed to Language.load. Loading them
by URL — which is what rl78-biome did from its WebView — does not work in a target: 'node'
extension bundle.
Not implemented yet
A clangBiome.lintWorkspace command, a toggle command with a keybinding, and .vsix packaging.
Range formatting is not planned.
License
MIT