123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo@gnu.org>
- ;;;
- ;;; This file is part of GNU Guix.
- ;;;
- ;;; GNU Guix is free software; you can redistribute it and/or modify it
- ;;; under the terms of the GNU General Public License as published by
- ;;; the Free Software Foundation; either version 3 of the License, or (at
- ;;; your option) any later version.
- ;;;
- ;;; GNU Guix is distributed in the hope that it will be useful, but
- ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
- ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;;; GNU General Public License for more details.
- ;;;
- ;;; You should have received a copy of the GNU General Public License
- ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
- (define-module (guix diagnostics)
- #:use-module (guix colors)
- #:use-module (guix i18n)
- #:use-module (srfi srfi-1)
- #:use-module (srfi srfi-9)
- #:use-module (srfi srfi-26)
- #:use-module (srfi srfi-35)
- #:use-module (ice-9 format)
- #:use-module (ice-9 match)
- #:export (warning
- info
- report-error
- leave
- <location>
- location
- location?
- location-file
- location-line
- location-column
- absolute-location
- source-properties->location
- location->source-properties
- location->string
- location->hyperlink
- &error-location
- error-location?
- error-location
- formatted-message
- formatted-message?
- formatted-message-string
- formatted-message-arguments
- &fix-hint
- fix-hint?
- condition-fix-hint
- guix-warning-port
- program-name
- define-with-syntax-properties))
- ;;; Commentary:
- ;;;
- ;;; This module provides the tools to report diagnostics to the user in a
- ;;; consistent way: errors, warnings, and notes.
- ;;;
- ;;; Code:
- (define (trivial-format-string? fmt)
- (define len
- (string-length fmt))
- (let loop ((start 0))
- (or (>= (+ 1 start) len)
- (let ((tilde (string-index fmt #\~ start)))
- (or (not tilde)
- (case (string-ref fmt (+ tilde 1))
- ((#\a #\A #\%) (loop (+ tilde 2)))
- (else #f)))))))
- (define-syntax highlight-argument
- (lambda (s)
- "Given FMT and ARG, expand ARG to a call that highlights it, provided FMT
- is a trivial format string."
- ;; Be conservative: limit format argument highlighting to cases where the
- ;; format string contains nothing but ~a escapes. If it contained ~s
- ;; escapes, this strategy wouldn't work.
- (syntax-case s ()
- ((_ "~a~%" arg) ;don't highlight whole messages
- #'arg)
- ((_ fmt arg)
- (trivial-format-string? (syntax->datum #'fmt))
- #'(%highlight-argument arg))
- ((_ fmt arg)
- #'arg))))
- (define* (%highlight-argument arg #:optional (port (guix-warning-port)))
- "Highlight ARG, a format string argument, if PORT supports colors."
- (cond ((string? arg)
- ;; If ARG contains white space, don't highlight it, on the grounds
- ;; that it may be a complete message in its own, like those produced
- ;; by 'guix lint.
- (if (string-any char-set:whitespace arg)
- arg
- (highlight arg port)))
- ((symbol? arg)
- (highlight (symbol->string arg) port))
- (else arg)))
- (define-syntax define-diagnostic
- (syntax-rules ()
- "Create a diagnostic macro (i.e., NAME), which will prepend PREFIX to all
- messages."
- ((_ name (G_ prefix) colors)
- (define-syntax name
- (lambda (x)
- (syntax-case x ()
- ((name location (underscore fmt) args (... ...))
- (and (string? (syntax->datum #'fmt))
- (free-identifier=? #'underscore #'G_))
- #'(begin
- (print-diagnostic-prefix prefix location
- #:colors colors)
- (format (guix-warning-port) (gettext fmt %gettext-domain)
- (highlight-argument fmt args) (... ...))))
- ((name location (N-underscore singular plural n)
- args (... ...))
- (and (string? (syntax->datum #'singular))
- (string? (syntax->datum #'plural))
- (free-identifier=? #'N-underscore #'N_))
- #'(begin
- (print-diagnostic-prefix prefix location
- #:colors colors)
- (format (guix-warning-port)
- (ngettext singular plural n %gettext-domain)
- (highlight-argument singular args) (... ...))))
- ((name (underscore fmt) args (... ...))
- (free-identifier=? #'underscore #'G_)
- #'(name #f (underscore fmt) args (... ...)))
- ((name (N-underscore singular plural n)
- args (... ...))
- (free-identifier=? #'N-underscore #'N_)
- #'(name #f (N-underscore singular plural n)
- args (... ...)))
- (id
- (identifier? #'id)
- ;; Run-time variant.
- #'(lambda (location fmt . args)
- (emit-diagnostic fmt args
- #:location location
- #:prefix prefix
- #:colors colors)))))))))
- ;; XXX: This doesn't work well for right-to-left languages.
- ;; TRANSLATORS: The goal is to emit "warning:" followed by a short phrase;
- ;; "~a" is a placeholder for that phrase.
- (define-diagnostic warning (G_ "warning: ") %warning-color) ;emit a warning
- (define-diagnostic info (G_ "") %info-color)
- (define-diagnostic report-error (G_ "error: ") %error-color)
- (define-syntax-rule (leave args ...)
- "Emit an error message and exit."
- (begin
- (report-error args ...)
- (exit 1)))
- (define* (emit-diagnostic fmt args
- #:key location (colors (color)) (prefix ""))
- "Report diagnostic message FMT with the given ARGS and the specified
- LOCATION, COLORS, and PREFIX.
- This procedure is used as a last resort when the format string is not known at
- macro-expansion time."
- (print-diagnostic-prefix (gettext prefix %gettext-domain)
- location #:colors colors)
- (apply format (guix-warning-port) fmt
- (if (trivial-format-string? fmt)
- (map %highlight-argument args)
- args)))
- (define %warning-color (color BOLD MAGENTA))
- (define %info-color (color BOLD))
- (define %error-color (color BOLD RED))
- (define* (print-diagnostic-prefix prefix #:optional location
- #:key (colors (color)))
- "Print PREFIX as a diagnostic line prefix."
- (define color?
- (color-output? (guix-warning-port)))
- (define location-color
- (if color?
- (cut colorize-string <> (color BOLD))
- identity))
- (define prefix-color
- (if color?
- (lambda (prefix)
- (colorize-string prefix colors))
- identity))
- (let ((prefix (if (string-null? prefix)
- prefix
- (gettext prefix %gettext-domain))))
- (if location
- (format (guix-warning-port) "~a: ~a"
- (location-color
- (if (supports-hyperlinks? (guix-warning-port))
- (location->hyperlink location)
- (location->string location)))
- (prefix-color prefix))
- (format (guix-warning-port) "~:[~*~;guix ~a: ~]~a"
- (program-name) (program-name)
- (prefix-color prefix)))))
- ;; A source location.
- (define-record-type <location>
- (make-location file line column)
- location?
- (file location-file) ; file name
- (line location-line) ; 1-indexed line
- (column location-column)) ; 0-indexed column
- (define (location file line column)
- "Return the <location> object for the given FILE, LINE, and COLUMN."
- (and line column file
- (make-location file line column)))
- (define (source-properties->location loc)
- "Return a location object based on the info in LOC, an alist as returned
- by Guile's `source-properties', `frame-source', `current-source-location',
- etc."
- ;; In accordance with the GCS, start line and column numbers at 1. Note
- ;; that unlike LINE and `port-column', COL is actually 0-indexed here...
- (match loc
- ((('line . line) ('column . col) ('filename . file)) ;common case
- (and file line col
- (make-location file (+ line 1) col)))
- (#f
- #f)
- (#(file line column)
- ;; Guile >= 3.0.6 uses vectors instead of alists internally, which can be
- ;; seen in the arguments to 'syntax-error' exceptions.
- (location file (+ 1 line) column))
- (_
- (let ((file (assq-ref loc 'filename))
- (line (assq-ref loc 'line))
- (col (assq-ref loc 'column)))
- (location file (and line (+ line 1)) col)))))
- (define (location->source-properties loc)
- "Return the source property association list based on the info in LOC,
- a location object."
- `((line . ,(and=> (location-line loc) 1-))
- (column . ,(location-column loc))
- (filename . ,(location-file loc))))
- (define (location->string loc)
- "Return a human-friendly, GNU-standard representation of LOC."
- (match loc
- (#f (G_ "<unknown location>"))
- (($ <location> file line column)
- (format #f "~a:~a:~a" file line column))))
- (define (location->hyperlink location)
- "Return a string corresponding to LOCATION, with escapes for a hyperlink."
- (let ((str (location->string location))
- (file (if (string-prefix? "/" (location-file location))
- (location-file location)
- (search-path %load-path (location-file location)))))
- (if file
- (file-hyperlink file str)
- str)))
- (define-condition-type &error-location &error
- error-location?
- (location error-location)) ;<location>
- (define-condition-type &fix-hint &condition
- fix-hint?
- (hint condition-fix-hint)) ;string
- (define-condition-type &formatted-message &error
- formatted-message?
- (format formatted-message-string)
- (arguments formatted-message-arguments))
- (define (check-format-string location format args)
- "Check that FORMAT, a format string, contains valid escapes, and that the
- number of arguments in ARGS matches the escapes in FORMAT."
- (define actual-count
- (length args))
- (define allowed-chars ;for 'simple-format'
- '(#\A #\S #\a #\s #\~ #\%))
- (define (format-chars fmt)
- (let loop ((chars (string->list fmt))
- (result '()))
- (match chars
- (()
- (reverse result))
- ((#\~ opt rest ...)
- (loop rest (cons opt result)))
- ((chr rest ...)
- (and (memv chr allowed-chars)
- (loop rest result))))))
- (match (format-chars format)
- (#f
- ;; XXX: In this case it could be that FMT contains invalid escapes, or it
- ;; could be that it contains escapes beyond ALLOWED-CHARS, for (ice-9
- ;; format). Instead of implementing '-Wformat', do nothing.
- #f)
- (chars
- (let ((count (fold (lambda (chr count)
- (case chr
- ((#\~ #\%) count)
- (else (+ count 1))))
- 0
- chars)))
- (unless (= count actual-count)
- (warning location (G_ "format string got ~a arguments, expected ~a~%")
- actual-count count))))))
- (define-syntax formatted-message
- (lambda (s)
- "Return a '&formatted-message' error condition."
- (syntax-case s (G_)
- ((_ (G_ str) args ...)
- (string? (syntax->datum #'str))
- (let ((str (syntax->datum #'str)))
- ;; Implement a subset of '-Wformat'.
- (check-format-string (source-properties->location
- (syntax-source s))
- str #'(args ...))
- (with-syntax ((str (string-append str "\n")))
- #'(condition
- (&formatted-message (format str)
- (arguments (list args ...))))))))))
- (define (absolute-location loc)
- "Replace the file name in LOC by an absolute location."
- (location (if (string-prefix? "/" (location-file loc))
- (location-file loc)
- ;; 'search-path' might return #f in obscure cases, such as
- ;; when %LOAD-PATH includes "." or ".." and LOC comes from a
- ;; file in a subdirectory thereof.
- (match (search-path %load-path (location-file loc))
- (#f
- (raise (formatted-message
- (G_ "file '~a' not found on load path")
- (location-file loc))))
- (str str)))
- (location-line loc)
- (location-column loc)))
- (define guix-warning-port
- (make-parameter (current-warning-port)))
- (define program-name
- ;; Name of the command-line program currently executing, or #f.
- (make-parameter #f))
- (define-syntax define-with-syntax-properties
- (lambda (x)
- "Define BINDING to be a syntax form replacing each VALUE-IDENTIFIER and
- SYNTAX-PROPERTIES-IDENTIFIER in body by the syntax and syntax-properties,
- respectively, of each ensuing syntax object."
- (syntax-case x ()
- ((_ (binding (value-identifier syntax-properties-identifier)
- ...)
- body ...)
- (and (and-map identifier? #'(value-identifier ...))
- (and-map identifier? #'(syntax-properties-identifier ...)))
- #'(define-syntax binding
- (lambda (y)
- (with-ellipsis :::
- (syntax-case y ()
- ((_ value-identifier ...)
- (with-syntax ((syntax-properties-identifier
- #`'#,(datum->syntax y
- (syntax-source
- #'value-identifier)))
- ...)
- #'(begin body ...)))
- (_
- (syntax-violation #f (format #f
- "Expected (~a~{ ~a~})"
- 'binding
- '(value-identifier ...))
- y)))))))
- (_
- (syntax-violation #f "Expected a definition of the form \
- (define-with-syntax-properties (binding (value syntax-properties) \
- ...) body ...)" x)))))
|