daml-lint
[!WARNING] This software is experimental and not intended for production use. Use at your own risk.
Static analysis scanner for Daml smart contracts. Catches security vulnerabilities and anti-patterns through AST pattern matching, similar to what Slither does for Solidity.
Part of the daml-tools workspace.
Parsing is the shared daml-parser crate — lexer (comments,
strings, layout-aware spans) → Haskell offside-rule layout resolution →
recursive-descent parser producing a typed AST with positions on every node.
daml-lint lowers that AST to a rule-facing IR and runs detectors over it. Files
that fail to parse degrade to partial structure with a diagnostic on stderr
(file:line:col); a scan never aborts on bad input.
Detectors
| Detector | Severity | Description |
|---|---|---|
missing-ensure-decimal |
HIGH | Template has Decimal fields without an ensure clause bounding them to > 0 |
unguarded-division |
HIGH | Division operation without a prior guard checking the denominator is non-zero |
missing-positive-amount |
HIGH | Choice accepts amount/quantity/price parameter without asserting it is positive |
archive-before-execute |
HIGH | Contract archived before a try/catch block — contract is lost if execution fails |
head-of-list-query |
MEDIUM | Pattern match on head of queryFilter result — non-deterministic ordering risk |
unbounded-fields |
MEDIUM | Text, List, or TextMap fields without size bounds in the ensure clause |
Installation
Requires Rust 1.87+ (its rquickjs dependency needs
rustc 1.87).
Or straight from the workspace repo:
Or from a local checkout:
Library features
The default features build the published CLI and custom-rule engine:
[]
= "0.1"
Library consumers that only need the built-in detectors and rule-facing IR can avoid the CLI parser and QuickJS runtime:
[]
= { = "0.1", = false }
Enable custom-rules only when you need JavaScript AST rules from library code.
The cli feature exists for the daml-lint binary.
Usage
Scan a single file:
Scan a directory recursively:
Choose an output format:
Write results to a file:
Custom detectors
Define your own detectors as AST rule scripts and pass them with --rules
(repeatable), in the style of solhint custom rules:
A rule is TypeScript/JavaScript (executed by an embedded QuickJS engine):
constants for metadata, plus visitor functions named after the node types you
care about — like solhint's ContractDefinition(node) callbacks. Write rules
in TypeScript against examples/daml-lint.d.ts for
type checking and autocomplete:
const NAME = "template-requires-ensure";
const SEVERITY = "medium";
const DESCRIPTION = "Every template must declare an ensure clause"; // optional
function on_template(template: Template): void {
if (template.ensure_clause === null) {
report(template, `Template '${template.name}' has no ensure clause`);
}
}
then compile to the JavaScript file you pass to --rules:
(Plain JavaScript rules work directly — the compile step is only for TypeScript.)
Visitors (define any subset, at least one):
| Function | Called for | Node fields |
|---|---|---|
on_template(template) |
each template | name, fields, signatory_exprs, observer_exprs, ensure_clause (null if absent), key_expr, key_type, maintainer_exprs, choices, interface_instances, span |
on_choice(choice, template) |
each choice | name, consuming, controller_exprs, observer_exprs, parameters, return_type, body, span |
on_field(field, template) |
each template field | name, type_, span |
on_function(function) |
each top-level function | name, type_signature, body, span |
on_import(import) |
each import | module_name, qualified, alias |
on_interface(interface) |
each interface | name, requires, viewtype, methods, choices, span |
check(m) |
once per module | ir_version, name, file, imports, templates, interfaces, functions, source |
Report findings with report(node, message) (location taken from the node's
span) or report(line, message). The rule's SEVERITY applies to all its
findings. Node shapes are declared in
examples/daml-lint.d.ts and mirror the IR in
src/ir.rs; statement nodes in body are objects keyed by kind,
e.g. "Create" in stmt.
Statements carry a typed expression AST: stmt.Let.value,
stmt.Assert.condition_expr, stmt.Exercise.cid/.argument, and
stmt.Other.expr are Expr nodes — tagged unions like
{ BinOp: { op: "/", lhs, rhs, span } } with a 1-based span on every
node (see the Expr type in the .d.ts). Type-bearing fields carry TypeNode
trees such as { Con: { name: "Party", qualifier: null, span } } and
{ App: { head, args, span } }; type spans include line/column,
JavaScript string offsets (start/end, suitable for
m.source.slice(start, end)), and parser byte offsets
(byte_start/byte_end). Compatibility-only raw-text fields and rendered
party-name lists were removed in the breaking custom-rule surface, so rules
should match on structure, not substrings.
examples/unguarded-division-ast.ts
shows a denominator-guard check written entirely on typed nodes.
Removed v1/v2 compatibility fields and their structured replacements:
| Removed field | Use instead | Notes |
|---|---|---|
choice.body_raw, function.body_raw |
body (Statement[]) |
Match statements structurally; only stmt.Other.raw / Expr.Unknown.raw preserve unsupported source text. |
template.ensure_clause.raw_text |
ensure_clause.expr (Expr) |
Match the condition structurally. |
stmt.Let.expr |
stmt.Let.value (Expr) |
The bound expression. |
stmt.Assert.condition |
stmt.Assert.condition_expr (Expr) |
The condition expression only. |
stmt.Fetch.cid_expr, stmt.Archive.cid_expr, stmt.Exercise.cid_expr |
.cid (Expr) |
The contract-id expression. |
stmt.Create.raw |
template_name + argument (Expr) |
argument is the created payload. |
stmt.Exercise.raw |
cid + choice_name + argument (Expr) |
argument is the choice argument, if present. |
choice.controllers |
controller_exprs (Expr[]) |
Flatten list expressions in the rule if you want list-literal party semantics. |
template.signatories, template.observers |
signatory_exprs, observer_exprs (Expr[]) |
Structured party expressions only. |
stmt.Other.raw and the Unknown expression's raw are deliberate raw-source
escape hatches for constructs with no structured form (e.g.
examples/no-trace.ts matches source text).
Heads up: visitors must be function declarations — arrow functions assigned
to const are not discovered. If a script fails at runtime, the CLI exits 2;
library callers can use Detector::try_detect to receive the rule error
without terminating the host process. Rule errors are never swallowed. A runaway
loop is interrupted so a broken rule can't hang CI. The engine runs JavaScript
(ES2023) — no Node APIs, no require/import, no filesystem or network.
Each rule's script is evaluated once and its visitors are then called for
every module — visitors should be stateless; don't accumulate findings in
top-level mutable state across files.
SEVERITY is one of critical, high, medium, low, info. Custom rules
run alongside the built-in detectors, appear in all output formats, and count
toward --fail-on. Rule names must not collide with built-in detector names
or each other.
Examples:
- examples/template-requires-ensure.ts — structural check on a single node
- examples/consuming-choice-signatory-controller.ts — cross-references choice controllers against template signatories
- examples/no-create-in-nonconsuming.ts — walks choice body statements, recursing into try/catch
- examples/no-trace.ts — banned-token check over raw source lines
- examples/unguarded-division-ast.ts — expression-level analysis on the typed AST (division denominators vs prior assertions)
Each example ships with its compiled .js next to it — that's the file
--rules takes.
To check that a rule script parses without running a scan, point the tool at a nonexistent path — rule errors are reported before file discovery. (A valid script then prints No .daml files found., which also exits 2 — go by the message, not the exit code.)
CI gating
Use --fail-on to control when the tool returns a non-zero exit code:
Output Formats
- SARIF — Standard format for static analysis tools. Integrates with GitHub Code Scanning and IDEs.
- Markdown — Human-readable report grouped by severity. Good for pull request comments.
- JSON — Flat findings array with summary counts. Good for dashboards and aggregation.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | No findings at or above the --fail-on threshold |
| 1 | One or more findings at or above the threshold |
| 2 | CLI error (invalid format, no files found, etc.) |
| 3 | A scanned file had parse errors (scan is not authoritative) |
Development
Tests run entirely offline: parser and layout integration tests use a vendored
copy of the daml-finance
sources under corpus/daml-finance/ (634 real
.daml files) — shared at the workspace root with daml-parser — as a
ground-truth corpus; see
corpus/daml-finance/README.md for
provenance and licensing.
Public API Stability
daml-lint is pre-1.0. The CLI exit codes and documented feature flags are the
stable user contract for 0.1.x. The rule-facing IR is intentionally public for
custom rules and library users, but it may gain structure in 0.x minor releases;
custom rules should check ir_version and match typed nodes rather than raw
source substrings. Detector result types such as Finding, Severity, and
DetectError are non-exhaustive; use their documented fields/accessors and keep
wildcard arms when matching enums. Patch releases should remain compatible.
License
AGPL-3.0-only. See LICENSE.