summaryrefslogtreecommitdiff
path: root/lisp/org/org-lint.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/org/org-lint.el')
-rw-r--r--lisp/org/org-lint.el1094
1 files changed, 615 insertions, 479 deletions
diff --git a/lisp/org/org-lint.el b/lisp/org/org-lint.el
index 6d8cf3f2374..ff2999cb8fa 100644
--- a/lisp/org/org-lint.el
+++ b/lisp/org/org-lint.el
@@ -22,282 +22,331 @@
;;; Commentary:
-;; This library implements linting for Org syntax. The sole public
-;; function is `org-lint', which see.
+;; This library implements linting for Org syntax. The process is
+;; started by calling `org-lint' command, which see.
-;; Internally, the library defines a new structure:
-;; `org-lint-checker', with the following slots:
-
-;; - NAME: Unique check identifier, as a non-nil symbol that doesn't
-;; start with an hyphen.
-;;
-;; The check is done calling the function `org-lint-NAME' with one
-;; mandatory argument, the parse tree describing the current Org
-;; buffer. Such function calls are wrapped within
-;; a `save-excursion' and point is always at `point-min'. Its
-;; return value has to be an alist (POSITION MESSAGE) when
-;; POSITION refer to the buffer position of the error, as an
-;; integer, and MESSAGE is a string describing the error.
-
-;; - DESCRIPTION: Summary about the check, as a string.
-
-;; - CATEGORIES: Categories relative to the check, as a list of
-;; symbol. They are used for filtering when calling `org-lint'.
-;; Checkers not explicitly associated to a category are collected
-;; in the `default' one.
-
-;; - TRUST: The trust level one can have in the check. It is either
-;; `low' or `high', depending on the heuristics implemented and
-;; the nature of the check. This has an indicative value only and
-;; is displayed along reports.
-
-;; All checks have to be listed in `org-lint--checkers'.
+;; New checkers are added by `org-lint-add-checker' function.
+;; Internally, all checks are listed in `org-lint--checkers'.
;; Results are displayed in a special "*Org Lint*" buffer with
;; a dedicated major mode, derived from `tabulated-list-mode'.
-;;
;; In addition to the usual key-bindings inherited from it, "C-j" and
;; "TAB" display problematic line reported under point whereas "RET"
;; jumps to it. Also, "h" hides all reports similar to the current
;; one. Additionally, "i" removes them from subsequent reports.
-;; Checks currently implemented are:
-
-;; - duplicate CUSTOM_ID properties
-;; - duplicate NAME values
-;; - duplicate targets
-;; - duplicate footnote definitions
-;; - orphaned affiliated keywords
-;; - obsolete affiliated keywords
-;; - missing language in source blocks
-;; - missing back-end in export blocks
-;; - invalid Babel call blocks
-;; - NAME values with a colon
-;; - deprecated export block syntax
-;; - deprecated Babel header properties
-;; - wrong header arguments in source blocks
-;; - misuse of CATEGORY keyword
-;; - "coderef" links with unknown destination
-;; - "custom-id" links with unknown destination
-;; - "fuzzy" links with unknown destination
-;; - "id" links with unknown destination
-;; - links to non-existent local files
-;; - SETUPFILE keywords with non-existent file parameter
-;; - INCLUDE keywords with wrong link parameter
-;; - obsolete markup in INCLUDE keyword
-;; - unknown items in OPTIONS keyword
-;; - spurious macro arguments or invalid macro templates
-;; - special properties in properties drawer
-;; - obsolete syntax for PROPERTIES drawers
-;; - Invalid EFFORT property value
-;; - missing definition for footnote references
-;; - missing reference for footnote definitions
-;; - non-footnote definitions in footnote section
-;; - probable invalid keywords
-;; - invalid blocks
-;; - misplaced planning info line
-;; - incomplete drawers
-;; - indented diary-sexps
-;; - obsolete QUOTE section
-;; - obsolete "file+application" link
-;; - spurious colons in tags
+;; Checks currently implemented report the following:
+
+;; - duplicates CUSTOM_ID properties,
+;; - duplicate NAME values,
+;; - duplicate targets,
+;; - duplicate footnote definitions,
+;; - orphaned affiliated keywords,
+;; - obsolete affiliated keywords,
+;; - deprecated export block syntax,
+;; - deprecated Babel header syntax,
+;; - missing language in source blocks,
+;; - missing back-end in export blocks,
+;; - invalid Babel call blocks,
+;; - NAME values with a colon,
+;; - wrong babel headers,
+;; - invalid value in babel headers,
+;; - misuse of CATEGORY keyword,
+;; - "coderef" links with unknown destination,
+;; - "custom-id" links with unknown destination,
+;; - "fuzzy" links with unknown destination,
+;; - "id" links with unknown destination,
+;; - links to non-existent local files,
+;; - SETUPFILE keywords with non-existent file parameter,
+;; - INCLUDE keywords with misleading link parameter,
+;; - obsolete markup in INCLUDE keyword,
+;; - unknown items in OPTIONS keyword,
+;; - spurious macro arguments or invalid macro templates,
+;; - special properties in properties drawers,
+;; - obsolete syntax for properties drawers,
+;; - invalid duration in EFFORT property,
+;; - missing definition for footnote references,
+;; - missing reference for footnote definitions,
+;; - non-footnote definitions in footnote section,
+;; - probable invalid keywords,
+;; - invalid blocks,
+;; - misplaced planning info line,
+;; - probable incomplete drawers,
+;; - probable indented diary-sexps,
+;; - obsolete QUOTE section,
+;; - obsolete "file+application" link,
+;; - obsolete escape syntax in links,
+;; - spurious colons in tags,
+;; - invalid bibliography file,
+;; - missing "print_bibliography" keyword,
+;; - invalid value for "cite_export" keyword,
+;; - incomplete citation object.
;;; Code:
+(require 'org-macs)
+(org-assert-version)
+
(require 'cl-lib)
(require 'ob)
+(require 'oc)
(require 'ol)
(require 'org-attach)
(require 'org-macro)
+(require 'org-fold)
(require 'ox)
+(require 'seq)
-;;; Checkers
+;;; Checkers structure
(cl-defstruct (org-lint-checker (:copier nil))
- (name 'missing-checker-name)
- (description "")
- (categories '(default))
- (trust 'high)) ; `low' or `high'
-
-(defun org-lint-missing-checker-name (_)
- (error
- "`A checker has no `:name' property. Please verify `org-lint--checkers'"))
-
-(defconst org-lint--checkers
- (list
- (make-org-lint-checker
- :name 'duplicate-custom-id
- :description "Report duplicates CUSTOM_ID properties"
- :categories '(link))
- (make-org-lint-checker
- :name 'duplicate-name
- :description "Report duplicate NAME values"
- :categories '(babel link))
- (make-org-lint-checker
- :name 'duplicate-target
- :description "Report duplicate targets"
- :categories '(link))
- (make-org-lint-checker
- :name 'duplicate-footnote-definition
- :description "Report duplicate footnote definitions"
- :categories '(footnote))
- (make-org-lint-checker
- :name 'orphaned-affiliated-keywords
- :description "Report orphaned affiliated keywords"
- :trust 'low)
- (make-org-lint-checker
- :name 'obsolete-affiliated-keywords
- :description "Report obsolete affiliated keywords"
- :categories '(obsolete))
- (make-org-lint-checker
- :name 'deprecated-export-blocks
- :description "Report deprecated export block syntax"
- :categories '(obsolete export)
- :trust 'low)
- (make-org-lint-checker
- :name 'deprecated-header-syntax
- :description "Report deprecated Babel header syntax"
- :categories '(obsolete babel)
- :trust 'low)
- (make-org-lint-checker
- :name 'missing-language-in-src-block
- :description "Report missing language in source blocks"
- :categories '(babel))
- (make-org-lint-checker
- :name 'missing-backend-in-export-block
- :description "Report missing back-end in export blocks"
- :categories '(export))
- (make-org-lint-checker
- :name 'invalid-babel-call-block
- :description "Report invalid Babel call blocks"
- :categories '(babel))
- (make-org-lint-checker
- :name 'colon-in-name
- :description "Report NAME values with a colon"
- :categories '(babel))
- (make-org-lint-checker
- :name 'wrong-header-argument
- :description "Report wrong babel headers"
- :categories '(babel))
- (make-org-lint-checker
- :name 'wrong-header-value
- :description "Report invalid value in babel headers"
- :categories '(babel)
- :trust 'low)
- (make-org-lint-checker
- :name 'deprecated-category-setup
- :description "Report misuse of CATEGORY keyword"
- :categories '(obsolete))
- (make-org-lint-checker
- :name 'invalid-coderef-link
- :description "Report \"coderef\" links with unknown destination"
- :categories '(link))
- (make-org-lint-checker
- :name 'invalid-custom-id-link
- :description "Report \"custom-id\" links with unknown destination"
- :categories '(link))
- (make-org-lint-checker
- :name 'invalid-fuzzy-link
- :description "Report \"fuzzy\" links with unknown destination"
- :categories '(link))
- (make-org-lint-checker
- :name 'invalid-id-link
- :description "Report \"id\" links with unknown destination"
- :categories '(link))
- (make-org-lint-checker
- :name 'link-to-local-file
- :description "Report links to non-existent local files"
- :categories '(link)
- :trust 'low)
- (make-org-lint-checker
- :name 'non-existent-setupfile-parameter
- :description "Report SETUPFILE keywords with non-existent file parameter"
- :trust 'low)
- (make-org-lint-checker
- :name 'wrong-include-link-parameter
- :description "Report INCLUDE keywords with misleading link parameter"
- :categories '(export)
- :trust 'low)
- (make-org-lint-checker
- :name 'obsolete-include-markup
- :description "Report obsolete markup in INCLUDE keyword"
- :categories '(obsolete export)
- :trust 'low)
- (make-org-lint-checker
- :name 'unknown-options-item
- :description "Report unknown items in OPTIONS keyword"
- :categories '(export)
- :trust 'low)
- (make-org-lint-checker
- :name 'invalid-macro-argument-and-template
- :description "Report spurious macro arguments or invalid macro templates"
- :categories '(export)
- :trust 'low)
- (make-org-lint-checker
- :name 'special-property-in-properties-drawer
- :description "Report special properties in properties drawers"
- :categories '(properties))
- (make-org-lint-checker
- :name 'obsolete-properties-drawer
- :description "Report obsolete syntax for properties drawers"
- :categories '(obsolete properties))
- (make-org-lint-checker
- :name 'invalid-effort-property
- :description "Report invalid duration in EFFORT property"
- :categories '(properties))
- (make-org-lint-checker
- :name 'undefined-footnote-reference
- :description "Report missing definition for footnote references"
- :categories '(footnote))
- (make-org-lint-checker
- :name 'unreferenced-footnote-definition
- :description "Report missing reference for footnote definitions"
- :categories '(footnote))
- (make-org-lint-checker
- :name 'extraneous-element-in-footnote-section
- :description "Report non-footnote definitions in footnote section"
- :categories '(footnote))
- (make-org-lint-checker
- :name 'invalid-keyword-syntax
- :description "Report probable invalid keywords"
- :trust 'low)
- (make-org-lint-checker
- :name 'invalid-block
- :description "Report invalid blocks"
- :trust 'low)
- (make-org-lint-checker
- :name 'misplaced-planning-info
- :description "Report misplaced planning info line"
- :trust 'low)
- (make-org-lint-checker
- :name 'incomplete-drawer
- :description "Report probable incomplete drawers"
- :trust 'low)
- (make-org-lint-checker
- :name 'indented-diary-sexp
- :description "Report probable indented diary-sexps"
- :trust 'low)
- (make-org-lint-checker
- :name 'quote-section
- :description "Report obsolete QUOTE section"
- :categories '(obsolete)
- :trust 'low)
- (make-org-lint-checker
- :name 'file-application
- :description "Report obsolete \"file+application\" link"
- :categories '(link obsolete))
- (make-org-lint-checker
- :name 'percent-encoding-link-escape
- :description "Report obsolete escape syntax in links"
- :categories '(link obsolete)
- :trust 'low)
- (make-org-lint-checker
- :name 'spurious-colons
- :description "Report spurious colons in tags"
- :categories '(tags)))
- "List of all available checkers.")
+ name summary function trust categories)
+
+(defvar org-lint--checkers nil
+ "List of all available checkers.
+This list is populated by `org-lint-add-checker' function.")
+
+;;;###autoload
+(defun org-lint-add-checker (name summary fun &rest props)
+ "Add a new checker for linter.
+
+NAME is a unique check identifier, as a non-nil symbol. SUMMARY
+is a short description of the check, as a string.
+
+The check is done calling the function FUN with one mandatory
+argument, the parse tree describing the current Org buffer. Such
+function calls are wrapped within a `save-excursion' and point is
+always at `point-min'. Its return value has to be an
+alist (POSITION MESSAGE) where POSITION refer to the buffer
+position of the error, as an integer, and MESSAGE is a one-line
+string describing the error.
+
+Optional argument PROPS provides additional information about the
+checker. Currently, two properties are supported:
+
+ `:categories'
+
+ Categories relative to the check, as a list of symbol. They
+ are used for filtering when calling `org-lint'. Checkers
+ not explicitly associated to a category are collected in the
+ `default' one.
+
+ `:trust'
+
+ The trust level one can have in the check. It is either
+ `low' or `high', depending on the heuristics implemented and
+ the nature of the check. This has an indicative value only
+ and is displayed along reports."
+ (declare (indent 1))
+ ;; Sanity checks.
+ (pcase name
+ (`nil (error "Name field is mandatory for checkers"))
+ ((pred symbolp) nil)
+ (_ (error "Invalid type for name field")))
+ (unless (functionp fun)
+ (error "Checker field is expected to be a valid function"))
+ ;; Install checker in `org-lint--checkers'; uniquify by name.
+ (setq org-lint--checkers
+ (cons (apply #'make-org-lint-checker
+ :name name
+ :summary summary
+ :function fun
+ props)
+ (seq-remove (lambda (c) (eq name (org-lint-checker-name c)))
+ org-lint--checkers))))
+
+
+;;; Reports UI
+
+(defvar org-lint--report-mode-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map tabulated-list-mode-map)
+ (define-key map (kbd "RET") 'org-lint--jump-to-source)
+ (define-key map (kbd "TAB") 'org-lint--show-source)
+ (define-key map (kbd "C-j") 'org-lint--show-source)
+ (define-key map (kbd "h") 'org-lint--hide-checker)
+ (define-key map (kbd "i") 'org-lint--ignore-checker)
+ map)
+ "Local keymap for `org-lint--report-mode' buffers.")
+
+(define-derived-mode org-lint--report-mode tabulated-list-mode "OrgLint"
+ "Major mode used to display reports emitted during linting.
+\\{org-lint--report-mode-map}"
+ (setf tabulated-list-format
+ `[("Line" 6
+ (lambda (a b)
+ (< (string-to-number (aref (cadr a) 0))
+ (string-to-number (aref (cadr b) 0))))
+ :right-align t)
+ ("Trust" 5 t)
+ ("Warning" 0 t)])
+ (tabulated-list-init-header))
+
+(defun org-lint--generate-reports (buffer checkers)
+ "Generate linting report for BUFFER.
+
+CHECKERS is the list of checkers used.
+
+Return an alist (ID [LINE TRUST DESCRIPTION CHECKER]), suitable
+for `tabulated-list-printer'."
+ (with-current-buffer buffer
+ (save-excursion
+ (goto-char (point-min))
+ (let ((ast (org-element-parse-buffer))
+ (id 0)
+ (last-line 1)
+ (last-pos 1))
+ ;; Insert unique ID for each report. Replace buffer positions
+ ;; with line numbers.
+ (mapcar
+ (lambda (report)
+ (list
+ (cl-incf id)
+ (apply #'vector
+ (cons
+ (progn
+ (goto-char (car report))
+ (beginning-of-line)
+ (prog1 (number-to-string
+ (cl-incf last-line
+ (count-lines last-pos (point))))
+ (setf last-pos (point))))
+ (cdr report)))))
+ ;; Insert trust level in generated reports. Also sort them
+ ;; by buffer position in order to optimize lines computation.
+ (sort (cl-mapcan
+ (lambda (c)
+ (let ((trust (symbol-name (org-lint-checker-trust c))))
+ (mapcar
+ (lambda (report)
+ (list (car report) trust (nth 1 report) c))
+ (save-excursion
+ (funcall (org-lint-checker-function c)
+ ast)))))
+ checkers)
+ #'car-less-than-car))))))
+
+(defvar-local org-lint--source-buffer nil
+ "Source buffer associated to current report buffer.")
+
+(defvar-local org-lint--local-checkers nil
+ "List of checkers used to build current report.")
+
+(defun org-lint--refresh-reports ()
+ (setq tabulated-list-entries
+ (org-lint--generate-reports org-lint--source-buffer
+ org-lint--local-checkers))
+ (tabulated-list-print))
+
+(defun org-lint--current-line ()
+ "Return current report line, as a number."
+ (string-to-number (aref (tabulated-list-get-entry) 0)))
+
+(defun org-lint--current-checker (&optional entry)
+ "Return current report checker.
+When optional argument ENTRY is non-nil, use this entry instead
+of current one."
+ (aref (if entry (nth 1 entry) (tabulated-list-get-entry)) 3))
+
+(defun org-lint--display-reports (source checkers)
+ "Display linting reports for buffer SOURCE.
+CHECKERS is the list of checkers used."
+ (let ((buffer (get-buffer-create "*Org Lint*")))
+ (with-current-buffer buffer
+ (org-lint--report-mode)
+ (setf org-lint--source-buffer source)
+ (setf org-lint--local-checkers checkers)
+ (org-lint--refresh-reports)
+ (add-hook 'tabulated-list-revert-hook #'org-lint--refresh-reports nil t))
+ (pop-to-buffer buffer)))
+
+(defun org-lint--jump-to-source ()
+ "Move to source line that generated the report at point."
+ (interactive)
+ (let ((l (org-lint--current-line)))
+ (switch-to-buffer-other-window org-lint--source-buffer)
+ (org-goto-line l)
+ (org-fold-show-set-visibility 'local)
+ (recenter)))
+
+(defun org-lint--show-source ()
+ "Show source line that generated the report at point."
+ (interactive)
+ (let ((buffer (current-buffer)))
+ (org-lint--jump-to-source)
+ (switch-to-buffer-other-window buffer)))
+
+(defun org-lint--hide-checker ()
+ "Hide all reports from checker that generated the report at point."
+ (interactive)
+ (let ((c (org-lint--current-checker)))
+ (setf tabulated-list-entries
+ (cl-remove-if (lambda (e) (equal c (org-lint--current-checker e)))
+ tabulated-list-entries))
+ (tabulated-list-print)))
+
+(defun org-lint--ignore-checker ()
+ "Ignore all reports from checker that generated the report at point.
+Checker will also be ignored in all subsequent reports."
+ (interactive)
+ (setf org-lint--local-checkers
+ (remove (org-lint--current-checker) org-lint--local-checkers))
+ (org-lint--hide-checker))
+
+
+;;; Main function
+
+;;;###autoload
+(defun org-lint (&optional arg)
+ "Check current Org buffer for syntax mistakes.
+
+By default, run all checkers. With a `\\[universal-argument]' prefix ARG, \
+select one
+category of checkers only. With a `\\[universal-argument] \
+\\[universal-argument]' prefix, run one precise
+checker by its name.
+
+ARG can also be a list of checker names, as symbols, to run."
+ (interactive "P")
+ (unless (derived-mode-p 'org-mode) (user-error "Not in an Org buffer"))
+ (when (called-interactively-p 'any)
+ (message "Org linting process starting..."))
+ (let ((checkers
+ (pcase arg
+ (`nil org-lint--checkers)
+ (`(4)
+ (let ((category
+ (completing-read
+ "Checker category: "
+ (mapcar #'org-lint-checker-categories org-lint--checkers)
+ nil t)))
+ (cl-remove-if-not
+ (lambda (c)
+ (assoc-string category (org-lint-checker-categories c)))
+ org-lint--checkers)))
+ (`(16)
+ (list
+ (let ((name (completing-read
+ "Checker name: "
+ (mapcar #'org-lint-checker-name org-lint--checkers)
+ nil t)))
+ (catch 'exit
+ (dolist (c org-lint--checkers)
+ (when (string= (org-lint-checker-name c) name)
+ (throw 'exit c)))))))
+ ((pred consp)
+ (cl-remove-if-not (lambda (c) (memq (org-lint-checker-name c) arg))
+ org-lint--checkers))
+ (_ (user-error "Invalid argument `%S' for `org-lint'" arg)))))
+ (if (not (called-interactively-p 'any))
+ (org-lint--generate-reports (current-buffer) checkers)
+ (org-lint--display-reports (current-buffer) checkers)
+ (message "Org linting process completed"))))
+
+
+;;; Checker functions
(defun org-lint--collect-duplicates
(ast type extract-key extract-position build-message)
@@ -334,7 +383,7 @@ called with one argument, the key used for comparison."
ast
'node-property
(lambda (property)
- (and (string-equal-ignore-case
+ (and (org-string-equal-ignore-case
"CUSTOM_ID" (org-element-property :key property))
(org-element-property :value property)))
(lambda (property _) (org-element-property :begin property))
@@ -601,39 +650,40 @@ in description"
(org-element-map ast 'keyword
(lambda (k)
(when (equal (org-element-property :key k) "INCLUDE")
- (let* ((value (org-element-property :value k))
- (path
- (and (string-match "^\\(\".+\"\\|\\S-+\\)[ \t]*" value)
- (save-match-data
- (org-strip-quotes (match-string 1 value))))))
- (if (not path)
- (list (org-element-property :post-affiliated k)
- "Missing location argument in INCLUDE keyword")
- (let* ((file (org-string-nw-p
- (if (string-match "::\\(.*\\)\\'" path)
- (substring path 0 (match-beginning 0))
- path)))
- (search (and (not (equal file path))
- (org-string-nw-p (match-string 1 path)))))
- (if (and file
- (not (file-remote-p file))
- (not (file-exists-p file)))
- (list (org-element-property :post-affiliated k)
- "Non-existent file argument in INCLUDE keyword")
- (let* ((visiting (if file (find-buffer-visiting file)
- (current-buffer)))
- (buffer (or visiting (find-file-noselect file)))
- (org-link-search-must-match-exact-headline t))
- (unwind-protect
- (with-current-buffer buffer
- (when (and search
- (not (ignore-errors
- (org-link-search search nil t))))
- (list (org-element-property :post-affiliated k)
- (format
- "Invalid search part \"%s\" in INCLUDE keyword"
- search))))
- (unless visiting (kill-buffer buffer))))))))))))
+ (let* ((value (org-element-property :value k))
+ (path
+ (and (string-match "^\\(\".+?\"\\|\\S-+\\)[ \t]*" value)
+ (save-match-data
+ (org-strip-quotes (match-string 1 value))))))
+ (if (not path)
+ (list (org-element-property :post-affiliated k)
+ "Missing location argument in INCLUDE keyword")
+ (let* ((file (org-string-nw-p
+ (if (string-match "::\\(.*\\)\\'" path)
+ (substring path 0 (match-beginning 0))
+ path)))
+ (search (and (not (equal file path))
+ (org-string-nw-p (match-string 1 path)))))
+ (unless (org-url-p file)
+ (if (and file
+ (not (file-remote-p file))
+ (not (file-exists-p file)))
+ (list (org-element-property :post-affiliated k)
+ "Non-existent file argument in INCLUDE keyword")
+ (let* ((visiting (if file (find-buffer-visiting file)
+ (current-buffer)))
+ (buffer (or visiting (find-file-noselect file)))
+ (org-link-search-must-match-exact-headline t))
+ (unwind-protect
+ (with-current-buffer buffer
+ (when (and search
+ (not (ignore-errors
+ (org-link-search search nil t))))
+ (list (org-element-property :post-affiliated k)
+ (format
+ "Invalid search part \"%s\" in INCLUDE keyword"
+ search))))
+ (unless visiting (kill-buffer buffer)))))))))))))
(defun org-lint-obsolete-include-markup (ast)
(let ((regexp (format "\\`\\(?:\".+\"\\|\\S-+\\)[ \t]+%s"
@@ -1016,8 +1066,10 @@ Use \"export %s\" instead"
(`keyword
(when (string= (org-element-property :key datum) "PROPERTY")
(let ((value (org-element-property :value datum)))
- (when (string-match "\\`header-args\\(?::\\(\\S-+\\)\\)?\\+? *"
- value)
+ (when (or (string-match "\\`header-args\\(?::\\(\\S-+\\)\\)?\\+ *"
+ value)
+ (string-match "\\`header-args\\(?::\\(\\S-+\\)\\)? *"
+ value))
(funcall verify
datum
(match-string 1 value)
@@ -1026,8 +1078,10 @@ Use \"export %s\" instead"
(`node-property
(let ((key (org-element-property :key datum)))
(when (let ((case-fold-search t))
- (string-match "\\`HEADER-ARGS\\(?::\\(\\S-+\\)\\)?\\+?"
- key))
+ (or (string-match "\\`HEADER-ARGS\\(?::\\(\\S-+\\)\\)?\\+"
+ key)
+ (string-match "\\`HEADER-ARGS\\(?::\\(\\S-+\\)\\)?"
+ key)))
(funcall verify
datum
(match-string 1 key)
@@ -1123,196 +1177,278 @@ Use \"export %s\" instead"
(list (org-element-property :begin h)
"Tags contain a spurious colon")))))
+(defun org-lint-non-existent-bibliography (ast)
+ (org-element-map ast 'keyword
+ (lambda (k)
+ (when (equal "BIBLIOGRAPHY" (org-element-property :key k))
+ (let ((file (org-strip-quotes (org-element-property :value k))))
+ (and (not (file-remote-p file))
+ (not (file-exists-p file))
+ (list (org-element-property :begin k)
+ (format "Non-existent bibliography %S" file))))))))
+
+(defun org-lint-missing-print-bibliography (ast)
+ (and (org-element-map ast 'citation #'identity nil t)
+ (not (org-element-map ast 'keyword
+ (lambda (k)
+ (equal "PRINT_BIBLIOGRAPHY" (org-element-property :key k)))
+ nil t))
+ (list
+ (list (point-max) "Possibly missing \"PRINT_BIBLIOGRAPHY\" keyword"))))
+
+(defun org-lint-invalid-cite-export-declaration (ast)
+ (org-element-map ast 'keyword
+ (lambda (k)
+ (when (equal "CITE_EXPORT" (org-element-property :key k))
+ (let ((value (org-element-property :value k))
+ (source (org-element-property :begin k)))
+ (if (equal value "")
+ (list source "Missing export processor name")
+ (condition-case _
+ (pcase (org-cite-read-processor-declaration value)
+ (`(,(and (pred symbolp) name)
+ ,(pred string-or-null-p)
+ ,(pred string-or-null-p))
+ (unless (org-cite-get-processor name)
+ (list source "Unknown cite export processor %S" name)))
+ (_
+ (list source "Invalid cite export processor declaration")))
+ (error
+ (list source "Invalid cite export processor declaration")))))))))
+
+(defun org-lint-incomplete-citation (ast)
+ (org-element-map ast 'plain-text
+ (lambda (text)
+ (and (string-match-p org-element-citation-prefix-re text)
+ ;; XXX: The code below signals the error at the beginning
+ ;; of the paragraph containing the faulty object. It is
+ ;; not very accurate but may be enough for now.
+ (list (org-element-property :contents-begin
+ (org-element-property :parent text))
+ "Possibly incomplete citation markup")))))
-;;; Reports UI
-
-(defvar org-lint--report-mode-map
- (let ((map (make-sparse-keymap)))
- (set-keymap-parent map tabulated-list-mode-map)
- (define-key map (kbd "RET") 'org-lint--jump-to-source)
- (define-key map (kbd "TAB") 'org-lint--show-source)
- (define-key map (kbd "C-j") 'org-lint--show-source)
- (define-key map (kbd "h") 'org-lint--hide-checker)
- (define-key map (kbd "i") 'org-lint--ignore-checker)
- map)
- "Local keymap for `org-lint--report-mode' buffers.")
-
-(define-derived-mode org-lint--report-mode tabulated-list-mode "OrgLint"
- "Major mode used to display reports emitted during linting.
-\\{org-lint--report-mode-map}"
- (setf tabulated-list-format
- `[("Line" 6
- (lambda (a b)
- (< (string-to-number (aref (cadr a) 0))
- (string-to-number (aref (cadr b) 0))))
- :right-align t)
- ("Trust" 5 t)
- ("Warning" 0 t)])
- (tabulated-list-init-header))
-
-(defun org-lint--generate-reports (buffer checkers)
- "Generate linting report for BUFFER.
-
-CHECKERS is the list of checkers used.
-
-Return an alist (ID [LINE TRUST DESCRIPTION CHECKER]), suitable
-for `tabulated-list-printer'."
- (with-current-buffer buffer
- (save-excursion
- (goto-char (point-min))
- (let ((ast (org-element-parse-buffer))
- (id 0)
- (last-line 1)
- (last-pos 1))
- ;; Insert unique ID for each report. Replace buffer positions
- ;; with line numbers.
- (mapcar
- (lambda (report)
- (list
- (cl-incf id)
- (apply #'vector
- (cons
- (progn
- (goto-char (car report))
- (beginning-of-line)
- (prog1 (number-to-string
- (cl-incf last-line
- (count-lines last-pos (point))))
- (setf last-pos (point))))
- (cdr report)))))
- ;; Insert trust level in generated reports. Also sort them
- ;; by buffer position in order to optimize lines computation.
- (sort (cl-mapcan
- (lambda (c)
- (let ((trust (symbol-name (org-lint-checker-trust c))))
- (mapcar
- (lambda (report)
- (list (car report) trust (nth 1 report) c))
- (save-excursion
- (funcall
- (intern (format "org-lint-%s"
- (org-lint-checker-name c)))
- ast)))))
- checkers)
- #'car-less-than-car))))))
-
-(defvar-local org-lint--source-buffer nil
- "Source buffer associated to current report buffer.")
-
-(defvar-local org-lint--local-checkers nil
- "List of checkers used to build current report.")
-
-(defun org-lint--refresh-reports ()
- (setq tabulated-list-entries
- (org-lint--generate-reports org-lint--source-buffer
- org-lint--local-checkers))
- (tabulated-list-print))
-
-(defun org-lint--current-line ()
- "Return current report line, as a number."
- (string-to-number (aref (tabulated-list-get-entry) 0)))
-
-(defun org-lint--current-checker (&optional entry)
- "Return current report checker.
-When optional argument ENTRY is non-nil, use this entry instead
-of current one."
- (aref (if entry (nth 1 entry) (tabulated-list-get-entry)) 3))
-
-(defun org-lint--display-reports (source checkers)
- "Display linting reports for buffer SOURCE.
-CHECKERS is the list of checkers used."
- (let ((buffer (get-buffer-create "*Org Lint*")))
- (with-current-buffer buffer
- (org-lint--report-mode)
- (setf org-lint--source-buffer source)
- (setf org-lint--local-checkers checkers)
- (org-lint--refresh-reports)
- (add-hook 'tabulated-list-revert-hook #'org-lint--refresh-reports nil t))
- (pop-to-buffer buffer)))
-
-(defun org-lint--jump-to-source ()
- "Move to source line that generated the report at point."
- (interactive)
- (let ((l (org-lint--current-line)))
- (switch-to-buffer-other-window org-lint--source-buffer)
- (org-goto-line l)
- (org-show-set-visibility 'local)
- (recenter)))
-
-(defun org-lint--show-source ()
- "Show source line that generated the report at point."
- (interactive)
- (let ((buffer (current-buffer)))
- (org-lint--jump-to-source)
- (switch-to-buffer-other-window buffer)))
-
-(defun org-lint--hide-checker ()
- "Hide all reports from checker that generated the report at point."
- (interactive)
- (let ((c (org-lint--current-checker)))
- (setf tabulated-list-entries
- (cl-remove-if (lambda (e) (equal c (org-lint--current-checker e)))
- tabulated-list-entries))
- (tabulated-list-print)))
-
-(defun org-lint--ignore-checker ()
- "Ignore all reports from checker that generated the report at point.
-Checker will also be ignored in all subsequent reports."
- (interactive)
- (setf org-lint--local-checkers
- (remove (org-lint--current-checker) org-lint--local-checkers))
- (org-lint--hide-checker))
-
-
-;;; Public function
-
-;;;###autoload
-(defun org-lint (&optional arg)
- "Check current Org buffer for syntax mistakes.
-
-By default, run all checkers. With a `\\[universal-argument]' prefix ARG, \
-select one
-category of checkers only. With a `\\[universal-argument] \
-\\[universal-argument]' prefix, run one precise
-checker by its name.
-
-ARG can also be a list of checker names, as symbols, to run."
- (interactive "P")
- (unless (derived-mode-p 'org-mode) (user-error "Not in an Org buffer"))
- (when (called-interactively-p 'any)
- (message "Org linting process starting..."))
- (let ((checkers
- (pcase arg
- (`nil org-lint--checkers)
- (`(4)
- (let ((category
- (completing-read
- "Checker category: "
- (mapcar #'org-lint-checker-categories org-lint--checkers)
- nil t)))
- (cl-remove-if-not
- (lambda (c)
- (assoc-string (org-lint-checker-categories c) category))
- org-lint--checkers)))
- (`(16)
- (list
- (let ((name (completing-read
- "Checker name: "
- (mapcar #'org-lint-checker-name org-lint--checkers)
- nil t)))
- (catch 'exit
- (dolist (c org-lint--checkers)
- (when (string= (org-lint-checker-name c) name)
- (throw 'exit c)))))))
- ((pred consp)
- (cl-remove-if-not (lambda (c) (memq (org-lint-checker-name c) arg))
- org-lint--checkers))
- (_ (user-error "Invalid argument `%S' for `org-lint'" arg)))))
- (if (not (called-interactively-p 'any))
- (org-lint--generate-reports (current-buffer) checkers)
- (org-lint--display-reports (current-buffer) checkers)
- (message "Org linting process completed"))))
+;;; Checkers declaration
+
+(org-lint-add-checker 'duplicate-custom-id
+ "Report duplicates CUSTOM_ID properties"
+ #'org-lint-duplicate-custom-id
+ :categories '(link))
+
+(org-lint-add-checker 'duplicate-name
+ "Report duplicate NAME values"
+ #'org-lint-duplicate-name
+ :categories '(babel 'link))
+
+(org-lint-add-checker 'duplicate-target
+ "Report duplicate targets"
+ #'org-lint-duplicate-target
+ :categories '(link))
+
+(org-lint-add-checker 'duplicate-footnote-definition
+ "Report duplicate footnote definitions"
+ #'org-lint-duplicate-footnote-definition
+ :categories '(footnote))
+
+(org-lint-add-checker 'orphaned-affiliated-keywords
+ "Report orphaned affiliated keywords"
+ #'org-lint-orphaned-affiliated-keywords
+ :trust 'low)
+
+(org-lint-add-checker 'obsolete-affiliated-keywords
+ "Report obsolete affiliated keywords"
+ #'org-lint-obsolete-affiliated-keywords
+ :categories '(obsolete))
+
+(org-lint-add-checker 'deprecated-export-blocks
+ "Report deprecated export block syntax"
+ #'org-lint-deprecated-export-blocks
+ :trust 'low :categories '(obsolete export))
+
+(org-lint-add-checker 'deprecated-header-syntax
+ "Report deprecated Babel header syntax"
+ #'org-lint-deprecated-header-syntax
+ :trust 'low :categories '(obsolete babel))
+
+(org-lint-add-checker 'missing-language-in-src-block
+ "Report missing language in source blocks"
+ #'org-lint-missing-language-in-src-block
+ :categories '(babel))
+
+(org-lint-add-checker 'missing-backend-in-export-block
+ "Report missing back-end in export blocks"
+ #'org-lint-missing-backend-in-export-block
+ :categories '(export))
+
+(org-lint-add-checker 'invalid-babel-call-block
+ "Report invalid Babel call blocks"
+ #'org-lint-invalid-babel-call-block
+ :categories '(babel))
+
+(org-lint-add-checker 'colon-in-name
+ "Report NAME values with a colon"
+ #'org-lint-colon-in-name
+ :categories '(babel))
+
+(org-lint-add-checker 'wrong-header-argument
+ "Report wrong babel headers"
+ #'org-lint-wrong-header-argument
+ :categories '(babel))
+
+(org-lint-add-checker 'wrong-header-value
+ "Report invalid value in babel headers"
+ #'org-lint-wrong-header-value
+ :categories '(babel) :trust 'low)
+
+(org-lint-add-checker 'deprecated-category-setup
+ "Report misuse of CATEGORY keyword"
+ #'org-lint-deprecated-category-setup
+ :categories '(obsolete))
+
+(org-lint-add-checker 'invalid-coderef-link
+ "Report \"coderef\" links with unknown destination"
+ #'org-lint-invalid-coderef-link
+ :categories '(link))
+
+(org-lint-add-checker 'invalid-custom-id-link
+ "Report \"custom-id\" links with unknown destination"
+ #'org-lint-invalid-custom-id-link
+ :categories '(link))
+
+(org-lint-add-checker 'invalid-fuzzy-link
+ "Report \"fuzzy\" links with unknown destination"
+ #'org-lint-invalid-fuzzy-link
+ :categories '(link))
+
+(org-lint-add-checker 'invalid-id-link
+ "Report \"id\" links with unknown destination"
+ #'org-lint-invalid-id-link
+ :categories '(link))
+
+(org-lint-add-checker 'link-to-local-file
+ "Report links to non-existent local files"
+ #'org-lint-link-to-local-file
+ :categories '(link) :trust 'low)
+
+(org-lint-add-checker 'non-existent-setupfile-parameter
+ "Report SETUPFILE keywords with non-existent file parameter"
+ #'org-lint-non-existent-setupfile-parameter
+ :trust 'low)
+
+(org-lint-add-checker 'wrong-include-link-parameter
+ "Report INCLUDE keywords with misleading link parameter"
+ #'org-lint-wrong-include-link-parameter
+ :categories '(export) :trust 'low)
+
+(org-lint-add-checker 'obsolete-include-markup
+ "Report obsolete markup in INCLUDE keyword"
+ #'org-lint-obsolete-include-markup
+ :categories '(obsolete export) :trust 'low)
+
+(org-lint-add-checker 'unknown-options-item
+ "Report unknown items in OPTIONS keyword"
+ #'org-lint-unknown-options-item
+ :categories '(export) :trust 'low)
+
+(org-lint-add-checker 'invalid-macro-argument-and-template
+ "Report spurious macro arguments or invalid macro templates"
+ #'org-lint-invalid-macro-argument-and-template
+ :categories '(export) :trust 'low)
+
+(org-lint-add-checker 'special-property-in-properties-drawer
+ "Report special properties in properties drawers"
+ #'org-lint-special-property-in-properties-drawer
+ :categories '(properties))
+
+(org-lint-add-checker 'obsolete-properties-drawer
+ "Report obsolete syntax for properties drawers"
+ #'org-lint-obsolete-properties-drawer
+ :categories '(obsolete properties))
+
+(org-lint-add-checker 'invalid-effort-property
+ "Report invalid duration in EFFORT property"
+ #'org-lint-invalid-effort-property
+ :categories '(properties))
+
+(org-lint-add-checker 'undefined-footnote-reference
+ "Report missing definition for footnote references"
+ #'org-lint-undefined-footnote-reference
+ :categories '(footnote))
+
+(org-lint-add-checker 'unreferenced-footnote-definition
+ "Report missing reference for footnote definitions"
+ #'org-lint-unreferenced-footnote-definition
+ :categories '(footnote))
+
+(org-lint-add-checker 'extraneous-element-in-footnote-section
+ "Report non-footnote definitions in footnote section"
+ #'org-lint-extraneous-element-in-footnote-section
+ :categories '(footnote))
+
+(org-lint-add-checker 'invalid-keyword-syntax
+ "Report probable invalid keywords"
+ #'org-lint-invalid-keyword-syntax
+ :trust 'low)
+
+(org-lint-add-checker 'invalid-block
+ "Report invalid blocks"
+ #'org-lint-invalid-block
+ :trust 'low)
+
+(org-lint-add-checker 'misplaced-planning-info
+ "Report misplaced planning info line"
+ #'org-lint-misplaced-planning-info
+ :trust 'low)
+
+(org-lint-add-checker 'incomplete-drawer
+ "Report probable incomplete drawers"
+ #'org-lint-incomplete-drawer
+ :trust 'low)
+
+(org-lint-add-checker 'indented-diary-sexp
+ "Report probable indented diary-sexps"
+ #'org-lint-indented-diary-sexp
+ :trust 'low)
+
+(org-lint-add-checker 'quote-section
+ "Report obsolete QUOTE section"
+ #'org-lint-quote-section
+ :categories '(obsolete) :trust 'low)
+
+(org-lint-add-checker 'file-application
+ "Report obsolete \"file+application\" link"
+ #'org-lint-file-application
+ :categories '(link obsolete))
+
+(org-lint-add-checker 'percent-encoding-link-escape
+ "Report obsolete escape syntax in links"
+ #'org-lint-percent-encoding-link-escape
+ :categories '(link obsolete) :trust 'low)
+
+(org-lint-add-checker 'spurious-colons
+ "Report spurious colons in tags"
+ #'org-lint-spurious-colons
+ :categories '(tags))
+
+(org-lint-add-checker 'non-existent-bibliography
+ "Report invalid bibliography file"
+ #'org-lint-non-existent-bibliography
+ :categories '(cite))
+
+(org-lint-add-checker 'missing-print-bibliography
+ "Report missing \"print_bibliography\" keyword"
+ #'org-lint-missing-print-bibliography
+ :categories '(cite))
+
+(org-lint-add-checker 'invalid-cite-export-declaration
+ "Report invalid value for \"cite_export\" keyword"
+ #'org-lint-invalid-cite-export-declaration
+ :categories '(cite))
+
+(org-lint-add-checker 'incomplete-citation
+ "Report incomplete citation object"
+ #'org-lint-incomplete-citation
+ :categories '(cite) :trust 'low)
(provide 'org-lint)