gnu: perl-markdown-discount: Use a shorter patch file name.
[jackhill/guix/guix.git] / guix / scripts / lint.scm
CommitLineData
b4f5e0e8
CR
1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2014 Cyril Roelandt <tipecaml@gmail.com>
50f5c46d 3;;; Copyright © 2014, 2015 Eric Bavier <bavier@member.fsf.org>
ac080e29 4;;; Copyright © 2013, 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
90ca9186 5;;; Copyright © 2015, 2016 Mathieu Lirzin <mthl@gnu.org>
013c3fb8 6;;; Copyright © 2016 Danny Milosavljevic <dannym+a@scratchpost.org>
891a843d 7;;; Copyright © 2016 Hartmut Goebel <h.goebel@crazy-compilers.com>
689db38e 8;;; Copyright © 2017 Alex Kost <alezost@gmail.com>
57d28987 9;;; Copyright © 2017 Tobias Geerinckx-Rice <me@tobias.gr>
b4f5e0e8
CR
10;;;
11;;; This file is part of GNU Guix.
12;;;
13;;; GNU Guix is free software; you can redistribute it and/or modify it
14;;; under the terms of the GNU General Public License as published by
15;;; the Free Software Foundation; either version 3 of the License, or (at
16;;; your option) any later version.
17;;;
18;;; GNU Guix is distributed in the hope that it will be useful, but
19;;; WITHOUT ANY WARRANTY; without even the implied warranty of
20;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21;;; GNU General Public License for more details.
22;;;
23;;; You should have received a copy of the GNU General Public License
24;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
25
26(define-module (guix scripts lint)
14d6ca3e 27 #:use-module ((guix store) #:hide (close-connection))
b4f5e0e8 28 #:use-module (guix base32)
17a7b75c 29 #:use-module (guix download)
c9815b5d 30 #:use-module (guix ftp-client)
4ce783a2 31 #:use-module (guix http-client)
b4f5e0e8 32 #:use-module (guix packages)
52b9efe3 33 #:use-module (guix licenses)
b4f5e0e8
CR
34 #:use-module (guix records)
35 #:use-module (guix ui)
adf0c531 36 #:use-module (guix upstream)
b4f5e0e8 37 #:use-module (guix utils)
f9704f17 38 #:use-module (guix memoization)
88981dd3 39 #:use-module (guix scripts)
105c260f 40 #:use-module (guix gnu-maintenance)
2b5115f8 41 #:use-module (guix monads)
5432734b 42 #:use-module (guix cve)
b4f5e0e8
CR
43 #:use-module (gnu packages)
44 #:use-module (ice-9 match)
574e847b
EB
45 #:use-module (ice-9 regex)
46 #:use-module (ice-9 format)
a3bf0969
LC
47 #:use-module (web uri)
48 #:use-module ((guix build download)
17a7b75c 49 #:select (maybe-expand-mirrors
4fd06a4d
LC
50 (open-connection-for-uri
51 . guix:open-connection-for-uri)
14d6ca3e 52 close-connection))
a3bf0969
LC
53 #:use-module (web request)
54 #:use-module (web response)
b4f5e0e8 55 #:use-module (srfi srfi-1)
2b5115f8 56 #:use-module (srfi srfi-6) ;Unicode string ports
b4f5e0e8
CR
57 #:use-module (srfi srfi-9)
58 #:use-module (srfi srfi-11)
17a7b75c 59 #:use-module (srfi srfi-26)
b210b35d
LC
60 #:use-module (srfi srfi-34)
61 #:use-module (srfi srfi-35)
b4f5e0e8 62 #:use-module (srfi srfi-37)
40a7d4e5 63 #:use-module (ice-9 rdelim)
b4f5e0e8 64 #:export (guix-lint
8202a513 65 check-description-style
b4f5e0e8 66 check-inputs-should-be-native
891a843d 67 check-inputs-should-not-be-an-input-at-all
56b1b74c 68 check-patch-file-names
907c98ac 69 check-synopsis-style
002c57c6 70 check-derivation
950d2ea4 71 check-home-page
40a7d4e5 72 check-source
50f5c46d 73 check-source-file-name
fac46e3f 74 check-mirror-url
52b9efe3 75 check-license
5432734b 76 check-vulnerabilities
adf0c531 77 check-for-updates
521a11e0 78 check-formatting
e04741f1 79 run-checkers
521a11e0
AK
80
81 %checkers
82 lint-checker
83 lint-checker?
84 lint-checker-name
85 lint-checker-description
86 lint-checker-check))
b4f5e0e8
CR
87
88\f
b4f5e0e8
CR
89;;;
90;;; Helpers
91;;;
92(define* (emit-warning package message #:optional field)
93 ;; Emit a warning about PACKAGE, printing the location of FIELD if it is
94 ;; given, the location of PACKAGE otherwise, the full name of PACKAGE and the
95 ;; provided MESSAGE.
96 (let ((loc (or (package-field-location package field)
97 (package-location package))))
f1a892c9 98 (format (guix-warning-port) "~a: ~a@~a: ~a~%"
b002e9d0 99 (location->string loc)
f1a892c9 100 (package-name package) (package-version package)
b002e9d0 101 message)))
b4f5e0e8 102
2b5115f8
LC
103(define (call-with-accumulated-warnings thunk)
104 "Call THUNK, accumulating any warnings in the current state, using the state
105monad."
106 (let ((port (open-output-string)))
107 (mlet %state-monad ((state (current-state))
108 (result -> (parameterize ((guix-warning-port port))
109 (thunk)))
110 (warning -> (get-output-string port)))
111 (mbegin %state-monad
112 (munless (string=? "" warning)
113 (set-current-state (cons warning state)))
114 (return result)))))
115
116(define-syntax-rule (with-accumulated-warnings exp ...)
117 "Evaluate EXP and accumulate warnings in the state monad."
118 (call-with-accumulated-warnings
119 (lambda ()
120 exp ...)))
121
b4f5e0e8
CR
122\f
123;;;
124;;; Checkers
125;;;
126(define-record-type* <lint-checker>
127 lint-checker make-lint-checker
128 lint-checker?
129 ;; TODO: add a 'certainty' field that shows how confident we are in the
130 ;; checker. Then allow users to only run checkers that have a certain
131 ;; 'certainty' level.
132 (name lint-checker-name)
133 (description lint-checker-description)
134 (check lint-checker-check))
135
136(define (list-checkers-and-exit)
137 ;; Print information about all available checkers and exit.
69daee23 138 (format #t (G_ "Available checkers:~%"))
b4f5e0e8
CR
139 (for-each (lambda (checker)
140 (format #t "- ~a: ~a~%"
141 (lint-checker-name checker)
69daee23 142 (G_ (lint-checker-description checker))))
b4f5e0e8
CR
143 %checkers)
144 (exit 0))
145
903581f9 146(define (properly-starts-sentence? s)
23464bfd 147 (string-match "^[(\"'`[:upper:][:digit:]]" s))
8202a513 148
15a6d433
LC
149(define (starts-with-abbreviation? s)
150 "Return #t if S starts with what looks like an abbreviation or acronym."
151 (string-match "^[A-Z][A-Z0-9]+\\>" s))
152
4bb54cc4
LC
153(define %quoted-identifier-rx
154 ;; A quoted identifier, like 'this'.
155 (make-regexp "['`][[:graph:]]+'"))
156
8202a513
CR
157(define (check-description-style package)
158 ;; Emit a warning if stylistic issues are found in the description of PACKAGE.
334c43e3
EB
159 (define (check-not-empty description)
160 (when (string-null? description)
161 (emit-warning package
69daee23 162 (G_ "description should not be empty")
334c43e3
EB
163 'description)))
164
2748ee3b
ML
165 (define (check-texinfo-markup description)
166 "Check that DESCRIPTION can be parsed as a Texinfo fragment. If the
167markup is valid return a plain-text version of DESCRIPTION, otherwise #f."
cd8b7cfb
ML
168 (catch #t
169 (lambda () (texi->plain-text description))
170 (lambda (keys . args)
171 (emit-warning package
69daee23 172 (G_ "Texinfo markup in description is invalid")
cd8b7cfb
ML
173 'description)
174 #f)))
3500e659 175
83f18e06
EB
176 (define (check-trademarks description)
177 "Check that DESCRIPTION does not contain '™' or '®' characters. See
178http://www.gnu.org/prep/standards/html_node/Trademarks.html."
179 (match (string-index description (char-set #\™ #\®))
180 ((and (? number?) index)
181 (emit-warning package
69daee23 182 (format #f (G_ "description should not contain ~
83f18e06
EB
183trademark sign '~a' at ~d")
184 (string-ref description index) index)
185 'description))
186 (else #t)))
187
4bb54cc4
LC
188 (define (check-quotes description)
189 "Check whether DESCRIPTION contains single quotes and suggest @code."
190 (when (regexp-exec %quoted-identifier-rx description)
191 (emit-warning package
192
193 ;; TRANSLATORS: '@code' is Texinfo markup and must be kept
194 ;; as is.
69daee23 195 (G_ "use @code or similar ornament instead of quotes")
4bb54cc4
LC
196 'description)))
197
903581f9 198 (define (check-proper-start description)
3c42965b
EB
199 (unless (or (properly-starts-sentence? description)
200 (string-prefix-ci? (package-name package) description))
574e847b 201 (emit-warning package
69daee23 202 (G_ "description should start with an upper-case letter or digit")
574e847b
EB
203 'description)))
204
205 (define (check-end-of-sentence-space description)
206 "Check that an end-of-sentence period is followed by two spaces."
207 (let ((infractions
208 (reverse (fold-matches
209 "\\. [A-Z]" description '()
210 (lambda (m r)
211 ;; Filter out matches of common abbreviations.
212 (if (find (lambda (s)
213 (string-suffix-ci? s (match:prefix m)))
214 '("i.e" "e.g" "a.k.a" "resp"))
215 r (cons (match:start m) r)))))))
216 (unless (null? infractions)
217 (emit-warning package
69daee23 218 (format #f (G_ "sentences in description should be followed ~
836f02bf 219by two spaces; possible infraction~p at ~{~a~^, ~}")
574e847b
EB
220 (length infractions)
221 infractions)
222 'description))))
223
224 (let ((description (package-description package)))
20be23c3
LC
225 (if (string? description)
226 (begin
227 (check-not-empty description)
4bb54cc4 228 (check-quotes description)
83f18e06 229 (check-trademarks description)
20be23c3
LC
230 ;; Use raw description for this because Texinfo rendering
231 ;; automatically fixes end of sentence space.
232 (check-end-of-sentence-space description)
233 (and=> (check-texinfo-markup description)
234 check-proper-start))
235 (emit-warning package
69daee23 236 (format #f (G_ "invalid description: ~s") description)
20be23c3 237 'description))))
8202a513 238
1730c5b5
LC
239(define (package-input-intersection inputs-to-check input-names)
240 "Return the intersection between INPUTS-TO-CHECK, the list of input tuples
241of a package, and INPUT-NAMES, a list of package specifications such as
242\"glib:bin\"."
891a843d
HG
243 (match inputs-to-check
244 (((labels packages . outputs) ...)
1730c5b5
LC
245 (filter-map (lambda (package output)
246 (and (package? package)
247 (let ((input (string-append
248 (package-name package)
249 (if (> (length output) 0)
250 (string-append ":" (car output))
251 ""))))
252 (and (member input input-names)
253 input))))
254 packages outputs))))
891a843d 255
b4f5e0e8
CR
256(define (check-inputs-should-be-native package)
257 ;; Emit a warning if some inputs of PACKAGE are likely to belong to its
258 ;; native inputs.
1730c5b5 259 (let ((inputs (package-inputs package))
891a843d 260 (input-names
27c81fcb
LC
261 '("pkg-config"
262 "cmake"
8f9d70fc
DC
263 "extra-cmake-modules"
264 "glib:bin"
265 "intltool"
266 "itstool"
e442246a
HG
267 "qttools"
268 "python-coverage" "python2-coverage"
269 "python-cython" "python2-cython"
270 "python-docutils" "python2-docutils"
271 "python-mock" "python2-mock"
272 "python-nose" "python2-nose"
273 "python-pbr" "python2-pbr"
274 "python-pytest" "python2-pytest"
275 "python-pytest-cov" "python2-pytest-cov"
276 "python-setuptools-scm" "python2-setuptools-scm"
277 "python-sphinx" "python2-sphinx")))
1730c5b5
LC
278 (for-each (lambda (input)
279 (emit-warning
280 package
69daee23 281 (format #f (G_ "'~a' should probably be a native input")
1730c5b5
LC
282 input)
283 'inputs-to-check))
284 (package-input-intersection inputs input-names))))
891a843d
HG
285
286(define (check-inputs-should-not-be-an-input-at-all package)
287 ;; Emit a warning if some inputs of PACKAGE are likely to should not be
288 ;; an input at all.
1730c5b5
LC
289 (let ((input-names '("python-setuptools"
290 "python2-setuptools"
291 "python-pip"
292 "python2-pip")))
293 (for-each (lambda (input)
294 (emit-warning
295 package
296 (format #f
69daee23 297 (G_ "'~a' should probably not be an input at all")
1730c5b5
LC
298 input)))
299 (package-input-intersection (package-direct-inputs package)
300 input-names))))
b4f5e0e8 301
17854ef9
LC
302(define (package-name-regexp package)
303 "Return a regexp that matches PACKAGE's name as a word at the beginning of a
304line."
305 (make-regexp (string-append "^" (regexp-quote (package-name package))
306 "\\>")
307 regexp/icase))
b4f5e0e8
CR
308
309(define (check-synopsis-style package)
310 ;; Emit a warning if stylistic issues are found in the synopsis of PACKAGE.
334c43e3
EB
311 (define (check-not-empty synopsis)
312 (when (string-null? synopsis)
313 (emit-warning package
69daee23 314 (G_ "synopsis should not be empty")
334c43e3
EB
315 'synopsis)))
316
b4f5e0e8
CR
317 (define (check-final-period synopsis)
318 ;; Synopsis should not end with a period, except for some special cases.
c04b82ff
EB
319 (when (and (string-suffix? "." synopsis)
320 (not (string-suffix? "etc." synopsis)))
321 (emit-warning package
69daee23 322 (G_ "no period allowed at the end of the synopsis")
c04b82ff 323 'synopsis)))
b4f5e0e8 324
105c260f
LC
325 (define check-start-article
326 ;; Skip this check for GNU packages, as suggested by Karl Berry's reply to
327 ;; <http://lists.gnu.org/archive/html/bug-womb/2014-11/msg00000.html>.
328 (if (false-if-exception (gnu-package? package))
329 (const #t)
330 (lambda (synopsis)
331 (when (or (string-prefix-ci? "A " synopsis)
332 (string-prefix-ci? "An " synopsis))
333 (emit-warning package
69daee23 334 (G_ "no article allowed at the beginning of \
836f02bf 335the synopsis")
105c260f 336 'synopsis)))))
b4f5e0e8 337
5622953d 338 (define (check-synopsis-length synopsis)
c04b82ff
EB
339 (when (>= (string-length synopsis) 80)
340 (emit-warning package
69daee23 341 (G_ "synopsis should be less than 80 characters long")
c04b82ff 342 'synopsis)))
5622953d 343
903581f9
EB
344 (define (check-proper-start synopsis)
345 (unless (properly-starts-sentence? synopsis)
346 (emit-warning package
69daee23 347 (G_ "synopsis should start with an upper-case letter or digit")
903581f9 348 'synopsis)))
8202a513 349
3c762a13 350 (define (check-start-with-package-name synopsis)
15a6d433
LC
351 (when (and (regexp-exec (package-name-regexp package) synopsis)
352 (not (starts-with-abbreviation? synopsis)))
86a41263 353 (emit-warning package
69daee23 354 (G_ "synopsis should not start with the package name")
86a41263 355 'synopsis)))
3c762a13 356
689db38e
AK
357 (define (check-texinfo-markup synopsis)
358 "Check that SYNOPSIS can be parsed as a Texinfo fragment. If the
359markup is valid return a plain-text version of SYNOPSIS, otherwise #f."
360 (catch #t
361 (lambda () (texi->plain-text synopsis))
362 (lambda (keys . args)
363 (emit-warning package
69daee23 364 (G_ "Texinfo markup in synopsis is invalid")
689db38e
AK
365 'synopsis)
366 #f)))
367
20be23c3 368 (define checks
689db38e
AK
369 (list check-not-empty
370 check-proper-start
371 check-final-period
372 check-start-article
373 check-start-with-package-name
374 check-synopsis-length
375 check-texinfo-markup))
20be23c3
LC
376
377 (match (package-synopsis package)
378 ((? string? synopsis)
379 (for-each (lambda (proc)
380 (proc synopsis))
381 checks))
382 (invalid
69daee23 383 (emit-warning package (format #f (G_ "invalid synopsis: ~s") invalid)
20be23c3 384 'synopsis))))
b4f5e0e8 385
bd7e1ffa 386(define* (probe-uri uri #:key timeout)
a3bf0969
LC
387 "Probe URI, a URI object, and return two values: a symbol denoting the
388probing status, such as 'http-response' when we managed to get an HTTP
bd7e1ffa
LC
389response from URI, and additional details, such as the actual HTTP response.
390
391TIMEOUT is the maximum number of seconds (possibly an inexact number) to wait
392for connections to complete; when TIMEOUT is #f, wait as long as needed."
a3bf0969
LC
393 (define headers
394 '((User-Agent . "GNU Guile")
395 (Accept . "*/*")))
396
397 (let loop ((uri uri)
398 (visited '()))
399 (match (uri-scheme uri)
400 ((or 'http 'https)
401 (catch #t
402 (lambda ()
4fd06a4d
LC
403 (let ((port (guix:open-connection-for-uri
404 uri #:timeout timeout))
a3bf0969
LC
405 (request (build-request uri #:headers headers)))
406 (define response
407 (dynamic-wind
408 (const #f)
409 (lambda ()
410 (write-request request port)
411 (force-output port)
412 (read-response port))
413 (lambda ()
14d6ca3e 414 (close-connection port))))
a3bf0969
LC
415
416 (case (response-code response)
61f28fe7 417 ((302 ; found (redirection)
57d28987
TGR
418 303 ; see other
419 307 ; temporary redirection
420 308) ; permanent redirection
a3bf0969
LC
421 (let ((location (response-location response)))
422 (if (or (not location) (member location visited))
423 (values 'http-response response)
424 (loop location (cons location visited))))) ;follow the redirect
61f28fe7
LC
425 ((301) ; moved permanently
426 (let ((location (response-location response)))
427 ;; Return RESPONSE, unless the final response as we follow
428 ;; redirects is not 200.
429 (if location
430 (let-values (((status response2)
431 (loop location (cons location visited))))
432 (case status
433 ((http-response)
434 (values 'http-response
435 (if (= 200 (response-code response2))
436 response
437 response2)))
438 (else
439 (values status response2))))
440 (values 'http-response response)))) ;invalid redirect
a3bf0969
LC
441 (else
442 (values 'http-response response)))))
443 (lambda (key . args)
444 (case key
445 ((bad-header bad-header-component)
446 ;; This can happen if the server returns an invalid HTTP header,
447 ;; as is the case with the 'Date' header at sqlite.org.
448 (values 'invalid-http-response #f))
bc3c41ce
LC
449 ((getaddrinfo-error system-error
450 gnutls-error tls-certificate-error)
a3bf0969
LC
451 (values key args))
452 (else
453 (apply throw key args))))))
c9815b5d
CR
454 ('ftp
455 (catch #t
456 (lambda ()
862d2479 457 (let ((conn (ftp-open (uri-host uri) #:timeout timeout)))
c9815b5d
CR
458 (define response
459 (dynamic-wind
460 (const #f)
461 (lambda ()
284fe313
LC
462 (ftp-chdir conn (dirname (uri-path uri)))
463 (ftp-size conn (basename (uri-path uri))))
c9815b5d 464 (lambda ()
284fe313 465 (ftp-close conn))))
661c99a4 466 (values 'ftp-response '(ok))))
c9815b5d
CR
467 (lambda (key . args)
468 (case key
661c99a4
LC
469 ((ftp-error)
470 (values 'ftp-response `(error ,@args)))
c9815b5d
CR
471 ((getaddrinfo-error system-error gnutls-error)
472 (values key args))
473 (else
474 (apply throw key args))))))
a3bf0969 475 (_
c9815b5d 476 (values 'unknown-protocol #f)))))
a3bf0969 477
c169d91e
LC
478(define (tls-certificate-error-string args)
479 "Return a string explaining the 'tls-certificate-error' arguments ARGS."
480 (call-with-output-string
481 (lambda (port)
482 (print-exception port #f
483 'tls-certificate-error args))))
484
17a7b75c 485(define (validate-uri uri package field)
91a0b9cc 486 "Return #t if the given URI can be reached, otherwise return #f and emit a
17a7b75c
CR
487warning for PACKAGE mentionning the FIELD."
488 (let-values (((status argument)
bd7e1ffa 489 (probe-uri uri #:timeout 3))) ;wait at most 3 seconds
17a7b75c
CR
490 (case status
491 ((http-response)
61f28fe7
LC
492 (cond ((= 200 (response-code argument))
493 (match (response-content-length argument)
494 ((? number? length)
495 ;; As of July 2016, SourceForge returns 200 (instead of 404)
496 ;; with a small HTML page upon failure. Attempt to detect
497 ;; such malicious behavior.
498 (or (> length 1000)
499 (begin
500 (emit-warning package
501 (format #f
502 (G_ "URI ~a returned \
503suspiciously small file (~a bytes)")
504 (uri->string uri)
505 length))
506 #f)))
507 (_ #t)))
508 ((= 301 (response-code argument))
509 (if (response-location argument)
bfcb3d76
LC
510 (begin
511 (emit-warning package
61f28fe7 512 (format #f (G_ "permanent redirect from ~a to ~a")
bfcb3d76 513 (uri->string uri)
61f28fe7
LC
514 (uri->string
515 (response-location argument))))
516 #t)
517 (begin
518 (emit-warning package
519 (format #f (G_ "invalid permanent redirect \
520from ~a")
521 (uri->string uri)))
bfcb3d76 522 #f)))
61f28fe7
LC
523 (else
524 (emit-warning package
525 (format #f
526 (G_ "URI ~a not reachable: ~a (~s)")
527 (uri->string uri)
528 (response-code argument)
529 (response-reason-phrase argument))
530 field)
531 #f)))
c9815b5d 532 ((ftp-response)
661c99a4
LC
533 (match argument
534 (('ok) #t)
535 (('error port command code message)
536 (emit-warning package
537 (format #f
69daee23 538 (G_ "URI ~a not reachable: ~a (~s)")
661c99a4 539 (uri->string uri)
91a0b9cc
LC
540 code (string-trim-both message)))
541 #f)))
17a7b75c
CR
542 ((getaddrinfo-error)
543 (emit-warning package
544 (format #f
69daee23 545 (G_ "URI ~a domain not found: ~a")
17a7b75c
CR
546 (uri->string uri)
547 (gai-strerror (car argument)))
06aac933
LC
548 field)
549 #f)
17a7b75c
CR
550 ((system-error)
551 (emit-warning package
552 (format #f
69daee23 553 (G_ "URI ~a unreachable: ~a")
17a7b75c
CR
554 (uri->string uri)
555 (strerror
556 (system-error-errno
557 (cons status argument))))
06aac933
LC
558 field)
559 #f)
bc3c41ce
LC
560 ((tls-certificate-error)
561 (emit-warning package
69daee23 562 (format #f (G_ "TLS certificate error: ~a")
c169d91e 563 (tls-certificate-error-string argument))))
17a7b75c
CR
564 ((invalid-http-response gnutls-error)
565 ;; Probably a misbehaving server; ignore.
566 #f)
61f28fe7 567 ((unknown-protocol) ;nothing we can do
17a7b75c
CR
568 #f)
569 (else
06aac933 570 (error "internal linter error" status)))))
17a7b75c 571
a3bf0969
LC
572(define (check-home-page package)
573 "Emit a warning if PACKAGE has an invalid 'home-page' field, or if that
574'home-page' is not reachable."
575 (let ((uri (and=> (package-home-page package) string->uri)))
576 (cond
577 ((uri? uri)
17a7b75c 578 (validate-uri uri package 'home-page))
a3bf0969
LC
579 ((not (package-home-page package))
580 (unless (or (string-contains (package-name package) "bootstrap")
581 (string=? (package-name package) "ld-wrapper"))
582 (emit-warning package
69daee23 583 (G_ "invalid value for home page")
a3bf0969
LC
584 'home-page)))
585 (else
69daee23 586 (emit-warning package (format #f (G_ "invalid home page URL: ~s")
a3bf0969
LC
587 (package-home-page package))
588 'home-page)))))
589
56b1b74c 590(define (check-patch-file-names package)
b210b35d
LC
591 "Emit a warning if the patches requires by PACKAGE are badly named or if the
592patch could not be found."
f3044a4b 593 (guard (c ((message-condition? c) ;raised by 'search-patch'
b210b35d
LC
594 (emit-warning package (condition-message c)
595 'patch-file-names)))
f3044a4b
ML
596 (unless (every (match-lambda ;patch starts with package name?
597 ((? string? patch)
598 (and=> (string-contains (basename patch)
599 (package-name package))
600 zero?))
601 (_ #f)) ;must be an <origin> or something like that.
602 (or (and=> (package-source package) origin-patches)
603 '()))
604 (emit-warning
605 package
69daee23 606 (G_ "file names of patches should start with the package name")
f3044a4b 607 'patch-file-names))))
b4f5e0e8 608
37627ffa
LC
609(define (escape-quotes str)
610 "Replace any quote character in STR by an escaped quote character."
611 (list->string
612 (string-fold-right (lambda (chr result)
613 (match chr
614 (#\" (cons* #\\ #\"result))
615 (_ (cons chr result))))
616 '()
617 str)))
618
619(define official-gnu-packages*
55b2d921
LC
620 (mlambda ()
621 "A memoizing version of 'official-gnu-packages' that returns the empty
37627ffa 622list when something goes wrong, such as a networking issue."
55b2d921
LC
623 (let ((gnus (false-if-exception (official-gnu-packages))))
624 (or gnus '()))))
37627ffa
LC
625
626(define (check-gnu-synopsis+description package)
627 "Make sure that, if PACKAGE is a GNU package, it uses the synopsis and
628descriptions maintained upstream."
629 (match (find (lambda (descriptor)
630 (string=? (gnu-package-name descriptor)
631 (package-name package)))
632 (official-gnu-packages*))
633 (#f ;not a GNU package, so nothing to do
634 #t)
20be23c3 635 (descriptor ;a genuine GNU package
37627ffa
LC
636 (let ((upstream (gnu-package-doc-summary descriptor))
637 (downstream (package-synopsis package))
638 (loc (or (package-field-location package 'synopsis)
639 (package-location package))))
20be23c3
LC
640 (when (and upstream
641 (or (not (string? downstream))
642 (not (string=? upstream downstream))))
37627ffa 643 (format (guix-warning-port)
69daee23 644 (G_ "~a: ~a: proposed synopsis: ~s~%")
37627ffa
LC
645 (location->string loc) (package-full-name package)
646 upstream)))
647
648 (let ((upstream (gnu-package-doc-description descriptor))
649 (downstream (package-description package))
650 (loc (or (package-field-location package 'description)
651 (package-location package))))
652 (when (and upstream
20be23c3
LC
653 (or (not (string? downstream))
654 (not (string=? (fill-paragraph upstream 100)
655 (fill-paragraph downstream 100)))))
37627ffa 656 (format (guix-warning-port)
69daee23 657 (G_ "~a: ~a: proposed description:~% \"~a\"~%")
37627ffa
LC
658 (location->string loc) (package-full-name package)
659 (fill-paragraph (escape-quotes upstream) 77 7)))))))
660
fac46e3f
LC
661(define (origin-uris origin)
662 "Return the list of URIs (strings) for ORIGIN."
663 (match (origin-uri origin)
664 ((? string? uri)
665 (list uri))
666 ((uris ...)
667 uris)))
668
17a7b75c
CR
669(define (check-source package)
670 "Emit a warning if PACKAGE has an invalid 'source' field, or if that
671'source' is not reachable."
2b5115f8
LC
672 (define (try-uris uris)
673 (run-with-state
674 (anym %state-monad
675 (lambda (uri)
676 (with-accumulated-warnings
677 (validate-uri uri package 'source)))
678 (append-map (cut maybe-expand-mirrors <> %mirrors)
679 uris))
680 '()))
681
17a7b75c
CR
682 (let ((origin (package-source package)))
683 (when (and origin
684 (eqv? (origin-method origin) url-fetch))
fac46e3f 685 (let ((uris (map string->uri (origin-uris origin))))
2b5115f8 686
06aac933 687 ;; Just make sure that at least one of the URIs is valid.
2b5115f8
LC
688 (call-with-values
689 (lambda () (try-uris uris))
690 (lambda (success? warnings)
691 ;; When everything fails, report all of WARNINGS, otherwise don't
692 ;; report anything.
693 ;;
694 ;; XXX: Ideally we'd still allow warnings to be raised if *some*
695 ;; URIs are unreachable, but distinguish that from the error case
696 ;; where *all* the URIs are unreachable.
697 (unless success?
698 (emit-warning package
69daee23 699 (G_ "all the source URIs are unreachable:")
2b5115f8
LC
700 'source)
701 (for-each (lambda (warning)
702 (display warning (guix-warning-port)))
703 (reverse warnings)))))))))
17a7b75c 704
50f5c46d
EB
705(define (check-source-file-name package)
706 "Emit a warning if PACKAGE's origin has no meaningful file name."
707 (define (origin-file-name-valid? origin)
ad25e996 708 ;; Return #f if the source file name contains only a version or is #f;
50f5c46d
EB
709 ;; indicates that the origin needs a 'file-name' field.
710 (let ((file-name (origin-actual-file-name origin))
711 (version (package-version package)))
712 (and file-name
5ae59eb6
AI
713 ;; Common in many projects is for the filename to start
714 ;; with a "v" followed by the version,
715 ;; e.g. "v3.2.0.tar.gz".
716 (not (string-match (string-append "^v?" version) file-name)))))
50f5c46d
EB
717
718 (let ((origin (package-source package)))
719 (unless (or (not origin) (origin-file-name-valid? origin))
720 (emit-warning package
69daee23 721 (G_ "the source file name should contain the package name")
50f5c46d
EB
722 'source))))
723
fac46e3f
LC
724(define (check-mirror-url package)
725 "Check whether PACKAGE uses source URLs that should be 'mirror://'."
726 (define (check-mirror-uri uri) ;XXX: could be optimized
727 (let loop ((mirrors %mirrors))
728 (match mirrors
729 (()
730 #t)
731 (((mirror-id mirror-urls ...) rest ...)
732 (match (find (cut string-prefix? <> uri) mirror-urls)
733 (#f
734 (loop rest))
735 (prefix
736 (emit-warning package
69daee23 737 (format #f (G_ "URL should be \
fac46e3f
LC
738'mirror://~a/~a'")
739 mirror-id
740 (string-drop uri (string-length prefix)))
741 'source)))))))
742
743 (let ((origin (package-source package)))
744 (when (and (origin? origin)
745 (eqv? (origin-method origin) url-fetch))
746 (let ((uris (origin-uris origin)))
747 (for-each check-mirror-uri uris)))))
748
002c57c6
LC
749(define (check-derivation package)
750 "Emit a warning if we fail to compile PACKAGE to a derivation."
751 (catch #t
752 (lambda ()
753 (guard (c ((nix-protocol-error? c)
754 (emit-warning package
69daee23 755 (format #f (G_ "failed to create derivation: ~a")
002c57c6
LC
756 (nix-protocol-error-message c))))
757 ((message-condition? c)
758 (emit-warning package
69daee23 759 (format #f (G_ "failed to create derivation: ~a")
002c57c6
LC
760 (condition-message c)))))
761 (with-store store
937690f9
LC
762 ;; Disable grafts since it can entail rebuilds.
763 (package-derivation store package #:graft? #f)
764
765 ;; If there's a replacement, make sure we can compute its
766 ;; derivation.
767 (match (package-replacement package)
768 (#f #t)
769 (replacement
770 (package-derivation store replacement #:graft? #f))))))
002c57c6
LC
771 (lambda args
772 (emit-warning package
69daee23 773 (format #f (G_ "failed to create derivation: ~s~%")
002c57c6
LC
774 args)))))
775
52b9efe3
LC
776(define (check-license package)
777 "Warn about type errors of the 'license' field of PACKAGE."
778 (match (package-license package)
779 ((or (? license?)
780 ((? license?) ...))
781 #t)
782 (x
69daee23 783 (emit-warning package (G_ "invalid license field")
52b9efe3
LC
784 'license))))
785
4e70fe4d
LC
786(define (patch-file-name patch)
787 "Return the basename of PATCH's file name, or #f if the file name could not
788be determined."
789 (match patch
790 ((? string?)
791 (basename patch))
792 ((? origin?)
793 (and=> (origin-actual-file-name patch) basename))))
794
4b879e0a
LC
795(define (call-with-networking-fail-safe message error-value proc)
796 "Call PROC catching any network-related errors. Upon a networking error,
797display a message including MESSAGE and return ERROR-VALUE."
4ce783a2 798 (guard (c ((http-get-error? c)
4b879e0a
LC
799 (warning (G_ "~a: HTTP GET error for ~a: ~a (~s)~%")
800 message
4ce783a2
LC
801 (uri->string (http-get-error-uri c))
802 (http-get-error-code c)
803 (http-get-error-reason c))
4b879e0a 804 error-value))
c169d91e 805 (catch #t
4b879e0a 806 proc
c169d91e
LC
807 (match-lambda*
808 (('getaddrinfo-error errcode)
4b879e0a
LC
809 (warning (G_ "~a: host lookup failure: ~a~%")
810 message
c169d91e 811 (gai-strerror errcode))
4b879e0a 812 error-value)
c169d91e 813 (('tls-certificate-error args ...)
4b879e0a
LC
814 (warning (G_ "~a: TLS certificate error: ~a")
815 message
c169d91e 816 (tls-certificate-error-string args))
4b879e0a 817 error-value)
c169d91e
LC
818 (args
819 (apply throw args))))))
4ce783a2 820
4b879e0a
LC
821(define-syntax-rule (with-networking-fail-safe message error-value exp ...)
822 (call-with-networking-fail-safe message error-value
823 (lambda () exp ...)))
824
825(define (current-vulnerabilities*)
826 "Like 'current-vulnerabilities', but return the empty list upon networking
827or HTTP errors. This allows network-less operation and makes problems with
828the NIST server non-fatal."
829 (with-networking-fail-safe (G_ "while retrieving CVE vulnerabilities")
830 '()
831 (current-vulnerabilities)))
832
5432734b
LC
833(define package-vulnerabilities
834 (let ((lookup (delay (vulnerabilities->lookup-proc
4ce783a2 835 (current-vulnerabilities*)))))
5432734b
LC
836 (lambda (package)
837 "Return a list of vulnerabilities affecting PACKAGE."
99effc8f
LC
838 ;; First we retrieve the Common Platform Enumeration (CPE) name and
839 ;; version for PACKAGE, then we can pass them to LOOKUP.
840 (let ((name (or (assoc-ref (package-properties package)
841 'cpe-name)
842 (package-name package)))
843 (version (or (assoc-ref (package-properties package)
844 'cpe-version)
845 (package-version package))))
846 ((force lookup) name version)))))
5432734b
LC
847
848(define (check-vulnerabilities package)
849 "Check for known vulnerabilities for PACKAGE."
9bee2bd1
LC
850 (let ((package (or (package-replacement package) package)))
851 (match (package-vulnerabilities package)
852 (()
853 #t)
854 ((vulnerabilities ...)
855 (let* ((patches (filter-map patch-file-name
856 (or (and=> (package-source package)
857 origin-patches)
858 '())))
859 (unpatched (remove (lambda (vuln)
860 (find (cute string-contains
861 <> (vulnerability-id vuln))
862 patches))
863 vulnerabilities)))
864 (unless (null? unpatched)
865 (emit-warning package
69daee23 866 (format #f (G_ "probably vulnerable to ~a")
9bee2bd1
LC
867 (string-join (map vulnerability-id unpatched)
868 ", ")))))))))
5432734b 869
adf0c531
EF
870(define (check-for-updates package)
871 "Check if there is an update available for PACKAGE."
6d6d1932
LC
872 (match (with-networking-fail-safe
873 (format #f (G_ "while retrieving upstream info for '~a'")
874 (package-name package))
875 #f
876 (package-latest-release* package (force %updaters)))
adf0c531
EF
877 ((? upstream-source? source)
878 (when (version>? (upstream-source-version source)
879 (package-version package))
880 (emit-warning package
735f6b63 881 (format #f (G_ "can be upgraded to ~a")
adf0c531
EF
882 (upstream-source-version source)))))
883 (#f #f))) ; cannot find newer upstream release
884
40a7d4e5
LC
885\f
886;;;
887;;; Source code formatting.
888;;;
889
890(define (report-tabulations package line line-number)
891 "Warn about tabulations found in LINE."
892 (match (string-index line #\tab)
893 (#f #t)
894 (index
895 (emit-warning package
69daee23 896 (format #f (G_ "tabulation on line ~a, column ~a")
40a7d4e5
LC
897 line-number index)))))
898
899(define (report-trailing-white-space package line line-number)
900 "Warn about trailing white space in LINE."
901 (unless (or (string=? line (string-trim-right line))
902 (string=? line (string #\page)))
903 (emit-warning package
904 (format #f
69daee23 905 (G_ "trailing white space on line ~a")
40a7d4e5
LC
906 line-number))))
907
908(define (report-long-line package line line-number)
909 "Emit a warning if LINE is too long."
910 ;; Note: We don't warn at 80 characters because sometimes hashes and URLs
911 ;; make it hard to fit within that limit and we want to avoid making too
912 ;; much noise.
913 (when (> (string-length line) 90)
914 (emit-warning package
69daee23 915 (format #f (G_ "line ~a is way too long (~a characters)")
40a7d4e5
LC
916 line-number (string-length line)))))
917
e0566f12
LC
918(define %hanging-paren-rx
919 (make-regexp "^[[:blank:]]*[()]+[[:blank:]]*$"))
920
921(define (report-lone-parentheses package line line-number)
922 "Emit a warning if LINE contains hanging parentheses."
923 (when (regexp-exec %hanging-paren-rx line)
924 (emit-warning package
925 (format #f
69daee23 926 (G_ "line ~a: parentheses feel lonely, \
e0566f12
LC
927move to the previous or next line")
928 line-number))))
929
40a7d4e5
LC
930(define %formatting-reporters
931 ;; List of procedures that report formatting issues. These are not separate
932 ;; checkers because they would need to re-read the file.
933 (list report-tabulations
934 report-trailing-white-space
e0566f12
LC
935 report-long-line
936 report-lone-parentheses))
40a7d4e5
LC
937
938(define* (report-formatting-issues package file starting-line
939 #:key (reporters %formatting-reporters))
940 "Report white-space issues in FILE starting from STARTING-LINE, and report
941them for PACKAGE."
9081a776
LC
942 (define (sexp-last-line port)
943 ;; Return the last line of the sexp read from PORT or an estimate thereof.
944 (define &failure (list 'failure))
945
946 (let ((start (ftell port))
947 (start-line (port-line port))
948 (sexp (catch 'read-error
949 (lambda () (read port))
950 (const &failure))))
951 (let ((line (port-line port)))
952 (seek port start SEEK_SET)
953 (set-port-line! port start-line)
954 (if (eq? sexp &failure)
955 (+ start-line 60) ;conservative estimate
956 line))))
40a7d4e5
LC
957
958 (call-with-input-file file
959 (lambda (port)
9081a776
LC
960 (let loop ((line-number 1)
961 (last-line #f))
40a7d4e5
LC
962 (let ((line (read-line port)))
963 (or (eof-object? line)
9081a776
LC
964 (and last-line (> line-number last-line))
965 (if (and (= line-number starting-line)
966 (not last-line))
967 (loop (+ 1 line-number)
968 (+ 1 (sexp-last-line port)))
969 (begin
970 (unless (< line-number starting-line)
971 (for-each (lambda (report)
972 (report package line line-number))
973 reporters))
974 (loop (+ 1 line-number) last-line)))))))))
40a7d4e5
LC
975
976(define (check-formatting package)
977 "Check the formatting of the source code of PACKAGE."
978 (let ((location (package-location package)))
979 (when location
980 (and=> (search-path %load-path (location-file location))
981 (lambda (file)
982 ;; Report issues starting from the line before the 'package'
983 ;; form, which usually contains the 'define' form.
984 (report-formatting-issues package file
985 (- (location-line location) 1)))))))
17a7b75c 986
37627ffa
LC
987\f
988;;;
989;;; List of checkers.
990;;;
991
b4f5e0e8
CR
992(define %checkers
993 (list
8202a513 994 (lint-checker
f4d5bca3 995 (name 'description)
8202a513
CR
996 (description "Validate package descriptions")
997 (check check-description-style))
37627ffa 998 (lint-checker
f4d5bca3 999 (name 'gnu-description)
37627ffa
LC
1000 (description "Validate synopsis & description of GNU packages")
1001 (check check-gnu-synopsis+description))
b4f5e0e8 1002 (lint-checker
f4d5bca3 1003 (name 'inputs-should-be-native)
b4f5e0e8
CR
1004 (description "Identify inputs that should be native inputs")
1005 (check check-inputs-should-be-native))
891a843d
HG
1006 (lint-checker
1007 (name 'inputs-should-not-be-input)
1008 (description "Identify inputs that should be inputs at all")
1009 (check check-inputs-should-not-be-an-input-at-all))
b4f5e0e8 1010 (lint-checker
56b1b74c 1011 (name 'patch-file-names)
b210b35d 1012 (description "Validate file names and availability of patches")
56b1b74c 1013 (check check-patch-file-names))
a3bf0969
LC
1014 (lint-checker
1015 (name 'home-page)
1016 (description "Validate home-page URLs")
1017 (check check-home-page))
17a7b75c 1018 (lint-checker
52b9efe3
LC
1019 (name 'license)
1020 ;; TRANSLATORS: <license> is the name of a data type and must not be
1021 ;; translated.
1022 (description "Make sure the 'license' field is a <license> \
1023or a list thereof")
1024 (check check-license))
1025 (lint-checker
17a7b75c
CR
1026 (name 'source)
1027 (description "Validate source URLs")
1028 (check check-source))
fac46e3f
LC
1029 (lint-checker
1030 (name 'mirror-url)
1031 (description "Suggest 'mirror://' URLs")
1032 (check check-mirror-url))
50f5c46d
EB
1033 (lint-checker
1034 (name 'source-file-name)
1035 (description "Validate file names of sources")
1036 (check check-source-file-name))
002c57c6
LC
1037 (lint-checker
1038 (name 'derivation)
1039 (description "Report failure to compile a package to a derivation")
1040 (check check-derivation))
b4f5e0e8 1041 (lint-checker
f4d5bca3 1042 (name 'synopsis)
8b9019a6 1043 (description "Validate package synopses")
40a7d4e5 1044 (check check-synopsis-style))
5432734b
LC
1045 (lint-checker
1046 (name 'cve)
1047 (description "Check the Common Vulnerabilities and Exposures\
1048 (CVE) database")
1049 (check check-vulnerabilities))
adf0c531
EF
1050 (lint-checker
1051 (name 'refresh)
1052 (description "Check the package for new upstream releases")
1053 (check check-for-updates))
40a7d4e5
LC
1054 (lint-checker
1055 (name 'formatting)
1056 (description "Look for formatting issues in the source")
1057 (check check-formatting))))
b4f5e0e8 1058
e04741f1
AK
1059(define* (run-checkers package #:optional (checkers %checkers))
1060 "Run the given CHECKERS on PACKAGE."
ac080e29 1061 (let ((tty? (isatty? (current-error-port))))
c79c6e59
LC
1062 (for-each (lambda (checker)
1063 (when tty?
ac080e29
LC
1064 (format (current-error-port) "checking ~a@~a [~a]...\x1b[K\r"
1065 (package-name package) (package-version package)
1066 (lint-checker-name checker))
c79c6e59
LC
1067 (force-output (current-error-port)))
1068 ((lint-checker-check checker) package))
013c3fb8
DM
1069 checkers)
1070 (when tty?
1071 (format (current-error-port) "\x1b[K")
1072 (force-output (current-error-port)))))
dd7c013d
CR
1073
1074\f
1075;;;
1076;;; Command-line options.
1077;;;
1078
1079(define %default-options
1080 ;; Alist of default option values.
1081 '())
1082
1083(define (show-help)
69daee23 1084 (display (G_ "Usage: guix lint [OPTION]... [PACKAGE]...
2a4e2e4b
AK
1085Run a set of checkers on the specified package; if none is specified,
1086run the checkers on all packages.\n"))
69daee23 1087 (display (G_ "
dd7c013d 1088 -c, --checkers=CHECKER1,CHECKER2...
69b4ffcf 1089 only run the specified checkers"))
69daee23 1090 (display (G_ "
dd7c013d 1091 -h, --help display this help and exit"))
69daee23 1092 (display (G_ "
dd7c013d 1093 -l, --list-checkers display the list of available lint checkers"))
69daee23 1094 (display (G_ "
dd7c013d
CR
1095 -V, --version display version information and exit"))
1096 (newline)
1097 (show-bug-report-information))
1098
1099
1100(define %options
1101 ;; Specification of the command-line options.
1102 ;; TODO: add some options:
1103 ;; * --certainty=[low,medium,high]: only run checkers that have at least this
1104 ;; 'certainty'.
1105 (list (option '(#\c "checkers") #t #f
8fbf5302 1106 (lambda (opt name arg result)
f4d5bca3 1107 (let ((names (map string->symbol (string-split arg #\,))))
dd7c013d 1108 (for-each (lambda (c)
f4d5bca3
LC
1109 (unless (memq c
1110 (map lint-checker-name
1111 %checkers))
69daee23 1112 (leave (G_ "~a: invalid checker~%") c)))
dd7c013d 1113 names)
8fbf5302
LC
1114 (alist-cons 'checkers
1115 (filter (lambda (checker)
1116 (member (lint-checker-name checker)
1117 names))
1118 %checkers)
1119 result))))
dd7c013d
CR
1120 (option '(#\h "help") #f #f
1121 (lambda args
1122 (show-help)
1123 (exit 0)))
1124 (option '(#\l "list-checkers") #f #f
1125 (lambda args
1126 (list-checkers-and-exit)))
1127 (option '(#\V "version") #f #f
1128 (lambda args
1129 (show-version-and-exit "guix lint")))))
b4f5e0e8
CR
1130
1131\f
1132;;;
1133;;; Entry Point
1134;;;
1135
1136(define (guix-lint . args)
1137 (define (parse-options)
1138 ;; Return the alist of option values.
a1ff7e1d
LC
1139 (parse-command-line args %options (list %default-options)
1140 #:build-options? #f))
b4f5e0e8
CR
1141
1142 (let* ((opts (parse-options))
1143 (args (filter-map (match-lambda
1144 (('argument . value)
1145 value)
1146 (_ #f))
dd7c013d
CR
1147 (reverse opts)))
1148 (checkers (or (assoc-ref opts 'checkers) %checkers)))
1149 (if (null? args)
1150 (fold-packages (lambda (p r) (run-checkers p checkers)) '())
1151 (for-each (lambda (spec)
1152 (run-checkers (specification->package spec) checkers))
1153 args))))