npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@daml-tools/daml-lint

v0.9.4

Published

Static analysis scanner for Daml smart contracts

Downloads

4,159

Readme

daml-lint

CI License: AGPL v3

[!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.

Documentation

The workspace docs split task guides, reference, and design background:

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

For JavaScript/TypeScript projects that want daml-lint as a dev dependency:

npm install --save-dev @daml-tools/daml-lint
npx daml-lint ./daml

Cargo installs require Rust 1.87+ (the rquickjs dependency needs rustc 1.87).

cargo install daml-lint

Or straight from the workspace repo:

cargo install --git https://github.com/stevennevins/daml-tools daml-lint

Or from a local checkout:

git clone https://github.com/stevennevins/daml-tools.git
cd daml-tools
cargo install --path crates/daml-lint

Library features

The default features build the published CLI and custom-rule engine:

[dependencies]
daml-lint = "0.9.4"

Library consumers that only need parser lowering and the rule-facing IR can avoid the CLI parser and QuickJS runtime:

[dependencies]
daml-lint = { version = "0.9.4", default-features = false }

Rust-facing finding locations, parser diagnostics, and IR spans use the coordinate newtypes from daml-syntax (LineNumber, CharColumn, Utf16Offset, and ByteOffset) so byte, UTF-16, line, and column coordinates cannot be mixed accidentally. JSON, SARIF, and custom-rule JavaScript output still serialize those coordinates as numbers.

The js-runtime feature enables the QuickJS-backed runtime used by shipped built-ins. The custom-rules feature implies js-runtime and enables loading user-provided rule files through --rules and configured plugin packages. Shipped built-ins are authored in TypeScript and embedded as generated JavaScript; no TypeScript toolchain is required at runtime. The shipped detectors are registered through create_builtin_detectors() rather than exposed as individual Rust detector modules. The cli feature enables the daml-lint binary and implies js-runtime.

Usage

Scan a single file:

daml-lint src/MyContract.daml

Scan a directory recursively:

daml-lint ./daml/

Choose an output format:

daml-lint ./daml/ --format sarif    # SARIF JSON (GitHub / IDE integration)
daml-lint ./daml/ --format markdown # Human-readable (default)
daml-lint ./daml/ --format json     # Machine-readable JSON
daml-lint ./daml/ --rule missing-ensure-decimal # run one built-in rule
daml-lint ./daml/ --group recommended           # run a rule group

Write results to a file:

daml-lint ./daml/ --format sarif --output report.sarif

Custom detectors

Define your own detectors as AST rule scripts and pass them with --rules (repeatable), in the style of solhint custom rules:

daml-lint ./daml/ --rules my-rule.js --rules another-rule.js

Installed plugin packages can also be enabled from ./daml.yaml:

daml-tools:
  lint:
    plugins: [template]
    plugin-paths: [./plugins]
    rules:
      missing-ensure-decimal: off
      template/template-requires-ensure:
        - warning
        - allowEmptyEnsure: false

template resolves to daml-lint-plugin-template in node_modules; use plugin-paths for local package roots during development. Rule options from the array form are exposed to the rule as global CONFIG.

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. The globalThis.__daml_lint_rule assignment is the TypeScript-checked rule object; the current runtime still discovers top-level metadata constants and visitor function declarations:

import type { Template } from "./daml-lint";

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`);
  }
}

globalThis.__daml_lint_rule = { NAME, SEVERITY, DESCRIPTION, on_template };

then compile to the JavaScript file you pass to --rules:

npx esbuild my-rule.ts --bundle --outfile=dist/my-rule.js

Type-only imports are erased by the build. Runtime helper imports must be bundled because the rule engine runs JavaScript without import, require, filesystem, or network APIs. 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). Pass report(node, message, evidence) when the report should show structural evidence instead of the source line. 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.

Choice consumption and import forms are surfaced as string enums (choice.consuming is "consuming" | "non-consuming"; import.qualified is "qualified" | "unqualified") to avoid boolean ambiguity.

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 } }, { App: { head, args, span } }, and { Lit: { kind: "Text", value: "cid", span } } for type-level literals (for example HasField "cid"); 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. | | choice.authorities | authority_exprs (Expr[]) | Source-level authority metadata clauses on choices. | | interface_instance.view | view_expr (Expr \| null) | Source-level view = ... on interface instances, distinct from method names. | | case_alt.guards | branches[].guards (CaseGuard[]) | Boolean and pattern guards on case alternative branches. | | case_alt.where | where_bindings (LetBinding[]) | Alternative-local where helper bindings on case alternatives. | | import.package | package_label (string \| null) | Decoded package string from import "pkg" Module; not an LF PackageId. | | 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. DetectError preserves the underlying ScriptLoadError through std::error::Error::source() when one is available, so library callers can inspect the typed failure chain instead of parsing strings. 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. Config can also use error (high) and warning (medium). Custom rules run alongside the built-in detectors, appear in all output formats, and count toward --fail-on. Direct --rules names must not collide with built-in detector names or each other. Installed plugin rules are reported under their configured plugin/rule ID.

Examples:

Each example is authored in TypeScript and ships with its compiled .js under examples/dist/ - that's the file --rules takes. Run npm run build:examples from this crate to refresh those generated files.

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.)

Library callers can load custom rules without writing temporary files: detectors::script::load_script_source(label, source) accepts in-memory JavaScript, and load_script_reader_with_options(label, reader, options) accepts any std::io::Read source plus JSON rule CONFIG.

CI gating

Use --fail-on to control when the tool returns a non-zero exit code:

daml-lint ./daml/ --fail-on medium   # fail on medium or above
daml-lint ./daml/ --fail-on critical # fail only on critical

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

cargo test

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 the current 0.9.x line. 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.

Breaking updates introduced in this branch:

  • Severity no longer implements Ord/PartialOrd; use rank() or meets_or_exceeds() for risk-based ordering and thresholds.
  • Severity::from_str now returns SeverityParseError instead of ().
  • parse_severity was removed; use value.parse::<Severity>() so invalid input preserves SeverityParseError.
  • Public IR/report DTO structs are #[non_exhaustive]; construct through parser lowering or documented constructors such as Finding::new.
  • parse_daml_with_diagnostics now returns a named ParseResult with fields (module, diagnostics) instead of a tuple.
  • Rule setting values are now canonical only: off, critical, high, medium, low, info (legacy warn/error and numeric shortcuts 0/1/2 are intentionally rejected).

License

AGPL-3.0-only. See LICENSE.