diagnostics.scm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 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. define-with-syntax-properties))
  53. ;;; Commentary:
  54. ;;;
  55. ;;; This module provides the tools to report diagnostics to the user in a
  56. ;;; consistent way: errors, warnings, and notes.
  57. ;;;
  58. ;;; Code:
  59. (define (trivial-format-string? fmt)
  60. (define len
  61. (string-length fmt))
  62. (let loop ((start 0))
  63. (or (>= (+ 1 start) len)
  64. (let ((tilde (string-index fmt #\~ start)))
  65. (or (not tilde)
  66. (case (string-ref fmt (+ tilde 1))
  67. ((#\a #\A #\%) (loop (+ tilde 2)))
  68. (else #f)))))))
  69. (define-syntax highlight-argument
  70. (lambda (s)
  71. "Given FMT and ARG, expand ARG to a call that highlights it, provided FMT
  72. is a trivial format string."
  73. ;; Be conservative: limit format argument highlighting to cases where the
  74. ;; format string contains nothing but ~a escapes. If it contained ~s
  75. ;; escapes, this strategy wouldn't work.
  76. (syntax-case s ()
  77. ((_ "~a~%" arg) ;don't highlight whole messages
  78. #'arg)
  79. ((_ fmt arg)
  80. (trivial-format-string? (syntax->datum #'fmt))
  81. #'(%highlight-argument arg))
  82. ((_ fmt arg)
  83. #'arg))))
  84. (define* (%highlight-argument arg #:optional (port (guix-warning-port)))
  85. "Highlight ARG, a format string argument, if PORT supports colors."
  86. (cond ((string? arg)
  87. ;; If ARG contains white space, don't highlight it, on the grounds
  88. ;; that it may be a complete message in its own, like those produced
  89. ;; by 'guix lint.
  90. (if (string-any char-set:whitespace arg)
  91. arg
  92. (highlight arg port)))
  93. ((symbol? arg)
  94. (highlight (symbol->string arg) port))
  95. (else arg)))
  96. (define-syntax define-diagnostic
  97. (syntax-rules ()
  98. "Create a diagnostic macro (i.e., NAME), which will prepend PREFIX to all
  99. messages."
  100. ((_ name (G_ prefix) colors)
  101. (define-syntax name
  102. (lambda (x)
  103. (syntax-case x ()
  104. ((name location (underscore fmt) args (... ...))
  105. (and (string? (syntax->datum #'fmt))
  106. (free-identifier=? #'underscore #'G_))
  107. #'(begin
  108. (print-diagnostic-prefix prefix location
  109. #:colors colors)
  110. (format (guix-warning-port) (gettext fmt %gettext-domain)
  111. (highlight-argument fmt args) (... ...))))
  112. ((name location (N-underscore singular plural n)
  113. args (... ...))
  114. (and (string? (syntax->datum #'singular))
  115. (string? (syntax->datum #'plural))
  116. (free-identifier=? #'N-underscore #'N_))
  117. #'(begin
  118. (print-diagnostic-prefix prefix location
  119. #:colors colors)
  120. (format (guix-warning-port)
  121. (ngettext singular plural n %gettext-domain)
  122. (highlight-argument singular args) (... ...))))
  123. ((name (underscore fmt) args (... ...))
  124. (free-identifier=? #'underscore #'G_)
  125. #'(name #f (underscore fmt) args (... ...)))
  126. ((name (N-underscore singular plural n)
  127. args (... ...))
  128. (free-identifier=? #'N-underscore #'N_)
  129. #'(name #f (N-underscore singular plural n)
  130. args (... ...)))
  131. (id
  132. (identifier? #'id)
  133. ;; Run-time variant.
  134. #'(lambda (location fmt . args)
  135. (emit-diagnostic fmt args
  136. #:location location
  137. #:prefix prefix
  138. #:colors colors)))))))))
  139. ;; XXX: This doesn't work well for right-to-left languages.
  140. ;; TRANSLATORS: The goal is to emit "warning:" followed by a short phrase;
  141. ;; "~a" is a placeholder for that phrase.
  142. (define-diagnostic warning (G_ "warning: ") %warning-color) ;emit a warning
  143. (define-diagnostic info (G_ "") %info-color)
  144. (define-diagnostic report-error (G_ "error: ") %error-color)
  145. (define-syntax-rule (leave args ...)
  146. "Emit an error message and exit."
  147. (begin
  148. (report-error args ...)
  149. (exit 1)))
  150. (define* (emit-diagnostic fmt args
  151. #:key location (colors (color)) (prefix ""))
  152. "Report diagnostic message FMT with the given ARGS and the specified
  153. LOCATION, COLORS, and PREFIX.
  154. This procedure is used as a last resort when the format string is not known at
  155. macro-expansion time."
  156. (print-diagnostic-prefix (gettext prefix %gettext-domain)
  157. location #:colors colors)
  158. (apply format (guix-warning-port) fmt
  159. (if (trivial-format-string? fmt)
  160. (map %highlight-argument args)
  161. args)))
  162. (define %warning-color (color BOLD MAGENTA))
  163. (define %info-color (color BOLD))
  164. (define %error-color (color BOLD RED))
  165. (define* (print-diagnostic-prefix prefix #:optional location
  166. #:key (colors (color)))
  167. "Print PREFIX as a diagnostic line prefix."
  168. (define color?
  169. (color-output? (guix-warning-port)))
  170. (define location-color
  171. (if color?
  172. (cut colorize-string <> (color BOLD))
  173. identity))
  174. (define prefix-color
  175. (if color?
  176. (lambda (prefix)
  177. (colorize-string prefix colors))
  178. identity))
  179. (let ((prefix (if (string-null? prefix)
  180. prefix
  181. (gettext prefix %gettext-domain))))
  182. (if location
  183. (format (guix-warning-port) "~a: ~a"
  184. (location-color (location->string location))
  185. (prefix-color prefix))
  186. (format (guix-warning-port) "~:[~*~;guix ~a: ~]~a"
  187. (program-name) (program-name)
  188. (prefix-color prefix)))))
  189. ;; A source location.
  190. (define-record-type <location>
  191. (make-location file line column)
  192. location?
  193. (file location-file) ; file name
  194. (line location-line) ; 1-indexed line
  195. (column location-column)) ; 0-indexed column
  196. (define (location file line column)
  197. "Return the <location> object for the given FILE, LINE, and COLUMN."
  198. (and line column file
  199. (make-location file line column)))
  200. (define (source-properties->location loc)
  201. "Return a location object based on the info in LOC, an alist as returned
  202. by Guile's `source-properties', `frame-source', `current-source-location',
  203. etc."
  204. ;; In accordance with the GCS, start line and column numbers at 1. Note
  205. ;; that unlike LINE and `port-column', COL is actually 1-indexed here...
  206. (match loc
  207. ((('line . line) ('column . col) ('filename . file)) ;common case
  208. (and file line col
  209. (make-location file (+ line 1) col)))
  210. (#f
  211. #f)
  212. (#(file line column)
  213. ;; Guile >= 3.0.6 uses vectors instead of alists internally, which can be
  214. ;; seen in the arguments to 'syntax-error' exceptions.
  215. (location file (+ 1 line) column))
  216. (_
  217. (let ((file (assq-ref loc 'filename))
  218. (line (assq-ref loc 'line))
  219. (col (assq-ref loc 'column)))
  220. (location file (and line (+ line 1)) col)))))
  221. (define (location->source-properties loc)
  222. "Return the source property association list based on the info in LOC,
  223. a location object."
  224. `((line . ,(and=> (location-line loc) 1-))
  225. (column . ,(location-column loc))
  226. (filename . ,(location-file loc))))
  227. (define (location->string loc)
  228. "Return a human-friendly, GNU-standard representation of LOC."
  229. (match loc
  230. (#f (G_ "<unknown location>"))
  231. (($ <location> file line column)
  232. (format #f "~a:~a:~a" file line column))))
  233. (define-condition-type &error-location &error
  234. error-location?
  235. (location error-location)) ;<location>
  236. (define-condition-type &fix-hint &condition
  237. fix-hint?
  238. (hint condition-fix-hint)) ;string
  239. (define-condition-type &formatted-message &error
  240. formatted-message?
  241. (format formatted-message-string)
  242. (arguments formatted-message-arguments))
  243. (define (check-format-string location format args)
  244. "Check that FORMAT, a format string, contains valid escapes, and that the
  245. number of arguments in ARGS matches the escapes in FORMAT."
  246. (define actual-count
  247. (length args))
  248. (define allowed-chars ;for 'simple-format'
  249. '(#\A #\S #\a #\s #\~ #\%))
  250. (define (format-chars fmt)
  251. (let loop ((chars (string->list fmt))
  252. (result '()))
  253. (match chars
  254. (()
  255. (reverse result))
  256. ((#\~ opt rest ...)
  257. (loop rest (cons opt result)))
  258. ((chr rest ...)
  259. (and (memv chr allowed-chars)
  260. (loop rest result))))))
  261. (match (format-chars format)
  262. (#f
  263. ;; XXX: In this case it could be that FMT contains invalid escapes, or it
  264. ;; could be that it contains escapes beyond ALLOWED-CHARS, for (ice-9
  265. ;; format). Instead of implementing '-Wformat', do nothing.
  266. #f)
  267. (chars
  268. (let ((count (fold (lambda (chr count)
  269. (case chr
  270. ((#\~ #\%) count)
  271. (else (+ count 1))))
  272. 0
  273. chars)))
  274. (unless (= count actual-count)
  275. (warning location (G_ "format string got ~a arguments, expected ~a~%")
  276. actual-count count))))))
  277. (define-syntax formatted-message
  278. (lambda (s)
  279. "Return a '&formatted-message' error condition."
  280. (syntax-case s (G_)
  281. ((_ (G_ str) args ...)
  282. (string? (syntax->datum #'str))
  283. (let ((str (syntax->datum #'str)))
  284. ;; Implement a subset of '-Wformat'.
  285. (check-format-string (source-properties->location
  286. (syntax-source s))
  287. str #'(args ...))
  288. (with-syntax ((str (string-append str "\n")))
  289. #'(condition
  290. (&formatted-message (format str)
  291. (arguments (list args ...))))))))))
  292. (define guix-warning-port
  293. (make-parameter (current-warning-port)))
  294. (define program-name
  295. ;; Name of the command-line program currently executing, or #f.
  296. (make-parameter #f))
  297. (define-syntax define-with-syntax-properties
  298. (lambda (x)
  299. "Define BINDING to be a syntax form replacing each VALUE-IDENTIFIER and
  300. SYNTAX-PROPERTIES-IDENTIFIER in body by the syntax and syntax-properties,
  301. respectively, of each ensuing syntax object."
  302. (syntax-case x ()
  303. ((_ (binding (value-identifier syntax-properties-identifier)
  304. ...)
  305. body ...)
  306. (and (and-map identifier? #'(value-identifier ...))
  307. (and-map identifier? #'(syntax-properties-identifier ...)))
  308. #'(define-syntax binding
  309. (lambda (y)
  310. (with-ellipsis :::
  311. (syntax-case y ()
  312. ((_ value-identifier ...)
  313. (with-syntax ((syntax-properties-identifier
  314. #`'#,(datum->syntax y
  315. (syntax-source
  316. #'value-identifier)))
  317. ...)
  318. #'(begin body ...)))
  319. (_
  320. (syntax-violation #f (format #f
  321. "Expected (~a~{ ~a~})"
  322. 'binding
  323. '(value-identifier ...))
  324. y)))))))
  325. (_
  326. (syntax-violation #f "Expected a definition of the form \
  327. (define-with-syntax-properties (binding (value syntax-properties) \
  328. ...) body ...)" x)))))