Skip to content

Latest commit

 

History

History
1023 lines (716 loc) · 31.6 KB

File metadata and controls

1023 lines (716 loc) · 31.6 KB

Rules

Rules are organized by category. Each rule shows its severity and default preset status.

Severity levels: error, warning, info

Presets: Rules belong to one or more built-in presets.

  • :default — low-noise rules suitable for existing codebases
  • :strict — extends :default with opinionated best-practice rules; good for new projects and AI-assisted coding
  • :all — every rule, including metrics and style preferences

The Default column below reflects :default preset membership. Rules marked as "strict" are off in :default but enabled in :strict.

Suppressing violations: Any rule can be suppressed using #+mallet (declaim (mallet:suppress-next :rule-name)). See the README for details.

Rule List

Correctness — Objectively wrong code

Rule Description Severity Default Options
:wrong-otherwise ecase/etypecase with otherwise/t clause error on
:mixed-optional-and-key Mixing &optional and &key parameters error on
:asdf-if-feature-keyword Feature expressions must use keywords in defsystem warning on
:asdf-secondary-system-name Secondary systems must use primary/suffix name warning on
:coalton-missing-to-boolean Coalton lisp Boolean form missing to-boolean warning off

Suspicious — Likely wrong or dangerous patterns

Rule Description Severity Default Options
:no-eval Runtime use of cl:eval warning on
:runtime-intern Runtime use of symbol-interning functions warning strict
:runtime-unintern Runtime use of cl:unintern warning strict
:asdf-operate-in-perform Calling asdf:operate inside :perform warning on

Practice — Commonly accepted best practices

Rule Description Severity Default Options
:no-package-use Use of :use in defpackage warning strict
:no-ignore-errors Use of cl:ignore-errors warning on
:no-allow-other-keys Use of &allow-other-keys in lambda lists warning strict
:double-colon-access Accessing internal symbols via :: warning strict
:error-without-custom-condition Calling error without a custom condition type warning strict
:asdf-component-strings ASDF components should use strings warning on
:asdf-reader-conditional #+/#- reader conditionals in defsystem info strict
:bare-float-literal Float literals should have explicit type markers info strict
:coalton-missing-declare Coalton function define missing declare type signature warning off

Cleanliness — Dead code and unused definitions

Rule Description Severity Default Fix Options
:unused-variables Variables that are never used warning on
:unused-local-functions Local functions that are never called warning on
:unused-local-nicknames Local nicknames that are never used warning on yes
:unused-imported-symbols Imported symbols that are never used warning on yes
:unused-loop-variables Loop variables that are never used info strict
:stale-suppression Suppression directive has no effect warning on

Style — Idiomatic patterns and naming

Rule Description Severity Default Options
:one-package-per-file Files should define their own package info off
:missing-else Use when/unless instead of if without else warning on
:progn-in-conditional Use cond/when/unless instead of bare progn in if/and/or info strict
:missing-otherwise case/typecase without otherwise clause warning strict
:defpackage-interned-symbol Use uninterned symbols in package definitions info strict
:needless-let* Use let when bindings are independent warning on
:special-variable-naming Special variables should be named *foo* info off
:constant-naming Constants should be named +foo+ info off
:asdf-redundant-package-prefix Redundant package prefixes in .asd files info strict
:missing-docstring Top-level definitions missing docstrings info off
:missing-package-docstring Package definitions missing docstrings info off
:missing-variable-docstring Variable definitions missing docstrings info off
:missing-struct-docstring Struct definitions missing docstrings info off
:redundant-progn progn with a single body form is redundant warning strict

Format — Whitespace and file formatting

Rule Description Severity Default Fix Options
:trailing-whitespace Lines with trailing whitespace warning on yes
:no-tabs Tab characters in source warning on
:missing-final-newline Files must end with a newline warning on yes
:closing-paren-on-own-line Closing parens on their own line warning strict
:line-length Lines exceeding maximum length info off :max (100)
:consecutive-blank-lines Excessive consecutive blank lines info off yes :max (2)

Metrics — Code quality measurements

Rule Description Severity Default Options
:function-length Function exceeds maximum line count info off :max (50)
:cyclomatic-complexity Function has high cyclomatic complexity info off :max (20), :variant (standard)
:comment-ratio Function has too many comments info off :max (0.5), :min-lines (5), :include-docstrings (nil)

Correctness

Rules that catch objectively wrong code — constructs that defeat language semantics or cause runtime errors.

:wrong-otherwise

ecase and etypecase should not have otherwise or t clauses.

;; Bad: defeats exhaustiveness checking
(ecase type
  (:a 1)
  (:b 2)
  (otherwise 0))

;; Good
(ecase type
  (:a 1)
  (:b 2))

Severity: error | Default: enabled

:mixed-optional-and-key

Don't mix &optional and &key in lambda lists.

;; Bad: ambiguous parameter binding
(defun foo (x &optional y &key z)
  ...)

;; Good: use only one
(defun foo (x &key y z)
  ...)

Severity: error | Default: enabled

:asdf-if-feature-keyword

Feature expressions in :if-feature and (:feature ...) dependency forms must use keywords. Plain symbols are evaluated as variables at read time and will likely cause errors. Applies only to .asd files. Handles compound feature expressions like (:and ...), (:or ...), and (:not ...).

;; Bad: plain symbols instead of keywords
(defsystem "my-system"
  :depends-on ((:feature unix "cl-cffi-gtk"))
  :components ((:file "main" :if-feature sbcl)
               (:file "other" :if-feature (:and unix linux))))

;; Good: keywords
(defsystem "my-system"
  :depends-on ((:feature :unix "cl-cffi-gtk"))
  :components ((:file "main" :if-feature :sbcl)
               (:file "other" :if-feature (:and :unix :linux))))

Severity: warning | Default: enabled

:asdf-secondary-system-name

Secondary system names in a .asd file must follow the primary/suffix convention. In a file named foo.asd, systems other than "foo" must be named "foo/something". Arbitrary names like "foo-tests" or "bar" are not allowed by ASDF and may cause issues with system resolution.

;; Bad: in my-system.asd
(defsystem "my-system"
  :depends-on ("alexandria"))

(defsystem "my-system-tests"       ; should be "my-system/tests"
  :depends-on ("my-system"))

(defsystem "other-project"          ; unrelated name
  :depends-on ("alexandria"))

;; Good: follows primary/suffix convention
(defsystem "my-system"
  :depends-on ("alexandria"))

(defsystem "my-system/tests"
  :depends-on ("my-system"))

Severity: warning | Default: enabled

:coalton-missing-to-boolean

(lisp Boolean (...) body) forms inside coalton-toplevel must use to-boolean to convert CL generalized booleans to Coalton True/False. Without it, CL predicates return nil instead of False, causing incorrect runtime behavior.

(coalton-toplevel
  ;; Bad: CL predicate returns nil, not False
  (declare even? (Integer -> Boolean))
  (define (even? n)
    (lisp Boolean (n)
      (cl:evenp n)))

  ;; Good: to-boolean converts nil/T to False/True
  (declare even? (Integer -> Boolean))
  (define (even? n)
    (lisp Boolean (n)
      (to-boolean (cl:evenp n)))))

Detection: Recursively searches all nesting levels inside coalton-toplevel for (lisp Boolean (...) body) patterns. Recognizes both to-boolean and package-qualified variants like coalton-library/classes:to-boolean.

Severity: warning | Default: disabled (available via --all or --enable coalton-missing-to-boolean)

Suspicious

Rules that detect likely wrong or dangerous patterns — code that probably indicates a bug or a security risk.

:no-eval

Deprecated alias: :eval-usage (use :no-eval)

Avoid using cl:eval at runtime. Runtime evaluation of arbitrary code is a common source of security vulnerabilities (code injection) and makes programs hard to reason about. This rule detects direct calls as well as indirect invocation via funcall and apply.

;; Bad
(eval user-input)
(funcall #'eval expr)
(apply #'eval forms)

;; Good: use compile-time macros or explicit dispatch instead
(case action
  (:add (+ x y))
  (:sub (- x y)))

Exclusions:

  • defmacro bodies are skipped (macro expansion code is not runtime)
  • eval-when bodies are only checked when :execute is in the situation list

Severity: warning | Default: enabled

:runtime-intern

Avoid using symbol-interning functions at runtime. Runtime interning causes symbol table side effects, makes code harder to reason about, and is often a sign that a macro or compile-time mechanism should be used instead.

Detected functions:

  • cl:intern, cl:unintern
  • uiop:intern*
  • alexandria:symbolicate, alexandria:format-symbol, alexandria:make-keyword
;; Bad
(intern name :my-package)
(alexandria:symbolicate prefix "-" suffix)

;; Good: use compile-time macros or static symbols
(defmacro def-accessor (name)
  `(defun ,(alexandria:symbolicate name '-get) () ...))

Exclusions:

  • defmacro bodies are skipped (macro expansion code is not runtime)
  • eval-when bodies are only checked when :execute is in the situation list

Severity: warning | Default: disabled

:runtime-unintern

Avoid calling cl:unintern at runtime. unintern mutates the live package structure and can break symbol identity across packages. This is a focused companion to :runtime-intern that targets only unintern.

;; Bad
(unintern sym :my-package)
(funcall #'unintern sym)

;; Good: redesign to avoid runtime package mutation

Exclusions:

  • defmacro bodies are skipped (macro expansion code is not runtime)

Severity: warning | Default: disabled

:asdf-operate-in-perform

Do not call asdf:operate or related functions inside :perform method bodies. These calls can cause infinite loops or unexpected build behavior. Use symbol-call instead. Applies only to .asd files.

Detected functions: operate, oos, load-system, test-system, clear-system, require-system, make, compile-system (when qualified with asdf: or an ASDF sub-package prefix).

;; Bad: direct asdf calls in :perform
(defsystem "my-system"
  :perform (test-op (o c)
    (asdf:load-system "my-system/tests")
    (asdf:test-system "my-system/tests")))

;; Good: use symbol-call
(defsystem "my-system"
  :perform (test-op (o c)
    (symbol-call :asdf :load-system "my-system/tests")
    (symbol-call :asdf :test-system "my-system/tests")))

Severity: warning | Default: enabled

Practice

Rules that enforce commonly accepted best practices — not wrong, but widely discouraged patterns.

:no-package-use

Avoid using :use in cl:defpackage or uiop:define-package. The :use option imports all exported symbols from the named package, which makes it hard to tell which symbols are actually used and risks symbol conflicts when the used package exports new symbols.

The packages #:cl, #:common-lisp, #:coalton, and #:coalton-prelude are exempt.

;; Bad
(defpackage #:my-package
  (:use #:cl #:alexandria)   ; imports all of alexandria's exports
  (:export #:my-function))

;; Good: import only what you need
(defpackage #:my-package
  (:use #:cl)
  (:import-from #:alexandria #:when-let #:if-let)
  (:export #:my-function))

Severity: warning | Default: disabled (:strict and above)

:no-ignore-errors

Deprecated alias: :ignore-errors-usage (use :no-ignore-errors)

Avoid using cl:ignore-errors at runtime. ignore-errors silently swallows all errors and returns nil, making it very difficult to diagnose problems. Use handler-case with specific condition types instead.

;; Bad: silently swallows all errors
(ignore-errors (parse-config path))

;; Good: handle specific conditions explicitly
(handler-case (parse-config path)
  (file-error (e) (format t "Cannot read config: ~A" e))
  (parse-error (e) (format t "Invalid config: ~A" e)))

Exclusions:

  • defmacro bodies are skipped (macro expansion code is not runtime)

Severity: warning | Default: enabled

:no-allow-other-keys

Deprecated alias: :allow-other-keys (use :no-allow-other-keys)

Avoid &allow-other-keys in lambda lists. It disables keyword argument checking, hiding typos and invalid arguments that would otherwise be caught at call sites.

;; Bad: silently accepts any keyword
(defun connect (host &key port timeout &allow-other-keys)
  ...)

;; Good: explicit parameters
(defun connect (host &key port timeout)
  ...)

Severity: warning | Default: enabled

:double-colon-access

Avoid accessing internal symbols via :: package qualifier. Using :: bypasses package encapsulation, coupling code to package internals and making it fragile against refactoring.

;; Bad
(my-lib::internal-function arg)

;; Good: use only exported symbols
(my-lib:public-function arg)

Severity: warning | Default: disabled (:strict and above)

:error-without-custom-condition

Avoid calling error with a string literal or a built-in CL condition type. These prevent callers from handling errors specifically with handler-case. Define and signal a custom condition type instead.

;; Bad: callers can only catch simple-error or cl:error
(error "connection failed: ~A" host)
(error 'cl:simple-error :format-control "failed: ~A" :format-arguments (list host))

;; Good: define a custom condition type
(define-condition connection-error (error)
  ((host :initarg :host :reader connection-error-host)))
(error 'connection-error :host host)

Severity: warning | Default: disabled

:asdf-reader-conditional

Avoid #+/#- reader conditionals inside defsystem bodies in .asd files. Reader conditionals are processed at read time and cannot be controlled by ASDF, making them less portable. Use ASDF's built-in :if-feature (component option) and (:feature ...) (dependency modifier) instead.

;; Bad: reader conditionals in defsystem
(defsystem "my-system"
  :components ((:file "main")
               #+sbcl (:file "sbcl-support")
               #-windows (:file "unix-support")))

;; Good: use :if-feature
(defsystem "my-system"
  :components ((:file "main")
               (:file "sbcl-support" :if-feature :sbcl)
               (:file "unix-support" :if-feature (:not :windows))))

Exclusions:

  • Reader conditionals inside :perform bodies are not flagged (they are legitimate runtime CL code)
  • Reader conditionals outside defsystem forms are ignored
  • Comments, string literals, and character literals are not scanned

Severity: info | Default: disabled

:asdf-component-strings

ASDF systems, components, and dependencies should use strings not symbols. Applies only to .asd files.

;; Bad
(defsystem #:my-system
  :depends-on (#:alexandria))

;; Good
(defsystem "my-system"
  :depends-on ("alexandria"))

Severity: warning | Default: enabled

:bare-float-literal

Float literals should have explicit type markers (f, d, s, l). Without a marker, the type depends on *read-default-float-format*, which can vary.

;; Bad: depends on *read-default-float-format*
(defvar *threshold* 0.5)

;; Good: explicit double-float
(defvar *threshold* 0.5d0)

Severity: info | Default: disabled

:coalton-missing-declare

Every function define inside coalton-toplevel should have a preceding declare type signature. Missing declarations can cause the monomorphism restriction and lack documentation of intent.

(coalton-toplevel
  ;; Bad: no type declaration
  (define (add-one x) (+ x 1))

  ;; Good: declare before define
  (declare add-one (Integer -> Integer))
  (define (add-one x) (+ x 1)))

What is not flagged:

  • Value defines: (define pi 314)
  • define-type, define-instance, define-struct forms
  • Lambda-value defines: (define foo (fn (x) x))

Detection: Each coalton-toplevel block is checked independently. A declare must appear before the corresponding define within the same block.

Severity: warning | Default: disabled (available via --all or --enable coalton-missing-declare)

Cleanliness

Rules that detect dead code and unused definitions.

:unused-variables

Variables should be used or explicitly ignored with (declare (ignore ...)) or underscore prefix.

;; Bad
(defun add (x y)
  (+ x 1))  ; y is unused

;; Good
(defun add (x _y)
  (+ x 1))

Severity: warning | Default: enabled

:unused-local-functions

Local functions defined in flet or labels should be used.

;; Bad
(flet ((helper (x) (* x 2))
       (unused (x) (+ x 1)))
  (helper 10))  ; unused is never called

;; Good
(flet ((helper (x) (* x 2)))
  (helper 10))

Severity: warning | Default: enabled

:unused-local-nicknames

Local nicknames defined in defpackage should be used in the package.

;; Bad: unused nickname
(defpackage #:foo
  (:use #:cl)
  (:local-nicknames
   (#:unused #:alexandria)))  ; never used

;; Good: all nicknames used
(defpackage #:foo
  (:use #:cl)
  (:local-nicknames
   (#:a #:alexandria)))

Severity: warning | Default: enabled

:unused-imported-symbols

Imported symbols should be used or re-exported.

;; Bad: imported but never used
(defpackage #:foo
  (:use #:cl)
  (:import-from #:alexandria
                #:flatten
                #:hash-table-keys))  ; never used

;; Good
(defpackage #:foo
  (:use #:cl)
  (:import-from #:alexandria #:flatten)
  (:export #:flatten))  ; re-exported

Severity: warning | Default: enabled

:unused-loop-variables

Loop variables should be used within the loop body.

;; Bad
(loop for x in list
      for y in other-list  ; y is unused
      collect x)

;; Good: explicitly ignore with underscore
(loop for x in list
      for _y in other-list
      collect x)

Severity: info | Default: disabled

:stale-suppression

A suppression directive (inline comment or #+mallet form) has no effect because no matching violation was found at the suppressed location.

This rule fires when:

  • A ; mallet:suppress rule-name comment is present but the named rule produces no violation on that form.
  • A ; mallet:disable / ; mallet:enable region contains no violations for the listed rules.

Enable this rule to catch outdated suppression comments left behind after code changes.

;; Bad: no violation here, suppression is stale
(let ((x (foo))     ; mallet:suppress needless-let*
      (y (bar)))
  (list x y))

;; Good: violation exists, suppression is meaningful
(let* ((x (foo))    ; mallet:suppress needless-let*
       (y (bar)))
  (list x y))

Severity: warning | Default: enabled

Style

Rules for idiomatic patterns and naming conventions.

:one-package-per-file

Each .lisp file should start with defpackage or uiop:define-package, defining its own package. Files that begin with in-package without a preceding defpackage in the same file are flagged.

;; Bad: in-package without a preceding defpackage
(in-package #:my-app/utils)

(defun helper () ...)

;; Good: file defines its own package
(defpackage #:my-app/utils
  (:use #:cl)
  (:export #:helper))
(in-package #:my-app/utils)

(defun helper () ...)

Severity: info | Default: disabled (available via --all or --enable one-package-per-file)

:missing-else

Deprecated alias: :if-without-else (use :missing-else)

Use when or unless instead of if without else.

;; Bad
(if condition
    (do-something))

;; Good
(when condition
  (do-something))

Severity: warning | Default: enabled

:progn-in-conditional

Use cond instead of if with bare progn. Use when instead of and with bare progn as the last argument. Use unless instead of or with bare progn as the last argument.

;; Bad: if with bare progn
(if condition
    (progn
      (do-one)
      (do-two)))

;; Good
(cond
  (condition
   (do-one)
   (do-two)))

;; Bad: and with bare progn as last argument
(and condition
     (progn
       (do-one)
       (do-two)))

;; Good
(when condition
  (do-one)
  (do-two))

;; Bad: or with bare progn as last argument
(or condition
    (progn
      (do-one)
      (do-two)))

;; Good
(unless condition
  (do-one)
  (do-two))

Severity: info | Default: disabled

:missing-otherwise

case and typecase should have an otherwise clause.

;; Bad
(case type
  (:a 1)
  (:b 2))

;; Good
(case type
  (:a 1)
  (:b 2)
  (otherwise nil))

Severity: warning | Default: disabled

:defpackage-interned-symbol

Deprecated alias: :interned-package-symbol (use :defpackage-interned-symbol)

Use uninterned symbols (#:symbol) in package definitions instead of keywords or bare symbols.

;; Bad: keywords
(defpackage :myapp
  (:use :cl))

;; Good: uninterned symbols
(defpackage #:myapp
  (:use #:cl))

Severity: info | Default: disabled

:needless-let*

Use let instead of let* when bindings don't depend on each other (including single-binding let*).

;; Bad: bindings are independent
(let* ((x (foo))
       (y (bar)))
  (list x y))

;; Good
(let ((x (foo))
      (y (bar)))
  (list x y))

Severity: warning | Default: enabled

:special-variable-naming

Special variables should be named *foo*. Applies to defvar, defparameter, and sb-ext:defglobal.

;; Bad
(defvar config nil)
(sb-ext:defglobal global-state nil)

;; Good
(defvar *config* nil)
(sb-ext:defglobal *global-state* nil)

Note: Variables named with +plus+ convention are also accepted (since defglobal is sometimes used for constants).

Severity: info | Default: disabled

:constant-naming

Constants should be named +foo+.

;; Bad
(defconstant pi 3.14159)

;; Good
(defconstant +pi+ 3.14159)

Severity: info | Default: disabled

:asdf-redundant-package-prefix

Package prefixes asdf:, cl:, common-lisp:, and uiop: are redundant in .asd files. These files run in the asdf-user package which already uses asdf, cl, and uiop, so qualifying symbols with those package names is unnecessary. Sub-package prefixes like uiop/filesystem: are also flagged.

;; Bad: redundant package prefixes
(asdf:defsystem "my-system"
  :depends-on ("alexandria")
  :description (cl:format nil "tests"))

;; Good: no prefixes needed
(defsystem "my-system"
  :depends-on ("alexandria")
  :description (format nil "tests"))

Severity: info | Default: disabled

:missing-docstring

Top-level definitions should have docstrings. Applies to defun, defmacro, defgeneric, and defclass forms. defmethod is exempt because methods inherit documentation from the generic function.

;; Bad: no docstring
(defun add (x y)
  (+ x y))

(defclass point ()
  ((x :initarg :x)
   (y :initarg :y)))

;; Good: docstrings present
(defun add (x y)
  "Return the sum of X and Y."
  (+ x y))

(defclass point ()
  ((x :initarg :x)
   (y :initarg :y))
  (:documentation "A two-dimensional point."))

Severity: info | Default: disabled

:missing-package-docstring

Package definitions should have a :documentation string. Applies to defpackage and define-package forms.

;; Bad: no :documentation option
(defpackage #:my-lib
  (:use #:cl)
  (:export #:connect))

;; Good: documentation present
(defpackage #:my-lib
  (:use #:cl)
  (:export #:connect)
  (:documentation "My library for connections."))

Severity: info | Default: disabled

:missing-variable-docstring

Variable definitions should have docstrings. Applies to defvar and defparameter forms. defvar without an initial value is exempt (cannot place a docstring there).

;; Bad: no docstring
(defvar *connection-timeout* 30)
(defparameter *max-retries* 5)

;; Good: docstrings present
(defvar *connection-timeout* 30
  "Seconds before a connection attempt times out.")
(defparameter *max-retries* 5
  "Number of times to retry a failed connection.")

Severity: info | Default: disabled

:missing-struct-docstring

Struct definitions should have docstrings. Checks both the body string position (like defun) and the (:documentation ...) option in name-and-options. Applies to defstruct forms.

;; Bad: no docstring
(defstruct point x y)

;; Good: body docstring
(defstruct point
  "A two-dimensional point."
  x y)

;; Also good: :documentation in options
(defstruct (point (:constructor make-point) (:documentation "A two-dimensional point."))
  x y)

Severity: info | Default: disabled

:redundant-progn

progn with a single body form is redundant — the progn wrapper can be removed.

;; Bad: progn with one form
(progn
  (do-something))

;; Good: no wrapper needed
(do-something)

Exclusions:

  • progn wrapping a single ,@splice (unquote-splicing) is allowed, since the splice may expand to multiple forms

Severity: warning | Default: disabled (:strict and above)

Format

Rules for whitespace and file formatting. These follow strong community consensus (Emacs/SLIME standards).

:trailing-whitespace

Lines should not have trailing whitespace.

Auto-fixable: --fix removes trailing whitespace.

Severity: warning | Default: enabled

:no-tabs

Use spaces instead of tab characters.

Severity: warning | Default: enabled

:missing-final-newline

Deprecated alias: :final-newline (use :missing-final-newline)

Files must end with a newline.

Auto-fixable: --fix appends a newline.

Severity: warning | Default: enabled

:closing-paren-on-own-line

Closing parentheses should follow the last expression on the same line, not appear alone on their own line. This is the idiomatic Common Lisp style, unlike Algol-family languages.

;; Bad
(defun foo ()
  (bar)
  )

;; Good
(defun foo ()
  (bar))

Exception: Closing parens after a line ending with a comment are allowed, since appending them to the comment line would break the comment.

;; OK: previous line ends with a comment
(defvar *alist*
  '((a . 1)
    (b . 2) ; last entry
    ))

Severity: warning | Default: disabled (:strict and above)

:line-length

Lines should not exceed maximum length.

Options: :max (default: 100)

(:enable :line-length :max 120)

Severity: info | Default: disabled

:consecutive-blank-lines

Limit consecutive blank lines.

Options: :max (default: 2)

Auto-fixable: --fix removes excess blank lines.

(:enable :consecutive-blank-lines :max 1)

Severity: info | Default: disabled

Metrics

Code quality metrics that measure complexity and size. These are informational and don't indicate bugs or errors.

:function-length

Functions should not exceed maximum line count.

Options: :max (default: 50)

Counting: Only code lines — excludes comments, blank lines, docstrings, and disabled reader conditionals (#+nil, #+(or), #-(and)).

Nested flet/labels functions are counted separately.

Severity: info | Default: disabled

:cyclomatic-complexity

Functions should not exceed maximum cyclomatic complexity.

Options:

  • :max (default: 20) - Maximum allowed complexity
  • :variant (default: :standard) - :standard or :modified

Complexity calculation (:standard variant):

  • Base: 1 per function
  • Conditionals: +1 for each if, when, unless
  • COND: +1 per clause (excluding final t/otherwise)
  • CASE/TYPECASE: +1 per clause (excluding final otherwise/t)
    • ecase, etypecase, ccase, ctypecase: all clauses count
  • Logical operators: +1 for each and or or
  • DO/DO*: +1 each (end-test condition)
  • LOOP: +1 per conditional keyword (when, unless, if, while, until)
  • Exception handling: +1 per handler clause
  • Third-party: Alexandria (if-let, when-let, etc.), Trivia (match, etc.), string-case

:modified variant: Same, except CASE/TYPECASE and case-like macros count as +1 total instead of per-clause.

Nested flet/labels functions are counted separately.

Severity: info | Default: disabled

:comment-ratio

Functions should not have too many comments relative to code. Useful for catching AI-generated code padded with excessive inline commentary.

Options:

  • :max (default: 0.5) - Maximum comment ratio (0.0–1.0)
  • :min-lines (default: 5) - Minimum qualifying lines before the rule applies
  • :include-docstrings (default: nil) - Count docstring lines as comments

Ratio formula: comment-lines / (comment-lines + code-lines)

Nested flet/labels functions are counted separately.

Notes:

  • Functions with fewer qualifying lines (comment + code) than :min-lines are skipped. Docstring lines are not counted toward the threshold unless :include-docstrings t is set.

Severity: info | Default: disabled