216 lines
6.3 KiB
Markdown
216 lines
6.3 KiB
Markdown
# gulie
|
|
|
|
A linter, static analyser, and formatter for [Guile Scheme](https://www.gnu.org/software/guile/).
|
|
|
|
```
|
|
$ 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-name` on a line, or region
|
|
disable/enable blocks
|
|
- **Auto-fix mode**: `--fix` applies automatic corrections where available
|
|
- **Configuration**: `.gulie.sexp` in your project root, overridable via CLI
|
|
- **CI friendly**: exit code 0 for clean, 1 for findings
|
|
|
|
## Requirements
|
|
|
|
- [Guile](https://www.gnu.org/software/guile/) 3.0 or later
|
|
|
|
## Installation
|
|
|
|
Clone the repository and ensure `bin/gulie` is on your `PATH`, or run it
|
|
directly:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
gulie mylib.scm
|
|
```
|
|
|
|
Check an entire project:
|
|
|
|
```sh
|
|
gulie src/
|
|
```
|
|
|
|
Auto-fix trailing whitespace and other fixable issues:
|
|
|
|
```sh
|
|
gulie --fix src/
|
|
```
|
|
|
|
Generate a config template:
|
|
|
|
```sh
|
|
gulie --init
|
|
```
|
|
|
|
## Configuration
|
|
|
|
gulie looks for `.gulie.sexp` in the current directory and parent directories.
|
|
Generate a template with `gulie --init`.
|
|
|
|
```scheme
|
|
((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:
|
|
|
|
```scheme
|
|
(define x "messy") ; gulie:suppress trailing-whitespace
|
|
```
|
|
|
|
Suppress all rules on the next line:
|
|
|
|
```scheme
|
|
;; gulie:suppress
|
|
(define intentionally-long-variable-name "value")
|
|
```
|
|
|
|
Region disable/enable:
|
|
|
|
```scheme
|
|
;; 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
|
|
|
|
```sh
|
|
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]
|