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