Static analyzer (ч. III.4) — status of every rule
The analyzer runs in oxl check, oxl build and the language server. A diagnostic of level E makes the command fail
(oxl check and oxl build exit with code 3); level W is reported and can be tuned in [lint].
Most rules are ordinary passes of compiler/src/sema; the flow-sensitive ones live in compiler/src/sema/Analyzer.cpp
(Sema::runAnalyzer, after ownership checking, before the lints).
Every code has a page docs/diagnostics/<code>.md.
Level E#
| rule | code | level | implemented | where | tests |
|---|---|---|---|---|---|
| incompatible schema change (§9.2) | schema-break | E | yes | Serial.cpp, SerialLock.cpp | §09.2/break-*, tests/unit/driver/schema_lock_test.cpp |
| non-transferable value sent to another task (§7.3) | send | E | yes | Ownership.cpp | §07.3/send-*, ownership_test.cpp |
| call with a disallowed effect (§6.2) | effects | E | yes | Passes.cpp, CheckCall.cpp | §06.2/*, §III.4/effects-fails-build |
| unhandled result with an error (§6.5) | unhandled-error | E | yes | CheckControl.cpp | §03.5/error-result-cannot-be-ignored, §III.4/unhandled-error-fails-build |
@must_close value leaves scope without close() (§4.3) | must-close | E | yes | Ownership.cpp | §II.1/*-stream-must-close, §III.4/must-close-fails-build |
secret.reveal() reaches a log, a string or a response | secret-leak | E | yes (D-3200) | Analyzer.cpp | §III.4/secret-leak-*, §III.4/secret-show-is-safe, analyzer_test.cpp |
| mixing kinds of time (§17.2) | time-mix | E | yes (D-3201) | CheckOps.cpp | §III.4/time-mix-time-instant, §III.4/time-same-kind-ok, analyzer_test.cpp |
@retry without @idempotent/@readonly (§10.2) | retry-unsafe | E | yes, under the code retry-not-idempotent (services task) | ServiceCheck.cpp | §10.2/retry-without-idempotent-or-readonly-is-error |
rpc nobody can call: no allow/@public/@allow, not @internal, never called (§13.4) | missing-policy | E | yes (D-3203) | ServiceCheck.cpp | §13.4/missing-policy-*, analyzer_test.cpp |
| service, actor, topic or saga without a role (§12.1) | unhosted | E | yes, under four codes (D-6553): service-not-served (ServiceCheck.cpp), actor-not-hosted (sharded actors), topic-without-journal, saga-without-role (Placement.cpp); checked when the program has a cluster with roles | ServiceCheck.cpp, Placement.cpp | §12.1/service-without-role, §12.1/actor-not-hosted, §12.1/topic-without-journal-role, §12.1/saga-without-*, §III.4/unhosted-actor-without-role |
saga step or compensation without @idempotent | saga-idempotency | E | yes (D-5105) | SagaSema.cpp | §11.3/saga-rpc-without-idempotent-is-an-error |
Three names of the Standard's table are reported under more specific codes (D-6553): retry-unsafe = retry-not-idempotent,
unhosted = service-not-served / actor-not-hosted / topic-without-journal / saga-without-role, durable-non-idempotent
= pipeline-idempotency. The codes are not renamed (DiagCodes.def: never rename a code); [lint] accepts the Standard's
names and applies the level to the codes behind them (diagCodesForLintName).
Level W#
| rule | code | implemented | where | tests |
|---|---|---|---|---|
unused import, variable, dep | unused | yes (imports, variables; dep with services) | Passes.cpp | §02.2/unused-import-warns |
blocking FFI call without blocking | blocking-in-task | yes | CheckCall.cpp | |
| possible cyclic wait between actors | actor-cycle | yes (D-6556) | LintRules.cpp | §III.4/actor-cycle-*, lint_rules_test.cpp |
| repeated literal in attributes/arguments | magic-literal | yes (D-6554), LSP quick fix (D-6559) | LintRules.cpp | §III.4/magic-literal-*, lint_rules_test.cpp, server_test.cpp |
| naming convention (IV.2) | naming | yes (D-6555) | LintRules.cpp | §III.4/naming-*, lint_rules_test.cpp |
| query in a loop over the result of another query | n-plus-one | yes (D-6557) | LintRules.cpp | §III.4/n-plus-one-*, lint_rules_test.cpp |
| query on a sharded table without shard key | scatter-gather | yes (D-6558) | LintRules.cpp | §III.4/scatter-gather-*, lint_rules_test.cpp |
| durable pipeline step calls a non-idempotent rpc | pipeline-idempotency (durable-non-idempotent) | yes (D-5105) | SagaSema.cpp | §10.6/pipeline-durable-step-without-idempotent-rpc-warns, §III.4/durable-non-idempotent-lint-name |
user rule ([lint] plugins) | lint-plugin | yes (D-6560) | LintPlugins.cpp | lint_rules_test.cpp |
| glob import in a library, use of a deprecated item | glob-import, deprecated | yes | extra rules, not in the table of III.4 |
The W rules check the main package only (not std, not dependencies, not the plugin files).
[lint] (oxl.toml)#
[lint]
magic-literal = "error" # "warn" | "error" | "off" ("allow" is the same as "off")
scatter-gather = "off"
unhosted = "error" # the Standard's names stand for the codes behind them (D-6553)
plugins = ["lints/no_todo.oxl"]
no_todo = "error" # a plugin's rule: the file name without .oxl, default "warn"A single-file program (oxl build app.oxl) takes the [lint] table of an oxl.toml in the same directory, and only that
table (D-6551).
Suppression on a declaration (D-7830)#
@allow(lint: <rule>, reason: "…") silences one rule for the declaration that carries it (item, method, field, variant, rpc
signature, actor message): the diagnostics of the rule inside its attributes, signature and body are dropped. <rule> is a name
of the tables above (magic-literal, scatter-gather, written with hyphens or as a string), a diagnostic code or the name of a
[lint] plugins rule. reason is required (bad-allow), an unknown rule is unknown-lint, rules that report errors cannot be
suppressed. oxl fix (D-7820) applies the quick fixes of the diagnostics that remain.
The rules in detail#
- magic-literal (D-6554, IV.1). Counted: duration and size literals in any attribute argument or call argument; integer
literals greater than 1 in the arguments of the limit attributes
@retry @ratelimit @window @shard @cache @timeout @deadline @idle, in call or attribute arguments named like a limit (count:,max:,limit:,size:,capacity:,retries:,attempts:,burst:,ttl:,timeout:, …), and theretry n/within dsuffixes of a chain.@tag/@v/@since/@removednumbers, literals inconst/staticinitializers, list elements and tests are not counted. Every occurrence of a literal used 2+ times is reported (with a note at the first one). The diagnostic carries the quick fix Extract to constantNAME:const NAME = litappended to the file andNAMEat every occurrence in that file;NAMEis the shared context (attribute or argument name) in UPPER_SNAKE_CASE plus the literal (TIMEOUT_3S), orDURATION_…/SIZE_…/LIMIT_…. - naming (D-6555, IV.2). UpperCamelCase: data, enums, interfaces, type aliases, services, actors, topics, configs,
attrdeclarations, enum variants. lowerCamelCase: functions, methods, msgs, rpcs, fields, actorvars, parameters, locals (let / for / match bindings, lambda parameters). UPPER_SNAKE_CASE:constandstatic(module level, impl and interface consts). snake_case: module directories; lower_snake_case: cluster roles. A leading_is ignored;extern c/export cfunctions, operators,initand generated names are skipped. The root module of a single-file program is named after the file and is not checked; the package name is not checked (it comes from oxl.toml and may follow the registry's rules). - actor-cycle (D-6556, §8.8). Nodes: actor types of the package. Edge A → B: a msg of A — or a function of the package
it calls, transitively — calls a msg of B through an
ActorRefand waits for it (not underspawn, a~>step or a closure). Every call that is an edge inside a strongly connected component (including a self-loop through a ref) is reported, with the cycle in the message. It is a type-level approximation: two instances of one actor type form a self-loop. - n-plus-one (D-6557). A query is a call of std.orm
Db.find/all/count,Table.all/count,Query.run/count(afrom x in db.Tquery lowers toQuery.run). Reported: a query inside the body (orwhere) of aforwhose iterable contains a query or names a local that was bound to an expression containing a query, and inside the arguments ofmap/forEach/each/filter/ flatMap/filterMap/any/every/foldcalled on such a value. Loops over other lists are not reported. - scatter-gather (D-6558, IV.6). For tables with
@shard(by: .f): aQuery.run/countwhosesqlWheretexts have no top-level"f" = ?Nconjunct (the runtime's routing test, D-4388),Table.all/count,Db.all/count, andDb.findwhenfis not the@pkfield.
Lint plugins (std.comptime, D-6560..D-6564)#
A plugin is an OxL file listed in [lint] plugins (paths relative to the package root, or to the directory of a single-file
program). It is compiled as a module lint_<stem> of the package, so it is type-checked like other code, and its functions
run in the comptime interpreter. Every comptime fn name(d: Decl) of the file (any name; the result may be () or
() | CompileError) is called once for every declaration and member of the package:
use std.comptime { Decl, CompileError, warn }
comptime fn noTodo(d: Decl) {
if d.kind == "fn" && d.name.startsWith("todo") { warn(d.span, "unfinished function `{d.name}`") }
}
comptime fn noLongSleeps(d: Decl) -> () | CompileError {
for e in d.exprs where e.kind == "duration" && e.text == "10s" {
fail CompileError{ at: e.span, message: "no 10s sleeps in {d.name}" }
}
}The API (std/comptime/comptime.oxl) is plain read-only data:
| type | fields |
|---|---|
Decl | kind, name, parent (enclosing declaration, "" at the top), isPub, span, attrs: List<Attr>, exprs: List<Expr> |
Attr | name ("timeout", "std.foo"), args: List<AttrArg>, span |
AttrArg | name ("" when positional), kind (an expression kind), text (source text), span |
Expr | kind, name, text (source text, ≤ 200 bytes), depth, span — every expression of the body in pre-order |
Span | file, line, col (1-based), offset, length |
CompileError | at: Span, message: str — fail it to report one finding and stop |
warn(at, message) | report a finding and go on |
Declaration kinds: fn const static data field enum variant interface method impl rpc-impl service rpc actor var msg type
topic config dynamic cluster provide attr test fixture mock pipeline saga policy dep import assert operator.
Expression kinds: int float bool char string bytes duration size date prefixed none unit path implicit-member it self tuple
list map struct lambda call method-call field tuple-field computed-field opt-field index unary binary try unwrap cast is block
if match for while loop break continue return fail yield respond chain spawn await scope select atomic unsafe blocking
comptime emit query. name: the path of a path/call/struct, the method of a method-call, the field of a field, the
operator of a binary, the prefix of a prefixed literal.
The comptime interpreter supports, besides the constant expressions: let, assignment, if, while, loop, for x in
list [where c], xs[i], xs.len()/isEmpty()/contains(x), the str methods (len, contains, startsWith, endsWith,
slice, lower, upper, trim…), calls of other comptime functions, string interpolation, fail, assert, panic.
Findings are diagnostics lint-plugin with the message [<rule>] <message>; the rule's level is [lint] <rule> = "warn" |
"error" | "off" (default warn). A plugin that cannot be evaluated (unsupported construct, panic, step limit) is comptime-error
at the plugin function, once; a plugin file without a matching function is comptime-error; a missing file is bad-manifest.
Plugins run only when the program has no errors.
How secret-leak works (D-3200)#
An intra-function taint analysis over typed bodies.
- Sources: a call
s.reveal()whose receiver has typesecret<T>. - Carriers:
let x = <tainted>andx = <tainted>taint the localx(an assignment of a clean value clears it);+, casts,?,!and methods that returnstr/bytes/Stringfrom a tainted receiver (reveal().trim()) stay tainted. - Sinks: an interpolation
"{x}",.show()of a tainted value,+that builds a string, arguments ofprint/eprintand of functions ofstd.obs/std.logor methods ofLogger/Log, andrespond x. - Not a leak: comparing, hashing, passing the plain text to a function that needs it,
"{secret}"(prints***). - Limits: it does not follow values through calls, fields or collections; an
rpcbody returning the value is checked once services are merged (the response sink isrespondtoday).
How time-mix works (D-3201)#
For + - == != < <= > >= <=> whose left operand is a kind of time (Time, Instant, Date, LocalDateTime, ZonedTime),
the right operand is inferred first; a different kind of time is time-mix (instead of a generic type-mismatch), and so is
Time ± Period (a Time has no calendar). Date/ZonedTime with Period is calendar arithmetic and is allowed.
Исходник: docs/analyzer.md