Fix 8 bugs found by running gulie on the Guix source tree
Bug 1: Config auto-discovery — load-config now walks CWD and parent directories for .gulie.sexp when no --config is given, matching the documented behavior. Added find-config helper. Bug 2: Compile-error messages garbled — the catch handler now properly destructures Guile exception args (subr fmt fmt-args . _) and uses apply+format instead of raw display, producing readable messages like "no code for module (guix licenses)" instead of "#f no code for module ~S ((guix licenses)) #f". Bug 3: --init template unreadable — generate-template now uses pretty-print instead of write, producing properly indented output. Bug 4: CLI options silently ignored — --pass, --disable, --rule, and --severity are now wired up in main and threaded into config as %pass, augmented disable list, enable list, and %min-severity respectively. Bug 5: enable/disable config keys never consulted — added filter-rules-by-config, used in run-line-rules and run-cst-rules. Bug 6: blank-lines rule spammed one diagnostic per excess line — changed trigger to fire once per group at the boundary. Bug 7: read-file-to-string slow — replaced line-by-line accumulation with get-string-all. Bug 8: No way to skip semantic pass — lint-file now respects %pass config key (surface/semantic/all) and %min-severity for filtering.
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,5 +10,9 @@ devenv.local.yaml
|
|||||||
# pre-commit
|
# pre-commit
|
||||||
.pre-commit-config.yaml
|
.pre-commit-config.yaml
|
||||||
|
|
||||||
|
# Agents
|
||||||
|
/.pi/
|
||||||
|
|
||||||
# Temporary
|
# Temporary
|
||||||
|
.gulie.sexp
|
||||||
/refs/
|
/refs/
|
||||||
|
|||||||
@@ -74,7 +74,6 @@
|
|||||||
0)
|
0)
|
||||||
|
|
||||||
((option-ref options 'list-rules #f)
|
((option-ref options 'list-rules #f)
|
||||||
;; Ensure rules are loaded
|
|
||||||
(list-all-rules)
|
(list-all-rules)
|
||||||
0)
|
0)
|
||||||
|
|
||||||
@@ -93,6 +92,31 @@
|
|||||||
(let* ((config-path (option-ref options 'config #f))
|
(let* ((config-path (option-ref options 'config #f))
|
||||||
(user-config (load-config config-path))
|
(user-config (load-config config-path))
|
||||||
(config (merge-configs default-config user-config))
|
(config (merge-configs default-config user-config))
|
||||||
|
;; Wire up --pass
|
||||||
|
(pass-str (option-ref options 'pass #f))
|
||||||
|
(config (if pass-str
|
||||||
|
(cons (cons '%pass (string->symbol pass-str)) config)
|
||||||
|
config))
|
||||||
|
;; Wire up --disable (add to existing disable list)
|
||||||
|
(cli-disable (option-ref options 'disable #f))
|
||||||
|
(config (if cli-disable
|
||||||
|
(let* ((current (or (assq-ref config 'disable) '()))
|
||||||
|
(new (cons (string->symbol cli-disable) current)))
|
||||||
|
(cons (cons 'disable new) config))
|
||||||
|
config))
|
||||||
|
;; Wire up --rule (single-rule mode: set enable to just that rule)
|
||||||
|
(cli-rule (option-ref options 'rule #f))
|
||||||
|
(config (if cli-rule
|
||||||
|
(cons (cons 'enable (list (string->symbol cli-rule)))
|
||||||
|
config)
|
||||||
|
config))
|
||||||
|
;; Wire up --severity
|
||||||
|
(cli-severity (option-ref options 'severity #f))
|
||||||
|
(config (if cli-severity
|
||||||
|
(cons (cons '%min-severity
|
||||||
|
(string->symbol cli-severity))
|
||||||
|
config)
|
||||||
|
config))
|
||||||
(paths (if (null? rest) (list ".") rest))
|
(paths (if (null? rest) (list ".") rest))
|
||||||
(ignore-pats (config-ignore-patterns config))
|
(ignore-pats (config-ignore-patterns config))
|
||||||
(files (discover-scheme-files paths ignore-pats)))
|
(files (discover-scheme-files paths ignore-pats)))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
(define-module (gulie compiler)
|
(define-module (gulie compiler)
|
||||||
#:use-module (system base compile)
|
#:use-module (system base compile)
|
||||||
|
#:use-module (ice-9 match)
|
||||||
#:use-module (ice-9 regex)
|
#:use-module (ice-9 regex)
|
||||||
#:use-module (srfi srfi-1)
|
#:use-module (srfi srfi-1)
|
||||||
#:use-module (gulie diagnostic)
|
#:use-module (gulie diagnostic)
|
||||||
@@ -67,13 +68,32 @@ Returns a list of <diagnostic> records."
|
|||||||
#:warning-level 3
|
#:warning-level 3
|
||||||
#:env (make-fresh-user-module))))
|
#:env (make-fresh-user-module))))
|
||||||
(lambda (key . args)
|
(lambda (key . args)
|
||||||
;; Compilation errors become diagnostics too
|
;; Format the error message properly.
|
||||||
|
;; Guile exceptions typically have args: (subr fmt fmt-args data)
|
||||||
(let ((msg (call-with-output-string
|
(let ((msg (call-with-output-string
|
||||||
(lambda (p)
|
(lambda (p)
|
||||||
(display key p)
|
(display key p)
|
||||||
(display ": " p)
|
(display ": " p)
|
||||||
(for-each (lambda (a) (display a p) (display " " p))
|
(catch #t
|
||||||
args)))))
|
(lambda ()
|
||||||
|
(match args
|
||||||
|
((subr fmt fmt-args . _)
|
||||||
|
(when subr
|
||||||
|
(display subr p)
|
||||||
|
(display ": " p))
|
||||||
|
(if fmt-args
|
||||||
|
(apply format p fmt (if (list? fmt-args)
|
||||||
|
fmt-args
|
||||||
|
(list fmt-args)))
|
||||||
|
(display fmt p)))
|
||||||
|
(_ (for-each (lambda (a)
|
||||||
|
(display a p) (display " " p))
|
||||||
|
args))))
|
||||||
|
(lambda _
|
||||||
|
;; Fallback: just display the raw args
|
||||||
|
(for-each (lambda (a)
|
||||||
|
(display a p) (display " " p))
|
||||||
|
args)))))))
|
||||||
(set! diagnostics
|
(set! diagnostics
|
||||||
(cons (make-diagnostic file 1 0 'error 'compile-error msg #f)
|
(cons (make-diagnostic file 1 0 'error 'compile-error msg #f)
|
||||||
diagnostics))))))
|
diagnostics))))))
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
(define-module (gulie config)
|
(define-module (gulie config)
|
||||||
#:use-module (srfi srfi-1)
|
#:use-module (srfi srfi-1)
|
||||||
|
#:use-module (ice-9 pretty-print)
|
||||||
#:export (default-config
|
#:export (default-config
|
||||||
|
find-config
|
||||||
load-config
|
load-config
|
||||||
merge-configs
|
merge-configs
|
||||||
config-ref
|
config-ref
|
||||||
@@ -90,14 +92,25 @@
|
|||||||
(define (config-indent-rules config)
|
(define (config-indent-rules config)
|
||||||
(or (config-ref config 'indent-rules) '()))
|
(or (config-ref config 'indent-rules) '()))
|
||||||
|
|
||||||
|
(define (find-config start-dir)
|
||||||
|
"Search START-DIR and its ancestors for .gulie.sexp. Returns the
|
||||||
|
path if found, or #f."
|
||||||
|
(let lp ((dir start-dir))
|
||||||
|
(let ((candidate (string-append dir "/.gulie.sexp")))
|
||||||
|
(cond
|
||||||
|
((file-exists? candidate) candidate)
|
||||||
|
((string=? dir "/") #f)
|
||||||
|
(else (lp (dirname dir)))))))
|
||||||
(define (load-config path)
|
(define (load-config path)
|
||||||
"Load a .gulie.sexp config file at PATH. Returns an alist."
|
"Load a .gulie.sexp config file at PATH. Returns an alist.
|
||||||
(if (and path (file-exists? path))
|
If PATH is #f, auto-discover by searching CWD and parent directories."
|
||||||
(call-with-input-file path
|
(let ((resolved (or path (find-config (getcwd)))))
|
||||||
|
(if (and resolved (file-exists? resolved))
|
||||||
|
(call-with-input-file resolved
|
||||||
(lambda (port)
|
(lambda (port)
|
||||||
(let ((data (read port)))
|
(let ((data (read port)))
|
||||||
(if (list? data) data '()))))
|
(if (list? data) data '()))))
|
||||||
'()))
|
'())))
|
||||||
|
|
||||||
(define (merge-configs base override)
|
(define (merge-configs base override)
|
||||||
"Merge OVERRIDE config on top of BASE. Override wins for scalar values;
|
"Merge OVERRIDE config on top of BASE. Override wins for scalar values;
|
||||||
@@ -115,5 +128,5 @@ lists are replaced, not appended."
|
|||||||
(display ";;; gulie configuration\n" port)
|
(display ";;; gulie configuration\n" port)
|
||||||
(display ";;; Place this file as .gulie.sexp in your project root.\n" port)
|
(display ";;; Place this file as .gulie.sexp in your project root.\n" port)
|
||||||
(display ";;; All fields are optional — defaults are shown below.\n\n" port)
|
(display ";;; All fields are optional — defaults are shown below.\n\n" port)
|
||||||
(write default-config port)
|
(pretty-print default-config port #:width 60 #:max-expr-width 50)
|
||||||
(newline port))
|
(newline port))
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
(define-module (gulie engine)
|
(define-module (gulie engine)
|
||||||
#:use-module (ice-9 ftw)
|
#:use-module (ice-9 ftw)
|
||||||
#:use-module (ice-9 rdelim)
|
#:use-module (ice-9 textual-ports)
|
||||||
#:use-module (srfi srfi-1)
|
#:use-module (srfi srfi-1)
|
||||||
#:use-module (gulie diagnostic)
|
#:use-module (gulie diagnostic)
|
||||||
#:use-module (gulie rule)
|
#:use-module (gulie rule)
|
||||||
@@ -17,17 +17,37 @@
|
|||||||
|
|
||||||
(define (read-file-to-string path)
|
(define (read-file-to-string path)
|
||||||
"Read entire file at PATH into a string."
|
"Read entire file at PATH into a string."
|
||||||
(call-with-input-file path
|
(call-with-input-file path get-string-all))
|
||||||
(lambda (port)
|
|
||||||
(let lp ((acc '()))
|
|
||||||
(let ((line (read-line port)))
|
|
||||||
(if (eof-object? line)
|
|
||||||
(string-join (reverse acc) "\n")
|
|
||||||
(lp (cons line acc))))))))
|
|
||||||
|
|
||||||
|
(define (severity-rank sev)
|
||||||
|
"Return a numeric rank for severity: error=3, warning=2, info=1."
|
||||||
|
(case sev
|
||||||
|
((error) 3)
|
||||||
|
((warning) 2)
|
||||||
|
((info) 1)
|
||||||
|
(else 0)))
|
||||||
|
(define (severity>=? sev min-sev)
|
||||||
|
"Is SEV at least as severe as MIN-SEV?"
|
||||||
|
(>= (severity-rank sev) (severity-rank min-sev)))
|
||||||
|
(define (filter-rules-by-config rules config)
|
||||||
|
"Filter RULES according to enable/disable lists in CONFIG.
|
||||||
|
If enable is non-empty, only those rules are kept.
|
||||||
|
Then any rules in disable are removed."
|
||||||
|
(let ((enabled (or (assq-ref config 'enable) '()))
|
||||||
|
(disabled (or (assq-ref config 'disable) '())))
|
||||||
|
(let ((rules (if (null? enabled)
|
||||||
|
rules
|
||||||
|
(filter (lambda (r)
|
||||||
|
(memq (rule-name r) enabled))
|
||||||
|
rules))))
|
||||||
|
(if (null? disabled)
|
||||||
|
rules
|
||||||
|
(filter (lambda (r)
|
||||||
|
(not (memq (rule-name r) disabled)))
|
||||||
|
rules)))))
|
||||||
(define (run-line-rules file lines config)
|
(define (run-line-rules file lines config)
|
||||||
"Run all line-type rules against LINES. Returns list of diagnostics."
|
"Run all line-type rules against LINES. Returns list of diagnostics."
|
||||||
(let ((line-rules (rules-of-type 'line))
|
(let ((line-rules (filter-rules-by-config (rules-of-type 'line) config))
|
||||||
(diagnostics '())
|
(diagnostics '())
|
||||||
(consecutive-blanks 0))
|
(consecutive-blanks 0))
|
||||||
(when (not (null? line-rules))
|
(when (not (null? line-rules))
|
||||||
@@ -52,7 +72,7 @@
|
|||||||
|
|
||||||
(define (run-cst-rules file cst config)
|
(define (run-cst-rules file cst config)
|
||||||
"Run all cst-type rules against CST. Returns list of diagnostics."
|
"Run all cst-type rules against CST. Returns list of diagnostics."
|
||||||
(let ((cst-rules (rules-of-type 'cst)))
|
(let ((cst-rules (filter-rules-by-config (rules-of-type 'cst) config)))
|
||||||
(if (null? cst-rules)
|
(if (null? cst-rules)
|
||||||
'()
|
'()
|
||||||
(append-map
|
(append-map
|
||||||
@@ -61,15 +81,19 @@
|
|||||||
cst-rules))))
|
cst-rules))))
|
||||||
|
|
||||||
(define (lint-file file config)
|
(define (lint-file file config)
|
||||||
"Lint a single FILE with CONFIG. Returns a sorted list of diagnostics."
|
"Lint a single FILE with CONFIG. Returns a sorted list of diagnostics.
|
||||||
|
CONFIG may contain '%pass with value 'surface, 'semantic, or 'all (default)
|
||||||
|
and '%min-severity with value 'error, 'warning, or 'info (default)."
|
||||||
(let* ((text (read-file-to-string file))
|
(let* ((text (read-file-to-string file))
|
||||||
(lines (string-split text #\newline))
|
(lines (string-split text #\newline))
|
||||||
|
(pass (or (assq-ref config '%pass) 'all))
|
||||||
|
(min-severity (or (assq-ref config '%min-severity) 'info))
|
||||||
(diagnostics '()))
|
(diagnostics '()))
|
||||||
;; Pass 1: line-based surface rules
|
;; Pass 1: line-based surface rules
|
||||||
|
(when (memq pass '(all surface))
|
||||||
(set! diagnostics (append (run-line-rules file lines config)
|
(set! diagnostics (append (run-line-rules file lines config)
|
||||||
diagnostics))
|
diagnostics))
|
||||||
;; Pass 1b: CST rules (if tokenizer is loaded)
|
;; Pass 1b: CST rules (if tokenizer is loaded)
|
||||||
;; Dynamically check if tokenizer module is available
|
|
||||||
(let ((tok-mod (resolve-module '(gulie tokenizer) #:ensure #f)))
|
(let ((tok-mod (resolve-module '(gulie tokenizer) #:ensure #f)))
|
||||||
(when tok-mod
|
(when tok-mod
|
||||||
(let ((tokenize (module-ref tok-mod 'tokenize))
|
(let ((tokenize (module-ref tok-mod 'tokenize))
|
||||||
@@ -79,16 +103,23 @@
|
|||||||
(tokens (tokenize text file))
|
(tokens (tokenize text file))
|
||||||
(cst (parse-cst tokens)))
|
(cst (parse-cst tokens)))
|
||||||
(set! diagnostics (append (run-cst-rules file cst config)
|
(set! diagnostics (append (run-cst-rules file cst config)
|
||||||
diagnostics)))))))
|
diagnostics))))))))
|
||||||
;; Pass 2: semantic rules (if compiler module is loaded)
|
;; Pass 2: semantic rules (if compiler module is loaded)
|
||||||
|
(when (memq pass '(all semantic))
|
||||||
(let ((comp-mod (resolve-module '(gulie compiler) #:ensure #f)))
|
(let ((comp-mod (resolve-module '(gulie compiler) #:ensure #f)))
|
||||||
(when comp-mod
|
(when comp-mod
|
||||||
(let ((compile-and-capture (module-ref comp-mod 'compile-and-capture-warnings)))
|
(let ((compile-and-capture (module-ref comp-mod 'compile-and-capture-warnings)))
|
||||||
(set! diagnostics (append (compile-and-capture file text config)
|
(set! diagnostics (append (compile-and-capture file text config)
|
||||||
diagnostics)))))
|
diagnostics))))))
|
||||||
;; Filter suppressions
|
;; Filter suppressions
|
||||||
(let ((suppressions (parse-suppressions text)))
|
(let ((suppressions (parse-suppressions text)))
|
||||||
(set! diagnostics (filter-suppressions diagnostics suppressions)))
|
(set! diagnostics (filter-suppressions diagnostics suppressions)))
|
||||||
|
;; Filter by minimum severity
|
||||||
|
(when (not (eq? min-severity 'info))
|
||||||
|
(set! diagnostics
|
||||||
|
(filter (lambda (d)
|
||||||
|
(severity>=? (diagnostic-severity d) min-severity))
|
||||||
|
diagnostics)))
|
||||||
;; Sort by location
|
;; Sort by location
|
||||||
(sort diagnostics diagnostic<?)))
|
(sort diagnostics diagnostic<?)))
|
||||||
|
|
||||||
|
|||||||
@@ -83,9 +83,9 @@
|
|||||||
(let ((max-blanks (or (assq-ref config 'max-blank-lines) 2))
|
(let ((max-blanks (or (assq-ref config 'max-blank-lines) 2))
|
||||||
(consecutive (or (assq-ref config '%consecutive-blanks) 0)))
|
(consecutive (or (assq-ref config '%consecutive-blanks) 0)))
|
||||||
(if (and (string-every char-whitespace? line-text)
|
(if (and (string-every char-whitespace? line-text)
|
||||||
(> consecutive max-blanks))
|
(= consecutive (1+ max-blanks)))
|
||||||
(list (make-diagnostic
|
(list (make-diagnostic
|
||||||
file line-num 0
|
file (- line-num max-blanks) 0
|
||||||
'warning 'blank-lines
|
'warning 'blank-lines
|
||||||
(format #f "more than ~a consecutive blank lines" max-blanks)
|
(format #f "more than ~a consecutive blank lines" max-blanks)
|
||||||
#f))
|
#f))
|
||||||
|
|||||||
Reference in New Issue
Block a user