summaryrefslogtreecommitdiff
path: root/guix/diagnostics.scm
blob: 3f1f527b4372c0d934eac631b64f6741489e68f4 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
;;; 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)))))