Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add phpstan-insert-ignore command #53

Merged
merged 2 commits into from
Nov 7, 2024
Merged
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
7 changes: 6 additions & 1 deletion README.org
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#+END_HTML
Emacs interface to [[https://github.com/phpstan/phpstan][PHPStan]], includes checker for [[http://www.flycheck.org/en/latest/][Flycheck]].
** Support version
- Emacs 24+
- Emacs 25+
- PHPStan latest/dev-master (NOT support 0.9 seriese)
- PHP 7.1+ or Docker runtime
** How to install
Expand Down Expand Up @@ -99,7 +99,12 @@ Add ~\PHPStan\dumpType(...);~ to your PHP code and analyze it to make PHPStan di
By default, if you press ~C-u~ before invoking the command, ~\PHPStan\dumpPhpDocType()~ will be inserted.

This feature was added in *PHPStan 1.12.7* and will dump types compatible with the ~@param~ and ~@return~ PHPDoc tags.
*** Command ~phpstan-insert-ignore~
Insert a ~@phpstan-ignore~ tag to suppress any PHPStan errors on the current line.

By default it inserts the tag on the previous line, but if there is already a tag at the end of the current line or on the previous line, the identifiers will be appended there.

If there is no existing tag and ~C-u~ is pressed before the command, it will be inserted at the end of the line.
** API
Most variables defined in this package are buffer local. If you want to set it for multiple projects, use [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Default-Value.html][setq-default]].

Expand Down
66 changes: 31 additions & 35 deletions flycheck-phpstan.el
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
(defvar flycheck-phpstan-executable)
(defvar flycheck-phpstan--temp-buffer-name "*Flycheck PHPStan*")


(defcustom flycheck-phpstan-ignore-metadata-list nil
"Set of metadata items to ignore in PHPStan messages for Flycheck."
:type '(set (const identifier)
Expand Down Expand Up @@ -76,45 +75,42 @@

(defun flycheck-phpstan-parse-output (output &optional _checker _buffer)
"Parse PHPStan errors from OUTPUT."
(with-current-buffer (flycheck-phpstan--temp-buffer)
(erase-buffer)
(insert output))
(flycheck-phpstan-parse-json (flycheck-phpstan--temp-buffer)))
(let* ((json-buffer (with-current-buffer (flycheck-phpstan--temp-buffer)
(erase-buffer)
(insert output)
(current-buffer)))
(data (phpstan--parse-json json-buffer))
(errors (phpstan--plist-to-alist (plist-get data :files))))
(unless phpstan-disable-buffer-errors
(phpstan-update-ignorebale-errors-from-json-buffer errors))
(flycheck-phpstan--build-errors errors)))

(defun flycheck-phpstan--temp-buffer ()
"Return a temporary buffer for decode JSON."
(get-buffer-create flycheck-phpstan--temp-buffer-name))

(defun flycheck-phpstan-parse-json (json-buffer)
"Parse PHPStan errors from JSON-BUFFER."
(let ((data (phpstan--parse-json json-buffer)))
(cl-loop for (file . entry) in (flycheck-phpstan--plist-to-alist (plist-get data :files))
append (cl-loop for messages in (plist-get entry :messages)
for text = (let* ((msg (plist-get messages :message))
(ignorable (plist-get messages :ignorable))
(identifier (unless (memq 'identifier flycheck-phpstan-ignore-metadata-list)
(plist-get messages :identifier)))
(tip (unless (memq 'tip flycheck-phpstan-ignore-metadata-list)
(plist-get messages :tip)))
(lines (list (when (and identifier ignorable)
(concat phpstan-identifier-prefix identifier))
(when tip
(concat phpstan-tip-message-prefix tip))))
(lines (cl-remove-if #'null lines)))
(if (null lines)
msg
(concat msg flycheck-phpstan-metadata-separator
(mapconcat #'identity lines "\n"))))
collect (flycheck-error-new-at (plist-get messages :line)
nil 'error text
:filename file)))))

(defun flycheck-phpstan--plist-to-alist (plist)
"Convert PLIST to association list."
(let (alist)
(while plist
(push (cons (substring-no-properties (symbol-name (pop plist)) 1) (pop plist)) alist))
(nreverse alist)))
(defun flycheck-phpstan--build-errors (errors)
"Build Flycheck errors from PHPStan ERRORS."
(cl-loop for (file . entry) in errors
append (cl-loop for messages in (plist-get entry :messages)
for text = (let* ((msg (plist-get messages :message))
(ignorable (plist-get messages :ignorable))
(identifier (unless (memq 'identifier flycheck-phpstan-ignore-metadata-list)
(plist-get messages :identifier)))
(tip (unless (memq 'tip flycheck-phpstan-ignore-metadata-list)
(plist-get messages :tip)))
(lines (list (when (and identifier ignorable)
(concat phpstan-identifier-prefix identifier))
(when tip
(concat phpstan-tip-message-prefix tip))))
(lines (cl-remove-if #'null lines)))
(if (null lines)
msg
(concat msg flycheck-phpstan-metadata-separator
(mapconcat #'identity lines "\n"))))
collect (flycheck-error-new-at (plist-get messages :line)
nil 'error text
:filename file))))

(flycheck-define-checker phpstan
"PHP static analyzer based on PHPStan."
Expand Down
105 changes: 104 additions & 1 deletion phpstan.el
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
;; Version: 0.7.2
;; Keywords: tools, php
;; Homepage: https://github.com/emacs-php/phpstan.el
;; Package-Requires: ((emacs "24.3") (compat "29") (php-mode "1.22.3") (php-runtime "0.2"))
;; Package-Requires: ((emacs "25.1") (compat "29") (php-mode "1.22.3") (php-runtime "0.2"))
;; License: GPL-3.0-or-later

;; This program is free software; you can redistribute it and/or modify
Expand Down Expand Up @@ -56,6 +56,7 @@
(require 'cl-lib)
(require 'php-project)
(require 'php-runtime)
(require 'seq)

(eval-when-compile
(require 'compat nil t)
Expand Down Expand Up @@ -154,8 +155,19 @@ have unexpected behaviors or performance implications."
:type '(cons string string)
:group 'phpstan)

(defcustom phpstan-disable-buffer-errors nil
"If T, don't keep errors per buffer to save memory."
:type 'boolean
:group 'phpstan)

(defcustom phpstan-not-ignorable-identifiers '("ignore.parseError")
"A list of identifiers that are prohibited from being added to the @phpstan-ignore tag."
:type '(repeat string))

(defvar-local phpstan--use-xdebug-option nil)

(defvar-local phpstan--ignorable-errors '())

;;;###autoload
(progn
(defvar phpstan-working-dir nil
Expand Down Expand Up @@ -271,6 +283,18 @@ NIL
(and (stringp (car v)) (listp (cdr v))))
(or (eq 'docker v) (null v) (stringp v))))))

;; Utilities:
(defun phpstan--plist-to-alist (plist)
"Convert PLIST to association list."
(let (alist)
(while plist
(push (cons (substring-no-properties (symbol-name (pop plist)) 1) (pop plist)) alist))
(nreverse alist)))

(defsubst phpstan--current-line ()
"Return the current buffer line at point. The first line is 1."
(line-number-at-pos nil t))

;; Functions:
(defun phpstan-get-working-dir ()
"Return path to working directory of PHPStan."
Expand Down Expand Up @@ -489,6 +513,85 @@ it returns the value of `SOURCE' as it is."
options
(and args (cons "--" args)))))

(defun phpstan-update-ignorebale-errors-from-json-buffer (errors)
"Update `phpstan--ignorable-errors' variable by ERRORS."
(let ((identifiers
(cl-loop for (_ . entry) in errors
append (cl-loop for message in (plist-get entry :messages)
if (plist-get message :ignorable)
collect (cons (plist-get message :line)
(plist-get message :identifier))))))
(setq phpstan--ignorable-errors
(mapcar (lambda (v) (cons (car v) (mapcar #'cdr (cdr v)))) (seq-group-by #'car identifiers)))))

(defconst phpstan--re-ignore-tag
(eval-when-compile
(rx (* (syntax whitespace)) "//" (* (syntax whitespace))
(group "@phpstan-ignore")
(* (syntax whitespace))
(* (not "("))
(group (? (+ (syntax whitespace) "("))))))

(cl-defun phpstan--check-existing-ignore-tag (&key in-previous)
"Check existing @phpstan-ignore PHPDoc tag.
If IN-PREVIOUS is NIL, check the previous line for the tag."
(let ((new-position (if in-previous 'previous-line 'this-line))
(line-end (line-end-position))
new-point append)
(save-excursion
(save-match-data
(if (re-search-forward phpstan--re-ignore-tag line-end t)
(progn
(setq new-point (match-beginning 2))
(goto-char new-point)
(when (eq (char-syntax (char-before)) ?\ )
(left-char)
(setq new-point (point)))
(setq append (not (eq (match-end 1) (match-beginning 2))))
(cl-values new-position new-point append))
(if in-previous
(cl-values nil nil nil)
(previous-logical-line)
(beginning-of-line)
(phpstan--check-existing-ignore-tag :in-previous t)))))))

;;;###autoload
(defun phpstan-insert-ignore (position)
"Insert an @phpstan-ignore comment at the specified POSITION.

POSITION determines where to insert the comment and can be either `this-line' or
`previous-line'.

- If POSITION is `this-line', the comment is inserted at the end of
the current line.
- If POSITION is `previous-line', the comment is inserted on a new line above
the current line."
(interactive
(list (if current-prefix-arg 'this-line 'previous-line)))
(save-restriction
(widen)
(let ((pos (point))
(identifiers (cl-set-difference (alist-get (phpstan--current-line) phpstan--ignorable-errors) phpstan-not-ignorable-identifiers :test #'equal))
(padding (if (eq position 'this-line) " " ""))
new-position new-point delete-region)
(cl-multiple-value-setq (new-position new-point append) (phpstan--check-existing-ignore-tag :in-previous nil))
(when new-position
(setq position new-position))
(unless (and append (null identifiers))
(if (not new-point)
(cond
((eq position 'this-line) (end-of-line))
((eq position 'previous-line) (progn
(previous-logical-line)
(end-of-line)
(newline-and-indent)))
((error "Unexpected position: %s" position)))
(setq padding "")
(goto-char new-point))
(insert (concat padding
(if new-position (if append ", " " ") "// @phpstan-ignore ")
(mapconcat #'identity identifiers ", ")))))))

;;;###autoload
(defun phpstan-insert-dumptype (&optional expression prefix-num)
"Insert PHPStan\\dumpType() expression-statement by EXPRESSION and PREFIX-NUM."
Expand Down
Loading