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