;;; 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 . ;;; 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)))) (should-error (treesit-node-first-child-for-pos doc-node 100) :type 'args-out-of-range) ;; `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)))) (should-error (treesit-node-descendant-for-range root-node 100 101) :type 'args-out-of-range) ;; `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))) (should-error (treesit-query-capture root-node "" 100 101) :type 'args-out-of-range) ;; 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 "") (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