diagnostics.scm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
  3. ;;;
  4. ;;; This file is part of GNU Guix.
  5. ;;;
  6. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  7. ;;; under the terms of the GNU General Public License as published by
  8. ;;; the Free Software Foundation; either version 3 of the License, or (at
  9. ;;; your option) any later version.
  10. ;;;
  11. ;;; GNU Guix is distributed in the hope that it will be useful, but
  12. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;;; GNU General Public License for more details.
  15. ;;;
  16. ;;; You should have received a copy of the GNU General Public License
  17. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  18. (define-module (guix diagnostics)
  19. #:use-module (guix colors)
  20. #:use-module (guix i18n)
  21. #:use-module (srfi srfi-1)
  22. #:use-module (srfi srfi-9)
  23. #:use-module (srfi srfi-26)
  24. #:use-module (srfi srfi-35)
  25. #:use-module (ice-9 format)
  26. #:use-module (ice-9 match)
  27. #:export (warning
  28. info
  29. report-error
  30. leave
  31. <location>
  32. location
  33. location?
  34. location-file
  35. location-line
  36. location-column
  37. source-properties->location
  38. location->source-properties
  39. location->string
  40. &error-location
  41. error-location?
  42. error-location
  43. formatted-message
  44. formatted-message?
  45. formatted-message-string
  46. formatted-message-arguments
  47. &fix-hint
  48. fix-hint?
  49. condition-fix-hint
  50. guix-warning-port
  51. program-name))
  52. ;;; Commentary:
  53. ;;;
  54. ;;; This module provides the tools to report diagnostics to the user in a
  55. ;;; consistent way: errors, warnings, and notes.
  56. ;;;
  57. ;;; Code:
  58. (define (trivial-format-string? fmt)
  59. (define len
  60. (string-length fmt))
  61. (let loop ((start 0))
  62. (or (>= (+ 1 start) len)
  63. (let ((tilde (string-index fmt #\~ start)))
  64. (or (not tilde)
  65. (case (string-ref fmt (+ tilde 1))
  66. ((#\a #\A #\%) (loop (+ tilde 2)))
  67. (else #f)))))))
  68. (define-syntax highlight-argument
  69. (lambda (s)
  70. "Given FMT and ARG, expand ARG to a call that highlights it, provided FMT
  71. is a trivial format string."
  72. ;; Be conservative: limit format argument highlighting to cases where the
  73. ;; format string contains nothing but ~a escapes. If it contained ~s
  74. ;; escapes, this strategy wouldn't work.
  75. (syntax-case s ()
  76. ((_ "~a~%" arg) ;don't highlight whole messages
  77. #'arg)
  78. ((_ fmt arg)
  79. (trivial-format-string? (syntax->datum #'fmt))
  80. #'(%highlight-argument arg))
  81. ((_ fmt arg)
  82. #'arg))))
  83. (define* (%highlight-argument arg #:optional (port (guix-warning-port)))
  84. "Highlight ARG, a format string argument, if PORT supports colors."
  85. (cond ((string? arg)
  86. ;; If ARG contains white space, don't highlight it, on the grounds
  87. ;; that it may be a complete message in its own, like those produced
  88. ;; by 'guix lint.
  89. (if (string-any char-set:whitespace arg)
  90. arg
  91. (highlight arg port)))
  92. ((symbol? arg)
  93. (highlight (symbol->string arg) port))
  94. (else arg)))
  95. (define-syntax define-diagnostic
  96. (syntax-rules ()
  97. "Create a diagnostic macro (i.e., NAME), which will prepend PREFIX to all
  98. messages."
  99. ((_ name (G_ prefix) colors)
  100. (define-syntax name
  101. (lambda (x)
  102. (syntax-case x ()
  103. ((name location (underscore fmt) args (... ...))
  104. (and (string? (syntax->datum #'fmt))
  105. (free-identifier=? #'underscore #'G_))
  106. #'(begin
  107. (print-diagnostic-prefix prefix location
  108. #:colors colors)
  109. (format (guix-warning-port) (gettext fmt %gettext-domain)
  110. (highlight-argument fmt args) (... ...))))
  111. ((name location (N-underscore singular plural n)
  112. args (... ...))
  113. (and (string? (syntax->datum #'singular))
  114. (string? (syntax->datum #'plural))
  115. (free-identifier=? #'N-underscore #'N_))
  116. #'(begin
  117. (print-diagnostic-prefix prefix location
  118. #:colors colors)
  119. (format (guix-warning-port)
  120. (ngettext singular plural n %gettext-domain)
  121. (highlight-argument singular args) (... ...))))
  122. ((name (underscore fmt) args (... ...))
  123. (free-identifier=? #'underscore #'G_)
  124. #'(name #f (underscore fmt) args (... ...)))
  125. ((name (N-underscore singular plural n)
  126. args (... ...))
  127. (free-identifier=? #'N-underscore #'N_)
  128. #'(name #f (N-underscore singular plural n)
  129. args (... ...)))
  130. (id
  131. (identifier? #'id)
  132. ;; Run-time variant.
  133. #'(lambda (location fmt . args)
  134. (emit-diagnostic fmt args
  135. #:location location
  136. #:prefix prefix
  137. #:colors colors)))))))))
  138. ;; XXX: This doesn't work well for right-to-left languages.
  139. ;; TRANSLATORS: The goal is to emit "warning:" followed by a short phrase;
  140. ;; "~a" is a placeholder for that phrase.
  141. (define-diagnostic warning (G_ "warning: ") %warning-color) ;emit a warning
  142. (define-diagnostic info (G_ "") %info-color)
  143. (define-diagnostic report-error (G_ "error: ") %error-color)
  144. (define-syntax-rule (leave args ...)
  145. "Emit an error message and exit."
  146. (begin
  147. (report-error args ...)
  148. (exit 1)))
  149. (define* (emit-diagnostic fmt args
  150. #:key location (colors (color)) (prefix ""))
  151. "Report diagnostic message FMT with the given ARGS and the specified
  152. LOCATION, COLORS, and PREFIX.
  153. This procedure is used as a last resort when the format string is not known at
  154. macro-expansion time."
  155. (print-diagnostic-prefix (gettext prefix %gettext-domain)
  156. location #:colors colors)
  157. (apply format (guix-warning-port) fmt
  158. (if (trivial-format-string? fmt)
  159. (map %highlight-argument args)
  160. args)))
  161. (define %warning-color (color BOLD MAGENTA))
  162. (define %info-color (color BOLD))
  163. (define %error-color (color BOLD RED))
  164. (define* (print-diagnostic-prefix prefix #:optional location
  165. #:key (colors (color)))
  166. "Print PREFIX as a diagnostic line prefix."
  167. (define color?
  168. (color-output? (guix-warning-port)))
  169. (define location-color
  170. (if color?
  171. (cut colorize-string <> (color BOLD))
  172. identity))
  173. (define prefix-color
  174. (if color?
  175. (lambda (prefix)
  176. (colorize-string prefix colors))
  177. identity))
  178. (let ((prefix (if (string-null? prefix)
  179. prefix
  180. (gettext prefix %gettext-domain))))
  181. (if location
  182. (format (guix-warning-port) "~a: ~a"
  183. (location-color (location->string location))
  184. (prefix-color prefix))
  185. (format (guix-warning-port) "~:[~*~;guix ~a: ~]~a"
  186. (program-name) (program-name)
  187. (prefix-color prefix)))))
  188. ;; A source location.
  189. (define-record-type <location>
  190. (make-location file line column)
  191. location?
  192. (file location-file) ; file name
  193. (line location-line) ; 1-indexed line
  194. (column location-column)) ; 0-indexed column
  195. (define (location file line column)
  196. "Return the <location> object for the given FILE, LINE, and COLUMN."
  197. (and line column file
  198. (make-location file line column)))
  199. (define (source-properties->location loc)
  200. "Return a location object based on the info in LOC, an alist as returned
  201. by Guile's `source-properties', `frame-source', `current-source-location',
  202. etc."
  203. ;; In accordance with the GCS, start line and column numbers at 1. Note
  204. ;; that unlike LINE and `port-column', COL is actually 1-indexed here...
  205. (match loc
  206. ((('line . line) ('column . col) ('filename . file)) ;common case
  207. (and file line col
  208. (make-location file (+ line 1) col)))
  209. (#f
  210. #f)
  211. (_
  212. (let ((file (assq-ref loc 'filename))
  213. (line (assq-ref loc 'line))
  214. (col (assq-ref loc 'column)))
  215. (location file (and line (+ line 1)) col)))))
  216. (define (location->source-properties loc)
  217. "Return the source property association list based on the info in LOC,
  218. a location object."
  219. `((line . ,(and=> (location-line loc) 1-))
  220. (column . ,(location-column loc))
  221. (filename . ,(location-file loc))))
  222. (define (location->string loc)
  223. "Return a human-friendly, GNU-standard representation of LOC."
  224. (match loc
  225. (#f (G_ "<unknown location>"))
  226. (($ <location> file line column)
  227. (format #f "~a:~a:~a" file line column))))
  228. (define-condition-type &error-location &error
  229. error-location?
  230. (location error-location)) ;<location>
  231. (define-condition-type &fix-hint &condition
  232. fix-hint?
  233. (hint condition-fix-hint)) ;string
  234. (define-condition-type &formatted-message &error
  235. formatted-message?
  236. (format formatted-message-string)
  237. (arguments formatted-message-arguments))
  238. (define (check-format-string location format args)
  239. "Check that FORMAT, a format string, contains valid escapes, and that the
  240. number of arguments in ARGS matches the escapes in FORMAT."
  241. (define actual-count
  242. (length args))
  243. (define allowed-chars ;for 'simple-format'
  244. '(#\A #\S #\a #\s #\~ #\%))
  245. (define (format-chars fmt)
  246. (let loop ((chars (string->list fmt))
  247. (result '()))
  248. (match chars
  249. (()
  250. (reverse result))
  251. ((#\~ opt rest ...)
  252. (loop rest (cons opt result)))
  253. ((chr rest ...)
  254. (and (memv chr allowed-chars)
  255. (loop rest result))))))
  256. (match (format-chars format)
  257. (#f
  258. ;; XXX: In this case it could be that FMT contains invalid escapes, or it
  259. ;; could be that it contains escapes beyond ALLOWED-CHARS, for (ice-9
  260. ;; format). Instead of implementing '-Wformat', do nothing.
  261. #f)
  262. (chars
  263. (let ((count (fold (lambda (chr count)
  264. (case chr
  265. ((#\~ #\%) count)
  266. (else (+ count 1))))
  267. 0
  268. chars)))
  269. (unless (= count actual-count)
  270. (warning location (G_ "format string got ~a arguments, expected ~a~%")
  271. actual-count count))))))
  272. (define-syntax formatted-message
  273. (lambda (s)
  274. "Return a '&formatted-message' error condition."
  275. (syntax-case s (G_)
  276. ((_ (G_ str) args ...)
  277. (string? (syntax->datum #'str))
  278. (let ((str (syntax->datum #'str)))
  279. ;; Implement a subset of '-Wformat'.
  280. (check-format-string (source-properties->location
  281. (syntax-source s))
  282. str #'(args ...))
  283. (with-syntax ((str (string-append str "\n")))
  284. #'(condition
  285. (&formatted-message (format str)
  286. (arguments (list args ...))))))))))
  287. (define guix-warning-port
  288. (make-parameter (current-warning-port)))
  289. (define program-name
  290. ;; Name of the command-line program currently executing, or #f.
  291. (make-parameter #f))