torb lint

torb lint reports the style rules the type checker leaves alone - Self, a question field, an unread binding, an unlabeled literal, a redundant Some or closure, private(var) - and uses of deprecated declarations, with a fix where certain.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

lint is the linter of torb: the rules a program can break and still type check, because they are about how it is written and not about what it means. Every finding names its rule, and a rule that knows the fix for certain carries it, so torb lint --fix is also how a change of a convention migrates the code that follows the old one.

Synopsis#

torb lint [--fix] [--rule <id>]... [path]...

Rules (every one runs on every program, and none can be switched off):
  self-name          Inside a `type` or an `extend`, its own name is written `Self` (fix)
  question-field     A `Bool` field is an adjective or a participle, not a question that starts with `is` (fix; runs the checker)
  unread-binding     A name that `const`, `for` or a closure binds and nothing reads is written `_` (fix; runs the checker)
  labeled-literal    `true`, `false` or `None` for a `Bool` or an optional carries the label (fix; runs the checker)
  redundant-wrap     No `Some(value)` or `Ok(value)` where the value wraps itself (fix; runs the checker)
  redundant-closure  A function is passed by its name, not by a closure that only calls it (fix; runs the checker)
  protected-field    A field only its own file writes is `protected var`, not `private(var)` (fix)
  deprecated         A use of a deprecated declaration becomes the replacement it names (fix; runs the checker)

  --fix         Write every fix a rule is certain of, then lint again and report what is left
  --rule <id>   Report or fix only this rule in this run (repeatable); it narrows the output and does not
                change what is valid

What it does#

A path is a file or a directory (default: the current directory), and a directory is read the way torb format reads one: every .trb file below it, without hidden directories, build directories and the files that are broken on purpose. A file that does not parse is named and skipped.

A finding is printed the way torb check prints a diagnostic, with warning in front, and with the id of its rule under it:

$ torb lint --rule self-name std/geometry
warning: `Box<Scalar>` is the type this is declared in: write `Self`
  --> std/geometry/src/box.trb:37:71
   |
37 |   static fn between(first: Vector3<Scalar>, second: Vector3<Scalar>): Box<Scalar> {
   |                                                                       ^^^^^^^^^^^
   = rule `self-name`
   = `torb lint --fix` writes the fix

...
49 findings in 11 of 15 files (self-name 49)

The rules#

self-name: inside a type or an extend, the type's own name is written Self (the owner, 2026-09-26). It finds the name where Self means exactly the same type and nowhere else: in a written type whose type arguments are the type's own parameters in their order (Box<Other> inside type Box<Item> is another type and stays), in an expression of a type without parameters (Point(0, 0), Point.origin, Shape.Circle), and Box<Item>(...) with exactly its own parameters - a bare Box(...) inside a generic type may build another instance, so it stays. A pattern has no Self, so Shape.Circle(radius) becomes .Circle(radius) there. A method with a type parameter named like one of the type's own is left alone, and a trait is not looked into.

question-field: a Bool field is an adjective or a participle (inclusive, shared, exported), and a question that is computed is a method (isEmpty()) - see Naming. A field of a type or of a case whose type is Bool and whose name is is followed by a capital letter is found.

Its fix is the rename torb rename makes: the field gets the word after is (isShared becomes shared) at its declaration and at every use the checker resolves to it, so the rule runs the checker. It offers the fix only where it is certain of that word - the last word after is ends like a participle or an adjective (-ed, -able, -ible, -ive, -ous, -ful, -less, but not -eed) - and where nothing stands in its way: the word is not a keyword, the type has no field or member of that name, and every file that mentions the field was checked without a problem. A noun (isCall), a preposition (isOn) and a keyword (isVar) need a person's word, and the finding names the torb rename that takes it. --fix writes the edit of every use as well; a plain run reports the declaration alone.

unread-binding: a name that an irrefutable pattern binds and that nothing reads is written _ - a const inside a body, what for binds, and the parameters of a closure. The refutable positions are the checker's, where the same thing is an error. Without name resolution the rule stays on the safe side: a name counts as read when it occurs as a word anywhere in the text it could be read in, so a comment, a string and a name bound again all count. A name that starts with _, a var, a using, a binding at the top level of a file and the parameters of a function (which are the labels of its calls) are left alone.

Its fix needs the checker. A value that may hold something with a close() is released at the end of its block, and _ would release it at once, so close() would run earlier (see DESTRUCTORS.md, 2a). The fix is only offered where the checker says the value is plain: a number, a Bool, a Char, a String, Void, a literal type, and a tuple, an Option, a Result, a range or a collection of the prelude made of them. Anywhere else the finding comes without a fix and names using as the other way to write a binding that is only there to be closed at the end of its block.

labeled-literal: true, false or None passed to a parameter that is declared as Bool or as an optional carries the parameter's label (hasCapacity: false), except in a call with a single argument and where the parameter's type is a type parameter - there the literal is the data, not an option. It needs the checker, so a run with it checks the projects of the paths first, and a file the checker has a problem with gets no finding of it. The fix writes the label, and because labeled arguments follow the positional ones it labels every positional argument behind the literal as well, with the parameter the checker matched it to - all but the closure that trails the call: list(3, false, name) becomes list(3, hasCapacity: false, name: name). Nothing moves, so which argument fills which parameter and the order they are evaluated in stay the same. A spread, a variadic parameter or an argument the checker did not match leaves the finding without a fix. Declaring the option last, with a default, is the other way out and a person's choice: a call that passes the default then leaves it out.

fn listEntries(entries: Int, kind: String, hasCapacity: Bool = true): Int {
  entries
}

print listEntries(3, "ArrayList", hasCapacity: false)

redundant-wrap: a written Some(value) or Ok(value) where the value would wrap itself - a value of exactly Value where an Option<Value> or a Result<Value, Failure> is expected becomes Some(value) or Ok(value) on its own (Conversions, rule 8), so the written case is a second spelling. Which calls those are is the checker's answer, because only it knows the expected type; the rule runs it like labeled-literal. A case the value needs is left alone: the inner Some of a nested Option, a value whose type the case's expectation decided (None, Fail(...), a call of a generic function, into()), Ok(void), and a case where nothing is expected. The fix replaces the call with its argument, in parentheses where the call is an operand and the argument an operation.

fn half(value: Int): Result<Int, String> {
  if value % 2 != 0 {
    return Fail "{value} is odd"
  }
  value / 2
}

print half(8)

redundant-closure: a closure that does nothing but call a declared function with its own parameters, each once and in their order, is written as the function: items.map { stripMargin(_) } and items.map { line => stripMargin(line) } are items.map(stripMargin). A declared fn is a value of its function type and can be passed wherever a closure is expected, so the closure is a second spelling. Whether the function means exactly what the closure means is the checker's answer, so the rule runs it like labeled-literal, and it finds only what it can fix:

  • the callee is a declared function with a body - a top-level or a local fn, or a static fn by its path (Uri.from) - and not a method, a native function (whether one can stand as a value is the back ends' question), or a field or a binding that holds a closure, whose value could change between two calls of the closure;
  • the function is not generic, none of its parameters has a default or is lazy or ...variadic, and each is var exactly where the closure's is;
  • its parameter and result types are the closure's own: nothing was converted, wrapped or made a trait value at the call or at an argument, and the closure itself is not quoted or lazy.

Anything else is no finding at all, because a closure that adapts the function is not a second spelling of it. The fix writes the function's name where the closure was. A closure that trails the call moves into the argument list (apply(1) { double(_) } becomes apply(1, double)), with its parameter's label where a labeled argument comes before it or where its position would fill another parameter: run(quiet: true) { tick(_) } becomes run(quiet: true, body: tick).

fn doubled(value: Int): Int {
  value * 2
}

print([1, 2, 3].map(doubled).toList())

protected-field: a field that everybody reads and only the file of its type writes is spelled protected var connections: Int, and private(var) connections: Int, the spelling it replaces, is found and rewritten to it (Fields, rule 4). The checker refuses private(var) with an error that names this rule, which is its fix: the two mean the same, so the fix is certain, and torb lint --fix --rule protected-field is the whole migration of a project. A private(var) field that writes var as well gets no fix, because the one it would get says var twice.

type Server {
  protected var connections: Int = 0

  var fn accepted() {
    connections = connections + 1
  }
}

var server = Server()
server.accepted()
print server.connections

deprecated: every use of a declaration marked deprecated(...) - the warnings torb check prints, as findings (Deprecation). Where the clause names a replacement, the fix writes it at the use - the name, or x() where a field became the method x - and renames the use that imported the declaration, unless the file sees the replacement already. It is how a deprecation of std moves the code of its users: torb lint --fix --rule deprecated is the whole migration. A use the replacement cannot stand in keeps its finding without a fix: a method as the target of an assignment or as the label of a constructor, and a name the file imported under another one, whose use is renamed instead. An argument written with the label of a deprecated parameter gets the label of its replacement (wait: 30 becomes timeout: 30); an argument by position, and a call that passes the replacement as well, keep their finding.

$ torb lint --fix --rule deprecated app
fixed 1 file; what is left:
3 files, no findings

--fix#

Every fix of every finding is applied, a fix that overlaps one taken before it is left out, and a file whose fixed text does not parse is fixed one fix at a time, keeping only the ones that parse. Then the whole lint runs again and reports what is left: the findings without a fix, and whatever a fix could not reach. What --fix wrote still has to pass torb check - a fix is certain about the rule, and the checker is the one that is certain about the program.

No configuration#

Every rule runs on every program, and nothing switches one off: there is no setting in project.trb, no configuration file, no comment that silences a finding and no flag that leaves a rule out. The rules belong to the version of torb, the way the layout of torb format does: every program looks the same and is formatted the same, so the format is part of the language (the owner, 2026-10-07). --rule only narrows one run - it reports or fixes the rules it names - and changes nothing about what is valid; --skip is an argument lint does not recognize.

Exit codes#

0 when nothing was found, 1 when something was (after --fix: when something is left), 2 for an argument lint does not recognize.

What is not decided#

  • The rule named and not built. A case whose type the expected type already names (.SwitchCase(path) for DecisionNode.SwitchCase(path)) needs the expected type of every argument.

Examples#

One rule over one package, with its fix:

$ torb lint --fix --rule self-name std/geometry
fixed 11 files; what is left:
15 files, no findings

The fix of question-field reaches every use, so it runs over every project that uses the fields - for the repository that is all of it:

$ torb lint --fix --rule question-field .

Every fix of the repository applied at once still type checks: torb lint --fix over a copy of the whole checkout, then torb check ., answers no problems.

  • Naming - which rules of a name the parser and the checker enforce, and which are conventions torb lint reports.
  • torb format - the other milestone 8 tool, for layout instead of naming.
  • torb rename - the rename question-field makes, with the word a person chose.
  • torb check - the errors, in the same format.
  • The torb command - every subcommand.
Esc