You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

276 lines
10 KiB

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2014 Cyril Roelandt <>
;;; This file is part of GNU Guix.
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; GNU General Public License for more details.
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <>.
(define-module (guix scripts lint)
#:use-module (guix base32)
#:use-module (guix packages)
#:use-module (guix records)
#:use-module (guix ui)
#:use-module (guix utils)
#:use-module (gnu packages)
#:use-module (ice-9 match)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-9)
#:use-module (srfi srfi-11)
#:use-module (srfi srfi-37)
#:export (guix-lint
;;; Helpers
(define* (emit-warning package message #:optional field)
;; Emit a warning about PACKAGE, printing the location of FIELD if it is
;; given, the location of PACKAGE otherwise, the full name of PACKAGE and the
;; provided MESSAGE.
(let ((loc (or (package-field-location package field)
(package-location package))))
(format (guix-warning-port) (_ "~a: ~a: ~a~%")
(location->string loc)
(package-full-name package)
;;; Checkers
(define-record-type* <lint-checker>
lint-checker make-lint-checker
;; TODO: add a 'certainty' field that shows how confident we are in the
;; checker. Then allow users to only run checkers that have a certain
;; 'certainty' level.
(name lint-checker-name)
(description lint-checker-description)
(check lint-checker-check))
(define (list-checkers-and-exit)
;; Print information about all available checkers and exit.
(format #t (_ "Available checkers:~%"))
(for-each (lambda (checker)
(format #t "- ~a: ~a~%"
(lint-checker-name checker)
(lint-checker-description checker)))
(exit 0))
(define (start-with-capital-letter? s)
(char-set-contains? char-set:upper-case (string-ref s 0)))
(define (check-description-style package)
;; Emit a warning if stylistic issues are found in the description of PACKAGE.
(let ((description (package-description package)))
(when (and (string? description)
(not (string-null? description))
(not (start-with-capital-letter? description)))
(emit-warning package
"description should start with an upper-case letter"
(define (check-inputs-should-be-native package)
;; Emit a warning if some inputs of PACKAGE are likely to belong to its
;; native inputs.
(let ((inputs (package-inputs package)))
(match inputs
(((labels packages . _) ...)
(when (member "pkg-config"
(map package-name (filter package? packages)))
(emit-warning package
"pkg-config should probably be a native input"
(define (check-synopsis-style package)
;; Emit a warning if stylistic issues are found in the synopsis of PACKAGE.
(define (check-final-period synopsis)
;; Synopsis should not end with a period, except for some special cases.
(if (and (string=? (string-take-right synopsis 1) ".")
(not (string=? (string-take-right synopsis 4) "etc.")))
(emit-warning package
"no period allowed at the end of the synopsis"
(define (check-start-article synopsis)
(if (or (string-ci=? (string-take synopsis 2) "A ")
(string-ci=? (string-take synopsis 3) "An "))
(emit-warning package
"no article allowed at the beginning of the synopsis"
(define (check-synopsis-length synopsis)
(if (>= (string-length synopsis) 80)
(emit-warning package
"synopsis should be less than 80 characters long"
(define (check-synopsis-start-upper-case synopsis)
(when (and (not (string-null? synopsis))
(not (start-with-capital-letter? synopsis)))
(emit-warning package
"synopsis should start with an upper-case letter"
(define (check-start-with-package-name synopsis)
(let ((idx (string-contains-ci synopsis (package-name package))))
(when (and idx
(= idx 0))
(emit-warning package
"synopsis should not start with the package name")
(let ((synopsis (package-synopsis package)))
(if (string? synopsis)
(check-synopsis-start-upper-case synopsis)
(check-final-period synopsis)
(check-start-article synopsis)
(check-start-with-package-name synopsis)
(check-synopsis-length synopsis)))))
(define (check-patches package)
;; Emit a warning if the patches requires by PACKAGE are badly named.
(let ((patches (and=> (package-source package) origin-patches))
(name (package-name package))
(full-name (package-full-name package)))
(if (and patches
(any (match-lambda
((? string? patch)
(let ((filename (basename patch)))
(not (or (eq? (string-contains filename name) 0)
(eq? (string-contains filename full-name)
;; This must be an <origin> or something like that.
(emit-warning package
"file names of patches should start with the package name"
(define %checkers
(name "description")
(description "Validate package descriptions")
(check check-description-style))
(name "inputs-should-be-native")
(description "Identify inputs that should be native inputs")
(check check-inputs-should-be-native))
(name "patch-filenames")
(description "Validate filenames of patches")
(check check-patches))
(name "synopsis")
(description "Validate package synopsis")
(check check-synopsis-style))))
(define (run-checkers package checkers)
;; Run the given CHECKERS on PACKAGE.
(for-each (lambda (checker)
((lint-checker-check checker) package))
;;; Command-line options.
(define %default-options
;; Alist of default option values.
(define (show-help)
(display (_ "Usage: guix lint [OPTION]... [PACKAGE]...
Run a set of checkers on the specified package; if none is specified, run the checkers on all packages.\n"))
(display (_ "
-c, --checkers=CHECKER1,CHECKER2...
only run the specificed checkers"))
(display (_ "
-h, --help display this help and exit"))
(display (_ "
-l, --list-checkers display the list of available lint checkers"))
(display (_ "
-V, --version display version information and exit"))
(define %options
;; Specification of the command-line options.
;; TODO: add some options:
;; * --certainty=[low,medium,high]: only run checkers that have at least this
;; 'certainty'.
(list (option '(#\c "checkers") #t #f
(lambda (opt name arg result arg-handler)
(let ((names (string-split arg #\,)))
(for-each (lambda (c)
(when (not (member c (map lint-checker-name
(leave (_ "~a: invalid checker") c)))
(values (alist-cons 'checkers
(filter (lambda (checker)
(member (lint-checker-name checker)
(option '(#\h "help") #f #f
(lambda args
(exit 0)))
(option '(#\l "list-checkers") #f #f
(lambda args
(option '(#\V "version") #f #f
(lambda args
(show-version-and-exit "guix lint")))))
;;; Entry Point
(define (guix-lint . args)
(define (parse-options)
;; Return the alist of option values.
(args-fold* args %options
(lambda (opt name arg result arg-handler)
(leave (_ "~A: unrecognized option~%") name))
(lambda (arg result arg-handler)
(alist-cons 'argument arg result))
%default-options #f))
(let* ((opts (parse-options))
(args (filter-map (match-lambda
(('argument . value)
(_ #f))
(reverse opts)))
(checkers (or (assoc-ref opts 'checkers) %checkers)))
(if (null? args)
(fold-packages (lambda (p r) (run-checkers p checkers)) '())
(for-each (lambda (spec)
(run-checkers (specification->package spec) checkers))