;;; xr-test.el --- Tests for xr.el                   -*- lexical-binding: t -*-

;; Copyright (C) 2019-2020 Free Software Foundation, Inc.

;; Author: Mattias Engdegård <mattias.engdegard@gmail.com>

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.


(require 'xr)
(require 'ert)

(defmacro xr-test--error (form)
  `(condition-case e ,form (error e)))

(ert-deftest xr-basic ()
  (should (equal (xr "a\\$b\\\\c\\[\\]\\q")
                 "a$b\\c[]q"))
  (should (equal (xr "\\(?:ab\\|c*d\\)?")
                 '(opt (or "ab" (seq (zero-or-more "c") "d")))))
  (should (equal (xr ".+")
                 '(one-or-more nonl)))
  (should (equal (xr-test--error (xr "$b\\"))
                 '(xr-parse-error "Backslash at end of regexp" 2 2)))
  (let ((text-quoting-style 'grave))
    (should (equal (xr-lint "$b\\")
                   '(((0 0 "Unescaped literal `$'" warning))
                     ((2 2 "Backslash at end of regexp" error)))))
    ))

(ert-deftest xr-repeat ()
  (should (equal (xr "\\(?:x?y\\)\\{3\\}")
                 '(= 3 (opt "x") "y")))
  (should (equal (xr "\\(?:x?y\\)\\{3,8\\}")
                 '(repeat 3 8 (opt "x") "y")))
  (should (equal (xr "\\(?:x?y\\)\\{3,\\}")
                     '(>= 3 (opt "x") "y")))
  (should (equal (xr "\\(?:x?y\\)\\{,8\\}")
                 '(repeat 0 8 (opt "x") "y")))
  (should (equal (xr "\\(?:xy\\)\\{4,4\\}")
                 '(= 4 "xy")))
  (should (equal (xr "a\\{,\\}")
                 '(zero-or-more "a")))
  (should (equal (xr "a\\{0\\}")
                 '(repeat 0 0 "a")))
  (should (equal (xr "a\\{0,\\}")
                 '(zero-or-more "a")))
  (should (equal (xr "a\\{0,0\\}")
                 '(repeat 0 0 "a")))
  (should (equal (xr "a\\{\\}")
                 '(repeat 0 0 "a")))
  (should (equal (xr "a\\{,1\\}")
                 '(repeat 0 1 "a")))
  (should (equal (xr "a\\{1,\\}")
                 '(>= 1 "a")))
  (should (equal (xr-test--error (xr "a\\{3,2\\}"))
                 '(xr-parse-error "Invalid repetition interval" 3 5)))
  (should (equal (xr-test--error (xr "a\\{1,2,3\\}"))
                 '(xr-parse-error "Missing \\}" 1 5)))
  )

(ert-deftest xr-backref ()
  (should (equal (xr "\\(ab\\)\\(?3:cd\\)\\1\\3")
                 '(seq (group "ab") (group-n 3 "cd") (backref 1) (backref 3))))
  (should (equal (xr "\\01")
                 "01"))
  (should (equal (xr-test--error (xr "\\(?abc\\)"))
                 '(xr-parse-error "Invalid \\(? syntax" 0 2)))
  (should (equal (xr-test--error (xr "\\(?2\\)"))
                 '(xr-parse-error "Invalid \\(? syntax" 0 2)))
  (should (equal (xr-test--error (xr "\\(?0:xy\\)"))
                 '(xr-parse-error "Invalid \\(? syntax" 0 2)))
  (should (equal (xr "\\(?29:xy\\)")
                 '(group-n 29 "xy")))
  (should (equal (xr-test--error (xr "\\(c?"))
                 '(xr-parse-error "Missing \\)" 0 3)))
  (should (equal (xr-test--error (xr "xy\\)"))
                 '(xr-parse-error "Unbalanced \\)" 2 3)))
  )

(ert-deftest xr-misc ()
  (should (equal (xr "^.\\w\\W\\`\\'\\=\\b\\B\\<\\>\\_<\\_>$")
                 '(seq bol nonl (syntax word) (not (syntax word)) bos eos point
                       word-boundary not-word-boundary bow eow
                       symbol-start symbol-end eol)))
  (should (equal (xr-test--error (xr "\\_a"))
                 '(xr-parse-error "Invalid \\_ sequence" 0 2)))
  (should (equal (xr "^\\(?:\\(?:\\)\\(?:\\)\\).$")
                 '(seq bol nonl eol)))
  )

(ert-deftest xr-syntax ()
  (should (equal (xr "\\s-\\s \\sw\\sW\\s_\\s.\\s(\\s)\\s\"")
                 '(seq (syntax whitespace) (syntax whitespace) (syntax word)
                       (syntax word)
                       (syntax symbol) (syntax punctuation)
                       (syntax open-parenthesis) (syntax close-parenthesis)
                       (syntax string-quote))))
  (should (equal (xr "\\s\\\\s/\\s$\\s'\\s<\\s>\\s!\\s|")
                 '(seq (syntax escape) (syntax character-quote)
                       (syntax paired-delimiter) (syntax expression-prefix)
                       (syntax comment-start) (syntax comment-end)
                       (syntax comment-delimiter) (syntax string-delimiter))))
  (should (equal (xr "\\S-\\S<")
                 '(seq (not (syntax whitespace))
                       (not (syntax comment-start)))))
  (should (equal (xr-test--error (xr "\\s"))
                 '(xr-parse-error "Incomplete \\s sequence" 0 1)))
  (should (equal (xr-test--error (xr "\\S"))
                 '(xr-parse-error "Incomplete \\S sequence" 0 1)))
  (let ((text-quoting-style 'grave))
    (should (equal (xr-test--error (xr "\\sq"))
                   '(xr-parse-error "Unknown syntax code `q'" 0 2)))
    (should (equal (xr-test--error (xr "\\Sq"))
                   '(xr-parse-error "Unknown syntax code `q'" 0 2)))
    ))

(ert-deftest xr-category ()
  (should (equal (xr "\\c0\\c1\\c2\\c3\\c4\\c5\\c6\\c7\\c8\\c9\\c<\\c>")
                 '(seq (category consonant) (category base-vowel)
                       (category upper-diacritical-mark)
                       (category lower-diacritical-mark)
                       (category tone-mark) (category symbol) (category digit)
                       (category vowel-modifying-diacritical-mark)
                       (category vowel-sign) (category semivowel-lower)
                       (category not-at-end-of-line)
                       (category not-at-beginning-of-line))))
  (should (equal (xr "\\cA\\cC\\cG\\cH\\cI\\cK\\cN\\cY\\c^")
          '(seq (category alpha-numeric-two-byte) (category chinese-two-byte)
                (category greek-two-byte) (category japanese-hiragana-two-byte)
                (category indian-two-byte)
                (category japanese-katakana-two-byte)
                (category korean-hangul-two-byte) (category cyrillic-two-byte)
                (category combining-diacritic))))
  (should (equal (xr "\\ca\\cb\\cc\\ce\\cg\\ch\\ci\\cj\\ck\\cl\\co\\cq\\cr")
          '(seq (category ascii) (category arabic) (category chinese)
                (category ethiopic) (category greek) (category korean)
                (category indian)  (category japanese)
                (category japanese-katakana) (category latin) (category lao)
                (category tibetan) (category japanese-roman))))
  (should (equal (xr "\\ct\\cv\\cw\\cy\\c|")
                 '(seq (category thai) (category vietnamese) (category hebrew)
                       (category cyrillic) (category can-break))))
  (should (equal (xr "\\C2\\C^")
                 '(seq (not (category upper-diacritical-mark))
                       (not (category combining-diacritic)))))
  (should (equal (xr "\\cR\\C.\\cL\\C ")
                 '(seq (category strong-right-to-left)
                       (not (category base)) (category strong-left-to-right)
                       (not (category space-for-indent)))))
  (should (equal (xr "\\c%\\C+")
                 '(seq (category ?%) (not (category ?+)))))
  (should (equal (xr-test--error (xr "\\c"))
                 '(xr-parse-error "Incomplete \\c sequence" 0 1)))
  (should (equal (xr-test--error (xr "\\C"))
                 '(xr-parse-error "Incomplete \\C sequence" 0 1)))
  )

(ert-deftest xr-lazy ()
  (should (equal (xr "\\(?:a.\\)*?")
                 '(*? "a" nonl)))
  (should (equal (xr "\\(?:a.\\)+?")
                 '(+? "a" nonl)))
  (should (equal (xr "\\(?:a.\\)??")
                 '(?? "a" nonl)))
  (should (equal (xr "\\(?:.\\(a+\\(?:b+?c*\\)?\\)??\\)*")
                 '(zero-or-more
                   nonl
                   (?? (group (one-or-more "a")
                              (opt (+? "b")
                                   (zero-or-more "c")))))))
  )

(ert-deftest xr-char-classes ()
  (should (equal (xr "[[:alnum:][:blank:]][[:alpha:]][[:cntrl:][:digit:]]")
                 '(seq (any alnum blank) alpha (any cntrl digit))))
  (should (equal (xr "[^[:lower:][:punct:]][^[:space:]]")
                 '(seq (not (any lower punct)) (not space))))
  (should (equal (xr "^[a-z]*")
                 '(seq bol (zero-or-more (any "a-z")))))
  (should (equal (xr "some[.]thing")
                 "some.thing"))
  (should (equal (xr "[^]-c]")
                 '(not (any "]-c"))))
  (should (equal (xr "[-^]")
                 '(any "^-")))
  (should (equal (xr "[a-z-+/*%0-4[:xdigit:]]")
                 '(any "0-4a-z" "%*+/-" xdigit)))
  (should (equal (xr "[^]A-Za-z-]*")
                 '(zero-or-more (not (any "A-Za-z" "]-")))))
  (should (equal (xr "[+*%A-Ka-k0-3${-}]")
                 '(any "0-3A-Ka-k{-}" "$%*+")))
  (should (equal (xr "[^\\\\o][A-\\\\][A-\\\\-a]")
                 '(seq (not (any "\\o")) (any "A-\\") (any "A-a"))))
  (should (equal (xr "[^A-FFGI-LI-Mb-da-eg-ki-ns-tz-v]")
                 '(not (any "A-FI-Ma-eg-ns-t" "G"))))
  (should (equal (xr "[z-a][^z-a]")
                 '(seq (any) anything)))
  (should (equal (xr "[[:alpha]]")
                 '(seq (any ":[ahlp") "]")))
  (should (equal (xr "[:alpha:]")
                 '(any ":ahlp")))
  (should (equal (xr "[[:digit:]-z]")
                 '(any "z-" digit)))
  (should (equal (xr "[A-[:digit:]]")
                 '(seq (any "A-[" ":dgit") "]")))
  (should (equal (xr "[^\n]")
                 'nonl))
  (let ((text-quoting-style 'grave))
    (should (equal (xr-test--error (xr "[[::]]"))
                   '(xr-parse-error "No character class `[::]'" 1 4)))
    (should (equal (xr-test--error (xr "[[:=:]]"))
                   '(xr-parse-error "No character class `[:=:]'" 1 5)))
    (should (equal (xr-test--error (xr "[[:letter:]]"))
                   '(xr-parse-error "No character class `[:letter:]'" 1 10)))
    (should (equal (xr-test--error (xr "[a-f"))
                   '(xr-parse-error "Unterminated character alternative" 0 3)))
    )
  (should (equal (xr "[aaaaaa][bananabanana][aaaa-cccc][a-ca-ca-c]")
                 '(seq "a" (any "abn") (any "a-c") (any "a-c"))))
  (should (equal (xr "[a-fb-gc-h][a-fc-kh-p]")
                 '(seq (any "a-h") (any "a-p"))))
  )

(ert-deftest xr-empty ()
  (should (equal (xr "")
                 ""))
  (should (equal (xr "a\\|")
                 '(or "a" "")))
  (should (equal (xr "\\|a")
                 '(or "" "a")))
  (should (equal (xr "a\\|\\|b")
                 '(or "a" "" "b")))
  )

(ert-deftest xr-anything ()
  (should (equal (xr "\\(?:.\\|\n\\)?\\(\n\\|.\\)*")
                 '(seq (opt anything) (zero-or-more (group anything)))))
  )

(ert-deftest xr-real ()
  (should (equal (xr "\\*\\*\\* EOOH \\*\\*\\*\n")
                 "*** EOOH ***\n"))
  (should (equal (xr "\\<\\(catch\\|finally\\)\\>[^_]")
                 '(seq bow (group (or "catch" "finally")) eow
                       (not (any "_")))))
  (should (equal (xr "[ \t\n]*:\\([^:]+\\|$\\)")
                 '(seq (zero-or-more (any "\t\n ")) ":"
                       (group (or (one-or-more (not (any ":")))
                                  eol)))))
  )

(ert-deftest xr-edge-cases ()
  (should (equal (xr "^a^b\\(?:^c^\\|^d^\\|e^\\)^")
                 '(seq bol "a^b" (or (seq bol "c^") (seq bol "d^") "e^") "^")))
  (should (equal (xr "$a$b\\(?:$c$\\|$d$\\|$e$\\)$")
                 '(seq "$a$b" (or (seq "$c" eol) (seq "$d" eol) (seq "$e" eol))
                       eol)))
  (should (equal (xr "*a\\|*b\\(*c\\)")
                 '(or "*a" (seq "*b" (group "*c")))))
  (should (equal (xr "+a\\|+b\\(+c\\)")
                 '(or "+a" (seq "+b" (group "+c")))))
  (should (equal (xr "?a\\|?b\\(^?c\\)")
                 '(or "?a" (seq "?b" (group bol "?c")))))
  (should (equal (xr "^**")
                 '(seq bol (zero-or-more "*"))))
  (should (equal (xr "^+")
                 '(seq bol "+")))
  (should (equal (xr "^?")
                 '(seq bol "?")))
  (should (equal (xr "*?a\\|^??b")
                 '(or (seq (opt "*") "a") (seq bol (opt "?") "b"))))
  (should (equal (xr "^\\{xy")
                 '(seq bol "{xy")))
  (should (equal (xr "\\{2,3\\}")
                 "{2,3}"))
  (should (equal (xr "\\(?:^\\)*")
                 '(zero-or-more bol)))
  (should (equal (xr "\\(?:^\\)\\{3\\}")
                 '(= 3 bol)))
  (should (equal (xr "\\^+")
                 '(one-or-more "^")))
  (should (equal (xr "\\c^?")
                 '(opt (category combining-diacritic))))
  (should (equal (xr "a^*")
                 '(seq "a" (zero-or-more "^"))))
  (should (equal (xr "a^\\{2,7\\}")
                 '(seq "a" (repeat 2 7 "^"))))
  (should (equal (xr "a\\|\\`?b")
                 '(or "a" (seq bos "?b"))))
  (should (equal (xr "a\\|\\`\\{3,4\\}b")
                 '(or "a" (seq bos "{3,4}b"))))
  (should (equal (xr "\\(?:\\`\\)*")
                 '(zero-or-more bos)))
  (should (equal (xr "\\(?:\\`\\)\\{3,4\\}")
                 '(repeat 3 4 bos)))
  )

(ert-deftest xr-simplify ()
  (should (equal (xr "a\\(?:b?\\(?:c.\\)d*\\)e")
                 '(seq "a" (opt "b") "c" nonl (zero-or-more "d") "e")))
  (should (equal (xr "a\\(?:b\\(?:c.d\\)e\\)f")
                 '(seq "abc" nonl "def")))
  )

(ert-deftest xr-pretty ()
  (should (equal (xr-pp-rx-to-str "A\e\r\n\t\0 \x7f\x80\ B\xff\x02")
                 "\"A\\e\\r\\n\\t\\x00 \\x7f\\200B\\xff\\x02\"\n"))
  (should (equal (xr-pp-rx-to-str '(?? nonl))
                 "(?? nonl)\n"))
  (should (equal (xr-pp-rx-to-str '(? ?\s))
                 "(? ?\\s)\n"))
  (should (equal (xr-pp-rx-to-str '(+? (*? ?*)))
                 "(+? (*? ?*))\n"))
  (should (equal (xr-pp-rx-to-str '(seq "a" ?a ?\s ?\n ?\" ?\\ ?0 ?\0 ?\177))
                 "(seq \"a\" ?a ?\\s ?\\n ?\\\" ?\\\\ ?0 #x00 #x7f)\n"))
  (should (equal (xr-pp-rx-to-str '(category ?Q))
                 "(category ?Q)\n"))
  (should (equal (xr-pp-rx-to-str '(any ?a ?\n ?\( ?\\ ?\200 ?Å ?Ω #x3fff80 32))
                 "(any ?a ?\\n ?\\( ?\\\\ #x80 ?Å ?Ω #x3fff80 ?\\s)\n"))
  (should (equal (xr-pp-rx-to-str '(any (?0 . ?9)))
                 "(any (?0 . ?9))\n"))
  (should (equal (xr-pp-rx-to-str '(repeat 42 ?a))
                 "(repeat 42 ?a)\n"))
  (should (equal (xr-pp-rx-to-str '(repeat 10 13 ?b))
                 "(repeat 10 13 ?b)\n"))
  (should (equal (xr-pp-rx-to-str '(** 9 32 ?c))
                 "(** 9 32 ?c)\n"))
  (should (equal (xr-pp-rx-to-str '(= 3 ?d))
                 "(= 3 ?d)\n"))
  (should (equal (xr-pp-rx-to-str '(>= 8 ?e))
                 "(>= 8 ?e)\n"))
  (should (equal (xr-pp-rx-to-str '(group-n 7 ?f))
                 "(group-n 7 ?f)\n"))
  (should (equal (xr-pp-rx-to-str '(backref 12 ?g))
                 "(backref 12 ?g)\n"))
  (defvar pp-default-function)   ; introduced in Emacs 30
  (let ((indent-tabs-mode nil)
        (pp-default-function 'pp-28))
    (should (equal (xr-pp-rx-to-str
                    '(seq (1+ nonl
                              (or "a"
                                  (not (any space))))
                          (* (? (not cntrl)
                                blank
                                (| nonascii "abcdef")))))
                   (concat
                    "(seq (1+ nonl\n"
                    "         (or \"a\"\n"
                    "             (not (any space))))\n"
                    "     (* (? (not cntrl)\n"
                    "           blank\n"
                    "           (| nonascii \"abcdef\"))))\n")))
    (with-temp-buffer
      (should (equal (xr-pp ".?\\|b+") nil))
      (should (equal (buffer-string)
                     (concat
                      "(or (opt nonl)\n"
                      "    (one-or-more \"b\"))\n"))))
    (with-temp-buffer
      (should (equal (xr-skip-set-pp "^ac-nq\\-u") nil))
      (should (equal (buffer-string) "(not (any \"c-n\" \"aqu-\"))\n"))))
  )

(ert-deftest xr-dialect ()
  (should (equal (xr "a*b+c?d\\{2,5\\}\\(e\\|f\\)[gh][^ij]" 'medium)
                 '(seq (zero-or-more "a") (one-or-more "b") (opt "c")
                       (repeat 2 5 "d") (group (or "e" "f"))
                       (any "gh") (not (any "ij")))))
  (should (equal (xr "a*b+c?d\\{2,5\\}\\(e\\|f\\)[gh][^ij]" 'verbose)
                 '(seq (zero-or-more "a") (one-or-more "b") (zero-or-one "c")
                       (repeat 2 5 "d") (group (or "e" "f"))
                       (any "gh") (not (any "ij")))))
  (should (equal (xr "a*b+c?d\\{2,5\\}\\(e\\|f\\)[gh][^ij]" 'brief)
                 '(seq (0+ "a") (1+ "b") (opt "c")
                       (repeat 2 5 "d") (group (or "e" "f"))
                       (any "gh") (not (any "ij")))))
  (should (equal (xr "a*b+c?d\\{2,5\\}\\(e\\|f\\)[gh][^ij]" 'terse)
                 '(: (* "a") (+ "b") (? "c")
                     (** 2 5 "d") (group (| "e" "f"))
                     (in "gh") (not (in "ij")))))
  (should (equal (xr "^\\`\\<.\\>\\'$" 'medium)
                 '(seq bol bos bow nonl eow eos eol)))
  (should (equal (xr "^\\`\\<.\\>\\'$" 'verbose)
                 '(seq line-start string-start word-start not-newline
                       word-end string-end line-end)))
  (should (equal (xr "^\\`\\<.\\>\\'$" 'brief)
                 '(seq bol bos bow nonl eow eos eol)))
  (should (equal (xr "^\\`\\<.\\>\\'$" 'terse)
                 '(: bol bos bow nonl eow eos eol)))
  (should-error (xr "a" 'asdf))
  )

(ert-deftest xr-lint ()
  (let ((text-quoting-style 'grave))
    (should (equal (xr-lint "^a*\\[\\?\\$\\(b\\{3\\}\\|c\\)$")
                   nil))
    (should (equal (xr-lint "a^b$c")
                   '(((1 1 "Unescaped literal `^'" warning))
                     ((3 3 "Unescaped literal `$'" warning)))))
    (should (equal (xr-lint "^**$")
                   '(((1 1 "Unescaped literal `*'" warning)))))
    (should (equal (xr-lint "a\\|\\`?b")
                   '(((5 5 "Unescaped literal `?'" warning)))))
    (should (equal (xr-lint "a\\|\\`\\{3,4\\}b")
                   '(((5 6 "Escaped non-special character `{'" warning))
                     ((10 11 "Escaped non-special character `}'" warning)))))
    (should (equal (xr-lint "\\{\\(+\\|?\\)\\[\\]\\}\\\t")
                   '(((0  1 "Escaped non-special character `{'" warning))
                     ((4  4 "Unescaped literal `+'" warning))
                     ((7  7 "Unescaped literal `?'" warning))
                     ((14 15 "Escaped non-special character `}'" warning))
                     ((16 17 "Escaped non-special character `\\t'" warning)))))
    (should (equal (xr-lint "\\}\\w\\a\\b\\%")
                   '(((0 1 "Escaped non-special character `}'" warning))
                     ((4 5 "Escaped non-special character `a'" warning))
                     ((8 9 "Escaped non-special character `%'" warning)))))
    (should (equal (xr-lint "a?+b+?\\(?:c*\\)*d\\{3\\}+e*?\\{2,5\\}")
                   '(((2 2 "Repetition of option" warning)
                      (0 1 "This is the inner expression" info))
                     ((14 14 "Repetition of repetition" warning)
                      (6 13 "This is the inner expression" info))
                     ((25 31 "Repetition of repetition" warning)
                      (22 24 "This is the inner expression" info)))))
    (should (equal (xr-lint "\\(?:a+\\)?")
                   nil))
    (should (equal (xr-lint "\\(a*\\)*\\(b+\\)*\\(c*\\)?\\(d+\\)?")
                   '(((6 6 "Repetition of repetition" warning)
                      (0 5 "This is the inner expression" info))
                     ((13 13 "Repetition of repetition" warning)
                      (7 12 "This is the inner expression" info))
                     ((20 20 "Optional repetition" warning)
                      (14 19 "This is the inner expression" info)))))
    (should (equal (xr-lint "\\(a?\\)+\\(b?\\)?")
                   '(((6 6 "Repetition of option" warning)
                      (0 5 "This is the inner expression" info))
                     ((13 13 "Optional option" warning)
                      (7 12 "This is the inner expression" info)))))
    (should (equal (xr-lint "\\(e*\\)\\{3\\}")
                   '(((6 10 "Repetition of repetition" warning)
                      (0 5 "This is the inner expression" info)))))
    (should (equal (xr-lint "\\(a?\\)\\{4,7\\}")
                   '(((6 12 "Repetition of option" warning)
                      (0 5 "This is the inner expression" info)))))
    (should (equal (xr-lint "\\(?:a?b+c?d*\\)*")
                   '(((14 14 "Repetition of effective repetition" warning)
                      (0 13 "This expression contains a repetition" info)))))
    (should (equal (xr-lint "\\(a?b+c?d*\\)*")
                   '(((12 12 "Repetition of effective repetition" warning)
                      (0 11 "This expression contains a repetition" info)))))
    (should (equal (xr-lint "a*\\|b+\\|\\(?:a\\)*")
                   '(((8 15 "Duplicated alternative branch" warning)
                      (0 1 "Previous occurrence here" info)))))
    (should (equal (xr-lint "a\\{,\\}")
                   '(((1 5 "Uncounted repetition" warning)))))
    (should (equal (xr-lint "a\\{\\}")
                   '(((1 4 "Implicit zero repetition" warning)))))
    (should (equal (xr-lint "\\'*\\<?\\(?:$\\)+")
                   '(((2 2 "Repetition of zero-width assertion" warning)
                      (0 1 "Zero-width assertion here" info))
                     ((5 5 "Optional zero-width assertion" warning)
                      (3 4 "Zero-width assertion here" info))
                     ((13 13 "Repetition of zero-width assertion" warning)
                      (6 12 "Zero-width assertion here" info)))))
    (should
     (equal
      (xr-lint "\\b\\{2\\}\\(a\\|\\|b\\)\\{,8\\}")
      '(((2 6 "Repetition of zero-width assertion" warning)
         (0 1 "Zero-width assertion here" info))
        ((17 22 "Repetition of expression matching an empty string" warning)
         (7 16 "This expression matches an empty string" info)))))
    (should (equal (xr-lint "\\(?:\\`\\)*")
                   '(((8 8 "Repetition of zero-width assertion" warning)
                      (0 7 "Zero-width assertion here" info)))))
    (should (equal (xr-lint "\\(?:\\`\\)\\{3,4\\}")
                   '(((8 14 "Repetition of zero-width assertion" warning)
                      (0 7 "Zero-width assertion here" info)))))
    ))

(ert-deftest xr-lint-char-alt ()
  (let ((text-quoting-style 'grave))
    (should (equal (xr-lint "[^]\\a-d^-]")
                   nil))
    (should
     (equal (xr-lint "a[\\\\[]b[d-g.d-g]c")
            '(((3 3 "Duplicated `\\' inside character alternative" warning)
               (2 2 "Previous occurrence here" info))
              ((12 14 "Duplicated `d-g' inside character alternative" warning)
               (8 10 "Previous occurrence here" info)))))
    (should (equal (xr-lint "[]-Qa-fz-t]")
                   '(((1 3 "Reversed range `]-Q' matches nothing" warning))
                     ((7 9 "Reversed range `z-t' matches nothing" warning)))))
    (should (equal (xr-lint "[z-a][^z-a]")
                   nil))
    (should (equal (xr-lint "[^A-FFGI-LI-Mb-da-eg-ki-ns-t33-7]")
                   '(((5 5 "Character `F' included in range `A-F'" warning)
                      (2 4 "Previous occurrence here" info))
                     ((10 12 "Ranges `I-L' and `I-M' overlap" warning)
                      (7 9 "Previous occurrence here" info))
                     ((16 18 "Ranges `a-e' and `b-d' overlap" warning)
                      (13 15 "Previous occurrence here" info))
                     ((22 24 "Ranges `g-k' and `i-n' overlap" warning)
                      (19 21 "Previous occurrence here" info))
                     ((25 27 "Two-character range `s-t'" warning))
                     ((29 31 "Range `3-7' includes character `3'" warning)
                      (28 28 "Previous occurrence here" info)))))
    (should (equal (xr-lint "[a[:digit:]b[:punct:]c[:digit:]]")
                   '(((22 30 "Duplicated character class `[:digit:]'" warning)
                      (2 10 "Previous occurrence here" info)))))
    (should
     (equal (xr-lint "[+[:alnum]][b[:x]]")
            '(((2 9 "Possibly missing `:' after character class" warning)))))
    (should (equal (xr-lint "[0-9[|]*/]")
                   '(((4 4 "Suspect `[' in char alternative" warning)))))
    (should (equal (xr-lint "[^][-].]")
                   nil))
    (should (equal (xr-lint "\\[\\([^\\[]*\\)\\]$")
                   nil))
    (should (equal (xr-lint "[0-1]")
                   nil))
    (should (equal (xr-lint "[^]-][]-^]")
                   '(((6 8 "Two-character range `]-^'" warning)))))
    (should
     (equal
      (xr-lint "[-A-Z][A-Z-][A-Z-a][^-A-Z][]-a][A-Z---.]")
      '(((16 16
             "Literal `-' not first or last in character alternative" warning)))))
    ;; The range "[\x70-\x8f]" only includes 70..7f and 3fff80..3fff8f;
    ;; the gap 80..3fff7f is excluded.
    (should (equal (xr-lint "[\x70-\x8f∃]") nil))
    (should (equal (xr-lint "[\x70-\x8f\x7e-å]")
                   '(((4 6 "Ranges `\x70-\\x7f' and `\x7e-å' overlap" warning)
                      (1 3 "Previous occurrence here" info)))))
    (should
     (equal (xr-lint "[\x70-\x8få-\x82]")
            '(((4 6 "Ranges `å-\\x82' and `\\x80-\\x8f' overlap" warning)
               (1 3 "Previous occurrence here" info)))))
    (should
     (equal (xr-lint "[A-z]")
            '(((1 3 "Range `A-z' between upper and lower case includes symbols"
                  warning)))))
    ))

(ert-deftest xr-lint-noisy ()
  (should-error (xr-lint "." nil 'bad))
  (let ((text-quoting-style 'grave))
    (dolist (checks '(nil all))
      (ert-info ((prin1-to-string checks) :prefix "checks: ")
        (should
         (equal
          (xr-lint "[0-9+-/*][&-+=]" nil checks)
          (if (eq checks 'all)
              '(((4 6 "Suspect character range `+-/': should `-' be literal?"
                    warning))
                ((10 12 "Suspect character range `&-+': should `-' be literal?"
                     warning)))
            nil)))
        (should
         (equal
          (xr-lint "[ \\t][-.\\d][\\Sw][\\rnt]" nil checks)
          (if (eq checks 'all)
              '(((2 3 "Possibly erroneous `\\t' in character alternative"
                    warning))
                ((8 9 "Possibly erroneous `\\d' in character alternative"
                    warning))
                ((12 13 "Possibly erroneous `\\S' in character alternative"
                     warning))))))
        (should (equal (xr-lint "\\(?:ta\\)\\(:?da\\)\\(:?\\)" nil checks)
                       (if (eq checks 'all)
                           '(((10 11 "Possibly mistyped `:?' at start of group"
                                  warning)))
                         nil)))
        (should
         (equal
          (xr-lint "%\\|[abc]\\|[[:digit:]]\\|\\s-\\|\\s_"
                   nil checks)
          (if (eq checks 'all)
              '(((0 7 "Or-pattern more efficiently expressed as character alternative" warning))
                ((3 20 "Or-pattern more efficiently expressed as character alternative" warning))
                ((10 25 "Or-pattern more efficiently expressed as character alternative" warning)))
            nil)))
        ))))

(ert-deftest xr-lint-repetition-of-empty ()
  (let ((text-quoting-style 'grave))
    (should
     (equal
      (xr-lint "\\(?:a*b?\\)*\\(c\\|d\\|\\)+\\(^\\|e\\)*\\(?:\\)*")
      '(((10 10 "Repetition of expression matching an empty string" warning)
         (0 9 "This expression matches an empty string" info))
        ((21 21 "Repetition of expression matching an empty string" warning)
         (11 20 "This expression matches an empty string" info)))))
    (should
     (equal
      (xr-lint "\\(?:a*?b??\\)+?")
      '(((12 13 "Repetition of expression matching an empty string" warning)
         (0 11 "This expression matches an empty string" info)))))
    (should
     (equal (xr-lint "\\(?:a*b?\\)?")
            '(((10 10 "Optional expression matching an empty string" warning)
               (0 9 "This expression matches an empty string" info)))))))

(ert-deftest xr-lint-branch-subsumption ()
  (let ((text-quoting-style 'grave))
    (should
     (equal (xr-lint "a.cde*f?g\\|g\\|abcdefg")
            '(((14 20 "Branch matches subset of a previous branch" warning)
               (0 8 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "abcd\\|e\\|[aA].[^0-9z]d")
            '(((9 21 "Branch matches superset of a previous branch" warning)
               (0 3 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "\\(?:\\(a\\)\\|.\\)\\(?:a\\|\\(.\\)\\)")
            '(((21 25 "Branch matches superset of a previous branch" warning)
               (18 18 "This is the subset branch" info)))))
    (should
     (equal (xr-lint ".\\|\n\\|\r")
            '(((6 6 "Branch matches subset of a previous branch" warning)
               (0 0 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[^mM]\\|[^a-zA-Z]")
            '(((7 15 "Branch matches subset of a previous branch" warning)
               (0 4 "This is the superset branch" info)))))
    (should (equal (xr-lint "[^mM]\\|[^A-LN-Z]")
                   nil))
    (should (equal (xr-lint "[ab]\\|[^bcd]")
                   nil))
    (should
     (equal (xr-lint "[ab]\\|[^cd]")
            '(((6 10 "Branch matches superset of a previous branch" warning)
               (0 3 "This is the subset branch" info)))))
    (should (equal (xr-lint ".\\|[a\n]")
                   nil))
    (should
     (equal (xr-lint "ab?c+\\|a?b*c*")
            '(((7 12 "Branch matches superset of a previous branch" warning)
               (0 4 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "\\(?:[aA]\\|b\\)\\|a")
            '(((15 15 "Branch matches subset of a previous branch" warning)
               (0 12 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\(?:a\\|b\\)\\|[abc]")
            '(((12 16 "Branch matches superset of a previous branch" warning)
               (0 9 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "\\(?:a\\|b\\)\\|\\(?:[abd]\\|[abc]\\)")
            '(((12 29 "Branch matches superset of a previous branch" warning)
               (0 9 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "ab\\|abc?")
            '(((4 7 "Branch matches superset of a previous branch" warning)
               (0 1 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "abc\\|abcd*e?")
            '(((5 11 "Branch matches superset of a previous branch" warning)
               (0 2 "This is the subset branch" info)))))
    (should (equal (xr-lint "[a[:digit:]]\\|[a\n]")
                   nil))
    (should
     (equal (xr-lint "[a[:ascii:]]\\|[a\n]")
            '(((14 17 "Branch matches subset of a previous branch" warning)
               (0 11 "This is the superset branch" info)))))

    (should
     (equal (xr-lint "[[:alnum:]]\\|[[:alpha:]]")
            '(((13 23 "Branch matches subset of a previous branch" warning)
               (0 10 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[[:alnum:]%]\\|[[:alpha:]%]")
            '(((14 25 "Branch matches subset of a previous branch" warning)
               (0 11 "This is the superset branch" info)))))
    (should (equal (xr-lint "[[:xdigit:]%]\\|[[:alpha:]%]")
                   nil))
    (should (equal (xr-lint "[[:alnum:]]\\|[^[:alpha:]]")
                   nil))
    (should (equal (xr-lint "[^[:alnum:]]\\|[[:alpha:]]")
                   nil))
    (should
     (equal (xr-lint "[[:digit:]]\\|[^[:punct:]]")
            '(((13 24 "Branch matches superset of a previous branch" warning)
               (0 10 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "[^[:digit:]]\\|[[:punct:]]")
            '(((14 24 "Branch matches subset of a previous branch" warning)
               (0 11 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[^[:digit:]]\\|[^[:xdigit:]]")
            '(((14 26 "Branch matches subset of a previous branch" warning)
               (0 11 "This is the superset branch" info)))))
    (should (equal (xr-lint "[^[:print:]]\\|[[:ascii:]]")
                   nil))
    (should (equal (xr-lint "[[:print:]]\\|[^[:ascii:]]")
                   nil))
    (should (equal (xr-lint "[^[:print:]]\\|[^[:ascii:]]")
                   nil))
    (should
     (equal (xr-lint "[[:digit:][:cntrl:]]\\|[[:ascii:]]")
            '(((22 32 "Branch matches superset of a previous branch" warning)
               (0 19 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "[[:alpha:]]\\|A")
            '(((13 13 "Branch matches subset of a previous branch" warning)
               (0 10 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[[:alpha:]]\\|[A-E]")
            '(((13 17 "Branch matches subset of a previous branch" warning)
               (0 10 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[[:alpha:]3-7]\\|[A-E46]")
            '(((16 22 "Branch matches subset of a previous branch" warning)
               (0 13 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[^[:alpha:]]\\|[123]")
            '(((14 18 "Branch matches subset of a previous branch" warning)
               (0 11 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[!-@]\\|[[:digit:]]")
            '(((7 17 "Branch matches subset of a previous branch" warning)
               (0 4 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[^a-z]\\|[[:digit:]]")
            '(((8 18 "Branch matches subset of a previous branch" warning)
               (0 5 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[^[:punct:]]\\|[a-z]")
            '(((14 18 "Branch matches subset of a previous branch" warning)
               (0 11 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[[:space:]]\\|[ \t\f]")
            '(((13 17 "Branch matches subset of a previous branch" warning)
               (0 10 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[[:word:]]\\|[a-gH-P2357]")
            '(((12 23 "Branch matches subset of a previous branch" warning)
               (0 9 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[^[:space:]]\\|[a-gH-P2357]")
            '(((14 25 "Branch matches subset of a previous branch" warning)
               (0 11 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[^z-a]\\|[^0-9[:space:]]")
            '(((8 22 "Branch matches subset of a previous branch" warning)
               (0 5 "This is the superset branch" info)))))

    (should
     (equal (xr-lint "\\(?:.\\|\n\\)\\|a")
            '(((12 12 "Branch matches subset of a previous branch" warning)
               (0 9 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\s-\\| ")
            '(((5 5 "Branch matches subset of a previous branch" warning)
               (0 2 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\S-\\|x")
            '(((5 5 "Branch matches subset of a previous branch" warning)
               (0 2 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\cl\\|å")
            '(((5 5 "Branch matches subset of a previous branch" warning)
               (0 2 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\Ca\\|ü")
            '(((5 5 "Branch matches subset of a previous branch" warning)
               (0 2 "This is the superset branch" info)))))
    
    (should
     (equal (xr-lint "\\w\\|[^z-a]")
            '(((4 9 "Branch matches superset of a previous branch" warning)
               (0 1 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "\\W\\|[^z-a]")
            '(((4 9 "Branch matches superset of a previous branch" warning)
               (0 1 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "\\w\\|a")
            '(((4 4 "Branch matches subset of a previous branch" warning)
               (0 1 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\W\\|\f")
            '(((4 4 "Branch matches subset of a previous branch" warning)
               (0 1 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[[:punct:]]\\|!")
            '(((13 13 "Branch matches subset of a previous branch" warning)
               (0 10 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[[:ascii:]]\\|[^α-ω]")
            '(((13 18 "Branch matches superset of a previous branch" warning)
               (0 10 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "[^a-f]\\|[h-z]")
            '(((8 12 "Branch matches subset of a previous branch" warning)
               (0 5 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "[0-9]\\|\\S(")
            '(((7 9 "Branch matches superset of a previous branch" warning)
               (0 4 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "a+\\|[ab]+")
            '(((4 8 "Branch matches superset of a previous branch" warning)
               (0 1 "This is the subset branch" info)))))
    (should
     (equal (xr-lint "[ab]?\\|a?")
            '(((7 8 "Branch matches subset of a previous branch" warning)
               (0 4 "This is the superset branch" info)))))

    (should
     (equal (xr-lint "a*\\|")
            '(((4 nil "Branch matches subset of a previous branch" warning)
               (0 1 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\|a*")
            '(((2 3 "Branch matches superset of a previous branch" warning)
               (0 nil "This is the subset branch" info)))))
    (should
     (equal (xr-lint "a?\\|")
            '(((4 nil "Branch matches subset of a previous branch" warning)
               (0 1 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\|a?")
            '(((2 3 "Branch matches superset of a previous branch" warning)
               (0 nil "This is the subset branch" info)))))
    (should
     (equal (xr-lint "a*?\\|")
            '(((5 nil "Branch matches subset of a previous branch" warning)
               (0 2 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\|a*?")
            '(((2 4 "Branch matches superset of a previous branch" warning)
               (0 nil "This is the subset branch" info)))))
    (should
     (equal (xr-lint "a??\\|")
            '(((5 nil "Branch matches subset of a previous branch" warning)
               (0 2 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\|a??")
            '(((2 4 "Branch matches superset of a previous branch" warning)
               (0 nil "This is the subset branch" info)))))
    (should
     (equal (xr-lint "a\\{0,4\\}\\|")
            '(((10 nil "Branch matches subset of a previous branch" warning)
               (0 7 "This is the superset branch" info)))))
    (should
     (equal (xr-lint "\\|a\\{0,4\\}")
            '(((2 9 "Branch matches superset of a previous branch" warning)
               (0 nil "This is the subset branch" info)))))
    ))

(ert-deftest xr-lint-subsumed-repetition ()
  (let ((text-quoting-style 'grave))
    (should
     (equal (xr-lint "\\(?:a.c\\|def\\)+\\(?:abc\\)*")
            '(((15 24 "Repetition subsumed by preceding repetition" warning)
               (0 14 "Subsuming repetition here" info)))))

    ;; Exhaustive test of all possible combinations.
    (should
     (equal (xr-lint "[ab]+a?,a?[ab]+,[ab]+a*,a*[ab]+")
            '(((5 6 "Repetition subsumed by preceding repetition" warning)
               (0 4 "Subsuming repetition here" info))
              ((10 14 "Repetition subsumes preceding repetition" warning)
               (8 9 "Subsumed repetition here" info))
              ((21 22 "Repetition subsumed by preceding repetition" warning)
               (16 20 "Subsuming repetition here" info))
              ((26 30 "Repetition subsumes preceding repetition" warning)
               (24 25 "Subsumed repetition here" info)))))

    (should
     (equal (xr-lint "[ab]*a?,a?[ab]*,[ab]*a*,a*[ab]*")
            '(((5 6 "Repetition subsumed by preceding repetition" warning)
               (0 4 "Subsuming repetition here" info))
              ((10 14 "Repetition subsumes preceding repetition" warning)
               (8 9 "Subsumed repetition here" info))
              ((21 22 "Repetition subsumed by preceding repetition" warning)
               (16 20 "Subsuming repetition here" info))
              ((26 30 "Repetition subsumes preceding repetition" warning)
               (24 25 "Subsumed repetition here" info)))))

    (should
     (equal (xr-lint "a+a?,a?a+,a+a*,a*a+,a*a?,a?a*,a*a*")
            '(((2 3 "Repetition subsumed by preceding repetition" warning)
               (0 1 "Subsuming repetition here" info))
              ((7 8 "Repetition subsumes preceding repetition" warning)
               (5 6 "Subsumed repetition here" info))
              ((12 13 "Repetition subsumed by preceding repetition" warning)
               (10 11 "Subsuming repetition here" info))
              ((17 18 "Repetition subsumes preceding repetition" warning)
               (15 16 "Subsumed repetition here" info))
              ((22 23 "Repetition subsumed by preceding repetition" warning)
               (20 21 "Subsuming repetition here" info))
              ((27 28 "Repetition subsumes preceding repetition" warning)
               (25 26 "Subsumed repetition here" info))
              ((32 33 "Repetition subsumed by preceding repetition" warning)
               (30 31 "Subsuming repetition here" info)))))

    (should
     (equal (xr-lint "[ab]+a??,a??[ab]+,[ab]+a*?,a*?[ab]+")
            '(((5 7 "Repetition subsumed by preceding repetition" warning)
               (0 4 "Subsuming repetition here" info))
              ((12 16 "Repetition subsumes preceding repetition" warning)
               (9 11 "Subsumed repetition here" info))
              ((23 25 "Repetition subsumed by preceding repetition" warning)
               (18 22 "Subsuming repetition here" info))
              ((30 34 "Repetition subsumes preceding repetition" warning)
               (27 29 "Subsumed repetition here" info)))))

    (should
     (equal (xr-lint "[ab]*a??,a??[ab]*,[ab]*a*?,a*?[ab]*")
            '(((5 7 "Repetition subsumed by preceding repetition" warning)
               (0 4 "Subsuming repetition here" info))
              ((12 16 "Repetition subsumes preceding repetition" warning)
               (9 11 "Subsumed repetition here" info))
              ((23 25 "Repetition subsumed by preceding repetition" warning)
               (18 22 "Subsuming repetition here" info))
              ((30 34 "Repetition subsumes preceding repetition" warning)
               (27 29 "Subsumed repetition here" info)))))

    (should
     (equal (xr-lint "a+a??,a??a+,a+a*?,a*?a+,a*a??,a??a*,a*a*?,a*?a*")
            '(((2 4 "Repetition subsumed by preceding repetition" warning)
               (0 1 "Subsuming repetition here" info))
              ((9 10 "Repetition subsumes preceding repetition" warning)
               (6 8 "Subsumed repetition here" info))
              ((14 16 "Repetition subsumed by preceding repetition" warning)
               (12 13 "Subsuming repetition here" info))
              ((21 22 "Repetition subsumes preceding repetition" warning)
               (18 20 "Subsumed repetition here" info))
              ((26 28 "Repetition subsumed by preceding repetition" warning)
               (24 25 "Subsuming repetition here" info))
              ((33 34 "Repetition subsumes preceding repetition" warning)
               (30 32 "Subsumed repetition here" info))
              ((38 40 "Repetition subsumed by preceding repetition" warning)
               (36 37 "Subsuming repetition here" info))
              ((45 46 "Repetition subsumes preceding repetition" warning)
               (42 44 "Subsumed repetition here" info)))))

    (should (equal (xr-lint "[ab]+?a?,a?[ab]+?,[ab]+?a*,a*[ab]+?")
                   nil))

    (should (equal (xr-lint "[ab]*?a?,a?[ab]*?,[ab]*?a*,a*[ab]*?")
                   nil))

    (should
     (equal (xr-lint "a+?a?,a?a+?,a+?a*,a*a+?,a*?a?,a?a*?,a*?a*,a*a*?")
            '(((39 40 "Repetition subsumes preceding repetition" warning)
               (36 38 "Subsumed repetition here" info))
              ((44 46 "Repetition subsumed by preceding repetition" warning)
               (42 43 "Subsuming repetition here" info)))))

    (should
     (equal (xr-lint "[ab]+?a??,a??[ab]+?,[ab]+?a*?,a*?[ab]+?")
            '(((6 8 "Repetition subsumed by preceding repetition" warning)
               (0 5 "Subsuming repetition here" info))
              ((13 18 "Repetition subsumes preceding repetition" warning)
               (10 12 "Subsumed repetition here" info))
              ((26 28 "Repetition subsumed by preceding repetition" warning)
               (20 25 "Subsuming repetition here" info))
              ((33 38"Repetition subsumes preceding repetition" warning)
               (30 32 "Subsumed repetition here" info)))))

    (should
     (equal (xr-lint "[ab]*?a??,a??[ab]*?,[ab]*?a*?,a*?[ab]*?")
            '(((6 8 "Repetition subsumed by preceding repetition" warning)
               (0 5 "Subsuming repetition here" info))
              ((13 18 "Repetition subsumes preceding repetition" warning)
               (10 12 "Subsumed repetition here" info))
              ((26 28 "Repetition subsumed by preceding repetition" warning)
               (20 25 "Subsuming repetition here" info))
              ((33 38 "Repetition subsumes preceding repetition" warning)
               (30 32 "Subsumed repetition here" info)))))

    (should
     (equal (xr-lint "a+?a??,a??a+?,a+?a*?,a*?a+?,a*?a??,a??a*?,a*?a*?")
            '(((3 5 "Repetition subsumed by preceding repetition" warning)
               (0 2 "Subsuming repetition here" info))
              ((10 12 "Repetition subsumes preceding repetition" warning)
               (7 9 "Subsumed repetition here" info))
              ((17 19 "Repetition subsumed by preceding repetition" warning)
               (14 16 "Subsuming repetition here" info))
              ((24 26 "Repetition subsumes preceding repetition" warning)
               (21 23 "Subsumed repetition here" info))
              ((31 33 "Repetition subsumed by preceding repetition" warning)
               (28 30 "Subsuming repetition here" info))
              ((38 40 "Repetition subsumes preceding repetition" warning)
               (35 37 "Subsumed repetition here" info))
              ((45 47 "Repetition subsumed by preceding repetition" warning)
               (42 44 "Subsuming repetition here" info)))))
    ))

(ert-deftest xr-lint-wrapped-subsumption ()
  (let ((text-quoting-style 'grave))
    (should
     (equal
      (xr-lint "\\(?:a*x[ab]+\\)*")
      '(((0 14
          "Last item in repetition subsumes first item (wrapped)" warning)))))
    (should
     (equal
      (xr-lint "\\([ab]*xya?\\)+")
      '(((0 13
          "First item in repetition subsumes last item (wrapped)" warning)))))
    (should
     (equal
      (xr-lint "\\(?3:a*xa*\\)\\{7\\}")
      '(((0 16
          "Last item in repetition subsumes first item (wrapped)" warning)))))
    ))

(ert-deftest xr-lint-bad-anchor ()
  (let ((text-quoting-style 'grave))
    (should (equal (xr-lint "a\\(?:^\\)")
                   '(((1 7 "Line-start anchor follows non-newline" warning)
                      (0 0 "This matches a non-newline at the end" info)))))
    (should (equal (xr-lint "a?\\(?:^\\)")
                   '(((2 8 "Line-start anchor follows non-newline" warning)
                      (0 1 "This matches a non-newline at the end" info)))))
    (should (equal (xr-lint "a\\(?:^\\|b\\)")
                   '(((1 10 "Line-start anchor follows non-newline" warning)
                      (0 0 "This matches a non-newline at the end" info)))))
    (should (equal (xr-lint "a?\\(?:^\\|b\\)")
                   nil))
    (should (equal (xr-lint "\\(?:$\\)a")
                   '(((7 7 "Non-newline follows end-of-line anchor" warning)
                      (0 6 "This matches at the end of a line" info)))))
    (should (equal (xr-lint "\\(?:$\\)\\(\n\\|a\\)")
                   '(((7 14 "Non-newline follows end-of-line anchor" warning)
                      (0 6 "This matches at the end of a line" info)))))
    (should (equal (xr-lint "\\(?:$\\|b\\)a")
                   '(((10 10 "Non-newline follows end-of-line anchor" warning)
                      (0 9 "This matches at the end of a line" info)))))
    (should (equal (xr-lint "\\(?:$\\|b\\)\\(\n\\|a\\)")
                   nil))
    (should (equal (xr-lint "\\(?3:$\\)[ab]\\(?2:^\\)")
                   '(((8 11 "Non-newline follows end-of-line anchor" warning)
                      (0 7 "This matches at the end of a line" info))
                     ((12 19 "Line-start anchor follows non-newline" warning)
                      (8 11 "This matches a non-newline at the end" info)))))
    (should (equal (xr-lint ".\\(?:^$\\).")
                   '(((1 8 "Line-start anchor follows non-newline" warning)
                      (0 0 "This matches a non-newline at the end" info))
                     ((9 9 "Non-newline follows end-of-line anchor" warning)
                      (1 8 "This matches at the end of a line" info)))))
    (should
     (equal (xr-lint "\\'b")
            '(((2 2 "Non-empty pattern follows end-of-text anchor" warning)
               (0 1 "This matches at the end of the text" info)))))
    (should
     (equal (xr-lint "\\'b?")
            '(((2 3 "Non-empty pattern follows end-of-text anchor" warning)
               (0 1 "This matches at the end of the text" info)))))
    (should (equal
             (xr-lint "\\(?:a\\|\\'\\)b")
             '(((11 11 "Non-empty pattern follows end-of-text anchor" warning)
                (0 10 "This matches at the end of the text" info)))))
    (should
     (equal (xr-lint "\\'\\(a\\|b?\\)")
            '(((2 10 "Non-empty pattern follows end-of-text anchor" warning)
               (0 1 "This matches at the end of the text" info)))))
    (should (equal (xr-lint "\\(?:a\\|\\'\\)b?")
                   nil))
    ))

(ert-deftest xr-lint-file ()
  (let ((text-quoting-style 'grave))
    (should
     (equal (xr-lint "a.b\\.c.*d.?e.+f." 'file)
            '(((1 1 "Possibly unescaped `.' in file-matching regexp" warning))
              ((15 15 "Possibly unescaped `.' in file-matching regexp"
                   warning)))))
    (should
     (equal (xr-lint "^abc$" 'file)
            '(((0 0 "Use \\` instead of ^ in file-matching regexp" warning))
              ((4 4 "Use \\' instead of $ in file-matching regexp" warning)))))))

(ert-deftest xr-skip-set ()
  (should (equal (xr-skip-set "0-9a-fA-F+*")
                 '(any "0-9A-Fa-f" "*+")))
  (should (equal (xr-skip-set "^ab-ex-")
                 '(not (any "b-e" "ax-"))))
  (should (equal (xr-skip-set "-^][\\")
                 '(any "[]^-")))
  (should (equal (xr-skip-set "\\^a\\-bc-\\fg")
                 '(any "c-f" "^abg-")))
  (should (equal (xr-skip-set "\\")
                 '(any)))
  (should (equal (xr-skip-set "--3^Q-\\")
                 '(any "--3Q-\\" "^")))
  (should (equal (xr-skip-set "^Q-\\c-\\n")
                 '(not (any "Q-c" "n-"))))
  (should (equal (xr-skip-set "\\\\A-")
                 '(any "A\\-")))
  (should (equal (xr-skip-set "[a-z]")
                 '(any "a-z" "[]")))
  (should (equal (xr-skip-set "[:ascii:]-[:digit:]")
                 '(any "-" ascii digit)))
  (should (equal (xr-skip-set "A-[:blank:]")
                 '(any "A-[" ":]abkln")))
  (should (equal (xr-skip-set "\\[:xdigit:]-b")
                 '(any "]-b" ":[dgitx")))
  (should (equal (xr-skip-set "^a-z+" 'terse)
                 '(not (in "a-z" "+"))))
  (let ((text-quoting-style 'grave))
    (should (equal (xr-test--error (xr-skip-set "[::]"))
                   '(xr-parse-error "No character class `[::]'" 0 3)))
    (should (equal (xr-test--error (xr-skip-set "[:whitespace:]"))
                   '(xr-parse-error
                     "No character class `[:whitespace:]'" 0 13)))
    )
  (should (equal (xr-skip-set ".")
                 "\\."))
  (should (equal (xr-skip-set "^")
                 'anything))
  (should (equal (xr-skip-set "[:alnum:]")
                 'alnum))
  (should (equal (xr-skip-set "^[:print:]")
                 '(not print)))
  )

(ert-deftest xr-skip-set-lint ()
  (let ((text-quoting-style 'grave))
    (should (equal (xr-skip-set-lint "A[:ascii:]B[:space:][:ascii:]")
                   '(((20 28 "Duplicated character class `[:ascii:]'" warning)))))
    (should (equal (xr-skip-set-lint "a\\bF-AM-M\\")
                   '(((1 2 "Unnecessarily escaped `b'" warning))
                     ((3 5 "Reversed range `F-A'" warning))
                     ((6 8 "Single-element range `M-M'" warning))
                     ((9 9 "Stray `\\' at end of string" warning)))))
    (should (equal (xr-skip-set-lint "A-Fa-z3D-KM-N!3-7\\!b")
                   '(((7 9 "Ranges `A-F' and `D-K' overlap" warning))
                     ((10 12 "Two-element range `M-N'" warning))
                     ((14 16 "Range `3-7' includes character `3'" warning))
                     ((17 18 "Duplicated character `!'" warning))
                     ((17 18 "Unnecessarily escaped `!'" warning))
                     ((19 19 "Character `b' included in range `a-z'" warning)))))
    (should (equal (xr-skip-set-lint "0a\\--\\\\n")
                   '(((2 6 "Range `--\\' includes character `0'" warning)))))

    (should (equal (xr-skip-set-lint "!-\\$")
                   '(((2 3 "Unnecessarily escaped `$'" warning)))))
    (should (equal (xr-skip-set-lint "[^a-z]")
                   '(((0 5 "Suspect skip set framed in `[...]'" warning)))))
    (should (equal (xr-skip-set-lint "[0-9]+")
                   '(((0 4. "Suspect skip set framed in `[...]'" warning)))))
    (should (equal (xr-skip-set-lint "[[:space:]].")
                   '(((0 10 "Suspect character class framed in `[...]'"
                         warning)))))
    (should
     (equal (xr-skip-set-lint "_[:alnum]")
            '(((1 8 "Possibly missing `:' after character class" warning)))))
    (should (equal (xr-skip-set-lint "_[:ab]")
                   nil))
    (should (equal (xr-skip-set-lint "")
                   '(((0 nil "Empty set matches nothing" warning)))))
    (should (equal (xr-skip-set-lint "^")
                   '(((0 nil "Negated empty set matches anything" warning)))))
    (should (equal (xr-skip-set-lint "A-Z-")
                   nil))
    (should (equal (xr-skip-set-lint "-A-Z")
                   nil))
    (should (equal (xr-skip-set-lint "^-A-Z")
                   nil))
    (should (equal (xr-skip-set-lint "A-Z-z")
                   '(((3 3 "Literal `-' not first or last" warning)))))
    (should (equal (xr-skip-set-lint "\x70-\x8f∃") nil))
    (should (equal (xr-skip-set-lint "\x70-\x8f\x7e-å")
                   '(((3 5 "Ranges `\x70-\\x7f' and `\x7e-å' overlap" warning)))))
    (should (equal (xr-skip-set-lint "\x70-\x8få-\x82")
                   '(((3 5 "Ranges `å-\\x82' and `\\x80-\\x8f' overlap"
                         warning)))))
    ))

;;; Unit tests for internal functions

(ert-deftest xr--tristate-some ()
  (should (equal (xr--tristate-some #'car '()) nil))
  (should (equal (xr--tristate-some #'car '((nil) (nil) (nil))) nil))
  (should (equal (xr--tristate-some #'car '((nil) (sometimes) (nil)))
                 'sometimes))
  (should (equal (xr--tristate-some #'car '((nil) (sometimes) (always) (nil)
                                            (sometimes)))
                 'always)))

(ert-deftest xr--tristate-all ()
  (should (equal (xr--tristate-all #'car '()) 'always))
  (should (equal (xr--tristate-all #'car '((always) (always) (always)))
                 'always))
  (should (equal (xr--tristate-all #'car '((nil) (nil) (nil))) nil))
  (should (equal (xr--tristate-all #'car '((nil) (sometimes) (nil)))
                 'sometimes))
  (should (equal (xr--tristate-all #'car '((nil) (sometimes) (always) (nil)))
                 'sometimes))
  (should (equal (xr--tristate-all #'car '((always) (nil)))
                 'sometimes)))


(provide 'xr-test)

;;; xr-test.el ends here
