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