glinter/rule
Rule builder API for custom glinter rules.
Two kinds of rules
Module rules visit a single file at a time. The orchestrator runs each module rule on each file (in parallel across files). Use a module rule when your lint only needs local context: one function body, one expression, one import.
Project rules visit every file in sequence, carrying accumulated state across the project. Use a project rule when you need cross-file context: counting exports across modules, checking for duplicate definitions, etc.
Module rule builder
Create a schema with new() or new_with_context(), attach visitors, then
call to_module_rule() to build a Rule. The builder erases the context
type via closures so the orchestrator can hold a uniform List(Rule).
rule.new("my_rule")
|> rule.with_expression_enter_visitor(fn(expr, span, ctx) {
// check expr, return errors
#([], ctx)
})
|> rule.to_module_rule()
Traversal order
For each file, the runner visits:
- Imports (import_visitor)
- Each function definition (function_visitor), then its body:
a. Statements (statement_visitor)
b. For each statement’s expression, a depth-first walk:
- Enter visitor (top-down, parent before children)
- Recurse into child expressions
- Exit visitor (bottom-up, children before parent)
- Final evaluation (final_evaluation)
Which visitor do I use?
| Use case | Visitor |
|---|---|
| Check every expression regardless of context | with_simple_expression_visitor |
| Check expressions with accumulated state (e.g. nesting depth) | with_expression_enter_visitor + context |
| Check after children are processed (e.g. collect child types) | with_expression_exit_visitor |
| Check function signatures, return types, or parameters | with_function_visitor |
| Check import statements (qualified/unqualified, duplicates) | with_import_visitor |
| Check top-level statements (use, assignment, assert) | with_statement_visitor |
| Emit errors after all traversal (e.g. “not used” warnings) | with_final_evaluation |
| Cross-file analysis (unused exports, duplicate modules) | new_project + with_module_visitor |
Context lifecycle
Context is per-rule, per-file (module rules) or per-project (project rules).
The context value starts from initial() and is threaded through every
visitor call. Each visitor receives the current context and returns
#(errors, new_context). The final context is available in
final_evaluation to emit deferred errors.
Use new_with_context() to seed an initial value. Use new() (which sets
context to Nil) if your rule is stateless.
Simple vs. stateful visitors
with_simple_* visitors wrap a stateless callback in a stateful one that
passes context through unchanged. Prefer them when you don’t need context:
they’re less boilerplate.
Built-in rule examples
See the src/glinter/rules/ directory for real examples:
avoid_panic.gleam: simple expression visitor (no context)deep_nesting.gleam: enter/exit visitors with depth-tracking contextunused_exports.gleam: cross-file reference counting (standalone; seesrc/glinter/unused_exports.gleam, not built with the project rule builder)missing_labels.gleam: custommodule_rule_from_fnfor pre-scanning
Types
Full lint result with file path and severity, produced by the orchestrator.
pub type LintResult {
LintResult(
rule: String,
severity: Severity,
file: String,
location: glance.Span,
message: String,
details: String,
)
}
Constructors
-
LintResult( rule: String, severity: Severity, file: String, location: glance.Span, message: String, details: String, )
Builder type for module rules. Generic over the context type.
pub opaque type ModuleRuleSchema(context)
Builder type for project rules. Generic over project and module context types.
pub opaque type ProjectRuleSchema(project_context, module_context)
A fully-built rule. Opaque – context types are erased via closures.
The orchestrator holds List(Rule) without knowing context types.
pub opaque type Rule
An error produced by a rule visitor. Opaque – use error() to create.
pub opaque type RuleError
Values
pub fn error(
message message: String,
details details: String,
location location: glance.Span,
) -> RuleError
Create an error with a message, details, and source location.
pub fn error_details(err: RuleError) -> String
pub fn error_location(err: RuleError) -> glance.Span
pub fn error_message(err: RuleError) -> String
pub fn is_project_rule(rule: Rule) -> Bool
pub fn module_rule_from_fn(
name name: String,
default_severity default_severity: Severity,
run run: fn(glance.Module, String) -> List(RuleError),
) -> Rule
Build a module Rule from a custom run function. Use this when a rule needs access to the full module before visitor traversal (e.g. pre-collecting function signatures).
pub fn new(name name: String) -> ModuleRuleSchema(Nil)
Create a new module rule schema with no context.
pub fn new_project(
name name: String,
initial initial: project_context,
) -> ProjectRuleSchema(project_context, module_context)
Create a new project rule schema. Project rules visit every file in sequence, carrying accumulated state across modules. Use when your lint needs cross-file context (unused exports, duplicate definitions, etc.).
The initial value seeds the project context. Module-level context is
derived from it via with_module_context.
pub fn new_with_context(
name name: String,
initial initial: context,
) -> ModuleRuleSchema(context)
Create a new module rule schema with an initial context value.
pub fn run_on_module(
rule rule: Rule,
module module: glance.Module,
source source: String,
) -> List(RuleError)
Run a module rule on a single parsed module. Returns errors.
pub fn run_on_project(
rule rule: Rule,
files files: List(#(String, glance.Module, String)),
) -> List(#(String, RuleError))
Run a project rule on all files. Returns errors paired with their file path. Module-visitor errors are tagged with the file they came from; final evaluation errors are tagged with whatever path the callback provides.
pub fn to_module_rule(schema: ModuleRuleSchema(context)) -> Rule
Build a Rule from a ModuleRuleSchema, erasing the context type via closures.
pub fn to_project_rule(schema: ProjectRuleSchema(pc, mc)) -> Rule
Build a Rule from a ProjectRuleSchema, erasing context types via closures.
pub fn visit_module(
module module: glance.Module,
schema schema: ModuleRuleSchema(context),
source source: String,
) -> #(List(RuleError), context)
Visit a module with a rule schema, returning errors and final context. Drives all registered visitors through a depth-first AST traversal.
pub fn with_default_severity(
schema schema: ModuleRuleSchema(context),
severity severity: Severity,
) -> ModuleRuleSchema(context)
pub fn with_expression_enter_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(glance.Expression, glance.Span, context) -> #(
List(RuleError),
context,
),
) -> ModuleRuleSchema(context)
Register an expression visitor called before children are traversed (top-down). Use when you need to check an expression before its children, e.g. enforcing a nesting depth limit on entry.
pub fn with_expression_exit_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(glance.Expression, glance.Span, context) -> #(
List(RuleError),
context,
),
) -> ModuleRuleSchema(context)
Register an expression visitor called after children are traversed (bottom-up). Use when you need information about child expressions first, e.g. collecting types of sub-expressions before checking the parent.
pub fn with_final_evaluation(
schema schema: ModuleRuleSchema(context),
evaluator evaluator: fn(context) -> List(RuleError),
) -> ModuleRuleSchema(context)
Register a callback that runs after all traversal completes. Receives the final context so you can emit deferred errors, for example flagging variables that were collected but never used.
pub fn with_final_project_evaluation(
schema schema: ProjectRuleSchema(pc, mc),
evaluator evaluator: fn(pc) -> List(#(String, RuleError)),
) -> ProjectRuleSchema(pc, mc)
pub fn with_function_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(
glance.Definition(glance.Function),
glance.Span,
context,
) -> #(List(RuleError), context),
) -> ModuleRuleSchema(context)
Visit every function definition (signature + body). The span covers the full definition. Use for checking return types, parameter labels, or function-level annotations. The function body is traversed separately by expression/statement visitors after this visitor runs.
pub fn with_import_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(glance.Definition(glance.Import), context) -> #(
List(RuleError),
context,
),
) -> ModuleRuleSchema(context)
Visit every import definition. Use for checking import style
(qualified vs. unqualified), duplicate imports, or forbidden modules.
The span is available on glance.Definition(glance.Import) via .location.
pub fn with_module_context(
schema schema: ProjectRuleSchema(pc, mc),
from_project_to_module from_project_to_module: fn(pc) -> mc,
from_module_to_project from_module_to_project: fn(mc, pc) -> pc,
) -> ProjectRuleSchema(pc, mc)
Set up context bridging between project and module levels. The first function creates a module context from the project context (called before each file). The second folds the module context back into the project context (called after each file).
pub fn with_module_visitor(
schema schema: ProjectRuleSchema(pc, mc),
builder builder: fn(ModuleRuleSchema(mc)) -> ModuleRuleSchema(
mc,
),
) -> ProjectRuleSchema(pc, mc)
Register a module-level visitor builder for project rules. The builder
receives a fresh ModuleRuleSchema for each file and should attach the
visitors needed per module (e.g. collecting references from each file).
The orchestrator runs this visitor on every file in sequence, folding
module context back into project context via with_module_context.
pub fn with_project_default_severity(
schema schema: ProjectRuleSchema(pc, mc),
severity severity: Severity,
) -> ProjectRuleSchema(pc, mc)
pub fn with_simple_expression_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(glance.Expression, glance.Span) -> List(
RuleError,
),
) -> ModuleRuleSchema(context)
pub fn with_simple_function_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(
glance.Definition(glance.Function),
glance.Span,
) -> List(RuleError),
) -> ModuleRuleSchema(context)
pub fn with_simple_import_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(glance.Definition(glance.Import)) -> List(
RuleError,
),
) -> ModuleRuleSchema(context)
pub fn with_simple_statement_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(glance.Statement) -> List(RuleError),
) -> ModuleRuleSchema(context)
pub fn with_statement_visitor(
schema schema: ModuleRuleSchema(context),
visitor visitor: fn(glance.Statement, context) -> #(
List(RuleError),
context,
),
) -> ModuleRuleSchema(context)
Visit every top-level statement in function bodies and blocks:
use, let, bare expressions, and assert. Expression visitors fire
separately for each expression within the statement. Use this when you
care about the statement kind (e.g. flagging let assert).