org-lint.el 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452
  1. ;;; org-lint.el --- Linting for Org documents -*- lexical-binding: t; -*-
  2. ;; Copyright (C) 2015-2022 Free Software Foundation, Inc.
  3. ;; Author: Nicolas Goaziou <mail@nicolasgoaziou.fr>
  4. ;; Keywords: outlines, hypermedia, calendar, wp
  5. ;; This file is part of GNU Emacs.
  6. ;; GNU Emacs is free software; you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; GNU Emacs is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;; This library implements linting for Org syntax. The process is
  18. ;; started by calling `org-lint' command, which see.
  19. ;; New checkers are added by `org-lint-add-checker' function.
  20. ;; Internally, all checks are listed in `org-lint--checkers'.
  21. ;; Results are displayed in a special "*Org Lint*" buffer with
  22. ;; a dedicated major mode, derived from `tabulated-list-mode'.
  23. ;; In addition to the usual key-bindings inherited from it, "C-j" and
  24. ;; "TAB" display problematic line reported under point whereas "RET"
  25. ;; jumps to it. Also, "h" hides all reports similar to the current
  26. ;; one. Additionally, "i" removes them from subsequent reports.
  27. ;; Checks currently implemented report the following:
  28. ;; - duplicates CUSTOM_ID properties,
  29. ;; - duplicate NAME values,
  30. ;; - duplicate targets,
  31. ;; - duplicate footnote definitions,
  32. ;; - orphaned affiliated keywords,
  33. ;; - obsolete affiliated keywords,
  34. ;; - deprecated export block syntax,
  35. ;; - deprecated Babel header syntax,
  36. ;; - missing language in source blocks,
  37. ;; - missing back-end in export blocks,
  38. ;; - invalid Babel call blocks,
  39. ;; - NAME values with a colon,
  40. ;; - wrong babel headers,
  41. ;; - invalid value in babel headers,
  42. ;; - misuse of CATEGORY keyword,
  43. ;; - "coderef" links with unknown destination,
  44. ;; - "custom-id" links with unknown destination,
  45. ;; - "fuzzy" links with unknown destination,
  46. ;; - "id" links with unknown destination,
  47. ;; - links to non-existent local files,
  48. ;; - SETUPFILE keywords with non-existent file parameter,
  49. ;; - INCLUDE keywords with misleading link parameter,
  50. ;; - obsolete markup in INCLUDE keyword,
  51. ;; - unknown items in OPTIONS keyword,
  52. ;; - spurious macro arguments or invalid macro templates,
  53. ;; - special properties in properties drawers,
  54. ;; - obsolete syntax for properties drawers,
  55. ;; - invalid duration in EFFORT property,
  56. ;; - missing definition for footnote references,
  57. ;; - missing reference for footnote definitions,
  58. ;; - non-footnote definitions in footnote section,
  59. ;; - probable invalid keywords,
  60. ;; - invalid blocks,
  61. ;; - misplaced planning info line,
  62. ;; - probable incomplete drawers,
  63. ;; - probable indented diary-sexps,
  64. ;; - obsolete QUOTE section,
  65. ;; - obsolete "file+application" link,
  66. ;; - obsolete escape syntax in links,
  67. ;; - spurious colons in tags,
  68. ;; - invalid bibliography file,
  69. ;; - missing "print_bibliography" keyword,
  70. ;; - invalid value for "cite_export" keyword,
  71. ;; - incomplete citation object.
  72. ;;; Code:
  73. (require 'cl-lib)
  74. (require 'ob)
  75. (require 'oc)
  76. (require 'ol)
  77. (require 'org-attach)
  78. (require 'org-macro)
  79. (require 'ox)
  80. (require 'seq)
  81. ;;; Checkers structure
  82. (cl-defstruct (org-lint-checker (:copier nil))
  83. name summary function trust categories)
  84. (defvar org-lint--checkers nil
  85. "List of all available checkers.
  86. This list is populated by `org-lint-add-checker' function.")
  87. ;;;###autoload
  88. (defun org-lint-add-checker (name summary fun &rest props)
  89. "Add a new checker for linter.
  90. NAME is a unique check identifier, as a non-nil symbol. SUMMARY
  91. is a short description of the check, as a string.
  92. The check is done calling the function FUN with one mandatory
  93. argument, the parse tree describing the current Org buffer. Such
  94. function calls are wrapped within a `save-excursion' and point is
  95. always at `point-min'. Its return value has to be an
  96. alist (POSITION MESSAGE) where POSITION refer to the buffer
  97. position of the error, as an integer, and MESSAGE is a one-line
  98. string describing the error.
  99. Optional argument PROPS provides additional information about the
  100. checker. Currently, two properties are supported:
  101. `:categories'
  102. Categories relative to the check, as a list of symbol. They
  103. are used for filtering when calling `org-lint'. Checkers
  104. not explicitly associated to a category are collected in the
  105. `default' one.
  106. `:trust'
  107. The trust level one can have in the check. It is either
  108. `low' or `high', depending on the heuristics implemented and
  109. the nature of the check. This has an indicative value only
  110. and is displayed along reports."
  111. (declare (indent 1))
  112. ;; Sanity checks.
  113. (pcase name
  114. (`nil (error "Name field is mandatory for checkers"))
  115. ((pred symbolp) nil)
  116. (_ (error "Invalid type for name field")))
  117. (unless (functionp fun)
  118. (error "Checker field is expected to be a valid function"))
  119. ;; Install checker in `org-lint--checkers'; uniquify by name.
  120. (setq org-lint--checkers
  121. (cons (apply #'make-org-lint-checker
  122. :name name
  123. :summary summary
  124. :function fun
  125. props)
  126. (seq-remove (lambda (c) (eq name (org-lint-checker-name c)))
  127. org-lint--checkers))))
  128. ;;; Reports UI
  129. (defvar org-lint--report-mode-map
  130. (let ((map (make-sparse-keymap)))
  131. (set-keymap-parent map tabulated-list-mode-map)
  132. (define-key map (kbd "RET") 'org-lint--jump-to-source)
  133. (define-key map (kbd "TAB") 'org-lint--show-source)
  134. (define-key map (kbd "C-j") 'org-lint--show-source)
  135. (define-key map (kbd "h") 'org-lint--hide-checker)
  136. (define-key map (kbd "i") 'org-lint--ignore-checker)
  137. map)
  138. "Local keymap for `org-lint--report-mode' buffers.")
  139. (define-derived-mode org-lint--report-mode tabulated-list-mode "OrgLint"
  140. "Major mode used to display reports emitted during linting.
  141. \\{org-lint--report-mode-map}"
  142. (setf tabulated-list-format
  143. `[("Line" 6
  144. (lambda (a b)
  145. (< (string-to-number (aref (cadr a) 0))
  146. (string-to-number (aref (cadr b) 0))))
  147. :right-align t)
  148. ("Trust" 5 t)
  149. ("Warning" 0 t)])
  150. (tabulated-list-init-header))
  151. (defun org-lint--generate-reports (buffer checkers)
  152. "Generate linting report for BUFFER.
  153. CHECKERS is the list of checkers used.
  154. Return an alist (ID [LINE TRUST DESCRIPTION CHECKER]), suitable
  155. for `tabulated-list-printer'."
  156. (with-current-buffer buffer
  157. (save-excursion
  158. (goto-char (point-min))
  159. (let ((ast (org-element-parse-buffer))
  160. (id 0)
  161. (last-line 1)
  162. (last-pos 1))
  163. ;; Insert unique ID for each report. Replace buffer positions
  164. ;; with line numbers.
  165. (mapcar
  166. (lambda (report)
  167. (list
  168. (cl-incf id)
  169. (apply #'vector
  170. (cons
  171. (progn
  172. (goto-char (car report))
  173. (beginning-of-line)
  174. (prog1 (number-to-string
  175. (cl-incf last-line
  176. (count-lines last-pos (point))))
  177. (setf last-pos (point))))
  178. (cdr report)))))
  179. ;; Insert trust level in generated reports. Also sort them
  180. ;; by buffer position in order to optimize lines computation.
  181. (sort (cl-mapcan
  182. (lambda (c)
  183. (let ((trust (symbol-name (org-lint-checker-trust c))))
  184. (mapcar
  185. (lambda (report)
  186. (list (car report) trust (nth 1 report) c))
  187. (save-excursion
  188. (funcall (org-lint-checker-function c)
  189. ast)))))
  190. checkers)
  191. #'car-less-than-car))))))
  192. (defvar-local org-lint--source-buffer nil
  193. "Source buffer associated to current report buffer.")
  194. (defvar-local org-lint--local-checkers nil
  195. "List of checkers used to build current report.")
  196. (defun org-lint--refresh-reports ()
  197. (setq tabulated-list-entries
  198. (org-lint--generate-reports org-lint--source-buffer
  199. org-lint--local-checkers))
  200. (tabulated-list-print))
  201. (defun org-lint--current-line ()
  202. "Return current report line, as a number."
  203. (string-to-number (aref (tabulated-list-get-entry) 0)))
  204. (defun org-lint--current-checker (&optional entry)
  205. "Return current report checker.
  206. When optional argument ENTRY is non-nil, use this entry instead
  207. of current one."
  208. (aref (if entry (nth 1 entry) (tabulated-list-get-entry)) 3))
  209. (defun org-lint--display-reports (source checkers)
  210. "Display linting reports for buffer SOURCE.
  211. CHECKERS is the list of checkers used."
  212. (let ((buffer (get-buffer-create "*Org Lint*")))
  213. (with-current-buffer buffer
  214. (org-lint--report-mode)
  215. (setf org-lint--source-buffer source)
  216. (setf org-lint--local-checkers checkers)
  217. (org-lint--refresh-reports)
  218. (add-hook 'tabulated-list-revert-hook #'org-lint--refresh-reports nil t))
  219. (pop-to-buffer buffer)))
  220. (defun org-lint--jump-to-source ()
  221. "Move to source line that generated the report at point."
  222. (interactive)
  223. (let ((l (org-lint--current-line)))
  224. (switch-to-buffer-other-window org-lint--source-buffer)
  225. (org-goto-line l)
  226. (org-show-set-visibility 'local)
  227. (recenter)))
  228. (defun org-lint--show-source ()
  229. "Show source line that generated the report at point."
  230. (interactive)
  231. (let ((buffer (current-buffer)))
  232. (org-lint--jump-to-source)
  233. (switch-to-buffer-other-window buffer)))
  234. (defun org-lint--hide-checker ()
  235. "Hide all reports from checker that generated the report at point."
  236. (interactive)
  237. (let ((c (org-lint--current-checker)))
  238. (setf tabulated-list-entries
  239. (cl-remove-if (lambda (e) (equal c (org-lint--current-checker e)))
  240. tabulated-list-entries))
  241. (tabulated-list-print)))
  242. (defun org-lint--ignore-checker ()
  243. "Ignore all reports from checker that generated the report at point.
  244. Checker will also be ignored in all subsequent reports."
  245. (interactive)
  246. (setf org-lint--local-checkers
  247. (remove (org-lint--current-checker) org-lint--local-checkers))
  248. (org-lint--hide-checker))
  249. ;;; Main function
  250. ;;;###autoload
  251. (defun org-lint (&optional arg)
  252. "Check current Org buffer for syntax mistakes.
  253. By default, run all checkers. With a `\\[universal-argument]' prefix ARG, \
  254. select one
  255. category of checkers only. With a `\\[universal-argument] \
  256. \\[universal-argument]' prefix, run one precise
  257. checker by its name.
  258. ARG can also be a list of checker names, as symbols, to run."
  259. (interactive "P")
  260. (unless (derived-mode-p 'org-mode) (user-error "Not in an Org buffer"))
  261. (when (called-interactively-p 'any)
  262. (message "Org linting process starting..."))
  263. (let ((checkers
  264. (pcase arg
  265. (`nil org-lint--checkers)
  266. (`(4)
  267. (let ((category
  268. (completing-read
  269. "Checker category: "
  270. (mapcar #'org-lint-checker-categories org-lint--checkers)
  271. nil t)))
  272. (cl-remove-if-not
  273. (lambda (c)
  274. (assoc-string (org-lint-checker-categories c) category))
  275. org-lint--checkers)))
  276. (`(16)
  277. (list
  278. (let ((name (completing-read
  279. "Checker name: "
  280. (mapcar #'org-lint-checker-name org-lint--checkers)
  281. nil t)))
  282. (catch 'exit
  283. (dolist (c org-lint--checkers)
  284. (when (string= (org-lint-checker-name c) name)
  285. (throw 'exit c)))))))
  286. ((pred consp)
  287. (cl-remove-if-not (lambda (c) (memq (org-lint-checker-name c) arg))
  288. org-lint--checkers))
  289. (_ (user-error "Invalid argument `%S' for `org-lint'" arg)))))
  290. (if (not (called-interactively-p 'any))
  291. (org-lint--generate-reports (current-buffer) checkers)
  292. (org-lint--display-reports (current-buffer) checkers)
  293. (message "Org linting process completed"))))
  294. ;;; Checker functions
  295. (defun org-lint--collect-duplicates
  296. (ast type extract-key extract-position build-message)
  297. "Helper function to collect duplicates in parse tree AST.
  298. EXTRACT-KEY is a function extracting key. It is called with
  299. a single argument: the element or object. Comparison is done
  300. with `equal'.
  301. EXTRACT-POSITION is a function returning position for the report.
  302. It is called with two arguments, the object or element, and the
  303. key.
  304. BUILD-MESSAGE is a function creating the report message. It is
  305. called with one argument, the key used for comparison."
  306. (let* (keys
  307. originals
  308. reports
  309. (make-report
  310. (lambda (position value)
  311. (push (list position (funcall build-message value)) reports))))
  312. (org-element-map ast type
  313. (lambda (datum)
  314. (let ((key (funcall extract-key datum)))
  315. (cond
  316. ((not key))
  317. ((assoc key keys) (cl-pushnew (assoc key keys) originals)
  318. (funcall make-report (funcall extract-position datum key) key))
  319. (t (push (cons key (funcall extract-position datum key)) keys))))))
  320. (dolist (e originals reports) (funcall make-report (cdr e) (car e)))))
  321. (defun org-lint-duplicate-custom-id (ast)
  322. (org-lint--collect-duplicates
  323. ast
  324. 'node-property
  325. (lambda (property)
  326. (and (eq (compare-strings "CUSTOM_ID" nil nil
  327. (org-element-property :key property) nil nil
  328. t)
  329. t)
  330. (org-element-property :value property)))
  331. (lambda (property _) (org-element-property :begin property))
  332. (lambda (key) (format "Duplicate CUSTOM_ID property \"%s\"" key))))
  333. (defun org-lint-duplicate-name (ast)
  334. (org-lint--collect-duplicates
  335. ast
  336. org-element-all-elements
  337. (lambda (datum) (org-element-property :name datum))
  338. (lambda (datum name)
  339. (goto-char (org-element-property :begin datum))
  340. (re-search-forward
  341. (format "^[ \t]*#\\+[A-Za-z]+:[ \t]*%s[ \t]*$" (regexp-quote name)))
  342. (match-beginning 0))
  343. (lambda (key) (format "Duplicate NAME \"%s\"" key))))
  344. (defun org-lint-duplicate-target (ast)
  345. (org-lint--collect-duplicates
  346. ast
  347. 'target
  348. (lambda (target) (split-string (org-element-property :value target)))
  349. (lambda (target _) (org-element-property :begin target))
  350. (lambda (key)
  351. (format "Duplicate target <<%s>>" (mapconcat #'identity key " ")))))
  352. (defun org-lint-duplicate-footnote-definition (ast)
  353. (org-lint--collect-duplicates
  354. ast
  355. 'footnote-definition
  356. (lambda (definition) (org-element-property :label definition))
  357. (lambda (definition _) (org-element-property :post-affiliated definition))
  358. (lambda (key) (format "Duplicate footnote definition \"%s\"" key))))
  359. (defun org-lint-orphaned-affiliated-keywords (ast)
  360. ;; Ignore orphan RESULTS keywords, which could be generated from
  361. ;; a source block returning no value.
  362. (let ((keywords (cl-set-difference org-element-affiliated-keywords
  363. '("RESULT" "RESULTS")
  364. :test #'equal)))
  365. (org-element-map ast 'keyword
  366. (lambda (k)
  367. (let ((key (org-element-property :key k)))
  368. (and (or (let ((case-fold-search t))
  369. (string-match-p "\\`ATTR_[-_A-Za-z0-9]+\\'" key))
  370. (member key keywords))
  371. (list (org-element-property :post-affiliated k)
  372. (format "Orphaned affiliated keyword: \"%s\"" key))))))))
  373. (defun org-lint-obsolete-affiliated-keywords (_)
  374. (let ((regexp (format "^[ \t]*#\\+%s:"
  375. (regexp-opt '("DATA" "LABEL" "RESNAME" "SOURCE"
  376. "SRCNAME" "TBLNAME" "RESULT" "HEADERS")
  377. t)))
  378. reports)
  379. (while (re-search-forward regexp nil t)
  380. (let ((key (upcase (match-string-no-properties 1))))
  381. (when (< (point)
  382. (org-element-property :post-affiliated (org-element-at-point)))
  383. (push
  384. (list (line-beginning-position)
  385. (format
  386. "Obsolete affiliated keyword: \"%s\". Use \"%s\" instead"
  387. key
  388. (pcase key
  389. ("HEADERS" "HEADER")
  390. ("RESULT" "RESULTS")
  391. (_ "NAME"))))
  392. reports))))
  393. reports))
  394. (defun org-lint-deprecated-export-blocks (ast)
  395. (let ((deprecated '("ASCII" "BEAMER" "HTML" "LATEX" "MAN" "MARKDOWN" "MD"
  396. "ODT" "ORG" "TEXINFO")))
  397. (org-element-map ast 'special-block
  398. (lambda (b)
  399. (let ((type (org-element-property :type b)))
  400. (when (member-ignore-case type deprecated)
  401. (list
  402. (org-element-property :post-affiliated b)
  403. (format
  404. "Deprecated syntax for export block. Use \"BEGIN_EXPORT %s\" \
  405. instead"
  406. type))))))))
  407. (defun org-lint-deprecated-header-syntax (ast)
  408. (let* ((deprecated-babel-properties
  409. ;; DIR is also used for attachments.
  410. (delete "dir"
  411. (mapcar (lambda (arg) (downcase (symbol-name (car arg))))
  412. org-babel-common-header-args-w-values)))
  413. (deprecated-re
  414. (format "\\`%s[ \t]" (regexp-opt deprecated-babel-properties t))))
  415. (org-element-map ast '(keyword node-property)
  416. (lambda (datum)
  417. (let ((key (org-element-property :key datum)))
  418. (pcase (org-element-type datum)
  419. (`keyword
  420. (let ((value (org-element-property :value datum)))
  421. (and (string= key "PROPERTY")
  422. (string-match deprecated-re value)
  423. (list (org-element-property :begin datum)
  424. (format "Deprecated syntax for \"%s\". \
  425. Use header-args instead"
  426. (match-string-no-properties 1 value))))))
  427. (`node-property
  428. (and (member-ignore-case key deprecated-babel-properties)
  429. (list
  430. (org-element-property :begin datum)
  431. (format "Deprecated syntax for \"%s\". \
  432. Use :header-args: instead"
  433. key))))))))))
  434. (defun org-lint-missing-language-in-src-block (ast)
  435. (org-element-map ast 'src-block
  436. (lambda (b)
  437. (unless (org-element-property :language b)
  438. (list (org-element-property :post-affiliated b)
  439. "Missing language in source block")))))
  440. (defun org-lint-missing-backend-in-export-block (ast)
  441. (org-element-map ast 'export-block
  442. (lambda (b)
  443. (unless (org-element-property :type b)
  444. (list (org-element-property :post-affiliated b)
  445. "Missing back-end in export block")))))
  446. (defun org-lint-invalid-babel-call-block (ast)
  447. (org-element-map ast 'babel-call
  448. (lambda (b)
  449. (cond
  450. ((not (org-element-property :call b))
  451. (list (org-element-property :post-affiliated b)
  452. "Invalid syntax in babel call block"))
  453. ((let ((h (org-element-property :end-header b)))
  454. (and h (string-match-p "\\`\\[.*\\]\\'" h)))
  455. (list
  456. (org-element-property :post-affiliated b)
  457. "Babel call's end header must not be wrapped within brackets"))))))
  458. (defun org-lint-deprecated-category-setup (ast)
  459. (org-element-map ast 'keyword
  460. (let (category-flag)
  461. (lambda (k)
  462. (cond
  463. ((not (string= (org-element-property :key k) "CATEGORY")) nil)
  464. (category-flag
  465. (list (org-element-property :post-affiliated k)
  466. "Spurious CATEGORY keyword. Set :CATEGORY: property instead"))
  467. (t (setf category-flag t) nil))))))
  468. (defun org-lint-invalid-coderef-link (ast)
  469. (let ((info (list :parse-tree ast)))
  470. (org-element-map ast 'link
  471. (lambda (link)
  472. (let ((ref (org-element-property :path link)))
  473. (and (equal (org-element-property :type link) "coderef")
  474. (not (ignore-errors (org-export-resolve-coderef ref info)))
  475. (list (org-element-property :begin link)
  476. (format "Unknown coderef \"%s\"" ref))))))))
  477. (defun org-lint-invalid-custom-id-link (ast)
  478. (let ((info (list :parse-tree ast)))
  479. (org-element-map ast 'link
  480. (lambda (link)
  481. (and (equal (org-element-property :type link) "custom-id")
  482. (not (ignore-errors (org-export-resolve-id-link link info)))
  483. (list (org-element-property :begin link)
  484. (format "Unknown custom ID \"%s\""
  485. (org-element-property :path link))))))))
  486. (defun org-lint-invalid-fuzzy-link (ast)
  487. (let ((info (list :parse-tree ast)))
  488. (org-element-map ast 'link
  489. (lambda (link)
  490. (and (equal (org-element-property :type link) "fuzzy")
  491. (not (ignore-errors (org-export-resolve-fuzzy-link link info)))
  492. (list (org-element-property :begin link)
  493. (format "Unknown fuzzy location \"%s\""
  494. (let ((path (org-element-property :path link)))
  495. (if (string-prefix-p "*" path)
  496. (substring path 1)
  497. path)))))))))
  498. (defun org-lint-invalid-id-link (ast)
  499. (org-element-map ast 'link
  500. (lambda (link)
  501. (let ((id (org-element-property :path link)))
  502. (and (equal (org-element-property :type link) "id")
  503. (not (org-id-find id))
  504. (list (org-element-property :begin link)
  505. (format "Unknown ID \"%s\"" id)))))))
  506. (defun org-lint-special-property-in-properties-drawer (ast)
  507. (org-element-map ast 'node-property
  508. (lambda (p)
  509. (let ((key (org-element-property :key p)))
  510. (and (member-ignore-case key org-special-properties)
  511. (list (org-element-property :begin p)
  512. (format
  513. "Special property \"%s\" found in a properties drawer"
  514. key)))))))
  515. (defun org-lint-obsolete-properties-drawer (ast)
  516. (org-element-map ast 'drawer
  517. (lambda (d)
  518. (when (equal (org-element-property :drawer-name d) "PROPERTIES")
  519. (let ((headline? (org-element-lineage d '(headline)))
  520. (before
  521. (mapcar #'org-element-type
  522. (assq d (reverse (org-element-contents
  523. (org-element-property :parent d)))))))
  524. (list (org-element-property :post-affiliated d)
  525. (if (or (and headline? (member before '(nil (planning))))
  526. (and (null headline?) (member before '(nil (comment)))))
  527. "Incorrect contents for PROPERTIES drawer"
  528. "Incorrect location for PROPERTIES drawer")))))))
  529. (defun org-lint-invalid-effort-property (ast)
  530. (org-element-map ast 'node-property
  531. (lambda (p)
  532. (when (equal "EFFORT" (org-element-property :key p))
  533. (let ((value (org-element-property :value p)))
  534. (and (org-string-nw-p value)
  535. (not (org-duration-p value))
  536. (list (org-element-property :begin p)
  537. (format "Invalid effort duration format: %S" value))))))))
  538. (defun org-lint-link-to-local-file (ast)
  539. (org-element-map ast 'link
  540. (lambda (l)
  541. (let ((type (org-element-property :type l)))
  542. (pcase type
  543. ((or "attachment" "file")
  544. (let* ((path (org-element-property :path l))
  545. (file (if (string= type "file")
  546. path
  547. (org-with-point-at (org-element-property :begin l)
  548. (org-attach-expand path)))))
  549. (and (not (file-remote-p file))
  550. (not (file-exists-p file))
  551. (list (org-element-property :begin l)
  552. (format (if (org-element-lineage l '(link))
  553. "Link to non-existent image file %S \
  554. in description"
  555. "Link to non-existent local file %S")
  556. file)))))
  557. (_ nil))))))
  558. (defun org-lint-non-existent-setupfile-parameter (ast)
  559. (org-element-map ast 'keyword
  560. (lambda (k)
  561. (when (equal (org-element-property :key k) "SETUPFILE")
  562. (let ((file (org-unbracket-string
  563. "\"" "\""
  564. (org-element-property :value k))))
  565. (and (not (org-url-p file))
  566. (not (file-remote-p file))
  567. (not (file-exists-p file))
  568. (list (org-element-property :begin k)
  569. (format "Non-existent setup file %S" file))))))))
  570. (defun org-lint-wrong-include-link-parameter (ast)
  571. (org-element-map ast 'keyword
  572. (lambda (k)
  573. (when (equal (org-element-property :key k) "INCLUDE")
  574. (let* ((value (org-element-property :value k))
  575. (path
  576. (and (string-match "^\\(\".+\"\\|\\S-+\\)[ \t]*" value)
  577. (save-match-data
  578. (org-strip-quotes (match-string 1 value))))))
  579. (if (not path)
  580. (list (org-element-property :post-affiliated k)
  581. "Missing location argument in INCLUDE keyword")
  582. (let* ((file (org-string-nw-p
  583. (if (string-match "::\\(.*\\)\\'" path)
  584. (substring path 0 (match-beginning 0))
  585. path)))
  586. (search (and (not (equal file path))
  587. (org-string-nw-p (match-string 1 path)))))
  588. (if (and file
  589. (not (file-remote-p file))
  590. (not (file-exists-p file)))
  591. (list (org-element-property :post-affiliated k)
  592. "Non-existent file argument in INCLUDE keyword")
  593. (let* ((visiting (if file (find-buffer-visiting file)
  594. (current-buffer)))
  595. (buffer (or visiting (find-file-noselect file)))
  596. (org-link-search-must-match-exact-headline t))
  597. (unwind-protect
  598. (with-current-buffer buffer
  599. (when (and search
  600. (not (ignore-errors
  601. (org-link-search search nil t))))
  602. (list (org-element-property :post-affiliated k)
  603. (format
  604. "Invalid search part \"%s\" in INCLUDE keyword"
  605. search))))
  606. (unless visiting (kill-buffer buffer))))))))))))
  607. (defun org-lint-obsolete-include-markup (ast)
  608. (let ((regexp (format "\\`\\(?:\".+\"\\|\\S-+\\)[ \t]+%s"
  609. (regexp-opt
  610. '("ASCII" "BEAMER" "HTML" "LATEX" "MAN" "MARKDOWN" "MD"
  611. "ODT" "ORG" "TEXINFO")
  612. t))))
  613. (org-element-map ast 'keyword
  614. (lambda (k)
  615. (when (equal (org-element-property :key k) "INCLUDE")
  616. (let ((case-fold-search t)
  617. (value (org-element-property :value k)))
  618. (when (string-match regexp value)
  619. (let ((markup (match-string-no-properties 1 value)))
  620. (list (org-element-property :post-affiliated k)
  621. (format "Obsolete markup \"%s\" in INCLUDE keyword. \
  622. Use \"export %s\" instead"
  623. markup
  624. markup))))))))))
  625. (defun org-lint-unknown-options-item (ast)
  626. (let ((allowed (delq nil
  627. (append
  628. (mapcar (lambda (o) (nth 2 o)) org-export-options-alist)
  629. (cl-mapcan
  630. (lambda (b)
  631. (mapcar (lambda (o) (nth 2 o))
  632. (org-export-backend-options b)))
  633. org-export-registered-backends))))
  634. reports)
  635. (org-element-map ast 'keyword
  636. (lambda (k)
  637. (when (string= (org-element-property :key k) "OPTIONS")
  638. (let ((value (org-element-property :value k))
  639. (start 0))
  640. (while (string-match "\\(.+?\\):\\((.*?)\\|\\S-+\\)?[ \t]*"
  641. value
  642. start)
  643. (setf start (match-end 0))
  644. (let ((item (match-string 1 value)))
  645. (unless (member item allowed)
  646. (push (list (org-element-property :post-affiliated k)
  647. (format "Unknown OPTIONS item \"%s\"" item))
  648. reports))
  649. (unless (match-string 2 value)
  650. (push (list (org-element-property :post-affiliated k)
  651. (format "Missing value for option item %S" item))
  652. reports))))))))
  653. reports))
  654. (defun org-lint-invalid-macro-argument-and-template (ast)
  655. (let* ((reports nil)
  656. (extract-placeholders
  657. (lambda (template)
  658. (let ((start 0)
  659. args)
  660. (while (string-match "\\$\\([1-9][0-9]*\\)" template start)
  661. (setf start (match-end 0))
  662. (push (string-to-number (match-string 1 template)) args))
  663. (sort (org-uniquify args) #'<))))
  664. (check-arity
  665. (lambda (arity macro)
  666. (let* ((name (org-element-property :key macro))
  667. (pos (org-element-property :begin macro))
  668. (args (org-element-property :args macro))
  669. (l (length args)))
  670. (cond
  671. ((< l (1- (car arity)))
  672. (push (list pos (format "Missing arguments in macro %S" name))
  673. reports))
  674. ((< l (car arity))
  675. (push (list pos (format "Missing argument in macro %S" name))
  676. reports))
  677. ((> l (1+ (cdr arity)))
  678. (push (let ((spurious-args (nthcdr (cdr arity) args)))
  679. (list pos
  680. (format "Spurious arguments in macro %S: %s"
  681. name
  682. (mapconcat #'org-trim spurious-args ", "))))
  683. reports))
  684. ((> l (cdr arity))
  685. (push (list pos
  686. (format "Spurious argument in macro %S: %s"
  687. name
  688. (org-last args)))
  689. reports))
  690. (t nil))))))
  691. ;; Check arguments for macro templates.
  692. (org-element-map ast 'keyword
  693. (lambda (k)
  694. (when (string= (org-element-property :key k) "MACRO")
  695. (let* ((value (org-element-property :value k))
  696. (name (and (string-match "^\\S-+" value)
  697. (match-string 0 value)))
  698. (template (and name
  699. (org-trim (substring value (match-end 0))))))
  700. (cond
  701. ((not name)
  702. (push (list (org-element-property :post-affiliated k)
  703. "Missing name in MACRO keyword")
  704. reports))
  705. ((not (org-string-nw-p template))
  706. (push (list (org-element-property :post-affiliated k)
  707. "Missing template in macro \"%s\"" name)
  708. reports))
  709. (t
  710. (unless (let ((args (funcall extract-placeholders template)))
  711. (equal (number-sequence 1 (or (org-last args) 0)) args))
  712. (push (list (org-element-property :post-affiliated k)
  713. (format "Unused placeholders in macro \"%s\""
  714. name))
  715. reports))))))))
  716. ;; Check arguments for macros.
  717. (org-macro-initialize-templates)
  718. (let ((templates (append
  719. (mapcar (lambda (m) (cons m "$1"))
  720. '("author" "date" "email" "title" "results"))
  721. org-macro-templates)))
  722. (org-element-map ast 'macro
  723. (lambda (macro)
  724. (let* ((name (org-element-property :key macro))
  725. (template (cdr (assoc-string name templates t))))
  726. (pcase template
  727. (`nil
  728. (push (list (org-element-property :begin macro)
  729. (format "Undefined macro %S" name))
  730. reports))
  731. ((guard (string= name "keyword"))
  732. (funcall check-arity '(1 . 1) macro))
  733. ((guard (string= name "modification-time"))
  734. (funcall check-arity '(1 . 2) macro))
  735. ((guard (string= name "n"))
  736. (funcall check-arity '(0 . 2) macro))
  737. ((guard (string= name "property"))
  738. (funcall check-arity '(1 . 2) macro))
  739. ((guard (string= name "time"))
  740. (funcall check-arity '(1 . 1) macro))
  741. ((pred functionp)) ;ignore (eval ...) templates
  742. (_
  743. (let* ((arg-numbers (funcall extract-placeholders template))
  744. (arity (if (null arg-numbers)
  745. '(0 . 0)
  746. (let ((m (apply #'max arg-numbers)))
  747. (cons m m)))))
  748. (funcall check-arity arity macro))))))))
  749. reports))
  750. (defun org-lint-undefined-footnote-reference (ast)
  751. (let ((definitions
  752. (org-element-map ast '(footnote-definition footnote-reference)
  753. (lambda (f)
  754. (and (or (eq 'footnote-definition (org-element-type f))
  755. (eq 'inline (org-element-property :type f)))
  756. (org-element-property :label f))))))
  757. (org-element-map ast 'footnote-reference
  758. (lambda (f)
  759. (let ((label (org-element-property :label f)))
  760. (and (eq 'standard (org-element-property :type f))
  761. (not (member label definitions))
  762. (list (org-element-property :begin f)
  763. (format "Missing definition for footnote [%s]"
  764. label))))))))
  765. (defun org-lint-unreferenced-footnote-definition (ast)
  766. (let ((references (org-element-map ast 'footnote-reference
  767. (lambda (f) (org-element-property :label f)))))
  768. (org-element-map ast 'footnote-definition
  769. (lambda (f)
  770. (let ((label (org-element-property :label f)))
  771. (and label
  772. (not (member label references))
  773. (list (org-element-property :post-affiliated f)
  774. (format "No reference for footnote definition [%s]"
  775. label))))))))
  776. (defun org-lint-colon-in-name (ast)
  777. (org-element-map ast org-element-all-elements
  778. (lambda (e)
  779. (let ((name (org-element-property :name e)))
  780. (and name
  781. (string-match-p ":" name)
  782. (list (progn
  783. (goto-char (org-element-property :begin e))
  784. (re-search-forward
  785. (format "^[ \t]*#\\+\\w+: +%s *$" (regexp-quote name)))
  786. (match-beginning 0))
  787. (format
  788. "Name \"%s\" contains a colon; Babel cannot use it as input"
  789. name)))))))
  790. (defun org-lint-misplaced-planning-info (_)
  791. (let ((case-fold-search t)
  792. reports)
  793. (while (re-search-forward org-planning-line-re nil t)
  794. (unless (memq (org-element-type (org-element-at-point))
  795. '(comment-block example-block export-block planning
  796. src-block verse-block))
  797. (push (list (line-beginning-position) "Misplaced planning info line")
  798. reports)))
  799. reports))
  800. (defun org-lint-incomplete-drawer (_)
  801. (let (reports)
  802. (while (re-search-forward org-drawer-regexp nil t)
  803. (let ((name (org-trim (match-string-no-properties 0)))
  804. (element (org-element-at-point)))
  805. (pcase (org-element-type element)
  806. (`drawer
  807. ;; Find drawer opening lines within non-empty drawers.
  808. (let ((end (org-element-property :contents-end element)))
  809. (when end
  810. (while (re-search-forward org-drawer-regexp end t)
  811. (let ((n (org-trim (match-string-no-properties 0))))
  812. (push (list (line-beginning-position)
  813. (format "Possible misleading drawer entry %S" n))
  814. reports))))
  815. (goto-char (org-element-property :end element))))
  816. (`property-drawer
  817. (goto-char (org-element-property :end element)))
  818. ((or `comment-block `example-block `export-block `src-block
  819. `verse-block)
  820. nil)
  821. (_
  822. ;; Find drawer opening lines outside of any drawer.
  823. (push (list (line-beginning-position)
  824. (format "Possible incomplete drawer %S" name))
  825. reports)))))
  826. reports))
  827. (defun org-lint-indented-diary-sexp (_)
  828. (let (reports)
  829. (while (re-search-forward "^[ \t]+%%(" nil t)
  830. (unless (memq (org-element-type (org-element-at-point))
  831. '(comment-block diary-sexp example-block export-block
  832. src-block verse-block))
  833. (push (list (line-beginning-position) "Possible indented diary-sexp")
  834. reports)))
  835. reports))
  836. (defun org-lint-invalid-block (_)
  837. (let ((case-fold-search t)
  838. (regexp "^[ \t]*#\\+\\(BEGIN\\|END\\)\\(?::\\|_[^[:space:]]*\\)?[ \t]*")
  839. reports)
  840. (while (re-search-forward regexp nil t)
  841. (let ((name (org-trim (buffer-substring-no-properties
  842. (line-beginning-position) (line-end-position)))))
  843. (cond
  844. ((and (string-prefix-p "END" (match-string 1) t)
  845. (not (eolp)))
  846. (push (list (line-beginning-position)
  847. (format "Invalid block closing line \"%s\"" name))
  848. reports))
  849. ((not (memq (org-element-type (org-element-at-point))
  850. '(center-block comment-block dynamic-block example-block
  851. export-block quote-block special-block
  852. src-block verse-block)))
  853. (push (list (line-beginning-position)
  854. (format "Possible incomplete block \"%s\""
  855. name))
  856. reports)))))
  857. reports))
  858. (defun org-lint-invalid-keyword-syntax (_)
  859. (let ((regexp "^[ \t]*#\\+\\([^[:space:]:]*\\)\\(?: \\|$\\)")
  860. (exception-re
  861. (format "[ \t]*#\\+%s\\(\\[.*\\]\\)?:\\(?: \\|$\\)"
  862. (regexp-opt org-element-dual-keywords)))
  863. reports)
  864. (while (re-search-forward regexp nil t)
  865. (let ((name (match-string-no-properties 1)))
  866. (unless (or (string-prefix-p "BEGIN" name t)
  867. (string-prefix-p "END" name t)
  868. (save-excursion
  869. (beginning-of-line)
  870. (let ((case-fold-search t)) (looking-at exception-re))))
  871. (push (list (match-beginning 0)
  872. (format "Possible missing colon in keyword \"%s\"" name))
  873. reports))))
  874. reports))
  875. (defun org-lint-extraneous-element-in-footnote-section (ast)
  876. (org-element-map ast 'headline
  877. (lambda (h)
  878. (and (org-element-property :footnote-section-p h)
  879. (org-element-map (org-element-contents h)
  880. (cl-remove-if
  881. (lambda (e)
  882. (memq e '(comment comment-block footnote-definition
  883. property-drawer section)))
  884. org-element-all-elements)
  885. (lambda (e)
  886. (not (and (eq (org-element-type e) 'headline)
  887. (org-element-property :commentedp e))))
  888. nil t '(footnote-definition property-drawer))
  889. (list (org-element-property :begin h)
  890. "Extraneous elements in footnote section are not exported")))))
  891. (defun org-lint-quote-section (ast)
  892. (org-element-map ast '(headline inlinetask)
  893. (lambda (h)
  894. (let ((title (org-element-property :raw-value h)))
  895. (and (or (string-prefix-p "QUOTE " title)
  896. (string-prefix-p (concat org-comment-string " QUOTE ") title))
  897. (list (org-element-property :begin h)
  898. "Deprecated QUOTE section"))))))
  899. (defun org-lint-file-application (ast)
  900. (org-element-map ast 'link
  901. (lambda (l)
  902. (let ((app (org-element-property :application l)))
  903. (and app
  904. (list (org-element-property :begin l)
  905. (format "Deprecated \"file+%s\" link type" app)))))))
  906. (defun org-lint-percent-encoding-link-escape (ast)
  907. (org-element-map ast 'link
  908. (lambda (l)
  909. (when (eq 'bracket (org-element-property :format l))
  910. (let* ((uri (org-element-property :path l))
  911. (start 0)
  912. (obsolete-flag
  913. (catch :obsolete
  914. (while (string-match "%\\(..\\)?" uri start)
  915. (setq start (match-end 0))
  916. (unless (member (match-string 1 uri) '("25" "5B" "5D" "20"))
  917. (throw :obsolete nil)))
  918. (string-match-p "%" uri))))
  919. (when obsolete-flag
  920. (list (org-element-property :begin l)
  921. "Link escaped with obsolete percent-encoding syntax")))))))
  922. (defun org-lint-wrong-header-argument (ast)
  923. (let* ((reports)
  924. (verify
  925. (lambda (datum language headers)
  926. (let ((allowed
  927. ;; If LANGUAGE is specified, restrict allowed
  928. ;; headers to both LANGUAGE-specific and default
  929. ;; ones. Otherwise, accept headers from any loaded
  930. ;; language.
  931. (append
  932. org-babel-header-arg-names
  933. (cl-mapcan
  934. (lambda (l)
  935. (let ((v (intern (format "org-babel-header-args:%s" l))))
  936. (and (boundp v) (mapcar #'car (symbol-value v)))))
  937. (if language (list language)
  938. (mapcar #'car org-babel-load-languages))))))
  939. (dolist (header headers)
  940. (let ((h (symbol-name (car header)))
  941. (p (or (org-element-property :post-affiliated datum)
  942. (org-element-property :begin datum))))
  943. (cond
  944. ((not (string-prefix-p ":" h))
  945. (push
  946. (list p
  947. (format "Missing colon in header argument \"%s\"" h))
  948. reports))
  949. ((assoc-string (substring h 1) allowed))
  950. (t (push (list p (format "Unknown header argument \"%s\"" h))
  951. reports)))))))))
  952. (org-element-map ast '(babel-call inline-babel-call inline-src-block keyword
  953. node-property src-block)
  954. (lambda (datum)
  955. (pcase (org-element-type datum)
  956. ((or `babel-call `inline-babel-call)
  957. (funcall verify
  958. datum
  959. nil
  960. (cl-mapcan #'org-babel-parse-header-arguments
  961. (list
  962. (org-element-property :inside-header datum)
  963. (org-element-property :end-header datum)))))
  964. (`inline-src-block
  965. (funcall verify
  966. datum
  967. (org-element-property :language datum)
  968. (org-babel-parse-header-arguments
  969. (org-element-property :parameters datum))))
  970. (`keyword
  971. (when (string= (org-element-property :key datum) "PROPERTY")
  972. (let ((value (org-element-property :value datum)))
  973. (when (string-match "\\`header-args\\(?::\\(\\S-+\\)\\)?\\+? *"
  974. value)
  975. (funcall verify
  976. datum
  977. (match-string 1 value)
  978. (org-babel-parse-header-arguments
  979. (substring value (match-end 0))))))))
  980. (`node-property
  981. (let ((key (org-element-property :key datum)))
  982. (when (let ((case-fold-search t))
  983. (string-match "\\`HEADER-ARGS\\(?::\\(\\S-+\\)\\)?\\+?"
  984. key))
  985. (funcall verify
  986. datum
  987. (match-string 1 key)
  988. (org-babel-parse-header-arguments
  989. (org-element-property :value datum))))))
  990. (`src-block
  991. (funcall verify
  992. datum
  993. (org-element-property :language datum)
  994. (cl-mapcan #'org-babel-parse-header-arguments
  995. (cons (org-element-property :parameters datum)
  996. (org-element-property :header datum))))))))
  997. reports))
  998. (defun org-lint-wrong-header-value (ast)
  999. (let (reports)
  1000. (org-element-map ast
  1001. '(babel-call inline-babel-call inline-src-block src-block)
  1002. (lambda (datum)
  1003. (let* ((type (org-element-type datum))
  1004. (language (org-element-property :language datum))
  1005. (allowed-header-values
  1006. (append (and language
  1007. (let ((v (intern (concat "org-babel-header-args:"
  1008. language))))
  1009. (and (boundp v) (symbol-value v))))
  1010. org-babel-common-header-args-w-values))
  1011. (datum-header-values
  1012. (org-babel-parse-header-arguments
  1013. (org-trim
  1014. (pcase type
  1015. (`src-block
  1016. (mapconcat
  1017. #'identity
  1018. (cons (org-element-property :parameters datum)
  1019. (org-element-property :header datum))
  1020. " "))
  1021. (`inline-src-block
  1022. (or (org-element-property :parameters datum) ""))
  1023. (_
  1024. (concat
  1025. (org-element-property :inside-header datum)
  1026. " "
  1027. (org-element-property :end-header datum))))))))
  1028. (dolist (header datum-header-values)
  1029. (let ((allowed-values
  1030. (cdr (assoc-string (substring (symbol-name (car header)) 1)
  1031. allowed-header-values))))
  1032. (unless (memq allowed-values '(:any nil))
  1033. (let ((values (cdr header))
  1034. groups-alist)
  1035. (dolist (v (if (stringp values) (split-string values)
  1036. (list values)))
  1037. (let ((valid-value nil))
  1038. (catch 'exit
  1039. (dolist (group allowed-values)
  1040. (cond
  1041. ((not (funcall
  1042. (if (stringp v) #'assoc-string #'assoc)
  1043. v group))
  1044. (when (memq :any group)
  1045. (setf valid-value t)
  1046. (push (cons group v) groups-alist)))
  1047. ((assq group groups-alist)
  1048. (push
  1049. (list
  1050. (or (org-element-property :post-affiliated datum)
  1051. (org-element-property :begin datum))
  1052. (format
  1053. "Forbidden combination in header \"%s\": %s, %s"
  1054. (car header)
  1055. (cdr (assq group groups-alist))
  1056. v))
  1057. reports)
  1058. (throw 'exit nil))
  1059. (t (push (cons group v) groups-alist)
  1060. (setf valid-value t))))
  1061. (unless valid-value
  1062. (push
  1063. (list
  1064. (or (org-element-property :post-affiliated datum)
  1065. (org-element-property :begin datum))
  1066. (format "Unknown value \"%s\" for header \"%s\""
  1067. v
  1068. (car header)))
  1069. reports))))))))))))
  1070. reports))
  1071. (defun org-lint-spurious-colons (ast)
  1072. (org-element-map ast '(headline inlinetask)
  1073. (lambda (h)
  1074. (when (member "" (org-element-property :tags h))
  1075. (list (org-element-property :begin h)
  1076. "Tags contain a spurious colon")))))
  1077. (defun org-lint-non-existent-bibliography (ast)
  1078. (org-element-map ast 'keyword
  1079. (lambda (k)
  1080. (when (equal "BIBLIOGRAPHY" (org-element-property :key k))
  1081. (let ((file (org-strip-quotes (org-element-property :value k))))
  1082. (and (not (file-remote-p file))
  1083. (not (file-exists-p file))
  1084. (list (org-element-property :begin k)
  1085. (format "Non-existent bibliography %S" file))))))))
  1086. (defun org-lint-missing-print-bibliography (ast)
  1087. (and (org-element-map ast 'citation #'identity nil t)
  1088. (not (org-element-map ast 'keyword
  1089. (lambda (k)
  1090. (equal "PRINT_BIBLIOGRAPHY" (org-element-property :key k)))
  1091. nil t))
  1092. (list
  1093. (list (point-max) "Possibly missing \"PRINT_BIBLIOGRAPHY\" keyword"))))
  1094. (defun org-lint-invalid-cite-export-declaration (ast)
  1095. (org-element-map ast 'keyword
  1096. (lambda (k)
  1097. (when (equal "CITE_EXPORT" (org-element-property :key k))
  1098. (let ((value (org-element-property :value k))
  1099. (source (org-element-property :begin k)))
  1100. (if (equal value "")
  1101. (list source "Missing export processor name")
  1102. (condition-case _
  1103. (pcase (org-cite-read-processor-declaration value)
  1104. (`(,(and (pred symbolp) name)
  1105. ,(pred string-or-null-p)
  1106. ,(pred string-or-null-p))
  1107. (unless (org-cite-get-processor name)
  1108. (list source "Unknown cite export processor %S" name)))
  1109. (_
  1110. (list source "Invalid cite export processor declaration")))
  1111. (error
  1112. (list source "Invalid cite export processor declaration")))))))))
  1113. (defun org-lint-incomplete-citation (ast)
  1114. (org-element-map ast 'plain-text
  1115. (lambda (text)
  1116. (and (string-match-p org-element-citation-prefix-re text)
  1117. ;; XXX: The code below signals the error at the beginning
  1118. ;; of the paragraph containing the faulty object. It is
  1119. ;; not very accurate but may be enough for now.
  1120. (list (org-element-property :contents-begin
  1121. (org-element-property :parent text))
  1122. "Possibly incomplete citation markup")))))
  1123. ;;; Checkers declaration
  1124. (org-lint-add-checker 'duplicate-custom-id
  1125. "Report duplicates CUSTOM_ID properties"
  1126. #'org-lint-duplicate-custom-id
  1127. :categories '(link))
  1128. (org-lint-add-checker 'duplicate-name
  1129. "Report duplicate NAME values"
  1130. #'org-lint-duplicate-name
  1131. :categories '(babel 'link))
  1132. (org-lint-add-checker 'duplicate-target
  1133. "Report duplicate targets"
  1134. #'org-lint-duplicate-target
  1135. :categories '(link))
  1136. (org-lint-add-checker 'duplicate-footnote-definition
  1137. "Report duplicate footnote definitions"
  1138. #'org-lint-duplicate-footnote-definition
  1139. :categories '(footnote))
  1140. (org-lint-add-checker 'orphaned-affiliated-keywords
  1141. "Report orphaned affiliated keywords"
  1142. #'org-lint-orphaned-affiliated-keywords
  1143. :trust 'low)
  1144. (org-lint-add-checker 'obsolete-affiliated-keywords
  1145. "Report obsolete affiliated keywords"
  1146. #'org-lint-obsolete-affiliated-keywords
  1147. :categories '(obsolete))
  1148. (org-lint-add-checker 'deprecated-export-blocks
  1149. "Report deprecated export block syntax"
  1150. #'org-lint-deprecated-export-blocks
  1151. :trust 'low :categories '(obsolete export))
  1152. (org-lint-add-checker 'deprecated-header-syntax
  1153. "Report deprecated Babel header syntax"
  1154. #'org-lint-deprecated-header-syntax
  1155. :trust 'low :categories '(obsolete babel))
  1156. (org-lint-add-checker 'missing-language-in-src-block
  1157. "Report missing language in source blocks"
  1158. #'org-lint-missing-language-in-src-block
  1159. :categories '(babel))
  1160. (org-lint-add-checker 'missing-backend-in-export-block
  1161. "Report missing back-end in export blocks"
  1162. #'org-lint-missing-backend-in-export-block
  1163. :categories '(export))
  1164. (org-lint-add-checker 'invalid-babel-call-block
  1165. "Report invalid Babel call blocks"
  1166. #'org-lint-invalid-babel-call-block
  1167. :categories '(babel))
  1168. (org-lint-add-checker 'colon-in-name
  1169. "Report NAME values with a colon"
  1170. #'org-lint-colon-in-name
  1171. :categories '(babel))
  1172. (org-lint-add-checker 'wrong-header-argument
  1173. "Report wrong babel headers"
  1174. #'org-lint-wrong-header-argument
  1175. :categories '(babel))
  1176. (org-lint-add-checker 'wrong-header-value
  1177. "Report invalid value in babel headers"
  1178. #'org-lint-wrong-header-value
  1179. :categories '(babel) :trust 'low)
  1180. (org-lint-add-checker 'deprecated-category-setup
  1181. "Report misuse of CATEGORY keyword"
  1182. #'org-lint-deprecated-category-setup
  1183. :categories '(obsolete))
  1184. (org-lint-add-checker 'invalid-coderef-link
  1185. "Report \"coderef\" links with unknown destination"
  1186. #'org-lint-invalid-coderef-link
  1187. :categories '(link))
  1188. (org-lint-add-checker 'invalid-custom-id-link
  1189. "Report \"custom-id\" links with unknown destination"
  1190. #'org-lint-invalid-custom-id-link
  1191. :categories '(link))
  1192. (org-lint-add-checker 'invalid-fuzzy-link
  1193. "Report \"fuzzy\" links with unknown destination"
  1194. #'org-lint-invalid-fuzzy-link
  1195. :categories '(link))
  1196. (org-lint-add-checker 'invalid-id-link
  1197. "Report \"id\" links with unknown destination"
  1198. #'org-lint-invalid-id-link
  1199. :categories '(link))
  1200. (org-lint-add-checker 'link-to-local-file
  1201. "Report links to non-existent local files"
  1202. #'org-lint-link-to-local-file
  1203. :categories '(link) :trust 'low)
  1204. (org-lint-add-checker 'non-existent-setupfile-parameter
  1205. "Report SETUPFILE keywords with non-existent file parameter"
  1206. #'org-lint-non-existent-setupfile-parameter
  1207. :trust 'low)
  1208. (org-lint-add-checker 'wrong-include-link-parameter
  1209. "Report INCLUDE keywords with misleading link parameter"
  1210. #'org-lint-wrong-include-link-parameter
  1211. :categories '(export) :trust 'low)
  1212. (org-lint-add-checker 'obsolete-include-markup
  1213. "Report obsolete markup in INCLUDE keyword"
  1214. #'org-lint-obsolete-include-markup
  1215. :categories '(obsolete export) :trust 'low)
  1216. (org-lint-add-checker 'unknown-options-item
  1217. "Report unknown items in OPTIONS keyword"
  1218. #'org-lint-unknown-options-item
  1219. :categories '(export) :trust 'low)
  1220. (org-lint-add-checker 'invalid-macro-argument-and-template
  1221. "Report spurious macro arguments or invalid macro templates"
  1222. #'org-lint-invalid-macro-argument-and-template
  1223. :categories '(export) :trust 'low)
  1224. (org-lint-add-checker 'special-property-in-properties-drawer
  1225. "Report special properties in properties drawers"
  1226. #'org-lint-special-property-in-properties-drawer
  1227. :categories '(properties))
  1228. (org-lint-add-checker 'obsolete-properties-drawer
  1229. "Report obsolete syntax for properties drawers"
  1230. #'org-lint-obsolete-properties-drawer
  1231. :categories '(obsolete properties))
  1232. (org-lint-add-checker 'invalid-effort-property
  1233. "Report invalid duration in EFFORT property"
  1234. #'org-lint-invalid-effort-property
  1235. :categories '(properties))
  1236. (org-lint-add-checker 'undefined-footnote-reference
  1237. "Report missing definition for footnote references"
  1238. #'org-lint-undefined-footnote-reference
  1239. :categories '(footnote))
  1240. (org-lint-add-checker 'unreferenced-footnote-definition
  1241. "Report missing reference for footnote definitions"
  1242. #'org-lint-unreferenced-footnote-definition
  1243. :categories '(footnote))
  1244. (org-lint-add-checker 'extraneous-element-in-footnote-section
  1245. "Report non-footnote definitions in footnote section"
  1246. #'org-lint-extraneous-element-in-footnote-section
  1247. :categories '(footnote))
  1248. (org-lint-add-checker 'invalid-keyword-syntax
  1249. "Report probable invalid keywords"
  1250. #'org-lint-invalid-keyword-syntax
  1251. :trust 'low)
  1252. (org-lint-add-checker 'invalid-block
  1253. "Report invalid blocks"
  1254. #'org-lint-invalid-block
  1255. :trust 'low)
  1256. (org-lint-add-checker 'misplaced-planning-info
  1257. "Report misplaced planning info line"
  1258. #'org-lint-misplaced-planning-info
  1259. :trust 'low)
  1260. (org-lint-add-checker 'incomplete-drawer
  1261. "Report probable incomplete drawers"
  1262. #'org-lint-incomplete-drawer
  1263. :trust 'low)
  1264. (org-lint-add-checker 'indented-diary-sexp
  1265. "Report probable indented diary-sexps"
  1266. #'org-lint-indented-diary-sexp
  1267. :trust 'low)
  1268. (org-lint-add-checker 'quote-section
  1269. "Report obsolete QUOTE section"
  1270. #'org-lint-quote-section
  1271. :categories '(obsolete) :trust 'low)
  1272. (org-lint-add-checker 'file-application
  1273. "Report obsolete \"file+application\" link"
  1274. #'org-lint-file-application
  1275. :categories '(link obsolete))
  1276. (org-lint-add-checker 'percent-encoding-link-escape
  1277. "Report obsolete escape syntax in links"
  1278. #'org-lint-percent-encoding-link-escape
  1279. :categories '(link obsolete) :trust 'low)
  1280. (org-lint-add-checker 'spurious-colons
  1281. "Report spurious colons in tags"
  1282. #'org-lint-spurious-colons
  1283. :categories '(tags))
  1284. (org-lint-add-checker 'non-existent-bibliography
  1285. "Report invalid bibliography file"
  1286. #'org-lint-non-existent-bibliography
  1287. :categories '(cite))
  1288. (org-lint-add-checker 'missing-print-bibliography
  1289. "Report missing \"print_bibliography\" keyword"
  1290. #'org-lint-missing-print-bibliography
  1291. :categories '(cite))
  1292. (org-lint-add-checker 'invalid-cite-export-declaration
  1293. "Report invalid value for \"cite_export\" keyword"
  1294. #'org-lint-invalid-cite-export-declaration
  1295. :categories '(cite))
  1296. (org-lint-add-checker 'incomplete-citation
  1297. "Report incomplete citation object"
  1298. #'org-lint-incomplete-citation
  1299. :categories '(cite) :trust 'low)
  1300. (provide 'org-lint)
  1301. ;; Local variables:
  1302. ;; generated-autoload-file: "org-loaddefs.el"
  1303. ;; End:
  1304. ;;; org-lint.el ends here