org-lint.el (69243B)
1 ;;; org-lint.el --- Linting for Org documents -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2015-2024 Free Software Foundation, Inc. 4 5 ;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr> 6 ;; Keywords: outlines, hypermedia, calendar, text 7 8 ;; This file is part of GNU Emacs. 9 10 ;; GNU Emacs is free software; you can redistribute it and/or modify 11 ;; it under the terms of the GNU General Public License as published by 12 ;; the Free Software Foundation, either version 3 of the License, or 13 ;; (at your option) any later version. 14 15 ;; GNU Emacs is distributed in the hope that it will be useful, 16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 ;; GNU General Public License for more details. 19 20 ;; You should have received a copy of the GNU General Public License 21 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. 22 23 ;;; Commentary: 24 25 ;; This library implements linting for Org syntax. The process is 26 ;; started by calling `org-lint' command, which see. 27 28 ;; New checkers are added by `org-lint-add-checker' function. 29 ;; Internally, all checks are listed in `org-lint--checkers'. 30 31 ;; Results are displayed in a special "*Org Lint*" buffer with 32 ;; a dedicated major mode, derived from `tabulated-list-mode'. 33 ;; In addition to the usual key-bindings inherited from it, "C-j" and 34 ;; "TAB" display problematic line reported under point whereas "RET" 35 ;; jumps to it. Also, "h" hides all reports similar to the current 36 ;; one. Additionally, "i" removes them from subsequent reports. 37 38 ;; Checks currently implemented report the following: 39 40 ;; - duplicates CUSTOM_ID properties, 41 ;; - duplicate NAME values, 42 ;; - duplicate targets, 43 ;; - duplicate footnote definitions, 44 ;; - orphaned affiliated keywords, 45 ;; - obsolete affiliated keywords, 46 ;; - deprecated export block syntax, 47 ;; - deprecated Babel header syntax, 48 ;; - missing language in source blocks, 49 ;; - missing backend in export blocks, 50 ;; - invalid Babel call blocks, 51 ;; - NAME values with a colon, 52 ;; - wrong babel headers, 53 ;; - invalid value in babel headers, 54 ;; - misuse of CATEGORY keyword, 55 ;; - "coderef" links with unknown destination, 56 ;; - "custom-id" links with unknown destination, 57 ;; - "fuzzy" links with unknown destination, 58 ;; - "id" links with unknown destination, 59 ;; - links to non-existent local files, 60 ;; - SETUPFILE keywords with non-existent file parameter, 61 ;; - INCLUDE keywords with misleading link parameter, 62 ;; - obsolete markup in INCLUDE keyword, 63 ;; - unknown items in OPTIONS keyword, 64 ;; - spurious macro arguments or invalid macro templates, 65 ;; - special properties in properties drawers, 66 ;; - obsolete syntax for properties drawers, 67 ;; - invalid duration in EFFORT property, 68 ;; - invalid ID property with a double colon, 69 ;; - missing definition for footnote references, 70 ;; - missing reference for footnote definitions, 71 ;; - non-footnote definitions in footnote section, 72 ;; - probable invalid keywords, 73 ;; - invalid blocks, 74 ;; - mismatched repeaters in planning info line, 75 ;; - misplaced planning info line, 76 ;; - probable incomplete drawers, 77 ;; - probable indented diary-sexps, 78 ;; - obsolete QUOTE section, 79 ;; - obsolete "file+application" link, 80 ;; - obsolete escape syntax in links, 81 ;; - spurious colons in tags, 82 ;; - invalid bibliography file, 83 ;; - missing "print_bibliography" keyword, 84 ;; - invalid value for "cite_export" keyword, 85 ;; - incomplete citation object. 86 87 88 ;;; Code: 89 90 (require 'org-macs) 91 (org-assert-version) 92 93 (require 'cl-lib) 94 (require 'ob) 95 (require 'oc) 96 (require 'ol) 97 (require 'org-attach) 98 (require 'org-macro) 99 (require 'org-fold) 100 (require 'ox) 101 (require 'seq) 102 103 104 ;;; Checkers structure 105 106 (cl-defstruct (org-lint-checker (:copier nil)) 107 name summary function trust categories) 108 109 (defvar org-lint--checkers nil 110 "List of all available checkers. 111 This list is populated by `org-lint-add-checker' function.") 112 113 ;;;###autoload 114 (defun org-lint-add-checker (name summary fun &rest props) 115 "Add a new checker for linter. 116 117 NAME is a unique check identifier, as a non-nil symbol. SUMMARY 118 is a short description of the check, as a string. 119 120 The check is done calling the function FUN with one mandatory 121 argument, the parse tree describing the current Org buffer. Such 122 function calls are wrapped within a `save-excursion' and point is 123 always at `point-min'. Its return value has to be an 124 alist (POSITION MESSAGE) where POSITION refer to the buffer 125 position of the error, as an integer, and MESSAGE is a one-line 126 string describing the error. 127 128 Optional argument PROPS provides additional information about the 129 checker. Currently, two properties are supported: 130 131 `:categories' 132 133 Categories relative to the check, as a list of symbol. They 134 are used for filtering when calling `org-lint'. Checkers 135 not explicitly associated to a category are collected in the 136 `default' one. 137 138 `:trust' 139 140 The trust level one can have in the check. It is either 141 `low' or `high', depending on the heuristics implemented and 142 the nature of the check. This has an indicative value only 143 and is displayed along reports." 144 (declare (indent 1)) 145 ;; Sanity checks. 146 (pcase name 147 (`nil (error "Name field is mandatory for checkers")) 148 ((pred symbolp) nil) 149 (_ (error "Invalid type for name field"))) 150 (unless (functionp fun) 151 (error "Checker field is expected to be a valid function")) 152 ;; Install checker in `org-lint--checkers'; uniquify by name. 153 (setq org-lint--checkers 154 (cons (apply #'make-org-lint-checker 155 :name name 156 :summary summary 157 :function fun 158 props) 159 (seq-remove (lambda (c) (eq name (org-lint-checker-name c))) 160 org-lint--checkers)))) 161 162 163 ;;; Reports UI 164 165 (defvar org-lint--report-mode-map 166 (let ((map (make-sparse-keymap))) 167 (set-keymap-parent map tabulated-list-mode-map) 168 (define-key map (kbd "RET") 'org-lint--jump-to-source) 169 (define-key map (kbd "TAB") 'org-lint--show-source) 170 (define-key map (kbd "C-j") 'org-lint--show-source) 171 (define-key map (kbd "h") 'org-lint--hide-checker) 172 (define-key map (kbd "i") 'org-lint--ignore-checker) 173 map) 174 "Local keymap for `org-lint--report-mode' buffers.") 175 176 (define-derived-mode org-lint--report-mode tabulated-list-mode "OrgLint" 177 "Major mode used to display reports emitted during linting. 178 \\{org-lint--report-mode-map}" 179 (setf tabulated-list-format 180 `[("Line" 6 181 (lambda (a b) 182 (< (string-to-number (aref (cadr a) 0)) 183 (string-to-number (aref (cadr b) 0)))) 184 :right-align t) 185 ("Trust" 5 t) 186 ("Warning" 0 t)]) 187 (tabulated-list-init-header)) 188 189 (defun org-lint--generate-reports (buffer checkers) 190 "Generate linting report for BUFFER. 191 192 CHECKERS is the list of checkers used. 193 194 Return an alist (ID [LINE TRUST DESCRIPTION CHECKER]), suitable 195 for `tabulated-list-printer'." 196 (with-current-buffer buffer 197 (save-excursion 198 (goto-char (point-min)) 199 (let ((ast (org-element-parse-buffer nil nil 'defer)) 200 (id 0) 201 (last-line 1) 202 (last-pos 1)) 203 ;; Insert unique ID for each report. Replace buffer positions 204 ;; with line numbers. 205 (mapcar 206 (lambda (report) 207 (list 208 (cl-incf id) 209 (apply #'vector 210 (cons 211 (progn 212 (goto-char (car report)) 213 (forward-line 0) 214 (prog1 (propertize 215 (number-to-string 216 (cl-incf last-line 217 (count-lines last-pos (point)))) 218 'org-lint-marker (car report)) 219 (setf last-pos (point)))) 220 (cdr report))))) 221 ;; Insert trust level in generated reports. Also sort them 222 ;; by buffer position in order to optimize lines computation. 223 (sort (cl-mapcan 224 (lambda (c) 225 (let ((trust (symbol-name (org-lint-checker-trust c)))) 226 (mapcar 227 (lambda (report) 228 (list (copy-marker (car report)) trust (nth 1 report) c)) 229 (save-excursion 230 (funcall (org-lint-checker-function c) 231 ast))))) 232 checkers) 233 #'car-less-than-car)))))) 234 235 (defvar-local org-lint--source-buffer nil 236 "Source buffer associated to current report buffer.") 237 238 (defvar-local org-lint--local-checkers nil 239 "List of checkers used to build current report.") 240 241 (defun org-lint--refresh-reports () 242 (setq tabulated-list-entries 243 (org-lint--generate-reports org-lint--source-buffer 244 org-lint--local-checkers)) 245 (tabulated-list-print)) 246 247 (defun org-lint--current-line () 248 "Return current report line, as a number." 249 (string-to-number (aref (tabulated-list-get-entry) 0))) 250 251 (defun org-lint--current-marker () 252 "Return current report marker." 253 (get-text-property 0 'org-lint-marker (aref (tabulated-list-get-entry) 0))) 254 255 (defun org-lint--current-checker (&optional entry) 256 "Return current report checker. 257 When optional argument ENTRY is non-nil, use this entry instead 258 of current one." 259 (aref (if entry (nth 1 entry) (tabulated-list-get-entry)) 3)) 260 261 (defun org-lint--display-reports (source checkers) 262 "Display linting reports for buffer SOURCE. 263 CHECKERS is the list of checkers used." 264 (let ((buffer (get-buffer-create "*Org Lint*"))) 265 (with-current-buffer buffer 266 (org-lint--report-mode) 267 (setf org-lint--source-buffer source) 268 (setf org-lint--local-checkers checkers) 269 (org-lint--refresh-reports) 270 (add-hook 'tabulated-list-revert-hook #'org-lint--refresh-reports nil t)) 271 (pop-to-buffer buffer))) 272 273 (defun org-lint--jump-to-source () 274 "Move to source line that generated the report at point." 275 (interactive) 276 (let ((mk (org-lint--current-marker))) 277 (switch-to-buffer-other-window org-lint--source-buffer) 278 (unless (<= (point-min) mk (point-max)) (widen)) 279 (goto-char mk) 280 (org-fold-show-set-visibility 'local) 281 (recenter))) 282 283 (defun org-lint--show-source () 284 "Show source line that generated the report at point." 285 (interactive) 286 (let ((buffer (current-buffer))) 287 (org-lint--jump-to-source) 288 (switch-to-buffer-other-window buffer))) 289 290 (defun org-lint--hide-checker () 291 "Hide all reports from checker that generated the report at point." 292 (interactive) 293 (let ((c (org-lint--current-checker))) 294 (setf tabulated-list-entries 295 (cl-remove-if (lambda (e) (equal c (org-lint--current-checker e))) 296 tabulated-list-entries)) 297 (tabulated-list-print))) 298 299 (defun org-lint--ignore-checker () 300 "Ignore all reports from checker that generated the report at point. 301 Checker will also be ignored in all subsequent reports." 302 (interactive) 303 (setf org-lint--local-checkers 304 (remove (org-lint--current-checker) org-lint--local-checkers)) 305 (org-lint--hide-checker)) 306 307 308 ;;; Main function 309 310 ;;;###autoload 311 (defun org-lint (&optional arg) 312 "Check current Org buffer for syntax mistakes. 313 314 By default, run all checkers. With a `\\[universal-argument]' prefix ARG, \ 315 select one 316 category of checkers only. With a `\\[universal-argument] \ 317 \\[universal-argument]' prefix, run one precise 318 checker by its name. 319 320 ARG can also be a list of checker names, as symbols, to run." 321 (interactive "P") 322 (unless (derived-mode-p 'org-mode) (user-error "Not in an Org buffer")) 323 (when (called-interactively-p 'any) 324 (message "Org linting process starting...")) 325 (let ((checkers 326 (pcase arg 327 (`nil org-lint--checkers) 328 (`(4) 329 (let ((category 330 (completing-read 331 "Checker category: " 332 (mapcar #'org-lint-checker-categories org-lint--checkers) 333 nil t))) 334 (cl-remove-if-not 335 (lambda (c) 336 (assoc-string category (org-lint-checker-categories c))) 337 org-lint--checkers))) 338 (`(16) 339 (list 340 (let ((name (completing-read 341 "Checker name: " 342 (mapcar #'org-lint-checker-name org-lint--checkers) 343 nil t))) 344 (catch 'exit 345 (dolist (c org-lint--checkers) 346 (when (string= (org-lint-checker-name c) name) 347 (throw 'exit c))))))) 348 ((pred consp) 349 (cl-remove-if-not (lambda (c) (memq (org-lint-checker-name c) arg)) 350 org-lint--checkers)) 351 (_ (user-error "Invalid argument `%S' for `org-lint'" arg))))) 352 (if (not (called-interactively-p 'any)) 353 (org-lint--generate-reports (current-buffer) checkers) 354 (org-lint--display-reports (current-buffer) checkers) 355 (message "Org linting process completed")))) 356 357 358 ;;; Checker functions 359 360 (defun org-lint--collect-duplicates 361 (ast type extract-key extract-position build-message) 362 "Helper function to collect duplicates in parse tree AST. 363 364 EXTRACT-KEY is a function extracting key. It is called with 365 a single argument: the element or object. Comparison is done 366 with `equal'. 367 368 EXTRACT-POSITION is a function returning position for the report. 369 It is called with two arguments, the object or element, and the 370 key. 371 372 BUILD-MESSAGE is a function creating the report message. It is 373 called with one argument, the key used for comparison." 374 (let* (keys 375 originals 376 reports 377 (make-report 378 (lambda (position value) 379 (push (list position (funcall build-message value)) reports)))) 380 (org-element-map ast type 381 (lambda (datum) 382 (let ((key (funcall extract-key datum))) 383 (cond 384 ((not key)) 385 ((assoc key keys) (cl-pushnew (assoc key keys) originals) 386 (funcall make-report (funcall extract-position datum key) key)) 387 (t (push (cons key (funcall extract-position datum key)) keys)))))) 388 (dolist (e originals reports) (funcall make-report (cdr e) (car e))))) 389 390 (defun org-lint-misplaced-heading (ast) 391 "Check for accidentally misplaced heading lines. 392 Example: 393 ** Heading 1 394 ** Heading 2** Oops heading 3 395 ** Heading 4" 396 (org-with-point-at ast 397 (goto-char (point-min)) 398 (let (result) 399 ;; Heuristics for 2+ level heading not at bol. 400 (while (re-search-forward (rx (not (any "*\n\r ,")) ;; Not a bol; not escaped ,** heading; not " *** words" 401 "*" (1+ "*") " ") nil t) 402 ;; Limit false-positive rate by only complaining about 403 ;; ** Heading** Heading and 404 ;; ** Oops heading 405 ;; Paragraph** Oops heading 406 (when (org-element-type-p 407 (org-element-at-point) 408 '(paragraph headline)) 409 (push (list (match-beginning 0) "Possibly misplaced heading line") result))) 410 result))) 411 412 (defun org-lint-duplicate-custom-id (ast) 413 (org-lint--collect-duplicates 414 ast 415 'node-property 416 (lambda (property) 417 (and (org-string-equal-ignore-case 418 "CUSTOM_ID" (org-element-property :key property)) 419 (org-element-property :value property))) 420 (lambda (property _) (org-element-begin property)) 421 (lambda (key) (format "Duplicate CUSTOM_ID property \"%s\"" key)))) 422 423 (defun org-lint-duplicate-name (ast) 424 (org-lint--collect-duplicates 425 ast 426 org-element-all-elements 427 (lambda (datum) (org-element-property :name datum)) 428 (lambda (datum name) 429 (goto-char (org-element-begin datum)) 430 (re-search-forward 431 (format "^[ \t]*#\\+[A-Za-z]+:[ \t]*%s[ \t]*$" (regexp-quote name))) 432 (match-beginning 0)) 433 (lambda (key) (format "Duplicate NAME \"%s\"" key)))) 434 435 (defun org-lint-duplicate-target (ast) 436 (org-lint--collect-duplicates 437 ast 438 'target 439 (lambda (target) (split-string (org-element-property :value target))) 440 (lambda (target _) (org-element-begin target)) 441 (lambda (key) 442 (format "Duplicate target <<%s>>" (mapconcat #'identity key " "))))) 443 444 (defun org-lint-duplicate-footnote-definition (ast) 445 (org-lint--collect-duplicates 446 ast 447 'footnote-definition 448 (lambda (definition) (org-element-property :label definition)) 449 (lambda (definition _) (org-element-post-affiliated definition)) 450 (lambda (key) (format "Duplicate footnote definition \"%s\"" key)))) 451 452 (defun org-lint-orphaned-affiliated-keywords (ast) 453 ;; Ignore orphan RESULTS keywords, which could be generated from 454 ;; a source block returning no value. 455 (let ((keywords (cl-set-difference org-element-affiliated-keywords 456 '("RESULT" "RESULTS") 457 :test #'equal))) 458 (org-element-map ast 'keyword 459 (lambda (k) 460 (let ((key (org-element-property :key k))) 461 (and (or (let ((case-fold-search t)) 462 (string-match-p "\\`ATTR_[-_A-Za-z0-9]+\\'" key)) 463 (member key keywords)) 464 (list (org-element-post-affiliated k) 465 (format "Orphaned affiliated keyword: \"%s\"" key)))))))) 466 467 (defun org-lint-regular-keyword-before-affiliated (ast) 468 (org-element-map ast 'keyword 469 (lambda (keyword) 470 (when (= (org-element-post-blank keyword) 0) 471 (let ((next-element (org-with-point-at (org-element-end keyword) 472 (org-element-at-point)))) 473 (when (< (org-element-begin next-element) (org-element-post-affiliated next-element)) 474 ;; A keyword followed without blank lines by an element with affiliated keywords. 475 ;; The keyword may be confused with affiliated keywords. 476 (list (org-element-begin keyword) 477 (format "Independent keyword %s may be confused with affiliated keywords below" 478 (org-element-property :key keyword))))))))) 479 480 (defun org-lint-obsolete-affiliated-keywords (_) 481 (let ((regexp (format "^[ \t]*#\\+%s:" 482 (regexp-opt '("DATA" "LABEL" "RESNAME" "SOURCE" 483 "SRCNAME" "TBLNAME" "RESULT" "HEADERS") 484 t))) 485 reports) 486 (while (re-search-forward regexp nil t) 487 (let ((key (upcase (match-string-no-properties 1)))) 488 (when (< (point) 489 (org-element-post-affiliated (org-element-at-point))) 490 (push 491 (list (line-beginning-position) 492 (format 493 "Obsolete affiliated keyword: \"%s\". Use \"%s\" instead" 494 key 495 (pcase key 496 ("HEADERS" "HEADER") 497 ("RESULT" "RESULTS") 498 (_ "NAME")))) 499 reports)))) 500 reports)) 501 502 (defun org-lint-deprecated-export-blocks (ast) 503 (let ((deprecated '("ASCII" "BEAMER" "HTML" "LATEX" "MAN" "MARKDOWN" "MD" 504 "ODT" "ORG" "TEXINFO"))) 505 (org-element-map ast 'special-block 506 (lambda (b) 507 (let ((type (org-element-property :type b))) 508 (when (member-ignore-case type deprecated) 509 (list 510 (org-element-post-affiliated b) 511 (format 512 "Deprecated syntax for export block. Use \"BEGIN_EXPORT %s\" \ 513 instead" 514 type)))))))) 515 516 (defun org-lint-deprecated-header-syntax (ast) 517 (let* ((deprecated-babel-properties 518 ;; DIR is also used for attachments. 519 (delete "dir" 520 (mapcar (lambda (arg) (downcase (symbol-name (car arg)))) 521 org-babel-common-header-args-w-values))) 522 (deprecated-re 523 (format "\\`%s[ \t]" (regexp-opt deprecated-babel-properties t)))) 524 (org-element-map ast '(keyword node-property) 525 (lambda (datum) 526 (let ((key (org-element-property :key datum))) 527 (pcase (org-element-type datum) 528 (`keyword 529 (let ((value (org-element-property :value datum))) 530 (and (string= key "PROPERTY") 531 (string-match deprecated-re value) 532 (list (org-element-begin datum) 533 (format "Deprecated syntax for \"%s\". \ 534 Use header-args instead" 535 (match-string-no-properties 1 value)))))) 536 (`node-property 537 (and (member-ignore-case key deprecated-babel-properties) 538 (list 539 (org-element-begin datum) 540 (format "Deprecated syntax for \"%s\". \ 541 Use :header-args: instead" 542 key)))))))))) 543 544 (defun org-lint-missing-language-in-src-block (ast) 545 (org-element-map ast 'src-block 546 (lambda (b) 547 (unless (org-element-property :language b) 548 (list (org-element-post-affiliated b) 549 "Missing language in source block"))))) 550 551 (defun org-lint-suspicious-language-in-src-block (ast) 552 (org-element-map ast 'src-block 553 (lambda (b) 554 (when-let* ((lang (org-element-property :language b))) 555 (unless (or (functionp (intern (format "org-babel-execute:%s" lang))) 556 ;; No babel backend, but there is corresponding 557 ;; major mode. 558 (fboundp (org-src-get-lang-mode lang))) 559 (list (org-element-property :post-affiliated b) 560 (format "Unknown source block language: '%s'" lang))))))) 561 562 (defun org-lint-missing-backend-in-export-block (ast) 563 (org-element-map ast 'export-block 564 (lambda (b) 565 (unless (org-element-property :type b) 566 (list (org-element-post-affiliated b) 567 "Missing backend in export block"))))) 568 569 (defun org-lint-invalid-babel-call-block (ast) 570 (org-element-map ast 'babel-call 571 (lambda (b) 572 (cond 573 ((not (org-element-property :call b)) 574 (list (org-element-post-affiliated b) 575 "Invalid syntax in babel call block")) 576 ((let ((h (org-element-property :end-header b))) 577 (and h (string-match-p "\\`\\[.*\\]\\'" h))) 578 (list 579 (org-element-post-affiliated b) 580 "Babel call's end header must not be wrapped within brackets")))))) 581 582 (defun org-lint-deprecated-category-setup (ast) 583 (org-element-map ast 'keyword 584 (let (category-flag) 585 (lambda (k) 586 (cond 587 ((not (string= (org-element-property :key k) "CATEGORY")) nil) 588 (category-flag 589 (list (org-element-post-affiliated k) 590 "Spurious CATEGORY keyword. Set :CATEGORY: property instead")) 591 (t (setf category-flag t) nil)))))) 592 593 (defun org-lint-invalid-coderef-link (ast) 594 (let ((info (list :parse-tree ast))) 595 (org-element-map ast 'link 596 (lambda (link) 597 (let ((ref (org-element-property :path link))) 598 (and (equal (org-element-property :type link) "coderef") 599 (not (ignore-errors (org-export-resolve-coderef ref info))) 600 (list (org-element-begin link) 601 (format "Unknown coderef \"%s\"" ref)))))))) 602 603 (defun org-lint-invalid-custom-id-link (ast) 604 (let ((info (list :parse-tree ast))) 605 (org-element-map ast 'link 606 (lambda (link) 607 (and (equal (org-element-property :type link) "custom-id") 608 (not (ignore-errors (org-export-resolve-id-link link info))) 609 (list (org-element-begin link) 610 (format "Unknown custom ID \"%s\"" 611 (org-element-property :path link)))))))) 612 613 (defun org-lint-invalid-fuzzy-link (ast) 614 (let ((info (list :parse-tree ast))) 615 (org-element-map ast 'link 616 (lambda (link) 617 (and (equal (org-element-property :type link) "fuzzy") 618 (not (ignore-errors (org-export-resolve-fuzzy-link link info))) 619 (list (org-element-begin link) 620 (format "Unknown fuzzy location \"%s\"" 621 (let ((path (org-element-property :path link))) 622 (if (string-prefix-p "*" path) 623 (substring path 1) 624 path))))))))) 625 626 (defun org-lint-invalid-id-link (ast) 627 (let ((id-locations-updated nil)) 628 (org-element-map ast 'link 629 (lambda (link) 630 (let ((id (org-element-property :path link))) 631 (and (equal (org-element-property :type link) "id") 632 (progn 633 (unless id-locations-updated 634 (org-id-update-id-locations nil t) 635 (setq id-locations-updated t)) 636 t) 637 ;; The locations are up-to-date with file changes after 638 ;; the call to `org-id-update-id-locations'. We do not 639 ;; need to double-check if recorded ID is still present 640 ;; in the file. 641 (not (org-id-find-id-file id)) 642 (list (org-element-begin link) 643 (format "Unknown ID \"%s\"" id)))))))) 644 645 (defun org-lint-confusing-brackets (ast) 646 (org-element-map ast 'link 647 (lambda (link) 648 (org-with-wide-buffer 649 (when (eq (char-after (org-element-end link)) ?\]) 650 (list (org-element-begin link) 651 (format "Trailing ']' after link end"))))))) 652 653 (defun org-lint-brackets-inside-description (ast) 654 (org-element-map ast 'link 655 (lambda (link) 656 (when (org-element-contents-begin link) 657 (org-with-point-at link 658 (goto-char (org-element-contents-begin link)) 659 (let ((count 0)) 660 (while (re-search-forward (rx (or ?\] ?\[)) (org-element-contents-end link) t) 661 (if (equal (match-string 0) "[") (cl-incf count) (cl-decf count))) 662 (when (> count 0) 663 (list (org-element-begin link) 664 (format "No closing ']' matches '[' in link description: %s" 665 (buffer-substring-no-properties 666 (org-element-contents-begin link) 667 (org-element-contents-end link))))))))))) 668 669 (defun org-lint-special-property-in-properties-drawer (ast) 670 (org-element-map ast 'node-property 671 (lambda (p) 672 (let ((key (org-element-property :key p))) 673 (and (member-ignore-case key org-special-properties) 674 (list (org-element-begin p) 675 (format 676 "Special property \"%s\" found in a properties drawer" 677 key))))))) 678 679 (defun org-lint-obsolete-properties-drawer (ast) 680 (org-element-map ast 'drawer 681 (lambda (d) 682 (when (equal (org-element-property :drawer-name d) "PROPERTIES") 683 (let ((headline? (org-element-lineage d 'headline)) 684 (before 685 (mapcar #'org-element-type 686 (assq d (reverse (org-element-contents 687 (org-element-parent d))))))) 688 (list (org-element-post-affiliated d) 689 (if (or (and headline? (member before '(nil (planning)))) 690 (and (null headline?) (member before '(nil (comment))))) 691 "Incorrect contents for PROPERTIES drawer" 692 "Incorrect location for PROPERTIES drawer"))))))) 693 694 (defun org-lint-invalid-effort-property (ast) 695 (org-element-map ast 'node-property 696 (lambda (p) 697 (when (equal "EFFORT" (org-element-property :key p)) 698 (let ((value (org-element-property :value p))) 699 (and (org-string-nw-p value) 700 (not (org-duration-p value)) 701 (list (org-element-begin p) 702 (format "Invalid effort duration format: %S" value)))))))) 703 704 (defun org-lint-invalid-id-property (ast) 705 (org-element-map ast 'node-property 706 (lambda (p) 707 (when (equal "ID" (org-element-property :key p)) 708 (let ((value (org-element-property :value p))) 709 (and (org-string-nw-p value) 710 (string-match-p "::" value) 711 (list (org-element-begin p) 712 (format "IDs should not include \"::\": %S" value)))))))) 713 714 (defun org-lint-link-to-local-file (ast) 715 (org-element-map ast 'link 716 (lambda (l) 717 (let ((type (org-element-property :type l))) 718 (pcase type 719 ((or "attachment" "file") 720 (let* ((path (org-element-property :path l)) 721 (file (if (string= type "file") 722 path 723 (org-with-point-at (org-element-begin l) 724 (org-attach-expand path))))) 725 (setq file (substitute-env-in-file-name file)) 726 (and (not (file-remote-p file)) 727 (not (file-exists-p file)) 728 (list (org-element-begin l) 729 (format (if (org-element-lineage l 'link) 730 "Link to non-existent image file %S \ 731 in description" 732 "Link to non-existent local file %S") 733 file))))) 734 (_ nil)))))) 735 736 (defun org-lint-non-existent-setupfile-parameter (ast) 737 (org-element-map ast 'keyword 738 (lambda (k) 739 (when (equal (org-element-property :key k) "SETUPFILE") 740 (let ((file (org-unbracket-string 741 "\"" "\"" 742 (org-element-property :value k)))) 743 (and (not (org-url-p file)) 744 (not (file-remote-p file)) 745 (not (file-exists-p file)) 746 (list (org-element-begin k) 747 (format "Non-existent setup file %S" file)))))))) 748 749 (defun org-lint-wrong-include-link-parameter (ast) 750 (org-element-map ast 'keyword 751 (lambda (k) 752 (when (equal (org-element-property :key k) "INCLUDE") 753 (let* ((value (org-element-property :value k)) 754 (path 755 (and (string-match "^\\(\".+?\"\\|\\S-+\\)[ \t]*" value) 756 (save-match-data 757 (org-strip-quotes (match-string 1 value)))))) 758 (if (not path) 759 (list (org-element-post-affiliated k) 760 "Missing location argument in INCLUDE keyword") 761 (let* ((file (org-string-nw-p 762 (if (string-match "::\\(.*\\)\\'" path) 763 (substring path 0 (match-beginning 0)) 764 path))) 765 (search (and (not (equal file path)) 766 (org-string-nw-p (match-string 1 path))))) 767 (unless (org-url-p file) 768 (if (and file 769 (not (file-remote-p file)) 770 (not (file-exists-p file))) 771 (list (org-element-post-affiliated k) 772 "Non-existent file argument in INCLUDE keyword") 773 (let* ((visiting (if file (find-buffer-visiting file) 774 (current-buffer))) 775 (buffer (or visiting (find-file-noselect file))) 776 (org-link-search-must-match-exact-headline t)) 777 (unwind-protect 778 (with-current-buffer buffer 779 (org-with-wide-buffer 780 (when (and search 781 (not (ignore-errors 782 (org-link-search search nil t)))) 783 (list (org-element-post-affiliated k) 784 (format 785 "Invalid search part \"%s\" in INCLUDE keyword" 786 search))))) 787 (unless visiting (kill-buffer buffer))))))))))))) 788 789 (defun org-lint-obsolete-include-markup (ast) 790 (let ((regexp (format "\\`\\(?:\".+\"\\|\\S-+\\)[ \t]+%s" 791 (regexp-opt 792 '("ASCII" "BEAMER" "HTML" "LATEX" "MAN" "MARKDOWN" "MD" 793 "ODT" "ORG" "TEXINFO") 794 t)))) 795 (org-element-map ast 'keyword 796 (lambda (k) 797 (when (equal (org-element-property :key k) "INCLUDE") 798 (let ((case-fold-search t) 799 (value (org-element-property :value k))) 800 (when (string-match regexp value) 801 (let ((markup (match-string-no-properties 1 value))) 802 (list (org-element-post-affiliated k) 803 (format "Obsolete markup \"%s\" in INCLUDE keyword. \ 804 Use \"export %s\" instead" 805 markup 806 markup)))))))))) 807 808 (defun org-lint-unknown-options-item (ast) 809 (let ((allowed (delq nil 810 (append 811 (mapcar (lambda (o) (nth 2 o)) org-export-options-alist) 812 (cl-mapcan 813 (lambda (b) 814 (mapcar (lambda (o) (nth 2 o)) 815 (org-export-backend-options b))) 816 org-export-registered-backends)))) 817 reports) 818 (org-element-map ast 'keyword 819 (lambda (k) 820 (when (string= (org-element-property :key k) "OPTIONS") 821 (let ((value (org-element-property :value k)) 822 (start 0)) 823 (while (string-match "\\(.+?\\):\\((.*?)\\|\\S-+\\)?[ \t]*" 824 value 825 start) 826 (setf start (match-end 0)) 827 (let ((item (match-string 1 value))) 828 (unless (member item allowed) 829 (push (list (org-element-post-affiliated k) 830 (format "Unknown OPTIONS item \"%s\"" item)) 831 reports)) 832 (unless (match-string 2 value) 833 (push (list (org-element-post-affiliated k) 834 (format "Missing value for option item %S" item)) 835 reports)))))))) 836 reports)) 837 838 (defun org-lint-export-option-keywords (ast) 839 "Check for options keyword properties without EXPORT in AST." 840 (require 'ox) 841 (let (options reports common-options options-alist) 842 (dolist (opt org-export-options-alist) 843 (when (stringp (nth 1 opt)) 844 (cl-pushnew (nth 1 opt) common-options :test #'equal))) 845 (dolist (backend org-export-registered-backends) 846 (dolist (opt (org-export-backend-options backend)) 847 (when (stringp (nth 1 opt)) 848 (cl-pushnew (or (org-export-backend-name backend) 'anonymous) 849 (alist-get (nth 1 opt) options-alist nil nil #'equal)) 850 (cl-pushnew (nth 1 opt) options :test #'equal)))) 851 (setq options-alist (nreverse options-alist)) 852 (org-element-map ast 'node-property 853 (lambda (node) 854 (let ((prop (org-element-property :key node))) 855 (when (and (or (member prop options) (member prop common-options)) 856 (not (member prop org-default-properties))) 857 (push (list (org-element-post-affiliated node) 858 (format "Potentially misspelled %sexport option \"%s\"%s. Consider \"EXPORT_%s\"." 859 (when (member prop common-options) 860 "global ") 861 prop 862 (if-let* ((backends 863 (and (not (member prop common-options)) 864 (cdr (assoc-string prop options-alist))))) 865 (format 866 " in %S export %s" 867 (if (= 1 (length backends)) (car backends) backends) 868 (if (> (length backends) 1) "backends" "backend")) 869 "") 870 prop)) 871 reports))))) 872 reports)) 873 874 (defun org-lint-invalid-macro-argument-and-template (ast) 875 (let* ((reports nil) 876 (extract-placeholders 877 (lambda (template) 878 (let ((start 0) 879 args) 880 (while (string-match "\\$\\([1-9][0-9]*\\)" template start) 881 (setf start (match-end 0)) 882 (push (string-to-number (match-string 1 template)) args)) 883 (sort (org-uniquify args) #'<)))) 884 (check-arity 885 (lambda (arity macro) 886 (let* ((name (org-element-property :key macro)) 887 (pos (org-element-begin macro)) 888 (args (org-element-property :args macro)) 889 (l (length args))) 890 (cond 891 ((< l (1- (car arity))) 892 (push (list pos (format "Missing arguments in macro %S" name)) 893 reports)) 894 ((< l (car arity)) 895 (push (list pos (format "Missing argument in macro %S" name)) 896 reports)) 897 ((> l (1+ (cdr arity))) 898 (push (let ((spurious-args (nthcdr (cdr arity) args))) 899 (list pos 900 (format "Spurious arguments in macro %S: %s" 901 name 902 (mapconcat #'org-trim spurious-args ", ")))) 903 reports)) 904 ((> l (cdr arity)) 905 (push (list pos 906 (format "Spurious argument in macro %S: %s" 907 name 908 (org-last args))) 909 reports)) 910 (t nil)))))) 911 ;; Check arguments for macro templates. 912 (org-element-map ast 'keyword 913 (lambda (k) 914 (when (string= (org-element-property :key k) "MACRO") 915 (let* ((value (org-element-property :value k)) 916 (name (and (string-match "^\\S-+" value) 917 (match-string 0 value))) 918 (template (and name 919 (org-trim (substring value (match-end 0)))))) 920 (cond 921 ((not name) 922 (push (list (org-element-post-affiliated k) 923 "Missing name in MACRO keyword") 924 reports)) 925 ((not (org-string-nw-p template)) 926 (push (list (org-element-post-affiliated k) 927 "Missing template in macro \"%s\"" name) 928 reports)) 929 (t 930 (unless (let ((args (funcall extract-placeholders template))) 931 (equal (number-sequence 1 (or (org-last args) 0)) args)) 932 (push (list (org-element-post-affiliated k) 933 (format "Unused placeholders in macro \"%s\"" 934 name)) 935 reports)))))))) 936 ;; Check arguments for macros. 937 (org-macro-initialize-templates) 938 (let ((templates (append 939 (mapcar (lambda (m) (cons m "$1")) 940 '("author" "date" "email" "title" "results")) 941 org-macro-templates))) 942 (org-element-map ast 'macro 943 (lambda (macro) 944 (let* ((name (org-element-property :key macro)) 945 (template (cdr (assoc-string name templates t)))) 946 (pcase template 947 (`nil 948 (push (list (org-element-begin macro) 949 (format "Undefined macro %S" name)) 950 reports)) 951 ((guard (string= name "keyword")) 952 (funcall check-arity '(1 . 1) macro)) 953 ((guard (string= name "modification-time")) 954 (funcall check-arity '(1 . 2) macro)) 955 ((guard (string= name "n")) 956 (funcall check-arity '(0 . 2) macro)) 957 ((guard (string= name "property")) 958 (funcall check-arity '(1 . 2) macro)) 959 ((guard (string= name "time")) 960 (funcall check-arity '(1 . 1) macro)) 961 ((pred functionp)) ;ignore (eval ...) templates 962 (_ 963 (let* ((arg-numbers (funcall extract-placeholders template)) 964 (arity (if (null arg-numbers) 965 '(0 . 0) 966 (let ((m (apply #'max arg-numbers))) 967 (cons m m))))) 968 (funcall check-arity arity macro)))))))) 969 reports)) 970 971 (defun org-lint-undefined-footnote-reference (ast) 972 (let ((definitions 973 (org-element-map ast '(footnote-definition footnote-reference) 974 (lambda (f) 975 (and (or (org-element-type-p f 'footnote-definition) 976 (eq 'inline (org-element-property :type f))) 977 (org-element-property :label f)))))) 978 (org-element-map ast 'footnote-reference 979 (lambda (f) 980 (let ((label (org-element-property :label f))) 981 (and (eq 'standard (org-element-property :type f)) 982 (not (member label definitions)) 983 (list (org-element-begin f) 984 (format "Missing definition for footnote [%s]" 985 label)))))))) 986 987 (defun org-lint-unreferenced-footnote-definition (ast) 988 (let ((references (org-element-map ast 'footnote-reference 989 (lambda (f) (org-element-property :label f))))) 990 (org-element-map ast 'footnote-definition 991 (lambda (f) 992 (let ((label (org-element-property :label f))) 993 (and label 994 (not (member label references)) 995 (list (org-element-post-affiliated f) 996 (format "No reference for footnote definition [%s]" 997 label)))))))) 998 999 (defun org-lint-mismatched-planning-repeaters (ast) 1000 (org-element-map ast 'planning 1001 (lambda (e) 1002 (let* ((scheduled (org-element-property :scheduled e)) 1003 (deadline (org-element-property :deadline e)) 1004 (scheduled-repeater-type (org-element-property 1005 :repeater-type scheduled)) 1006 (deadline-repeater-type (org-element-property 1007 :repeater-type deadline)) 1008 (scheduled-repeater-value (org-element-property 1009 :repeater-value scheduled)) 1010 (deadline-repeater-value (org-element-property 1011 :repeater-value deadline))) 1012 (when (and scheduled deadline 1013 (memq scheduled-repeater-type '(cumulate catch-up)) 1014 (memq deadline-repeater-type '(cumulate catch-up)) 1015 (> scheduled-repeater-value 0) 1016 (> deadline-repeater-value 0) 1017 (not 1018 (and 1019 (eq scheduled-repeater-type deadline-repeater-type) 1020 (eq (org-element-property :repeater-unit scheduled) 1021 (org-element-property :repeater-unit deadline)) 1022 (eql scheduled-repeater-value deadline-repeater-value)))) 1023 (list 1024 (org-element-property :begin e) 1025 "Different repeaters in SCHEDULED and DEADLINE timestamps.")))))) 1026 1027 (defun org-lint-misplaced-planning-info (_) 1028 (let ((case-fold-search t) 1029 reports) 1030 (while (re-search-forward org-planning-line-re nil t) 1031 (unless (org-element-type-p 1032 (org-element-at-point) 1033 '(comment-block example-block export-block planning 1034 src-block verse-block)) 1035 (push (list (line-beginning-position) "Misplaced planning info line") 1036 reports))) 1037 reports)) 1038 1039 (defun org-lint-incomplete-drawer (_) 1040 (let (reports) 1041 (while (re-search-forward org-drawer-regexp nil t) 1042 (let ((name (org-trim (match-string-no-properties 0))) 1043 (element (org-element-at-point))) 1044 (pcase (org-element-type element) 1045 (`drawer 1046 ;; Find drawer opening lines within non-empty drawers. 1047 (let ((end (org-element-contents-end element))) 1048 (when end 1049 (while (re-search-forward org-drawer-regexp end t) 1050 (let ((n (org-trim (match-string-no-properties 0)))) 1051 (push (list (line-beginning-position) 1052 (format "Possible misleading drawer entry %S" n)) 1053 reports)))) 1054 (goto-char (org-element-end element)))) 1055 (`property-drawer 1056 (goto-char (org-element-end element))) 1057 ((or `comment-block `example-block `export-block `src-block 1058 `verse-block) 1059 nil) 1060 (_ 1061 ;; Find drawer opening lines outside of any drawer. 1062 (push (list (line-beginning-position) 1063 (format "Possible incomplete drawer %S" name)) 1064 reports))))) 1065 reports)) 1066 1067 (defun org-lint-indented-diary-sexp (_) 1068 (let (reports) 1069 (while (re-search-forward "^[ \t]+%%(" nil t) 1070 (unless (org-element-type-p 1071 (org-element-at-point) 1072 '(comment-block diary-sexp example-block export-block 1073 src-block verse-block)) 1074 (push (list (line-beginning-position) "Possible indented diary-sexp") 1075 reports))) 1076 reports)) 1077 1078 (defun org-lint-invalid-block (_) 1079 (let ((case-fold-search t) 1080 (regexp "^[ \t]*#\\+\\(BEGIN\\|END\\)\\(?::\\|_[^[:space:]]*\\)?[ \t]*") 1081 reports) 1082 (while (re-search-forward regexp nil t) 1083 (let ((name (org-trim (buffer-substring-no-properties 1084 (line-beginning-position) (line-end-position))))) 1085 (cond 1086 ((and (string-prefix-p "END" (match-string 1) t) 1087 (not (eolp))) 1088 (push (list (line-beginning-position) 1089 (format "Invalid block closing line \"%s\"" name)) 1090 reports)) 1091 ((not (org-element-type-p 1092 (org-element-at-point) 1093 '(center-block comment-block dynamic-block example-block 1094 export-block quote-block special-block 1095 src-block verse-block))) 1096 (push (list (line-beginning-position) 1097 (format "Possible incomplete block \"%s\"" 1098 name)) 1099 reports))))) 1100 reports)) 1101 1102 (defun org-lint-invalid-keyword-syntax (_) 1103 (let ((regexp "^[ \t]*#\\+\\([^[:space:]:]*\\)\\(?: \\|$\\)") 1104 (exception-re 1105 (format "[ \t]*#\\+%s\\(\\[.*\\]\\)?:\\(?: \\|$\\)" 1106 (regexp-opt org-element-dual-keywords))) 1107 reports) 1108 (while (re-search-forward regexp nil t) 1109 (let ((name (match-string-no-properties 1))) 1110 (unless (or (string-prefix-p "BEGIN" name t) 1111 (string-prefix-p "END" name t) 1112 (save-excursion 1113 (forward-line 0) 1114 (let ((case-fold-search t)) (looking-at exception-re)))) 1115 (push (list (match-beginning 0) 1116 (format "Possible missing colon in keyword \"%s\"" name)) 1117 reports)))) 1118 reports)) 1119 1120 (defun org-lint-invalid-image-alignment (ast) 1121 (apply 1122 #'nconc 1123 (org-element-map ast 'paragraph 1124 (lambda (p) 1125 (let ((center-re ":center[[:space:]]+\\(\\S-+\\)") 1126 (align-re ":align[[:space:]]+\\(\\S-+\\)") 1127 (keyword-string 1128 (car-safe (org-element-property :attr_org p))) 1129 reports) 1130 (when keyword-string 1131 (when (and (string-match align-re keyword-string) 1132 (not (member (match-string 1 keyword-string) 1133 '("left" "center" "right")))) 1134 (push 1135 (list (org-element-begin p) 1136 (format 1137 "\"%s\" not a supported value for #+ATTR_ORG keyword attribute \":align\"." 1138 (match-string 1 keyword-string))) 1139 reports)) 1140 (when (and (string-match center-re keyword-string) 1141 (not (equal (match-string 1 keyword-string) "t"))) 1142 (push 1143 (list (org-element-begin p) 1144 (format 1145 "\"%s\" not a supported value for #+ATTR_ORG keyword attribute \":center\"." 1146 (match-string 1 keyword-string))) 1147 reports))) 1148 reports))))) 1149 1150 (defun org-lint-extraneous-element-in-footnote-section (ast) 1151 (org-element-map ast 'headline 1152 (lambda (h) 1153 (and (org-element-property :footnote-section-p h) 1154 (org-element-map (org-element-contents h) 1155 (cl-remove-if 1156 (lambda (e) 1157 (memq e '(comment comment-block footnote-definition 1158 property-drawer section))) 1159 org-element-all-elements) 1160 (lambda (e) 1161 (not (and (org-element-type-p e 'headline) 1162 (org-element-property :commentedp e)))) 1163 nil t '(footnote-definition property-drawer)) 1164 (list (org-element-begin h) 1165 "Extraneous elements in footnote section are not exported"))))) 1166 1167 (defun org-lint-quote-section (ast) 1168 (org-element-map ast '(headline inlinetask) 1169 (lambda (h) 1170 (let ((title (org-element-property :raw-value h))) 1171 (and (or (string-prefix-p "QUOTE " title) 1172 (string-prefix-p (concat org-comment-string " QUOTE ") title)) 1173 (list (org-element-begin h) 1174 "Deprecated QUOTE section")))))) 1175 1176 (defun org-lint-file-application (ast) 1177 (org-element-map ast 'link 1178 (lambda (l) 1179 (let ((app (org-element-property :application l))) 1180 (and app 1181 (list (org-element-begin l) 1182 (format "Deprecated \"file+%s\" link type" app))))))) 1183 1184 (defun org-lint-percent-encoding-link-escape (ast) 1185 (org-element-map ast 'link 1186 (lambda (l) 1187 (when (eq 'bracket (org-element-property :format l)) 1188 (let* ((uri (org-element-property :path l)) 1189 (start 0) 1190 (obsolete-flag 1191 (catch :obsolete 1192 (while (string-match "%\\(..\\)?" uri start) 1193 (setq start (match-end 0)) 1194 (unless (member (match-string 1 uri) '("25" "5B" "5D" "20")) 1195 (throw :obsolete nil))) 1196 (string-match-p "%" uri)))) 1197 (when obsolete-flag 1198 (list (org-element-begin l) 1199 "Link escaped with obsolete percent-encoding syntax"))))))) 1200 1201 (defun org-lint-wrong-header-argument (ast) 1202 (let* ((reports) 1203 (verify 1204 (lambda (datum language headers) 1205 (let ((allowed 1206 ;; If LANGUAGE is specified, restrict allowed 1207 ;; headers to both LANGUAGE-specific and default 1208 ;; ones. Otherwise, accept headers from any loaded 1209 ;; language. 1210 (append 1211 org-babel-header-arg-names 1212 (cl-mapcan 1213 (lambda (l) 1214 (let ((v (intern (format "org-babel-header-args:%s" l)))) 1215 (and (boundp v) (mapcar #'car (symbol-value v))))) 1216 (if language (list language) 1217 (mapcar #'car org-babel-load-languages)))))) 1218 (dolist (header headers) 1219 (let ((h (symbol-name (car header))) 1220 (p (or (org-element-post-affiliated datum) 1221 (org-element-begin datum)))) 1222 (cond 1223 ((not (string-prefix-p ":" h)) 1224 (push 1225 (list p 1226 (format "Missing colon in header argument \"%s\"" h)) 1227 reports)) 1228 ((assoc-string (substring h 1) allowed)) 1229 (t (push (list p (format "Unknown header argument \"%s\"" h)) 1230 reports))))))))) 1231 (org-element-map ast '(babel-call inline-babel-call inline-src-block keyword 1232 node-property src-block) 1233 (lambda (datum) 1234 (pcase (org-element-type datum) 1235 ((or `babel-call `inline-babel-call) 1236 (funcall verify 1237 datum 1238 nil 1239 (cl-mapcan #'org-babel-parse-header-arguments 1240 (list 1241 (org-element-property :inside-header datum) 1242 (org-element-property :end-header datum))))) 1243 (`inline-src-block 1244 (funcall verify 1245 datum 1246 (org-element-property :language datum) 1247 (org-babel-parse-header-arguments 1248 (org-element-property :parameters datum)))) 1249 (`keyword 1250 (when (string= (org-element-property :key datum) "PROPERTY") 1251 (let ((value (org-element-property :value datum))) 1252 (when (or (string-match "\\`header-args\\(?::\\(\\S-+\\)\\)?\\+ *" 1253 value) 1254 (string-match "\\`header-args\\(?::\\(\\S-+\\)\\)? *" 1255 value)) 1256 (funcall verify 1257 datum 1258 (match-string 1 value) 1259 (org-babel-parse-header-arguments 1260 (substring value (match-end 0)))))))) 1261 (`node-property 1262 (let ((key (org-element-property :key datum))) 1263 (when (let ((case-fold-search t)) 1264 (or (string-match "\\`HEADER-ARGS\\(?::\\(\\S-+\\)\\)?\\+" 1265 key) 1266 (string-match "\\`HEADER-ARGS\\(?::\\(\\S-+\\)\\)?" 1267 key))) 1268 (funcall verify 1269 datum 1270 (match-string 1 key) 1271 (org-babel-parse-header-arguments 1272 (org-element-property :value datum)))))) 1273 (`src-block 1274 (funcall verify 1275 datum 1276 (org-element-property :language datum) 1277 (cl-mapcan #'org-babel-parse-header-arguments 1278 (cons (org-element-property :parameters datum) 1279 (org-element-property :header datum)))))))) 1280 reports)) 1281 1282 (defun org-lint-empty-header-argument (ast) 1283 (let* (reports) 1284 (org-element-map ast '(babel-call inline-babel-call inline-src-block src-block) 1285 (lambda (datum) 1286 (let ((headers 1287 (pcase (org-element-type datum) 1288 ((or `babel-call `inline-babel-call) 1289 (cl-mapcan 1290 (lambda (header) (org-babel-parse-header-arguments header 'no-eval)) 1291 (list 1292 (org-element-property :inside-header datum) 1293 (org-element-property :end-header datum)))) 1294 (`inline-src-block 1295 (org-babel-parse-header-arguments 1296 (org-element-property :parameters datum) 1297 'no-eval)) 1298 (`src-block 1299 (cl-mapcan 1300 (lambda (header) (org-babel-parse-header-arguments header 'no-eval)) 1301 (cons (org-element-property :parameters datum) 1302 (org-element-property :header datum))))))) 1303 (dolist (header headers) 1304 (when (not (cdr header)) 1305 (push 1306 (list 1307 (or (org-element-post-affiliated datum) 1308 (org-element-begin datum)) 1309 (format "Empty value in header argument \"%s\"" (symbol-name (car header)))) 1310 reports)))))) 1311 reports)) 1312 1313 (defun org-lint-wrong-header-value (ast) 1314 (let (reports) 1315 (org-element-map ast 1316 '(babel-call inline-babel-call inline-src-block src-block) 1317 (lambda (datum) 1318 (let* ((type (org-element-type datum)) 1319 (language (org-element-property :language datum)) 1320 (allowed-header-values 1321 (append (and language 1322 (let ((v (intern (concat "org-babel-header-args:" 1323 language)))) 1324 (and (boundp v) (symbol-value v)))) 1325 org-babel-common-header-args-w-values)) 1326 (datum-header-values 1327 (org-babel-parse-header-arguments 1328 (org-trim 1329 (pcase type 1330 (`src-block 1331 (mapconcat 1332 #'identity 1333 (cons (org-element-property :parameters datum) 1334 (org-element-property :header datum)) 1335 " ")) 1336 (`inline-src-block 1337 (or (org-element-property :parameters datum) "")) 1338 (_ 1339 (concat 1340 (org-element-property :inside-header datum) 1341 " " 1342 (org-element-property :end-header datum)))))))) 1343 (dolist (header datum-header-values) 1344 (let ((allowed-values 1345 (cdr (assoc-string (substring (symbol-name (car header)) 1) 1346 allowed-header-values)))) 1347 (unless (memq allowed-values '(:any nil)) 1348 (let ((values (cdr header)) 1349 groups-alist) 1350 (dolist (v (if (stringp values) (split-string values) 1351 (list values))) 1352 (let ((valid-value nil)) 1353 (catch 'exit 1354 (dolist (group allowed-values) 1355 (cond 1356 ((not (funcall 1357 (if (stringp v) #'assoc-string #'assoc) 1358 v group)) 1359 (when (memq :any group) 1360 (setf valid-value t) 1361 (push (cons group v) groups-alist))) 1362 ((assq group groups-alist) 1363 (push 1364 (list 1365 (or (org-element-post-affiliated datum) 1366 (org-element-begin datum)) 1367 (format 1368 "Forbidden combination in header \"%s\": %s, %s" 1369 (car header) 1370 (cdr (assq group groups-alist)) 1371 v)) 1372 reports) 1373 (throw 'exit nil)) 1374 (t (push (cons group v) groups-alist) 1375 (setf valid-value t)))) 1376 (unless valid-value 1377 (push 1378 (list 1379 (or (org-element-post-affiliated datum) 1380 (org-element-begin datum)) 1381 (format "Unknown value \"%s\" for header \"%s\"" 1382 v 1383 (car header))) 1384 reports)))))))))))) 1385 reports)) 1386 1387 (defun org-lint-named-result (ast) 1388 (org-element-map ast org-element-all-elements 1389 (lambda (el) 1390 (when-let* ((result (org-element-property :results el)) 1391 (result-name (org-element-property :name el)) 1392 (origin-block 1393 (if (org-string-nw-p (car result)) 1394 (condition-case _ 1395 (org-export-resolve-link (car result) `(:parse-tree ,ast)) 1396 (org-link-broken nil)) 1397 (org-export-get-previous-element el nil)))) 1398 (when (org-element-type-p origin-block 'src-block) 1399 (list (org-element-begin el) 1400 (format "Links to \"%s\" will not be valid during export unless the parent source block has :exports results or both" result-name))))))) 1401 1402 (defun org-lint-spurious-colons (ast) 1403 (org-element-map ast '(headline inlinetask) 1404 (lambda (h) 1405 (when (member "" (org-element-property :tags h)) 1406 (list (org-element-begin h) 1407 "Tags contain a spurious colon"))))) 1408 1409 (defun org-lint-non-existent-bibliography (ast) 1410 (org-element-map ast 'keyword 1411 (lambda (k) 1412 (when (equal "BIBLIOGRAPHY" (org-element-property :key k)) 1413 (let ((file (org-strip-quotes (org-element-property :value k)))) 1414 (and (not (file-remote-p file)) 1415 (not (file-exists-p file)) 1416 (list (org-element-begin k) 1417 (format "Non-existent bibliography %S" file)))))))) 1418 1419 (defun org-lint-missing-print-bibliography (ast) 1420 (and (org-element-map ast 'citation #'identity nil t) 1421 (not (org-element-map ast 'keyword 1422 (lambda (k) 1423 (equal "PRINT_BIBLIOGRAPHY" (org-element-property :key k))) 1424 nil t)) 1425 (list 1426 (list (point-max) "Possibly missing \"PRINT_BIBLIOGRAPHY\" keyword")))) 1427 1428 (defun org-lint-invalid-cite-export-declaration (ast) 1429 (org-element-map ast 'keyword 1430 (lambda (k) 1431 (when (equal "CITE_EXPORT" (org-element-property :key k)) 1432 (let ((value (org-element-property :value k)) 1433 (source (org-element-begin k))) 1434 (if (equal value "") 1435 (list source "Missing export processor name") 1436 (condition-case _ 1437 (pcase (org-cite-read-processor-declaration value) 1438 (`(,(and (pred symbolp) name) 1439 ,(pred string-or-null-p) 1440 ,(pred string-or-null-p)) 1441 (unless (or (org-cite-get-processor name) 1442 (progn 1443 (org-cite-try-load-processor name) 1444 (org-cite-get-processor name))) 1445 (list source (format "Unknown cite export processor %S" name)))) 1446 (_ 1447 (list source "Invalid cite export processor declaration"))) 1448 (error 1449 (list source "Invalid cite export processor declaration"))))))))) 1450 1451 (defun org-lint-incomplete-citation (ast) 1452 (org-element-map ast 'plain-text 1453 (lambda (text) 1454 (and (string-match-p org-element-citation-prefix-re text) 1455 ;; XXX: The code below signals the error at the beginning 1456 ;; of the paragraph containing the faulty object. It is 1457 ;; not very accurate but may be enough for now. 1458 (list (org-element-contents-begin 1459 (org-element-parent text)) 1460 "Possibly incomplete citation markup"))))) 1461 1462 (defun org-lint-item-number (ast) 1463 (org-element-map ast 'item 1464 (lambda (item) 1465 (unless (org-element-property :counter item) 1466 (when-let* ((bullet (org-element-property :bullet item)) 1467 (bullet-number 1468 (cond 1469 ((string-match "[A-Za-z]" bullet) 1470 (- (string-to-char (upcase (match-string 0 bullet))) 1471 64)) 1472 ((string-match "[0-9]+" bullet) 1473 (string-to-number (match-string 0 bullet))))) 1474 (true-number 1475 (org-list-get-item-number 1476 (org-element-begin item) 1477 (org-element-property :structure item) 1478 (org-list-prevs-alist (org-element-property :structure item)) 1479 (org-list-parents-alist (org-element-property :structure item))))) 1480 (unless (equal bullet-number (car (last true-number))) 1481 (list 1482 (org-element-begin item) 1483 (format "Bullet counter \"%s\" is not the same with item position %d. Consider adding manual [@%d] counter." 1484 bullet (car (last true-number)) bullet-number)))))))) 1485 1486 (defun org-lint-LaTeX-$ (ast) 1487 "Report semi-obsolete $...$ LaTeX fragments. 1488 AST is the buffer parse tree." 1489 (org-element-map ast 'latex-fragment 1490 (lambda (fragment) 1491 (and (string-match-p "^[$][^$]" (org-element-property :value fragment)) 1492 (list (org-element-begin fragment) 1493 "Potentially confusing LaTeX fragment format. Prefer using more reliable \\(...\\)"))))) 1494 (defun org-lint-LaTeX-$-ambiguous (_) 1495 "Report LaTeX fragment-like text. 1496 AST is the buffer parse tree." 1497 (org-with-wide-buffer 1498 (let ((ambiguous-latex-re (rx "$." digit)) 1499 report context) 1500 (while (re-search-forward ambiguous-latex-re nil t) 1501 (setq context (org-element-context)) 1502 (when (or (eq 'latex-fragment (org-element-type context)) 1503 (memq 'latex-fragment (org-element-restriction context))) 1504 (push 1505 (list 1506 (point) 1507 "$ symbol potentially matching LaTeX fragment boundary. Consider using \\dollar entity.") 1508 report))) 1509 report))) 1510 (defun org-lint-timestamp-syntax (ast) 1511 "Report malformed timestamps. 1512 AST is the buffer parse tree." 1513 (org-element-map ast 'timestamp 1514 (lambda (timestamp) 1515 (let ((expected (org-element-interpret-data timestamp)) 1516 (actual (buffer-substring-no-properties 1517 (org-element-property :begin timestamp) 1518 (org-element-property :end timestamp)))) 1519 (unless (equal expected actual) 1520 (list (org-element-property :begin timestamp) 1521 (format "Potentially malformed timestamp %s. Parsed as: %s" actual expected))))))) 1522 (defun org-lint-inactive-planning (ast) 1523 "Report inactive timestamp in SCHEDULED/DEADLINE. 1524 AST is the buffer parse tree." 1525 (org-element-map ast 'planning 1526 (lambda (planning) 1527 (let ((scheduled (org-element-property :scheduled planning)) 1528 (deadline (org-element-property :deadline planning))) 1529 (cond 1530 ((memq (org-element-property :type scheduled) '(inactive inactive-range)) 1531 (list (org-element-begin planning) "Inactive timestamp in SCHEDULED will not appear in agenda.")) 1532 ((memq (org-element-property :type deadline) '(inactive inactive-range)) 1533 (list (org-element-begin planning) "Inactive timestamp in DEADLINE will not appear in agenda.")) 1534 (t nil)))))) 1535 1536 (defvar org-beamer-frame-environment) ; defined in ox-beamer.el 1537 (defun org-lint-beamer-frame (ast) 1538 "Check for occurrences of begin or end frame." 1539 (require 'ox-beamer) 1540 (org-with-point-at ast 1541 (goto-char (point-min)) 1542 (let (result) 1543 (while (re-search-forward 1544 (concat "\\\\\\(begin\\|end\\){" org-beamer-frame-environment "}") nil t) 1545 (push (list (match-beginning 0) "Beamer frame name may cause error when exporting. Consider customizing `org-beamer-frame-environment'.") result)) 1546 result))) 1547 1548 1549 ;;; Checkers declaration 1550 1551 (org-lint-add-checker 'misplaced-heading 1552 "Report accidentally misplaced heading lines." 1553 #'org-lint-misplaced-heading :trust 'low) 1554 1555 (org-lint-add-checker 'duplicate-custom-id 1556 "Report duplicates CUSTOM_ID properties" 1557 #'org-lint-duplicate-custom-id 1558 :categories '(link)) 1559 1560 (org-lint-add-checker 'duplicate-name 1561 "Report duplicate NAME values" 1562 #'org-lint-duplicate-name 1563 :categories '(babel 'link)) 1564 1565 (org-lint-add-checker 'duplicate-target 1566 "Report duplicate targets" 1567 #'org-lint-duplicate-target 1568 :categories '(link)) 1569 1570 (org-lint-add-checker 'duplicate-footnote-definition 1571 "Report duplicate footnote definitions" 1572 #'org-lint-duplicate-footnote-definition 1573 :categories '(footnote)) 1574 1575 (org-lint-add-checker 'orphaned-affiliated-keywords 1576 "Report orphaned affiliated keywords" 1577 #'org-lint-orphaned-affiliated-keywords 1578 :trust 'low) 1579 1580 (org-lint-add-checker 'combining-keywords-with-affiliated 1581 "Report independent keywords preceding affiliated keywords." 1582 #'org-lint-regular-keyword-before-affiliated 1583 :trust 'low) 1584 1585 (org-lint-add-checker 'obsolete-affiliated-keywords 1586 "Report obsolete affiliated keywords" 1587 #'org-lint-obsolete-affiliated-keywords 1588 :categories '(obsolete)) 1589 1590 (org-lint-add-checker 'deprecated-export-blocks 1591 "Report deprecated export block syntax" 1592 #'org-lint-deprecated-export-blocks 1593 :trust 'low :categories '(obsolete export)) 1594 1595 (org-lint-add-checker 'deprecated-header-syntax 1596 "Report deprecated Babel header syntax" 1597 #'org-lint-deprecated-header-syntax 1598 :trust 'low :categories '(obsolete babel)) 1599 1600 (org-lint-add-checker 'missing-language-in-src-block 1601 "Report missing language in source blocks" 1602 #'org-lint-missing-language-in-src-block 1603 :categories '(babel)) 1604 1605 (org-lint-add-checker 'suspicious-language-in-src-block 1606 "Report suspicious language in source blocks" 1607 #'org-lint-suspicious-language-in-src-block 1608 :trust 'low :categories '(babel)) 1609 1610 (org-lint-add-checker 'missing-backend-in-export-block 1611 "Report missing backend in export blocks" 1612 #'org-lint-missing-backend-in-export-block 1613 :categories '(export)) 1614 1615 (org-lint-add-checker 'invalid-babel-call-block 1616 "Report invalid Babel call blocks" 1617 #'org-lint-invalid-babel-call-block 1618 :categories '(babel)) 1619 1620 (org-lint-add-checker 'wrong-header-argument 1621 "Report wrong babel headers" 1622 #'org-lint-wrong-header-argument 1623 :categories '(babel)) 1624 1625 (org-lint-add-checker 'wrong-header-value 1626 "Report invalid value in babel headers" 1627 #'org-lint-wrong-header-value 1628 :categories '(babel) :trust 'low) 1629 1630 (org-lint-add-checker 'named-result 1631 "Report results evaluation with #+name keyword." 1632 #'org-lint-named-result 1633 :categories '(babel) :trust 'high) 1634 1635 (org-lint-add-checker 'empty-header-argument 1636 "Report empty values in babel headers" 1637 #'org-lint-empty-header-argument 1638 :categories '(babel) :trust 'low) 1639 1640 (org-lint-add-checker 'deprecated-category-setup 1641 "Report misuse of CATEGORY keyword" 1642 #'org-lint-deprecated-category-setup 1643 :categories '(obsolete)) 1644 1645 (org-lint-add-checker 'invalid-coderef-link 1646 "Report \"coderef\" links with unknown destination" 1647 #'org-lint-invalid-coderef-link 1648 :categories '(link)) 1649 1650 (org-lint-add-checker 'invalid-custom-id-link 1651 "Report \"custom-id\" links with unknown destination" 1652 #'org-lint-invalid-custom-id-link 1653 :categories '(link)) 1654 1655 (org-lint-add-checker 'invalid-fuzzy-link 1656 "Report \"fuzzy\" links with unknown destination" 1657 #'org-lint-invalid-fuzzy-link 1658 :categories '(link)) 1659 1660 (org-lint-add-checker 'invalid-id-link 1661 "Report \"id\" links with unknown destination" 1662 #'org-lint-invalid-id-link 1663 :categories '(link)) 1664 1665 (org-lint-add-checker 'trailing-bracket-after-link 1666 "Report potentially confused trailing ']' after link." 1667 #'org-lint-confusing-brackets 1668 :categories '(link) :trust 'low) 1669 1670 (org-lint-add-checker 'unclosed-brackets-in-link-description 1671 "Report potentially confused trailing ']' after link." 1672 #'org-lint-brackets-inside-description 1673 :categories '(link) :trust 'low) 1674 1675 (org-lint-add-checker 'link-to-local-file 1676 "Report links to non-existent local files" 1677 #'org-lint-link-to-local-file 1678 :categories '(link) :trust 'low) 1679 1680 (org-lint-add-checker 'non-existent-setupfile-parameter 1681 "Report SETUPFILE keywords with non-existent file parameter" 1682 #'org-lint-non-existent-setupfile-parameter 1683 :trust 'low) 1684 1685 (org-lint-add-checker 'wrong-include-link-parameter 1686 "Report INCLUDE keywords with misleading link parameter" 1687 #'org-lint-wrong-include-link-parameter 1688 :categories '(export) :trust 'low) 1689 1690 (org-lint-add-checker 'obsolete-include-markup 1691 "Report obsolete markup in INCLUDE keyword" 1692 #'org-lint-obsolete-include-markup 1693 :categories '(obsolete export) :trust 'low) 1694 1695 (org-lint-add-checker 'unknown-options-item 1696 "Report unknown items in OPTIONS keyword" 1697 #'org-lint-unknown-options-item 1698 :categories '(export) :trust 'low) 1699 1700 (org-lint-add-checker 'misspelled-export-option 1701 "Report potentially misspelled export options in properties." 1702 #'org-lint-export-option-keywords 1703 :categories '(export) :trust 'low) 1704 1705 (org-lint-add-checker 'invalid-macro-argument-and-template 1706 "Report spurious macro arguments or invalid macro templates" 1707 #'org-lint-invalid-macro-argument-and-template 1708 :categories '(export) :trust 'low) 1709 1710 (org-lint-add-checker 'special-property-in-properties-drawer 1711 "Report special properties in properties drawers" 1712 #'org-lint-special-property-in-properties-drawer 1713 :categories '(properties)) 1714 1715 (org-lint-add-checker 'obsolete-properties-drawer 1716 "Report obsolete syntax for properties drawers" 1717 #'org-lint-obsolete-properties-drawer 1718 :categories '(obsolete properties)) 1719 1720 (org-lint-add-checker 'invalid-effort-property 1721 "Report invalid duration in EFFORT property" 1722 #'org-lint-invalid-effort-property 1723 :categories '(properties)) 1724 1725 (org-lint-add-checker 'invalid-id-property 1726 "Report search string delimiter \"::\" in ID property" 1727 #'org-lint-invalid-id-property 1728 :categories '(properties)) 1729 1730 (org-lint-add-checker 'undefined-footnote-reference 1731 "Report missing definition for footnote references" 1732 #'org-lint-undefined-footnote-reference 1733 :categories '(footnote)) 1734 1735 (org-lint-add-checker 'unreferenced-footnote-definition 1736 "Report missing reference for footnote definitions" 1737 #'org-lint-unreferenced-footnote-definition 1738 :categories '(footnote)) 1739 1740 (org-lint-add-checker 'extraneous-element-in-footnote-section 1741 "Report non-footnote definitions in footnote section" 1742 #'org-lint-extraneous-element-in-footnote-section 1743 :categories '(footnote)) 1744 1745 (org-lint-add-checker 'invalid-keyword-syntax 1746 "Report probable invalid keywords" 1747 #'org-lint-invalid-keyword-syntax 1748 :trust 'low) 1749 1750 (org-lint-add-checker 'invalid-image-alignment 1751 "Report unsupported align attribute for keyword" 1752 #'org-lint-invalid-image-alignment 1753 :trust 'high) 1754 1755 (org-lint-add-checker 'invalid-block 1756 "Report invalid blocks" 1757 #'org-lint-invalid-block 1758 :trust 'low) 1759 1760 (org-lint-add-checker 'mismatched-planning-repeaters 1761 "Report mismatched repeaters in planning info line" 1762 #'org-lint-mismatched-planning-repeaters 1763 :trust 'low) 1764 1765 (org-lint-add-checker 'misplaced-planning-info 1766 "Report misplaced planning info line" 1767 #'org-lint-misplaced-planning-info 1768 :trust 'low) 1769 1770 (org-lint-add-checker 'incomplete-drawer 1771 "Report probable incomplete drawers" 1772 #'org-lint-incomplete-drawer 1773 :trust 'low) 1774 1775 (org-lint-add-checker 'indented-diary-sexp 1776 "Report probable indented diary-sexps" 1777 #'org-lint-indented-diary-sexp 1778 :trust 'low) 1779 1780 (org-lint-add-checker 'quote-section 1781 "Report obsolete QUOTE section" 1782 #'org-lint-quote-section 1783 :categories '(obsolete) :trust 'low) 1784 1785 (org-lint-add-checker 'file-application 1786 "Report obsolete \"file+application\" link" 1787 #'org-lint-file-application 1788 :categories '(link obsolete)) 1789 1790 (org-lint-add-checker 'percent-encoding-link-escape 1791 "Report obsolete escape syntax in links" 1792 #'org-lint-percent-encoding-link-escape 1793 :categories '(link obsolete) :trust 'low) 1794 1795 (org-lint-add-checker 'spurious-colons 1796 "Report spurious colons in tags" 1797 #'org-lint-spurious-colons 1798 :categories '(tags)) 1799 1800 (org-lint-add-checker 'non-existent-bibliography 1801 "Report invalid bibliography file" 1802 #'org-lint-non-existent-bibliography 1803 :categories '(cite)) 1804 1805 (org-lint-add-checker 'missing-print-bibliography 1806 "Report missing \"print_bibliography\" keyword" 1807 #'org-lint-missing-print-bibliography 1808 :categories '(cite)) 1809 1810 (org-lint-add-checker 'invalid-cite-export-declaration 1811 "Report invalid value for \"cite_export\" keyword" 1812 #'org-lint-invalid-cite-export-declaration 1813 :categories '(cite)) 1814 1815 (org-lint-add-checker 'incomplete-citation 1816 "Report incomplete citation object" 1817 #'org-lint-incomplete-citation 1818 :categories '(cite) :trust 'low) 1819 1820 (org-lint-add-checker 'item-number 1821 "Report inconsistent item numbers in lists" 1822 #'org-lint-item-number 1823 :categories '(plain-list)) 1824 1825 (org-lint-add-checker 'LaTeX-$ 1826 "Report potentially confusing $...$ LaTeX markup." 1827 #'org-lint-LaTeX-$ 1828 :categories '(markup)) 1829 (org-lint-add-checker 'LaTeX-$ 1830 "Report $ that might be treated as LaTeX fragment boundary." 1831 #'org-lint-LaTeX-$-ambiguous 1832 :categories '(markup) :trust 'low) 1833 (org-lint-add-checker 'beamer-frame 1834 "Report that frame text contains beamer frame environment." 1835 #'org-lint-beamer-frame 1836 :categories '(export) :trust 'low) 1837 (org-lint-add-checker 'timestamp-syntax 1838 "Report malformed timestamps." 1839 #'org-lint-timestamp-syntax 1840 :categories '(timestamp) :trust 'low) 1841 (org-lint-add-checker 'planning-inactive 1842 "Report inactive timestamps in SCHEDULED/DEADLINE." 1843 #'org-lint-inactive-planning 1844 :categories '(timestamp) :trust 'high) 1845 1846 (provide 'org-lint) 1847 1848 ;; Local variables: 1849 ;; generated-autoload-file: "org-loaddefs.el" 1850 ;; End: 1851 1852 ;;; org-lint.el ends here