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