branch: externals/javaimp
commit a434ee966015752c65dc503de5159046c1edac42
Author: Filipp Gunbin <[email protected]>
Commit: Filipp Gunbin <[email protected]>

    Reorganize project structure
---
 .elpaignore                                      |   4 +
 Makefile                                         |  25 ++
 javaimp-gradle.el                                |  49 +--
 javaimp-maven.el                                 |  79 +++--
 javaimp-parse.el                                 | 343 +++++++++++-------
 javaimp-tests.el                                 | 424 +----------------------
 javaimp-util.el                                  | 155 ++-------
 javaimp.el                                       | 150 ++++----
 {testdata => tests/data}/gradle-multi.tar.gz     | Bin
 {testdata => tests/data}/maven-multi.tar.gz      | Bin
 {testdata => tests/data}/maven-single.tar.gz     | Bin
 {testdata => tests/data}/test1-misc-classes.java |   0
 javaimp-tests-gradle.el => tests/gradle.el       |  46 ++-
 tests/imenu.el                                   |  85 +++++
 javaimp-tests-maven.el => tests/maven.el         |  51 ++-
 javaimp-tests.el => tests/parse.el               | 293 +++++-----------
 tests/tests.el                                   |  24 ++
 17 files changed, 672 insertions(+), 1056 deletions(-)

diff --git a/.elpaignore b/.elpaignore
new file mode 100644
index 0000000000..c7946ef938
--- /dev/null
+++ b/.elpaignore
@@ -0,0 +1,4 @@
+.gitignore
+Makefile
+tests
+javaimp-tests.el
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000..6c8f3ec3e9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,25 @@
+# -*- Makefile -*-
+
+# "make check" by default runs non-expensive tests.  To run all tests,
+# use "make check SELECTOR=t".
+
+EMACS = emacs -Q -batch -L .
+SRCS = javaimp-util.el javaimp-gradle.el javaimp-maven.el javaimp-parse.el 
javaimp.el
+TESTS = $(wildcard tests/*.el)
+OBJS = $(SRCS:.el=.elc) $(TESTS:.el=.elc)
+SELECTOR ?= (not (tag :expensive))
+
+.PHONY: all check test clean
+
+%.elc: %.el
+       ${EMACS} -f batch-byte-compile $^
+
+all: $(OBJS)
+
+check test: all
+       ${EMACS} \
+         $(addprefix -l ,$(OBJS)) \
+         -eval '(ert-run-tests-batch-and-exit (quote ${SELECTOR}))'
+
+clean:
+       $(RM) -f $(OBJS)
diff --git a/javaimp-gradle.el b/javaimp-gradle.el
index e3b801a5be..bed838d712 100644
--- a/javaimp-gradle.el
+++ b/javaimp-gradle.el
@@ -20,30 +20,37 @@
 
 (require 'javaimp-util)
 
-(defun javaimp--gradle-visit (file)
+(defcustom javaimp-gradle-program "gradle"
+  "Path to the `gradle' program.  If the visited project has local
+gradlew (Gradle wrapper), it is used in preference."
+  :type 'string
+  :group 'javaimp)
+
+
+(defun javaimp-gradle-visit (file)
   "Calls gradle on FILE to get various project information.
 
 Passes specially crafted init file as -I argument to gradle and
 invokes task contained in it.  This task outputs all needed
 information."
   (message "Visiting Gradle build file %s..." file)
-  (let* ((alists (javaimp--gradle-call file #'javaimp--gradle-handler))
+  (let* ((alists (javaimp-gradle--call file #'javaimp-gradle--handler))
          (modules (mapcar (lambda (alist)
-                            (javaimp--gradle-module-from-alist alist file))
+                            (javaimp-gradle--module-from-alist alist file))
                           alists)))
     ;; first module is always root
     (message "Building tree for root: %s"
              (javaimp-print-id (javaimp-module-id (car modules))))
     (list
-     (javaimp--build-tree (car modules) modules
-                         ;; more or less reliable way to find children
-                         ;; is to look for modules with "this" as the
-                         ;; parent
-                          (lambda (el tested)
-                            (equal (javaimp-module-parent-id tested)
-                                   (javaimp-module-id el)))))))
-
-(defun javaimp--gradle-handler ()
+     (javaimp-tree-build (car modules) modules
+                        ;; more or less reliable way to find children
+                        ;; is to look for modules with "this" as the
+                        ;; parent
+                         (lambda (el tested)
+                           (equal (javaimp-module-parent-id tested)
+                                  (javaimp-module-id el)))))))
+
+(defun javaimp-gradle--handler ()
   "Parse current buffer into list of project descriptors, each of
 which is an alist of attributes (NAME . VALUE).  Each attribute
 occupies one line, and is of the form 'NAME=VALUE'.  See file
@@ -65,12 +72,12 @@ descriptor."
       (push alist res))
     (nreverse res)))
 
-(defun javaimp--gradle-module-from-alist (alist file-orig)
+(defun javaimp-gradle--module-from-alist (alist file-orig)
   "Make `javaimp-module' structure out of attribute alist ALIST."
   (make-javaimp-module
-   :id (javaimp--gradle-id-from-semi-separated
+   :id (javaimp-gradle--id-from-semi-separated
         (cdr (assq 'id alist)))
-   :parent-id (javaimp--gradle-id-from-semi-separated
+   :parent-id (javaimp-gradle--id-from-semi-separated
                (cdr (assq 'parent-id alist)))
    :file (cdr (assq 'file alist))
    :file-orig file-orig
@@ -87,10 +94,10 @@ descriptor."
                 (cdr (assq 'build-dir alist))))
    :dep-jars (javaimp--split-native-path (cdr (assq 'dep-jars alist)))
    :load-ts (current-time)
-   :dep-jars-fetcher #'javaimp--gradle-fetch-dep-jars
+   :dep-jars-fetcher #'javaimp-gradle--fetch-dep-jars
    :raw nil))
 
-(defun javaimp--gradle-id-from-semi-separated (str)
+(defun javaimp-gradle--id-from-semi-separated (str)
   (when str
     (let ((parts (split-string str ";" t))
           artifact)
@@ -106,8 +113,8 @@ descriptor."
                        :artifact artifact
                        :version (nth 2 parts)))))
 
-(defun javaimp--gradle-fetch-dep-jars (module ids)
-  (javaimp--gradle-call
+(defun javaimp-gradle--fetch-dep-jars (module ids)
+  (javaimp-gradle--call
    ;; Always invoke on orig file (which is root build script) because
    ;; module's own file may not exist, even if reported by Gradle as
    ;; project.buildFile.  Furthermore, we use that file's directory to
@@ -120,7 +127,7 @@ descriptor."
      (unless (string-empty-p mod-path)
        (format ":%s:" mod-path)))))
 
-(defun javaimp--gradle-call (file handler &optional mod-path)
+(defun javaimp-gradle--call (file handler &optional mod-path)
   (let* (;; There is (was) "-b" switch for specifying build file,
          ;; however it became deprecated in Gradle 7, so we try to run
          ;; in build file directory.
@@ -142,7 +149,7 @@ descriptor."
      "-Dorg.gradle.java.compile-classpath-packaging=true"
      "-I" (javaimp-cygpath-convert-maybe
            (expand-file-name "javaimp-init-script.gradle"
-                             (concat javaimp--basedir
+                             (concat javaimp-basedir
                                      (file-name-as-directory "support"))))
      (concat mod-path "javaimpTask"))))
 
diff --git a/javaimp-maven.el b/javaimp-maven.el
index c4b7269550..fafaad3d00 100644
--- a/javaimp-maven.el
+++ b/javaimp-maven.el
@@ -23,26 +23,33 @@
 
 (require 'javaimp-util)
 
-(defun javaimp--maven-visit (file)
+(defcustom javaimp-mvn-program "mvn"
+  "Path to the `mvn' program.  If the visited project has local
+mvnw (Maven wrapper), it is used in preference."
+  :type 'string
+  :group 'javaimp)
+
+
+(defun javaimp-maven-visit (file)
   "Calls \"mvn help:effective-pom\" on FILE,
 reads project structure from the output and records which files
 belong to which modules and other module information.  Returns
 resulting module trees."
   (message "Visiting Maven POM file %s..." file)
-  (let* ((xml-tree (javaimp--maven-call
+  (let* ((xml-tree (javaimp-maven--call
                     file
-                    #'javaimp--maven-effective-pom-handler
+                    #'javaimp-maven--effective-pom-handler
                     "help:effective-pom"))
         (projects (or (if (assq 'project xml-tree)
                            (list (assq 'project xml-tree))
-                         (javaimp--xml-children (assq 'projects xml-tree) 
'project))
+                         (javaimp-xml-children (assq 'projects xml-tree) 
'project))
                        (user-error "No projects found")))
         (modules (mapcar (lambda (proj-elt)
-                            (javaimp--maven-module-from-xml proj-elt file))
+                            (javaimp-maven--module-from-xml proj-elt file))
                           projects)))
     ;; Set :file slots in a separate step because Maven doesn't give
     ;; pom.xml file location for each subproject.
-    (javaimp--maven-fill-modules-files file modules)
+    (javaimp-maven--fill-modules-files file modules)
     (when-let ((without-files (seq-filter (lambda (m)
                                             (null (javaimp-module-file m)))
                                           modules)))
@@ -71,7 +78,7 @@ resulting module trees."
       (mapcar (lambda (root)
                 (message "Building tree for root: %s"
                          (javaimp-print-id (javaimp-module-id root)))
-                (javaimp--build-tree
+                (javaimp-tree-build
                  root modules
                 ;; more or less reliable way to find children is to
                 ;; look for modules with "this" as the parent
@@ -80,7 +87,7 @@ resulting module trees."
                           (javaimp-module-id el)))))
               roots))))
 
-(defun javaimp--maven-effective-pom-handler ()
+(defun javaimp-maven--effective-pom-handler ()
   (let ((start
         (save-excursion
           (progn
@@ -97,46 +104,46 @@ resulting module trees."
             (match-end 0)))))
     (xml-parse-region start end)))
 
-(defun javaimp--maven-module-from-xml (elt file-orig)
-  (let ((build-elt (javaimp--xml-child 'build elt)))
+(defun javaimp-maven--module-from-xml (elt file-orig)
+  (let ((build-elt (javaimp-xml-child 'build elt)))
     (make-javaimp-module
-     :id (javaimp--maven-id-from-xml elt)
-     :parent-id (javaimp--maven-id-from-xml (javaimp--xml-child 'parent elt))
+     :id (javaimp-maven--id-from-xml elt)
+     :parent-id (javaimp-maven--id-from-xml (javaimp-xml-child 'parent elt))
      ;; <project> element does not contain pom file path, so we set this slot
-     ;; later, see javaimp--maven-fill-modules-files
+     ;; later, see javaimp-maven--fill-modules-files
      :file nil
      :file-orig file-orig
      ;; jar/war supported
-     :final-name (let ((packaging (or (javaimp--xml-first-child
-                                      (javaimp--xml-child 'packaging elt))
+     :final-name (let ((packaging (or (javaimp-xml-first-child
+                                      (javaimp-xml-child 'packaging elt))
                                       "jar")))
                    (when (member packaging '("jar" "war"))
-                     (concat (javaimp--xml-first-child
-                              (javaimp--xml-child 'finalName build-elt))
+                     (concat (javaimp-xml-first-child
+                              (javaimp-xml-child 'finalName build-elt))
                              "." packaging)))
      :source-dirs (list (file-name-as-directory
                         (javaimp-cygpath-convert-maybe
-                         (javaimp--xml-first-child
-                          (javaimp--xml-child 'sourceDirectory build-elt))))
+                         (javaimp-xml-first-child
+                          (javaimp-xml-child 'sourceDirectory build-elt))))
                         (file-name-as-directory
                         (javaimp-cygpath-convert-maybe
-                         (javaimp--xml-first-child
-                          (javaimp--xml-child 'testSourceDirectory 
build-elt)))))
+                         (javaimp-xml-first-child
+                          (javaimp-xml-child 'testSourceDirectory 
build-elt)))))
      :build-dir (file-name-as-directory
                 (javaimp-cygpath-convert-maybe
-                 (javaimp--xml-first-child (javaimp--xml-child 'directory 
build-elt))))
+                 (javaimp-xml-first-child (javaimp-xml-child 'directory 
build-elt))))
      :dep-jars nil          ; dep-jars is initialized lazily on demand
      :load-ts (current-time)
-     :dep-jars-fetcher #'javaimp--maven-fetch-dep-jars
+     :dep-jars-fetcher #'javaimp-maven--fetch-dep-jars
      :raw elt)))
 
-(defun javaimp--maven-id-from-xml (elt)
+(defun javaimp-maven--id-from-xml (elt)
   (make-javaimp-id
-   :group (javaimp--xml-first-child (javaimp--xml-child 'groupId elt))
-   :artifact (javaimp--xml-first-child (javaimp--xml-child 'artifactId elt))
-   :version (javaimp--xml-first-child (javaimp--xml-child 'version elt))))
+   :group (javaimp-xml-first-child (javaimp-xml-child 'groupId elt))
+   :artifact (javaimp-xml-first-child (javaimp-xml-child 'artifactId elt))
+   :version (javaimp-xml-first-child (javaimp-xml-child 'version elt))))
 
-(defun javaimp--maven-fill-modules-files (file modules)
+(defun javaimp-maven--fill-modules-files (file modules)
   "Reads <module> ids from FILE, looks up corresponding modules in
 MODULES, sets their :file slot, then recurses for each of them.
 A submodule file path is constructed by appending relative path
@@ -147,7 +154,7 @@ are somewhat arbitrary."
                     (insert-file-contents file)
                     (xml-parse-region (point-min) (point-max))))
         (project-elt (assq 'project xml-tree))
-        (this-id (javaimp--maven-id-from-xml project-elt))
+        (this-id (javaimp-maven--id-from-xml project-elt))
          (module (seq-find
                   (lambda (m)
                     (let ((mid (javaimp-module-id m)))
@@ -175,20 +182,20 @@ are somewhat arbitrary."
     ;; help:effective-pom because Maven can extend the list in the
     ;; file with elements from profiles
     (let* ((submodules-elt
-            (javaimp--xml-child 'modules (javaimp-module-raw module)))
-           (submodules (javaimp--xml-children submodules-elt 'module))
-           (rel-paths (mapcar #'javaimp--xml-first-child submodules)))
+            (javaimp-xml-child 'modules (javaimp-module-raw module)))
+           (submodules (javaimp-xml-children submodules-elt 'module))
+           (rel-paths (mapcar #'javaimp-xml-first-child submodules)))
       ;; recurse into each submodule
       (dolist (rel-path rel-paths)
-       (javaimp--maven-fill-modules-files
+       (javaimp-maven--fill-modules-files
          (concat (file-name-directory file)
                  (mapconcat #'file-name-as-directory
                             (split-string rel-path "[/\\\\]+" t) nil)
                 "pom.xml")
         modules)))))
 
-(defun javaimp--maven-fetch-dep-jars (module _ids)
-  (javaimp--maven-call
+(defun javaimp-maven--fetch-dep-jars (module _ids)
+  (javaimp-maven--call
    ;; always invoke for this module's pom.xml
    (javaimp-module-file module)
    (lambda ()
@@ -200,7 +207,7 @@ are somewhat arbitrary."
    ;; build tool wrapper.
    (file-name-directory (javaimp-module-file-orig module))))
 
-(defun javaimp--maven-call (file handler task &optional dir)
+(defun javaimp-maven--call (file handler task &optional dir)
   (let* ((default-directory (or dir (file-name-directory file)))
          ;; Prefer local mvn wrapper
          (local-mvnw (if (memq system-type '(cygwin windows-nt))
diff --git a/javaimp-parse.el b/javaimp-parse.el
index fd3b2ae305..ee7b28eaca 100644
--- a/javaimp-parse.el
+++ b/javaimp-parse.el
@@ -18,20 +18,45 @@
 ;; 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 'javaimp-util)
+(require 'cc-mode)                      ;for java-mode-syntax-table
+(require 'cl-lib)
+(require 'seq)
 
-(defconst javaimp--parse-classlike-keywords
+(cl-defstruct javaimp-scope
+  type
+  name
+  start
+  open-brace
+  parent)
+
+
+(defconst javaimp-scope-classlike-types
+  '(class interface enum))
+
+(defconst javaimp-scope-all-types
+  (append
+   '(anon-class
+     array
+     method
+     simple-statement
+     statement
+     array)
+   javaimp-scope-classlike-types))
+
+
+(defconst javaimp-parse--classlike-keywords
   (mapcar #'symbol-name
-          javaimp--classlike-scope-types))
+          javaimp-scope-classlike-types))
 
-(defconst javaimp--parse-stmt-keywords
+(defconst javaimp-parse--stmt-keywords
   '("if" "else" "for" "while" "do" "switch" "try" "catch" "finally"
     "static"                            ; static initializer block
     ))
-(defconst javaimp--parse-stmt-keyword-maxlen
-  (seq-max (mapcar #'length javaimp--parse-stmt-keywords)))
+(defconst javaimp-parse--stmt-keyword-maxlen
+  (seq-max (mapcar #'length javaimp-parse--stmt-keywords)))
 
-(defun javaimp--directive-regexp (directive)
+
+(defun javaimp-parse--directive-regexp (directive)
   "Return regexp suitable for matching package-like DIRECTIVE, a
 regexp.  First group is directive, second group is identifier."
   (rx bol (* space)
@@ -39,25 +64,40 @@ regexp.  First group is directive, second group is 
identifier."
       (group (+ (any alnum ?_)) (* ?. (+ (any alnum ?_ ?*))))
       (* space) ?\;))
 
-(defconst javaimp--parse-package-regexp
-  (javaimp--directive-regexp "package"))
-(defconst javaimp--parse-import-regexp
-  (javaimp--directive-regexp "import\\(?:[[:space:]]+static\\)?"))
+(defconst javaimp-parse--package-regexp
+  (javaimp-parse--directive-regexp "package"))
+(defconst javaimp-parse--import-regexp
+  (javaimp-parse--directive-regexp "import\\(?:[[:space:]]+static\\)?"))
+
+
+(defvar javaimp-syntax-table
+  (make-syntax-table java-mode-syntax-table) ;TODO don't depend on cc-mode
+  "Javaimp syntax table")
+
+(defvar javaimp--arglist-syntax-table
+  (let ((st (make-syntax-table javaimp-syntax-table)))
+    (modify-syntax-entry ?< "(>" st)
+    (modify-syntax-entry ?> ")<" st)
+    (modify-syntax-entry ?. "_" st) ; separates parts of fully-qualified type
+    st)
+  "Enables parsing angle brackets as lists")
+
 
-(defvar-local javaimp--parse-dirty-pos nil
+(defvar-local javaimp-parse--dirty-pos nil
   "Marker which points to a buffer position after which all parsed
 information should be considered as stale.  Usually set by
 modification change hooks.  Nil value means we haven't yet parsed
 anything in the buffer.  A marker pointing nowhere means
 everything's up-to-date.")
 
-(defsubst javaimp--parse-substr-before-< (str)
+
+(defsubst javaimp-parse--substr-before-< (str)
   (let ((end (string-search "<" str)))
     (if end
         (string-trim (substring str 0 end))
       str)))
 
-(defun javaimp--parse-rsb-keyword (regexp &optional bound noerror count)
+(defun javaimp-parse--rsb-keyword (regexp &optional bound noerror count)
   "Like `re-search-backward', but count only occurences which start
 outside any syntactic context as given by `syntax-ppss-context'.
 Assumes point is outside of any context initially."
@@ -70,7 +110,7 @@ Assumes point is outside of any context initially."
                   (syntax-ppss-context (syntax-ppss)))))
     res))
 
-(defun javaimp--parse-arglist (beg end &optional only-type)
+(defun javaimp-parse--arglist (beg end &optional only-type)
   "Parse arg list between BEG and END, of the form 'TYPE NAME,
 ...'.  Return list of conses (TYPE . NAME).  If ONLY-TYPE is
 non-nil, then name parsing is skipped."
@@ -81,24 +121,24 @@ non-nil, then name parsing is skipped."
         (ignore-errors
           (let (res)
             (while (progn
-                     (javaimp--parse-skip-back-until)
+                     (javaimp-parse--skip-back-until)
                      (not (bobp)))
-              (push (javaimp--parse-arglist-one-arg only-type) res)
+              (push (javaimp-parse--arglist-one-arg only-type) res)
               ;; move back to the previous argument, if any
-              (when (javaimp--parse-skip-back-until
+              (when (javaimp-parse--skip-back-until
                      (lambda (_last-what _last-pos)
                        (and (not (bobp))
                             (= (char-before) ?,))))
                 (backward-char)))       ; skip comma
             res))))))
 
-(defun javaimp--parse-arglist-one-arg (only-type)
+(defun javaimp-parse--arglist-one-arg (only-type)
   "Parse one argument as type and name backwards starting from
 point and return it in the form (TYPE . NAME).  Name is skipped
 if ONLY-TYPE is non-nil.  Leave point at where the job is done:
 skipping further backwards is done by the caller."
   (let ((limit (progn
-                 (javaimp--parse-skip-back-until)
+                 (javaimp-parse--skip-back-until)
                  (point)))
         name)
     ;; Parse name
@@ -106,12 +146,12 @@ skipping further backwards is done by the caller."
       (if (= 0 (skip-syntax-backward "w_"))
           (error "Cannot parse argument name")
         (setq name (buffer-substring-no-properties (point) limit))
-        (javaimp--parse-skip-back-until)
+        (javaimp-parse--skip-back-until)
         (setq limit (point))))
     ;; Parse type: allow anything, but stop at the word boundary which
     ;; is not inside list (this is presumably the type start..)
     (if-let ((last-skip
-              (javaimp--parse-skip-back-until
+              (javaimp-parse--skip-back-until
                (lambda (_last-what last-pos)
                  (save-excursion
                    (if last-pos (goto-char last-pos))
@@ -125,7 +165,7 @@ skipping further backwards is done by the caller."
             (cons type name)))
       (error "Cannot parse argument type"))))
 
-(defun javaimp--parse-skip-back-until (&optional stop-p)
+(defun javaimp-parse--skip-back-until (&optional stop-p)
   "Goes backwards until position at which STOP-P returns non-nil, or reaching 
bob.
 
 STOP-P is invoked with two arguments which describe the last
@@ -167,20 +207,20 @@ function is to just skip whitespace / comments."
                  (setq last-what 'char
                        last-pos (point)))))))))
 
-(defun javaimp--parse-preceding (regexp scope-start &optional bound skip-count)
+(defun javaimp-parse--preceding (regexp scope-start &optional bound skip-count)
   "Returns non-nil if a match for REGEXP is found before point,
 but not before BOUND.  Matches inside comments / strings are
 skipped.  Potential match is checked to be SKIP-COUNT lists away
 from the SCOPE-START (1 is for scope start itself, so if you want
 to skip one additional list, use 2 etc.).  If a match is found,
 then match-data is set, as for `re-search-backward'."
-  (and (javaimp--parse-rsb-keyword regexp bound t)
+  (and (javaimp-parse--rsb-keyword regexp bound t)
        (ignore-errors
          ;; Does our match belong to the right block?
          (= (scan-lists (match-end 0) (or skip-count 1) -1)
             (1+ scope-start)))))
 
-(defun javaimp--parse-decl-suffix (regexp brace-pos &optional bound)
+(defun javaimp-parse--decl-suffix (regexp brace-pos &optional bound)
   "Attempts to parse declaration suffix backwards from point (but
 not farther than BOUND), returning non-nil on success.  More
 precisely, the value is the end of the match for REGEXP.  Point
@@ -188,14 +228,14 @@ is left before the match.  Otherwise, the result is nil 
and point
 is unchanged."
   (let ((pos (point)))
     (catch 'found
-      (while (javaimp--parse-rsb-keyword regexp bound t)
+      (while (javaimp-parse--rsb-keyword regexp bound t)
         (let ((scan-pos (match-end 0)))
           (with-syntax-table javaimp--arglist-syntax-table
             ;; Skip over any number of lists, which may be exceptions
             ;; in "throws", or something like that
             (while (and scan-pos (<= scan-pos brace-pos))
               (if (ignore-errors
-                    (= (scan-lists scan-pos 1 -1) ;As in 
javaimp--parse-preceding
+                    (= (scan-lists scan-pos 1 -1) ;As in 
javaimp-parse--preceding
                        (1+ brace-pos)))
                   (progn
                     (goto-char (match-beginning 0))
@@ -209,58 +249,119 @@ is unchanged."
 
 ;;; Scopes
 
-(defvar javaimp--parse-scope-hook
+(defun javaimp-scope-copy (scope)
+  "Recursively copies SCOPE and its parents."
+  (let* ((res (copy-javaimp-scope scope))
+         (tmp res)
+         orig-parent)
+    (while (setq orig-parent (javaimp-scope-parent tmp))
+      (setf (javaimp-scope-parent tmp) (copy-javaimp-scope orig-parent))
+      (setq tmp (javaimp-scope-parent tmp)))
+    res))
+
+(defun javaimp-scope-filter-parents (scope pred)
+  "Rewrite SCOPE's parents so that only those matching PRED are
+left."
+  (while scope
+    (let ((parent (javaimp-scope-parent scope)))
+      (if (and parent
+               (not (funcall pred parent)))
+          ;; leave out this parent
+          (setf (javaimp-scope-parent scope) (javaimp-scope-parent parent))
+        (setq scope (javaimp-scope-parent scope))))))
+
+(defun javaimp-scope-concat-parents (scope)
+  (let (parents)
+    (while (setq scope (javaimp-scope-parent scope))
+      (push scope parents))
+    (mapconcat #'javaimp-scope-name parents ".")))
+
+(defsubst javaimp-scope-test-type (scope leaf-types parent-types)
+  (declare (indent 1))
+  (let ((res (memq (javaimp-scope-type scope) leaf-types)))
+    (while (and res
+                (setq scope (javaimp-scope-parent scope)))
+      (setq res (memq (javaimp-scope-type scope) parent-types)))
+    res))
+
+(defun javaimp-scope-defun-p (&optional additional)
+  "Return predicate which matches scopes in
+`javaimp-scope-classlike-types'.  ADDITIONAL is a list of scope
+types.  If it includes `method', then also method leafs are
+included.  If it includes `anon-class', then also leafs and
+parents may be anonymous classes."
+  (let ((leaf-types (append javaimp-scope-classlike-types
+                            (when (memq 'method additional) '(method))
+                            (when (memq 'anon-class additional) 
'(anon-class))))
+        (parent-types (append javaimp-scope-classlike-types
+                              (when (memq 'anon-class additional) 
'(anon-class)))))
+    (lambda (s)
+      (javaimp-scope-test-type s leaf-types parent-types))))
+
+(defun javaimp-scope-same-parent-p (parent)
+  (if parent
+      (lambda (s)
+        (and (javaimp-scope-parent s)
+             (= (javaimp-scope-open-brace (javaimp-scope-parent s))
+                (javaimp-scope-open-brace parent))))
+    (lambda (s)
+      (not (javaimp-scope-parent s)))))
+
+
+;; Scope parsing
+
+(defvar javaimp-parse--scope-hook
   (mapcar (lambda (parser)
             (lambda (arg)
               (save-excursion
                 (funcall parser arg))))
-          '(javaimp--parse-scope-array
+          '(javaimp-parse--scope-array
             ;; anon-class should be before method/stmt because it
             ;; looks similar, but with "new" in front
-            javaimp--parse-scope-anon-class
-            javaimp--parse-scope-class
-            javaimp--parse-scope-simple-stmt
-            javaimp--parse-scope-method-or-stmt
+            javaimp-parse--scope-anon-class
+            javaimp-parse--scope-class
+            javaimp-parse--scope-simple-stmt
+            javaimp-parse--scope-method-or-stmt
             ))
   "List of parser functions, each of which is called in
 `save-excursion' and with one argument, the position of opening
 brace.")
 
-(defun javaimp--parse-scope-class (brace-pos)
+(defun javaimp-parse--scope-class (brace-pos)
   "Attempts to parse 'class' / 'interface' / 'enum' scope."
-  (when (javaimp--parse-preceding
-         (regexp-opt javaimp--parse-classlike-keywords 'symbols)
+  (when (javaimp-parse--preceding
+         (regexp-opt javaimp-parse--classlike-keywords 'symbols)
          brace-pos
          ;; closest preceding closing paren is a good bound
          ;; because there _will be_ such char in frequent case
          ;; of method/stmt
          (save-excursion
-           (when (javaimp--parse-rsb-keyword ")" nil t 1)
+           (when (javaimp-parse--rsb-keyword ")" nil t 1)
              (1+ (point)))))
     (let* ((keyword-start (match-beginning 1))
            (keyword-end (match-end 1))
            arglist)
       (goto-char brace-pos)
-      (or (javaimp--parse-decl-suffix "\\_<extends\\_>" brace-pos keyword-end)
-          (javaimp--parse-decl-suffix "\\_<implements\\_>" brace-pos 
keyword-end)
-          (javaimp--parse-decl-suffix "\\_<permits\\_>" brace-pos keyword-end))
+      (or (javaimp-parse--decl-suffix "\\_<extends\\_>" brace-pos keyword-end)
+          (javaimp-parse--decl-suffix "\\_<implements\\_>" brace-pos 
keyword-end)
+          (javaimp-parse--decl-suffix "\\_<permits\\_>" brace-pos keyword-end))
       ;; we either skipped back over the valid declaration
       ;; suffix(-es), or there wasn't any
-      (setq arglist (javaimp--parse-arglist keyword-end (point) t))
+      (setq arglist (javaimp-parse--arglist keyword-end (point) t))
       (when (= (length arglist) 1)
         (make-javaimp-scope :type (intern
                                    (buffer-substring-no-properties
                                     keyword-start keyword-end))
-                            :name (javaimp--parse-substr-before-< (caar 
arglist))
+                            :name (javaimp-parse--substr-before-< (caar 
arglist))
                             :start keyword-start)))))
 
-(defun javaimp--parse-scope-simple-stmt (_brace-pos)
+(defun javaimp-parse--scope-simple-stmt (_brace-pos)
   "Attempts to parse 'simple-statement' scope."
-  (and (javaimp--parse-skip-back-until)
+  (and (javaimp-parse--skip-back-until)
        (or (and (= (char-before (1- (point))) ?-) ; ->
                 (= (char-before) ?>))
-           (looking-back (regexp-opt javaimp--parse-stmt-keywords 'words)
-                         (- (point) javaimp--parse-stmt-keyword-maxlen) nil))
+           (looking-back (regexp-opt javaimp-parse--stmt-keywords 'words)
+                         (- (point) javaimp-parse--stmt-keyword-maxlen) nil))
        (make-javaimp-scope
         :type 'simple-statement
         :name (or (match-string 1)
@@ -268,42 +369,42 @@ brace.")
         :start (or (match-beginning 1)
                    (- (point) 2)))))
 
-(defun javaimp--parse-scope-anon-class (brace-pos)
+(defun javaimp-parse--scope-anon-class (brace-pos)
   "Attempts to parse 'anon-class' scope."
   ;; skip arg-list and ws
   (when (and (progn
-                 (javaimp--parse-skip-back-until)
+                 (javaimp-parse--skip-back-until)
                  (= (char-before) ?\)))
                (ignore-errors
                  (goto-char
                   (scan-lists (point) -1 0))))
       (let ((end (point))
             start arglist)
-        (when (javaimp--parse-preceding "\\_<new\\_>" brace-pos nil 2)
+        (when (javaimp-parse--preceding "\\_<new\\_>" brace-pos nil 2)
           (setq start (match-beginning 0)
-                arglist (javaimp--parse-arglist (match-end 0) end t))
+                arglist (javaimp-parse--arglist (match-end 0) end t))
           (when (= (length arglist) 1)
             (make-javaimp-scope :type 'anon-class
                                 :name
                                 (concat "<anon>"
-                                        (javaimp--parse-substr-before-< (caar 
arglist)))
+                                        (javaimp-parse--substr-before-< (caar 
arglist)))
                                 :start start))))))
 
-(defun javaimp--parse-scope-method-or-stmt (brace-pos)
+(defun javaimp-parse--scope-method-or-stmt (brace-pos)
   "Attempts to parse 'method' or 'statement' scope."
   (let (;; take the closest preceding closing paren as the bound
         (throws-search-bound (save-excursion
-                               (when (javaimp--parse-rsb-keyword ")" nil t 1)
+                               (when (javaimp-parse--rsb-keyword ")" nil t 1)
                                  (1+ (point))))))
     (when throws-search-bound
       (let ((throws-args
-             (when-let ((pos (javaimp--parse-decl-suffix
+             (when-let ((pos (javaimp-parse--decl-suffix
                               "\\_<throws\\_>" brace-pos throws-search-bound)))
-               (or (javaimp--parse-arglist pos brace-pos t)
+               (or (javaimp-parse--arglist pos brace-pos t)
                    t))))
         (when (and (not (eq throws-args t))
                    (progn
-                     (javaimp--parse-skip-back-until)
+                     (javaimp-parse--skip-back-until)
                      (= (char-before) ?\)))
                    (ignore-errors
                      ;; for method this is arglist
@@ -313,42 +414,44 @@ brace.")
                  (arglist-region (cons (1+ (point))
                                        (1- (scan-lists (point) 1 0))))
                  (count (progn
-                          (javaimp--parse-skip-back-until)
+                          (javaimp-parse--skip-back-until)
                           (skip-syntax-backward "w_")))
                  (name (and (< count 0)
                             (buffer-substring-no-properties
                              (point) (+ (point) (abs count)))))
                  (type (when name
-                         (if (and (member name javaimp--parse-stmt-keywords)
+                         (if (and (member name javaimp-parse--stmt-keywords)
                                   (not throws-args))
                              'statement 'method))))
             (when type
               (make-javaimp-scope
                :type type
                :name (if (eq type 'method)
-                         (let ((args (javaimp--parse-arglist
+                         (let ((args (javaimp-parse--arglist
                                       (car arglist-region)
                                       (cdr arglist-region))))
                            (concat name "(" (mapconcat #'car args ",") ")"))
                        name)
                :start (point)))))))))
 
-(defun javaimp--parse-scope-array (_brace-pos)
+(defun javaimp-parse--scope-array (_brace-pos)
   "Attempts to parse 'array' scope."
-  (and (javaimp--parse-skip-back-until)
+  (and (javaimp-parse--skip-back-until)
        (member (char-before) '(?, ?\]))
        (make-javaimp-scope :type 'array
                            :name ""
                            :start nil)))
 
-(defun javaimp--parse-scopes (count)
+
+
+(defun javaimp-parse--scopes (count)
   "Attempts to parse COUNT enclosing scopes at point.
 Returns most nested one, with its parents sets accordingly.  If
 COUNT is nil then goes all the way up.
 
 Examines and sets property 'javaimp-parse-scope' at each scope's
 open brace.  If neither of functions in
-`javaimp--parse-scope-hook' return non-nil then the property
+`javaimp-parse--scope-hook' return non-nil then the property
 value is set to the symbol `unknown'.  Additionally, if a scope
 is recognized, but any of its parents is 'unknown', then it's set
 to 'unknown' too.
@@ -367,7 +470,7 @@ nothing."
           (let ((scope (get-text-property (point) 'javaimp-parse-scope)))
             (unless scope
               (setq scope (run-hook-with-args-until-success
-                           'javaimp--parse-scope-hook (point)))
+                           'javaimp-parse--scope-hook (point)))
               (if scope
                   (setf (javaimp-scope-open-brace scope) (point))
                 (setq scope 'unknown))
@@ -395,36 +498,36 @@ nothing."
         (setq res (cdr res)))
       parent)))
 
-(defun javaimp--parse-all-scopes ()
+(defun javaimp-parse--all-scopes ()
   "Parses all scopes in this buffer which are after
-`javaimp--parse-dirty-pos', if it points anywhere.  Makes it
+`javaimp-parse--dirty-pos', if it points anywhere.  Makes it
 point nowhere when done."
-  (unless javaimp--parse-dirty-pos      ;init on first use
-    (setq javaimp--parse-dirty-pos (point-min-marker))
-    (javaimp--parse-setup-buffer))
-  (when (marker-position javaimp--parse-dirty-pos)
+  (unless javaimp-parse--dirty-pos      ;init on first use
+    (setq javaimp-parse--dirty-pos (point-min-marker))
+    (javaimp-parse--setup-buffer))
+  (when (marker-position javaimp-parse--dirty-pos)
     (with-silent-modifications          ;we update only private props
-      (remove-text-properties javaimp--parse-dirty-pos (point-max)
+      (remove-text-properties javaimp-parse--dirty-pos (point-max)
                               '(javaimp-parse-scope nil))
       (goto-char (point-max))
       (let ((parse-sexp-ignore-comments t)
             ;; Can be removed when we no longer rely on cc-mode
             (parse-sexp-lookup-properties nil))
         (with-syntax-table javaimp-syntax-table
-          (while (javaimp--parse-rsb-keyword "{" javaimp--parse-dirty-pos t)
+          (while (javaimp-parse--rsb-keyword "{" javaimp-parse--dirty-pos t)
             (save-excursion
               (forward-char)
               ;; Set props at this brace and all the way up
-              (javaimp--parse-scopes nil))))))
-    (set-marker javaimp--parse-dirty-pos nil)))
+              (javaimp-parse--scopes nil))))))
+    (set-marker javaimp-parse--dirty-pos nil)))
 
-(defun javaimp--parse-setup-buffer ()
+(defun javaimp-parse--setup-buffer ()
   ;; FIXME This may be done in major/minor mode setup
   (setq syntax-ppss-table javaimp-syntax-table)
   (setq-local multibyte-syntax-as-symbol t)
-  (add-hook 'after-change-functions #'javaimp--parse-update-dirty-pos))
+  (add-hook 'after-change-functions #'javaimp-parse--update-dirty-pos))
 
-(defun javaimp--parse-enclosing-scope (&optional pred)
+(defun javaimp-parse--enclosing-scope (&optional pred)
   "Return innermost enclosing scope matching PRED."
   (with-syntax-table javaimp-syntax-table
     (let ((state (syntax-ppss)))
@@ -435,7 +538,7 @@ point nowhere when done."
       (catch 'found
         (while t
           (let ((res (save-excursion
-                       (javaimp--parse-scopes nil))))
+                       (javaimp-parse--scopes nil))))
             (when (and (javaimp-scope-p res)
                        (or (null pred)
                            (funcall pred res)))
@@ -447,27 +550,27 @@ point nowhere when done."
                   (setq state (syntax-ppss)))
               (throw 'found nil))))))))
 
-(defun javaimp--parse-class-abstract-methods ()
+(defun javaimp-parse--class-abstract-methods ()
   (goto-char (point-max))
   (let (res)
-    (while (javaimp--parse-rsb-keyword "\\_<abstract\\_>" nil t)
+    (while (javaimp-parse--rsb-keyword "\\_<abstract\\_>" nil t)
       (save-excursion
         (let ((enclosing (nth 1 (syntax-ppss))))
           (when (and enclosing
-                     (javaimp--parse-rsb-keyword ";" nil t -1)
+                     (javaimp-parse--rsb-keyword ";" nil t -1)
                      ;; are we in the same nest?
                      (= (nth 1 (syntax-ppss)) enclosing))
             (backward-char)        ;skip semicolon
             ;; now parse as normal method scope
-            (when-let ((scope (javaimp--parse-scope-method-or-stmt (point)))
+            (when-let ((scope (javaimp-parse--scope-method-or-stmt (point)))
                        ;; note that an abstract method with no
                        ;; parents will be ignored
-                       (parent (javaimp--parse-scopes nil)))
-              (setf (javaimp-scope-parent scope) (javaimp--copy-scope parent))
+                       (parent (javaimp-parse--scopes nil)))
+              (setf (javaimp-scope-parent scope) (javaimp-scope-copy parent))
               (push scope res))))))
     res))
 
-(defun javaimp--parse-interface-abstract-methods (int-scope)
+(defun javaimp-parse--interface-abstract-methods (int-scope)
   (let ((start (1+ (javaimp-scope-open-brace int-scope)))
         (end (ignore-errors
                (1- (scan-lists (javaimp-scope-open-brace int-scope) 1 0))))
@@ -475,49 +578,49 @@ point nowhere when done."
     (when (and start end)
       (goto-char end)
       (while (and (> (point) start)
-                  (javaimp--parse-rsb-keyword ";" start t))
+                  (javaimp-parse--rsb-keyword ";" start t))
         ;; are we in the same nest?
         (if (= (nth 1 (syntax-ppss)) (javaimp-scope-open-brace int-scope))
             (save-excursion
               ;; now parse as normal method scope
-              (when-let ((scope (javaimp--parse-scope-method-or-stmt (point))))
+              (when-let ((scope (javaimp-parse--scope-method-or-stmt (point))))
                 (setf (javaimp-scope-parent scope) int-scope)
                 (push scope res)))
           ;; we've entered another nest, go back to its start
           (goto-char (nth 1 (syntax-ppss))))))
     res))
 
-(defun javaimp--parse-update-dirty-pos (beg _end _old-len)
+(defun javaimp-parse--update-dirty-pos (beg _end _old-len)
   "Function to add to `after-change-functions' hook."
-  (when (and javaimp--parse-dirty-pos
-             (or (not (marker-position javaimp--parse-dirty-pos))
-                 (< beg javaimp--parse-dirty-pos)))
-    (set-marker javaimp--parse-dirty-pos beg)))
+  (when (and javaimp-parse--dirty-pos
+             (or (not (marker-position javaimp-parse--dirty-pos))
+                 (< beg javaimp-parse--dirty-pos)))
+    (set-marker javaimp-parse--dirty-pos beg)))
 
 
 ;; Functions intended to be called from other parts of javaimp.  They
 ;; do not preserve excursion / restriction - it's the caller's
 ;; responsibility.
 
-(defun javaimp--parse-get-package ()
+(defun javaimp-parse-get-package ()
   "Return the package declared in the current file.  Leaves point
 at the end of directive."
-  (javaimp--parse-all-scopes)
+  (javaimp-parse--all-scopes)
   (goto-char (point-max))
-  (when (javaimp--parse-rsb-keyword javaimp--parse-package-regexp nil t 1)
+  (when (javaimp-parse--rsb-keyword javaimp-parse--package-regexp nil t 1)
     (goto-char (match-end 0))
     (match-string 2)))
 
-(defun javaimp--parse-get-imports ()
+(defun javaimp-parse-get-imports ()
   "Parse import directives in the current buffer and return (REGION
 . CLASS-ALIST).  REGION, a cons of two positions, spans from bol
 of first import to eol of last import.  CLASS-ALIST contains
 elements (CLASS . TYPE), where CLASS is a string and TYPE is
 either of symbols `normal' or 'static'."
-  (javaimp--parse-all-scopes)
+  (javaimp-parse--all-scopes)
   (goto-char (point-max))
   (let (start-pos end-pos class-alist)
-    (while (javaimp--parse-rsb-keyword javaimp--parse-import-regexp nil t)
+    (while (javaimp-parse--rsb-keyword javaimp-parse--import-regexp nil t)
       (setq start-pos (line-beginning-position))
       (unless end-pos
         (setq end-pos (line-end-position)))
@@ -528,7 +631,7 @@ either of symbols `normal' or 'static'."
     (cons (and start-pos end-pos (cons start-pos end-pos))
           class-alist)))
 
-(defun javaimp--parse-get-all-scopes (&optional beg end pred parent-pred)
+(defun javaimp-parse-get-all-scopes (&optional beg end pred parent-pred)
   "Return all scopes in the current buffer between positions BEG
 and END, both exclusive, optionally filtering them with PRED, and
 their parents with PARENT-PRED.  Neither of PRED or PARENT-PRED
@@ -536,7 +639,7 @@ should move point.  Note that parents may be outside of 
region
 given by BEG and END.  BEG is the LIMIT argument to
 `previous-single-property-change', and so may be nil.  END
 defaults to end of accessible portion of the buffer."
-  (javaimp--parse-all-scopes)
+  (javaimp-parse--all-scopes)
   (let ((pos (or end (point-max)))
         scope res)
     (while (and (setq pos (previous-single-property-change
@@ -547,46 +650,46 @@ defaults to end of accessible portion of the buffer."
       (when (and (javaimp-scope-p scope)
                  (or (null pred)
                      (funcall pred scope)))
-        (setq scope (javaimp--copy-scope scope))
+        (setq scope (javaimp-scope-copy scope))
         (when parent-pred
-          (javaimp--filter-scope-parents scope parent-pred))
+          (javaimp-scope-filter-parents scope parent-pred))
         (push scope res)))
     res))
 
-(defun javaimp--parse-get-enclosing-scope (&optional pred parent-pred)
+(defun javaimp-parse-get-enclosing-scope (&optional pred parent-pred)
   "Return innermost enclosing scope at point, optionally checking
 it with PRED, and its parents with PARENT-PRED."
   (save-excursion
-    (javaimp--parse-all-scopes))
-  (when-let ((scope (javaimp--parse-enclosing-scope pred)))
-    (setq scope (javaimp--copy-scope scope))
+    (javaimp-parse--all-scopes))
+  (when-let ((scope (javaimp-parse--enclosing-scope pred)))
+    (setq scope (javaimp-scope-copy scope))
     (when parent-pred
-      (javaimp--filter-scope-parents scope parent-pred))
+      (javaimp-scope-filter-parents scope parent-pred))
     scope))
 
-(defun javaimp--parse-get-class-abstract-methods ()
-  (javaimp--parse-all-scopes)
-  (javaimp--parse-class-abstract-methods))
+(defun javaimp-parse-get-class-abstract-methods ()
+  (javaimp-parse--all-scopes)
+  (javaimp-parse--class-abstract-methods))
 
-(defun javaimp--parse-get-interface-abstract-methods ()
-  (let ((interfaces (javaimp--parse-get-all-scopes
+(defun javaimp-parse-get-interface-abstract-methods ()
+  (let ((interfaces (javaimp-parse-get-all-scopes
                      nil nil
                      (lambda (s)
-                       (javaimp-test-scope-type s
-                         '(interface) javaimp--classlike-scope-types)))))
-    (seq-mapcat #'javaimp--parse-interface-abstract-methods
+                       (javaimp-scope-test-type s
+                         '(interface) javaimp-scope-classlike-types)))))
+    (seq-mapcat #'javaimp-parse--interface-abstract-methods
                 interfaces)))
 
 
-(defmacro javaimp--parse-without-hook (&rest body)
+(defmacro javaimp-parse-without-hook (&rest body)
   "Execute BODY, temporarily removing
-`javaimp--parse-update-dirty-pos' from `after-change-functions'
+`javaimp-parse--update-dirty-pos' from `after-change-functions'
 hook."
   (declare (debug t) (indent 0))
   `(unwind-protect
        (progn
-         (remove-hook 'after-change-functions 
#'javaimp--parse-update-dirty-pos)
+         (remove-hook 'after-change-functions 
#'javaimp-parse--update-dirty-pos)
          ,@body)
-     (add-hook 'after-change-functions #'javaimp--parse-update-dirty-pos)))
+     (add-hook 'after-change-functions #'javaimp-parse--update-dirty-pos)))
 
 (provide 'javaimp-parse)
diff --git a/javaimp-tests.el b/javaimp-tests.el
index 42b5daa069..165eef1ec2 100644
--- a/javaimp-tests.el
+++ b/javaimp-tests.el
@@ -1,431 +1,19 @@
-;;; javaimp-tests.el --- javaimp tests  -*- lexical-binding: t; -*-
+;;; javaimp-tests.el --- javaimp test util  -*- lexical-binding: t; -*-
 
-;; Copyright (C) 2016-2021  Free Software Foundation, Inc.
+;; Copyright (C) 2022-2022  Free Software Foundation, Inc.
 
 ;; Author: Filipp Gunbin <[email protected]>
 ;; Maintainer: Filipp Gunbin <[email protected]>
 
-(require 'ert)
-(require 'javaimp)
-
-;; Use this selector to run all tests which do not invoke build tool:
-;;
-;; (and "^javaimp-" (not (tag :runs-build-tool)))
-;;
-
-;; Tests for low-level helpers of scope parsers.
-
-(ert-deftest javaimp-test--parse-arglist ()
-  (dolist (data '(("")
-                  ("  ")
-                  ("int i"
-                   ("int" . "i"))
-                  ("\nint\ni\n,\nint\nj\n"
-                   ("int" . "i")
-                   ("int" . "j"))
-                  (" List<? extends Comparable<? super T>> list, T... elements"
-                   ("List<? extends Comparable<? super T>>" . "list")
-                   ("T..." . "elements"))
-                  ("org.foo.Map <? extends K,   \n? extends V> m, String [] 
array, int i"
-                   ;; TODO remove extra ws within
-                   ("org.foo.Map <? extends K, ? extends V>" . "m")
-                   ("String []" . "array")
-                   ("int" . "i"))
-                  (" Bi_Function<? super K, ? super V, ? extends V> function "
-                   ("Bi_Function<? super K, ? super V, ? extends V>" . 
"function"))
-                  ("@Annotation1 final int i,
[email protected]_2(value_1 = \"value1 , (){}\", value2 = -2.3) String[][] 
arr"
-                   ("int" . "i")
-                   ("String[][]" . "arr"))
-                  ))
-    (with-temp-buffer
-      (insert (car data))
-      (should (equal (javaimp--parse-arglist (point-min) (point-max))
-                     (cdr data))))))
-
-(ert-deftest javaimp-test--parse-arglist-throws ()
-  (dolist (data '(("")
-                  ("  ")
-                  ("Exception1"
-                   ("Exception1"))
-                  ("\nEx1\n,\nEx2\n"
-                   ("Ex1")
-                   ("Ex2"))
-                  (" Exception1 , org.foo_bar_3.Exception_2 "
-                   ("Exception1")
-                   ("org.foo_bar_3.Exception_2"))
-                  ("Exception_1<? extends org.foo.Exception_2<? super 
Exception3>,
-  Exception4<? super Exception5>>, \nException6,Exception7<Exception8>"
-                   ("Exception_1<? extends org.foo.Exception_2<? super 
Exception3>, \
-Exception4<? super Exception5>>")
-                   ("Exception6")
-                   ("Exception7<Exception8>"))))
-    (with-temp-buffer
-      (insert (car data))
-      (should (equal (javaimp--parse-arglist (point-min) (point-max) t)
-                     (cdr data))))))
-
-
-
-;; Tests for single scope parsers, which should be in
-;; `javaimp--parse-scope-hook'.
-
-(ert-deftest javaimp-test--parse-scope-class ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-class
-    '("class Foo {"
-      class "Foo")
-    '("class Foo extends Bar {"
-      class "Foo")
-    '("class Foo implements Bar {"
-      class "Foo")
-    '("class Foo implements Bar, Baz {"
-      class "Foo")
-    '("public class Foo extends Bar implements Baz1 , Baz2 {"
-      class "Foo")
-    `(,(subst-char-in-string
-        ?  ?\n
-        "public class Foo extends Bar implements Baz1 , Baz2 {")
-      class "Foo")
-    '("class Foo<Bar, Baz> extends FooSuper<Bar, Baz> \
-implements Interface1<Bar, Baz>, Interface2 {"
-      class "Foo")
-    '("class Foo<E extends Bar> {"
-      class "Foo")
-    '("class Foo<Enum<?>> {"
-      class "Foo")
-    '("class Foo<T extends Baz<? extends Baz2>> \
-extends Bar<? extends Baz<? extends Baz2>> {"
-      class "Foo")
-    '("interface Foo<Bar, Baz> {"
-      interface "Foo")
-    '("private enum Foo {"
-      enum "Foo")
-    ))
-
-(ert-deftest javaimp-test--parse-scope-anon-class ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-anon-class
-    '(" = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {"
-      anon-class "<anon>Object")
-    `(,(subst-char-in-string
-        ?  ?\n
-        " = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {")
-      anon-class "<anon>Object")
-    '(" = (obj.getField()).new Object<Class1, Class2>(1, baz) {"
-      anon-class "<anon>Object")
-    '(" = obj.new Object<>(1, baz) {"
-      anon-class "<anon>Object")
-    ))
-
-(ert-deftest javaimp-test--parse-scope-method-or-stmt ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-method-or-stmt
-    '("static void foo_bar ( String a , int b ) {"
-      method "foo_bar(String,int)")
-    `(,(subst-char-in-string
-        ?  ?\n
-        "static void foo_bar ( String a , int b ) {")
-      method "foo_bar(String,int)")
-    '("void foo_bar(String a, int b) throws E1, E2 {"
-      method "foo_bar(String,int)")
-    '("void foo_bar()
-throws E1 {"
-      method "foo_bar()")
-    '("if (foo_bar(a, b) < 2) {"
-      statement "if")
-    ))
-
-(ert-deftest javaimp-test--parse-scope-simple-stmt ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-simple-stmt
-    '(" try {"
-      simple-statement "try")
-    `(,(subst-char-in-string ?  ?\n " try {")
-      simple-statement "try")
-    ;; static initializer
-    '("static {"
-      simple-statement "static")
-    ;; lambda
-    '("it -> {"
-      simple-statement "lambda")
-    '("(x, y) -> {"
-      simple-statement "lambda")
-    ))
-
-(ert-deftest javaimp-test--parse-scope-array ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-array
-    '("new String[] {"
-      array "")
-    ;; TODO fix
-    ;; '("new Object[][] { {"
-    ;;   array "")
-    ;; '("new int[] {{1, 2}, {"
-    ;;   array "")
-    ))
-
-(defun javaimp-test--single-parser (parser &rest test-items)
-  (declare (indent 1))
-  (dolist (item test-items)
-    (with-temp-buffer
-      (insert (nth 0 item))
-      (let* ((javaimp--parse-scope-hook
-              (lambda (arg)
-                (save-excursion
-                  (funcall parser arg))))
-             (scopes (javaimp--parse-get-all-scopes)))
-        (should (= 1 (length scopes)))
-        (should (eq (javaimp-scope-type (car scopes)) (nth 1 item)))
-        (should (equal (javaimp-scope-name (car scopes)) (nth 2 item)))))))
-
-
-;; Tests for parse "api"
-
-(ert-deftest javaimp-test--parse-get-package ()
-  (with-temp-buffer
-    (insert "
-  package  foo.bar.baz  ;
-//package commented.line;
-/*
-package commented.block;
-*/
-")
-    (should (equal (javaimp--parse-get-package) "foo.bar.baz"))))
-
-(ert-deftest javaimp-test--parse-get-imports ()
-  (with-temp-buffer
-    (insert "
-  import  some.class1  ;
-import  static  some.class.fun1;
-//import commented.line;
-/*
-import static commented.block;
-*/
-import someclass2;
-import my.package.* ;
-
-import static some_class.fun_2; // comment
-// comment outside
-")
-    (should (equal
-             (javaimp--parse-get-imports)
-             '((2 . 206)
-               ("some.class1" . normal)
-               ("some.class.fun1" . static)
-               ("someclass2" . normal)
-               ("my.package.*" . normal)
-               ("some_class.fun_2" . static))))))
-
-(ert-deftest javaimp-test--parse-get-all-scopes ()
-  (with-temp-buffer
-    (insert-file-contents
-     (concat javaimp--basedir "testdata/test1-misc-classes.java"))
-    (should-not javaimp--parse-dirty-pos)
-    ;;
-    ;; parse full buffer
-    (javaimp-test--check-named-scopes)
-    (should javaimp--parse-dirty-pos)
-    (should-not (marker-position javaimp--parse-dirty-pos))
-    ;;
-    ;; reparse half of the buffer
-    (set-marker javaimp--parse-dirty-pos (/ (- (point-max) (point-min)) 2))
-    (javaimp-test--check-named-scopes)
-    (should-not (marker-position javaimp--parse-dirty-pos))
-    ;;
-    ;; don't reparse
-    (javaimp-test--check-named-scopes)))
-
-(defun javaimp-test--check-named-scopes ()
-  (let* ((scopes (javaimp--parse-get-all-scopes
-                  nil nil
-                  (lambda (s)
-                    (memq (javaimp-scope-type s) '(class interface enum 
method)))
-                  (lambda (s)
-                    (memq (javaimp-scope-type s) '(class interface enum 
method)))))
-         (actual (mapcar
-                  (lambda (s)
-                    (let (res)
-                      (while s
-                        (push (list (javaimp-scope-type s)
-                                    (javaimp-scope-name s))
-                              res)
-                        (setq s (javaimp-scope-parent s)))
-                      (nreverse res)))
-                  scopes))
-         (expected
-          '(((class "Top"))
-            ((class "CInner1") (class "Top"))
-            ((method "foo()") (class "CInner1") (class "Top"))
-            ((class "CInner1_CLocal1")
-             (method "foo()") (class "CInner1") (class "Top"))
-            ((method "foo()")
-             (class "CInner1_CLocal1")
-             (method "foo()") (class "CInner1") (class "Top"))
-            ((class "CInner1_CLocal1_CLocal1")
-             (method "foo()")
-             (class "CInner1_CLocal1")
-             (method "foo()") (class "CInner1") (class "Top"))
-            ((method "foo()")
-             (class "CInner1_CLocal1_CLocal1")
-             (method "foo()")
-             (class "CInner1_CLocal1")
-             (method "foo()") (class "CInner1") (class "Top"))
-
-            ((class "CInner1_CLocal2")
-             (method "foo()") (class "CInner1") (class "Top"))
-            ((method "foo()")
-             (class "CInner1_CLocal2")
-             (method "foo()") (class "CInner1") (class "Top"))
-
-            ((method "toString()")
-             (class "CInner1") (class "Top"))
-
-            ((class "CInner1_CInner1") (class "CInner1") (class "Top"))
-            ((method "foo()")
-             (class "CInner1_CInner1") (class "CInner1") (class "Top"))
-            ((method "bar()")
-             (class "CInner1_CInner1") (class "CInner1") (class "Top"))
-
-            ((interface "IInner1") (class "Top"))
-            ((method "foo()") (interface "IInner1") (class "Top"))
-            ((class "IInner1_CInner1") (interface "IInner1") (class "Top"))
-            ((method "foo()")
-             (class "IInner1_CInner1") (interface "IInner1") (class "Top"))
-            ((method "defaultMethod(String)")
-             (interface "IInner1") (class "Top"))
-
-            ((interface "IInner1_IInner1") (interface "IInner1") (class "Top"))
-            ((method "defaultMethod(String)")
-             (interface "IInner1_IInner1") (interface "IInner1") (class "Top"))
-
-            ((enum "EnumInner1") (class "Top"))
-            ((method "EnumInner1()") (enum "EnumInner1") (class "Top"))
-            ((method "foo()") (enum "EnumInner1") (class "Top"))
-            ((enum "EnumInner1_EInner1") (enum "EnumInner1") (class "Top"))
-
-            ((class "ColocatedTop"))
-            ((method "foo()") (class "ColocatedTop"))
-            ((method "bar(String,String)") (class "ColocatedTop")))))
-    (should (= (length expected) (length actual)))
-    (dotimes (i (length expected))
-      (should (equal (nth i expected) (nth i actual))))
-    ;;
-    (let ((data
-         `((,(nth 0 scopes) "Top" 26 36)
-           (,(nth 16 scopes) "foo()" 1798 1804)
-           (,(nth 23 scopes) "EnumInner1_EInner1" 2462 2486)
-           (,(nth 25 scopes) "foo()" 2554 2560))))
-      (dolist (elt data)
-        (let ((scope (nth 0 elt)))
-          (should (equal (nth 1 elt) (javaimp-scope-name scope)))
-          (should (equal (nth 2 elt) (javaimp-scope-start scope)))
-          (should (equal (nth 3 elt) (javaimp-scope-open-brace scope))))))))
-
-
-
-;; Tests for javaimp--get-buffer-classes
-
-(ert-deftest javaimp-test--get-buffer-classes ()
-  (with-temp-buffer
-    (insert-file-contents
-     (concat javaimp--basedir "testdata/test1-misc-classes.java"))
-    (should (equal (javaimp--get-buffer-classes)
-                   '("org.foo.Top"
-                     "org.foo.Top.CInner1"
-                     "org.foo.Top.CInner1.CInner1_CInner1"
-                     "org.foo.Top.IInner1"
-                     "org.foo.Top.IInner1.IInner1_CInner1"
-                     "org.foo.Top.IInner1.IInner1_IInner1"
-                     "org.foo.Top.EnumInner1"
-                     "org.foo.Top.EnumInner1.EnumInner1_EInner1"
-                     "org.foo.ColocatedTop")))))
-
-
-;; Tests for imenu function
-
-(ert-deftest javaimp-test--imenu ()
-  (let ((actual (with-temp-buffer
-                  (insert-file-contents
-                   (concat javaimp--basedir 
"testdata/test1-misc-classes.java"))
-                  (let ((imenu-use-markers nil))
-                    (javaimp-imenu-create-index))))
-        (expected-names
-         '("foo() [Top.CInner1]"
-           "foo() [Top.CInner1.CInner1_CInner1]"
-           "abstract_method() [Top.CInner1.CInner1_CInner1]"
-           "bar()"
-           "baz() [Top.CInner1.CInner1_CInner1]"
-           "foo() [Top.IInner1]"
-           "abstract_method() [Top.IInner1]"
-           "foo() [Top.IInner1.IInner1_CInner1]"
-           "baz() [Top.IInner1]"
-           "defaultMethod(String) [Top.IInner1]"
-           "foo() [Top.IInner1.IInner1_IInner1]"
-           "defaultMethod(String) [Top.IInner1.IInner1_IInner1]"
-           "baz() [Top.IInner1.IInner1_IInner1]"
-           "EnumInner1()"
-           "foo() [Top.EnumInner1]"
-           "foo() [ColocatedTop]"
-           "bar(String,String)")))
-    (should (= (length expected-names) (length actual)))
-    (dotimes (i (length expected-names))
-      (should (equal (nth i expected-names) (car (nth i actual)))))))
-
-(ert-deftest javaimp-test--imenu-use-sub-alists ()
-  (let ((actual (with-temp-buffer
-                  (insert-file-contents
-                   (concat javaimp--basedir 
"testdata/test1-misc-classes.java"))
-                  (let ((imenu-use-markers nil)
-                        (javaimp-imenu-use-sub-alists t))
-                    (javaimp-imenu-create-index))))
-        (expected
-         '(("Top"
-            ("CInner1"
-             ("foo()" . 98)
-             ("CInner1_CInner1"
-              ("foo()" . 1099)
-              ("abstract_method()" . 1148)
-              ("bar()" . 1192)
-              ("baz()" . 1281)))
-            ("IInner1"
-             ("foo()" . 1603)
-             ("abstract_method()" . 1715)
-             ("IInner1_CInner1"
-              ("foo()" . 1798))
-             ("baz()" . 1934)
-             ("defaultMethod(String)" . 1963)
-             ("IInner1_IInner1"
-              ("foo()" . 2122)
-              ("defaultMethod(String)" . 2157)
-              ("baz()" . 2258)))
-            ("EnumInner1"
-             ("EnumInner1()" . 2353)
-             ("foo()" . 2399)
-             ;; "EnumInner1_EInner1" omitted because no methods inside
-             ))
-           ("ColocatedTop"
-            ("foo()" . 2554)
-            ("bar(String,String)" . 2578)))))
-    (javaimp-test--imenu-simplify-entries actual)
-    (should (equal expected actual))))
-
-(defun javaimp-test--imenu-simplify-entries (alist)
-  (dolist (elt alist)
-    (if (and (= (length elt) 4)
-             (functionp (nth 2 elt)))
-        (setcdr elt (nth 1 elt))
-      (javaimp-test--imenu-simplify-entries (cdr elt)))))
-
-
-;; Utility
-
-(defun javaimp-test--with-data (filename handler)
-  "Untars testdata/FILENAME into temporary directory and runs
-HANDLER, supplying temp directory name as the only arg."
+(defun javaimp-call-with-data (filename handler)
+  "Untar FILENAME into temporary directory and call HANDLER,
+supplying that directory name as the only arg."
   (let ((tmpdir (file-name-as-directory (make-temp-file "javaimp" t))))
     (unwind-protect
         (let ((rc (call-process
                    "tar" nil nil nil
                    "-x"
-                   "-f" (concat javaimp--basedir
-                                (file-name-as-directory "testdata")
-                                filename)
+                   "-f" filename
                    "-C" tmpdir)))
           (unless (= rc 0)
             (error "Cannot untar test data %s: %d" filename rc))
diff --git a/javaimp-util.el b/javaimp-util.el
index 7105323768..8813b7afb6 100644
--- a/javaimp-util.el
+++ b/javaimp-util.el
@@ -21,34 +21,27 @@
 
 ;;; Code:
 
-(require 'xml)
 (require 'cl-lib)
 (require 'seq)
+(require 'xml)
+
+(defconst javaimp-basedir (file-name-directory load-file-name))
 
-(defconst javaimp--basedir (file-name-directory load-file-name))
-
-(defconst javaimp--classlike-scope-types
-  '(class interface enum))
-
-(defconst javaimp--all-scope-types
-  (append
-   '(anon-class
-     array
-     method
-     simple-statement
-     statement
-     array)
-   javaimp--classlike-scope-types))
-
-(defconst javaimp--show-scopes-scope-type-abbrevs
-  '((anon-class . "ac")
-    (statement . "st")
-    (simple-statement . "ss")
-    (array . "ar")
-    (method . "me")
-    (class . "cl")
-    (interface . "in")
-    (enum . "en")))
+(defcustom javaimp-java-home
+  (let ((val (getenv "JAVA_HOME")))
+    (and val (not (string-blank-p val))
+         val))
+  "Path to the JDK.  The directory given should contain
+subdirectory \"jre/lib\" (pre-JDK9) or just \"lib\".  By default,
+it is initialized from the JAVA_HOME environment variable."
+  :type 'string
+  :group 'javaimp)
+
+(defcustom javaimp-cygpath-program
+  (if (eq system-type 'cygwin) "cygpath")
+  "Path to the `cygpath' program (Cygwin only)."
+  :type 'string
+  :group 'javaimp)
 
 (defvar javaimp-tool-output-buf-name "*javaimp-tool-output*"
   "Name of the buffer to which `javaimp--call-build-tool' copies
@@ -83,12 +76,6 @@ build tool output.  Can be let-bound to nil to suppress 
copying.")
 (cl-defstruct javaimp-cached-file
   file read-ts classes)
 
-(cl-defstruct javaimp-scope
-  type                                  ; see javaimp--all-scope-types
-  name
-  start
-  open-brace
-  parent)
 
 (defsubst javaimp-print-id (id)
   (format "%s:%s:%s"
@@ -99,87 +86,26 @@ build tool output.  Can be let-bound to nil to suppress 
copying.")
 
 ;; Xml
 
-(defun javaimp--xml-children (xml-tree child-name)
+(defun javaimp-xml-children (xml-tree child-name)
   "Returns list of children of XML-TREE filtered by CHILD-NAME"
   (seq-filter (lambda (child)
                (and (consp child)
                     (eq (car child) child-name)))
              (cddr xml-tree)))
 
-(defun javaimp--xml-child (name el)
+(defun javaimp-xml-child (name el)
   "Returns a child of EL named by symbol NAME"
   (assq name (cddr el)))
 
-(defun javaimp--xml-first-child (el)
+(defun javaimp-xml-first-child (el)
   "Returns a first child of EL"
   (car (cddr el)))
 
 
 
-;; Scopes
-
-(defun javaimp--copy-scope (scope)
-  "Recursively copies SCOPE and its parents."
-  (let* ((res (copy-javaimp-scope scope))
-         (tmp res)
-         orig-parent)
-    (while (setq orig-parent (javaimp-scope-parent tmp))
-      (setf (javaimp-scope-parent tmp) (copy-javaimp-scope orig-parent))
-      (setq tmp (javaimp-scope-parent tmp)))
-    res))
-
-(defun javaimp--filter-scope-parents (scope pred)
-  "Rewrite SCOPE's parents so that only those matching PRED are
-left."
-  (while scope
-    (let ((parent (javaimp-scope-parent scope)))
-      (if (and parent
-               (not (funcall pred parent)))
-          ;; leave out this parent
-          (setf (javaimp-scope-parent scope) (javaimp-scope-parent parent))
-        (setq scope (javaimp-scope-parent scope))))))
-
-(defun javaimp--concat-scope-parents (scope)
-  (let (parents)
-    (while (setq scope (javaimp-scope-parent scope))
-      (push scope parents))
-    (mapconcat #'javaimp-scope-name parents ".")))
-
-(defsubst javaimp-test-scope-type (scope leaf-types parent-types)
-  (declare (indent 1))
-  (let ((res (memq (javaimp-scope-type scope) leaf-types)))
-    (while (and res
-                (setq scope (javaimp-scope-parent scope)))
-      (setq res (memq (javaimp-scope-type scope) parent-types)))
-    res))
-
-(defun javaimp--defun-scope-pred (&optional additional)
-  "Return predicate which matches scopes in
-`javaimp--classlike-scope-types'.  ADDITIONAL is a list of scope
-types.  If it includes `method', then also method leafs are
-included.  If it includes `anon-class', then also leafs and
-parents may be anonymous classes."
-  (let ((leaf-types (append javaimp--classlike-scope-types
-                            (when (memq 'method additional) '(method))
-                            (when (memq 'anon-class additional) 
'(anon-class))))
-        (parent-types (append javaimp--classlike-scope-types
-                              (when (memq 'anon-class additional) 
'(anon-class)))))
-    (lambda (s)
-      (javaimp-test-scope-type s leaf-types parent-types))))
-
-(defun javaimp--scope-same-parent-pred (parent)
-  (if parent
-      (lambda (s)
-        (and (javaimp-scope-parent s)
-             (= (javaimp-scope-open-brace (javaimp-scope-parent s))
-                (javaimp-scope-open-brace parent))))
-    (lambda (s)
-      (not (javaimp-scope-parent s)))))
-
-
 ;; Tree
 
-(defun javaimp--build-tree (this all child-p &optional parent-node sort-pred)
+(defun javaimp-tree-build (this all child-p &optional parent-node sort-pred)
   "Recursively builds tree for element THIS and its children.
 Children are those elements from ALL for which CHILD-P invoked
 with this element and tested element returns non-nil.  Children
@@ -195,20 +121,21 @@ recursive calls."
                       :contents this))
           (child-nodes
            (mapcar (lambda (child)
-                     (javaimp--build-tree
+                     (javaimp-tree-build
                        child all child-p this-node sort-pred))
                    children)))
       (setf (javaimp-node-children this-node) child-nodes)
       this-node)))
 
-(defun javaimp--find-node (contents-pred forest &optional unwrap)
+
+(defun javaimp-tree-find-node (contents-pred forest &optional unwrap)
   "Return first node for which CONTENTS-PRED returns non-nil.  If
 UNWRAP is non-nil, then node contents is returned."
   (catch 'found
     (dolist (tree forest)
-      (javaimp--find-node-in-tree tree contents-pred unwrap))))
+      (javaimp-tree--find-node-1 tree contents-pred unwrap))))
 
-(defun javaimp--find-node-in-tree (tree contents-pred unwrap)
+(defun javaimp-tree--find-node-1 (tree contents-pred unwrap)
   (when tree
     (if (funcall contents-pred (javaimp-node-contents tree))
        (throw 'found
@@ -216,30 +143,30 @@ UNWRAP is non-nil, then node contents is returned."
                    (javaimp-node-contents tree)
                  tree)))
     (dolist (child (javaimp-node-children tree))
-      (javaimp--find-node-in-tree child contents-pred unwrap))))
+      (javaimp-tree--find-node-1 child contents-pred unwrap))))
 
 
-(defun javaimp--collect-nodes (contents-pred forest)
+(defun javaimp-tree-collect-nodes (contents-pred forest)
   "Return all nodes' contents for which CONTENTS-PRED returns
 non-nil."
   (apply #'seq-concatenate 'list
         (mapcar (lambda (tree)
                    (delq nil
-                        (javaimp--collect-nodes-from-tree tree contents-pred)))
+                        (javaimp-tree--collect-nodes-1 tree contents-pred)))
                 forest)))
 
-(defun javaimp--collect-nodes-from-tree (tree contents-pred)
+(defun javaimp-tree--collect-nodes-1 (tree contents-pred)
   (when tree
     (cons (and (funcall contents-pred (javaimp-node-contents tree))
                (javaimp-node-contents tree))
          (apply #'seq-concatenate 'list
                 (mapcar (lambda (child)
                            (delq nil
-                                (javaimp--collect-nodes-from-tree child 
contents-pred)))
+                                (javaimp-tree--collect-nodes-1 child 
contents-pred)))
                         (javaimp-node-children tree))))))
 
 
-(defun javaimp--map-nodes (function pred forest)
+(defun javaimp-tree-map-nodes (function pred forest)
   "Recursively applies FUNCTION to each node's contents in FOREST
 and returns new tree.  FUNCTION should return (t . VALUE) if the
 result for this node should be made a list of the form (VALUE
@@ -248,10 +175,10 @@ this case children are discarded).  The result for each 
node is
 additionally tested by PRED."
   (delq nil
         (mapcar (lambda (tree)
-                  (javaimp--map-nodes-from-tree tree function pred))
+                  (javaimp-tree--map-nodes-1 tree function pred))
                 forest)))
 
-(defun javaimp--map-nodes-from-tree (tree function pred)
+(defun javaimp-tree--map-nodes-1 (tree function pred)
   (when tree
     (let* ((cell (funcall function (javaimp-node-contents tree)))
            (res
@@ -259,7 +186,7 @@ additionally tested by PRED."
                 (let ((children
                        (delq nil
                              (mapcar (lambda (child)
-                                       (javaimp--map-nodes-from-tree
+                                       (javaimp-tree--map-nodes-1
                                         child function pred))
                                      (javaimp-node-children tree)))))
                   (cons (cdr cell) children))
@@ -267,14 +194,9 @@ additionally tested by PRED."
       (and (funcall pred res)
            res))))
 
-(defun javaimp--get-root (node)
-  (while (javaimp-node-parent node)
-    (setq node (javaimp-node-parent node)))
-  node)
-
 
 
-;; Files & caches
+;; Misc
 
 (defsubst javaimp--get-file-ts (file)
   (nth 5 (file-attributes file)))
@@ -307,9 +229,6 @@ error, the cache for FILE is cleared."
      (signal (car err) (cdr err)))))
 
 
-
-;; System
-
 ;; TODO use functions `cygwin-convert-file-name-from-windows' and
 ;; `cygwin-convert-file-name-to-windows' when they are available
 ;; instead of calling `cygpath'.  See
diff --git a/javaimp.el b/javaimp.el
index 4d42e4adb7..857c127cef 100644
--- a/javaimp.el
+++ b/javaimp.el
@@ -58,7 +58,7 @@
 ;; (setq javaimp-project forest nil)
 ;;
 ;; Project structure and dependency information is retrieved from the
-;; build tool, see `javaimp--maven-visit' and `javaimp--gradle-visit',
+;; build tool, see `javaimp-maven-visit' and `javaimp-gradle-visit',
 ;; and the `javaimp-handler-regexp-alist' variable.  The output from
 ;; the build tool can be inspected in buffer named by
 ;; `javaimp-tool-output-buf-name' variable.  If there exists
@@ -136,11 +136,11 @@
 
 ;;; Code:
 
+(require 'javaimp-util)
 (require 'javaimp-maven)
 (require 'javaimp-gradle)
 (require 'javaimp-parse)
-(require 'javaimp-util)
-(require 'cc-mode)                      ;for java-mode-syntax-table
+
 (require 'imenu)
 
 
@@ -170,15 +170,6 @@ The order of classes which were not matched is defined by
 `javaimp-import-group-alist'"
   :type 'integer)
 
-(defcustom javaimp-java-home
-  (let ((val (getenv "JAVA_HOME")))
-    (and val (not (string-blank-p val))
-         val))
-  "Path to the JDK.  The directory given should contain
-subdirectory \"jre/lib\" (pre-JDK9) or just \"lib\".  By default,
-it is initialized from the JAVA_HOME environment variable."
-  :type 'string)
-
 (defcustom javaimp-additional-source-dirs nil
   "List of directories where additional (e.g. generated)
 source files reside.
@@ -220,28 +211,13 @@ class)."
   "Path to the `jmod' program used to read contents of jmod files."
   :type 'string)
 
-(defcustom javaimp-cygpath-program
-  (if (eq system-type 'cygwin) "cygpath")
-  "Path to the `cygpath' program (Cygwin only)."
-  :type 'string)
-
-(defcustom javaimp-mvn-program "mvn"
-  "Path to the `mvn' program.  If the visited project has local
-mvnw (Maven wrapper), it is used in preference."
-  :type 'string)
-
-(defcustom javaimp-gradle-program "gradle"
-  "Path to the `gradle' program.  If the visited project has local
-gradlew (Gradle wrapper), it is used in preference."
-  :type 'string)
-
 
 
 ;; Variables
 
 (defvar javaimp-handler-regexp-alist
-  `(("\\`build.gradle" . ,#'javaimp--gradle-visit)
-    ("\\`pom.xml\\'" . ,#'javaimp--maven-visit))
+  `(("\\`build.gradle" . ,#'javaimp-gradle-visit)
+    ("\\`pom.xml\\'" . ,#'javaimp-maven-visit))
   "Alist of file name patterns vs corresponding handler function.
 A handler function takes one argument, a FILE.")
 
@@ -258,18 +234,6 @@ struct.")
 is expanded file name and CACHED-FILE is javaimp-cached-file
 struct.")
 
-(defvar javaimp-syntax-table
-  (make-syntax-table java-mode-syntax-table) ;TODO don't depend on cc-mode
-  "Javaimp syntax table")
-
-(defvar javaimp--arglist-syntax-table
-  (let ((st (make-syntax-table javaimp-syntax-table)))
-    (modify-syntax-entry ?< "(>" st)
-    (modify-syntax-entry ?> ")<" st)
-    (modify-syntax-entry ?. "_" st) ; separates parts of fully-qualified type
-    st)
-  "Enables parsing angle brackets as lists")
-
 (defconst javaimp--jar-error-header
   "There were errors when reading some of the dependency files,
 they are listed below.
@@ -286,6 +250,16 @@ 
https://docs.gradle.org/current/userguide/java_library_plugin.html\
 #sec:java_library_classes_usage
 ")
 
+(defconst javaimp-show-scopes-type-abbrevs
+  '((anon-class . "ac")
+    (statement . "st")
+    (simple-statement . "ss")
+    (array . "ar")
+    (method . "me")
+    (class . "cl")
+    (interface . "in")
+    (enum . "en")))
+
 
 ;;;###autoload
 (defun javaimp-visit-project (file)
@@ -423,15 +397,15 @@ output."
 (defun javaimp-find-module (predicate)
   "Returns first module in `javaimp-project-forest' for which
 PREDICATE returns non-nil."
-  (javaimp--find-node predicate javaimp-project-forest t))
+  (javaimp-tree-find-node predicate javaimp-project-forest t))
 
 (defun javaimp-collect-modules (predicate)
   "Returns all modules in `javaimp-project-forest' for which
 PREDICATE returns non-nil."
-  (javaimp--collect-nodes predicate javaimp-project-forest))
+  (javaimp-tree-collect-nodes predicate javaimp-project-forest))
 
 (defun javaimp-map-modules (function)
-  (javaimp--map-nodes function #'always javaimp-project-forest))
+  (javaimp-tree-map-nodes function #'always javaimp-project-forest))
 
 
 ;;; Adding imports
@@ -458,7 +432,7 @@ current module or source tree, see
   (interactive
    (let* ((file (expand-file-name (or buffer-file-name
                                      (error "Buffer is not visiting a 
file!"))))
-         (node (javaimp--find-node
+         (node (javaimp-tree-find-node
                 (lambda (m)
                    (seq-some (lambda (dir)
                                (string-prefix-p dir file))
@@ -544,7 +518,7 @@ If there's no such directive, then the last resort is just
      (if-let ((package (save-excursion
                          (save-restriction
                            (widen)
-                           (javaimp--parse-get-package)))))
+                           (javaimp-parse-get-package)))))
          (string-remove-suffix
           (mapconcat #'file-name-as-directory (split-string package "\\." t) 
nil)
           default-directory)
@@ -577,22 +551,22 @@ If there's no such directive, then the last resort is just
     (insert-file-contents file)
     ;; We need only class-likes, and this is temp buffer, so for
     ;; efficiency avoid parsing anything else
-    (let ((javaimp--parse-scope-hook #'javaimp--parse-scope-class))
+    (let ((javaimp-parse--scope-hook #'javaimp-parse--scope-class))
       (javaimp--get-buffer-classes))))
 
 (defun javaimp--get-buffer-classes ()
   "Return fully-qualified names of all class-like scopes in the
 current buffer.  Anonymous classes are not included."
-  (let ((package (javaimp--parse-get-package))
-        (scopes (javaimp--parse-get-all-scopes
-                 nil nil (javaimp--defun-scope-pred))))
+  (let ((package (javaimp-parse-get-package))
+        (scopes (javaimp-parse-get-all-scopes
+                 nil nil (javaimp-scope-defun-p))))
     (mapcar (lambda (class)
               (if package
                   (concat package "." class)
                 class))
             (mapcar (lambda (scope)
                       (let ((name (javaimp-scope-name scope))
-                            (parent-names (javaimp--concat-scope-parents 
scope)))
+                            (parent-names (javaimp-scope-concat-parents 
scope)))
                         (if (string-empty-p parent-names)
                             name
                           (concat parent-names "." name))))
@@ -620,15 +594,15 @@ are assigned a default order defined by
 
 Additionally, merge imports from ADD-ALIST, an alist of the same
 form as CLASS-ALIST in return value of
-`javaimp--parse-get-imports'."
+`javaimp-parse-get-imports'."
   (interactive)
   (barf-if-buffer-read-only)
   (save-excursion
     (save-restriction
       (widen)
-      (let ((parsed (javaimp--parse-get-imports)))
+      (let ((parsed (javaimp-parse-get-imports)))
         (when (or (cdr parsed) add-alist)
-          (javaimp--parse-without-hook
+          (javaimp-parse-without-hook
             (javaimp--position-for-insert-imports (car parsed))
             (let ((with-order
                   (mapcar
@@ -664,7 +638,7 @@ form as CLASS-ALIST in return value of
       (progn
         (delete-region (car old-region) (cdr old-region))
         (goto-char (car old-region)))
-    (if (javaimp--parse-get-package)
+    (if (javaimp-parse-get-package)
         (insert "\n\n")
       ;; As a last resort, go to bob and skip comments
       (goto-char (point-min))
@@ -689,13 +663,22 @@ form as CLASS-ALIST in return value of
 
 ;; Imenu support
 
+(defsubst javaimp-imenu--make-entry (scope)
+  (list (javaimp-scope-name scope)
+        (if imenu-use-markers
+            (copy-marker (javaimp-scope-start scope))
+          (javaimp-scope-start scope))
+        #'javaimp-imenu--function
+        scope))
+
+
 ;;;###autoload
 (defun javaimp-imenu-create-index ()
   "Function to use as `imenu-create-index-function', can be set
 in a major mode hook."
   (let ((forest (javaimp-imenu--get-forest)))
     (if javaimp-imenu-use-sub-alists
-        (javaimp--map-nodes
+        (javaimp-tree-map-nodes
          (lambda (scope)
            (if (eq (javaimp-scope-type scope) 'method)
                ;; entry
@@ -709,7 +692,7 @@ in a major mode hook."
       (let ((entries
              (mapcar #'javaimp-imenu--make-entry
                      (seq-sort-by #'javaimp-scope-start #'<
-                                  (javaimp--collect-nodes
+                                  (javaimp-tree-collect-nodes
                                    (lambda (scope)
                                      (eq (javaimp-scope-type scope) 'method))
                                    forest))))
@@ -724,14 +707,14 @@ in a major mode hook."
                   (setcar entry
                           (format "%s [%s]"
                                   (car entry)
-                                  (javaimp--concat-scope-parents
+                                  (javaimp-scope-concat-parents
                                    (nth 3 entry))))))
               entries)))))
 
 (defun javaimp-imenu--get-forest ()
   (let* ((defun-scopes
-          (javaimp--parse-get-all-scopes
-           nil nil (javaimp--defun-scope-pred '(method))))
+          (javaimp-parse-get-all-scopes
+           nil nil (javaimp-scope-defun-p '(method))))
          (methods (seq-filter
                    (lambda (scope)
                      (eq (javaimp-scope-type scope) 'method))
@@ -744,36 +727,29 @@ in a major mode hook."
                                     (null (javaimp-scope-parent s)))
                                   classes))
          (abstract-methods (append
-                            (javaimp--parse-get-class-abstract-methods)
-                            (javaimp--parse-get-interface-abstract-methods))))
+                            (javaimp-parse-get-class-abstract-methods)
+                            (javaimp-parse-get-interface-abstract-methods))))
     (mapcar
      (lambda (top-class)
        (message "Building tree for top-level class-like scope: %s"
                 (javaimp-scope-name top-class))
-       (javaimp--build-tree top-class
-                            (append methods
-                                    classes
-                                    abstract-methods)
-                            (lambda (el tested)
-                              (equal el (javaimp-scope-parent tested)))
-                            nil
-                            (lambda (s1 s2)
-                              (< (javaimp-scope-start s1)
-                                 (javaimp-scope-start s2)))))
+       (javaimp-tree-build top-class
+                           (append methods
+                                   classes
+                                   abstract-methods)
+                           (lambda (el tested)
+                             (equal el (javaimp-scope-parent tested)))
+                           nil
+                           (lambda (s1 s2)
+                             (< (javaimp-scope-start s1)
+                                (javaimp-scope-start s2)))))
      top-classes)))
 
-(defsubst javaimp-imenu--make-entry (scope)
-  (list (javaimp-scope-name scope)
-        (if imenu-use-markers
-            (copy-marker (javaimp-scope-start scope))
-          (javaimp-scope-start scope))
-        #'javaimp-imenu--function
-        scope))
-
 (defun javaimp-imenu--function (_index-name index-position _scope)
   (goto-char index-position)
   (back-to-indentation))
 
+
 
 ;; Show scopes
 
@@ -829,8 +805,8 @@ opening brace."
          (save-excursion
            (save-restriction
              (widen)
-             (javaimp--parse-get-all-scopes
-              nil nil (javaimp--defun-scope-pred '(method anon-class))))))
+             (javaimp-parse-get-all-scopes
+              nil nil (javaimp-scope-defun-p '(method anon-class))))))
         (source-buf (current-buffer))
         (source-default-dir default-directory)
         (buf (get-buffer-create "*javaimp-scopes*")))
@@ -853,7 +829,7 @@ opening brace."
                                (line-number-at-pos (javaimp-scope-start 
scope)))
                              depth
                              (cdr (assq (javaimp-scope-type scope)
-                                        
javaimp--show-scopes-scope-type-abbrevs))
+                                        javaimp-show-scopes-type-abbrevs))
                              (make-string (* 2 depth) ? )
                              (javaimp-scope-name scope))
                      'mouse-face 'highlight
@@ -928,8 +904,8 @@ after this group of defuns."
     (save-restriction
       (widen)
       (let* ((pos (point))
-             (defun-pred (javaimp--defun-scope-pred '(method anon-class)))
-             (enc (javaimp--parse-get-enclosing-scope defun-pred))
+             (defun-pred (javaimp-scope-defun-p '(method anon-class)))
+             (enc (javaimp-parse-get-enclosing-scope defun-pred))
              (parent
               (if (and enc (eq (javaimp-scope-type enc) 'method))
                   ;; We're inside a method, and need to look at
@@ -945,9 +921,9 @@ after this group of defuns."
                               (ignore-errors
                                 (scan-lists
                                  (javaimp-scope-open-brace parent) 1 0))))
-             (sibling-pred (javaimp--scope-same-parent-pred parent))
+             (sibling-pred (javaimp-scope-same-parent-p parent))
              (siblings
-              (javaimp--parse-get-all-scopes
+              (javaimp-parse-get-all-scopes
                ;; beg/end are not strictly needed, pred is enough, but
                ;; provide them for effectiveness
                parent-beg parent-end
diff --git a/testdata/gradle-multi.tar.gz b/tests/data/gradle-multi.tar.gz
similarity index 100%
rename from testdata/gradle-multi.tar.gz
rename to tests/data/gradle-multi.tar.gz
diff --git a/testdata/maven-multi.tar.gz b/tests/data/maven-multi.tar.gz
similarity index 100%
rename from testdata/maven-multi.tar.gz
rename to tests/data/maven-multi.tar.gz
diff --git a/testdata/maven-single.tar.gz b/tests/data/maven-single.tar.gz
similarity index 100%
rename from testdata/maven-single.tar.gz
rename to tests/data/maven-single.tar.gz
diff --git a/testdata/test1-misc-classes.java 
b/tests/data/test1-misc-classes.java
similarity index 100%
rename from testdata/test1-misc-classes.java
rename to tests/data/test1-misc-classes.java
diff --git a/javaimp-tests-gradle.el b/tests/gradle.el
similarity index 73%
rename from javaimp-tests-gradle.el
rename to tests/gradle.el
index 413c297939..f9e4e78e15 100644
--- a/javaimp-tests-gradle.el
+++ b/tests/gradle.el
@@ -1,4 +1,4 @@
-;;; javaimp-tests-gradle.el --- javaimp Gradle tests  -*- lexical-binding: t; 
-*-
+;;; tests/gradle.el --- javaimp Gradle tests  -*- lexical-binding: t; -*-
 
 ;; Copyright (C) 2021-2021  Free Software Foundation, Inc.
 
@@ -6,34 +6,16 @@
 ;; Maintainer: Filipp Gunbin <[email protected]>
 
 (require 'ert)
-(require 'javaimp)
+(require 'javaimp-gradle)
 (require 'javaimp-tests)
 
-;; Tests for Gradle project parsing.
-;;
 ;; Note that you have to set up `javaimp-gradle-program' to point to
 ;; Gradle installation, which is rather untypical for Gradle projects
 ;; (most often, a "Gradle wrapper" is used, resulting in project-local
 ;; copy of Gradle).
 
-(ert-deftest javaimp-test--gradle-visit-multi ()
-  :tags '(:runs-build-tool)
-  (javaimp-test--with-data
-   "gradle-multi.tar.gz"
-   (lambda (tmpdir)
-     (should
-      (equal
-       (javaimp-test--gradle-get-tree
-        (concat tmpdir (file-name-as-directory "multi")))
-       '((("<root>:org.example:1.0" . "build.gradle")
-          (("child:org.example:1.0" . "child/build.gradle")
-           (("child.grandchild:org.example:1.0" . 
"child/grandchild/build.gradle")))
-          ;; directory layout different from project layout
-          (("non-direct-child:org.example:1.0" . 
"foo/non-direct-child/build.gradle"))
-          )))))))
-
-(defun javaimp-test--gradle-get-tree (project-dir)
-  (javaimp--map-nodes
+(defun javaimp-test-gradle--get-tree (project-dir)
+  (javaimp-tree-map-nodes
    (lambda (mod)
      (cons t
            (cons (javaimp-print-id (javaimp-module-id mod))
@@ -44,6 +26,22 @@
                   ;; /var/ on macOS), so do it too
                   (file-truename project-dir)))))
    #'always
-   (javaimp--gradle-visit (concat project-dir "build.gradle"))))
+   (javaimp-gradle-visit (concat project-dir "build.gradle"))))
 
-(provide 'javaimp-tests-gradle)
+
+(ert-deftest javaimp-gradle-visit-multi ()
+  :tags '(:expensive)
+  (javaimp-call-with-data
+   (file-name-concat
+    javaimp-basedir "tests" "data" "gradle-multi.tar.gz")
+   (lambda (tmpdir)
+     (should
+      (equal
+       (javaimp-test-gradle--get-tree
+        (concat tmpdir (file-name-as-directory "multi")))
+       '((("<root>:org.example:1.0" . "build.gradle")
+          (("child:org.example:1.0" . "child/build.gradle")
+           (("child.grandchild:org.example:1.0" . 
"child/grandchild/build.gradle")))
+          ;; directory layout different from project layout
+          (("non-direct-child:org.example:1.0" . 
"foo/non-direct-child/build.gradle"))
+          )))))))
diff --git a/tests/imenu.el b/tests/imenu.el
new file mode 100644
index 0000000000..39c186f55a
--- /dev/null
+++ b/tests/imenu.el
@@ -0,0 +1,85 @@
+;;; tests/imenu.el --- javaimp Imenu tests  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022-2022  Free Software Foundation, Inc.
+
+;; Author: Filipp Gunbin <[email protected]>
+;; Maintainer: Filipp Gunbin <[email protected]>
+
+(require 'ert)
+(require 'javaimp)
+
+(defun javaimp-test-imenu--simplify-entries (alist)
+  (dolist (elt alist)
+    (if (and (= (length elt) 4)
+             (functionp (nth 2 elt)))
+        (setcdr elt (nth 1 elt))
+      (javaimp-test-imenu--simplify-entries (cdr elt)))))
+
+
+(ert-deftest javaimp-imenu-create-index ()
+  (let ((actual (with-temp-buffer
+                  (insert-file-contents
+                   (file-name-concat
+                    javaimp-basedir "tests" "data" "test1-misc-classes.java"))
+                  (let ((imenu-use-markers nil))
+                    (javaimp-imenu-create-index))))
+        (expected-names
+         '("foo() [Top.CInner1]"
+           "foo() [Top.CInner1.CInner1_CInner1]"
+           "abstract_method() [Top.CInner1.CInner1_CInner1]"
+           "bar()"
+           "baz() [Top.CInner1.CInner1_CInner1]"
+           "foo() [Top.IInner1]"
+           "abstract_method() [Top.IInner1]"
+           "foo() [Top.IInner1.IInner1_CInner1]"
+           "baz() [Top.IInner1]"
+           "defaultMethod(String) [Top.IInner1]"
+           "foo() [Top.IInner1.IInner1_IInner1]"
+           "defaultMethod(String) [Top.IInner1.IInner1_IInner1]"
+           "baz() [Top.IInner1.IInner1_IInner1]"
+           "EnumInner1()"
+           "foo() [Top.EnumInner1]"
+           "foo() [ColocatedTop]"
+           "bar(String,String)")))
+    (should (= (length expected-names) (length actual)))
+    (dotimes (i (length expected-names))
+      (should (equal (nth i expected-names) (car (nth i actual)))))))
+
+(ert-deftest javaimp-imenu-create-index-use-sub-alists ()
+  (let ((actual (with-temp-buffer
+                  (insert-file-contents
+                   (file-name-concat
+                    javaimp-basedir "tests" "data" "test1-misc-classes.java"))
+                  (let ((imenu-use-markers nil)
+                        (javaimp-imenu-use-sub-alists t))
+                    (javaimp-imenu-create-index))))
+        (expected
+         '(("Top"
+            ("CInner1"
+             ("foo()" . 98)
+             ("CInner1_CInner1"
+              ("foo()" . 1099)
+              ("abstract_method()" . 1148)
+              ("bar()" . 1192)
+              ("baz()" . 1281)))
+            ("IInner1"
+             ("foo()" . 1603)
+             ("abstract_method()" . 1715)
+             ("IInner1_CInner1"
+              ("foo()" . 1798))
+             ("baz()" . 1934)
+             ("defaultMethod(String)" . 1963)
+             ("IInner1_IInner1"
+              ("foo()" . 2122)
+              ("defaultMethod(String)" . 2157)
+              ("baz()" . 2258)))
+            ("EnumInner1"
+             ("EnumInner1()" . 2353)
+             ("foo()" . 2399)
+             ;; "EnumInner1_EInner1" omitted because no methods inside
+             ))
+           ("ColocatedTop"
+            ("foo()" . 2554)
+            ("bar(String,String)" . 2578)))))
+    (javaimp-test-imenu--simplify-entries actual)
+    (should (equal expected actual))))
diff --git a/javaimp-tests-maven.el b/tests/maven.el
similarity index 77%
rename from javaimp-tests-maven.el
rename to tests/maven.el
index 4dad154ea2..ee83f63f88 100644
--- a/javaimp-tests-maven.el
+++ b/tests/maven.el
@@ -1,4 +1,4 @@
-;;; javaimp-tests-maven.el --- javaimp Maven tests  -*- lexical-binding: t; -*-
+;;; tests/maven.el --- javaimp Maven tests  -*- lexical-binding: t; -*-
 
 ;; Copyright (C) 2021-2021  Free Software Foundation, Inc.
 
@@ -6,12 +6,10 @@
 ;; Maintainer: Filipp Gunbin <[email protected]>
 
 (require 'ert)
-(require 'javaimp)
+(require 'javaimp-maven)
 (require 'javaimp-tests)
 
-;; Tests for Maven project parsing.
-
-;; "testdata" dir contains some archived Maven projects.  If you need
+;; "data" dir contains some archived Maven projects.  If you need
 ;; to run Maven on them manually, use this in the untarred directory:
 ;;
 ;; `mvn -U -s settings.xml -f <name>/pom.xml help:effective-pom'.
@@ -21,25 +19,37 @@
 ;; `<?xml version="1.0" encoding="UTF-8"?><settings/>'
 ;;
 
-(ert-deftest javaimp-test--maven-visit-single ()
-  :tags '(:runs-build-tool)
-  (javaimp-test--with-data
-   "maven-single.tar.gz"
+(defun javaimp-test-maven--get-tree (project-dir)
+  (javaimp-tree-map-nodes
+   (lambda (mod)
+     (cons t
+           (cons (javaimp-print-id (javaimp-module-id mod))
+                 (file-relative-name (javaimp-module-file mod) project-dir))))
+   #'always
+   (javaimp-maven-visit (concat project-dir "pom.xml"))))
+
+
+(ert-deftest javaimp-maven-visit-single ()
+  :tags '(:expensive)
+  (javaimp-call-with-data
+   (file-name-concat
+    javaimp-basedir "tests" "data" "maven-single.tar.gz")
    (lambda (tmpdir)
      (should
       (equal
-       (javaimp-test--maven-get-tree
+       (javaimp-test-maven--get-tree
         (concat tmpdir (file-name-as-directory "single")))
        '((("single:org.example:1.0.0" . "pom.xml"))))))))
 
-(ert-deftest javaimp-test--maven-visit-multi ()
-  :tags '(:runs-build-tool)
-  (javaimp-test--with-data
-   "maven-multi.tar.gz"
+(ert-deftest javaimp-maven-visit-multi ()
+  :tags '(:expensive)
+  (javaimp-call-with-data
+   (file-name-concat
+    javaimp-basedir "tests" "data" "maven-multi.tar.gz")
    (lambda (tmpdir)
      (should
       (equal
-       (javaimp-test--maven-get-tree
+       (javaimp-test-maven--get-tree
         (concat tmpdir (file-name-as-directory "multi")))
        '(;; Main tree:
          (("multi:org.example:1.0.0" . "pom.xml")
@@ -69,14 +79,3 @@
          ;; And "dangling-parent-link" project is not present at all,
          ;; because we have no way of knowing about it
          ))))))
-
-(defun javaimp-test--maven-get-tree (project-dir)
-  (javaimp--map-nodes
-   (lambda (mod)
-     (cons t
-           (cons (javaimp-print-id (javaimp-module-id mod))
-                 (file-relative-name (javaimp-module-file mod) project-dir))))
-   #'always
-   (javaimp--maven-visit (concat project-dir "pom.xml"))))
-
-(provide 'javaimp-tests-maven)
diff --git a/javaimp-tests.el b/tests/parse.el
similarity index 58%
copy from javaimp-tests.el
copy to tests/parse.el
index 42b5daa069..4cb16a47c2 100644
--- a/javaimp-tests.el
+++ b/tests/parse.el
@@ -1,21 +1,17 @@
-;;; javaimp-tests.el --- javaimp tests  -*- lexical-binding: t; -*-
+;;; tests/parse.el --- javaimp parsing tests  -*- lexical-binding: t; -*-
 
-;; Copyright (C) 2016-2021  Free Software Foundation, Inc.
+;; Copyright (C) 2022-2022  Free Software Foundation, Inc.
 
 ;; Author: Filipp Gunbin <[email protected]>
 ;; Maintainer: Filipp Gunbin <[email protected]>
 
 (require 'ert)
-(require 'javaimp)
+(require 'javaimp-parse)
+(require 'javaimp-util)
 
-;; Use this selector to run all tests which do not invoke build tool:
-;;
-;; (and "^javaimp-" (not (tag :runs-build-tool)))
-;;
+;; Tests for parse helpers
 
-;; Tests for low-level helpers of scope parsers.
-
-(ert-deftest javaimp-test--parse-arglist ()
+(ert-deftest javaimp-parse-arglist ()
   (dolist (data '(("")
                   ("  ")
                   ("int i"
@@ -40,10 +36,10 @@
                   ))
     (with-temp-buffer
       (insert (car data))
-      (should (equal (javaimp--parse-arglist (point-min) (point-max))
+      (should (equal (javaimp-parse--arglist (point-min) (point-max))
                      (cdr data))))))
 
-(ert-deftest javaimp-test--parse-arglist-throws ()
+(ert-deftest javaimp-parse-arglist-throws ()
   (dolist (data '(("")
                   ("  ")
                   ("Exception1"
@@ -62,16 +58,30 @@ Exception4<? super Exception5>>")
                    ("Exception7<Exception8>"))))
     (with-temp-buffer
       (insert (car data))
-      (should (equal (javaimp--parse-arglist (point-min) (point-max) t)
+      (should (equal (javaimp-parse--arglist (point-min) (point-max) t)
                      (cdr data))))))
 
 
 
-;; Tests for single scope parsers, which should be in
-;; `javaimp--parse-scope-hook'.
+;; Tests for scope parsers
+
+(defun javaimp-test-parse--scope (parser &rest test-items)
+  (declare (indent 1))
+  (dolist (item test-items)
+    (with-temp-buffer
+      (insert (nth 0 item))
+      (let* ((javaimp-parse--scope-hook
+              (lambda (arg)
+                (save-excursion
+                  (funcall parser arg))))
+             (scopes (javaimp-parse-get-all-scopes)))
+        (should (= 1 (length scopes)))
+        (should (eq (javaimp-scope-type (car scopes)) (nth 1 item)))
+        (should (equal (javaimp-scope-name (car scopes)) (nth 2 item)))))))
+
 
-(ert-deftest javaimp-test--parse-scope-class ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-class
+(ert-deftest javaimp-parse-scope-class ()
+  (javaimp-test-parse--scope #'javaimp-parse--scope-class
     '("class Foo {"
       class "Foo")
     '("class Foo extends Bar {"
@@ -102,8 +112,8 @@ extends Bar<? extends Baz<? extends Baz2>> {"
       enum "Foo")
     ))
 
-(ert-deftest javaimp-test--parse-scope-anon-class ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-anon-class
+(ert-deftest javaimp-parse-scope-anon-class ()
+  (javaimp-test-parse--scope #'javaimp-parse--scope-anon-class
     '(" = new Object < Class1 , Class2 > ( 1 + 1 , baz ) {"
       anon-class "<anon>Object")
     `(,(subst-char-in-string
@@ -116,8 +126,8 @@ extends Bar<? extends Baz<? extends Baz2>> {"
       anon-class "<anon>Object")
     ))
 
-(ert-deftest javaimp-test--parse-scope-method-or-stmt ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-method-or-stmt
+(ert-deftest javaimp-parse-scope-method-or-stmt ()
+  (javaimp-test-parse--scope #'javaimp-parse--scope-method-or-stmt
     '("static void foo_bar ( String a , int b ) {"
       method "foo_bar(String,int)")
     `(,(subst-char-in-string
@@ -133,8 +143,8 @@ throws E1 {"
       statement "if")
     ))
 
-(ert-deftest javaimp-test--parse-scope-simple-stmt ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-simple-stmt
+(ert-deftest javaimp-parse-scope-simple-stmt ()
+  (javaimp-test-parse--scope #'javaimp-parse--scope-simple-stmt
     '(" try {"
       simple-statement "try")
     `(,(subst-char-in-string ?  ?\n " try {")
@@ -149,8 +159,8 @@ throws E1 {"
       simple-statement "lambda")
     ))
 
-(ert-deftest javaimp-test--parse-scope-array ()
-  (javaimp-test--single-parser #'javaimp--parse-scope-array
+(ert-deftest javaimp-parse-scope-array ()
+  (javaimp-test-parse--scope #'javaimp-parse--scope-array
     '("new String[] {"
       array "")
     ;; TODO fix
@@ -160,79 +170,12 @@ throws E1 {"
     ;;   array "")
     ))
 
-(defun javaimp-test--single-parser (parser &rest test-items)
-  (declare (indent 1))
-  (dolist (item test-items)
-    (with-temp-buffer
-      (insert (nth 0 item))
-      (let* ((javaimp--parse-scope-hook
-              (lambda (arg)
-                (save-excursion
-                  (funcall parser arg))))
-             (scopes (javaimp--parse-get-all-scopes)))
-        (should (= 1 (length scopes)))
-        (should (eq (javaimp-scope-type (car scopes)) (nth 1 item)))
-        (should (equal (javaimp-scope-name (car scopes)) (nth 2 item)))))))
 
 
-;; Tests for parse "api"
-
-(ert-deftest javaimp-test--parse-get-package ()
-  (with-temp-buffer
-    (insert "
-  package  foo.bar.baz  ;
-//package commented.line;
-/*
-package commented.block;
-*/
-")
-    (should (equal (javaimp--parse-get-package) "foo.bar.baz"))))
-
-(ert-deftest javaimp-test--parse-get-imports ()
-  (with-temp-buffer
-    (insert "
-  import  some.class1  ;
-import  static  some.class.fun1;
-//import commented.line;
-/*
-import static commented.block;
-*/
-import someclass2;
-import my.package.* ;
-
-import static some_class.fun_2; // comment
-// comment outside
-")
-    (should (equal
-             (javaimp--parse-get-imports)
-             '((2 . 206)
-               ("some.class1" . normal)
-               ("some.class.fun1" . static)
-               ("someclass2" . normal)
-               ("my.package.*" . normal)
-               ("some_class.fun_2" . static))))))
+;; Tests for parse api
 
-(ert-deftest javaimp-test--parse-get-all-scopes ()
-  (with-temp-buffer
-    (insert-file-contents
-     (concat javaimp--basedir "testdata/test1-misc-classes.java"))
-    (should-not javaimp--parse-dirty-pos)
-    ;;
-    ;; parse full buffer
-    (javaimp-test--check-named-scopes)
-    (should javaimp--parse-dirty-pos)
-    (should-not (marker-position javaimp--parse-dirty-pos))
-    ;;
-    ;; reparse half of the buffer
-    (set-marker javaimp--parse-dirty-pos (/ (- (point-max) (point-min)) 2))
-    (javaimp-test--check-named-scopes)
-    (should-not (marker-position javaimp--parse-dirty-pos))
-    ;;
-    ;; don't reparse
-    (javaimp-test--check-named-scopes)))
-
-(defun javaimp-test--check-named-scopes ()
-  (let* ((scopes (javaimp--parse-get-all-scopes
+(defun javaimp-test-parse--check-defuns ()
+  (let* ((scopes (javaimp-parse-get-all-scopes
                   nil nil
                   (lambda (s)
                     (memq (javaimp-scope-type s) '(class interface enum 
method)))
@@ -318,118 +261,56 @@ import static some_class.fun_2; // comment
           (should (equal (nth 3 elt) (javaimp-scope-open-brace scope))))))))
 
 
-
-;; Tests for javaimp--get-buffer-classes
-
-(ert-deftest javaimp-test--get-buffer-classes ()
+(ert-deftest javaimp-parse-get-package ()
   (with-temp-buffer
-    (insert-file-contents
-     (concat javaimp--basedir "testdata/test1-misc-classes.java"))
-    (should (equal (javaimp--get-buffer-classes)
-                   '("org.foo.Top"
-                     "org.foo.Top.CInner1"
-                     "org.foo.Top.CInner1.CInner1_CInner1"
-                     "org.foo.Top.IInner1"
-                     "org.foo.Top.IInner1.IInner1_CInner1"
-                     "org.foo.Top.IInner1.IInner1_IInner1"
-                     "org.foo.Top.EnumInner1"
-                     "org.foo.Top.EnumInner1.EnumInner1_EInner1"
-                     "org.foo.ColocatedTop")))))
-
-
-;; Tests for imenu function
-
-(ert-deftest javaimp-test--imenu ()
-  (let ((actual (with-temp-buffer
-                  (insert-file-contents
-                   (concat javaimp--basedir 
"testdata/test1-misc-classes.java"))
-                  (let ((imenu-use-markers nil))
-                    (javaimp-imenu-create-index))))
-        (expected-names
-         '("foo() [Top.CInner1]"
-           "foo() [Top.CInner1.CInner1_CInner1]"
-           "abstract_method() [Top.CInner1.CInner1_CInner1]"
-           "bar()"
-           "baz() [Top.CInner1.CInner1_CInner1]"
-           "foo() [Top.IInner1]"
-           "abstract_method() [Top.IInner1]"
-           "foo() [Top.IInner1.IInner1_CInner1]"
-           "baz() [Top.IInner1]"
-           "defaultMethod(String) [Top.IInner1]"
-           "foo() [Top.IInner1.IInner1_IInner1]"
-           "defaultMethod(String) [Top.IInner1.IInner1_IInner1]"
-           "baz() [Top.IInner1.IInner1_IInner1]"
-           "EnumInner1()"
-           "foo() [Top.EnumInner1]"
-           "foo() [ColocatedTop]"
-           "bar(String,String)")))
-    (should (= (length expected-names) (length actual)))
-    (dotimes (i (length expected-names))
-      (should (equal (nth i expected-names) (car (nth i actual)))))))
-
-(ert-deftest javaimp-test--imenu-use-sub-alists ()
-  (let ((actual (with-temp-buffer
-                  (insert-file-contents
-                   (concat javaimp--basedir 
"testdata/test1-misc-classes.java"))
-                  (let ((imenu-use-markers nil)
-                        (javaimp-imenu-use-sub-alists t))
-                    (javaimp-imenu-create-index))))
-        (expected
-         '(("Top"
-            ("CInner1"
-             ("foo()" . 98)
-             ("CInner1_CInner1"
-              ("foo()" . 1099)
-              ("abstract_method()" . 1148)
-              ("bar()" . 1192)
-              ("baz()" . 1281)))
-            ("IInner1"
-             ("foo()" . 1603)
-             ("abstract_method()" . 1715)
-             ("IInner1_CInner1"
-              ("foo()" . 1798))
-             ("baz()" . 1934)
-             ("defaultMethod(String)" . 1963)
-             ("IInner1_IInner1"
-              ("foo()" . 2122)
-              ("defaultMethod(String)" . 2157)
-              ("baz()" . 2258)))
-            ("EnumInner1"
-             ("EnumInner1()" . 2353)
-             ("foo()" . 2399)
-             ;; "EnumInner1_EInner1" omitted because no methods inside
-             ))
-           ("ColocatedTop"
-            ("foo()" . 2554)
-            ("bar(String,String)" . 2578)))))
-    (javaimp-test--imenu-simplify-entries actual)
-    (should (equal expected actual))))
-
-(defun javaimp-test--imenu-simplify-entries (alist)
-  (dolist (elt alist)
-    (if (and (= (length elt) 4)
-             (functionp (nth 2 elt)))
-        (setcdr elt (nth 1 elt))
-      (javaimp-test--imenu-simplify-entries (cdr elt)))))
+    (insert "
+  package  foo.bar.baz  ;
+//package commented.line;
+/*
+package commented.block;
+*/
+")
+    (should (equal (javaimp-parse-get-package) "foo.bar.baz"))))
 
-
-;; Utility
+(ert-deftest javaimp-parse-get-imports ()
+  (with-temp-buffer
+    (insert "
+  import  some.class1  ;
+import  static  some.class.fun1;
+//import commented.line;
+/*
+import static commented.block;
+*/
+import someclass2;
+import my.package.* ;
 
-(defun javaimp-test--with-data (filename handler)
-  "Untars testdata/FILENAME into temporary directory and runs
-HANDLER, supplying temp directory name as the only arg."
-  (let ((tmpdir (file-name-as-directory (make-temp-file "javaimp" t))))
-    (unwind-protect
-        (let ((rc (call-process
-                   "tar" nil nil nil
-                   "-x"
-                   "-f" (concat javaimp--basedir
-                                (file-name-as-directory "testdata")
-                                filename)
-                   "-C" tmpdir)))
-          (unless (= rc 0)
-            (error "Cannot untar test data %s: %d" filename rc))
-          (funcall handler tmpdir))
-      (delete-directory tmpdir t))))
+import static some_class.fun_2; // comment
+// comment outside
+")
+    (should (equal
+             (javaimp-parse-get-imports)
+             '((2 . 206)
+               ("some.class1" . normal)
+               ("some.class.fun1" . static)
+               ("someclass2" . normal)
+               ("my.package.*" . normal)
+               ("some_class.fun_2" . static))))))
 
-(provide 'javaimp-tests)
+(ert-deftest javaimp-parse-get-all-scopes ()
+  (with-temp-buffer
+    (insert-file-contents
+     (file-name-concat javaimp-basedir "tests" "data" 
"test1-misc-classes.java"))
+    (should-not javaimp-parse--dirty-pos)
+    ;;
+    ;; parse full buffer
+    (javaimp-test-parse--check-defuns)
+    (should javaimp-parse--dirty-pos)
+    (should-not (marker-position javaimp-parse--dirty-pos))
+    ;;
+    ;; reparse half of the buffer
+    (set-marker javaimp-parse--dirty-pos (/ (- (point-max) (point-min)) 2))
+    (javaimp-test-parse--check-defuns)
+    (should-not (marker-position javaimp-parse--dirty-pos))
+    ;;
+    ;; don't reparse
+    (javaimp-test-parse--check-defuns)))
diff --git a/tests/tests.el b/tests/tests.el
new file mode 100644
index 0000000000..feab94b36a
--- /dev/null
+++ b/tests/tests.el
@@ -0,0 +1,24 @@
+;;; tests/tests.el --- javaimp tests  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022-2022  Free Software Foundation, Inc.
+
+;; Author: Filipp Gunbin <[email protected]>
+;; Maintainer: Filipp Gunbin <[email protected]>
+
+(require 'ert)
+(require 'javaimp)
+
+(ert-deftest javaimp-get-buffer-classes ()
+  (with-temp-buffer
+    (insert-file-contents
+     (file-name-concat javaimp-basedir "tests" "data" 
"test1-misc-classes.java"))
+    (should (equal (javaimp--get-buffer-classes)
+                   '("org.foo.Top"
+                     "org.foo.Top.CInner1"
+                     "org.foo.Top.CInner1.CInner1_CInner1"
+                     "org.foo.Top.IInner1"
+                     "org.foo.Top.IInner1.IInner1_CInner1"
+                     "org.foo.Top.IInner1.IInner1_IInner1"
+                     "org.foo.Top.EnumInner1"
+                     "org.foo.Top.EnumInner1.EnumInner1_EInner1"
+                     "org.foo.ColocatedTop")))))

Reply via email to