├── README.md ├── _config.yml ├── lua └── phpcs │ ├── init.lua │ └── utils.lua └── test.php /README.md: -------------------------------------------------------------------------------- 1 | # nvim-phpcsf 2 | 3 | ## What is nvim-phpcsf? 4 | `nvim-phpcsf` is a simple nvim plugin wrapper for both phpcs and phpcbf. 5 | The PHP_CodeSniffer's output is populated using the telescope picker. Telescope helps to navigate through phpcs errors and warnings and preview. 6 | 7 | 8 | ## Instalation 9 | Install [telescope](https://github.com/nvim-telescope/telescope.nvim) and [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer). 10 | Using the [vim-plug](https://github.com/junegunn/vim-plug) plugin manager add the following in your VIM configuration (e.g. ~/.vimrc or ~/.config/nvim/init.vim when using Neovim): 11 | 12 | ``` 13 | Plug 'praem90/nvim-phpcsf' 14 | ``` 15 | 16 | To run sniffer 17 | ``` 18 | :lua require'phpcs'.cs() 19 | ``` 20 | 21 | To run beautifier 22 | ``` 23 | :lua require'phpcs'.cbf() 24 | ``` 25 | 26 | To run PHP_CodeBeautifier after save (It is recommended to run this after the buffer has been written BufWritePost) 27 | ``` 28 | augroup PHBSCF 29 | autocmd! 30 | autocmd BufWritePost,BufReadPost,InsertLeave *.php :lua require'phpcs'.cs() 31 | autocmd BufWritePost *.php :lua require'phpcs'.cbf() 32 | augroup END 33 | ``` 34 | 35 | ## Configurations 36 | ```vim 37 | let g:nvim_phpcs_config_phpcs_path = 'phpcs' 38 | let g:nvim_phpcs_config_phpcbf_path = 'phpcbf' 39 | let g:nvim_phpcs_config_phpcs_standard = 'PSR12' " or path to your ruleset phpcs.xml 40 | ``` 41 | 42 | Using lua 43 | 44 | ```lua 45 | require("phpcs").setup({ 46 | phpcs = "phpcs", 47 | phpcbf = "phpcbf", 48 | standard = "PSR12" 49 | }) 50 | ``` 51 | 52 | ## Thanks 53 | [@thePrimeagen](https://github.com/theprimeagen) 54 | [@tjDevries](https://github.com/tjDevries) 55 | 56 | ## TODO: 57 | - [x] Detect phpcs.xml automatically on the project root 58 | - [x] Add sign to current buffer 59 | 60 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /lua/phpcs/init.lua: -------------------------------------------------------------------------------- 1 | local M = {}; 2 | local root = vim.loop.cwd() 3 | local phpcs_path = "$HOME/.config/composer/vendor/bin/phpcs" 4 | local phpcbf_path = "$HOME/.config/composer/vendor/bin/phpcbf" 5 | local phpcs_standard = nil 6 | 7 | local Job = require'plenary.job' 8 | local lutils = require('phpcs.utils') 9 | 10 | -- Config Variables 11 | M.phpcs_path = vim.g.nvim_phpcs_config_phpcs_path or phpcs_path 12 | M.phpcbf_path = vim.g.nvim_phpcs_config_phpcbf_path or phpcbf_path 13 | M.phpcs_standard = vim.g.nvim_phpcs_config_phpcs_standard or phpcs_standard 14 | M.last_stderr = '' 15 | M.last_stdout = '' 16 | M.nvim_namespace = nil 17 | M.max_line_numbers = 1000 18 | 19 | M.phpcs_job_count = 0; 20 | 21 | M.detect_local_paths = function () 22 | if (lutils.file_exists('phpcs.xml')) then 23 | M.phpcs_standard = root .. '/phpcs.xml' 24 | end 25 | 26 | if (lutils.file_exists('vendor/bin/phpcs')) then 27 | M.phpcs_path = root .. '/vendor/bin/phpcs' 28 | end 29 | 30 | if (lutils.file_exists('vendor/bin/phpcbf')) then 31 | M.phpcbf_path = root .. '/vendor/bin/phpcbf' 32 | end 33 | 34 | M.nvim_namespace = vim.api.nvim_create_namespace("phpcs") 35 | end 36 | 37 | M.cs = function () 38 | if vim.fn.executable(M.phpcs_path) == 0 then 39 | return 40 | end 41 | 42 | if M.phpcs_job_count >= 2 then 43 | return 44 | end 45 | 46 | M.phpcs_job_count = M.phpcs_job_count + 1 47 | 48 | if M.phpcs_job_count > 1 then 49 | return 50 | end 51 | 52 | local bufnr = vim.api.nvim_get_current_buf() 53 | 54 | local report_file = os.tmpname(); 55 | 56 | local args = { 57 | "--stdin-path=" .. vim.api.nvim_buf_get_name(bufnr), 58 | "--report=json", 59 | "--report-file=" .. report_file, 60 | } 61 | if M.phpcs_standard then 62 | table.insert(args, "--standard=" .. M.phpcs_standard) 63 | end 64 | table.insert(args, "-") 65 | 66 | local opts = { 67 | command = M.phpcs_path, 68 | args = args, 69 | writer = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true), 70 | on_exit = vim.schedule_wrap(function() 71 | local file = io.open(report_file, "r") 72 | if file ~= nil then 73 | local content = file:read("*a") 74 | M.publish_diagnostic(content, bufnr) 75 | end 76 | 77 | M.phpcs_job_count = M.phpcs_job_count - 1 78 | 79 | if M.phpcs_job_count > 0 then 80 | M.phpcs_job_count = 0 81 | M.cs() 82 | end 83 | end), 84 | } 85 | 86 | Job:new(opts):start() 87 | end 88 | 89 | --[[ 90 | -- new_opts = { 91 | bufnr = 0, -- Buffer no. defaults to current 92 | force = false, -- Ignore file size 93 | timeout = 1000, -- Timeout in ms for the job. Default 1000ms 94 | } 95 | ]] 96 | M.cbf = function (new_opts) 97 | if vim.fn.executable(M.phpcbf_path) == 0 then 98 | return 99 | end 100 | 101 | new_opts = new_opts or {} 102 | new_opts.bufnr = new_opts.bufnr or vim.api.nvim_get_current_buf() 103 | 104 | if not new_opts.force then 105 | if M.max_line_numbers and vim.api.nvim_buf_line_count(new_opts.bufnr) > M.max_line_numbers then 106 | -- print("File too large. Ignoring code beautifier" ) 107 | return 108 | end 109 | end 110 | local args = {} 111 | if M.phpcs_standard then 112 | table.insert(args, "--standard=" .. M.phpcs_standard) 113 | 114 | end 115 | table.insert(args, vim.api.nvim_buf_get_name(new_opts.bufnr)) 116 | 117 | local opts = { 118 | command = M.phpcbf_path, 119 | args = args, 120 | on_exit = vim.schedule_wrap(function(j) 121 | if j.code ~= 0 then 122 | vim.cmd("e") 123 | end 124 | end), 125 | cwd = vim.fn.getcwd(), 126 | } 127 | 128 | Job:new(opts):start() 129 | end 130 | 131 | M.publish_diagnostic = function (results, bufnr) 132 | bufnr = bufnr or vim.api.nvim_get_current_buf() 133 | 134 | local diagnostics = parse_json(results, bufnr) 135 | 136 | vim.diagnostic.set(M.nvim_namespace, bufnr, diagnostics) 137 | end 138 | 139 | function parse_json(encoded, bufnr) 140 | local decoded = vim.json.decode(encoded) 141 | local diagnostics = {} 142 | local uri = vim.fn.bufname(bufnr); 143 | 144 | local error_codes = { 145 | ['error'] = vim.lsp.protocol.DiagnosticSeverity.Error, 146 | warning = vim.lsp.protocol.DiagnosticSeverity.Warning, 147 | } 148 | 149 | if not decoded.files[uri] then 150 | return diagnostics 151 | end 152 | 153 | for _, message in ipairs(decoded.files[uri].messages) do 154 | table.insert(diagnostics, { 155 | severity = error_codes[string.lower(message.type)], 156 | lnum = tonumber(message.line) -1, 157 | col = tonumber(message.column) -1, 158 | message = message.message 159 | }) 160 | end 161 | 162 | return diagnostics 163 | end 164 | 165 | 166 | M.detect_local_paths() 167 | 168 | --- Setup and configure nvim-phpcsf 169 | --- 170 | --- @param opts table|nil 171 | --- - phpcs (string|nil): 172 | --- PHPCS path 173 | --- - phpcbf (string|nil): 174 | --- PHPCBF path 175 | --- - standard (string|nil): 176 | --- PHPCS standard 177 | M.setup = function (opts) 178 | if opts == nil then 179 | M.detect_local_paths() 180 | return 181 | end 182 | 183 | if opts.phpcs ~= nil then 184 | M.phpcs_path = opts.phpcs 185 | end 186 | 187 | if opts.phpcbf ~= nil then 188 | M.phpcbf_path = opts.phpcbf 189 | end 190 | 191 | if opts.standard ~= nil then 192 | M.phpcs_standard = opts.standard 193 | end 194 | 195 | if opts.max_line_numbers ~= nil then 196 | M.max_line_numbers = opts.max_line_numbers 197 | end 198 | end 199 | 200 | return M 201 | -------------------------------------------------------------------------------- /lua/phpcs/utils.lua: -------------------------------------------------------------------------------- 1 | function backticks_table(cmd) 2 | local tab = {} 3 | local pipe = assert(io.popen(cmd), 4 | "backticks_table(" .. cmd .. ") failed.") 5 | local line = pipe:read("*line") 6 | while line do 7 | table.insert(tab, line) 8 | line = pipe:read("*line") 9 | end 10 | return tab 11 | end 12 | 13 | function split(s, delimiter) 14 | result = {} 15 | for match in (s..delimiter):gmatch("(.-)"..delimiter) do 16 | table.insert(result, all_trim(match)) 17 | end 18 | return result; 19 | end 20 | 21 | function file_exists(filename) 22 | local stat = vim.loop.fs_stat(vim.loop.cwd() .. '/' ..filename) 23 | return stat and stat.type == 'file' 24 | end 25 | 26 | function all_trim(s) 27 | return s:match( "^%s*(.-)%s*$" ) 28 | end 29 | 30 | function close_handle(handle) 31 | if not handle:is_closing() then handle:close() end 32 | end 33 | 34 | function implode(delimiter, list) 35 | local len = #list 36 | if len == 0 then 37 | return "" 38 | end 39 | local string = list[1] 40 | for i = 2, len do 41 | string = string .. delimiter .. list[i] 42 | end 43 | return string 44 | end 45 | 46 | return { 47 | backticks_table = backticks_table, 48 | split = split, 49 | implode = implode, 50 | close_handle = close_handle, 51 | is_empty = function (s) 52 | return s == nil or s == '' 53 | end, 54 | file_exists = file_exists 55 | } 56 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 |