Lint

is the linter's core: the rule registry, the AST walker, and the public entry points (lint, lint-with, lint-source, lint-source-with). Plugin authors interact with it via register-rule! for a rule over one node and register-file-rule! for one over a whole file, and mark-opt-in! for a rule that should run only when it is asked for.

fix-source

defn

(Fn [(Ref String a)] (Result String ParseErr))

                        (fix-source src)
                    

applies every registered fix to src and returns the rewritten source. Findings whose rule has no fix, and fixes that overlap one already applied, are left alone; the rewrite is repeated until it reaches a fixpoint or a fixed pass limit. Only the bytes of fixed forms change, so formatting and comments elsewhere are preserved. A parse error is returned as such, and clean input comes back byte-identical.

Nothing under a quote or a quasiquote is ever rewritten: the first is data, and a splice makes the arity of the second unknowable at lint time. The argument of an unquote or unquote-splicing inside a quasiquote is ordinary code, and is fixed as such.

fix-source-with

defn

(Fn [(Ref (Fn [(Ref String StaticLifetime)] Bool) a), (Ref String b)] (Result String ParseErr))

                        (fix-source-with keep? src)
                    

like fix-source, but only applies fixes whose rule names satisfy keep?.

fixable?

defn

(Fn [(Ref String StaticLifetime)] Bool)

                        (fixable? name)
                    

true iff at least one fix is registered for the rule called name.

lint

defn

(Fn [(Ref (Array (Box Located)) a)] (Array Diagnostic))

                        (lint forms)
                    

applies every registered rule to every node in forms, recursing into compound forms. Returns one Diagnostic per finding.

A form under a quote is data, and is neither reported nor recursed into. A form under a quasiquote is a template for code, so it is still reported; the argument of an unquote or unquote-splicing inside one is ordinary code again.

Findings covered by an inline ; angler-disable-… directive are dropped, and directives that are unused, name a rule that does not exist, or open like a directive without being one are reported under unused-suppression, unknown-suppression-rule and unknown-suppression-directive.

Rules marked with mark-opt-in! are skipped; ask for them by name through lint-with.

lint-source

defn

(Fn [(Ref String a)] (Result (Array Diagnostic) ParseErr))

                        (lint-source src)
                    

parses src via Reader.parse and lints with every registered rule that is on by default. Returns (Result (Array Diagnostic) ParseErr) so a parse failure surfaces distinctly from a clean parse with diagnostics.

lint-source-with

defn

(Fn [(Ref (Fn [(Ref String StaticLifetime)] Bool) a), (Ref String b)] (Result (Array Diagnostic) ParseErr))

                        (lint-source-with keep? src)
                    

like lint-source, but only runs rules whose names satisfy keep?. An opt-in rule runs if keep? admits it.

lint-with

defn

(Fn [(Ref (Fn [(Ref String StaticLifetime)] Bool) a), (Ref (Array (Box Located)) b)] (Array Diagnostic))

                        (lint-with keep? forms)
                    

like lint, but only runs rules whose names satisfy keep?. An opt-in rule runs if keep? admits it: the caller is stating the policy.

list-rules

defn

(Fn [] (Array Rule))

                        (list-rules)
                    

returns a copy of the currently registered rule registry, file rules included. Useful for angler --list-rules and for plugins that want to inspect the active rule set.

load-rules-from-source!

defn

(Fn [(Ref String a)] (Result Int ParseErr))

                        (load-rules-from-source! src)
                    

parses src as a rules file and registers each rule found. Each top-level form must be an array literal [<pattern> "name" "message"]. Symbols starting with ? in the pattern are metavariables, just as in register-pattern-rule!. Non-conforming top-level forms are silently skipped. Returns the number of rules loaded, or a parse error.

lst-head?

defn

(Fn [(Ref Form a), (Ref String b)] Bool)

                        (lst-head? f name)
                    

true iff f is (Form.Lst …) whose first child is a Form.Sym matching name.

mark-opt-in!

defn

(Fn [String] ())

                        (mark-opt-in! name)
                    

marks the rule called name as off by default, so it runs only when it is asked for by name. Use it for a rule whose finding is a judgement call rather than a defect, so registering it does not change what an existing run reports.

lint and lint-source skip an opt-in rule. lint-with and lint-source-with run whatever their keep? admits — the caller is already stating the policy — and the CLI runs an opt-in rule when --only names it.

nth-form

defn

(Fn [(Ref (Array (Box Located)) a), Int] (Ref Form a))

                        (nth-form items i)
                    

pulls the Form out of the i-th Located child of an Array (Box Located). Most rules want the structural shape, not the position, when descending into a compound form's children.

nth-located

defn

(Fn [(Ref (Array (Box a)) b), Int] (Ref a b))

                        (nth-located items i)
                    

returns the i-th Located child of an Array (Box Located). Use it over nth-form when a rule or fix needs the child's source position as well as its shape.

opt-in?

defn

(Fn [(Ref String StaticLifetime)] Bool)

                        (opt-in? name)
                    

true iff the rule called name was marked off by default with mark-opt-in!.

register-file-rule!

defn

(Fn [String, String, (Fn [(Ref (Array (Box Located)) a)] (Array Diagnostic))] ())

                        (register-file-rule! name description f)
                    

registers a whole-file rule under name with a one-line description. f gets the file's top-level forms and returns every finding it has; use it for what a Rule cannot see from one node — whether a form sits at top level, or whether something written elsewhere in the file exempts it.

A file rule lists under list-rules, answers to --only and --disable, and honours inline ; angler-disable-… directives, all like a node rule. It cannot carry a --fix. It is on by default like a node rule; pair it with mark-opt-in! if it should not be. The register-rule! caveat about top-level def ordering applies here too.

register-fix!

defn

(Fn [String, (Fn [(Ref Located a), (Ref String b)] (Maybe Fix))] ())

                        (register-fix! name fn)
                    

registers a rewrite for the rule called name. fn receives a node the rule fired on and the full source it came from, and returns the Fix to apply, or Nothing if this particular node has no rewrite that is certainly semantics-preserving.

Several fixes may share a rule name; the first one to return Just wins. A rule without a fix is reported by lint exactly as before and skipped by fix-source.

register-pattern-fix-rule!

defn

(Fn [String, String, (Ref String a), String, String] ())

                        (register-pattern-fix-rule! name description pattern-str message template)
                    

is register-pattern-rule! plus a rewrite: template-str is source text in which every ?name is replaced by the original bytes the metavariable matched, and the matched node is replaced by the result. A ? that is not followed by a name character is literal text, so (unless (empty? ?x) ?y) is a valid template.

(Lint.register-pattern-fix-rule! @"double-not"
                                 @"(not (not x)) is just x"
                                 "(not (not ?x))"
                                 @"(not (not x)) is just x"
                                 @"?x")

Only register a template that is unconditionally semantics-preserving: fix-source applies it without asking.

register-pattern-rule!

defn

(Fn [String, String, (Ref String a), String] ())

                        (register-pattern-rule! name description pattern-str message)
                    

registers a lint rule that fires when pattern-str (a single Carp form) structurally matches a node. Symbols starting with ? followed by one or more name characters and nothing else are metavariables: they match any form, and if the same ?name appears more than once the bound forms must have equal string representations. Any other symbol starting with ? is an ordinary symbol and matches literally.

(Lint.register-pattern-rule! @"set-self"
                             @"(set! x x) is a no-op"
                             "(set! ?x ?x)"
                             @"(set! x x) is a no-op")

register-rule!

defn

(Fn [String, String, (Fn [(Ref Located a)] (Maybe Diagnostic))] ())

                        (register-rule! name description fn)
                    

registers a rule under name with a one-line description. Subsequent runs of Lint.lint / Lint.lint-source include it. Built-ins call this at load time; plugins do the same after loading angler.

Caveat: Carp initializes global defs in dependency-graph order, not in textual or load order. If your plugin uses a top-level def to call register-rule!, its slot in the registry — and therefore its slot in the diagnostic output for any one form — is unspecified relative to angler's built-ins. Rule firing is still correct; only the order of diagnostics on a single node varies. If you need strict ordering, register from main instead of from a top-level def.

sym?

defn

(Fn [(Ref Form a), (Ref String b)] Bool)

                        (sym? f name)
                    

true iff f is a Form.Sym whose dotted name equals name. name may itself contain . to match a multi-segment symbol — e.g. (sym? f "Parser.try") matches Parser.try.