aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/lint-org.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-25 01:50:23 -0500
committerCraig Jennings <c@cjennings.net>2026-07-25 01:50:23 -0500
commit8822b0dab56ca50269771b4c9d06b78657c2b8b4 (patch)
treee318861637cdcf3281f123fe4ca01b9957d78c4e /.ai/scripts/lint-org.el
parentf7139d82173ccf9effdcdee50df22912f3f05fa0 (diff)
downloadrulesets-8822b0dab56ca50269771b4c9d06b78657c2b8b4.tar.gz
rulesets-8822b0dab56ca50269771b4c9d06b78657c2b8b4.zip
fix(lint-org): stop flagging correctly paired verbatim blocks
org-lint's invalid-block checker flags both delimiters of a paired example or src block whose body holds a heading-shaped line. Its parser reads the `** ` as a structural break and loses the open block. Every org file that documents org syntax inside a block collects two false findings. The checker is org-lint's own, not one lint-org implements, so I filtered its output instead of patching a local checker. `lo--matched-block-regions` pairs blocks by scanning lines under org's real rule (once open, only the block's own `#+end_TYPE` closes it). A finding inside a matched region gets dropped, delimiters included, since that's where org-lint reports. I scanned lines rather than asking org because org's parser is what's confused. An unterminated block never lands in a matched region, so it still reports. Three tests cover the false positives: a heading in an example block, a literal `#+end_example` inside a src block, and uppercase delimiters. Two more guard the real finding, including a file with one paired and one unterminated block that proves the suppression is per-block.
Diffstat (limited to '.ai/scripts/lint-org.el')
-rw-r--r--.ai/scripts/lint-org.el53
1 files changed, 53 insertions, 0 deletions
diff --git a/.ai/scripts/lint-org.el b/.ai/scripts/lint-org.el
index 928f83b..33dc52f 100644
--- a/.ai/scripts/lint-org.el
+++ b/.ai/scripts/lint-org.el
@@ -306,6 +306,52 @@ Craig-specific annotation marker rather than Babel src-block syntax."
(lo--goto-line line)
(looking-at-p "^[ \t]*#\\+begin_src[ \t]+cj:")))
+(defvar-local lo--matched-blocks-cache nil
+ "Cons of (TICK . REGIONS) memoizing `lo--matched-block-regions'.
+TICK is the `buffer-chars-modified-tick' the regions were computed at, so a
+fix applied mid-pass invalidates them.")
+
+(defun lo--matched-block-regions ()
+ "Return ((BEGIN-LINE . END-LINE) ...) for every correctly paired block.
+Scans lines directly rather than asking org, because org's own parser is what
+mis-reads these blocks: a heading-shaped line inside a verbatim body reads as a
+structural break and loses the open block. The scan applies org's real rule —
+once a block is open, only its own `#+end_TYPE' closes it, so a nested
+`#+begin_' or a foreign `#+end_' in the body is just text."
+ (let ((tick (buffer-chars-modified-tick)))
+ (if (eql (car lo--matched-blocks-cache) tick)
+ (cdr lo--matched-blocks-cache)
+ (let ((case-fold-search t)
+ (regions nil) (open-type nil) (open-line nil) (line 0))
+ (save-excursion
+ (goto-char (point-min))
+ (while (not (eobp))
+ (setq line (1+ line))
+ (let ((text (buffer-substring-no-properties
+ (line-beginning-position) (line-end-position))))
+ (cond
+ (open-type
+ (when (string-match
+ (format "\\`[ \t]*#\\+end_%s[ \t]*\\'"
+ (regexp-quote open-type))
+ text)
+ (push (cons open-line line) regions)
+ (setq open-type nil open-line nil)))
+ ((string-match "\\`[ \t]*#\\+begin_\\([^ \t\n]+\\)" text)
+ (setq open-type (match-string 1 text)
+ open-line line))))
+ (forward-line 1)))
+ (setq lo--matched-blocks-cache (cons tick (nreverse regions)))
+ (cdr lo--matched-blocks-cache)))))
+
+(defun lo--in-matched-block-p (line)
+ "Non-nil when LINE sits within a correctly paired block, delimiters included.
+org-lint reports `invalid-block' at the delimiter lines themselves, so the
+range has to be inclusive for the suppression to reach them."
+ (cl-some (lambda (region)
+ (and (>= line (car region)) (<= line (cdr region))))
+ (lo--matched-block-regions)))
+
(defun lo--handle-item (item)
(let ((name (lo--checker-name item))
(line (lo--line item))
@@ -318,6 +364,13 @@ Craig-specific annotation marker rather than Babel src-block syntax."
wrong-header-argument))
(lo--cj-comment-block-opener-p line))
nil)
+ ;; `invalid-block' on a block that is in fact correctly paired — the
+ ;; checker is org-lint's own, so this filters its output rather than
+ ;; fixing a local checker. A genuinely unterminated block isn't in any
+ ;; matched region, so it still reports.
+ ((and (eq name 'invalid-block)
+ (lo--in-matched-block-p line))
+ nil)
((eq name 'item-number)
(lo--apply-or-preview name line msg #'lo-fix-item-number))
((eq name 'missing-language-in-src-block)