diff options
Diffstat (limited to 'lisp/org/org-lint.el')
-rw-r--r-- | lisp/org/org-lint.el | 1094 |
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) |