Static linting and actionable diagnostics

RiX linting checks scope ownership, control flow, reactive boundaries, mathematical intent, and plugin contracts without evaluating a program. It is read-only by default: linting does not rewrite source, load plugins, or change evaluator behavior. Source edits require the explicit --fix flag, and even then only diagnostics carrying a safe replacement are applied.

Levels and profiles

Lint one or more files, starting with the highest-confidence findings and gradually widening the review:

rix lint --level=essential program.rix
rix lint --level=standard program.rix
rix lint --level=thorough program.rix
rix lint --level=pedantic program.rix

The default is standard. Levels are cumulative: essential is level 1, standard adds level 2, thorough adds level 3, and pedantic adds level 4. Essential and standard prioritize actionable semantic problems. Thorough adds heuristics that may need human confirmation. Pedantic adds convention and style review. Diagnostic severity independently says whether a finding is an error, warning, or note.

Profiles select relevant domains:

Profile Included concerns
default Core semantics and syntax gotchas
plugin Default checks plus plugin contracts
reactive Default checks plus dependency and identity rules
math Default checks plus exact arithmetic and refinement rules
teaching Syntax, math, and reactive explanations useful in coursework
pedantic / all Every domain, including style information

Profiles can be repeated or comma-separated. Plugin files automatically use the plugin profile unless an explicit profile is supplied.

Valid-but-noteworthy constructs such as dense conditionals, deliberate block capture boundaries, function values, and capture-density suggestions live in the teaching or style concerns. They do not dilute the default or plugin profiles; request --profile=teaching, --profile=pedantic, or --profile=all when that review is useful.

Warnings are advisory by default. CI can opt into failure on warnings:

rix lint --strict plugins/my-plugin/my-plugin.plugin.rix

Use --json for editor/build integration, --sarif for SARIF 2.1.0, and --list-rules for the installed rule catalog. Custom operators can be supplied with --operator-file=FILE; a single - input reads standard input. --closed-plugin-set treats the listed plugins as a complete dependency closure and errors when a requires contract has no provider in that set.

Explicit fixes and baselines

Ordinary lint is observational. This command is the opt-in boundary for source edits:

rix lint --fix program.rix

Currently only statically proven @ capture-marker corrections are marked safe. A diagnostic may suggest a larger refactor without exposing an automatic edit. Standard input cannot be fixed.

A baseline supports incremental adoption without changing source:

rix lint --write-baseline=lint-baseline.json existing.rix
rix lint --baseline=lint-baseline.json existing.rix new-code.rix

Writing a baseline is also explicit. Baselines key on rule, absolute file, and message; new diagnostics remain visible.

Local suppressions

Suppress a named rule and include a reason:

## rix-lint-disable-next-line RX1601 -- this definition intentionally snapshots the initial seed
$$frozen := seed + 1;

rix-lint-disable-line, rix-lint-disable, and rix-lint-enable are also recognized. Codes can be comma- or whitespace-separated; ALL is available for exceptional generated code. At pedantic level a suppression without -- reason produces RX2002. The API retains suppressed records separately.

Coverage-assisted prioritization

--coverage=FILE accepts executed-line JSON and annotates/prioritizes findings:

{
  "files": {
    "/absolute/path/program.rix": { "executedLines": [1, 2, 5, 8] }
  }
}

Findings are marked observed, unobserved, or unknown. Coverage changes priority and evidence, never whether a static rule exists.

Scope explanation

To inspect ownership rather than only warnings, select a source line or the identifier nearest a column:

rix explain-scope plugin.rix:42
rix explain-scope --json plugin.rix:42:18

Each entry reports whether the name is current, captured, missing a capture, or incorrectly marked outer, plus its owning scope and suggested spelling.

Rule catalog

Code Meaning
RX1001 A bare name belongs to an enclosing scope and needs @ or an explicit import.
RX1002 @name points outward even though name belongs to the current scope.
RX1003 @name requests an enclosing binding that does not exist.
RX1101 A number is used directly as a decision; 0 is truthy.
RX1102 A conditional or loop may receive undecided ? without handling/refinement.
RX1201 An identity-preserving update targets a statically known immutable value.
RX1202 = aliases a known mutable cell where copy assignment may be intended.
RX1203 The result of a known non-mutating collection method is ignored.
RX1302 A nested binding shadows an enclosing binding.
RX1303 A binding is initialized along only some conditional paths.
RX1401 No visible loop operation can change a binding used by its condition.
RX1402 One loop-control binding advances in both body and update slot.
RX1403 A closure created in a loop refers to a loop-local binding.
RX1501 A self-call is not in tail position and may consume unbounded stack.
RX1601 A reactive definition takes an untracked snapshot where $name may be intended.
RX1602 A $$name identity is used in an ordinary value position.
RX1603 In-place $name mutation has no visible .Touch() publication.
RX1604 Static $name edges form a reactive dependency cycle.
RX1701 Lowercase f(x) parsed as implicit multiplication, not a direct call.
RX1702 Literal index zero is used on a one-based collection.
RX1703 A string/collection is used with JavaScript-like emptiness assumptions.
RX1704 Ternary nesting is dense enough to obscure branch meaning.
RX1705 A single-expression block creates a capture boundary where grouping may be intended.
RX1706 A function binding is referenced as a value rather than directly called.
RX1801 Exact / versus truncating // deserves an intent check.
RX1802 Fraction equality may mean same stored pair or equivalent rational value.
RX1803 An exact value is explicitly converted to an inexact Float.
RX1804 A divisor is literal zero or an unguarded function parameter.
RX1805 Polynomial / needs the rational-function capability.
RX1806 Refinement has no visible precision/work budget.
RX1901RX1910 Plugin header, exports, mount, dependencies, collisions, portability, schemas, mutation naming, idempotence, and capability groups.
RX2001 A lazy branch is capture-dense enough to consider extraction (style profile at thorough level).
RX2002 A lint suppression lacks an explanatory reason.

Errors always produce a nonzero status. --strict additionally fails on warnings, but not informational suggestions.

JavaScript API

lintRix(source, options) returns ordered diagnostics. analyzeRix returns diagnostics, suppressedDiagnostics, ownership scopes, and the resolved level/profiles. Options include level, profile/profiles, plugin metadata, custom operators, and a pre-parsed AST.

import {
    applyRixLintFixes,
    formatLintDiagnostic,
    lintDiagnosticsToSarif,
    lintRix,
} from "rix";

const source = "x=1; {; x; };";
const diagnostics = lintRix(source, { file: "example.rix", level: "essential" });
for (const diagnostic of diagnostics) console.log(formatLintDiagnostic(diagnostic));

// This throws unless edit:true is supplied.
const fixed = applyRixLintFixes(source, diagnostics, { edit: true });
const sarif = lintDiagnosticsToSarif(diagnostics);

Runtime assistance and scope model

The evaluator gives capture-direction hints when static linting was not run. Selected lazy branches name their ?:, ?_, or ?? context; loop failures name the condition/body/update phase and iteration; immutable ~= failures explain the identity-preserving contract.

The analyzer follows evaluator ownership rather than JavaScript visibility:

  • a function owns its parameters and top-level body bindings;
  • a standalone {; ... } owns a new isolated scope;
  • a loop owns its header locals, and direct blocks in loop slots share it;
  • a block used as a lazy branch is nested and needs explicit captures;
  • direct function calls retain callable lookup behavior.

Dynamic plugin dispatch, host values, and alias effects can still require runtime evidence. This is why levels, profiles, coverage annotations, and reasoned suppressions are separate controls rather than pretending every heuristic has the same confidence.

Back to top