guix: lint: Check for proper end-of-sentence space.
[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>
86a41263 3;;; Copyright © 2014 Eric Bavier <bavier@member.fsf.org>
b4f5e0e8
CR
4;;;
5;;; This file is part of GNU Guix.
6;;;
7;;; GNU Guix is free software; you can redistribute it and/or modify it
8;;; under the terms of the GNU General Public License as published by
9;;; the Free Software Foundation; either version 3 of the License, or (at
10;;; your option) any later version.
11;;;
12;;; GNU Guix is distributed in the hope that it will be useful, but
13;;; WITHOUT ANY WARRANTY; without even the implied warranty of
14;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15;;; GNU General Public License for more details.
16;;;
17;;; You should have received a copy of the GNU General Public License
18;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
19
20(define-module (guix scripts lint)
21 #:use-module (guix base32)
22 #:use-module (guix packages)
23 #:use-module (guix records)
24 #:use-module (guix ui)
25 #:use-module (guix utils)
26 #:use-module (gnu packages)
27 #:use-module (ice-9 match)
574e847b
EB
28 #:use-module (ice-9 regex)
29 #:use-module (ice-9 format)
b4f5e0e8
CR
30 #:use-module (srfi srfi-1)
31 #:use-module (srfi srfi-9)
32 #:use-module (srfi srfi-11)
33 #:use-module (srfi srfi-37)
34 #:export (guix-lint
8202a513 35 check-description-style
b4f5e0e8
CR
36 check-inputs-should-be-native
37 check-patches
38 check-synopsis-style))
39
40\f
b4f5e0e8
CR
41;;;
42;;; Helpers
43;;;
44(define* (emit-warning package message #:optional field)
45 ;; Emit a warning about PACKAGE, printing the location of FIELD if it is
46 ;; given, the location of PACKAGE otherwise, the full name of PACKAGE and the
47 ;; provided MESSAGE.
48 (let ((loc (or (package-field-location package field)
49 (package-location package))))
b002e9d0
LC
50 (format (guix-warning-port) (_ "~a: ~a: ~a~%")
51 (location->string loc)
52 (package-full-name package)
53 message)))
b4f5e0e8
CR
54
55\f
56;;;
57;;; Checkers
58;;;
59(define-record-type* <lint-checker>
60 lint-checker make-lint-checker
61 lint-checker?
62 ;; TODO: add a 'certainty' field that shows how confident we are in the
63 ;; checker. Then allow users to only run checkers that have a certain
64 ;; 'certainty' level.
65 (name lint-checker-name)
66 (description lint-checker-description)
67 (check lint-checker-check))
68
69(define (list-checkers-and-exit)
70 ;; Print information about all available checkers and exit.
71 (format #t (_ "Available checkers:~%"))
72 (for-each (lambda (checker)
73 (format #t "- ~a: ~a~%"
74 (lint-checker-name checker)
75 (lint-checker-description checker)))
76 %checkers)
77 (exit 0))
78
8202a513 79(define (start-with-capital-letter? s)
574e847b
EB
80 (and (not (string-null? s))
81 (char-set-contains? char-set:upper-case (string-ref s 0))))
8202a513
CR
82
83(define (check-description-style package)
84 ;; Emit a warning if stylistic issues are found in the description of PACKAGE.
574e847b
EB
85 (define (check-starts-with-upper-case description)
86 (unless (start-with-capital-letter? description)
87 (emit-warning package
88 "description should start with an upper-case letter"
89 'description)))
90
91 (define (check-end-of-sentence-space description)
92 "Check that an end-of-sentence period is followed by two spaces."
93 (let ((infractions
94 (reverse (fold-matches
95 "\\. [A-Z]" description '()
96 (lambda (m r)
97 ;; Filter out matches of common abbreviations.
98 (if (find (lambda (s)
99 (string-suffix-ci? s (match:prefix m)))
100 '("i.e" "e.g" "a.k.a" "resp"))
101 r (cons (match:start m) r)))))))
102 (unless (null? infractions)
103 (emit-warning package
104 (format #f "sentences in description should be followed ~
105by two spaces; possible infraction~p at ~{~a~^, ~}"
106 (length infractions)
107 infractions)
108 'description))))
109
110 (let ((description (package-description package)))
111 (when (string? description)
112 (begin
113 (check-starts-with-upper-case description)
114 (check-end-of-sentence-space description)))))
8202a513 115
b4f5e0e8
CR
116(define (check-inputs-should-be-native package)
117 ;; Emit a warning if some inputs of PACKAGE are likely to belong to its
118 ;; native inputs.
119 (let ((inputs (package-inputs package)))
120 (match inputs
121 (((labels packages . _) ...)
122 (when (member "pkg-config"
123 (map package-name (filter package? packages)))
124 (emit-warning package
125 "pkg-config should probably be a native input"
126 'inputs))))))
127
128
129(define (check-synopsis-style package)
130 ;; Emit a warning if stylistic issues are found in the synopsis of PACKAGE.
131 (define (check-final-period synopsis)
132 ;; Synopsis should not end with a period, except for some special cases.
c04b82ff
EB
133 (when (and (string-suffix? "." synopsis)
134 (not (string-suffix? "etc." synopsis)))
135 (emit-warning package
136 "no period allowed at the end of the synopsis"
137 'synopsis)))
b4f5e0e8
CR
138
139 (define (check-start-article synopsis)
c04b82ff
EB
140 (when (or (string-prefix-ci? "A " synopsis)
141 (string-prefix-ci? "An " synopsis))
142 (emit-warning package
143 "no article allowed at the beginning of the synopsis"
144 'synopsis)))
b4f5e0e8 145
5622953d 146 (define (check-synopsis-length synopsis)
c04b82ff
EB
147 (when (>= (string-length synopsis) 80)
148 (emit-warning package
149 "synopsis should be less than 80 characters long"
150 'synopsis)))
5622953d 151
8202a513
CR
152 (define (check-synopsis-start-upper-case synopsis)
153 (when (and (not (string-null? synopsis))
154 (not (start-with-capital-letter? synopsis)))
155 (emit-warning package
156 "synopsis should start with an upper-case letter"
157 'synopsis)))
158
3c762a13 159 (define (check-start-with-package-name synopsis)
86a41263
EB
160 (when (string-prefix-ci? (package-name package) synopsis)
161 (emit-warning package
162 "synopsis should not start with the package name"
163 'synopsis)))
3c762a13 164
b4f5e0e8 165 (let ((synopsis (package-synopsis package)))
b4f5e0e8 166 (begin
8202a513 167 (check-synopsis-start-upper-case synopsis)
b4f5e0e8 168 (check-final-period synopsis)
5622953d 169 (check-start-article synopsis)
3c762a13 170 (check-start-with-package-name synopsis)
5622953d 171 (check-synopsis-length synopsis)))))
c04b82ff 172 (when (string? synopsis)
b4f5e0e8
CR
173
174(define (check-patches package)
175 ;; Emit a warning if the patches requires by PACKAGE are badly named.
176 (let ((patches (and=> (package-source package) origin-patches))
177 (name (package-name package))
178 (full-name (package-full-name package)))
c04b82ff
EB
179 (when (and patches
180 (any (match-lambda
181 ((? string? patch)
182 (let ((filename (basename patch)))
183 (not (or (eq? (string-contains filename name) 0)
184 (eq? (string-contains filename full-name)
185 0)))))
186 (_
187 ;; This must be an <origin> or something like that.
188 #f))
189 patches))
190 (emit-warning package
191 "file names of patches should start with the package name"
192 'patches))))
b4f5e0e8
CR
193
194(define %checkers
195 (list
8202a513
CR
196 (lint-checker
197 (name "description")
198 (description "Validate package descriptions")
199 (check check-description-style))
b4f5e0e8
CR
200 (lint-checker
201 (name "inputs-should-be-native")
202 (description "Identify inputs that should be native inputs")
203 (check check-inputs-should-be-native))
204 (lint-checker
205 (name "patch-filenames")
206 (description "Validate filenames of patches")
207 (check check-patches))
208 (lint-checker
209 (name "synopsis")
210 (description "Validate package synopsis")
211 (check check-synopsis-style))))
212
dd7c013d
CR
213(define (run-checkers package checkers)
214 ;; Run the given CHECKERS on PACKAGE.
b4f5e0e8
CR
215 (for-each (lambda (checker)
216 ((lint-checker-check checker) package))
dd7c013d
CR
217 checkers))
218
219\f
220;;;
221;;; Command-line options.
222;;;
223
224(define %default-options
225 ;; Alist of default option values.
226 '())
227
228(define (show-help)
229 (display (_ "Usage: guix lint [OPTION]... [PACKAGE]...
230Run a set of checkers on the specified package; if none is specified, run the checkers on all packages.\n"))
231 (display (_ "
232 -c, --checkers=CHECKER1,CHECKER2...
233 only run the specificed checkers"))
234 (display (_ "
235 -h, --help display this help and exit"))
236 (display (_ "
237 -l, --list-checkers display the list of available lint checkers"))
238 (display (_ "
239 -V, --version display version information and exit"))
240 (newline)
241 (show-bug-report-information))
242
243
244(define %options
245 ;; Specification of the command-line options.
246 ;; TODO: add some options:
247 ;; * --certainty=[low,medium,high]: only run checkers that have at least this
248 ;; 'certainty'.
249 (list (option '(#\c "checkers") #t #f
250 (lambda (opt name arg result arg-handler)
251 (let ((names (string-split arg #\,)))
252 (for-each (lambda (c)
253 (when (not (member c (map lint-checker-name
254 %checkers)))
255 (leave (_ "~a: invalid checker") c)))
256 names)
257 (values (alist-cons 'checkers
258 (filter (lambda (checker)
259 (member (lint-checker-name checker)
260 names))
261 %checkers)
262 result)
263 #f))))
264 (option '(#\h "help") #f #f
265 (lambda args
266 (show-help)
267 (exit 0)))
268 (option '(#\l "list-checkers") #f #f
269 (lambda args
270 (list-checkers-and-exit)))
271 (option '(#\V "version") #f #f
272 (lambda args
273 (show-version-and-exit "guix lint")))))
b4f5e0e8
CR
274
275\f
276;;;
277;;; Entry Point
278;;;
279
280(define (guix-lint . args)
281 (define (parse-options)
282 ;; Return the alist of option values.
283 (args-fold* args %options
dd7c013d 284 (lambda (opt name arg result arg-handler)
b4f5e0e8 285 (leave (_ "~A: unrecognized option~%") name))
dd7c013d 286 (lambda (arg result arg-handler)
b4f5e0e8 287 (alist-cons 'argument arg result))
dd7c013d 288 %default-options #f))
b4f5e0e8
CR
289
290 (let* ((opts (parse-options))
291 (args (filter-map (match-lambda
292 (('argument . value)
293 value)
294 (_ #f))
dd7c013d
CR
295 (reverse opts)))
296 (checkers (or (assoc-ref opts 'checkers) %checkers)))
297 (if (null? args)
298 (fold-packages (lambda (p r) (run-checkers p checkers)) '())
299 (for-each (lambda (spec)
300 (run-checkers (specification->package spec) checkers))
301 args))))