目前的 Flymake 和 Flycheck 後端可以檢查語法錯誤、類型錯誤等,但是無法檢查語義錯誤和一些常見的 Bug pattern。今天突發奇想,可以用 GPT 來做,於是糊了一個簡單的 flymake 後端和檢查命令,使用效果類似這樣:
拋磚引玉用的代碼:
;; -*- lexical-binding: t; -*-
(require 'gptel) ; 只使用其中的 API key 函數
(require 'flymake)
(defconst flygpt-prompt
"You are a professional programming expert, and are asked to find any potential problems in the code, including bugs, security vulnerabilities, style issues, and documentation/comment conformance.
You report your findings in JSON format:
[{ \"line\": <line number>, \"diag\": <detailed problem description> },
... ]
If nothing is wrong, reply [].")
(defun line-numbered-string (str)
(with-temp-buffer
(setq cnt 0)
(dolist (line (split-string str "\n"))
(cl-incf cnt)
(insert (format "%d: %s\n" cnt line)))
(buffer-string)))
(defun flygpt-buffer ()
(interactive)
(let* ((source-buffer (current-buffer))
(url-request-method "POST")
(url-request-extra-headers
`(("Content-Type" . "application/json")
("Authorization" . ,(concat "Bearer " (gptel-api-key-from-auth-source)))))
(url-proxy-services '(("https" . "127.0.0.1:7890")))
(url-request-data (json-encode `(("model" . "gpt-3.5-turbo-16k")
("temperature" . 0)
("messages" . [(:role "system" :content ,flygpt-prompt)
(:role "user" :content ,(line-numbered-string (buffer-string)))])))))
(url-retrieve
"https://api.openai.com/v1/chat/completions"
(lambda (status)
(goto-char (point-min))
(search-forward-regexp "^$")
(let* ((json-object-type 'plist)
(api-resp (json-read))
(diags-json (plist-get (plist-get (seq-first (plist-get api-resp :choices)) :message) :content))
(diags (with-temp-buffer
(insert diags-json)
(goto-char (point-min))
(json-read))))
(with-current-buffer (get-buffer-create "*flygpt*")
(erase-buffer)
(cl-loop
for diag in (seq-into diags 'list)
for msg = (plist-get diag :diag)
for line = (plist-get diag :line)
do (insert (format "Line %d: %s\n" line msg))
finally (display-buffer "*flygpt*"))))))))
(setq flymake-gpt-buffer nil)
(defun flymake-gpt (report-fn &rest _)
(when flymake-gpt-buffer
(kill-process (get-buffer-process flymake-gpt-buffer))
(kill-buffer flymake-gpt-buffer))
(let* ((source-buffer (current-buffer))
(url-request-method "POST")
(url-request-extra-headers
`(("Content-Type" . "application/json")
("Authorization" . ,(concat "Bearer " (gptel-api-key-from-auth-source)))))
(url-proxy-services '(("https" . "127.0.0.1:7890")))
(url-request-data (json-encode `(("model" . "gpt-3.5-turbo-16k")
("temperature" . 0)
("messages" . [(:role "system" :content ,flygpt-prompt)
(:role "user" :content ,(line-numbered-string (buffer-string)))])))))
(url-retrieve
"https://api.openai.com/v1/chat/completions"
(lambda (status)
(goto-char (point-min))
(search-forward-regexp "^$")
(let* ((json-object-type 'plist)
(api-resp (json-read))
(diags-json (plist-get (plist-get (seq-first (plist-get api-resp :choices)) :message) :content))
(diags (with-temp-buffer
(insert diags-json)
(goto-char (point-min))
(json-read))))
(cl-loop
for diag in (seq-into diags 'list)
for msg = (plist-get diag :diag)
for line = (plist-get diag :line)
for (beg . end) = (flymake-diag-region source-buffer line)
collect (flymake-make-diagnostic source-buffer beg end :warning msg)
into flymake-diags
finally (funcall report-fn flymake-diags))
(kill-buffer flymake-gpt-buffer)
(setq flymake-gpt-buffer nil))))))
(defun turn-on-flymake-gpt ()
(interactive)
(add-hook 'flymake-diagnostic-functions 'flymake-gpt nil t)
(setq flymake-no-changes-timeout nil)
(flymake-mode))
(provide 'flygpt)