8000 Harden Go tool integration edge cases by dajiaohuang · Pull Request #451 · dominikh/go-mode.el · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions go-guru.el
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,23 @@ Parse and return the resulting JSON object."
(with-current-buffer json-buffer
(unless (zerop exitcode)
;; Failed: use buffer contents (sans final \n) as an error.
(error "%s" (buffer-substring (point-min) (1- (point-max)))))
(error "%s" (go-guru--error-output json-buffer)))
;; Success: parse JSON.
(goto-char (point-min))
(json-read))))
;; Clean up temporary buffers.
(kill-buffer json-buffer)
(kill-buffer input-buffer))))

(defun go-guru--error-output (buffer)
"Return BUFFER contents, removing one trailing newline if present."
(with-current-buffer buffer
(let ((end (point-max)))
(when (and (> end (point-min))
(eq (char-before end) ?\n))
(setq end (1- end)))
(buffer-substring-no-properties (point-min) end))))

(define-compilation-mode go-guru-output-mode "Go guru"
"Go guru output mode is a variant of `compilation-mode' for the
output of the Go guru tool."
Expand Down Expand Up @@ -230,11 +239,15 @@ output of the Go guru tool."
(setq start end) ; break out of loop
(setq p (1- p)) ; exclude final space
(let* ((posn (buffer-substring-no-properties start p))
(flen (cl-search ":" posn)) ; length of filename
(filename (if (< flen 19)
(substring posn 0 flen)
(concat "…" (substring posn (- flen 19) flen)))))
(put-text-property start p 'display filename)
(filename (when (string-match "\\`\\(.+\\):[0-9]+:[0-9]+:\\'" posn)
(match-string 1 posn))))
(when filename
(let ((flen (length filename)))
(put-text-property
start p 'display
(if (< flen 19)
filename
(concat "…" (substring filename (- flen 19)))))))
(forward-line 1)
(setq start (point))))))))

Expand Down Expand Up @@ -323,19 +336,28 @@ If BUFFER, return the number of characters in that buffer instead."
(defun go-guru--goto-pos (posn other-window)
"Find the file containing the position POSN (of the form `file:line:col')
set the point to it, switching the current buffer."
(let ((file-line-pos (split-string posn ":")))
(funcall (if other-window #'find-file-other-window #'find-file) (car file-line-pos))
(pcase-let ((`(,filename ,line ,column) (go-guru--parse-position posn)))
(funcall (if other-window #'find-file-other-window #'find-file) filename)
(goto-char (point-min))
(forward-line (1- (string-to-number (cadr file-line-pos))))
(go-guru--goto-byte-column (string-to-number (cl-caddr file-line-pos)))))
(forward-line (1- line))
(go-guru--goto-byte-column column)))

(defun go-guru--parse-position (posn)
"Parse guru POSN in `file:line:column' form.
The file part may itself contain colons, as in a Windows drive path."
(if (and (string-match "\\`\\(.+\\):\\([0-9]+\\):\\([0-9]+\\)\\'" posn))
(list (match-string 1 posn)
(string-to-number (match-string 2 posn))
(string-to-number (match-string 3 posn)))
(error "Invalid guru position: %s" posn)))

(defun go-guru--goto-pos-no-file (posn)
"Given `file:line:col', go to the line and column. The file
component will be ignored."
(let ((file-line-pos (split-string posn ":")))
(pcase-let ((`(,_filename ,line ,column) (go-guru--parse-position posn)))
(goto-char (point-min))
(forward-line (1- (string-to-number (cadr file-line-pos))))
(go-guru--goto-byte-column (string-to-number (cl-caddr file-line-pos)))))
(forward-line (1- line))
(go-guru--goto-byte-column column)))

;;;###autoload
(defun go-guru-callees ()
Expand Down
75 changes: 50 additions & 25 deletions go-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -2050,12 +2050,19 @@ you save any file, kind of defeating the point of autoloading."

(defun go--godoc (query command)
(unless (string= query "")
(set-process-sentinel
(start-process-shell-command "godoc" (godoc--get-buffer query)
(concat command " " query))
'godoc--buffer-sentinel)
(let* ((command-args (go--godoc-command-args command query))
(proc (apply #'start-process "godoc" (godoc--get-buffer query)
(car command-args) (cdr command-args))))
(set-process-sentinel proc 'godoc--buffer-sentinel))
nil))

(defun go--godoc-command-args (command query)
"Return COMMAND's arguments followed by QUERY as a single argument."
(let ((command-args (split-string-and-unquote command)))
(unless command-args
(error "Empty godoc command"))
(append command-args (list query))))

(defun godoc-at-point (point)
"Show Go documentation for the identifier at POINT.

Expand Down Expand Up @@ -2169,6 +2176,16 @@ Tries to look for a URL at point."
(while (search-forward "\\" end t)
(put-text-property (1- (point)) (point) 'syntax-table (if (= (char-after) ?`) '(1) '(9))))))

(defun go--import-exists-p (import)
"Return non-nil if IMPORT already appears in an active import declaration."
(save-excursion
(goto-char (point-min))
10BC8 (re-search-forward
(concat "^[[:space:]]*\\(?:import[[:space:]]+\\)?\\(?:[[:word:]_.]+[[:space:]]+\\)?\""
(regexp-quote import)
"\"\\(?:[[:space:]]*//.*\\)?$")
nil t)))

(defun go-import-add (arg import)
"Add a new IMPORT to the list of imports.

Expand Down Expand Up @@ -2196,7 +2213,9 @@ uncommented, otherwise a new import will be added."
(setq line (format "\"%s\"" import)))

(goto-char (point-min))
(if (re-search-forward (concat "^[[:space:]]*//[[:space:]]*import " line "$") nil t)
(if (go--import-exists-p import)
(message "%s is already imported" import)
(if (re-search-forward (concat "^[[:space:]]*//[[:space:]]*import " line "$") nil t)
(uncomment-region (line-beginning-position) (line-end-position))
(cl-case (go-goto-imports)
(fail (message "Could not find a place to add import."))
Expand All @@ -2210,7 +2229,7 @@ uncommented, otherwise a new import will be added."
(uncomment-region (line-beginning-position) (line-end-position))
(insert "\n\t" line)))
(single (insert "import " line "\n"))
(none (insert "\nimport (\n\t" line "\n)\n")))))))
(none (insert "\nimport (\n\t" line "\n)\n"))))))))

(defun go-root-and-paths ()
(let* ((output (process-lines go-command "env" "GOROOT" "GOPATH"))
Expand Down Expand Up @@ -2449,6 +2468,17 @@ Use DIVISOR to scale absolute counts to a [0,10] scale."
(overlay-put ov 'face face)
(overlay-put ov 'help-echo (format "Count: %d" count))))

(defun go--coverage-parse-line (line)
"Parse one Go coverage LINE into its file and six numeric fields.
Return nil for lines that do not match the coverprofile format."
(when (string-match
"\\`\\(.*\\):\\([0-9]+\\)\\.\\([0-9]+\\),\\([0-9]+\\)\\.\\([0-9]+\\) \\([0-9]+\\) \\([0-9]+\\)\\'"
line)
(cons (match-string 1 line)
(mapcar (lambda (index)
(string-to-number (match-string index line)))
'(2 3 4 5 6 7)))))

(defun go--coverage-clear-overlays ()
"Remove existing overlays and put a single untracked overlay
over the entire buffer."
Expand All @@ -2466,24 +2496,20 @@ divisor for FILE-NAME."
(insert-file-contents coverage-file)
(go--goto-line 2) ;; Skip over mode
(while (not (eobp))
(let* ((parts (split-string (buffer-substring (line-beginning-position) (line-end-position)) ":"))
(file (car parts))
(rest (split-string (nth 1 parts) "[., ]")))

(cl-destructuring-bind
(start-line start-column end-line end-column _num count)
(mapcar #'string-to-number rest)

(when (string= (file-name-nondirectory file) file-name)
(if (> count max-count)
(setq max-count count))
(push (make-go--covered :start-line start-line
:start-column start-column
:end-line end-line
:end-column end-column
:covered (/= count 0)
:count count)
ranges)))
(let ((record (go--coverage-parse-line
(buffer-substring (line-beginning-position) (line-end-position)))))
(when record
(pcase-let ((`(,file ,start-line ,start-column ,end-line ,end-column ,_num ,count) record))
(when (string= (file-name-nondirectory file) file-name)
(if (> count max-count)
(setq max-count count))
(push (make-go--covered :start-line start-line
:start-column start-column
:end-line end-line
:end-column end-column
:covered (/= count 0)
:count count)
ranges))))

(forward-line)))

Expand Down Expand Up @@ -2820,7 +2846,6 @@ current buffer in the format specified by guru's -modified flag."
(insert-buffer-substring buffer))

(defun go--buffer-size-bytes (&optional buffer)
(message "buffer; %s" buffer)
"Return the number of bytes in the current buffer.
If BUFFER, return the number of characters in that buffer instead."
(with-current-buffer (or buffer (current-buffer))
Expand Down
72 changes: 72 additions & 0 deletions test/go-guru-test.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
;;; go-guru-test.el --- Tests for go-guru.el -*- lexical-binding: t; -*-

(require 'ert)
(add-to-list 'load-path "..")
(require 'go-guru)

(ert-deftest go-guru--parse-position-supports-colons-in-paths ()
(should (equal (go-guru--parse-position "/tmp/example.go:12:3")
'("/tmp/example.go" 12 3)))
(should (equal (go-guru--parse-position "C:\\work\\example.go:12:3")
'("C:\\work\\example.go" 12 3)))
(should-error (go-guru--parse-position "example.go:line:column")))

(ert-deftest go--godoc-command-keeps-query-as-one-argument ()
(should (equal (go--godoc-command-args "go doc" "fmt; echo unsafe")
'("go" "doc" "fmt; echo unsafe")))
(should-error (go--godoc-command-args "" "fmt")))

(ert-deftest go--godoc-starts-process-without-a-shell ()
(let (start-args)
(cl-letf (((symbol-function 'godoc--get-buffer)
(lambda (_query) (get-buffer-create " *godoc test*")))
((symbol-function 'start-process)
(lambda (&rest args) (setq start-args args) 'fake-process))
((symbol-function 'set-process-sentinel) (lambda (&rest _args) nil)))
(unwind-protect
(go--godoc "fmt; echo unsafe" "go doc")
(when (get-buffer " *godoc test*")
(kill-buffer " *godoc test*"))))
(should (equal (nthcdr 2 start-args)
'("go" "doc" "fmt; echo unsafe")))))

(ert-deftest go-guru--error-output-preserves-content-without-newline ()
(with-temp-buffer
(insert "failed")
(should (equal (go-guru--error-output (current-buffer)) "failed")))
(with-temp-buffer
(insert "failed\n")
(should (equal (go-guru--error-output (current-buffer)) "failed")))
(with-temp-buffer
(should (equal (go-guru--error-output (current-buffer)) ""))))

(ert-deftest go-guru--compilation-filter-ignores-non-location-diagnostics ()
(with-temp-buffer
(insert "go: warning: missing module\n")
(let ((compilation-filter-start (point-min)))
(go-guru--compilation-filter-hook))
(should (equal (buffer-string) "go: warning: missing module\n"))))

(ert-deftest go-guru--compilation-filter-keeps-drive-colon-in-path ()
(with-temp-buffer
(insert "C:\\src\\example.go:10:5: error\n")
(let ((compilation-filter-start (point-min)))
(go-guru--compilation-filter-hook))
(should (equal (get-text-property (point-min) 'display)
"C:\\src\\example.go"))))

(ert-deftest go--coverage-parse-line-supports-drive-colons ()
(should (equal (go--coverage-parse-line "C:/work/example.go:1.2,3.4 1 7")
'("C:/work/example.go" 1 2 3 4 1 7)))
(should-not (go--coverage-parse-line "not a coverage record")))

(ert-deftest go--buffer-size-bytes-has-no-message-side-effect ()
(let (message-called)
(cl-letf (((symbol-function 'message)
(lambda (&rest _args) (setq message-called t))))
(with-temp-buffer
(insert "abc")
(should (= (go--buffer-size-bytes) 3))))
(should-not message-called)))

;;; go-guru-test.el ends here
20 changes: 20 additions & 0 deletions test/go-import-test.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
;;; go-import-test.el --- Tests for Go import editing -*- lexical-binding: t; -*-

(require  8 9;ert)
(require 'go-mode)

(ert-deftest go-import-add-does-not-duplicate-existing-imports ()
(with-temp-buffer
(go-mode)
(insert "package main\n\nimport (\n\t\"fmt\"\n)\n")
(let ((before (buffer-string)))
(go-import-add nil "fmt")
(should (equal (buffer-string) before))))
(with-temp-buffer
(go-mode)
(insert "package main\n\nimport io \"io\"\n")
(let ((before (buffer-string)))
(go-import-add nil "io")
(should (equal (buffer-string) before)))))

;;; go-import-test.el ends here
0