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>
9 ;;; This file is part of GNU Guix.
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.
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.
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
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)
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
69 check-source-file-name
80 lint-checker-description
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
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)
98 (define (call-with-accumulated-warnings thunk)
99 "Call THUNK, accumulating any warnings in the current state, using the state
101 (let ((port (open-output-string)))
102 (mlet %state-monad ((state (current-state))
103 (result -> (parameterize ((guix-warning-port port))
105 (warning -> (get-output-string port)))
107 (munless (string=? "" warning)
108 (set-current-state (cons warning state)))
111 (define-syntax-rule (with-accumulated-warnings exp ...)
112 "Evaluate EXP and accumulate warnings in the state monad."
113 (call-with-accumulated-warnings
121 (define-record-type* <lint-checker>
122 lint-checker make-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))))
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")
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."
164 (lambda () (texi->plain-text description))
165 (lambda (keys . args)
166 (emit-warning package
167 (_ "Texinfo markup in description is invalid")
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)
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
190 (_ "use @code or similar ornament instead of quotes")
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")
200 (define (check-end-of-sentence-space description)
201 "Check that an end-of-sentence period is followed by two spaces."
203 (reverse (fold-matches
204 "\\. [A-Z]" description '()
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~^, ~}")
219 (let ((description (package-description package)))
220 (if (string? description)
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)
230 (emit-warning package
231 (format #f (_ "invalid description: ~s") 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
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))
247 (when (member input input-names)
249 (format #f (_ message) input)
250 'inputs-to-check)))))
253 (define (check-inputs-should-be-native package)
254 ;; Emit a warning if some inputs of PACKAGE are likely to belong to its
256 (let ((message "'~a' should probably be a native input")
257 (inputs (package-inputs package))
260 "extra-cmake-modules"
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
280 (let ((message "'~a' should probably not be an input at all")
281 (inputs (package-inputs package))
283 '("python-setuptools"
287 (warn-if-package-has-input package (package-inputs package)
289 (warn-if-package-has-input package (package-native-inputs package)
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
297 (make-regexp (string-append "^" (regexp-quote (package-name package))
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")
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")
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))
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 \
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")
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")
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")
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)
360 (emit-warning package (format #f (_ "invalid synopsis: ~s") invalid)
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."
371 '((User-Agent . "GNU Guile")
376 (match (uri-scheme uri)
380 (let ((port (open-connection-for-uri uri #:timeout timeout))
381 (request (build-request uri #:headers headers)))
386 (write-request request port)
388 (read-response port))
390 (close-connection port))))
392 (case (response-code response)
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
399 (values 'http-response response)))))
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)
410 (apply throw key args))))))
414 (let ((conn (ftp-open (uri-host uri) #:timeout timeout)))
419 (ftp-chdir conn (dirname (uri-path uri)))
420 (ftp-size conn (basename (uri-path uri))))
423 (values 'ftp-response '(ok))))
427 (values 'ftp-response `(error ,@args)))
428 ((getaddrinfo-error system-error gnutls-error)
431 (apply throw key args))))))
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
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
449 (if (= 200 (response-code argument))
450 (match (response-content-length argument)
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.
457 (emit-warning package
459 (_ "URI ~a returned \
460 suspiciously small file (~a bytes)")
466 (emit-warning package
468 (_ "URI ~a not reachable: ~a (~s)")
470 (response-code argument)
471 (response-reason-phrase argument))
477 (('error port command code message)
478 (emit-warning package
480 (_ "URI ~a not reachable: ~a (~s)")
482 code (string-trim-both message)))
485 (emit-warning package
487 (_ "URI ~a domain not found: ~a")
489 (gai-strerror (car argument)))
493 (emit-warning package
495 (_ "URI ~a unreachable: ~a")
499 (cons status argument))))
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.
509 ((unknown-protocol) ;nothing we can do
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)))
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")
528 (emit-warning package (format #f (_ "invalid home page URL: ~s")
529 (package-home-page package))
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)
538 (unless (every (match-lambda ;patch starts with package name?
540 (and=> (string-contains (basename patch)
541 (package-name package))
543 (_ #f)) ;must be an <origin> or something like that.
544 (or (and=> (package-source package) origin-patches)
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."
554 (string-fold-right (lambda (chr result)
556 (#\" (cons* #\\ #\"result))
557 (_ (cons chr result))))
561 (define official-gnu-packages*
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))))
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
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))))
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)
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))))
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)
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)
618 (with-accumulated-warnings
619 (validate-uri uri package 'source)))
620 (append-map (cut maybe-expand-mirrors <> %mirrors)
624 (let ((origin (package-source package)))
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.
631 (lambda () (try-uris uris))
632 (lambda (success? warnings)
633 ;; When everything fails, report all of WARNINGS, otherwise don't
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.
640 (emit-warning package
641 (_ "all the source URIs are unreachable:")
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)))
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")
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))
674 (((mirror-id mirror-urls ...) rest ...)
675 (match (find (cut string-prefix? <> uri) mirror-urls)
679 (emit-warning package
680 (format #f (_ "URL should be \
683 (string-drop uri (string-length prefix)))
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."
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)))))
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
710 (match (package-replacement package)
713 (package-derivation store replacement #:graft? #f))))))
715 (emit-warning package
716 (format #f (_ "failed to create derivation: ~s~%")
719 (define (check-license package)
720 "Warn about type errors of the 'license' field of PACKAGE."
721 (match (package-license package)
726 (emit-warning package (_ "invalid license field")
729 (define (patch-file-name patch)
730 "Return the basename of PATCH's file name, or #f if the file name could not
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 \
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~%"))
752 (current-vulnerabilities))
754 (('getaddrinfo-error errcode)
755 (warning (_ "failed to lookup NIST host: ~a~%")
756 (gai-strerror errcode))
757 (warning (_ "assuming no CVE vulnerabilities~%"))
759 (('tls-certificate-error args ...)
760 (warning (_ "TLS certificate error: ~a")
761 (tls-certificate-error-string args))
762 (warning (_ "assuming no CVE vulnerabilities~%"))
765 (apply throw args))))))
767 (define package-vulnerabilities
768 (let ((lookup (delay (vulnerabilities->lookup-proc
769 (current-vulnerabilities*)))))
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)
776 (package-name package)))
777 (version (or (assoc-ref (package-properties package)
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)
788 ((vulnerabilities ...)
789 (let* ((patches (filter-map patch-file-name
790 (or (and=> (package-source package)
793 (unpatched (remove (lambda (vuln)
794 (find (cute string-contains
795 <> (vulnerability-id vuln))
798 (unless (null? unpatched)
799 (emit-warning package
800 (format #f (_ "probably vulnerable to ~a")
801 (string-join (map vulnerability-id unpatched)
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)
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
824 (_ "trailing white space on line ~a")
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
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
845 (_ "line ~a: parentheses feel lonely, \
846 move to the previous or next line")
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
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
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
869 (let loop ((line-number 1))
870 (let ((line (read-line port)))
871 (or (eof-object? line)
872 (> line-number last-line)
874 (unless (< line-number starting-line)
875 (for-each (lambda (report)
876 (report package line line-number))
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)))
884 (and=> (search-path %load-path (location-file location))
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.
900 (description "Validate package descriptions")
901 (check check-description-style))
903 (name 'gnu-description)
904 (description "Validate synopsis & description of GNU packages")
905 (check check-gnu-synopsis+description))
907 (name 'inputs-should-be-native)
908 (description "Identify inputs that should be native inputs")
909 (check check-inputs-should-be-native))
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))
915 (name 'patch-file-names)
916 (description "Validate file names and availability of patches")
917 (check check-patch-file-names))
920 (description "Validate home-page URLs")
921 (check check-home-page))
924 ;; TRANSLATORS: <license> is the name of a data type and must not be
926 (description "Make sure the 'license' field is a <license> \
928 (check check-license))
931 (description "Validate source URLs")
932 (check check-source))
935 (description "Suggest 'mirror://' URLs")
936 (check check-mirror-url))
938 (name 'source-file-name)
939 (description "Validate file names of sources")
940 (check check-source-file-name))
943 (description "Report failure to compile a package to a derivation")
944 (check check-derivation))
947 (description "Validate package synopses")
948 (check check-synopsis-style))
951 (description "Check the Common Vulnerabilities and Exposures\
953 (check check-vulnerabilities))
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)
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))
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.
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"))
988 -c, --checkers=CHECKER1,CHECKER2...
989 only run the specified checkers"))
991 -h, --help display this help and exit"))
993 -l, --list-checkers display the list of available lint checkers"))
995 -V, --version display version information and exit"))
997 (show-bug-report-information))
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
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)
1010 (map lint-checker-name
1012 (leave (_ "~a: invalid checker~%") c)))
1014 (alist-cons 'checkers
1015 (filter (lambda (checker)
1016 (member (lint-checker-name checker)
1020 (option '(#\h "help") #f #f
1024 (option '(#\l "list-checkers") #f #f
1026 (list-checkers-and-exit)))
1027 (option '(#\V "version") #f #f
1029 (show-version-and-exit "guix lint")))))
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))
1046 (let* ((opts (parse-options))
1047 (args (filter-map (match-lambda
1048 (('argument . value)
1052 (checkers (or (assoc-ref opts 'checkers) %checkers)))
1054 (fold-packages (lambda (p r) (run-checkers p checkers)) '())
1055 (for-each (lambda (spec)
1056 (run-checkers (specification->package spec) checkers))