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