Fix "Odd length text property list" error when property value is nil

The bug occurred because tp--parse-args was incorrectly omitting nil values
from the property list, producing (face) instead of (face nil).

Fixed the same bug pattern in:
- tp--parse-args (used by tp-set, tp-reset, tp-add)
- tp-add-to-layers
- tp-add-to-all-layers

Added 6 new regression tests for nil property values.

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-05 09:52:26 +00:00
parent 4cca06dedc
commit d11d109f7b
2 changed files with 60 additions and 11 deletions

View File

@ -3766,5 +3766,59 @@ internally, and the final result should have face properties, not tp-palette."
(should (listp mouse-face-prop))
(should (memq 'highlight mouse-face-prop))))))
;;; ============================================================
;;; Nil Value Property Tests (Issue: "Odd length text property list")
;;; ============================================================
(ert-deftest tp-test-set-with-nil-value ()
"Test tp-set with nil value produces valid property list.
Regression test for: (tp-set \"emacs\" 'face nil) erroring with
\"Odd length text property list\"."
(tp-test-with-temp-buffer
(let ((result (tp-set "emacs" 'face nil)))
;; Result should be #("emacs" 0 5 (face nil))
(should (stringp result))
(should (eq (get-text-property 0 'face result) nil))
;; Verify the property list is valid (has even length)
(let ((props (text-properties-at 0 result)))
(should (= (% (length props) 2) 0))))))
(ert-deftest tp-test-set-with-nil-value-in-middle ()
"Test tp-set with nil value in middle of property list."
(tp-test-with-temp-buffer
(let ((result (tp-set "emacs" 'face 'bold 'help-echo nil 'display "test")))
;; Result should have face=bold, help-echo=nil, display="test"
(should (eq (get-text-property 0 'face result) 'bold))
(should (eq (get-text-property 0 'help-echo result) nil))
(should (equal (get-text-property 0 'display result) "test")))))
(ert-deftest tp-test-set-with-multiple-nil-values ()
"Test tp-set with multiple nil values."
(tp-test-with-temp-buffer
(let ((result (tp-set "emacs" 'face nil 'help-echo nil)))
(should (eq (get-text-property 0 'face result) nil))
(should (eq (get-text-property 0 'help-echo result) nil)))))
(ert-deftest tp-test-reset-with-nil-value ()
"Test tp-reset with nil value works correctly."
(tp-test-with-temp-buffer
(let ((result (tp-reset "emacs" 'face nil)))
;; Result should have face=nil
(should (eq (get-text-property 0 'face result) nil)))))
(ert-deftest tp-test-add-with-nil-value ()
"Test tp-add with nil value works correctly."
(tp-test-with-temp-buffer
(let ((result (tp-add "emacs" 'face nil)))
;; Result should have face=nil
(should (eq (get-text-property 0 'face result) nil)))))
(ert-deftest tp-test-set-nil-value-in-buffer ()
"Test tp-set with nil value in buffer region."
(tp-test-with-temp-buffer
(insert "emacs")
(tp-set 1 6 '(face nil))
(should (eq (tp-at 1 'face) nil))))
(provide 'tp-ert-tests)
;;; tp-ert-tests.el ends here

17
tp.el
View File

@ -1055,11 +1055,10 @@ Supports multiple calling conventions:
(null rest))
(setq props (list end-or-prop)))
;; Standard flat plist: (tp-set "str" 'prop1 val1 'prop2 val2 ...)
;; Always include props-or-val even if it's nil, to handle (tp-set "str" 'prop nil)
(t
(setq props (if end-or-prop
(if props-or-val
(cons end-or-prop (cons props-or-val rest))
(list end-or-prop))
(cons end-or-prop (cons props-or-val rest))
nil)))))
;; First arg is a number - region convention
((numberp start-or-string)
@ -3924,11 +3923,9 @@ Returns the modified object (string) or nil for buffer operations."
start 0
end (length start-or-string))
;; Construct plist from end-or-plist, plist-or-object, and rest
;; Always include plist-or-object even if nil, to handle (... 'prop nil)
(when end-or-plist
(setq plist (cons end-or-plist
(if plist-or-object
(cons plist-or-object rest)
rest)))))
(setq plist (cons end-or-plist (cons plist-or-object rest)))))
;; Region form: (tp-add-to-layers ids start end plist object)
((numberp start-or-string)
(setq start start-or-string
@ -3996,11 +3993,9 @@ Returns the modified object (string) or nil for buffer operations."
start 0
end (length start-or-string))
;; Construct plist from end-or-plist, plist-or-object, and rest
;; Always include plist-or-object even if nil, to handle (... 'prop nil)
(when end-or-plist
(setq plist (cons end-or-plist
(if plist-or-object
(cons plist-or-object rest)
rest)))))
(setq plist (cons end-or-plist (cons plist-or-object rest)))))
;; Region form: (tp-add-to-all-layers start end plist object)
((numberp start-or-string)
(setq start start-or-string