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