SpecLint (spec_lint v0.1.0)

Copy Markdown View Source

Internal, unstable programmatic API. The supported public interfaces are the Mix task, its configuration, and its JSON findings file.

Compares @spec declarations with the signatures the Elixir compiler infers for the same functions.

The compiler stores an inferred signature for every function in the ExCk chunk of the BEAM file. SpecLint translates each spec clause into the compiler's types (SpecLint.Typespec), applies the inferred signature to the spec's argument types with the compiler's own rule (apply_infer/2), and compares the returns:

  • return_conflict (error): the inferred return is disjoint from the spec return, so no value the function returns satisfies the spec;
  • domain_rejected (error): no inferred clause accepts the spec's arguments, so the compiler would warn on every conforming call;
  • clause_conflict (warning): a clause that accepts only spec-conforming arguments returns a value disjoint from the spec return. A warning, not an error: the compiler keeps clauses that can never match (def f(:b = x) when is_integer(x)), so a stored clause is not proof of reachable behaviour;
  • missing_return (warning): the inferred return has a literal shape the spec does not declare, such as an {:error, _} tuple, an atom or a struct the spec never mentions;
  • unexpected_return (warning): the spec says no_return() and the compiler infers a concrete return.

Nothing else is reported: inference is deliberately conservative, so an inferred type wider than the spec is normal and silent.

Summary

Types

One reported problem.

An ignore entry: a module, a {module, function} pair, a {module, function, arity} triple, or a regex matched against "Module.function/arity".

Functions

Whether finding matches one of the ignore entries.

"Module.function/arity" of a finding.

Analyses every Elixir.*.beam file in the ebins directories.

Types

finding()

@type finding() :: %{
  severity: :error | :warning,
  check: atom(),
  module: module(),
  function: atom(),
  arity: arity(),
  file: String.t() | nil,
  line: pos_integer() | nil,
  message: String.t(),
  spec: [String.t()],
  inferred: [String.t()]
}

One reported problem.

ignore()

@type ignore() ::
  module() | {module(), atom()} | {module(), atom(), arity()} | Regex.t()

An ignore entry: a module, a {module, function} pair, a {module, function, arity} triple, or a regex matched against "Module.function/arity".

Functions

ignored?(finding, ignore)

@spec ignored?(finding(), [ignore()]) :: boolean()

Whether finding matches one of the ignore entries.

mfa(finding)

@spec mfa(finding()) :: String.t()

"Module.function/arity" of a finding.

run(ebins, opts \\ [])

@spec run([Path.t()], keyword()) :: %{
  findings: [finding()],
  specs: non_neg_integer(),
  skipped: [{module(), atom(), arity(), term()}]
}

Analyses every Elixir.*.beam file in the ebins directories.

Options: :ignore, a list of ignore/0 entries whose findings are dropped; :modules, a list of modules to restrict the analysis to. Returns the findings, the number of spec clauses compared and the spec clauses skipped because a type could not be translated, with the reason.