Skip to content

Linting ​

The compiler rejects incorrect programs. Lints report valid code that is likely mistaken or needlessly hard to maintain. They run during hoo check, hoo build, and editor diagnostics; there is no separate lint command.

Only source in the root package is linted. Generated syntax is skipped. A deny lint makes checking and building fail, but does not change whether the program is valid Hoopoe.

Built-in rules ​

These rules are warnings by default:

  • unused-import
  • unused-private-declaration
  • unused-binding
  • unreachable-code
  • constant-condition
  • duplicate-map-key
  • reversed-range
  • empty-range
  • identical-if-branches
  • duplicate-condition
  • opposite-condition
  • collapsible-if
  • collapsible-else-if
  • constant-logical-expression
  • erasing-operation
  • needless-if
  • wildcard-only-match
  • boolean-match
  • identity-operation
  • modulo-one
  • zero-bitwise-operation
  • double-negation
  • nonminimal-boolean
  • overly-complex-boolean
  • equal-operands
  • repeated-logical-operand
  • opposite-logical-operands
  • manual-range-contains
  • literal-membership-search
  • length-comparison
  • float-equality
  • empty-string-comparison
  • string-plus-empty
  • unnecessary-cast
  • redundant-closure-call
  • identical-match-arms
  • ignored-return-value
  • unused-lint-control
  • broad-lint-allow
  • too-many-arguments
  • too-many-boolean-arguments
  • function-too-long
  • high-cognitive-complexity
  • non-snake-case-function
  • non-snake-case-binding
  • non-pascal-case-type
  • non-screaming-snake-case-constant
  • managed-field
  • managed-child-capture
  • unused in release builds
  • echo-in-release in release builds

needless-boolean-comparison and shadowed-binding are available but allowed by default. Enable or change a rule in hoopoe.toml:

toml
[lints]
unused-binding = "deny"
shadowed-binding = "warn"

The levels are allow, warn, and deny. Unknown rule names are errors.

Per-node controls ​

Lint controls use ordinary inert metadata and can annotate any syntax node:

hoo
@lint.allow = #["unused-private-declaration"]
private func retained_for_debugging(): int = 1

The standard options are @lint.allow, @lint.warn, and @lint.deny. Each takes a list of package-qualified rule names. A control applies to its syntax subtree; the nearest control wins. Unknown rules and conflicting levels on the same node are errors.

These options use the same checked metadata model as other attributes. Custom metadata is declared with @attributes(\(option: Type)); quoted meta.Expression<T> values and contextual this keep their normal meaning. Attached macros and @extend(...) remain syntax transformations, not lint controls.

Custom rules ​

A local lint package has a conventional src/lint.hoo entrypoint. Every public function in that file is a rule and must be a public const func. Private functions are helpers. Rules are explicitly enabled from the application manifest:

toml
[lint-packages.quality]
path = "../quality-lints"

[lints."quality/no-placeholder"]
level = "deny"
word = "TODO"
extensions = ["hoo", "md"]

The table name is <package-alias>/<public-function>. Extra keys become typed arguments after the target argument. Strings, integers, floats, booleans, datetimes, arrays, and tables are supported. An omitted Option<T> parameter receives None; other omitted parameters and unknown options are errors.

A rule's first parameter selects what it checks. lint.Module runs once per root-package module. A narrower target such as lint.Function runs once for each matching source-authored node:

hoopoe
import std/lint as lint

public const func no_placeholder(
  function: lint.Function,
  word: string,
  extensions: #[string],
): #[lint.Diagnostic] = {
  // Inspect function.text, span, inferred type, and resolution.
  #[]
}

lint.Module contains the module source and a source-ordered list of nodes from the expanded syntax. Generated nodes are omitted. Each lint.Node provides its kind, source text, span, inferred type when available, and resolved symbol identity when available. The same lint-oriented fields are available on the narrow targets: declaration types (Declaration, Function, Let, Struct, Enum, Interface, Implementation, Namespace, Import, Effect, TypeAlias, ExternalDeclaration, ExternalFunction, and ExternalLet) and syntax types (Expression<T>, Statement, Pattern, Type, Parameter, Field, Variant, MatchArm, and ImportName). A rule returns #[lint.Diagnostic]; diagnostics support secondary labels, notes, and help text.

Structural patterns ​

Attach @lint.pattern(\(...)) to a custom rule to run it only when its target matches a syntax pattern:

hoopoe
@lint.pattern(\($value && ($value || $fallback)))
public const func absorbed_operand(
  whole: lint.Expression<boolean>,
  value: lint.Expression<boolean>,
  fallback: lint.Expression<boolean>,
  message: string,
): #[lint.Diagnostic] = #[lint.Diagnostic(
  message = message,
  span = whole.span,
  labels = #[lint.Label(message = "repeated value", span = value.span)],
  notes = #[fallback.text],
  help = "use the repeated value directly",
)]

The first parameter receives the whole match. A $name in the pattern captures one real syntax node and requires a parameter with the same name and a compatible lint.* syntax type. Repeating a capture name requires the matched syntax to be equal each time. $_ matches one syntax node without creating a parameter. A concrete lint.Expression<T> only matches expressions whose inferred type is T; a rule-generic T matches any expression type. Remaining parameters are typed TOML options as usual. Whitespace and comments do not affect matching.

The compiler rejects missing capture parameters, syntax parameters without a capture, and capture parameters that do not use a std/lint syntax type before running the rule.

Custom rules use the compiler's bounded, pure compile-time interpreter. They cannot call host APIs, so the same rule behavior is available to native tools, the language server, and WebAssembly clients when the rule source is supplied. Rule parse, option, execution, and output failures are errors.

Autofixes are not currently supported.