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