Gulie – the GUile LInt Exorcist
A linter, static analyser, and formatter for Guile Scheme.
$ gulie gulie/
gulie/engine.scm:97:80: warning: line-length: line exceeds 80 characters (82)
gulie/tokenizer.scm:131:0: warning: trailing-whitespace: trailing whitespace
Why
No linter, formatter, or static analysis tool exists for Guile Scheme. gulie fills that gap with a two-pass architecture that catches both surface-level formatting issues and deep semantic problems.
Features
- Surface rules (no parsing needed): trailing whitespace, line length, tabs, excessive blank lines, comment style conventions
- Semantic rules (via Guile's compiler): unused variables, unbound variables, arity mismatches, format string errors, shadowed top-levels, unused modules
- Inline suppression:
; gulie:suppress rule-nameon a line, or region disable/enable blocks - Auto-fix mode:
--fixapplies automatic corrections where available - Configuration:
.gulie.sexpin your project root, overridable via CLI - CI friendly: exit code 0 for clean, 1 for findings
Requirements
- Guile 3.0 or later
Installation
Clone the repository and ensure bin/gulie is on your PATH, or run it
directly:
bin/gulie --check .
Usage
gulie [OPTIONS] [FILE|DIR...]
Options:
-h, --help Show help message
-v, --version Print version
--check Check mode (default): report issues, exit non-zero on findings
--fix Fix mode: auto-fix what's possible, report the rest
--init Generate .gulie.sexp template in current directory
--pass PASS Run only: surface, semantic, all (default: all)
--config FILE Config file path (default: auto-discover .gulie.sexp)
--rule RULE Enable only this rule
--disable RULE Disable this rule
--severity SEV Minimum severity: error, warning, info
--output FORMAT Output format: standard (default), json, compact
--list-rules List all available rules
Examples
Check a single file:
gulie mylib.scm
Check an entire project:
gulie src/
Auto-fix trailing whitespace and other fixable issues:
gulie --fix src/
Generate a config template:
gulie --init
Configuration
gulie looks for .gulie.sexp in the current directory and parent directories.
Generate a template with gulie --init.
((line-length . 80)
(indent . 2)
(max-blank-lines . 2)
(enable trailing-whitespace line-length no-tabs blank-lines
comment-semicolons unused-variable unbound-variable
arity-mismatch)
(disable)
(rules
(line-length (max . 100)))
(indent-rules
(define . 1) (let . 1) (lambda . 1)
(with-syntax . 1) (match . 1))
(ignore "build/**" ".direnv/**"))
Inline Suppression
Suppress a rule on the current line:
(define x "messy") ; gulie:suppress trailing-whitespace
Suppress all rules on the next line:
;; gulie:suppress
(define intentionally-long-variable-name "value")
Region disable/enable:
;; gulie:disable line-length
(define long-line ...............................................)
(define another .................................................)
;; gulie:enable line-length
Architecture
gulie uses a two-pass design:
.gulie.sexp
|
file.scm --+--> [Tokenizer] --> tokens --> [CST parser] --> CST
| |
| [Pass 1: Surface] line rules + CST rules
| |
| diagnostics-1
|
+--> [Guile compiler] --> Tree-IL --> CPS
|
[Pass 2: Semantic] Guile's built-in analyses
|
diagnostics-2
|
[merge + suppress + sort + report]
Pass 1 uses a hand-written tokenizer that preserves all whitespace, comments,
and exact source text. The critical invariant:
(string-concatenate (map token-text (tokenize input))) reproduces the input
exactly. This feeds a lightweight concrete syntax tree for formatting checks.
Pass 2 delegates to Guile's own compiler and analysis infrastructure:
unused-variable-analysis, arity-analysis, format-analysis, and others.
These are battle-tested and handle macroexpansion correctly.
The two passes are independent because Guile's reader irrecoverably strips comments and whitespace — there is no way to get formatting info and semantic info from a single parse.
Rules
| Rule | Type | Category | Description |
|---|---|---|---|
trailing-whitespace |
line | format | Trailing spaces or tabs |
line-length |
line | format | Line exceeds maximum width |
no-tabs |
line | format | Tab characters in source |
blank-lines |
line | format | Excessive consecutive blank lines |
comment-semicolons |
cst | style | Comment style conventions (;/;;/;;;) |
unused-variable |
semantic | correctness | Unused local variable |
unused-toplevel |
semantic | correctness | Unused top-level definition |
unused-module |
semantic | correctness | Unused module import |
unbound-variable |
semantic | correctness | Reference to undefined variable |
arity-mismatch |
semantic | correctness | Wrong number of arguments |
shadowed-toplevel |
semantic | correctness | Top-level binding shadows import |
format-string |
semantic | correctness | Format string validation |
Module Structure
gulie/
cli.scm Command-line interface
config.scm Configuration loading and merging
diagnostic.scm Diagnostic record type and formatting
tokenizer.scm Hand-written lexer preserving all tokens
cst.scm Token stream to concrete syntax tree
compiler.scm Guile compiler wrapper for semantic analysis
rule.scm Rule record type and registry
engine.scm Orchestrator: file discovery, pass sequencing
suppression.scm Inline suppression parsing and filtering
rules/
surface.scm Line-based formatting rules
comments.scm Comment style rules
Testing
guile --no-auto-compile -L . -s test/run-tests.scm
84 tests covering tokenizer roundtrip, CST parsing, surface rules, suppression, and semantic analysis.
Licence
[TODO: add licence]