Normalize reversed bounds and validate SUBEXP in the pattern engine

SRC-1: tp--pattern-apply-single now swaps START > END bounds before
branching on the object type, so the string path stops signaling a
raw substring args-out-of-range while the buffer path keeps its
historical narrow-to-region swap; the behavior is now uniform and
documented in all six tp-match-*/tp-regexp-* docstrings.

SRC-2: a SUBEXP larger than the pattern's capture-group count (per
regexp-opt-depth) signals "Regexp X has no group N" instead of
silently matching nothing, while legal non-participating and
zero-width groups keep working quietly.

Also corrects the return wording of tp-match-set/tp-regexp-set (a
NEW string for string objects, not "the modified string") and adds
the missing return sections to tp-match-add/tp-regexp-add, matching
the verified tp-match-reset behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kinneyzhang 2026-07-27 02:24:11 +08:00
parent e870b87941
commit 575c99be88
2 changed files with 93 additions and 21 deletions

View File

@ -684,5 +684,46 @@ never registered the buffer (REG-1)."
(tp-layer-reset)
(setq tp-search-reg1b-color nil)))
;;; SRC-1: reversed START/END bounds are swapped on both object paths
(ert-deftest tp-search-test-reversed-bounds-string-swaps ()
"String-path START > END is swapped instead of signaling out-of-range."
(let ((res (tp-match-set "o" '(face bold) "foo" 2 1)))
(should (eq (get-text-property 1 'face res) 'bold))
(should-not (get-text-property 2 'face res)))
;; Swapped bounds behave exactly like the same bounds in order.
(should (equal-including-properties
(tp-regexp-set "o+" '(face bold) "foo" 3 1)
(tp-regexp-set "o+" '(face bold) "foo" 1 3))))
(ert-deftest tp-search-test-reversed-bounds-buffer-swaps ()
"Buffer-path START > END keeps its historical swap behavior."
(with-temp-buffer
(insert "foo")
(should (equal (tp-match-set "o" '(face bold) nil 3 2)
'((2 . 3))))
(should (eq (get-text-property 2 'face) 'bold))
(should-not (get-text-property 3 'face))))
;;; SRC-2: SUBEXP beyond the pattern's group count signals clearly
(ert-deftest tp-search-test-subexp-beyond-group-count-errors ()
"A SUBEXP larger than the pattern's group count errors on both paths."
(should-error (tp-regexp-set "abc" '(face bold)
(copy-sequence "abc") nil nil 5))
(with-temp-buffer
(insert "abc")
(should-error (tp-regexp-add "a\\(b\\)c" '(face bold) nil nil nil 2))
;; Nothing was applied before the error.
(should-not (get-text-property 1 'face))))
(ert-deftest tp-search-test-subexp-non-participating-group-quiet ()
"A legal group that never participates still returns nil quietly."
(with-temp-buffer
(insert "xbc")
(should (null (tp-regexp-set "\\(a\\)bc\\|xbc" '(face bold)
nil nil nil 1)))
(should-not (get-text-property 1 'face))))
(provide 'tp-search-tests)
;;; tp-search-tests.el ends here

View File

@ -49,7 +49,9 @@ is a regexp. APPLY-FN is called with (START END PROPS OBJECT) for
each match.
START and END restrict matching to the [START, END) portion of
OBJECT, in native coordinates (0-based for strings, 1-based for
buffers); nil means the object's bounds. Matching behaves as if
buffers); nil means the object's bounds. If START > END the bounds
are swapped (matching the buffer path's historical narrow-to-region
behavior, now uniform across object types). Matching behaves as if
OBJECT consisted only of that portion (the buffer path narrows, the
string path matches against the substring), so no match crosses the
boundaries.
@ -57,7 +59,9 @@ When SUBEXP is non-nil, it names a capture group of PATTERN: the
properties and returned regions cover (match-beginning SUBEXP) to
\(match-end SUBEXP) of each match, and a match in which that group
does not participate contributes nothing. The scan still advances
past the whole match.
past the whole match. A SUBEXP larger than PATTERN's group count
\(per `regexp-opt-depth') signals an error instead of silently
matching nothing.
For strings, returns a NEW string with properties applied
\(non-destructive).
For buffers, modifies in-place and returns list of regions.
@ -66,6 +70,14 @@ Zero-width matches (an empty literal pattern, or a regexp that can
match the empty string) are recorded and the scan advances one
position past them, so the search always terminates."
(let ((regexp (if literal (regexp-quote pattern) pattern)))
;; Reversed bounds are swapped, not signaled: the buffer path's
;; narrow-to-region always did this, so the string path follows.
(when (and start end (> start end))
(cl-rotatef start end))
;; A group number beyond the pattern's group count could never
;; match; make the typo loud instead of a silent no-op.
(when (and subexp (> subexp (regexp-opt-depth regexp)))
(error "Regexp %S has no group %d" pattern subexp))
(cond
;; String object
((stringp object)
@ -247,12 +259,13 @@ or `define-tp-group'.
OBJECT is a buffer or string; nil means current buffer.
START and END restrict matching to the [START, END) portion of
OBJECT, in native coordinates (0-based for strings, 1-based for
buffers); nil means the object's bounds. Matching behaves as if
OBJECT consisted only of that portion, so no match crosses the
boundaries.
buffers); nil means the object's bounds. If START > END the bounds
are swapped. Matching behaves as if OBJECT consisted only of that
portion, so no match crosses the boundaries.
Returns:
- For strings: the modified string
- For strings: a NEW string with properties applied (the original
string is not modified)
- For buffers: list of (START . END) pairs for all matches."
(tp--match-apply pattern (tp--ensure-props plist) #'tp-set object
start end))
@ -269,12 +282,14 @@ or `define-tp-group'.
OBJECT is a buffer or string; nil means current buffer.
START and END restrict matching to the [START, END) portion of
OBJECT, in native coordinates (0-based for strings, 1-based for
buffers); nil means the object's bounds.
buffers); nil means the object's bounds. If START > END the bounds
are swapped.
Unlike `tp-match-set', this completely replaces all existing properties.
For strings, returns a NEW string (original is not modified).
For buffers, modifies in-place and returns list of regions."
For buffers, modifies in-place and returns list of (START . END)
regions."
(tp--match-apply pattern (tp--ensure-props plist)
#'tp--reset-apply
object start end))
@ -303,9 +318,14 @@ or `define-tp-group'.
OBJECT is a buffer or string; nil means current buffer.
START and END restrict matching to the [START, END) portion of
OBJECT, in native coordinates (0-based for strings, 1-based for
buffers); nil means the object's bounds.
buffers); nil means the object's bounds. If START > END the bounds
are swapped.
Unlike `tp-match-set', this deeply merges nested properties."
Unlike `tp-match-set', this deeply merges nested properties.
For strings, returns a NEW string (original is not modified).
For buffers, modifies in-place and returns list of (START . END)
regions."
(tp--match-apply pattern (tp--ensure-props plist) #'tp--deep-merge-apply
object start end))
@ -322,16 +342,18 @@ or `define-tp-group'.
OBJECT is a buffer or string; nil means current buffer.
START and END restrict matching to the [START, END) portion of
OBJECT, in native coordinates (0-based for strings, 1-based for
buffers); nil means the object's bounds. Matching behaves as if
OBJECT consisted only of that portion, so no match crosses the
boundaries.
buffers); nil means the object's bounds. If START > END the bounds
are swapped. Matching behaves as if OBJECT consisted only of that
portion, so no match crosses the boundaries.
When SUBEXP is non-nil, it names a capture group of PATTERN (1 for
the first group, like font-lock highlights): properties apply to that
group of each match instead of the whole match, and a match in which
the group does not participate contributes nothing.
the group does not participate contributes nothing. A SUBEXP larger
than PATTERN's group count signals an error.
Returns:
- For strings: the modified string
- For strings: a NEW string with properties applied (the original
string is not modified)
- For buffers: list of (START . END) pairs for all matches."
(tp--regexp-apply pattern (tp--ensure-props plist) #'tp-set object
start end subexp))
@ -348,15 +370,18 @@ or `define-tp-group'.
OBJECT is a buffer or string; nil means current buffer.
START and END restrict matching to the [START, END) portion of
OBJECT, in native coordinates (0-based for strings, 1-based for
buffers); nil means the object's bounds.
buffers); nil means the object's bounds. If START > END the bounds
are swapped.
When SUBEXP is non-nil, properties apply to that capture group of
each match instead of the whole match; a match in which the group
does not participate contributes nothing.
does not participate contributes nothing. A SUBEXP larger than
PATTERN's group count signals an error.
Unlike `tp-regexp-set', this completely replaces all existing properties.
For strings, returns a NEW string (original is not modified).
For buffers, modifies in-place and returns list of regions."
For buffers, modifies in-place and returns list of (START . END)
regions."
(tp--regexp-apply pattern (tp--ensure-props plist)
#'tp--reset-apply
object start end subexp))
@ -373,12 +398,18 @@ or `define-tp-group'.
OBJECT is a buffer or string; nil means current buffer.
START and END restrict matching to the [START, END) portion of
OBJECT, in native coordinates (0-based for strings, 1-based for
buffers); nil means the object's bounds.
buffers); nil means the object's bounds. If START > END the bounds
are swapped.
When SUBEXP is non-nil, properties apply to that capture group of
each match instead of the whole match; a match in which the group
does not participate contributes nothing.
does not participate contributes nothing. A SUBEXP larger than
PATTERN's group count signals an error.
Unlike `tp-regexp-set', this deeply merges nested properties."
Unlike `tp-regexp-set', this deeply merges nested properties.
For strings, returns a NEW string (original is not modified).
For buffers, modifies in-place and returns list of (START . END)
regions."
(tp--regexp-apply pattern (tp--ensure-props plist) #'tp--deep-merge-apply
object start end subexp))