glinter

A linter for the Gleam programming language. It parses Gleam source files into ASTs using glance and checks them against a configurable set of rules. Many rules are based on the official Gleam conventions.

Installation

gleam add --dev glinter

Usage

# Lint the src/ directory (default)
gleam run -m glinter

# Lint specific files or directories
gleam run -m glinter src/myapp/ test/

# JSON output
gleam run -m glinter --format json

# Show stats (files, lines, timing)
gleam run -m glinter --stats

# Lint a different project (resolves gleam.toml and paths relative to project dir)
gleam run -m glinter --project /path/to/my/project

The exit code is 1 if any errors are found, 0 otherwise (warnings alone don't fail the run).

Multi-Package Projects

For monorepos with multiple Gleam packages, add glinter as a dev dependency to one package (e.g. server) and lint each package separately using --project:

# From any directory, lint each package
gleam run -m glinter --project server
gleam run -m glinter --project client
gleam run -m glinter --project shared

Each package uses its own gleam.toml for configuration. You can wrap this in a script to lint all packages at once:

#!/bin/sh
# bin/lint
set -e
for pkg in server client shared; do
  echo "Linting $pkg..."
  gleam run -m glinter --project "$pkg"
done

Rules

Error Handling

These rules enforce explicit error handling. Gleam's Result type exists so errors are handled at every level, not silently discarded.

Code Quality

These rules catch debug artifacts and patterns that shouldn't ship to production.

Style

These rules enforce consistency and catch patterns that make code harder to read.

Type Annotations

Complexity

Labels

Imports

Cross-Module

FFI

Configuration

Configuration lives in your project's gleam.toml under the [tools.glinter] key:

[tools.glinter]
stats = true  # show file count, line count, and timing after each run
warnings_as_errors = true  # promote all warnings to errors (exit 1 on any issue)
include = ["src/", "test/"]  # directories to lint (default: ["src/"])
exclude = ["src/server/sql.gleam"]  # skip generated files entirely

[tools.glinter.rules]
avoid_panic = "error"
avoid_todo = "error"
echo = "warning"
assert_ok_pattern = "warning"
discarded_result = "warning"
short_variable_name = "warning"
unnecessary_variable = "warning"
redundant_case = "warning"
unwrap_used = "warning"
deep_nesting = "warning"
function_complexity = "off"  # off by default
module_complexity = "off"  # off by default
prefer_guard_clause = "warning"
missing_labels = "warning"
label_possible = "warning"
unused_exports = "warning"
missing_type_annotation = "warning"
todo_without_message = "warning"
unqualified_import = "warning"
panic_without_message = "warning"
string_inspect = "warning"
duplicate_import = "warning"
unnecessary_string_concatenation = "warning"
trailing_underscore = "warning"
error_context_lost = "warning"
stringly_typed_error = "warning"
thrown_away_error = "warning"
ffi_usage = "off"  # off by default

Each rule can be set to "error", "warning", or "off".

Excluding Files

Skip files entirely (useful for generated code). Supports globs:

[tools.glinter]
exclude = ["src/server/sql.gleam", "src/generated/**/*.gleam"]

Ignoring Rules Per File

Suppress specific rules for files where they don't make sense. Also supports globs:

[tools.glinter.ignore]
"src/my_complex_module.gleam" = ["deep_nesting", "function_complexity"]
"test/**/*.gleam" = ["assert_ok_pattern", "short_variable_name", "missing_type_annotation", "label_possible", "missing_labels", "unqualified_import"]

Suppressing Warnings with Comments

Use // nolint: comments for targeted suppression. Fix the code first — only suppress when the violation is intentional.

Three levels, from narrowest to broadest. Use the narrowest scope that covers your case.

Line-level

Place the comment on the line directly above the code it suppresses, or inline on the same line:

// nolint: thrown_away_error -- key absent means use default
Error(_) -> Ok([])

let _ = setup() // nolint: discarded_result -- fire and forget

Function-level

Place the comment directly above fn or pub fn (no blank lines between). Suppresses the listed rules for the entire function body:

// nolint: deep_nesting, function_complexity -- recursive AST walker
fn walk_expression(expr, context) {
  // all deep_nesting and function_complexity warnings suppressed here
}

Optional reason

Add -- after the rule list to explain why:

// nolint: avoid_panic -- fallback body is unreachable with dual @external

Stale annotation detection

Glinter warns (nolint_unused) when a // nolint: annotation:

Output Formats

Text (default)

src/app.gleam:12: [error] avoid_panic: Use of 'panic' is discouraged
src/app.gleam:25: [warning] echo: Use of 'echo' is discouraged

JSON

gleam run -m glinter --format json
{
  "results": [
    {
      "rule": "avoid_panic",
      "severity": "error",
      "file": "src/app.gleam",
      "line": 12,
      "message": "Use of 'panic' is discouraged"
    }
  ],
  "summary": {
    "total": 1,
    "errors": 1,
    "warnings": 0
  }
}

When --stats is enabled, a stats object is included:

{
  "stats": {
    "files": 23,
    "lines": 2544,
    "elapsed_ms": 45
  }
}

Custom Rules (Plugins)

Add project-specific rules by calling glinter.run with extra rules from your own packages:

// test/review.gleam
import glinter
import my_project/rules

pub fn main() {
  glinter.run(extra_rules: [
    rules.no_raw_sql(),
    rules.require_org_id_filter(),
  ])
}
gleam run -m review

Custom rules use the same builder API as built-in rules and get the same config treatment (on/off/severity in gleam.toml, file-level ignores). See src/glinter/rule.gleam for the API.

If you write a rule that would be useful to the broader Gleam community, PRs are welcome. Contributed rules can ship with a default severity of off so projects opt in explicitly.

Running Tests

gleam test

License

MIT