This changelog is loose. Versions are not semantic, they are incremental. Splint is not meant to be infrastructure, so don't rely on it like infrastructure; it is a helpful development tool.
lint/misplaced-type-hint: Prefer (defn make-str ^String [] "abc") over (defn ^String make-str [] "abc"). Only checks defn forms at the moment.edamame to 1.4.28.style/redundant-nested-call: Prefer (+ 1 2 3 4) to (+ 1 2 (+ 3 4)).style/into-literal ignores when in a threaded context.lint/def-fn ignores when in a syntax-quoted context.lint/defmethod-names: Require that defmethod calls define a name for the function body. This helps improve stack traces. Suggested names are built from the dispatch value and cannot be trusted to be unique or usable, so while the rule is safe, it does not support autocorrect. Disabled by default.style/is-eq-order relaxed expected input to accept any non-quoted list. (See #25.):autocorrect.--interactive has been added to the CLI options, making it usable. (oops)--interactive now applies :autocorrect so it doesn't need to be specified.Big feature: Safety and Autocorrection
Every rule has been marked as safe or unsafe. Safe rules don't generate false positives and any suggested alternatives can be used directly. Unsafe rules may generate false positives or their suggested alternatives may contain errors.
Rules that are safe may also perform autocorrection, which is tracked in defrule with :autocorrect. Rules may only perform autocorrection if they're safe.
The Rules Overview has been expanded as well.
edamame 1.4.27 supports the Clojure 1.12 array syntax: Integer/1.@note, @safety, and @examples. All existing # Examples have been converted to @examples, and the relevant rule docstrings have been updated.:autocorrect to defrule, :safe to config schema.lint/redundant-str-call ignores when used in threading macros. (See #20.)lint/redundant-call ignores when used in threading macros. (See #21.)nil when input is too short in patterns, which fixes subtle issues with lint/cond-else.support-clojure-version? only compare minor versions if major version numbers match, and likewise with incremental/minor version numbers.lint/redundant-str-call: Don't call str on input that's guaranteed to be a string: Prefer "foo" to (str "foo"), (str "foo" bar) to (str (str "foo" bar)), and (format "foo%s" bar) to (str (format "foo%s" bar)). (See clj-kondo#2323 for inspiration.)lint/duplicate-case-test: Don't use the same case test constant more than once.lint/locking-object: Prefer to lock on a symbol bound to (Object.).--only RULE cli flag to run only specified rules or genres. Can be used multiple times. (#13)clojure.pprint to fipp for pretty-printing code. Fast and easy to extend.Splint encountered an error: "" which is unhelpful and shameful.default.edn are now :links, a vector of strings. This allows for listing multiple references.?? only 1 or 2 arguments, and if provided, that the predicate is a symbol.json and json-pretty now work with Babashka, by relying on Babashka's built-in chehire.core instead of clojure.data.json. This shouldn't result in any observable differences. I'd use cheshire.core for both, but cheshire.core is much bigger and more complicated than clojure.data.json, and it's a pain in the ass imo.deftest (including in new_rule.tmpl) back to using defexpect. Sean fixed the 3-arg issue when I raised it in https://github.com/clojure-expectations/clojure-test/issues/35, and it's nice to only import a single namespace instead of multiple.naming/conventional-aliases docs.naming/record-name now uses camel-snake-kebab to check and convert the given record name to PascalCase.:method-value style to style/new-object to suggest Foo/new instead of Foo..lint/dot-class-method and lint/dot-obj-method when lint/prefer-method-values is enabled.runner functions.(:config ctx) to (:rules ctx) as map of rule-name to rule map. Add (:rules-by-type ctx), a map of simple-type to vector of rule names. Change check-all-rules-of-type to reduce over rule names and pull the rule map from ctx.lint/duplicate-field-name.global. (See #11 and #12)lint/assoc-fn when f is a macro. Covered or explicitly, no good generalized solution at the moment. (See #15.)--print-config properly includes the genre of printed rules.style/is-eq-order: Prefer (is (= 200 status)) over (is (= status 200)) when writing assertions.style/prefer-for-with-literals: Prefer (for [item coll] {:a 1 :b item}) over (map #(hash-map :a 1 :b %) coll). (See #10.)-r/--require cli flag that can be used multiple times and require top-level config option that takes a vector of strings. These are loaded with load-file at run-time to allow for custom rules to be written and used. (See #8.) This is inherently unsafe, so don't run code you don't know.?* or ?+ will immediately return the rest of the current input instead of accumulating it one-by-one.lint/warn-on-reflection now checks that the file contains a proper ns form before issuing a diagnostic.General performance increases in rules:
lint/body-unquote-splicinglint/if-else-nillint/underscore-in-namespacelint/warn-on-reflectionmetrics/parameter-countnaming/conversion-functionnaming/predicatenaming/record-namenaming/single-segment-namespacestyle/def-fnstyle/eq-zerostyle/prefer-clj-stringstyle/prefer-condpstyle/reduce-strstyle/single-key-instyle/tostringstyle/useless-doRemove documentation about ?_ short form, as it's covered by the existing ? and _ binding rules.
Expand ?foo short-forms in patterns to their (? foo) special form. Simplifies matching functions, makes the pattern DSL more consistent. Now ?|foo will throw immediately instead of part-way through macroexpansion.
Updated pattern docs with a small example at the top.
Simplified ?| matcher logic to use a set, as that's faster than creating multiple read-form patterns in a let block and checking each one.
Obj/staticMethod when given (. Obj (staticMethod)) in lint/dot-class-usage.naming/conversion-functions when there's no - in the part before -to-. (Will warn on f-to-g, will not warn on expect-f-to-c.)lint/assoc-fn.lint/prefer-method-values: Prefer (^[] String/toUpperCase "noah") to (.toUpperCase "noah").lint/require-explicit-param-tags: Prefer (^[File] File/mkdir (io/file \"a\")) to (File/mkdir (io/file \"a\")). Prefer (^[String String] File/createTempFile \"abc\" \"b\") to (^[_ _] File/createTempFile \"abc\" \"b\"). Has :missing, :wildcard, and :both styles, which check for lack of any :param-tags, usage of _ in a :param-tags, and both. Defaults to :wildcard.lint/prefer-method-values in performance/dot-equals.new_rule.tmpl to use deftest. defexpect is a thin wrapper and has the annoying "if given two non-expect entries, wrap in expect", which doesn't work when we use custom expect macros.re-find: and string: syntaxes for path :excludes. re-find uses clojure.core/re-find, so the regex doesn't have to match the entire file path, just any portion. string uses clojure.string/includes?, so a fixed string anywhere in the file path.^[]/:param-tags feature.lint/underscore-in-namespace: Prefer (ns foo-bar) to (ns foo_bar).performance/dot-equals: Prefer (.equals "foo" bar) to (= "foo" bar). Only cares about string literals right now.performance/single-literal-merge: Prefer (assoc m :a 1 :b 2) to (merge m {:a 1 :b 2}). Has :single and :multiple styles, either a single assoc call or threaded multiple calls.performance/into-transducer: Prefer (into [] (map f) coll) to (into [] (map f coll)).style/trivial-for: Prefer (map f items) over (for [item items] (f item)).style/reduce-str: Prefer (clojure.string/join coll) over (reduce str coll).global top-level .splint.edn config that applies to all rules.:excludes in both global and rules-specific configs. Accepts a vector of java.nio.file.FileSystem/getPathMatcher globs or regexes. When in global, matching files are removed from being processed at all. When in a specific rule, the rule is disabled before matching files are checked.min, max, and distinct? to lint/redundant-call.style/set-literal-as-fn default to false. It's not idiomatic and I don't know that it's any faster either.--auto-gen-config, adds test.performance/assoc-many should only trigger when there are more than 1 pair.The big feature here is adding support to run splint without specifying paths. Now Splint will read the deps.edn or project.clj file in the current directory and check the paths in the base definition as well as :dev and :test aliases/profiles if no path argument is given. Splint still doesn't support specifying paths in .splint.edn, nor does it allow for using paths from a project file as well as additional paths when called, but those are planned.
The second big change is moving from the old DSL to the new pangloss/pattern inspired DSL. More flexible, more usable, overall better.
The third is adding performance rules. These are off by default and are designed for those who want to pinch their cycles. Some affect common code (get-in) and some are much more rare (satisfies), but they're all designed to help you be mindful of slower paths.
spat.parser to splint.parser.spat.pattern to splint.pattern. RIP spat, you treated me well for 9 months, but keeping spat and splint separate is no longer helpful.performance/assoc-many: Prefer (-> m (assoc :k1 1) (assoc :k2 2)) over (assoc m :k1 1 :k2 2).performance/avoid-satisfies: Do not use clojure.core/satisfies?, full stop.performance/get-in-literals: Prefer (-> m :k1 :k2 :k3) over (get-in m [:k1 :k2 :k3]).performance/get-keyword: Prefer (:k m) over (get m :k).style/redundant-regex-constructor: Prefer #"abc" over (re-pattern #"abc").->list: concrete list building instead of apply . Useful anywhere a lazy-seq might be returned otherwise. seq/vec input: 40/43 us -> 28/15 usmapv*: mapv but short-circuits empty input and uses object-array. Still unsure of this one. 36 us -> 36 usrun!*: run! but short-circuits empty input and uses .iterator to perform the side-effects. Does not support reduced. 7 us -> 950 nspmap*: Avoids lazy-seq overhead and relies on Java's built-in Executors. 3.34 s -> 202 mswalk* and postwalk*: Primarily useful in replace, but may prove useful otherwise. Only supports simple-type defined types. 72 us -> 25 ussplint.config/read-project-file returns a map of :clojure-version and :paths, taken from the project file (deps.edn or project.clj) in the current directory. If no file is found, :paths is nil and :clojure-version is pulled from *clojure-version*.:min-clojure-version in defrule, allowing for rules to specify the minimum version of clojure they require. Rules that are below the supported version are disabled at preparation time and can't be enabled during a run. Acceptable shape is a map of at least one of :major, :minor, and :incremental.
test-helpers/with-temp-file and test-helpers/print-to-file! to test file contents.spat.parser/parse-string and spat.parser/parse-string-all into the test-helper namespace, and replace with parse-file which accepts the file-obj map.splint.config/slurp-edn to read config files, parsed with edamame.:spat/lit metadata to :splint/lit. :spat/lit still works for the time being, but no promises.splint.printer/print-results now accepts only the results object, which should additionally have :checked-files and :total-time.simple, full, and clj-kondo now print the number of files checked as well: "Linting took 1ms, checked 3 files, 3 style warnings"splint.replace/revert-splint-reader-macros into splint.printer where it belongs.symbol to correctly print unprintable special characters by converting sexprs to strings and then converting those to symbols.simple-type and drop-quote to splint.utils.style/prefer-clj-string: Prefer clojure.string functions over raw interop. Defaults to true.--[no]summary cli flag to print or not print the summary line.:filename in Diagnostic is now a java.io.File object, not a string. This is propogated through everything. I suspect no one is using these so I think I could change the Diagnostic as well, but maybe I'll wait a min.make-edamame-opts now accepts both features and ns-state, and parse-string and parse-string-all take in features instead of ns-state.cljc files twice, both clj and cljs, with their respective sides of the reader conditionals applied.lint/warn-on-reflection only runs in clj files.farolero. Didn't provide any benefits over judicious try/catch use. :(matcher-combinators.core/Matcher protocol to java.io.File, making match? work nicely with both strings and file objects.rules-by-type from a map of simple-type -> map of rule name -> rule to simple-type -> vec of rule.@, not splint/deref, etc).lint/warn-on-reflection: Require that (set! *warn-on-reflection* true) is called after the ns declaration at the start of every file. Defaults to false.--config. Add --print-config. No timeline for removal of --config (maybe never?).edn / edn-pretty output: Print diagnostics as edn using clojure.core/prn and clojure.pprint/pprint.json and json-pretty keys are now sorted.metrics/parameter-count: Function parameter vectors shouldn't have more than 4 positional parameters. Has :positional and :include-rest styles (only positional or include & args rest params too?), and :count configurable value to set maximum number of parameters allowed.-s / --silent command line flag to print literally nothing when running Splint.json output: Print diagnostics as json using clojure.data.json.json-pretty output: Same as json but prettified with pprint.:checked-files.corpus files to handle large-scale tests.dev/.splint.runner/run-impl to decomplect processing cli options and returning a status code from performing the actual config loading and rule building and running.check-all to properly call the existing architecture instead of mock it, to accurately test the run-impl flow.splint.runner errors. Shows no signs of slowing down the app, so will investigate other areas for usage as well.defn metadata when fn name exactly matches defn or defn- and second form is a symbol.--no-parallel was producing a lazy seq, now consumes to actually check all files. Oops lol.nil parent form instead of treating the whole file as a top-level vector of forms. Fixes naming/lisp-case.attr-maps to defn metadata when parsing defn forms.--parallel and --output, now those are added later (see #5).lint/if-not-do, style/when-not-do.metrics/fn-length: Function bodies shouldn't be longer than 10 lines. Has :body and :defn styles, and :length configurable value to set maximum length.test-helpers/expect-match to assert on submatches, transition all existing check-X functions to use it instead.defn forms in postprocessing and attach as metadata instead of parsing in individual rules.style/multiple-arity-order with :arglists metadata.#(.someMethod %) in lint/fn-wrapper.and and or in style/prefer-condp.io/resource issue..class files from jar.-v and --version cli flags to print the current version.--config TYPE cli flag to print the diff, local, or full configuration.splint.rules.helpers.parse-defn when trying to parse ill-formed function definitions.#(do [%1 %2]) in style/useless-do, add docstring note about it.naming/single-segment-namespace: Prefer (ns foo.bar) to (ns foo).lint/prefer-require-over-use: Prefer (:require [clojure.string ...]) to (:use clojure.string). Accepts different styles in the replacement form: :as, :refer [...] and :refer :all.naming/conventional-aliases: Prefer idiomatic aliases for core libraries ([clojure.string :as str] to [clojure.string :as string]).naming/lisp-case: Prefer kebab-case over other cases for top-level definitions. Relies on camel-snake-kebab.style/multiple-arity-order: Function definitions should have multiple arities sorted fewest arguments to most: (defn foo ([a] 1) ([a b] 2) ([a b & more] 3))lint/fn-wrapper introduced in v1.2.3.*warn-on-reflection* to all rules and rule template.:spat/import-ns metadata as way to track when a symbol has been imported.noahtheduke.spat.pattern/simple-type for performance.volatile instead of atom for bindings in noahtheduke.spat.pattern.keep to reduce to avoid seq and laziness manipulation.some-> where appropriate for short-circuiting.&&. rest args and parsed lists in :on-match handlers by attaching :noahtheduke.spat.pattern/rest metadata to bound rest args.edamame to v1.3.21 to handle #:: {:a 1} auto-resolved namespaced maps with spaces between the colons and the map literal.lint/thread-macro-one-arg supports :inline and :avoid-collections styles.:updated field in configuration edn, show in rule docs.:guide-ref for style/prefer-clj-math.<hr> between each rule's docs.lint/dorun-map.deploy justfile recipe.markdown output: Same text as full but with a fancy horizontal bar, header, and code blocks.:chosen-style allows for rules to have configuration and different "styles". The first supported is lint/not-empty? showing either seq or not-empty.ctx is no longer an atom, but a plain map. The :diagnostics entry is now the atom.splint.runner/check-form returns the entire updated ctx object instead of just the diagnostics. (I'm not entirely sure that's reasonable, but it's easily changed.)lint to style genre:
apply-strapply-str-interposeapply-str-reverseassoc-assocconj-vectoreq-falseeq-nileq-trueeq-zerofilter-complementfilter-vec-filtervfirst-firstfirst-nextlet-domapcat-apply-applymapcat-concat-mapminus-oneminus-zeromultiply-by-onemultiply-by-zeroneg-checksnested-additionnested-multiplynext-firstnext-nextnot-eqnot-nilnot-some-predplus-oneplus-zeropos-checkstostringupdate-in-assocuseless-dowhen-dowhen-not-callwhen-not-dowhen-not-emptywhen-not-notctx as first argument to :on-match functions to pass in config to rules. Update functions in splint.runner as necessary.noahtheduke.spat.pattern functions.:spat/lit metadata to treat special symbols in pattern DSL as their literal values.clojure.core, then in noahtheduke.splint.rules.helpers.:var to :binding.style/def-fn: Prefer (let [z f] (defn x [y] (z y))) over (def x (let [z f] (fn [y] (z y))))lint/try-splicing: Prefer (try (do ~@body) (finally ...)) over (try ~@body (finally ...)).lint/body-unquote-splicing: Prefer (binding [max mymax] (let [res# (do ~@body)] res#)) over (binding [max mymax] ~@body).--parallel and --no-parallel for running splint in parallel or not. Defaults to true.:uneval config option for :splint/disable.build.clj to resources/SPLINT_VERSION.naming/record-name: Add :message.style/prefer-condp: Only runs if given more than 1 predicate branch.style/set-literal-as-fn: Allow quoted symbols in sets.Actually wrote out something of a changelog.
lint/duplicate-field-name: (defrecord Foo [a b a])naming/conversion-functions: Should use x->y instead of x-to-y.style/set-literal-as-fn: Should use (case elem (a b) true false) instead of (#{'a 'b} elem):new-rule task now creates a test stub in the correct test directory.#_:splint/disable is treated as metadata on the following form and disables all rules. #_{:splint/disable []} can take genres of rules ([lint]) and/or specific rules ([lint/loop-do]) and disables those rules. See below (Thoughts and Followup) for discussion and Configuration for more details.defrule now requires the provided rule-name to be fully qualified, and doesn't perform any *ns* magic to derive the genre.:init-type in defrule to handle symbol matching.:dispatch reader macros provided by Edamame now wrap their sexps in the appropriate (splint/X sexp) form, to distinguish them from the symbol forms. Aka #(inc %) is now rendered as (splint/fn [%1] (inc %1)), vs the original (fn* ...), or #'x is now (splint/var x) vs (var x). This allows for writing rules targeting the literal form instead of the symbol form, and requires that rule patterns rely on functions in noahtheduke.splint.rules.helpers to cover these alternates.noahtheduke.splint.rules.helpers as an autoresolving namespace so rules can use predicates defined within it without importing or qualifying.violation to diagnostic.lint/duplicate-field-name wasn't checking that ?fields was a vector before calling count on it.I want another parser because I want access to comments. Without comments, I can't parse magic comments, meaning I can't enable or disable rules inline, only globally. That's annoying and not ideal. However, every solution I've dreamed up has some deep issue.
Edamame is our current parser and it's extremely fast (40ms to parse clojure/core.clj) but it drops comments. I've forked it to try to add them, but that would mean handling them in every other part of the parser, such as syntax-quote and maps and sets, making dealing with those objects really hard. :sob:
Rewrite-clj only exposes comments in the zip api, meaning I have to operate on the zipper objects with zipper functions (horrible and slow). It's nice to rely on Clojure built-ins instead of (loop [zloc zloc] (z/next* ...)) nonsense.
clj-kondo is faster than rewrite-clj and has a nicer api, but the resulting tree isn't as easy to work with as Edamame and it's slower. Originally built Spat in it and found it to be annoying to use.
parcera looked promising, but the pre-processing in parcera/ast is slow and operating on the Java directly is deeply cumbersome. The included grammar also makes some odd choices and I don't know ANTLR4 well enough to know how to fix them (such as including the : in keyword strings). Additionally, if I were to switch, I would have to update/touch every existing rule.
After tinkering with Edamame for a bit, I've found a solution that requires no changes to edamame to support: #_:splint/disable. This style of directive applies metadata to the following form: #_{:splint/disable [lint/plus-one]} (+ 1 x). Edamame normally discard #_/discarded forms, so on Borkdude's recommendation, I use str/replace to convert it at parse-time to metadata. This uses an existing convention and handles the issue of disabling multiple items or disabling for only a certain portion of the file.
Update readme with some better writing.
dev/sorted-rules-require for internal use only:no-doc.lint/cond-else to style/cond-else.Renamed to Splint! Things are really coming together now.
lint/assoc-in-one-arglint/update-in-one-argnaming/predicatenaming/record-namestyle/let-ifstyle/let-whenstyle/new-objectstyle/prefer-booleanstyle/prefer-condpstyle/redundant-letstyle/single-key-in-M:gen-docs for rule documentation generation and formatting.-M:new-rule task to generate a new rule file from a template.-M:deploy task to push to clojars.lint/with-meta-vary-meta to style/prefer-vary-meta.lint/thread-macro-no-arg to lint/redundant-call.Initial release of spat, announcement on Clojurian Slack and bbin installation set up. Contains working pattern matching system, a bunch of rules, and a simple runner.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |