bootloader: Fix comment about 'menu-entries'.
[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)
f363c836 46 #:use-module (guix cve)
f363c836
CB
47 #:use-module (ice-9 match)
48 #:use-module (ice-9 regex)
49 #:use-module (ice-9 format)
50 #:use-module (web client)
51 #:use-module (web uri)
52 #:use-module ((guix build download)
53 #:select (maybe-expand-mirrors
54 (open-connection-for-uri
55 . guix:open-connection-for-uri)
56 close-connection))
57 #:use-module (web request)
58 #:use-module (web response)
59 #:use-module (srfi srfi-1)
60 #:use-module (srfi srfi-6) ;Unicode string ports
61 #:use-module (srfi srfi-9)
62 #:use-module (srfi srfi-11)
63 #:use-module (srfi srfi-26)
64 #:use-module (srfi srfi-34)
65 #:use-module (srfi srfi-35)
66 #:use-module (ice-9 rdelim)
67 #:export (check-description-style
68 check-inputs-should-be-native
69 check-inputs-should-not-be-an-input-at-all
70 check-patch-file-names
71 check-synopsis-style
72 check-derivation
73 check-home-page
74 check-source
75 check-source-file-name
76 check-source-unstable-tarball
77 check-mirror-url
78 check-github-url
79 check-license
80 check-vulnerabilities
81 check-for-updates
82 check-formatting
83
84 lint-warning
85 lint-warning?
86 lint-warning-package
87 lint-warning-message
88 lint-warning-message-text
89 lint-warning-message-data
90 lint-warning-location
91
38f3176a
CB
92 %local-checkers
93 %network-dependent-checkers
94 %all-checkers
f363c836
CB
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)
674b9df3
LC
743 (let loop ((uris uris)
744 (warnings '()))
745 (match uris
746 (()
747 (reverse warnings))
748 ((uri rest ...)
749 (match (validate-uri uri package 'source)
750 (#t
751 ;; We found a working URL, so stop right away.
752 '())
753 ((? lint-warning? warning)
754 (loop rest (cons warning warnings))))))))
f363c836
CB
755
756 (let ((origin (package-source package)))
757 (if (and origin
758 (eqv? (origin-method origin) url-fetch))
848ae71e
LC
759 (let* ((uris (append-map (cut maybe-expand-mirrors <> %mirrors)
760 (map string->uri (origin-uris origin))))
f363c836
CB
761 (warnings (warnings-for-uris uris)))
762
763 ;; Just make sure that at least one of the URIs is valid.
848ae71e 764 (if (= (length uris) (length warnings))
f363c836
CB
765 ;; When everything fails, report all of WARNINGS, otherwise don't
766 ;; report anything.
767 ;;
768 ;; XXX: Ideally we'd still allow warnings to be raised if *some*
769 ;; URIs are unreachable, but distinguish that from the error case
770 ;; where *all* the URIs are unreachable.
771 (cons*
772 (make-warning package
773 (G_ "all the source URIs are unreachable:")
774 #:field 'source)
775 warnings)
776 '()))
777 '())))
778
779(define (check-source-file-name package)
780 "Emit a warning if PACKAGE's origin has no meaningful file name."
781 (define (origin-file-name-valid? origin)
782 ;; Return #f if the source file name contains only a version or is #f;
783 ;; indicates that the origin needs a 'file-name' field.
784 (let ((file-name (origin-actual-file-name origin))
785 (version (package-version package)))
786 (and file-name
787 ;; Common in many projects is for the filename to start
788 ;; with a "v" followed by the version,
789 ;; e.g. "v3.2.0.tar.gz".
790 (not (string-match (string-append "^v?" version) file-name)))))
791
792 (let ((origin (package-source package)))
793 (if (or (not origin) (origin-file-name-valid? origin))
794 '()
795 (list
796 (make-warning package
797 (G_ "the source file name should contain the package name")
798 #:field 'source)))))
799
800(define (check-source-unstable-tarball package)
801 "Emit a warning if PACKAGE's source is an autogenerated tarball."
802 (define (check-source-uri uri)
803 (if (and (string=? (uri-host (string->uri uri)) "github.com")
804 (match (split-and-decode-uri-path
805 (uri-path (string->uri uri)))
806 ((_ _ "archive" _ ...) #t)
807 (_ #f)))
808 (make-warning package
809 (G_ "the source URI should not be an autogenerated tarball")
810 #:field 'source)
811 #f))
812
813 (let ((origin (package-source package)))
814 (if (and (origin? origin)
815 (eqv? (origin-method origin) url-fetch))
816 (filter-map check-source-uri
817 (origin-uris origin))
818 '())))
819
820(define (check-mirror-url package)
821 "Check whether PACKAGE uses source URLs that should be 'mirror://'."
822 (define (check-mirror-uri uri) ;XXX: could be optimized
823 (let loop ((mirrors %mirrors))
824 (match mirrors
825 (()
826 #f)
827 (((mirror-id mirror-urls ...) rest ...)
828 (match (find (cut string-prefix? <> uri) mirror-urls)
829 (#f
830 (loop rest))
831 (prefix
832 (make-warning package
833 (G_ "URL should be \
834'mirror://~a/~a'")
835 (list mirror-id
836 (string-drop uri (string-length prefix)))
837 #:field 'source)))))))
838
839 (let ((origin (package-source package)))
840 (if (and (origin? origin)
841 (eqv? (origin-method origin) url-fetch))
842 (let ((uris (origin-uris origin)))
843 (filter-map check-mirror-uri uris))
844 '())))
845
846(define* (check-github-url package #:key (timeout 3))
847 "Check whether PACKAGE uses source URLs that redirect to GitHub."
848 (define (follow-redirect url)
849 (let* ((uri (string->uri url))
850 (port (guix:open-connection-for-uri uri #:timeout timeout))
851 (response (http-head uri #:port port)))
852 (close-port port)
853 (case (response-code response)
854 ((301 302)
855 (uri->string (assoc-ref (response-headers response) 'location)))
856 (else #f))))
857
858 (define (follow-redirects-to-github uri)
859 (cond
860 ((string-prefix? "https://github.com/" uri) uri)
861 ((string-prefix? "http" uri)
862 (and=> (follow-redirect uri) follow-redirects-to-github))
863 ;; Do not attempt to follow redirects on URIs other than http and https
864 ;; (such as mirror, file)
865 (else #f)))
866
867 (let ((origin (package-source package)))
868 (if (and (origin? origin)
869 (eqv? (origin-method origin) url-fetch))
870 (filter-map
871 (lambda (uri)
872 (and=> (follow-redirects-to-github uri)
873 (lambda (github-uri)
874 (if (string=? github-uri uri)
875 #f
876 (make-warning
877 package
878 (G_ "URL should be '~a'")
879 (list github-uri)
880 #:field 'source)))))
881 (origin-uris origin))
882 '())))
883
884(define (check-derivation package)
885 "Emit a warning if we fail to compile PACKAGE to a derivation."
886 (define (try system)
887 (catch #t
888 (lambda ()
889 (guard (c ((store-protocol-error? c)
890 (make-warning package
891 (G_ "failed to create ~a derivation: ~a")
892 (list system
893 (store-protocol-error-message c))))
894 ((message-condition? c)
895 (make-warning package
896 (G_ "failed to create ~a derivation: ~a")
897 (list system
898 (condition-message c)))))
899 (with-store store
900 ;; Disable grafts since it can entail rebuilds.
901 (parameterize ((%graft? #f))
902 (package-derivation store package system #:graft? #f)
903
904 ;; If there's a replacement, make sure we can compute its
905 ;; derivation.
906 (match (package-replacement package)
907 (#f #t)
908 (replacement
909 (package-derivation store replacement system
910 #:graft? #f)))))))
911 (lambda args
912 (make-warning package
913 (G_ "failed to create ~a derivation: ~s")
914 (list system args)))))
915
916 (filter lint-warning?
917 (map try (package-supported-systems package))))
918
919(define (check-license package)
920 "Warn about type errors of the 'license' field of PACKAGE."
921 (match (package-license package)
922 ((or (? license?)
923 ((? license?) ...))
924 '())
925 (x
926 (list
927 (make-warning package (G_ "invalid license field")
928 #:field 'license)))))
929
930(define (call-with-networking-fail-safe message error-value proc)
931 "Call PROC catching any network-related errors. Upon a networking error,
932display a message including MESSAGE and return ERROR-VALUE."
933 (guard (c ((http-get-error? c)
934 (warning (G_ "~a: HTTP GET error for ~a: ~a (~s)~%")
935 message
936 (uri->string (http-get-error-uri c))
937 (http-get-error-code c)
938 (http-get-error-reason c))
939 error-value))
940 (catch #t
941 proc
942 (match-lambda*
943 (('getaddrinfo-error errcode)
944 (warning (G_ "~a: host lookup failure: ~a~%")
945 message
946 (gai-strerror errcode))
947 error-value)
948 (('tls-certificate-error args ...)
949 (warning (G_ "~a: TLS certificate error: ~a")
950 message
951 (tls-certificate-error-string args))
952 error-value)
953 (args
954 (apply throw args))))))
955
956(define-syntax-rule (with-networking-fail-safe message error-value exp ...)
957 (call-with-networking-fail-safe message error-value
958 (lambda () exp ...)))
959
960(define (current-vulnerabilities*)
961 "Like 'current-vulnerabilities', but return the empty list upon networking
962or HTTP errors. This allows network-less operation and makes problems with
963the NIST server non-fatal."
964 (with-networking-fail-safe (G_ "while retrieving CVE vulnerabilities")
965 '()
966 (current-vulnerabilities)))
967
968(define package-vulnerabilities
969 (let ((lookup (delay (vulnerabilities->lookup-proc
970 (current-vulnerabilities*)))))
971 (lambda (package)
972 "Return a list of vulnerabilities affecting PACKAGE."
973 ;; First we retrieve the Common Platform Enumeration (CPE) name and
974 ;; version for PACKAGE, then we can pass them to LOOKUP.
975 (let ((name (or (assoc-ref (package-properties package)
976 'cpe-name)
977 (package-name package)))
978 (version (or (assoc-ref (package-properties package)
979 'cpe-version)
980 (package-version package))))
981 ((force lookup) name version)))))
982
983(define (check-vulnerabilities package)
984 "Check for known vulnerabilities for PACKAGE."
985 (let ((package (or (package-replacement package) package)))
986 (match (package-vulnerabilities package)
987 (()
988 '())
989 ((vulnerabilities ...)
990 (let* ((patched (package-patched-vulnerabilities package))
991 (known-safe (or (assq-ref (package-properties package)
992 'lint-hidden-cve)
993 '()))
994 (unpatched (remove (lambda (vuln)
995 (let ((id (vulnerability-id vuln)))
996 (or (member id patched)
997 (member id known-safe))))
998 vulnerabilities)))
999 (if (null? unpatched)
1000 '()
1001 (list
1002 (make-warning
1003 package
1004 (G_ "probably vulnerable to ~a")
1005 (list (string-join (map vulnerability-id unpatched)
1006 ", "))))))))))
1007
1008(define (check-for-updates package)
1009 "Check if there is an update available for PACKAGE."
1010 (match (with-networking-fail-safe
58d5f280
LC
1011 (format #f (G_ "while retrieving upstream info for '~a'")
1012 (package-name package))
f363c836
CB
1013 #f
1014 (package-latest-release* package (force %updaters)))
1015 ((? upstream-source? source)
1016 (if (version>? (upstream-source-version source)
1017 (package-version package))
1018 (list
1019 (make-warning package
1020 (G_ "can be upgraded to ~a")
1021 (list (upstream-source-version source))
1022 #:field 'version))
1023 '()))
1024 (#f '()))) ; cannot find newer upstream release
1025
1026\f
1027;;;
1028;;; Source code formatting.
1029;;;
1030
1031(define (report-tabulations package line line-number)
1032 "Warn about tabulations found in LINE."
1033 (match (string-index line #\tab)
1034 (#f #t)
1035 (index
1036 (make-warning package
1037 (G_ "tabulation on line ~a, column ~a")
1038 (list line-number index)
1039 #:location
1040 (location (package-file package)
1041 line-number
1042 index)))))
1043
1044(define (report-trailing-white-space package line line-number)
1045 "Warn about trailing white space in LINE."
1046 (unless (or (string=? line (string-trim-right line))
1047 (string=? line (string #\page)))
1048 (make-warning package
1049 (G_ "trailing white space on line ~a")
1050 (list line-number)
1051 #:location
1052 (location (package-file package)
1053 line-number
1054 0))))
1055
1056(define (report-long-line package line line-number)
1057 "Emit a warning if LINE is too long."
1058 ;; Note: We don't warn at 80 characters because sometimes hashes and URLs
1059 ;; make it hard to fit within that limit and we want to avoid making too
1060 ;; much noise.
1061 (when (> (string-length line) 90)
1062 (make-warning package
1063 (G_ "line ~a is way too long (~a characters)")
1064 (list line-number (string-length line))
1065 #:location
1066 (location (package-file package)
1067 line-number
1068 0))))
1069
1070(define %hanging-paren-rx
1071 (make-regexp "^[[:blank:]]*[()]+[[:blank:]]*$"))
1072
1073(define (report-lone-parentheses package line line-number)
1074 "Emit a warning if LINE contains hanging parentheses."
1075 (when (regexp-exec %hanging-paren-rx line)
1076 (make-warning package
1077 (G_ "parentheses feel lonely, \
1078move to the previous or next line")
1079 (list line-number)
1080 #:location
1081 (location (package-file package)
1082 line-number
1083 0))))
1084
1085(define %formatting-reporters
1086 ;; List of procedures that report formatting issues. These are not separate
1087 ;; checkers because they would need to re-read the file.
1088 (list report-tabulations
1089 report-trailing-white-space
1090 report-long-line
1091 report-lone-parentheses))
1092
1093(define* (report-formatting-issues package file starting-line
1094 #:key (reporters %formatting-reporters))
1095 "Report white-space issues in FILE starting from STARTING-LINE, and report
1096them for PACKAGE."
1097 (define (sexp-last-line port)
1098 ;; Return the last line of the sexp read from PORT or an estimate thereof.
1099 (define &failure (list 'failure))
1100
1101 (let ((start (ftell port))
1102 (start-line (port-line port))
1103 (sexp (catch 'read-error
1104 (lambda () (read port))
1105 (const &failure))))
1106 (let ((line (port-line port)))
1107 (seek port start SEEK_SET)
1108 (set-port-line! port start-line)
1109 (if (eq? sexp &failure)
1110 (+ start-line 60) ;conservative estimate
1111 line))))
1112
1113 (call-with-input-file file
1114 (lambda (port)
1115 (let loop ((line-number 1)
1116 (last-line #f)
1117 (warnings '()))
1118 (let ((line (read-line port)))
1119 (if (or (eof-object? line)
1120 (and last-line (> line-number last-line)))
1121 warnings
1122 (if (and (= line-number starting-line)
1123 (not last-line))
1124 (loop (+ 1 line-number)
1125 (+ 1 (sexp-last-line port))
1126 warnings)
1127 (loop (+ 1 line-number)
1128 last-line
1129 (append
1130 warnings
1131 (if (< line-number starting-line)
1132 '()
1133 (filter
1134 lint-warning?
1135 (map (lambda (report)
1136 (report package line line-number))
1137 reporters))))))))))))
1138
1139(define (check-formatting package)
1140 "Check the formatting of the source code of PACKAGE."
1141 (let ((location (package-location package)))
1142 (if location
1143 (and=> (search-path %load-path (location-file location))
1144 (lambda (file)
1145 ;; Report issues starting from the line before the 'package'
1146 ;; form, which usually contains the 'define' form.
1147 (report-formatting-issues package file
1148 (- (location-line location) 1))))
1149 '())))
1150
1151\f
1152;;;
1153;;; List of checkers.
1154;;;
1155
38f3176a 1156(define %local-checkers
f363c836
CB
1157 (list
1158 (lint-checker
1159 (name 'description)
1160 (description "Validate package descriptions")
1161 (check check-description-style))
f363c836
CB
1162 (lint-checker
1163 (name 'inputs-should-be-native)
1164 (description "Identify inputs that should be native inputs")
1165 (check check-inputs-should-be-native))
1166 (lint-checker
1167 (name 'inputs-should-not-be-input)
1168 (description "Identify inputs that shouldn't be inputs at all")
1169 (check check-inputs-should-not-be-an-input-at-all))
f363c836
CB
1170 (lint-checker
1171 (name 'license)
1172 ;; TRANSLATORS: <license> is the name of a data type and must not be
1173 ;; translated.
1174 (description "Make sure the 'license' field is a <license> \
1175or a list thereof")
1176 (check check-license))
f363c836
CB
1177 (lint-checker
1178 (name 'mirror-url)
1179 (description "Suggest 'mirror://' URLs")
1180 (check check-mirror-url))
f363c836
CB
1181 (lint-checker
1182 (name 'source-file-name)
1183 (description "Validate file names of sources")
1184 (check check-source-file-name))
1185 (lint-checker
1186 (name 'source-unstable-tarball)
1187 (description "Check for autogenerated tarballs")
1188 (check check-source-unstable-tarball))
1189 (lint-checker
1190 (name 'derivation)
1191 (description "Report failure to compile a package to a derivation")
1192 (check check-derivation))
38f3176a
CB
1193 (lint-checker
1194 (name 'patch-file-names)
1195 (description "Validate file names and availability of patches")
1196 (check check-patch-file-names))
1197 (lint-checker
1198 (name 'formatting)
1199 (description "Look for formatting issues in the source")
1200 (check check-formatting))))
1201
1202(define %network-dependent-checkers
1203 (list
f363c836
CB
1204 (lint-checker
1205 (name 'synopsis)
1206 (description "Validate package synopses")
1207 (check check-synopsis-style))
38f3176a
CB
1208 (lint-checker
1209 (name 'gnu-description)
1210 (description "Validate synopsis & description of GNU packages")
1211 (check check-gnu-synopsis+description))
1212 (lint-checker
1213 (name 'home-page)
1214 (description "Validate home-page URLs")
1215 (check check-home-page))
1216 (lint-checker
1217 (name 'source)
1218 (description "Validate source URLs")
1219 (check check-source))
1220 (lint-checker
1221 (name 'github-url)
1222 (description "Suggest GitHub URLs")
1223 (check check-github-url))
f363c836
CB
1224 (lint-checker
1225 (name 'cve)
1226 (description "Check the Common Vulnerabilities and Exposures\
1227 (CVE) database")
1228 (check check-vulnerabilities))
1229 (lint-checker
1230 (name 'refresh)
1231 (description "Check the package for new upstream releases")
38f3176a
CB
1232 (check check-for-updates))))
1233
1234(define %all-checkers
1235 (append %local-checkers
1236 %network-dependent-checkers))