org-lint.el 51 KB

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