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