aboutsummaryrefslogtreecommitdiff
path: root/modules/config-utilities.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-30 09:25:09 -0500
committerCraig Jennings <c@cjennings.net>2026-04-30 09:25:09 -0500
commitf90a087d5025952f0ca1b81d322f29891a40e540 (patch)
tree3d95e20b7e4a9f4022c461fbf8d330e6eff164fb /modules/config-utilities.el
parentc5ef22aacd5954106f55c8c7b6ae52f7f7bbfa76 (diff)
downloaddotemacs-f90a087d5025952f0ca1b81d322f29891a40e540.tar.gz
dotemacs-f90a087d5025952f0ca1b81d322f29891a40e540.zip
fix(config-utilities): repair validate-org-agenda-timestamps property check
Two bugs in cj/validate-org-agenda-timestamps surfaced while extracting testable helpers. 1. The DEADLINE / SCHEDULED / TIMESTAMP property lookup used (intern (downcase prop)) as the key, producing 'deadline, 'scheduled, 'timestamp. org-element-property expects keywords (:deadline, :scheduled, :timestamp) and returns nil for plain symbols. The property-check branch had never reported anything since the function was written. Only inline-regex matches inside headline contents have ever been flagged. Fixed by building the keyword form: (intern (concat ":" (downcase prop))). 2. Once #1 is fixed, every property timestamp would also match the inline-timestamp regex during the contents scan (since the DEADLINE: / SCHEDULED: / TIMESTAMP lines fall inside contents-begin/end on a parsed headline), producing duplicate reports. Added a per-headline list of property timestamp strings and a member check before pushing an inline match. The function is also restructured into three pieces to make it testable: - cj/--validate-timestamps-in-buffer FILE — pure-ish: walks the current buffer, returns a list of (FILE POS HEAD PROP TS) tuples. - cj/--format-validation-report-section FILE INVALID — pure: returns the per-file org-formatted string. - cj/validate-org-agenda-timestamps (interactive) — orchestrates both helpers across org-agenda-files into a report buffer. The interactive entry-point's behaviour is unchanged from the user's side except that DEADLINE / SCHEDULED / TIMESTAMP property timestamps are now actually checked.
Diffstat (limited to 'modules/config-utilities.el')
-rw-r--r--modules/config-utilities.el96
1 files changed, 65 insertions, 31 deletions
diff --git a/modules/config-utilities.el b/modules/config-utilities.el
index 9b913d17..8d094fda 100644
--- a/modules/config-utilities.el
+++ b/modules/config-utilities.el
@@ -306,6 +306,69 @@ Returns the count of files deleted."
;; ------------------------ Validate Org Agenda Entries ------------------------
+(defun cj/--validate-timestamps-in-buffer (file)
+ "Scan the current buffer for invalid org timestamps.
+Walks every headline. Checks DEADLINE / SCHEDULED / TIMESTAMP
+properties plus inline timestamps in headline contents. An inline
+match whose raw text equals a property timestamp on the same headline
+is not reported a second time.
+
+Returns a list of (FILE POS HEADLINE-TEXT PROP TIMESTAMP-STRING) tuples
+in document order. FILE is the value passed in; the function does not
+look it up itself."
+ (require 'org)
+ (require 'org-element)
+ (let ((invalid '())
+ (props '("DEADLINE" "SCHEDULED" "TIMESTAMP"))
+ (parse-tree (org-element-parse-buffer 'headline)))
+ (org-element-map parse-tree 'headline
+ (lambda (hl)
+ (let ((headline-text (org-element-property :raw-value hl))
+ (begin-pos (org-element-property :begin hl))
+ (property-timestamps '()))
+ (dolist (prop props)
+ (let ((timestamp (org-element-property
+ (intern (concat ":" (downcase prop))) hl)))
+ (when timestamp
+ (let ((time-str (org-element-property :raw-value timestamp)))
+ (push time-str property-timestamps)
+ (unless (ignore-errors (org-time-string-to-absolute time-str))
+ (push (list file begin-pos headline-text prop time-str)
+ invalid))))))
+ (let ((contents-begin (org-element-property :contents-begin hl))
+ (contents-end (org-element-property :contents-end hl)))
+ (when (and contents-begin contents-end)
+ (save-excursion
+ (goto-char contents-begin)
+ (while (re-search-forward org-ts-regexp contents-end t)
+ (let ((ts-string (match-string 0)))
+ (unless (or (member ts-string property-timestamps)
+ (ignore-errors
+ (org-time-string-to-absolute ts-string)))
+ (push (list file begin-pos headline-text
+ "inline timestamp" ts-string)
+ invalid))))))))))
+ (nreverse invalid)))
+
+(defun cj/--format-validation-report-section (file invalid-entries)
+ "Return the per-FILE string section for the timestamp validation report.
+INVALID-ENTRIES is a list of (FILE POS HEADLINE PROP TS) tuples as
+returned by `cj/--validate-timestamps-in-buffer'. An empty list
+produces a section with the \"No invalid timestamps found.\" line."
+ (concat
+ (format "* %s\n" file)
+ (if invalid-entries
+ (mapconcat
+ (lambda (entry)
+ (cl-destructuring-bind (f pos head prop ts) entry
+ (format
+ "- [[file:%s::%d][%s]]\n - Property/Type: %s\n - Invalid timestamp: \"%s\"\n"
+ f pos head prop ts)))
+ invalid-entries
+ "")
+ "No invalid timestamps found.\n")
+ "\n"))
+
(defun cj/validate-org-agenda-timestamps ()
"Scan all files in `org-agenda-files' for invalid timestamps.
Checks DEADLINE, SCHEDULED, TIMESTAMP properties and inline timestamps in
@@ -322,38 +385,9 @@ entries, property/type, and raw timestamp string."
(insert "* Overview\nScan of org-agenda-files for invalid timestamps.\n\n"))
(dolist (file org-agenda-files)
(with-current-buffer (find-file-noselect file)
- (let ((invalid-entries '())
- (props '("DEADLINE" "SCHEDULED" "TIMESTAMP"))
- (parse-tree (org-element-parse-buffer 'headline)))
- (org-element-map parse-tree 'headline
- (lambda (hl)
- (let ((headline-text (org-element-property :raw-value hl))
- (begin-pos (org-element-property :begin hl)))
- (dolist (prop props)
- (let ((timestamp (org-element-property (intern (downcase prop)) hl)))
- (when timestamp
- (let ((time-str (org-element-property :raw-value timestamp)))
- (unless (ignore-errors (org-time-string-to-absolute time-str))
- (push (list file begin-pos headline-text prop time-str) invalid-entries))))))
- (let ((contents-begin (org-element-property :contents-begin hl))
- (contents-end (org-element-property :contents-end hl)))
- (when (and contents-begin contents-end)
- (save-excursion
- (goto-char contents-begin)
- (while (re-search-forward org-ts-regexp contents-end t)
- (let ((ts-string (match-string 0)))
- (unless (ignore-errors (org-time-string-to-absolute ts-string))
- (push (list file begin-pos headline-text "inline timestamp" ts-string) invalid-entries))))))))))
-
+ (let ((invalid (cj/--validate-timestamps-in-buffer file)))
(with-current-buffer report-buffer
- (insert (format "* %s\n" file))
- (if invalid-entries
- (dolist (entry (reverse invalid-entries))
- (cl-destructuring-bind (f pos head prop ts) entry
- (insert (format "- [[file:%s::%d][%s]]\n - Property/Type: %s\n - Invalid timestamp: \"%s\"\n"
- f pos head prop ts))))
- (insert "No invalid timestamps found.\n")))
- (with-current-buffer report-buffer (insert "\n")))))
+ (insert (cj/--format-validation-report-section file invalid))))))
(pop-to-buffer report-buffer)))
;; --------------------------- Org-Alert-Check Timers --------------------------