A linter is a Nim module that declares one or more types: Check and exports a types: Linter value. Like any other Nim module, it can sit next to the config that uses it in your project or be installed with Nimble.
A minimal linter
This is an example linter from the tests:
import std/strutils import compiler/ast import lint #startSettings type VarNamesSettings* = object minLength*: int = 3 maxLength*: int = 12 ConstNamesSettings* = object prefix*: string = "k" #endSettings proc runVarNames(ast: PNode, s: VarNamesSettings): seq[Finding] = for node in ast.sons: if node.kind == nkVarSection: for def in node.sons: if def[0].kind != nkIdent: continue let name = def[0].ident.s if name == "killTheLinter": raise newException(ValueError, "There is a bug in this check") if name.len < s.minLength: result.add def[0].toFinding( "variable `" & name & "` is shorter than " & $s.minLength & " characters" ) elif name.len > s.maxLength: result.add def[0].toFinding( "variable `" & name & "` is longer than " & $s.maxLength & " characters" ) #startRunConstNames proc runConstNames(ast: PNode, s: ConstNamesSettings): seq[Finding] = if s.prefix.len == 0: return for node in ast.sons: if node.kind == nkConstSection: for def in node.sons: if def[0].kind != nkIdent: continue let name = def[0].ident.s if not name.startsWith(s.prefix): result.add def[0].toFinding( "constant `" & name & "` should start with `" & s.prefix & "`" ) #endRunConstNames let nameLinter* = Linter() varNames* = nameLinter.addCheck(runVarNames) constNames* = nameLinter.addCheck(runConstNames)
Three things make this a linter:
- a settings type, which may be empty
- a proc taking the file's AST and the settings, returning a sequence of types: Finding
- a types: Linter value and one types: Check per rule, created with types: addCheck
Checks receive the parsed file as a PNode from Nim's own compiler AST module (compiler/ast). Lint neither converts the tree nor provides a tree type of its own: the node a check walks is exactly the node the parser produced.
Writing a check means importing compiler/ast and matching on nodes. A check doesn't modify the tree: the same AST is shared by every step that runs against the file.
--path:"$nim/"
Put that in a config file affecting your linter and Lint config, e.g. linter.nims for linter.nim.
Settings
Settings are a plain object. Give the fields a default value in the type definition.
type VarNamesSettings* = object minLength*: int = 3 maxLength*: int = 12 ConstNamesSettings* = object prefix*: string = "k"
A config that provides settings has its keys checked against this type, so a misspelled key is a compile error.
Findings
A types: Finding carries a position and a message, and nothing else.
A check does not know the file it runs against, the linter it belongs to, or the severity its findings are reported with. The runner attaches all of that when it collects findings, which is what lets the same check run at different severities in different steps.
Use types: toFinding to build one from an AST node; it extracts the position from the node:
proc runConstNames(ast: PNode, s: ConstNamesSettings): seq[Finding] = if s.prefix.len == 0: return for node in ast.sons: if node.kind == nkConstSection: for def in node.sons: if def[0].kind != nkIdent: continue let name = def[0].ident.s if not name.startsWith(s.prefix): result.add def[0].toFinding( "constant `" & name & "` should start with `" & s.prefix & "`" )