Liking cljdoc? Tell your friends :D

Developing a new rule

The various checks that Splint performs are called "rules". Each rule looks for a single suspicious form or multiple variations of one, called a :pattern or :patterns for multiple. Splint checks the pattern against every appropriate form in every file it checks, and if there's a match, it calls the function :on-match to potentially create and store a Diagnostic.

When they're initialized, rules store the type of data that they're initially looking for, called the :init-type. This helps limit the number of rules Splint has to check for each form. (For example, when looking at (foo bar), only rules with :init-type :list will be selected.) :init-type is automatically derived from the provided :patterns, but can be deliberately specified to help in ambiguous situations.

For each file, Splint performs a depth-first descent into every form, starting with any full-file rules that operate on a vector of the top-level forms (:init-type :file). Quoted forms are not checked, but syntax-quoted forms are. This is because quoted forms aren't usually intended to be evaluated code, but syntax-quoted forms are.

Writing a new rule

Rules are fairly simple. The call to defrule takes a fully qualified name (namespace being the rule's genre), a docstring with examples, and the rule map. The resulting "rule" is both stored in the global-rules atom as well as defined with def as a var in the calling namespace (dropping the rule-name's namespace).

To load them, use the -r/--require command line arg with -r path/to/file.clj or in the config file with {require ["path/to/file.clj"]}. The specified files are loaded with clojure.core/load-file at run-time. (There is currently no support for loading rules from jars or dependencies.) The rule must be enabled in .splint.edn for it to be used, either by rule name (example/new-rule {:enabled true}) or by genre (example {:enabled true}). (See configuration for more details.)

WARNING: Unlike other tools, this doesn't use SCI. Loading arbitrary code is inherently unsafe, so don't load code you don't know!!!!!!

Rule name and genre

Rule names should be short descriptions of the problem domain or the preferred solution. Genres are meant to loosely group rules according to their focus.

The genres are described in Genre Overview.

Docstrings

Docstrings are rendered with cljdoc's markdown parser, and there's light preprocessing that extracts the code examples and wraps them in code blocks to give them syntax highlighting. The text of the docstring should give only as much detail as necessary for someone new to the rule's intent. If there's a larger discussion online, link to it in the config edn file.

For example, "How to ns" is a popular article about how to structure ns forms. The docstring of a potential style/how-to-ns should not include it in full, the docstring should say something like, "Alessandra Sierra's recommended ns style recommends putting :refer-clojure first, :require second, and :import third. Use vectors with :requires, and lists with :import. Please see the link for further details."

When contributing new rules to Splint, the examples must follow a specific format: They start with Examples: surrounded by blank lines, and then alternating ; avoid and ; prefer lines. All of the lines after Examples: will be wrapped in code fences marked as clojure.

For example, lint/assoc-fn:

  "...

  Examples:

  ; avoid
  (assoc coll :a (+ (:a coll) 5))
  (assoc coll :a (+ (coll :a) 5))
  (assoc coll :a (+ (get coll :a) 5))

  ; prefer
  (update coll :a + 5)
  "

will be rendered as:


Examples

; avoid
(assoc coll :a (+ (:a coll) 5))
(assoc coll :a (+ (coll :a) 5))
(assoc coll :a (+ (get coll :a) 5))

; prefer
(update coll :a + 5)

Rule map

The rule name has two major requirements: either a :pattern form or a :patterns vector of forms, and either a :replace pattern or an :on-match function.

Patterns are described in detail in Patterns, but to give a brief overview, they are quoted forms that are compiled into functions that return nil if no match is found or a map (empty if no bindings are used or full of symbol-to-bound value pairs if bindings are used). Literals are matched with =, collection literals (lists, vectors, maps, and sets) have their type and size checked and then each expected internal is matched the same way. Any symbol that starts with a question-mark is treated as a binding.

For example, if you want to match on an if that has nil in the else branch, you would write '(if ?pred ?truthy nil). This is converted into a function that checks (in this order): The form is a list. The list is 4 elements long. The first element is the symbol 'if. The second element can be any value and is bound to the symbol ?pred. The third element can be any value and is bound to the symbol ?truthy?. The fourth element is nil.

The macro noahtheduke.splint.pattern/pattern builds the matching function.

A simple replacement pattern is defined with :replace. This can be a quoted form of any shape and will be substituted in when printing diagnostics. It uses some of the same logic as the patterns. For our purposes, the main one is that any symbols that start with a quotation mark is substituted for the value of that symbol in the binding output from the :pattern. (The rest of the details are also in Patterns.)

For example, if the pattern '(if ?pred ?truthy nil) matches, then the replacement could be '(when ?pred ?truthy), which would insert the values that ?pred and truthy matched on into the replacement form.

To handle more complex replacement logic, use :on-match. It must be a function that accepts 4 arguments: the context object (ctx), the currently matched rule object, the current form that is being evaluated, and the bindings map from the pattern. Unlike the limited DSL available in the patterns, this is regular Clojure and can perform any action one likes.

If :on-match returns a Diagnostic, then the diagnostic is stored in the context. The helper function ->diagnostic takes the rule and form to point to the right spot in the code, and can optionally take a map with :replace-form and :message keys to set the diagnostic's reported alternative form and to override the rule's message object with a more specific message to print in all diagnostics.

If :on-match does not return a Diagnostic, then the rule is considered to have not made a match (even tho the pattern returned a logical value). This allows for further refinement when writing rules without having to extend the pattern language unduly.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close