├── .github └── funding.yml ├── .gitignore ├── AUTORS ├── LICENCE ├── README.org ├── TODO.org ├── php-cs-fixer-config.php └── php-cs-fixer.el /.github/funding.yml: -------------------------------------------------------------------------------- 1 | funding: 2 | - type: buy_me_a_coffee 3 | url: https://buymeacoffee.com/pivaldi 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | -------------------------------------------------------------------------------- /AUTORS: -------------------------------------------------------------------------------- 1 | The go-mode Authors 2 | Philippe Ivaldi -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 OVYA (Renée Costes Group). All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * PHP-CS-Fixer Wrapper for Emacs 2 | 3 | ** Features 4 | 5 | Allows in the /Emacs/ editor to fix most issues in /PHP/ code when 6 | you want to follow the coding standards /PSR-1/ and /PSR-2/. 7 | 8 | Under the hood, this package provides the customisable /Elisp/ command 9 | /php-cs-fix/ wich wraps the command line /php-cs-fixer/ *version >=2.0* 10 | provided by the powerful [[http://cs.sensiolabs.org/][PHP Coding Standards Fixer]] 11 | 12 | ** Installation 13 | *** System part 14 | 15 | Install [[https://github.com/FriendsOfPHP/PHP-CS-Fixer][PHP-CS-Fixer]] so that the command line =php-cs-fixer= is 16 | executable in your system. 17 | 18 | *** Emacs part 19 | 20 | **** From Melpa 21 | 22 | This package can be installed from [[https://melpa.org/#/php-cs-fixer][Melpa]] : =M-x package-install php-cs-fixer = 23 | 24 | **** Manually 25 | 26 | This package requires that the =cl-lib= package was installed. 27 | 28 | To install =php-cs-fixer.el=, place =php-cs-fixer.el= in a 29 | directory of your choice, add it to your load path and require 30 | =php-cs-fixer= writing this code in your emacs configuration file (aka .emacs) : 31 | 32 | #+BEGIN_SRC elisp 33 | (add-to-list 'load-path "/place/where/you/put/it/") 34 | (require 'php-cs-fixer) 35 | #+END_SRC 36 | 37 | Either evaluate both statements with =C-x C-e=, or restart /Emacs/. 38 | 39 | ** Usage 40 | 41 | Try =M-x php-cs-fix [ret]= when editing a /PHP/ file. 42 | 43 | If you want an automatic fix when saving all php files, place this code in your /Emacs/ configuration file : 44 | #+BEGIN_SRC elisp 45 | (add-hook 'before-save-hook 'php-cs-fixer-before-save) 46 | #+END_SRC 47 | 48 | ** Customisable variables 49 | 50 | - =php-cs-fixer-command= is the =php-cs-fixer= command (default is =php-cs-fixer=) ; 51 | - =php-cs-fixer-config-option= is the =php-cs-fixer --config= option value (default is =nil=, see the file 52 | =php-cs-fixer-config.php= for an example of configuration file) ; 53 | - =php-cs-fixer-rules-level-part-options= is the =php-cs-fixer --rules= base part options value (default is ='("@Symfony")=) ; 54 | - =php-cs-fixer-rules-fixer-part-options= is the =php-cs-fixer --rules= exact rules part options 55 | value (default is =("no_multiline_whitespace_before_semicolons" "concat_space")=). 56 | - =php-cs-fixer-fix-popup-on-error= : should fixing a file popups an error buffer when it failed ?\\ 57 | Warning : when =nil= (the default), error fixing a file will calls the Emacs 58 | function =warn= that can be inhibited by =warning-suppress-types=. 59 | 60 | Try =M-x customize-group [ret] php-cs-fixer [ret]= to modify the values of these variables. 61 | 62 | ** If you appreciate this project 63 | 64 | [[https://buymeacoffee.com/pivaldi][☕ Buy Me a Coffee]] 65 | -------------------------------------------------------------------------------- /TODO.org: -------------------------------------------------------------------------------- 1 | #+title: Todo 2 | 3 | - Add support for Apheleia 4 | #+begin_src lisp 5 | (cl-defun pim--php-cs-fixer-apheleia (&key buffer scratch formatter remote callback remote &allow-other-keys) 6 | "Called by `apheleia--run-formatter-function'. 7 | :buffer buffer 8 | Original buffer being formatted. This shouldn't be 9 | modified. You can use it to check things like the 10 | current major mode, or the buffer filename. If you 11 | use it as input for the formatter, your formatter 12 | won't work when chained after another formatter. 13 | :scratch scratch 14 | Buffer the formatter should modify. This starts out 15 | containing the original file contents, which will be 16 | the same as `buffer' except it has already been 17 | transformed by any formatters that ran previously. 18 | Name of the current formatter symbol, e.g. `black'. 19 | :formatter 20 | The current formatter (a symbol) 21 | :callback 22 | Callback. Should pass an error value (cons of symbol 23 | and data, like for `signal') or nil. For backwards 24 | compatibility it can also invoke only on success, 25 | with no args. 26 | :remote remote 27 | The remote part of the buffers file-name or directory. 28 | :async (not remote) 29 | Whether the formatter should be run async or not. 30 | :callback 31 | Callback when formatting scratch has failed. 32 | " 33 | (when (and (equal formatter 'php-cs-fixer) (not remote)) 34 | (with-current-buffer buffer 35 | (php-cs-fixer-fix) 36 | (funcall callback)))) 37 | (with-eval-after-load 'apheleia 38 | (setf (alist-get 'php-cs-fixer apheleia-formatters) 39 | 'pim--php-cs-fixer-apheleia) 40 | (setf (alist-get 'php-mode apheleia-mode-alist) '(php-cs-fixer)) 41 | ) 42 | #+end_src 43 | - Use =replace-buffer-contents= 44 | See [[https://github.com/radian-software/apheleia/issues/38]] 45 | - See https://github.com/purcell/emacs-reformatter 46 | -------------------------------------------------------------------------------- /php-cs-fixer-config.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | setRules([ 10 | '@Symfony' => true, 11 | 'array_syntax' => ['syntax' => 'short'], 12 | 'array_indentation' => true, 13 | 'multiline_whitespace_before_semicolons' => true, 14 | 'method_argument_space' => ['ensure_fully_multiline' => true], 15 | 'declare_equal_normalize' => [ 16 | 'space' => 'single', 17 | ], 18 | 'binary_operator_spaces' => [ 19 | 'default' => 'single_space' 20 | ], 21 | 'concat_space' => ['spacing' => 'one'], 22 | ]); 23 | 24 | return $config->setFinder($finder); 25 | -------------------------------------------------------------------------------- /php-cs-fixer.el: -------------------------------------------------------------------------------- 1 | ;;; php-cs-fixer.el --- The php-cs-fixer wrapper -*- lexical-binding: t; -*- 2 | 3 | ;;; License: 4 | ;; Copyright 2015 OVYA (Renée Costes Group). All rights reserved. 5 | ;; Use of this source code is governed by a BSD-style 6 | ;; license that can be found in the LICENSE file. 7 | 8 | ;;; Author: Philippe Ivaldi for OVYA 9 | ;; Source: Some pieces of code are copied from go-mode.el https://github.com/dominikh/go-mode.el 10 | ;; Version: 2.1.0 11 | ;; Keywords: languages php 12 | ;; Package-Requires: ((emacs "24.3")) 13 | ;; URL: https://github.com/pivaldi/php-cs-fixer 14 | ;; 15 | ;;; Commentary: 16 | ;; This file is not part of GNU Emacs. 17 | ;; See the file README.org for further information. 18 | 19 | ;;; Code: 20 | 21 | (require 'cl-lib) 22 | 23 | ;;;###autoload 24 | (defgroup php-cs-fixer nil 25 | "The php-cs-fixer wrapper group." 26 | :tag "PHP" 27 | :prefix "php-cs-fixer-" 28 | :group 'languages 29 | :link '(url-link :tag "Source code repository" "https://github.com/OVYA/php-cs-fixer") 30 | :link '(url-link :tag "Executable dependency" "https://github.com/FriendsOfPHP/PHP-CS-Fixer")) 31 | 32 | (defcustom php-cs-fixer-command "php-cs-fixer" 33 | "The php-cs-fixer command." 34 | :type 'string 35 | :group 'php-cs-fixer) 36 | 37 | (defcustom php-cs-fixer-config-option nil 38 | "The php-cs-fixer config option. 39 | If not nil `php-cs-rules-level-part-options` 40 | and `php-cs-rules-fixer-part-options` are not used." 41 | :type 'string 42 | :group 'php-cs-fixer) 43 | 44 | (defcustom php-cs-fixer-rules-level-part-options '("@Symfony") 45 | "The php-cs-fixer --rules base part options." 46 | :type '(repeat 47 | (choice 48 | ;; (const :tag "Not set" :value nil) 49 | (const :value "@DoctrineAnnotation") 50 | (const :value "@PHP54Migration") 51 | (const :value "@PHP56Migration:risky") 52 | (const :value "@PHP70Migration") 53 | (const :value "@PHP70Migration:risky") 54 | (const :value "@PHP71Migration") 55 | (const :value "@PHP71Migration:risky") 56 | (const :value "@PHP73Migration") 57 | (const :value "@PHP74Migration") 58 | (const :value "@PHP74Migration:risky") 59 | (const :value "@PHP80Migration") 60 | (const :value "@PHP80Migration:risky") 61 | (const :value "@PHP81Migration") 62 | (const :value "@PHPUnit30Migration:risky") 63 | (const :value "@PHPUnit32Migration:risky") 64 | (const :value "@PHPUnit35Migration:risky") 65 | (const :value "@PHPUnit43Migration:risky") 66 | (const :value "@PHPUnit48Migration:risky") 67 | (const :value "@PHPUnit50Migration:risky") 68 | (const :value "@PHPUnit52Migration:risky") 69 | (const :value "@PHPUnit54Migration:risky") 70 | (const :value "@PHPUnit55Migration:risky") 71 | (const :value "@PHPUnit56Migration:risky") 72 | (const :value "@PHPUnit57Migration:risky") 73 | (const :value "@PHPUnit60Migration:risky") 74 | (const :value "@PHPUnit75Migration:risky") 75 | (const :value "@PHPUnit84Migration:risky") 76 | (const :value "@PSR1") 77 | (const :value "@PSR12") 78 | (const :value "@PSR12:risky") 79 | (const :value "@PSR2") 80 | (const :value "@PhpCsFixer") 81 | (const :value "@PhpCsFixer:risky") 82 | (const :value "@Symfony") 83 | (const :value "@Symfony:risky"))) 84 | :group 'php-cs-fixer) 85 | 86 | (defcustom php-cs-fixer-rules-fixer-part-options 87 | '("multiline_whitespace_before_semicolons" "concat_space") 88 | "The php-cs-fixer --rules part options. 89 | These options are not part of `php-cs-fixer-rules-level-part-options`." 90 | :type '(repeat string) 91 | :group 'php-cs-fixer) 92 | 93 | (defcustom php-cs-fixer-fix-popup-on-error nil 94 | "Should `php-cs-fixer-fix` popup an error buffer on error ?" 95 | :type 'boolean 96 | :group 'php-cs-fixer) 97 | 98 | 99 | ;; Copy of go--goto-line from https://github.com/dominikh/go-mode.el 100 | (defun php-cs-fixer--goto-line (line) 101 | "Private goto line to LINE." 102 | (goto-char (point-min)) 103 | (forward-line (1- line))) 104 | 105 | (defun php-cs-fixer--delete-whole-line (&optional arg) 106 | "Delete the current line without putting it in the `kill-ring`. 107 | Derived from the function `kill-whole-line'. 108 | ARG is defined as for that function." 109 | (delete-region 110 | (progn (forward-line 0) (point)) 111 | (progn (forward-line (or arg 0)) (point)))) 112 | 113 | ;; Derivated of go--apply-rcs-patch from https://github.com/dominikh/go-mode.el 114 | (defun php-cs-fixer--apply-rcs-patch (patch-buffer) 115 | "Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer." 116 | (let ((target-buffer (current-buffer)) 117 | ;; Relative offset between buffer line numbers and line numbers 118 | ;; in patch. 119 | ;; 120 | ;; Line numbers in the patch are based on the source file, so 121 | ;; we have to keep an offset when making changes to the 122 | ;; buffer. 123 | ;; 124 | ;; Appending lines decrements the offset (possibly making it 125 | ;; negative), deleting lines increments it. This order 126 | ;; simplifies the forward-line invocations. 127 | (line-offset 0)) 128 | (save-excursion 129 | (with-current-buffer patch-buffer 130 | (goto-char (point-min)) 131 | (while (not (eobp)) 132 | (unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)") 133 | (error "Invalid rcs patch or internal error in php-cs-fixer--apply-rcs-patch")) 134 | (forward-line) 135 | (let ((action (match-string 1)) 136 | (from (string-to-number (match-string 2))) 137 | (len (string-to-number (match-string 3)))) 138 | (cond 139 | ((equal action "a") 140 | (let ((start (point))) 141 | (forward-line len) 142 | (let ((text (buffer-substring start (point)))) 143 | (with-current-buffer target-buffer 144 | (cl-decf line-offset len) 145 | (goto-char (point-min)) 146 | (forward-line (- from len line-offset)) 147 | (insert text))))) 148 | ((equal action "d") 149 | (with-current-buffer target-buffer 150 | (php-cs-fixer--goto-line (- from line-offset)) 151 | (cl-incf line-offset len) 152 | (php-cs-fixer--delete-whole-line len))) 153 | (t 154 | (error "Invalid rcs patch or internal error in php-cs-fixer--apply-rcs-patch"))))))))) 155 | 156 | (defun php-cs-fixer--kill-error-buffer (errbuf) 157 | "Private function that kill the error buffer ERRBUF." 158 | (let ((win (get-buffer-window errbuf))) 159 | (if win 160 | (quit-window t win) 161 | (kill-buffer errbuf)))) 162 | 163 | (defun php-cs-fixer--build-rules-options () 164 | "Private method to build the --rules options." 165 | (if php-cs-fixer-config-option "" 166 | (let ((base-opts 167 | (concat 168 | (if php-cs-fixer-rules-level-part-options 169 | (mapconcat 'identity php-cs-fixer-rules-level-part-options ",") 170 | nil))) 171 | (other-opts (if php-cs-fixer-rules-fixer-part-options (concat "," (mapconcat 'identity php-cs-fixer-rules-fixer-part-options ",")) nil))) 172 | (concat 173 | "--rules=" base-opts 174 | (if other-opts other-opts ""))))) 175 | 176 | (defvar php-cs-fixer-command-not-found-msg "Command php-cs-fixer not found. 177 | Fix this issue removing the Emacs package php-cs-fixer or installing the program php-cs-fixer") 178 | 179 | (defvar php-cs-fixer-command-bad-version-msg "Command php-cs-fixer version not supported. 180 | Fix this issue removing the Emacs package php-cs-fixer or updating the program php-cs-fixer to version 2.*") 181 | 182 | (defvar php-cs-fixer-is-command-ok-var nil) 183 | 184 | (defun php-cs-fixer--is-command-ok () 185 | "Private Method. 186 | Return t if the command `php-cs-fixer-command` 187 | is available and supported by this package, return nil otherwise. 188 | The test is done at first call and the same result will returns 189 | for the next calls." 190 | (if php-cs-fixer-is-command-ok-var 191 | (= 1 php-cs-fixer-is-command-ok-var) 192 | (progn 193 | (message "Testing php-cs-fixer existence and version...") 194 | (setq php-cs-fixer-is-command-ok-var 0) 195 | 196 | (if (executable-find php-cs-fixer-command) 197 | (if (string-match ".+ [2-3].[0-9]+.*" 198 | (shell-command-to-string 199 | (concat php-cs-fixer-command " --version"))) 200 | (progn (setq php-cs-fixer-is-command-ok-var 1) t) 201 | (progn 202 | (warn php-cs-fixer-command-bad-version-msg) 203 | nil)) 204 | (progn (warn php-cs-fixer-command-not-found-msg) nil))))) 205 | 206 | ;;;###autoload 207 | (defun php-cs-fixer-fix () 208 | "Formats the current PHP buffer according to the PHP-CS-Fixer tool." 209 | (interactive) 210 | (when (php-cs-fixer--is-command-ok) 211 | (let* ((tmpfile (make-temp-file "PHP-CS-Fixer-" nil ".php")) 212 | (filename (buffer-file-name)) 213 | (date (format-time-string "%Y-%m-%d %H:%M:%S %Z" (current-time))) 214 | (patchbuf (get-buffer-create "*PHP-CS-Fixer patch*")) 215 | (errbuf (get-buffer-create 216 | (format 217 | "*PHP-CS-Fixer Errors %s*" 218 | (substring (md5 filename) 0 10)))) 219 | (coding-system-for-read 'utf-8) 220 | (coding-system-for-write 'utf-8) 221 | (errorp nil)) 222 | (save-restriction 223 | (widen) 224 | (if errbuf 225 | (with-current-buffer errbuf 226 | (setq buffer-read-only nil) 227 | (erase-buffer) 228 | (insert (format "%s - An error occurs fixing the file `%s`\n\n" date filename)))) 229 | (with-current-buffer patchbuf 230 | (erase-buffer)) 231 | 232 | (write-region nil nil tmpfile) 233 | 234 | ;; We're using errbuf for the mixed stdout and stderr output. This 235 | ;; is not an issue because php-cs-fixer -q does not produce any stdout 236 | ;; output in case of success. 237 | (if (and (zerop (call-process "php" nil errbuf nil "-l" tmpfile)) 238 | (zerop (call-process php-cs-fixer-command 239 | nil errbuf nil 240 | "fix" 241 | (if php-cs-fixer-config-option 242 | (concat "--config=" (shell-quote-argument php-cs-fixer-config-option)) 243 | (php-cs-fixer--build-rules-options)) 244 | "--using-cache=no" 245 | "--quiet" 246 | tmpfile))) 247 | (if (zerop (call-process-region (point-min) (point-max) "diff" nil patchbuf nil "-n" "-" tmpfile)) 248 | (message "Buffer is already php-cs-fixed") 249 | (progn 250 | (php-cs-fixer--apply-rcs-patch patchbuf) 251 | (message "Applied php-cs-fixer"))) 252 | (progn 253 | (if php-cs-fixer-fix-popup-on-error 254 | (let ((window (display-buffer errbuf '(nil (allow-no-window . t))))) 255 | (setq errorp t) 256 | (set-window-point window 1) 257 | (set-window-dedicated-p window t) 258 | (and (window-full-width-p window) 259 | (not (eq window (frame-root-window (window-frame window)))) 260 | (save-excursion 261 | (save-selected-window 262 | (select-window window) 263 | (enlarge-window (- (* 2 window-min-height) (window-height))))))) 264 | (warn (with-current-buffer errbuf (buffer-string))))))) 265 | (unless errorp (php-cs-fixer--kill-error-buffer errbuf)) 266 | (kill-buffer patchbuf) 267 | (delete-file tmpfile)))) 268 | 269 | ;; Prevents warning reference to free variable. 270 | (defvar geben-temporary-file-directory) 271 | 272 | ;;;###autoload 273 | (defun php-cs-fixer-before-save () 274 | "Used to automatically fix the file saving the buffer. 275 | Add this to .emacs to run php-cs-fix on the current buffer when saving: 276 | (add-hook \\='before-save-hook \\='php-cs-fixer-before-save)." 277 | (interactive) 278 | (when (and 279 | buffer-file-name 280 | (string= (file-name-extension buffer-file-name) "php") 281 | (or (not (boundp 'geben-temporary-file-directory)) 282 | (not (string-match geben-temporary-file-directory (file-name-directory buffer-file-name))))) 283 | (php-cs-fixer-fix))) 284 | 285 | (provide 'php-cs-fixer) 286 | ;;; php-cs-fixer.el ends here 287 | --------------------------------------------------------------------------------