diagnostics.scm 14 KB

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