diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el index 228a4484616..2697f1a3107 100644 --- a/lisp/progmodes/python.el +++ b/lisp/progmodes/python.el @@ -1491,10 +1491,18 @@ Optional argument NOEND is internal and makes the logic to not jump to the end of line when moving forward searching for the end of the statement." (interactive "^") - (let (string-start bs-pos) + (let (string-start bs-pos (last-string-end 0)) (while (and (or noend (goto-char (line-end-position))) (not (eobp)) (cond ((setq string-start (python-syntax-context 'string)) + ;; The assertion can only fail if syntax table + ;; text properties and the `syntax-ppss' cache + ;; are somehow out of whack. This has been + ;; observed when using `syntax-ppss' during + ;; narrowing. + (cl-assert (> string-start last-string-end) + :show-args + "Overlapping strings detected") (goto-char string-start) (if (python-syntax-context 'paren) ;; Ended up inside a paren, roll again. @@ -1504,8 +1512,10 @@ of the statement." (goto-char (+ (point) (python-syntax-count-quotes (char-after (point)) (point)))) - (or (re-search-forward (rx (syntax string-delimiter)) nil t) - (goto-char (point-max))))) + (setq last-string-end + (or (re-search-forward + (rx (syntax string-delimiter)) nil t) + (goto-char (point-max)))))) ((python-syntax-context 'paren) ;; The statement won't end before we've escaped ;; at least one level of parenthesis. diff --git a/test/lisp/progmodes/python-tests.el b/test/lisp/progmodes/python-tests.el index 1e6b867d30b..2f4c2fb849d 100644 --- a/test/lisp/progmodes/python-tests.el +++ b/test/lisp/progmodes/python-tests.el @@ -5314,6 +5314,25 @@ class SomeClass: (or enabled (hs-minor-mode -1))))) +(ert-deftest python-tests--python-nav-end-of-statement--infloop () + "Checks that `python-nav-end-of-statement' doesn't infloop in a +buffer with overlapping strings." + (python-tests-with-temp-buffer "''' '\n''' ' '\n" + (syntax-propertize (point-max)) + ;; Create a situation where strings nominally overlap. This + ;; shouldn't happen in practice, but apparently it can happen when + ;; a package calls `syntax-ppss' in a narrowed buffer during JIT + ;; lock. + (put-text-property 4 5 'syntax-table (string-to-syntax "|")) + (remove-text-properties 8 9 '(syntax-table nil)) + (goto-char 4) + (setq-local syntax-propertize-function nil) + ;; The next form should not infloop. We have to disable + ;; ‘debug-on-error’ so that ‘cl-assert’ doesn’t call the debugger. + (should-error (let ((debug-on-error nil)) + (python-nav-end-of-statement))) + (should (eolp)))) + (provide 'python-tests)