diagnostics.scm 14 KB

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