summaryrefslogtreecommitdiff
path: root/test/src/treesit-tests.el
diff options
context:
space:
mode:
Diffstat (limited to 'test/src/treesit-tests.el')
-rw-r--r--test/src/treesit-tests.el565
1 files changed, 565 insertions, 0 deletions
diff --git a/test/src/treesit-tests.el b/test/src/treesit-tests.el
new file mode 100644
index 00000000000..80fde408cd3
--- /dev/null
+++ b/test/src/treesit-tests.el
@@ -0,0 +1,565 @@
+;;; treesit-tests.el --- tests for src/treesit.c -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2021-2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'treesit)
+
+(declare-function treesit-language-available-p "treesit.c")
+
+(declare-function treesit-parser-root-node "treesit.c")
+(declare-function treesit-parser-set-included-ranges "treesit.c")
+(declare-function treesit-parser-included-ranges "treesit.c")
+
+(declare-function treesit-parser-create "treesit.c")
+(declare-function treesit-parser-delete "treesit.c")
+(declare-function treesit-parser-list "treesit.c")
+(declare-function treesit-parser-buffer "treesit.c")
+(declare-function treesit-parser-language "treesit.c")
+
+(declare-function treesit-query-expand "treesit.c")
+(declare-function treesit-query-compile "treesit.c")
+(declare-function treesit-query-capture "treesit.c")
+
+(declare-function treesit-node-type "treesit.c")
+(declare-function treesit-node-start "treesit.c")
+(declare-function treesit-node-end "treesit.c")
+(declare-function treesit-node-string "treesit.c")
+(declare-function treesit-node-parent "treesit.c")
+(declare-function treesit-node-child "treesit.c")
+(declare-function treesit-node-check "treesit.c")
+(declare-function treesit-node-field-name-for-child "treesit.c")
+(declare-function treesit-node-child-count "treesit.c")
+(declare-function treesit-node-child-by-field-name "treesit.c")
+(declare-function treesit-node-next-sibling "treesit.c")
+(declare-function treesit-node-prev-sibling "treesit.c")
+(declare-function treesit-node-first-child-for-pos "treesit.c")
+(declare-function treesit-node-descendant-for-range "treesit.c")
+(declare-function treesit-node-eq "treesit.c")
+
+
+(ert-deftest treesit-basic-parsing ()
+ "Test basic parsing routines."
+ (skip-unless (treesit-language-available-p 'json))
+ (with-temp-buffer
+ (let ((parser (treesit-parser-create 'json)))
+ (should
+ (eq parser (car (treesit-parser-list))))
+ (should
+ (equal (treesit-node-string
+ (treesit-parser-root-node parser))
+ "(ERROR)"))
+
+ (insert "[1,2,3]")
+ (should
+ (equal (treesit-node-string
+ (treesit-parser-root-node parser))
+ "(document (array (number) (number) (number)))"))
+
+ (goto-char (point-min))
+ (forward-char 3)
+ (insert "{\"name\": \"Bob\"},")
+ (should
+ (equal
+ (treesit-node-string
+ (treesit-parser-root-node parser))
+ "(document (array (number) (object (pair key: (string (string_content)) value: (string (string_content)))) (number) (number)))")))))
+
+(ert-deftest treesit-node-api ()
+ "Tests for node API."
+ (skip-unless (treesit-language-available-p 'json))
+ (with-temp-buffer
+ (let (parser root-node doc-node object-node pair-node)
+ (progn
+ (insert "[1,2,{\"name\": \"Bob\"},3]")
+ (setq parser (treesit-parser-create 'json))
+ (setq root-node (treesit-parser-root-node
+ parser)))
+ ;; `treesit-node-type'.
+ (should (equal "document" (treesit-node-type root-node)))
+ ;; `treesit-node-check'.
+ (should (eq t (treesit-node-check root-node 'named)))
+ (should (eq nil (treesit-node-check root-node 'missing)))
+ (should (eq nil (treesit-node-check root-node 'extra)))
+ (should (eq nil (treesit-node-check root-node 'has-error)))
+ ;; `treesit-node-child'.
+ (setq doc-node (treesit-node-child root-node 0))
+ (should (equal "array" (treesit-node-type doc-node)))
+ (should (equal (treesit-node-string doc-node)
+ "(array (number) (number) (object (pair key: (string (string_content)) value: (string (string_content)))) (number))"))
+ ;; `treesit-node-child-count'.
+ (should (eql 9 (treesit-node-child-count doc-node)))
+ (should (eql 4 (treesit-node-child-count doc-node t)))
+ ;; `treesit-node-field-name-for-child'.
+ (setq object-node (treesit-node-child doc-node 2 t))
+ (setq pair-node (treesit-node-child object-node 0 t))
+ (should (equal "object" (treesit-node-type object-node)))
+ (should (equal "pair" (treesit-node-type pair-node)))
+ (should (equal "key"
+ (treesit-node-field-name-for-child
+ pair-node 0)))
+ ;; `treesit-node-child-by-field-name'.
+ (should (equal "(string (string_content))"
+ (treesit-node-string
+ (treesit-node-child-by-field-name
+ pair-node "key"))))
+ ;; `treesit-node-next-sibling'.
+ (should (equal "(number)"
+ (treesit-node-string
+ (treesit-node-next-sibling object-node t))))
+ (should (equal "(\",\")"
+ (treesit-node-string
+ (treesit-node-next-sibling object-node))))
+ ;; `treesit-node-prev-sibling'.
+ (should (equal "(number)"
+ (treesit-node-string
+ (treesit-node-prev-sibling object-node t))))
+ (should (equal "(\",\")"
+ (treesit-node-string
+ (treesit-node-prev-sibling object-node))))
+ ;; `treesit-node-first-child-for-pos'.
+ (should (equal "(number)"
+ (treesit-node-string
+ (treesit-node-first-child-for-pos
+ doc-node 3 t))))
+ (should (equal "(\",\")"
+ (treesit-node-string
+ (treesit-node-first-child-for-pos
+ doc-node 3))))
+ ;; `treesit-node-descendant-for-range'.
+ (should (equal "(\"{\")"
+ (treesit-node-string
+ (treesit-node-descendant-for-range
+ root-node 6 7))))
+ (should (equal "(object (pair key: (string (string_content)) value: (string (string_content))))"
+ (treesit-node-string
+ (treesit-node-descendant-for-range
+ root-node 6 7 t))))
+ ;; `treesit-node-eq'.
+ (should (treesit-node-eq root-node root-node))
+ (should (not (treesit-node-eq root-node doc-node))))))
+
+(ert-deftest treesit-query-api ()
+ "Tests for query API."
+ (skip-unless (treesit-language-available-p 'json))
+ (with-temp-buffer
+ (let (parser root-node)
+ (progn
+ (insert "[1,2,{\"name\": \"Bob\"},3]")
+ (setq parser (treesit-parser-create 'json))
+ (setq root-node (treesit-parser-root-node
+ parser)))
+
+ ;; Test `treesit-query-capture' on string, sexp and compiled
+ ;; queries.
+ (dolist (query1
+ ;; String query.
+ '("(string) @string
+(pair key: (_) @keyword)
+((_) @bob (#match \"^B.b$\" @bob))
+(number) @number
+((number) @n3 (#equal \"3\" @n3)) "
+ ;; Sexp query.
+ ((string) @string
+ (pair key: (_) @keyword)
+ ((_) @bob (:match "^B.b$" @bob))
+ (number) @number
+ ((number) @n3 (:equal "3" @n3)))))
+ ;; Test `treesit-query-compile'.
+ (dolist (query (list query1
+ (treesit-query-compile 'json query1)))
+ (should
+ (equal
+ '((number . "1") (number . "2")
+ (keyword . "\"name\"")
+ (string . "\"name\"")
+ (string . "\"Bob\"")
+ (bob . "Bob")
+ (number . "3")
+ (n3 . "3"))
+ (mapcar (lambda (entry)
+ (cons (car entry)
+ (treesit-node-text
+ (cdr entry))))
+ (treesit-query-capture root-node query))))))
+ ;; Test `treesit-query-expand'.
+ (should
+ (equal
+ "(type field: (_) @capture .) ? * + \"return\""
+ (treesit-query-expand
+ '((type field: (_) @capture :anchor)
+ :? :* :+ "return")))))))
+
+(ert-deftest treesit-narrow ()
+ "Tests if narrowing works."
+ (skip-unless (treesit-language-available-p 'json))
+ (with-temp-buffer
+ (let (parser)
+ (progn
+ (insert "xxx[1,{\"name\": \"Bob\"},2,3]xxx")
+ (narrow-to-region (+ (point-min) 3) (- (point-max) 3))
+ (setq parser (treesit-parser-create 'json))
+ (treesit-parser-root-node parser))
+ ;; This test is from the basic test.
+ (should
+ (equal
+ (treesit-node-string
+ (treesit-parser-root-node parser))
+ "(document (array (number) (object (pair key: (string (string_content)) value: (string (string_content)))) (number) (number)))"))
+
+ (widen)
+ (goto-char (point-min))
+ (insert "ooo")
+ (should (equal "oooxxx[1,{\"name\": \"Bob\"},2,3]xxx"
+ (buffer-string)))
+ (delete-region 10 26)
+ (should (equal "oooxxx[1,2,3]xxx"
+ (buffer-string)))
+ (narrow-to-region (+ (point-min) 6) (- (point-max) 3))
+ ;; This test is also from the basic test.
+ (should
+ (equal (treesit-node-string
+ (treesit-parser-root-node parser))
+ "(document (array (number) (number) (number)))"))
+ (widen)
+ (goto-char (point-max))
+ (insert "[1,2]")
+ (should (equal "oooxxx[1,2,3]xxx[1,2]"
+ (buffer-string)))
+ (narrow-to-region (- (point-max) 5) (point-max))
+ (should
+ (equal (treesit-node-string
+ (treesit-parser-root-node parser))
+ "(document (array (number) (number)))"))
+ (widen)
+ (goto-char (point-min))
+ (insert "[1]")
+ (should (equal "[1]oooxxx[1,2,3]xxx[1,2]"
+ (buffer-string)))
+ (narrow-to-region (point-min) (+ (point-min) 3))
+ (should
+ (equal (treesit-node-string
+ (treesit-parser-root-node parser))
+ "(document (array (number)))")))))
+
+(ert-deftest treesit-cross-boundary ()
+ "Tests for cross-boundary edits.
+Cross-boundary means crossing visible_beg and visible_end. We
+don't test if parser parses correctly, instead we just check
+edits like this don't produce assertion errors. (I inserted a
+bunch of assertions that checks e.g. visible_beg <=
+visible_end.)"
+ (skip-unless (treesit-language-available-p 'json))
+ (with-temp-buffer
+ (let (parser)
+ (progn
+ (insert "xxx[1,{\"name\": \"Bob\"},2,3]xxx")
+ (narrow-to-region (+ (point-min) 3) (- (point-max) 3))
+ (setq parser (treesit-parser-create 'json))
+ ;; Now visible_beg/end = visible boundary.
+ (treesit-parser-root-node parser))
+ ;; Now parser knows the content of the visible region.
+ (widen)
+ ;; Now visible_beg/end don't change, but visible region expanded.
+ (delete-region 1 7)
+ ;; (1) This change is across visible_beg. I expect
+ ;; ts_record_change to receive (start=1, old_end=7, new_end=1).
+ (treesit-parser-root-node parser)
+ ;; Above form forces a parse which calls
+ ;; `ts_ensure_position_synced'. Now visible_beg/end matches the
+ ;; visible region (whole buffer). We want to test that this
+ ;; doesn't cause assertion error.
+
+ (should (equal "{\"name\": \"Bob\"},2,3]xxx" (buffer-string)))
+ (narrow-to-region 1 16)
+ (should (equal "{\"name\": \"Bob\"}" (buffer-string)))
+ (treesit-parser-root-node parser)
+ ;; Call `ts_ensure_position_synced' again to update visible_beg/end.
+ (widen)
+ (goto-char 14)
+ (insert "by")
+ ;; (2) This change is inside [visible_beg, visible_end].
+ (should (equal "{\"name\": \"Bobby\"},2,3]xxx" (buffer-string)))
+ (delete-region 14 23)
+ ;; This delete is across visible_end.
+ (should (equal "{\"name\": \"Bobxxx" (buffer-string)))
+ (treesit-parser-root-node parser)
+ ;; visible_beg/end synced.
+
+ (narrow-to-region 3 7)
+ (should (equal "name" (buffer-string)))
+ (treesit-parser-root-node parser)
+ ;; visible_beg/end synced.
+ (widen)
+ (goto-char (point-min))
+ (insert "zzz")
+ (should (equal "zzz{\"name\": \"Bobxxx" (buffer-string)))
+ ;; (3) Test inserting before visible_beg.
+ (treesit-parser-root-node parser)
+ ;; visible_beg/end synced.
+
+ (narrow-to-region 4 11)
+ (should (equal "{\"name\"" (buffer-string)))
+ (treesit-parser-root-node parser)
+ ;; visible_beg/end synced.
+ (widen)
+ (goto-char (point-max))
+ (insert "yyy")
+ ;; (4) This change is after visible_end.
+ (treesit-parser-root-node parser)
+ ;; Sync up visible_beg/end.
+ (should (equal "zzz{\"name\": \"Bobxxxyyy" (buffer-string)))
+
+ (narrow-to-region 1 17)
+ (should (equal "zzz{\"name\": \"Bob" (buffer-string)))
+ (treesit-parser-root-node parser)
+ ;; Sync up visible_beg/end.
+ (widen)
+ (delete-region 13 (point-max))
+ (treesit-parser-root-node parser)
+ ;; Sync up visible_beg/end.
+ (should (equal "zzz{\"name\": " (buffer-string)))
+ ;; Ideally we want to also test the case where we delete and
+ ;; insert simultaneously, but the only such use is in
+ ;; `casify_region', all others either only inserts or only
+ ;; deletes. I'll leave it to someone to try to write a test
+ ;; that calls that.
+ )))
+
+(ert-deftest treesit-range ()
+ "Tests if range works."
+ (skip-unless (treesit-language-available-p 'json))
+ (with-temp-buffer
+ (let (parser)
+ (progn
+ (insert "[[1],oooxxx[1,2,3],xxx[1,2]]")
+ (setq parser (treesit-parser-create 'json))
+ (treesit-parser-root-node parser))
+
+ (should (eq (treesit-parser-included-ranges parser) nil))
+
+ (should-error
+ (treesit-parser-set-included-ranges
+ parser '((1 . 6) (5 . 20)))
+ :type '(treesit-range-invalid))
+
+ (treesit-parser-set-included-ranges
+ parser '((1 . 6) (12 . 20) (23 . 29)))
+ (should (equal '((1 . 6) (12 . 20) (23 . 29))
+ (treesit-parser-included-ranges parser)))
+ (should (equal "(document (array (array (number)) (array (number) (number) (number)) (array (number) (number))))"
+ (treesit-node-string
+ (treesit-parser-root-node parser))))
+
+ (treesit-parser-set-included-ranges parser nil)
+ (should (eq (treesit-parser-included-ranges parser) nil))
+
+ ;; `treesit--merge-ranges'.
+ (let ((old-ranges '((1 . 10) ; (1) -- before (a)
+ (20 . 30); (2) -- intersect with (b)
+ (42 . 46) (47 . 48) ; (3) -- inside (c)
+ (55 . 65) (70 . 75) ; (4) -- intersect start-end
+ (80 . 90) ; (4)
+ ))
+ (new-ranges '((10 . 15) ; (a)
+ (18 . 25) (26 . 28) ; (b)
+ (40 . 50) ; (c)
+ (90 . 100) ; (d) -- after (4)
+ ))
+ (result '((1 . 10) ; (1)
+ (10 . 15) ; (a)
+ (18 . 25) (26 . 28) ; (b)
+ (40 . 50) ; (c)
+ (80 . 90) ; (4)
+ (90 . 100) ; (d)
+ )))
+ (should (equal (treesit--merge-ranges
+ old-ranges new-ranges 60 75)
+ result)))
+ ;; TODO: More tests.
+ )))
+
+(ert-deftest treesit-multi-lang ()
+ "Tests if parsing multiple language works."
+ (skip-unless (and (treesit-language-available-p 'html)
+ (treesit-language-available-p 'css)
+ (treesit-language-available-p 'javascript)))
+ (with-temp-buffer
+ (let (css js css-range js-range)
+ (progn
+ (insert "<html><script>1</script><style>body {}</style></html>")
+ (treesit-parser-create 'html)
+ (setq css (treesit-parser-create 'css))
+ (setq js (treesit-parser-create 'javascript)))
+ ;; JavaScript.
+ (setq js-range
+ (treesit-query-range
+ 'html
+ '((script_element (raw_text) @capture))))
+ (should (equal '((15 . 16)) js-range))
+ (treesit-parser-set-included-ranges js js-range)
+ (should (equal "(program (expression_statement (number)))"
+ (treesit-node-string
+ (treesit-parser-root-node js))))
+ ;; CSS.
+ (setq css-range
+ (treesit-query-range
+ 'html
+ '((style_element (raw_text) @capture))))
+ (should (equal '((32 . 39)) css-range))
+ (treesit-parser-set-included-ranges css css-range)
+ (should
+ (equal "(stylesheet (rule_set (selectors (tag_name)) (block)))"
+ (treesit-node-string
+ (treesit-parser-root-node css))))
+ ;; TODO: More tests.
+ )))
+
+(ert-deftest treesit-parser-supplemental ()
+ "Supplemental node functions."
+ (skip-unless (treesit-language-available-p 'json))
+ ;; `treesit-parse-string'.
+ (should (equal (treesit-node-string
+ (treesit-parse-string
+ "[1,2,{\"name\": \"Bob\"},3]"
+ 'json))
+ "(document (array (number) (number) (object (pair key: (string (string_content)) value: (string (string_content)))) (number)))"))
+ (with-temp-buffer
+ (let (parser root-node)
+ (progn
+ (insert "[1,2,{\"name\": \"Bob\"},3]")
+ (setq parser (treesit-parser-create 'json))
+ (setq root-node (treesit-parser-root-node
+ parser))
+ (treesit-node-child root-node 0))
+ )))
+
+(ert-deftest treesit-node-supplemental ()
+ "Supplemental node functions."
+ (skip-unless (treesit-language-available-p 'json))
+ (let (parser root-node doc-node)
+ (progn
+ (insert "[1,2,{\"name\": \"Bob\"},3]")
+ (setq parser (treesit-parser-create 'json))
+ (setq root-node (treesit-parser-root-node
+ parser))
+ (setq doc-node (treesit-node-child root-node 0)))
+ ;; `treesit-node-buffer'.
+ (should (equal (treesit-node-buffer root-node)
+ (current-buffer)))
+ ;; `treesit-node-language'.
+ (should (eq (treesit-node-language root-node)
+ 'json))
+ ;; `treesit-node-at'.
+ (should (equal (treesit-node-string
+ (treesit-node-at 1 'json))
+ "(\"[\")"))
+ ;; `treesit-node-on'
+ (should (equal (treesit-node-string
+ (treesit-node-on 1 2 'json))
+ "(\"[\")"))
+ ;; `treesit-buffer-root-node'.
+ (should (treesit-node-eq
+ (treesit-buffer-root-node 'json)
+ root-node))
+ ;; `treesit-filter-child'.
+ (should (equal (mapcar
+ (lambda (node)
+ (treesit-node-type node))
+ (treesit-filter-child
+ doc-node (lambda (node)
+ (treesit-node-check node 'named))))
+ '("number" "number" "object" "number")))
+ ;; `treesit-node-text'.
+ (should (equal (treesit-node-text doc-node)
+ "[1,2,{\"name\": \"Bob\"},3]"))
+ ;; `treesit-node-index'.
+ (should (eq (treesit-node-index doc-node)
+ 0))
+ ;; TODO:
+ ;; `treesit-parent-until'
+ ;; `treesit-parent-while'
+ ;; `treesit-node-children'
+ ;; `treesit-node-field-name'
+ ;; `treesit-search-forward-goto'
+ ))
+
+(ert-deftest treesit-node-at ()
+ "Test `treesit-node-at'."
+ (skip-unless (treesit-language-available-p 'json))
+ (let (parser)
+ (progn
+ (insert "[1, 2, 3,4] ")
+ (setq parser (treesit-parser-create 'json))
+ (treesit-parser-root-node parser))
+ ;; Point at ",", should return ",".
+ (goto-char (point-min))
+ (search-forward "1")
+ (should (equal (treesit-node-text
+ (treesit-node-at (point)))
+ ","))
+ ;; Point behind ",", should still return the ",".
+ (search-forward ",")
+ (should (equal (treesit-node-text
+ (treesit-node-at (point)))
+ ","))
+ ;; Point between "," and "2", should return 2.
+ (forward-char)
+ (should (equal (treesit-node-text
+ (treesit-node-at (point)))
+ "2"))
+ ;; EOF, should return the last leaf node "]".
+ (goto-char (point-max))
+ (should (equal (treesit-node-text
+ (treesit-node-at (point)))
+ "]"))))
+
+(ert-deftest treesit-node-check ()
+ "Test `treesit-node-check'."
+ (skip-unless (treesit-language-available-p 'json))
+ (let (parser root-node array-node comment-node)
+ (progn
+ (insert "/* comment */ [1, 2, 3,4 ")
+ (setq parser (treesit-parser-create 'json))
+ (setq root-node (treesit-parser-root-node
+ parser))
+ (setq comment-node (treesit-node-child root-node 0))
+ (setq array-node (treesit-node-child root-node 1)))
+
+ (should (treesit-node-check comment-node 'extra))
+ (should (treesit-node-check array-node 'has-error))
+ (should-error (treesit-node-check array-node 'xxx))
+ (should (treesit-node-check (treesit-node-child array-node -1)
+ 'missing))
+ (goto-char (point-max))
+ (insert "]")
+ (should (treesit-node-check array-node 'outdated))))
+
+;; TODO
+;; - Functions in treesit.el
+;; - treesit-load-name-override-list
+;; - treesit-search-subtree
+;; - treesit-search-forward
+;; - treesit-induce-sparse-tree
+;; - treesit-search-forward
+
+
+(provide 'treesit-tests)
+;;; treesit-tests.el ends here