branch: externals/javaimp commit 9c834db1a69168f25498c0f2c3fe49ecc71f29d9 Author: Filipp Gunbin <fgun...@fastmail.fm> Commit: Filipp Gunbin <fgun...@fastmail.fm>
javaimp-parse.el: Add parsing of methods and statements --- javaimp-parse.el | 268 ++++++++++++++++++++++++++++++++++++++++++++++++------- javaimp-tests.el | 46 ++++++++++ 2 files changed, 283 insertions(+), 31 deletions(-) diff --git a/javaimp-parse.el b/javaimp-parse.el index 7c29794..e603b4b 100644 --- a/javaimp-parse.el +++ b/javaimp-parse.el @@ -20,39 +20,161 @@ (require 'cl-lib) (require 'seq) +(require 'cc-mode) ;for java-mode-syntax-table + +(defcustom javaimp-parse-format-method-name + #'javaimp--parse-format-method-name-full + "Function to format method name, invoked with 3 arguments: +NAME, ARGS and THROWS-ARGS. The last two are lists with elements +of the form (TYPE . NAME). For THROWS-ARGS, only TYPE is +present." + :group 'javaimp + :type 'function) (cl-defstruct javaimp-scope - type ; one of anonymous-class, class, interface, enum, local-class, unknown + type ; one of anonymous-class, class, interface, enum, local-class, + ; method, statement, unknown name start open-brace) -(defsubst javaimp--parse-is-class (scope) - (memq (javaimp-scope-type scope) '(class interface enum))) +(defconst javaimp--parse-class-keywords + '("class" "interface" "enum")) +(defconst javaimp--parse-stmt-keywords + '("if" "for" "while" "switch" "try" "catch")) -(defconst javaimp--parse-class-re +(defun javaimp--parse-block-re (keywords) (concat - (regexp-opt '("class" "interface" "enum") 'words) + (regexp-opt keywords 'words) (rx (and (+ (syntax whitespace)) (group (+ (any alnum ?_))))))) +(defconst javaimp--parse-class-re + (javaimp--parse-block-re javaimp--parse-class-keywords)) +(defconst javaimp--parse-stmt-re + (javaimp--parse-block-re javaimp--parse-stmt-keywords)) -(defun javaimp--parse-get-package () +(defsubst javaimp--parse-is-class (scope) + (member (symbol-name (javaimp-scope-type scope)) javaimp--parse-class-keywords)) + +(defvar javaimp--arglist-syntax-table + (let ((st (make-syntax-table java-mode-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") + + + +;;; Arglist + +(defun javaimp--parse-arglist (beg end &optional only-type) (save-excursion (save-restriction - (widen) - (goto-char (point-min)) - (catch 'found - (while (re-search-forward "^\\s *package\\s +\\([^;]+\\)\\s *;" nil t) - (let ((state (syntax-ppss))) - (unless (syntax-ppss-context state) - (throw 'found (match-string 1))))))))) + (narrow-to-region beg end) + (with-syntax-table javaimp--arglist-syntax-table ;skip generics like lists + (goto-char (point-max)) + (ignore-errors + (let (res) + (while (not (bobp)) + (push (javaimp--parse-arglist-one-arg only-type) res) + ;; move back to the previous argument, if any + (when (javaimp--parse-arglist-until (lambda () + (and (not (bobp)) + (= (char-before) ?,)))) + (backward-char))) + res)))))) + +(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 + (skip-syntax-backward "-") + (point))) + name) + ;; Parse name + (unless only-type + (if (= 0 (skip-syntax-backward "w_")) + (error "Cannot parse argument name")) + (setq name (buffer-substring-no-properties (point) limit)) + (skip-syntax-backward "-") + (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 (javaimp--parse-arglist-until (lambda () + (save-excursion + (skip-syntax-forward "-" limit) + (looking-at "\\_<")))) + (progn + (skip-syntax-forward "-") ;undo skipping by ..-until + (let ((type (replace-regexp-in-string + "[[:space:]\n]+" " " + (buffer-substring-no-properties (point) limit)))) + (cons type name))) + (error "Cannot parse argument type")))) + +(defun javaimp--parse-arglist-until (stop-p) + ;; Goes backwards until position at which STOP-P returns non-nil. + ;; Returns non-nil if stopped on this condition, nil if reached bob. + ;; STOP-P is invoked (possibly at the bob) without arguments and + ;; should not move point. Backward movement also skips any + ;; whitespace, so STOP-P looking forward should be prepared to + ;; see leading whitespace. + (catch 'done + (while t + (skip-syntax-backward "-") + (let ((state (syntax-ppss))) + (cond ((syntax-ppss-context state) + ;; move out of comment/string if in one + (goto-char (nth 8 state))) + ((funcall stop-p) + (throw 'done t)) + ((bobp) + (throw 'done nil)) + ((= (char-syntax (char-before)) ?\)) + (backward-list)) + (t + (backward-char))))))) + + + +;;; Formatting +(defsubst javaimp--parse-format-method-name-full (name args throws-args) + "Outputs NAME, ARGS (name and type) and THROWS-ARGS (only type)." + (concat name + "(" + (mapconcat (lambda (arg) + (concat (car arg) " " (cdr arg))) + args + ", ") + ")" + (if throws-args + (concat "throws " + (mapconcat #'car throws-args ", "))) + )) + +(defsubst javaimp--parse-format-method-name-types (name args throws-args) + "Outputs NAME and ARGS (only type)." + (concat name + "(" + (mapconcat #'car args ", ") + ")" + )) + + +;;; Scopes (defvar javaimp--parse-scope-hook - '(javaimp--parse-scope-class + '(;; should be before method/stmt because looks similar, but with + ;; "new " in front javaimp--parse-scope-anonymous-class - javaimp--parse-scope-unknown ;fallback + javaimp--parse-scope-method-or-stmt + javaimp--parse-scope-class + javaimp--parse-scope-unknown ; catch-all )) (defun javaimp--parse-scope-class (state) @@ -69,24 +191,93 @@ (defun javaimp--parse-scope-anonymous-class (state) (save-excursion - (let (end) - (if (and (re-search-backward "\\<new\\s +" nil t) - ;; skip arg list and ws - (setq end (save-excursion - (ignore-errors - (goto-char - (scan-lists (nth 1 state) -1 0)) - (skip-syntax-backward "-") - (point)))) - (not (save-match-data - (search-forward "(" end t)))) + ;; skip arg-list and ws + (when (and (progn + (skip-syntax-backward "-") + (= (char-before) ?\))) + (ignore-errors + (goto-char + (scan-lists (point) -1 0)))) + (skip-syntax-backward "-") + (let ((end (point))) + (when (and (re-search-backward "\\<new\\s-+" nil t) + (not (save-match-data + (search-forward "(" end t)))) (make-javaimp-scope :type 'anonymous-class - :name (concat - "Anon_" - (buffer-substring-no-properties - (match-end 0) end)) + :name (concat "Anon_" + (buffer-substring-no-properties + (match-end 0) end)) :start (match-beginning 0) - :open-brace (nth 1 state)))))) + :open-brace (nth 1 state))))))) + +(defun javaimp--parse-scope-method-or-stmt (state) + (save-excursion + (let ((throws-args (javaimp--parse-scope-method-throws state))) + (when (and (not (eq throws-args t)) + (progn + (skip-syntax-backward "-") + (= (char-before) ?\))) + (ignore-errors + (goto-char + (scan-lists (point) -1 0)))) + (let* (;; leave open/close parens out + (arglist-region (cons (1+ (point)) + (1- (scan-lists (point) 1 0)))) + (count (progn + (skip-syntax-backward "-") + (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) + (not throws-args)) + 'statement 'method)))) + (when type + (make-javaimp-scope + :type type + :name (if (eq type 'statement) + name + (funcall javaimp-parse-format-method-name + name + (javaimp--parse-arglist (car arglist-region) + (cdr arglist-region)) + throws-args)) + :start (point) + :open-brace (nth 1 state)))))))) + +(defun javaimp--parse-scope-method-throws (state) + "Subroutine of `javaimp--parse-scope-method-or-stmt'. Attempts +to parse throws clause backwards, returning THROWS-ARGS in the +same format as `javaimp--parse-arglist' (but here, only TYPE +element component will be present). Point is left at the +position from where method signature parsing may be continued. +Returns t if parsing failed and should not be continued." + (let ((pos (point)) + res) + (when (re-search-backward "\\<throws\\s-+" nil t) + (setq res (save-match-data + (javaimp--parse-arglist + (match-end 0) + (save-excursion + (goto-char pos) + (skip-syntax-backward "-") + (point)) + t))) + (if res + (goto-char (match-beginning 0)) + ;; If throws-args did not parse, then it could either 1) be + ;; malformed, 2) just belong to some other method. In case 2 + ;; we want to return to where we started and continue without + ;; throws. + (if (ignore-errors + ;; Is the next list construct the same which we're + ;; analyzing? If yes, then assume throws is malformed. + (/= (scan-lists (match-end 0) 1 -1) + (1+ (nth 1 state)))) + (goto-char pos) + (setq res t)))) + res)) (defun javaimp--parse-scope-unknown (state) (make-javaimp-scope :type 'unknown @@ -123,6 +314,21 @@ (setq tmp (cdr tmp)))) res)) + + +;; Main + +(defun javaimp--parse-get-package () + (save-excursion + (save-restriction + (widen) + (goto-char (point-min)) + (catch 'found + (while (re-search-forward "^\\s *package\\s +\\([^;]+\\)\\s *;" nil t) + (let ((state (syntax-ppss))) + (unless (syntax-ppss-context state) + (throw 'found (match-string 1))))))))) + (defun javaimp--parse-get-file-classes (file) (with-temp-buffer (insert-file-contents file) diff --git a/javaimp-tests.el b/javaimp-tests.el index 9951a81..497514c 100644 --- a/javaimp-tests.el +++ b/javaimp-tests.el @@ -23,6 +23,52 @@ (should (eql (length projects) 2))))) +(ert-deftest javaimp-test--parse-arglist () + (dolist (data '(("") + (" ") + ("int i" + ("int" . "i")) + (" 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, +@org.foo.Annotation_2(value_1 = \"value1 , (){}\", value2 = -2.3) String[][] arr" + ("int" . "i") + ("String[][]" . "arr")) + )) + (with-temp-buffer + (java-mode) + (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")) + (" 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 + (java-mode) + (insert (car data)) + (should (equal (javaimp--parse-arglist (point-min) (point-max) t) + (cdr data)))))) + (ert-deftest javaimp-test--parse-get-package () (with-temp-buffer (insert "//package org.commented1;