lint: Report lonely parentheses.
[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>
06aac933 4;;; Copyright © 2013, 2014, 2015 Ludovic Courtès <ludo@gnu.org>
3500e659 5;;; Copyright © 2015 Mathieu Lirzin <mthl@openmailbox.org>
b4f5e0e8
CR
6;;;
7;;; This file is part of GNU Guix.
8;;;
9;;; GNU Guix is free software; you can redistribute it and/or modify it
10;;; under the terms of the GNU General Public License as published by
11;;; the Free Software Foundation; either version 3 of the License, or (at
12;;; your option) any later version.
13;;;
14;;; GNU Guix is distributed in the hope that it will be useful, but
15;;; WITHOUT ANY WARRANTY; without even the implied warranty of
16;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17;;; GNU General Public License for more details.
18;;;
19;;; You should have received a copy of the GNU General Public License
20;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
21
22(define-module (guix scripts lint)
002c57c6 23 #:use-module (guix store)
b4f5e0e8 24 #:use-module (guix base32)
17a7b75c 25 #:use-module (guix download)
c9815b5d 26 #:use-module (guix ftp-client)
b4f5e0e8 27 #:use-module (guix packages)
52b9efe3 28 #:use-module (guix licenses)
b4f5e0e8
CR
29 #:use-module (guix records)
30 #:use-module (guix ui)
31 #:use-module (guix utils)
88981dd3 32 #:use-module (guix scripts)
105c260f 33 #:use-module (guix gnu-maintenance)
2b5115f8 34 #:use-module (guix monads)
b4f5e0e8
CR
35 #:use-module (gnu packages)
36 #:use-module (ice-9 match)
574e847b
EB
37 #:use-module (ice-9 regex)
38 #:use-module (ice-9 format)
a3bf0969
LC
39 #:use-module (web uri)
40 #:use-module ((guix build download)
17a7b75c
CR
41 #:select (maybe-expand-mirrors
42 open-connection-for-uri))
a3bf0969
LC
43 #:use-module (web request)
44 #:use-module (web response)
b4f5e0e8 45 #:use-module (srfi srfi-1)
2b5115f8 46 #:use-module (srfi srfi-6) ;Unicode string ports
b4f5e0e8
CR
47 #:use-module (srfi srfi-9)
48 #:use-module (srfi srfi-11)
17a7b75c 49 #:use-module (srfi srfi-26)
b210b35d
LC
50 #:use-module (srfi srfi-34)
51 #:use-module (srfi srfi-35)
b4f5e0e8 52 #:use-module (srfi srfi-37)
40a7d4e5 53 #:use-module (ice-9 rdelim)
b4f5e0e8 54 #:export (guix-lint
8202a513 55 check-description-style
b4f5e0e8 56 check-inputs-should-be-native
56b1b74c 57 check-patch-file-names
907c98ac 58 check-synopsis-style
002c57c6 59 check-derivation
950d2ea4 60 check-home-page
40a7d4e5 61 check-source
50f5c46d 62 check-source-file-name
52b9efe3 63 check-license
521a11e0
AK
64 check-formatting
65
66 %checkers
67 lint-checker
68 lint-checker?
69 lint-checker-name
70 lint-checker-description
71 lint-checker-check))
b4f5e0e8
CR
72
73\f
b4f5e0e8
CR
74;;;
75;;; Helpers
76;;;
77(define* (emit-warning package message #:optional field)
78 ;; Emit a warning about PACKAGE, printing the location of FIELD if it is
79 ;; given, the location of PACKAGE otherwise, the full name of PACKAGE and the
80 ;; provided MESSAGE.
81 (let ((loc (or (package-field-location package field)
82 (package-location package))))
836f02bf 83 (format (guix-warning-port) "~a: ~a: ~a~%"
b002e9d0
LC
84 (location->string loc)
85 (package-full-name package)
86 message)))
b4f5e0e8 87
2b5115f8
LC
88(define (call-with-accumulated-warnings thunk)
89 "Call THUNK, accumulating any warnings in the current state, using the state
90monad."
91 (let ((port (open-output-string)))
92 (mlet %state-monad ((state (current-state))
93 (result -> (parameterize ((guix-warning-port port))
94 (thunk)))
95 (warning -> (get-output-string port)))
96 (mbegin %state-monad
97 (munless (string=? "" warning)
98 (set-current-state (cons warning state)))
99 (return result)))))
100
101(define-syntax-rule (with-accumulated-warnings exp ...)
102 "Evaluate EXP and accumulate warnings in the state monad."
103 (call-with-accumulated-warnings
104 (lambda ()
105 exp ...)))
106
b4f5e0e8
CR
107\f
108;;;
109;;; Checkers
110;;;
111(define-record-type* <lint-checker>
112 lint-checker make-lint-checker
113 lint-checker?
114 ;; TODO: add a 'certainty' field that shows how confident we are in the
115 ;; checker. Then allow users to only run checkers that have a certain
116 ;; 'certainty' level.
117 (name lint-checker-name)
118 (description lint-checker-description)
119 (check lint-checker-check))
120
121(define (list-checkers-and-exit)
122 ;; Print information about all available checkers and exit.
123 (format #t (_ "Available checkers:~%"))
124 (for-each (lambda (checker)
125 (format #t "- ~a: ~a~%"
126 (lint-checker-name checker)
db6dcf81 127 (_ (lint-checker-description checker))))
b4f5e0e8
CR
128 %checkers)
129 (exit 0))
130
903581f9 131(define (properly-starts-sentence? s)
431e5f5a 132 (string-match "^[(\"'[:upper:][:digit:]]" s))
8202a513 133
15a6d433
LC
134(define (starts-with-abbreviation? s)
135 "Return #t if S starts with what looks like an abbreviation or acronym."
136 (string-match "^[A-Z][A-Z0-9]+\\>" s))
137
8202a513
CR
138(define (check-description-style package)
139 ;; Emit a warning if stylistic issues are found in the description of PACKAGE.
334c43e3
EB
140 (define (check-not-empty description)
141 (when (string-null? description)
142 (emit-warning package
836f02bf 143 (_ "description should not be empty")
334c43e3
EB
144 'description)))
145
3500e659
ML
146 (define (check-texinfo-markup package)
147 "Check that PACKAGE description can be parsed as a Texinfo fragment."
148 (catch 'parser-error
149 (lambda () (package-description-string package))
150 (lambda (keys . args)
151 (emit-warning package (_ "Texinfo markup in description is invalid")))))
152
903581f9 153 (define (check-proper-start description)
3c42965b
EB
154 (unless (or (properly-starts-sentence? description)
155 (string-prefix-ci? (package-name package) description))
574e847b 156 (emit-warning package
836f02bf 157 (_ "description should start with an upper-case letter or digit")
574e847b
EB
158 'description)))
159
160 (define (check-end-of-sentence-space description)
161 "Check that an end-of-sentence period is followed by two spaces."
162 (let ((infractions
163 (reverse (fold-matches
164 "\\. [A-Z]" description '()
165 (lambda (m r)
166 ;; Filter out matches of common abbreviations.
167 (if (find (lambda (s)
168 (string-suffix-ci? s (match:prefix m)))
169 '("i.e" "e.g" "a.k.a" "resp"))
170 r (cons (match:start m) r)))))))
171 (unless (null? infractions)
172 (emit-warning package
836f02bf
LC
173 (format #f (_ "sentences in description should be followed ~
174by two spaces; possible infraction~p at ~{~a~^, ~}")
574e847b
EB
175 (length infractions)
176 infractions)
177 'description))))
178
179 (let ((description (package-description package)))
180 (when (string? description)
903581f9 181 (check-not-empty description)
3500e659 182 (check-texinfo-markup package)
903581f9
EB
183 (check-proper-start description)
184 (check-end-of-sentence-space description))))
8202a513 185
b4f5e0e8
CR
186(define (check-inputs-should-be-native package)
187 ;; Emit a warning if some inputs of PACKAGE are likely to belong to its
188 ;; native inputs.
189 (let ((inputs (package-inputs package)))
190 (match inputs
191 (((labels packages . _) ...)
192 (when (member "pkg-config"
193 (map package-name (filter package? packages)))
194 (emit-warning package
836f02bf 195 (_ "pkg-config should probably be a native input")
b4f5e0e8
CR
196 'inputs))))))
197
17854ef9
LC
198(define (package-name-regexp package)
199 "Return a regexp that matches PACKAGE's name as a word at the beginning of a
200line."
201 (make-regexp (string-append "^" (regexp-quote (package-name package))
202 "\\>")
203 regexp/icase))
b4f5e0e8
CR
204
205(define (check-synopsis-style package)
206 ;; Emit a warning if stylistic issues are found in the synopsis of PACKAGE.
334c43e3
EB
207 (define (check-not-empty synopsis)
208 (when (string-null? synopsis)
209 (emit-warning package
836f02bf 210 (_ "synopsis should not be empty")
334c43e3
EB
211 'synopsis)))
212
b4f5e0e8
CR
213 (define (check-final-period synopsis)
214 ;; Synopsis should not end with a period, except for some special cases.
c04b82ff
EB
215 (when (and (string-suffix? "." synopsis)
216 (not (string-suffix? "etc." synopsis)))
217 (emit-warning package
836f02bf 218 (_ "no period allowed at the end of the synopsis")
c04b82ff 219 'synopsis)))
b4f5e0e8 220
105c260f
LC
221 (define check-start-article
222 ;; Skip this check for GNU packages, as suggested by Karl Berry's reply to
223 ;; <http://lists.gnu.org/archive/html/bug-womb/2014-11/msg00000.html>.
224 (if (false-if-exception (gnu-package? package))
225 (const #t)
226 (lambda (synopsis)
227 (when (or (string-prefix-ci? "A " synopsis)
228 (string-prefix-ci? "An " synopsis))
229 (emit-warning package
836f02bf
LC
230 (_ "no article allowed at the beginning of \
231the synopsis")
105c260f 232 'synopsis)))))
b4f5e0e8 233
5622953d 234 (define (check-synopsis-length synopsis)
c04b82ff
EB
235 (when (>= (string-length synopsis) 80)
236 (emit-warning package
836f02bf 237 (_ "synopsis should be less than 80 characters long")
c04b82ff 238 'synopsis)))
5622953d 239
903581f9
EB
240 (define (check-proper-start synopsis)
241 (unless (properly-starts-sentence? synopsis)
242 (emit-warning package
836f02bf 243 (_ "synopsis should start with an upper-case letter or digit")
903581f9 244 'synopsis)))
8202a513 245
3c762a13 246 (define (check-start-with-package-name synopsis)
15a6d433
LC
247 (when (and (regexp-exec (package-name-regexp package) synopsis)
248 (not (starts-with-abbreviation? synopsis)))
86a41263 249 (emit-warning package
836f02bf 250 (_ "synopsis should not start with the package name")
86a41263 251 'synopsis)))
3c762a13 252
b4f5e0e8 253 (let ((synopsis (package-synopsis package)))
c04b82ff 254 (when (string? synopsis)
903581f9
EB
255 (check-not-empty synopsis)
256 (check-proper-start synopsis)
257 (check-final-period synopsis)
258 (check-start-article synopsis)
259 (check-start-with-package-name synopsis)
260 (check-synopsis-length synopsis))))
b4f5e0e8 261
a3bf0969
LC
262(define (probe-uri uri)
263 "Probe URI, a URI object, and return two values: a symbol denoting the
264probing status, such as 'http-response' when we managed to get an HTTP
265response from URI, and additional details, such as the actual HTTP response."
266 (define headers
267 '((User-Agent . "GNU Guile")
268 (Accept . "*/*")))
269
270 (let loop ((uri uri)
271 (visited '()))
272 (match (uri-scheme uri)
273 ((or 'http 'https)
274 (catch #t
275 (lambda ()
276 (let ((port (open-connection-for-uri uri))
277 (request (build-request uri #:headers headers)))
278 (define response
279 (dynamic-wind
280 (const #f)
281 (lambda ()
282 (write-request request port)
283 (force-output port)
284 (read-response port))
285 (lambda ()
286 (close port))))
287
288 (case (response-code response)
289 ((301 302 307)
290 (let ((location (response-location response)))
291 (if (or (not location) (member location visited))
292 (values 'http-response response)
293 (loop location (cons location visited))))) ;follow the redirect
294 (else
295 (values 'http-response response)))))
296 (lambda (key . args)
297 (case key
298 ((bad-header bad-header-component)
299 ;; This can happen if the server returns an invalid HTTP header,
300 ;; as is the case with the 'Date' header at sqlite.org.
301 (values 'invalid-http-response #f))
302 ((getaddrinfo-error system-error gnutls-error)
303 (values key args))
304 (else
305 (apply throw key args))))))
c9815b5d
CR
306 ('ftp
307 (catch #t
308 (lambda ()
284fe313 309 (let ((conn (ftp-open (uri-host uri) 21)))
c9815b5d
CR
310 (define response
311 (dynamic-wind
312 (const #f)
313 (lambda ()
284fe313
LC
314 (ftp-chdir conn (dirname (uri-path uri)))
315 (ftp-size conn (basename (uri-path uri))))
c9815b5d 316 (lambda ()
284fe313 317 (ftp-close conn))))
661c99a4 318 (values 'ftp-response '(ok))))
c9815b5d
CR
319 (lambda (key . args)
320 (case key
661c99a4
LC
321 ((ftp-error)
322 (values 'ftp-response `(error ,@args)))
c9815b5d
CR
323 ((getaddrinfo-error system-error gnutls-error)
324 (values key args))
325 (else
326 (apply throw key args))))))
a3bf0969 327 (_
c9815b5d 328 (values 'unknown-protocol #f)))))
a3bf0969 329
17a7b75c 330(define (validate-uri uri package field)
91a0b9cc 331 "Return #t if the given URI can be reached, otherwise return #f and emit a
17a7b75c
CR
332warning for PACKAGE mentionning the FIELD."
333 (let-values (((status argument)
334 (probe-uri uri)))
335 (case status
336 ((http-response)
06aac933 337 (or (= 200 (response-code argument))
91a0b9cc
LC
338 (begin
339 (emit-warning package
340 (format #f
341 (_ "URI ~a not reachable: ~a (~s)")
342 (uri->string uri)
343 (response-code argument)
344 (response-reason-phrase argument))
345 field)
346 #f)))
c9815b5d 347 ((ftp-response)
661c99a4
LC
348 (match argument
349 (('ok) #t)
350 (('error port command code message)
351 (emit-warning package
352 (format #f
353 (_ "URI ~a not reachable: ~a (~s)")
354 (uri->string uri)
91a0b9cc
LC
355 code (string-trim-both message)))
356 #f)))
17a7b75c
CR
357 ((getaddrinfo-error)
358 (emit-warning package
359 (format #f
360 (_ "URI ~a domain not found: ~a")
361 (uri->string uri)
362 (gai-strerror (car argument)))
06aac933
LC
363 field)
364 #f)
17a7b75c
CR
365 ((system-error)
366 (emit-warning package
367 (format #f
368 (_ "URI ~a unreachable: ~a")
369 (uri->string uri)
370 (strerror
371 (system-error-errno
372 (cons status argument))))
06aac933
LC
373 field)
374 #f)
17a7b75c
CR
375 ((invalid-http-response gnutls-error)
376 ;; Probably a misbehaving server; ignore.
377 #f)
c9815b5d 378 ((unknown-protocol) ;nothing we can do
17a7b75c
CR
379 #f)
380 (else
06aac933 381 (error "internal linter error" status)))))
17a7b75c 382
a3bf0969
LC
383(define (check-home-page package)
384 "Emit a warning if PACKAGE has an invalid 'home-page' field, or if that
385'home-page' is not reachable."
386 (let ((uri (and=> (package-home-page package) string->uri)))
387 (cond
388 ((uri? uri)
17a7b75c 389 (validate-uri uri package 'home-page))
a3bf0969
LC
390 ((not (package-home-page package))
391 (unless (or (string-contains (package-name package) "bootstrap")
392 (string=? (package-name package) "ld-wrapper"))
393 (emit-warning package
394 (_ "invalid value for home page")
395 'home-page)))
396 (else
397 (emit-warning package (format #f (_ "invalid home page URL: ~s")
398 (package-home-page package))
399 'home-page)))))
400
56b1b74c 401(define (check-patch-file-names package)
b210b35d
LC
402 "Emit a warning if the patches requires by PACKAGE are badly named or if the
403patch could not be found."
404 (guard (c ((message-condition? c) ;raised by 'search-patch'
405 (emit-warning package (condition-message c)
406 'patch-file-names)))
407 (let ((patches (and=> (package-source package) origin-patches))
408 (name (package-name package))
409 (full-name (package-full-name package)))
410 (when (and patches
411 (any (match-lambda
412 ((? string? patch)
413 (let ((file (basename patch)))
414 (not (or (eq? (string-contains file name) 0)
415 (eq? (string-contains file full-name)
416 0)))))
417 (_
418 ;; This must be an <origin> or something like that.
419 #f))
420 patches))
421 (emit-warning package
422 (_ "file names of patches should start with \
836f02bf 423the package name")
b210b35d 424 'patch-file-names)))))
b4f5e0e8 425
37627ffa
LC
426(define (escape-quotes str)
427 "Replace any quote character in STR by an escaped quote character."
428 (list->string
429 (string-fold-right (lambda (chr result)
430 (match chr
431 (#\" (cons* #\\ #\"result))
432 (_ (cons chr result))))
433 '()
434 str)))
435
436(define official-gnu-packages*
437 (memoize
438 (lambda ()
439 "A memoizing version of 'official-gnu-packages' that returns the empty
440list when something goes wrong, such as a networking issue."
441 (let ((gnus (false-if-exception (official-gnu-packages))))
442 (or gnus '())))))
443
444(define (check-gnu-synopsis+description package)
445 "Make sure that, if PACKAGE is a GNU package, it uses the synopsis and
446descriptions maintained upstream."
447 (match (find (lambda (descriptor)
448 (string=? (gnu-package-name descriptor)
449 (package-name package)))
450 (official-gnu-packages*))
451 (#f ;not a GNU package, so nothing to do
452 #t)
453 (descriptor ;a genuine GNU package
454 (let ((upstream (gnu-package-doc-summary descriptor))
455 (downstream (package-synopsis package))
456 (loc (or (package-field-location package 'synopsis)
457 (package-location package))))
458 (unless (and upstream (string=? upstream downstream))
459 (format (guix-warning-port)
836f02bf 460 (_ "~a: ~a: proposed synopsis: ~s~%")
37627ffa
LC
461 (location->string loc) (package-full-name package)
462 upstream)))
463
464 (let ((upstream (gnu-package-doc-description descriptor))
465 (downstream (package-description package))
466 (loc (or (package-field-location package 'description)
467 (package-location package))))
468 (when (and upstream
469 (not (string=? (fill-paragraph upstream 100)
470 (fill-paragraph downstream 100))))
471 (format (guix-warning-port)
836f02bf 472 (_ "~a: ~a: proposed description:~% \"~a\"~%")
37627ffa
LC
473 (location->string loc) (package-full-name package)
474 (fill-paragraph (escape-quotes upstream) 77 7)))))))
475
17a7b75c
CR
476(define (check-source package)
477 "Emit a warning if PACKAGE has an invalid 'source' field, or if that
478'source' is not reachable."
2b5115f8
LC
479 (define (try-uris uris)
480 (run-with-state
481 (anym %state-monad
482 (lambda (uri)
483 (with-accumulated-warnings
484 (validate-uri uri package 'source)))
485 (append-map (cut maybe-expand-mirrors <> %mirrors)
486 uris))
487 '()))
488
17a7b75c
CR
489 (let ((origin (package-source package)))
490 (when (and origin
491 (eqv? (origin-method origin) url-fetch))
492 (let* ((strings (origin-uri origin))
493 (uris (if (list? strings)
494 (map string->uri strings)
495 (list (string->uri strings)))))
2b5115f8 496
06aac933 497 ;; Just make sure that at least one of the URIs is valid.
2b5115f8
LC
498 (call-with-values
499 (lambda () (try-uris uris))
500 (lambda (success? warnings)
501 ;; When everything fails, report all of WARNINGS, otherwise don't
502 ;; report anything.
503 ;;
504 ;; XXX: Ideally we'd still allow warnings to be raised if *some*
505 ;; URIs are unreachable, but distinguish that from the error case
506 ;; where *all* the URIs are unreachable.
507 (unless success?
508 (emit-warning package
509 (_ "all the source URIs are unreachable:")
510 'source)
511 (for-each (lambda (warning)
512 (display warning (guix-warning-port)))
513 (reverse warnings)))))))))
17a7b75c 514
50f5c46d
EB
515(define (check-source-file-name package)
516 "Emit a warning if PACKAGE's origin has no meaningful file name."
517 (define (origin-file-name-valid? origin)
518 ;; Return #t if the source file name contains only a version or is #f;
519 ;; indicates that the origin needs a 'file-name' field.
520 (let ((file-name (origin-actual-file-name origin))
521 (version (package-version package)))
522 (and file-name
523 (not (or (string-prefix? version file-name)
524 ;; Common in many projects is for the filename to start
525 ;; with a "v" followed by the version,
526 ;; e.g. "v3.2.0.tar.gz".
527 (string-prefix? (string-append "v" version) file-name))))))
528
529 (let ((origin (package-source package)))
530 (unless (or (not origin) (origin-file-name-valid? origin))
531 (emit-warning package
532 (_ "the source file name should contain the package name")
533 'source))))
534
002c57c6
LC
535(define (check-derivation package)
536 "Emit a warning if we fail to compile PACKAGE to a derivation."
537 (catch #t
538 (lambda ()
539 (guard (c ((nix-protocol-error? c)
540 (emit-warning package
541 (format #f (_ "failed to create derivation: ~a")
542 (nix-protocol-error-message c))))
543 ((message-condition? c)
544 (emit-warning package
545 (format #f (_ "failed to create derivation: ~a")
546 (condition-message c)))))
547 (with-store store
548 (package-derivation store package))))
549 (lambda args
550 (emit-warning package
551 (format #f (_ "failed to create derivation: ~s~%")
552 args)))))
553
52b9efe3
LC
554(define (check-license package)
555 "Warn about type errors of the 'license' field of PACKAGE."
556 (match (package-license package)
557 ((or (? license?)
558 ((? license?) ...))
559 #t)
560 (x
561 (emit-warning package (_ "invalid license field")
562 'license))))
563
40a7d4e5
LC
564\f
565;;;
566;;; Source code formatting.
567;;;
568
569(define (report-tabulations package line line-number)
570 "Warn about tabulations found in LINE."
571 (match (string-index line #\tab)
572 (#f #t)
573 (index
574 (emit-warning package
575 (format #f (_ "tabulation on line ~a, column ~a")
576 line-number index)))))
577
578(define (report-trailing-white-space package line line-number)
579 "Warn about trailing white space in LINE."
580 (unless (or (string=? line (string-trim-right line))
581 (string=? line (string #\page)))
582 (emit-warning package
583 (format #f
584 (_ "trailing white space on line ~a")
585 line-number))))
586
587(define (report-long-line package line line-number)
588 "Emit a warning if LINE is too long."
589 ;; Note: We don't warn at 80 characters because sometimes hashes and URLs
590 ;; make it hard to fit within that limit and we want to avoid making too
591 ;; much noise.
592 (when (> (string-length line) 90)
593 (emit-warning package
594 (format #f (_ "line ~a is way too long (~a characters)")
595 line-number (string-length line)))))
596
e0566f12
LC
597(define %hanging-paren-rx
598 (make-regexp "^[[:blank:]]*[()]+[[:blank:]]*$"))
599
600(define (report-lone-parentheses package line line-number)
601 "Emit a warning if LINE contains hanging parentheses."
602 (when (regexp-exec %hanging-paren-rx line)
603 (emit-warning package
604 (format #f
605 (_ "line ~a: parentheses feel lonely, \
606move to the previous or next line")
607 line-number))))
608
40a7d4e5
LC
609(define %formatting-reporters
610 ;; List of procedures that report formatting issues. These are not separate
611 ;; checkers because they would need to re-read the file.
612 (list report-tabulations
613 report-trailing-white-space
e0566f12
LC
614 report-long-line
615 report-lone-parentheses))
40a7d4e5
LC
616
617(define* (report-formatting-issues package file starting-line
618 #:key (reporters %formatting-reporters))
619 "Report white-space issues in FILE starting from STARTING-LINE, and report
620them for PACKAGE."
621 (define last-line
622 ;; Number of the presumed last line.
623 ;; XXX: Ideally we'd stop at the boundaries of the surrounding sexp, but
624 ;; for now just use this simple heuristic.
625 (+ starting-line 60))
626
627 (call-with-input-file file
628 (lambda (port)
629 (let loop ((line-number 1))
630 (let ((line (read-line port)))
631 (or (eof-object? line)
632 (> line-number last-line)
633 (begin
634 (unless (< line-number starting-line)
635 (for-each (lambda (report)
636 (report package line line-number))
637 reporters))
638 (loop (+ 1 line-number)))))))))
639
640(define (check-formatting package)
641 "Check the formatting of the source code of PACKAGE."
642 (let ((location (package-location package)))
643 (when location
644 (and=> (search-path %load-path (location-file location))
645 (lambda (file)
646 ;; Report issues starting from the line before the 'package'
647 ;; form, which usually contains the 'define' form.
648 (report-formatting-issues package file
649 (- (location-line location) 1)))))))
17a7b75c 650
37627ffa
LC
651\f
652;;;
653;;; List of checkers.
654;;;
655
b4f5e0e8
CR
656(define %checkers
657 (list
8202a513 658 (lint-checker
f4d5bca3 659 (name 'description)
8202a513
CR
660 (description "Validate package descriptions")
661 (check check-description-style))
37627ffa 662 (lint-checker
f4d5bca3 663 (name 'gnu-description)
37627ffa
LC
664 (description "Validate synopsis & description of GNU packages")
665 (check check-gnu-synopsis+description))
b4f5e0e8 666 (lint-checker
f4d5bca3 667 (name 'inputs-should-be-native)
b4f5e0e8
CR
668 (description "Identify inputs that should be native inputs")
669 (check check-inputs-should-be-native))
670 (lint-checker
56b1b74c 671 (name 'patch-file-names)
b210b35d 672 (description "Validate file names and availability of patches")
56b1b74c 673 (check check-patch-file-names))
a3bf0969
LC
674 (lint-checker
675 (name 'home-page)
676 (description "Validate home-page URLs")
677 (check check-home-page))
17a7b75c 678 (lint-checker
52b9efe3
LC
679 (name 'license)
680 ;; TRANSLATORS: <license> is the name of a data type and must not be
681 ;; translated.
682 (description "Make sure the 'license' field is a <license> \
683or a list thereof")
684 (check check-license))
685 (lint-checker
17a7b75c
CR
686 (name 'source)
687 (description "Validate source URLs")
688 (check check-source))
50f5c46d
EB
689 (lint-checker
690 (name 'source-file-name)
691 (description "Validate file names of sources")
692 (check check-source-file-name))
002c57c6
LC
693 (lint-checker
694 (name 'derivation)
695 (description "Report failure to compile a package to a derivation")
696 (check check-derivation))
b4f5e0e8 697 (lint-checker
f4d5bca3 698 (name 'synopsis)
8b9019a6 699 (description "Validate package synopses")
40a7d4e5
LC
700 (check check-synopsis-style))
701 (lint-checker
702 (name 'formatting)
703 (description "Look for formatting issues in the source")
704 (check check-formatting))))
b4f5e0e8 705
dd7c013d
CR
706(define (run-checkers package checkers)
707 ;; Run the given CHECKERS on PACKAGE.
c79c6e59
LC
708 (let ((tty? (isatty? (current-error-port)))
709 (name (package-full-name package)))
710 (for-each (lambda (checker)
711 (when tty?
712 (format (current-error-port) "checking ~a [~a]...\r"
713 name (lint-checker-name checker))
714 (force-output (current-error-port)))
715 ((lint-checker-check checker) package))
716 checkers)))
dd7c013d
CR
717
718\f
719;;;
720;;; Command-line options.
721;;;
722
723(define %default-options
724 ;; Alist of default option values.
725 '())
726
727(define (show-help)
728 (display (_ "Usage: guix lint [OPTION]... [PACKAGE]...
2a4e2e4b
AK
729Run a set of checkers on the specified package; if none is specified,
730run the checkers on all packages.\n"))
dd7c013d
CR
731 (display (_ "
732 -c, --checkers=CHECKER1,CHECKER2...
69b4ffcf 733 only run the specified checkers"))
dd7c013d
CR
734 (display (_ "
735 -h, --help display this help and exit"))
736 (display (_ "
737 -l, --list-checkers display the list of available lint checkers"))
738 (display (_ "
739 -V, --version display version information and exit"))
740 (newline)
741 (show-bug-report-information))
742
743
744(define %options
745 ;; Specification of the command-line options.
746 ;; TODO: add some options:
747 ;; * --certainty=[low,medium,high]: only run checkers that have at least this
748 ;; 'certainty'.
749 (list (option '(#\c "checkers") #t #f
8fbf5302 750 (lambda (opt name arg result)
f4d5bca3 751 (let ((names (map string->symbol (string-split arg #\,))))
dd7c013d 752 (for-each (lambda (c)
f4d5bca3
LC
753 (unless (memq c
754 (map lint-checker-name
755 %checkers))
756 (leave (_ "~a: invalid checker~%") c)))
dd7c013d 757 names)
8fbf5302
LC
758 (alist-cons 'checkers
759 (filter (lambda (checker)
760 (member (lint-checker-name checker)
761 names))
762 %checkers)
763 result))))
dd7c013d
CR
764 (option '(#\h "help") #f #f
765 (lambda args
766 (show-help)
767 (exit 0)))
768 (option '(#\l "list-checkers") #f #f
769 (lambda args
770 (list-checkers-and-exit)))
771 (option '(#\V "version") #f #f
772 (lambda args
773 (show-version-and-exit "guix lint")))))
b4f5e0e8
CR
774
775\f
776;;;
777;;; Entry Point
778;;;
779
780(define (guix-lint . args)
781 (define (parse-options)
782 ;; Return the alist of option values.
783 (args-fold* args %options
8fbf5302 784 (lambda (opt name arg result)
b4f5e0e8 785 (leave (_ "~A: unrecognized option~%") name))
8fbf5302 786 (lambda (arg result)
b4f5e0e8 787 (alist-cons 'argument arg result))
8fbf5302 788 %default-options))
b4f5e0e8
CR
789
790 (let* ((opts (parse-options))
791 (args (filter-map (match-lambda
792 (('argument . value)
793 value)
794 (_ #f))
dd7c013d
CR
795 (reverse opts)))
796 (checkers (or (assoc-ref opts 'checkers) %checkers)))
797 (if (null? args)
798 (fold-packages (lambda (p r) (run-checkers p checkers)) '())
799 (for-each (lambda (spec)
800 (run-checkers (specification->package spec) checkers))
801 args))))