org-lint.el 50 KB

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