org-lint.el 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457
  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 (eq (compare-strings "CUSTOM_ID" nil nil
  330. (org-element-property :key property) nil nil
  331. t)
  332. t)
  333. (org-element-property :value property)))
  334. (lambda (property _) (org-element-property :begin property))
  335. (lambda (key) (format "Duplicate CUSTOM_ID property \"%s\"" key))))
  336. (defun org-lint-duplicate-name (ast)
  337. (org-lint--collect-duplicates
  338. ast
  339. org-element-all-elements
  340. (lambda (datum) (org-element-property :name datum))
  341. (lambda (datum name)
  342. (goto-char (org-element-property :begin datum))
  343. (re-search-forward
  344. (format "^[ \t]*#\\+[A-Za-z]+:[ \t]*%s[ \t]*$" (regexp-quote name)))
  345. (match-beginning 0))
  346. (lambda (key) (format "Duplicate NAME \"%s\"" key))))
  347. (defun org-lint-duplicate-target (ast)
  348. (org-lint--collect-duplicates
  349. ast
  350. 'target
  351. (lambda (target) (split-string (org-element-property :value target)))
  352. (lambda (target _) (org-element-property :begin target))
  353. (lambda (key)
  354. (format "Duplicate target <<%s>>" (mapconcat #'identity key " ")))))
  355. (defun org-lint-duplicate-footnote-definition (ast)
  356. (org-lint--collect-duplicates
  357. ast
  358. 'footnote-definition
  359. (lambda (definition) (org-element-property :label definition))
  360. (lambda (definition _) (org-element-property :post-affiliated definition))
  361. (lambda (key) (format "Duplicate footnote definition \"%s\"" key))))
  362. (defun org-lint-orphaned-affiliated-keywords (ast)
  363. ;; Ignore orphan RESULTS keywords, which could be generated from
  364. ;; a source block returning no value.
  365. (let ((keywords (cl-set-difference org-element-affiliated-keywords
  366. '("RESULT" "RESULTS")
  367. :test #'equal)))
  368. (org-element-map ast 'keyword
  369. (lambda (k)
  370. (let ((key (org-element-property :key k)))
  371. (and (or (let ((case-fold-search t))
  372. (string-match-p "\\`ATTR_[-_A-Za-z0-9]+\\'" key))
  373. (member key keywords))
  374. (list (org-element-property :post-affiliated k)
  375. (format "Orphaned affiliated keyword: \"%s\"" key))))))))
  376. (defun org-lint-obsolete-affiliated-keywords (_)
  377. (let ((regexp (format "^[ \t]*#\\+%s:"
  378. (regexp-opt '("DATA" "LABEL" "RESNAME" "SOURCE"
  379. "SRCNAME" "TBLNAME" "RESULT" "HEADERS")
  380. t)))
  381. reports)
  382. (while (re-search-forward regexp nil t)
  383. (let ((key (upcase (match-string-no-properties 1))))
  384. (when (< (point)
  385. (org-element-property :post-affiliated (org-element-at-point)))
  386. (push
  387. (list (line-beginning-position)
  388. (format
  389. "Obsolete affiliated keyword: \"%s\". Use \"%s\" instead"
  390. key
  391. (pcase key
  392. ("HEADERS" "HEADER")
  393. ("RESULT" "RESULTS")
  394. (_ "NAME"))))
  395. reports))))
  396. reports))
  397. (defun org-lint-deprecated-export-blocks (ast)
  398. (let ((deprecated '("ASCII" "BEAMER" "HTML" "LATEX" "MAN" "MARKDOWN" "MD"
  399. "ODT" "ORG" "TEXINFO")))
  400. (org-element-map ast 'special-block
  401. (lambda (b)
  402. (let ((type (org-element-property :type b)))
  403. (when (member-ignore-case type deprecated)
  404. (list
  405. (org-element-property :post-affiliated b)
  406. (format
  407. "Deprecated syntax for export block. Use \"BEGIN_EXPORT %s\" \
  408. instead"
  409. type))))))))
  410. (defun org-lint-deprecated-header-syntax (ast)
  411. (let* ((deprecated-babel-properties
  412. ;; DIR is also used for attachments.
  413. (delete "dir"
  414. (mapcar (lambda (arg) (downcase (symbol-name (car arg))))
  415. org-babel-common-header-args-w-values)))
  416. (deprecated-re
  417. (format "\\`%s[ \t]" (regexp-opt deprecated-babel-properties t))))
  418. (org-element-map ast '(keyword node-property)
  419. (lambda (datum)
  420. (let ((key (org-element-property :key datum)))
  421. (pcase (org-element-type datum)
  422. (`keyword
  423. (let ((value (org-element-property :value datum)))
  424. (and (string= key "PROPERTY")
  425. (string-match deprecated-re value)
  426. (list (org-element-property :begin datum)
  427. (format "Deprecated syntax for \"%s\". \
  428. Use header-args instead"
  429. (match-string-no-properties 1 value))))))
  430. (`node-property
  431. (and (member-ignore-case key deprecated-babel-properties)
  432. (list
  433. (org-element-property :begin datum)
  434. (format "Deprecated syntax for \"%s\". \
  435. Use :header-args: instead"
  436. key))))))))))
  437. (defun org-lint-missing-language-in-src-block (ast)
  438. (org-element-map ast 'src-block
  439. (lambda (b)
  440. (unless (org-element-property :language b)
  441. (list (org-element-property :post-affiliated b)
  442. "Missing language in source block")))))
  443. (defun org-lint-missing-backend-in-export-block (ast)
  444. (org-element-map ast 'export-block
  445. (lambda (b)
  446. (unless (org-element-property :type b)
  447. (list (org-element-property :post-affiliated b)
  448. "Missing back-end in export block")))))
  449. (defun org-lint-invalid-babel-call-block (ast)
  450. (org-element-map ast 'babel-call
  451. (lambda (b)
  452. (cond
  453. ((not (org-element-property :call b))
  454. (list (org-element-property :post-affiliated b)
  455. "Invalid syntax in babel call block"))
  456. ((let ((h (org-element-property :end-header b)))
  457. (and h (string-match-p "\\`\\[.*\\]\\'" h)))
  458. (list
  459. (org-element-property :post-affiliated b)
  460. "Babel call's end header must not be wrapped within brackets"))))))
  461. (defun org-lint-deprecated-category-setup (ast)
  462. (org-element-map ast 'keyword
  463. (let (category-flag)
  464. (lambda (k)
  465. (cond
  466. ((not (string= (org-element-property :key k) "CATEGORY")) nil)
  467. (category-flag
  468. (list (org-element-property :post-affiliated k)
  469. "Spurious CATEGORY keyword. Set :CATEGORY: property instead"))
  470. (t (setf category-flag t) nil))))))
  471. (defun org-lint-invalid-coderef-link (ast)
  472. (let ((info (list :parse-tree ast)))
  473. (org-element-map ast 'link
  474. (lambda (link)
  475. (let ((ref (org-element-property :path link)))
  476. (and (equal (org-element-property :type link) "coderef")
  477. (not (ignore-errors (org-export-resolve-coderef ref info)))
  478. (list (org-element-property :begin link)
  479. (format "Unknown coderef \"%s\"" ref))))))))
  480. (defun org-lint-invalid-custom-id-link (ast)
  481. (let ((info (list :parse-tree ast)))
  482. (org-element-map ast 'link
  483. (lambda (link)
  484. (and (equal (org-element-property :type link) "custom-id")
  485. (not (ignore-errors (org-export-resolve-id-link link info)))
  486. (list (org-element-property :begin link)
  487. (format "Unknown custom ID \"%s\""
  488. (org-element-property :path link))))))))
  489. (defun org-lint-invalid-fuzzy-link (ast)
  490. (let ((info (list :parse-tree ast)))
  491. (org-element-map ast 'link
  492. (lambda (link)
  493. (and (equal (org-element-property :type link) "fuzzy")
  494. (not (ignore-errors (org-export-resolve-fuzzy-link link info)))
  495. (list (org-element-property :begin link)
  496. (format "Unknown fuzzy location \"%s\""
  497. (let ((path (org-element-property :path link)))
  498. (if (string-prefix-p "*" path)
  499. (substring path 1)
  500. path)))))))))
  501. (defun org-lint-invalid-id-link (ast)
  502. (org-element-map ast 'link
  503. (lambda (link)
  504. (let ((id (org-element-property :path link)))
  505. (and (equal (org-element-property :type link) "id")
  506. (not (org-id-find id))
  507. (list (org-element-property :begin link)
  508. (format "Unknown ID \"%s\"" id)))))))
  509. (defun org-lint-special-property-in-properties-drawer (ast)
  510. (org-element-map ast 'node-property
  511. (lambda (p)
  512. (let ((key (org-element-property :key p)))
  513. (and (member-ignore-case key org-special-properties)
  514. (list (org-element-property :begin p)
  515. (format
  516. "Special property \"%s\" found in a properties drawer"
  517. key)))))))
  518. (defun org-lint-obsolete-properties-drawer (ast)
  519. (org-element-map ast 'drawer
  520. (lambda (d)
  521. (when (equal (org-element-property :drawer-name d) "PROPERTIES")
  522. (let ((headline? (org-element-lineage d '(headline)))
  523. (before
  524. (mapcar #'org-element-type
  525. (assq d (reverse (org-element-contents
  526. (org-element-property :parent d)))))))
  527. (list (org-element-property :post-affiliated d)
  528. (if (or (and headline? (member before '(nil (planning))))
  529. (and (null headline?) (member before '(nil (comment)))))
  530. "Incorrect contents for PROPERTIES drawer"
  531. "Incorrect location for PROPERTIES drawer")))))))
  532. (defun org-lint-invalid-effort-property (ast)
  533. (org-element-map ast 'node-property
  534. (lambda (p)
  535. (when (equal "EFFORT" (org-element-property :key p))
  536. (let ((value (org-element-property :value p)))
  537. (and (org-string-nw-p value)
  538. (not (org-duration-p value))
  539. (list (org-element-property :begin p)
  540. (format "Invalid effort duration format: %S" value))))))))
  541. (defun org-lint-link-to-local-file (ast)
  542. (org-element-map ast 'link
  543. (lambda (l)
  544. (let ((type (org-element-property :type l)))
  545. (pcase type
  546. ((or "attachment" "file")
  547. (let* ((path (org-element-property :path l))
  548. (file (if (string= type "file")
  549. path
  550. (org-with-point-at (org-element-property :begin l)
  551. (org-attach-expand path)))))
  552. (and (not (file-remote-p file))
  553. (not (file-exists-p file))
  554. (list (org-element-property :begin l)
  555. (format (if (org-element-lineage l '(link))
  556. "Link to non-existent image file %S \
  557. in description"
  558. "Link to non-existent local file %S")
  559. file)))))
  560. (_ nil))))))
  561. (defun org-lint-non-existent-setupfile-parameter (ast)
  562. (org-element-map ast 'keyword
  563. (lambda (k)
  564. (when (equal (org-element-property :key k) "SETUPFILE")
  565. (let ((file (org-unbracket-string
  566. "\"" "\""
  567. (org-element-property :value k))))
  568. (and (not (org-url-p file))
  569. (not (file-remote-p file))
  570. (not (file-exists-p file))
  571. (list (org-element-property :begin k)
  572. (format "Non-existent setup file %S" file))))))))
  573. (defun org-lint-wrong-include-link-parameter (ast)
  574. (org-element-map ast 'keyword
  575. (lambda (k)
  576. (when (equal (org-element-property :key k) "INCLUDE")
  577. (let* ((value (org-element-property :value k))
  578. (path
  579. (and (string-match "^\\(\".+\"\\|\\S-+\\)[ \t]*" value)
  580. (save-match-data
  581. (org-strip-quotes (match-string 1 value))))))
  582. (if (not path)
  583. (list (org-element-property :post-affiliated k)
  584. "Missing location argument in INCLUDE keyword")
  585. (let* ((file (org-string-nw-p
  586. (if (string-match "::\\(.*\\)\\'" path)
  587. (substring path 0 (match-beginning 0))
  588. path)))
  589. (search (and (not (equal file path))
  590. (org-string-nw-p (match-string 1 path)))))
  591. (unless (org-url-p file)
  592. (if (and file
  593. (not (file-remote-p file))
  594. (not (file-exists-p file)))
  595. (list (org-element-property :post-affiliated k)
  596. "Non-existent file argument in INCLUDE keyword")
  597. (let* ((visiting (if file (find-buffer-visiting file)
  598. (current-buffer)))
  599. (buffer (or visiting (find-file-noselect file)))
  600. (org-link-search-must-match-exact-headline t))
  601. (unwind-protect
  602. (with-current-buffer buffer
  603. (when (and search
  604. (not (ignore-errors
  605. (org-link-search search nil t))))
  606. (list (org-element-property :post-affiliated k)
  607. (format
  608. "Invalid search part \"%s\" in INCLUDE keyword"
  609. search))))
  610. (unless visiting (kill-buffer buffer)))))))))))))
  611. (defun org-lint-obsolete-include-markup (ast)
  612. (let ((regexp (format "\\`\\(?:\".+\"\\|\\S-+\\)[ \t]+%s"
  613. (regexp-opt
  614. '("ASCII" "BEAMER" "HTML" "LATEX" "MAN" "MARKDOWN" "MD"
  615. "ODT" "ORG" "TEXINFO")
  616. t))))
  617. (org-element-map ast 'keyword
  618. (lambda (k)
  619. (when (equal (org-element-property :key k) "INCLUDE")
  620. (let ((case-fold-search t)
  621. (value (org-element-property :value k)))
  622. (when (string-match regexp value)
  623. (let ((markup (match-string-no-properties 1 value)))
  624. (list (org-element-property :post-affiliated k)
  625. (format "Obsolete markup \"%s\" in INCLUDE keyword. \
  626. Use \"export %s\" instead"
  627. markup
  628. markup))))))))))
  629. (defun org-lint-unknown-options-item (ast)
  630. (let ((allowed (delq nil
  631. (append
  632. (mapcar (lambda (o) (nth 2 o)) org-export-options-alist)
  633. (cl-mapcan
  634. (lambda (b)
  635. (mapcar (lambda (o) (nth 2 o))
  636. (org-export-backend-options b)))
  637. org-export-registered-backends))))
  638. reports)
  639. (org-element-map ast 'keyword
  640. (lambda (k)
  641. (when (string= (org-element-property :key k) "OPTIONS")
  642. (let ((value (org-element-property :value k))
  643. (start 0))
  644. (while (string-match "\\(.+?\\):\\((.*?)\\|\\S-+\\)?[ \t]*"
  645. value
  646. start)
  647. (setf start (match-end 0))
  648. (let ((item (match-string 1 value)))
  649. (unless (member item allowed)
  650. (push (list (org-element-property :post-affiliated k)
  651. (format "Unknown OPTIONS item \"%s\"" item))
  652. reports))
  653. (unless (match-string 2 value)
  654. (push (list (org-element-property :post-affiliated k)
  655. (format "Missing value for option item %S" item))
  656. reports))))))))
  657. reports))
  658. (defun org-lint-invalid-macro-argument-and-template (ast)
  659. (let* ((reports nil)
  660. (extract-placeholders
  661. (lambda (template)
  662. (let ((start 0)
  663. args)
  664. (while (string-match "\\$\\([1-9][0-9]*\\)" template start)
  665. (setf start (match-end 0))
  666. (push (string-to-number (match-string 1 template)) args))
  667. (sort (org-uniquify args) #'<))))
  668. (check-arity
  669. (lambda (arity macro)
  670. (let* ((name (org-element-property :key macro))
  671. (pos (org-element-property :begin macro))
  672. (args (org-element-property :args macro))
  673. (l (length args)))
  674. (cond
  675. ((< l (1- (car arity)))
  676. (push (list pos (format "Missing arguments in macro %S" name))
  677. reports))
  678. ((< l (car arity))
  679. (push (list pos (format "Missing argument in macro %S" name))
  680. reports))
  681. ((> l (1+ (cdr arity)))
  682. (push (let ((spurious-args (nthcdr (cdr arity) args)))
  683. (list pos
  684. (format "Spurious arguments in macro %S: %s"
  685. name
  686. (mapconcat #'org-trim spurious-args ", "))))
  687. reports))
  688. ((> l (cdr arity))
  689. (push (list pos
  690. (format "Spurious argument in macro %S: %s"
  691. name
  692. (org-last args)))
  693. reports))
  694. (t nil))))))
  695. ;; Check arguments for macro templates.
  696. (org-element-map ast 'keyword
  697. (lambda (k)
  698. (when (string= (org-element-property :key k) "MACRO")
  699. (let* ((value (org-element-property :value k))
  700. (name (and (string-match "^\\S-+" value)
  701. (match-string 0 value)))
  702. (template (and name
  703. (org-trim (substring value (match-end 0))))))
  704. (cond
  705. ((not name)
  706. (push (list (org-element-property :post-affiliated k)
  707. "Missing name in MACRO keyword")
  708. reports))
  709. ((not (org-string-nw-p template))
  710. (push (list (org-element-property :post-affiliated k)
  711. "Missing template in macro \"%s\"" name)
  712. reports))
  713. (t
  714. (unless (let ((args (funcall extract-placeholders template)))
  715. (equal (number-sequence 1 (or (org-last args) 0)) args))
  716. (push (list (org-element-property :post-affiliated k)
  717. (format "Unused placeholders in macro \"%s\""
  718. name))
  719. reports))))))))
  720. ;; Check arguments for macros.
  721. (org-macro-initialize-templates)
  722. (let ((templates (append
  723. (mapcar (lambda (m) (cons m "$1"))
  724. '("author" "date" "email" "title" "results"))
  725. org-macro-templates)))
  726. (org-element-map ast 'macro
  727. (lambda (macro)
  728. (let* ((name (org-element-property :key macro))
  729. (template (cdr (assoc-string name templates t))))
  730. (pcase template
  731. (`nil
  732. (push (list (org-element-property :begin macro)
  733. (format "Undefined macro %S" name))
  734. reports))
  735. ((guard (string= name "keyword"))
  736. (funcall check-arity '(1 . 1) macro))
  737. ((guard (string= name "modification-time"))
  738. (funcall check-arity '(1 . 2) macro))
  739. ((guard (string= name "n"))
  740. (funcall check-arity '(0 . 2) macro))
  741. ((guard (string= name "property"))
  742. (funcall check-arity '(1 . 2) macro))
  743. ((guard (string= name "time"))
  744. (funcall check-arity '(1 . 1) macro))
  745. ((pred functionp)) ;ignore (eval ...) templates
  746. (_
  747. (let* ((arg-numbers (funcall extract-placeholders template))
  748. (arity (if (null arg-numbers)
  749. '(0 . 0)
  750. (let ((m (apply #'max arg-numbers)))
  751. (cons m m)))))
  752. (funcall check-arity arity macro))))))))
  753. reports))
  754. (defun org-lint-undefined-footnote-reference (ast)
  755. (let ((definitions
  756. (org-element-map ast '(footnote-definition footnote-reference)
  757. (lambda (f)
  758. (and (or (eq 'footnote-definition (org-element-type f))
  759. (eq 'inline (org-element-property :type f)))
  760. (org-element-property :label f))))))
  761. (org-element-map ast 'footnote-reference
  762. (lambda (f)
  763. (let ((label (org-element-property :label f)))
  764. (and (eq 'standard (org-element-property :type f))
  765. (not (member label definitions))
  766. (list (org-element-property :begin f)
  767. (format "Missing definition for footnote [%s]"
  768. label))))))))
  769. (defun org-lint-unreferenced-footnote-definition (ast)
  770. (let ((references (org-element-map ast 'footnote-reference
  771. (lambda (f) (org-element-property :label f)))))
  772. (org-element-map ast 'footnote-definition
  773. (lambda (f)
  774. (let ((label (org-element-property :label f)))
  775. (and label
  776. (not (member label references))
  777. (list (org-element-property :post-affiliated f)
  778. (format "No reference for footnote definition [%s]"
  779. label))))))))
  780. (defun org-lint-colon-in-name (ast)
  781. (org-element-map ast org-element-all-elements
  782. (lambda (e)
  783. (let ((name (org-element-property :name e)))
  784. (and name
  785. (string-match-p ":" name)
  786. (list (progn
  787. (goto-char (org-element-property :begin e))
  788. (re-search-forward
  789. (format "^[ \t]*#\\+\\w+: +%s *$" (regexp-quote name)))
  790. (match-beginning 0))
  791. (format
  792. "Name \"%s\" contains a colon; Babel cannot use it as input"
  793. name)))))))
  794. (defun org-lint-misplaced-planning-info (_)
  795. (let ((case-fold-search t)
  796. reports)
  797. (while (re-search-forward org-planning-line-re nil t)
  798. (unless (memq (org-element-type (org-element-at-point))
  799. '(comment-block example-block export-block planning
  800. src-block verse-block))
  801. (push (list (line-beginning-position) "Misplaced planning info line")
  802. reports)))
  803. reports))
  804. (defun org-lint-incomplete-drawer (_)
  805. (let (reports)
  806. (while (re-search-forward org-drawer-regexp nil t)
  807. (let ((name (org-trim (match-string-no-properties 0)))
  808. (element (org-element-at-point)))
  809. (pcase (org-element-type element)
  810. (`drawer
  811. ;; Find drawer opening lines within non-empty drawers.
  812. (let ((end (org-element-property :contents-end element)))
  813. (when end
  814. (while (re-search-forward org-drawer-regexp end t)
  815. (let ((n (org-trim (match-string-no-properties 0))))
  816. (push (list (line-beginning-position)
  817. (format "Possible misleading drawer entry %S" n))
  818. reports))))
  819. (goto-char (org-element-property :end element))))
  820. (`property-drawer
  821. (goto-char (org-element-property :end element)))
  822. ((or `comment-block `example-block `export-block `src-block
  823. `verse-block)
  824. nil)
  825. (_
  826. ;; Find drawer opening lines outside of any drawer.
  827. (push (list (line-beginning-position)
  828. (format "Possible incomplete drawer %S" name))
  829. reports)))))
  830. reports))
  831. (defun org-lint-indented-diary-sexp (_)
  832. (let (reports)
  833. (while (re-search-forward "^[ \t]+%%(" nil t)
  834. (unless (memq (org-element-type (org-element-at-point))
  835. '(comment-block diary-sexp example-block export-block
  836. src-block verse-block))
  837. (push (list (line-beginning-position) "Possible indented diary-sexp")
  838. reports)))
  839. reports))
  840. (defun org-lint-invalid-block (_)
  841. (let ((case-fold-search t)
  842. (regexp "^[ \t]*#\\+\\(BEGIN\\|END\\)\\(?::\\|_[^[:space:]]*\\)?[ \t]*")
  843. reports)
  844. (while (re-search-forward regexp nil t)
  845. (let ((name (org-trim (buffer-substring-no-properties
  846. (line-beginning-position) (line-end-position)))))
  847. (cond
  848. ((and (string-prefix-p "END" (match-string 1) t)
  849. (not (eolp)))
  850. (push (list (line-beginning-position)
  851. (format "Invalid block closing line \"%s\"" name))
  852. reports))
  853. ((not (memq (org-element-type (org-element-at-point))
  854. '(center-block comment-block dynamic-block example-block
  855. export-block quote-block special-block
  856. src-block verse-block)))
  857. (push (list (line-beginning-position)
  858. (format "Possible incomplete block \"%s\""
  859. name))
  860. reports)))))
  861. reports))
  862. (defun org-lint-invalid-keyword-syntax (_)
  863. (let ((regexp "^[ \t]*#\\+\\([^[:space:]:]*\\)\\(?: \\|$\\)")
  864. (exception-re
  865. (format "[ \t]*#\\+%s\\(\\[.*\\]\\)?:\\(?: \\|$\\)"
  866. (regexp-opt org-element-dual-keywords)))
  867. reports)
  868. (while (re-search-forward regexp nil t)
  869. (let ((name (match-string-no-properties 1)))
  870. (unless (or (string-prefix-p "BEGIN" name t)
  871. (string-prefix-p "END" name t)
  872. (save-excursion
  873. (beginning-of-line)
  874. (let ((case-fold-search t)) (looking-at exception-re))))
  875. (push (list (match-beginning 0)
  876. (format "Possible missing colon in keyword \"%s\"" name))
  877. reports))))
  878. reports))
  879. (defun org-lint-extraneous-element-in-footnote-section (ast)
  880. (org-element-map ast 'headline
  881. (lambda (h)
  882. (and (org-element-property :footnote-section-p h)
  883. (org-element-map (org-element-contents h)
  884. (cl-remove-if
  885. (lambda (e)
  886. (memq e '(comment comment-block footnote-definition
  887. property-drawer section)))
  888. org-element-all-elements)
  889. (lambda (e)
  890. (not (and (eq (org-element-type e) 'headline)
  891. (org-element-property :commentedp e))))
  892. nil t '(footnote-definition property-drawer))
  893. (list (org-element-property :begin h)
  894. "Extraneous elements in footnote section are not exported")))))
  895. (defun org-lint-quote-section (ast)
  896. (org-element-map ast '(headline inlinetask)
  897. (lambda (h)
  898. (let ((title (org-element-property :raw-value h)))
  899. (and (or (string-prefix-p "QUOTE " title)
  900. (string-prefix-p (concat org-comment-string " QUOTE ") title))
  901. (list (org-element-property :begin h)
  902. "Deprecated QUOTE section"))))))
  903. (defun org-lint-file-application (ast)
  904. (org-element-map ast 'link
  905. (lambda (l)
  906. (let ((app (org-element-property :application l)))
  907. (and app
  908. (list (org-element-property :begin l)
  909. (format "Deprecated \"file+%s\" link type" app)))))))
  910. (defun org-lint-percent-encoding-link-escape (ast)
  911. (org-element-map ast 'link
  912. (lambda (l)
  913. (when (eq 'bracket (org-element-property :format l))
  914. (let* ((uri (org-element-property :path l))
  915. (start 0)
  916. (obsolete-flag
  917. (catch :obsolete
  918. (while (string-match "%\\(..\\)?" uri start)
  919. (setq start (match-end 0))
  920. (unless (member (match-string 1 uri) '("25" "5B" "5D" "20"))
  921. (throw :obsolete nil)))
  922. (string-match-p "%" uri))))
  923. (when obsolete-flag
  924. (list (org-element-property :begin l)
  925. "Link escaped with obsolete percent-encoding syntax")))))))
  926. (defun org-lint-wrong-header-argument (ast)
  927. (let* ((reports)
  928. (verify
  929. (lambda (datum language headers)
  930. (let ((allowed
  931. ;; If LANGUAGE is specified, restrict allowed
  932. ;; headers to both LANGUAGE-specific and default
  933. ;; ones. Otherwise, accept headers from any loaded
  934. ;; language.
  935. (append
  936. org-babel-header-arg-names
  937. (cl-mapcan
  938. (lambda (l)
  939. (let ((v (intern (format "org-babel-header-args:%s" l))))
  940. (and (boundp v) (mapcar #'car (symbol-value v)))))
  941. (if language (list language)
  942. (mapcar #'car org-babel-load-languages))))))
  943. (dolist (header headers)
  944. (let ((h (symbol-name (car header)))
  945. (p (or (org-element-property :post-affiliated datum)
  946. (org-element-property :begin datum))))
  947. (cond
  948. ((not (string-prefix-p ":" h))
  949. (push
  950. (list p
  951. (format "Missing colon in header argument \"%s\"" h))
  952. reports))
  953. ((assoc-string (substring h 1) allowed))
  954. (t (push (list p (format "Unknown header argument \"%s\"" h))
  955. reports)))))))))
  956. (org-element-map ast '(babel-call inline-babel-call inline-src-block keyword
  957. node-property src-block)
  958. (lambda (datum)
  959. (pcase (org-element-type datum)
  960. ((or `babel-call `inline-babel-call)
  961. (funcall verify
  962. datum
  963. nil
  964. (cl-mapcan #'org-babel-parse-header-arguments
  965. (list
  966. (org-element-property :inside-header datum)
  967. (org-element-property :end-header datum)))))
  968. (`inline-src-block
  969. (funcall verify
  970. datum
  971. (org-element-property :language datum)
  972. (org-babel-parse-header-arguments
  973. (org-element-property :parameters datum))))
  974. (`keyword
  975. (when (string= (org-element-property :key datum) "PROPERTY")
  976. (let ((value (org-element-property :value datum)))
  977. (when (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. (string-match "\\`HEADER-ARGS\\(?::\\(\\S-+\\)\\)?\\+?"
  988. key))
  989. (funcall verify
  990. datum
  991. (match-string 1 key)
  992. (org-babel-parse-header-arguments
  993. (org-element-property :value datum))))))
  994. (`src-block
  995. (funcall verify
  996. datum
  997. (org-element-property :language datum)
  998. (cl-mapcan #'org-babel-parse-header-arguments
  999. (cons (org-element-property :parameters datum)
  1000. (org-element-property :header datum))))))))
  1001. reports))
  1002. (defun org-lint-wrong-header-value (ast)
  1003. (let (reports)
  1004. (org-element-map ast
  1005. '(babel-call inline-babel-call inline-src-block src-block)
  1006. (lambda (datum)
  1007. (let* ((type (org-element-type datum))
  1008. (language (org-element-property :language datum))
  1009. (allowed-header-values
  1010. (append (and language
  1011. (let ((v (intern (concat "org-babel-header-args:"
  1012. language))))
  1013. (and (boundp v) (symbol-value v))))
  1014. org-babel-common-header-args-w-values))
  1015. (datum-header-values
  1016. (org-babel-parse-header-arguments
  1017. (org-trim
  1018. (pcase type
  1019. (`src-block
  1020. (mapconcat
  1021. #'identity
  1022. (cons (org-element-property :parameters datum)
  1023. (org-element-property :header datum))
  1024. " "))
  1025. (`inline-src-block
  1026. (or (org-element-property :parameters datum) ""))
  1027. (_
  1028. (concat
  1029. (org-element-property :inside-header datum)
  1030. " "
  1031. (org-element-property :end-header datum))))))))
  1032. (dolist (header datum-header-values)
  1033. (let ((allowed-values
  1034. (cdr (assoc-string (substring (symbol-name (car header)) 1)
  1035. allowed-header-values))))
  1036. (unless (memq allowed-values '(:any nil))
  1037. (let ((values (cdr header))
  1038. groups-alist)
  1039. (dolist (v (if (stringp values) (split-string values)
  1040. (list values)))
  1041. (let ((valid-value nil))
  1042. (catch 'exit
  1043. (dolist (group allowed-values)
  1044. (cond
  1045. ((not (funcall
  1046. (if (stringp v) #'assoc-string #'assoc)
  1047. v group))
  1048. (when (memq :any group)
  1049. (setf valid-value t)
  1050. (push (cons group v) groups-alist)))
  1051. ((assq group groups-alist)
  1052. (push
  1053. (list
  1054. (or (org-element-property :post-affiliated datum)
  1055. (org-element-property :begin datum))
  1056. (format
  1057. "Forbidden combination in header \"%s\": %s, %s"
  1058. (car header)
  1059. (cdr (assq group groups-alist))
  1060. v))
  1061. reports)
  1062. (throw 'exit nil))
  1063. (t (push (cons group v) groups-alist)
  1064. (setf valid-value t))))
  1065. (unless valid-value
  1066. (push
  1067. (list
  1068. (or (org-element-property :post-affiliated datum)
  1069. (org-element-property :begin datum))
  1070. (format "Unknown value \"%s\" for header \"%s\""
  1071. v
  1072. (car header)))
  1073. reports))))))))))))
  1074. reports))
  1075. (defun org-lint-spurious-colons (ast)
  1076. (org-element-map ast '(headline inlinetask)
  1077. (lambda (h)
  1078. (when (member "" (org-element-property :tags h))
  1079. (list (org-element-property :begin h)
  1080. "Tags contain a spurious colon")))))
  1081. (defun org-lint-non-existent-bibliography (ast)
  1082. (org-element-map ast 'keyword
  1083. (lambda (k)
  1084. (when (equal "BIBLIOGRAPHY" (org-element-property :key k))
  1085. (let ((file (org-strip-quotes (org-element-property :value k))))
  1086. (and (not (file-remote-p file))
  1087. (not (file-exists-p file))
  1088. (list (org-element-property :begin k)
  1089. (format "Non-existent bibliography %S" file))))))))
  1090. (defun org-lint-missing-print-bibliography (ast)
  1091. (and (org-element-map ast 'citation #'identity nil t)
  1092. (not (org-element-map ast 'keyword
  1093. (lambda (k)
  1094. (equal "PRINT_BIBLIOGRAPHY" (org-element-property :key k)))
  1095. nil t))
  1096. (list
  1097. (list (point-max) "Possibly missing \"PRINT_BIBLIOGRAPHY\" keyword"))))
  1098. (defun org-lint-invalid-cite-export-declaration (ast)
  1099. (org-element-map ast 'keyword
  1100. (lambda (k)
  1101. (when (equal "CITE_EXPORT" (org-element-property :key k))
  1102. (let ((value (org-element-property :value k))
  1103. (source (org-element-property :begin k)))
  1104. (if (equal value "")
  1105. (list source "Missing export processor name")
  1106. (condition-case _
  1107. (pcase (org-cite-read-processor-declaration value)
  1108. (`(,(and (pred symbolp) name)
  1109. ,(pred string-or-null-p)
  1110. ,(pred string-or-null-p))
  1111. (unless (org-cite-get-processor name)
  1112. (list source "Unknown cite export processor %S" name)))
  1113. (_
  1114. (list source "Invalid cite export processor declaration")))
  1115. (error
  1116. (list source "Invalid cite export processor declaration")))))))))
  1117. (defun org-lint-incomplete-citation (ast)
  1118. (org-element-map ast 'plain-text
  1119. (lambda (text)
  1120. (and (string-match-p org-element-citation-prefix-re text)
  1121. ;; XXX: The code below signals the error at the beginning
  1122. ;; of the paragraph containing the faulty object. It is
  1123. ;; not very accurate but may be enough for now.
  1124. (list (org-element-property :contents-begin
  1125. (org-element-property :parent text))
  1126. "Possibly incomplete citation markup")))))
  1127. ;;; Checkers declaration
  1128. (org-lint-add-checker 'duplicate-custom-id
  1129. "Report duplicates CUSTOM_ID properties"
  1130. #'org-lint-duplicate-custom-id
  1131. :categories '(link))
  1132. (org-lint-add-checker 'duplicate-name
  1133. "Report duplicate NAME values"
  1134. #'org-lint-duplicate-name
  1135. :categories '(babel 'link))
  1136. (org-lint-add-checker 'duplicate-target
  1137. "Report duplicate targets"
  1138. #'org-lint-duplicate-target
  1139. :categories '(link))
  1140. (org-lint-add-checker 'duplicate-footnote-definition
  1141. "Report duplicate footnote definitions"
  1142. #'org-lint-duplicate-footnote-definition
  1143. :categories '(footnote))
  1144. (org-lint-add-checker 'orphaned-affiliated-keywords
  1145. "Report orphaned affiliated keywords"
  1146. #'org-lint-orphaned-affiliated-keywords
  1147. :trust 'low)
  1148. (org-lint-add-checker 'obsolete-affiliated-keywords
  1149. "Report obsolete affiliated keywords"
  1150. #'org-lint-obsolete-affiliated-keywords
  1151. :categories '(obsolete))
  1152. (org-lint-add-checker 'deprecated-export-blocks
  1153. "Report deprecated export block syntax"
  1154. #'org-lint-deprecated-export-blocks
  1155. :trust 'low :categories '(obsolete export))
  1156. (org-lint-add-checker 'deprecated-header-syntax
  1157. "Report deprecated Babel header syntax"
  1158. #'org-lint-deprecated-header-syntax
  1159. :trust 'low :categories '(obsolete babel))
  1160. (org-lint-add-checker 'missing-language-in-src-block
  1161. "Report missing language in source blocks"
  1162. #'org-lint-missing-language-in-src-block
  1163. :categories '(babel))
  1164. (org-lint-add-checker 'missing-backend-in-export-block
  1165. "Report missing back-end in export blocks"
  1166. #'org-lint-missing-backend-in-export-block
  1167. :categories '(export))
  1168. (org-lint-add-checker 'invalid-babel-call-block
  1169. "Report invalid Babel call blocks"
  1170. #'org-lint-invalid-babel-call-block
  1171. :categories '(babel))
  1172. (org-lint-add-checker 'colon-in-name
  1173. "Report NAME values with a colon"
  1174. #'org-lint-colon-in-name
  1175. :categories '(babel))
  1176. (org-lint-add-checker 'wrong-header-argument
  1177. "Report wrong babel headers"
  1178. #'org-lint-wrong-header-argument
  1179. :categories '(babel))
  1180. (org-lint-add-checker 'wrong-header-value
  1181. "Report invalid value in babel headers"
  1182. #'org-lint-wrong-header-value
  1183. :categories '(babel) :trust 'low)
  1184. (org-lint-add-checker 'deprecated-category-setup
  1185. "Report misuse of CATEGORY keyword"
  1186. #'org-lint-deprecated-category-setup
  1187. :categories '(obsolete))
  1188. (org-lint-add-checker 'invalid-coderef-link
  1189. "Report \"coderef\" links with unknown destination"
  1190. #'org-lint-invalid-coderef-link
  1191. :categories '(link))
  1192. (org-lint-add-checker 'invalid-custom-id-link
  1193. "Report \"custom-id\" links with unknown destination"
  1194. #'org-lint-invalid-custom-id-link
  1195. :categories '(link))
  1196. (org-lint-add-checker 'invalid-fuzzy-link
  1197. "Report \"fuzzy\" links with unknown destination"
  1198. #'org-lint-invalid-fuzzy-link
  1199. :categories '(link))
  1200. (org-lint-add-checker 'invalid-id-link
  1201. "Report \"id\" links with unknown destination"
  1202. #'org-lint-invalid-id-link
  1203. :categories '(link))
  1204. (org-lint-add-checker 'link-to-local-file
  1205. "Report links to non-existent local files"
  1206. #'org-lint-link-to-local-file
  1207. :categories '(link) :trust 'low)
  1208. (org-lint-add-checker 'non-existent-setupfile-parameter
  1209. "Report SETUPFILE keywords with non-existent file parameter"
  1210. #'org-lint-non-existent-setupfile-parameter
  1211. :trust 'low)
  1212. (org-lint-add-checker 'wrong-include-link-parameter
  1213. "Report INCLUDE keywords with misleading link parameter"
  1214. #'org-lint-wrong-include-link-parameter
  1215. :categories '(export) :trust 'low)
  1216. (org-lint-add-checker 'obsolete-include-markup
  1217. "Report obsolete markup in INCLUDE keyword"
  1218. #'org-lint-obsolete-include-markup
  1219. :categories '(obsolete export) :trust 'low)
  1220. (org-lint-add-checker 'unknown-options-item
  1221. "Report unknown items in OPTIONS keyword"
  1222. #'org-lint-unknown-options-item
  1223. :categories '(export) :trust 'low)
  1224. (org-lint-add-checker 'invalid-macro-argument-and-template
  1225. "Report spurious macro arguments or invalid macro templates"
  1226. #'org-lint-invalid-macro-argument-and-template
  1227. :categories '(export) :trust 'low)
  1228. (org-lint-add-checker 'special-property-in-properties-drawer
  1229. "Report special properties in properties drawers"
  1230. #'org-lint-special-property-in-properties-drawer
  1231. :categories '(properties))
  1232. (org-lint-add-checker 'obsolete-properties-drawer
  1233. "Report obsolete syntax for properties drawers"
  1234. #'org-lint-obsolete-properties-drawer
  1235. :categories '(obsolete properties))
  1236. (org-lint-add-checker 'invalid-effort-property
  1237. "Report invalid duration in EFFORT property"
  1238. #'org-lint-invalid-effort-property
  1239. :categories '(properties))
  1240. (org-lint-add-checker 'undefined-footnote-reference
  1241. "Report missing definition for footnote references"
  1242. #'org-lint-undefined-footnote-reference
  1243. :categories '(footnote))
  1244. (org-lint-add-checker 'unreferenced-footnote-definition
  1245. "Report missing reference for footnote definitions"
  1246. #'org-lint-unreferenced-footnote-definition
  1247. :categories '(footnote))
  1248. (org-lint-add-checker 'extraneous-element-in-footnote-section
  1249. "Report non-footnote definitions in footnote section"
  1250. #'org-lint-extraneous-element-in-footnote-section
  1251. :categories '(footnote))
  1252. (org-lint-add-checker 'invalid-keyword-syntax
  1253. "Report probable invalid keywords"
  1254. #'org-lint-invalid-keyword-syntax
  1255. :trust 'low)
  1256. (org-lint-add-checker 'invalid-block
  1257. "Report invalid blocks"
  1258. #'org-lint-invalid-block
  1259. :trust 'low)
  1260. (org-lint-add-checker 'misplaced-planning-info
  1261. "Report misplaced planning info line"
  1262. #'org-lint-misplaced-planning-info
  1263. :trust 'low)
  1264. (org-lint-add-checker 'incomplete-drawer
  1265. "Report probable incomplete drawers"
  1266. #'org-lint-incomplete-drawer
  1267. :trust 'low)
  1268. (org-lint-add-checker 'indented-diary-sexp
  1269. "Report probable indented diary-sexps"
  1270. #'org-lint-indented-diary-sexp
  1271. :trust 'low)
  1272. (org-lint-add-checker 'quote-section
  1273. "Report obsolete QUOTE section"
  1274. #'org-lint-quote-section
  1275. :categories '(obsolete) :trust 'low)
  1276. (org-lint-add-checker 'file-application
  1277. "Report obsolete \"file+application\" link"
  1278. #'org-lint-file-application
  1279. :categories '(link obsolete))
  1280. (org-lint-add-checker 'percent-encoding-link-escape
  1281. "Report obsolete escape syntax in links"
  1282. #'org-lint-percent-encoding-link-escape
  1283. :categories '(link obsolete) :trust 'low)
  1284. (org-lint-add-checker 'spurious-colons
  1285. "Report spurious colons in tags"
  1286. #'org-lint-spurious-colons
  1287. :categories '(tags))
  1288. (org-lint-add-checker 'non-existent-bibliography
  1289. "Report invalid bibliography file"
  1290. #'org-lint-non-existent-bibliography
  1291. :categories '(cite))
  1292. (org-lint-add-checker 'missing-print-bibliography
  1293. "Report missing \"print_bibliography\" keyword"
  1294. #'org-lint-missing-print-bibliography
  1295. :categories '(cite))
  1296. (org-lint-add-checker 'invalid-cite-export-declaration
  1297. "Report invalid value for \"cite_export\" keyword"
  1298. #'org-lint-invalid-cite-export-declaration
  1299. :categories '(cite))
  1300. (org-lint-add-checker 'incomplete-citation
  1301. "Report incomplete citation object"
  1302. #'org-lint-incomplete-citation
  1303. :categories '(cite) :trust 'low)
  1304. (provide 'org-lint)
  1305. ;; Local variables:
  1306. ;; generated-autoload-file: "org-loaddefs.el"
  1307. ;; End:
  1308. ;;; org-lint.el ends here