org-lint.el 44 KB

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