PHP Linter PythonScript
-
If you happen to write PHP code, it can be helpful to run PHP’s built-in linter (syntax checker) on your code (
php -r file.php).If you have the NP++ PythonScript plugin installed, you can use this script to do the job. I assigned it to a hotkey to make it quick and easy to run.
Honestly, it seems like this should be a simple thing to do, but it needs a lot of scaffolding to handle all the edge cases (saved/unsaved file, command line argument quoting, font encoding, etc).
Positive and negative feedback is very much appreciated.
# -*- coding: utf-8 -*- # phpLinter.py - Runs PHP's built-in syntax checker on the current # Notepad++ file and shows the results in the PythonScript console. # If the linter reports an error, the editor's cursor will be move # to the specified line. # # - Your file must be saved (you'll be prompted to save it if it's not). # - php.exe must be in your PATH. # - Works with the PythonScript plugin's Python 2.7 runtime # - If you set ASK_BEFORE_SAVE to False, the script will automatically # save your file prior to linting. import subprocess import re import os import sys import locale from Npp import * # --- user prefs --- NEWLINE_MODE = "remove" # how to modify PHP output; options: "keep", "collapse", "remove" ASK_BEFORE_SAVE = True # True = message box before saving DISPLAY_RESULT = False # True = show result after PHP output # ------------------- # --- enum fallbacks for older PythonScript builds --- try: IDYES = MESSAGEBOXRESULTS.IDYES IDNO = MESSAGEBOXRESULTS.IDNO except Exception: IDYES, IDNO = 6, 7 try: MB_YESNO = MESSAGEBOXFLAGS.YESNO MB_ICONQ = MESSAGEBOXFLAGS.ICONQUESTION except Exception: MB_YESNO, MB_ICONQ = 0x00000004, 0x00000020 # ---------------------------------------------------- def _nl(s=u""): console.write((s if isinstance(s, unicode) else s.decode('utf-8','replace')) + u"\r\n") def _quote_arg(arg): # simple Windows/posix-safe echo quoting for display only if isinstance(arg, unicode): a = arg else: try: a = arg.decode('utf-8') except Exception: a = unicode(arg, errors='replace') if re.search(ur'[\s"]', a): return u'"' + a.replace(u'"', u'\\"') + u'"' return a def _to_unicode(b): # convert subprocess output to unicode safely under Py2 if isinstance(b, unicode): return b enc = locale.getpreferredencoding() or 'utf-8' try: return b.decode(enc, 'replace') except Exception: return b.decode('utf-8', 'replace') def normalize_output(s, mode): if mode == "keep": return s if mode == "remove": # delete any line that is empty or whitespace-only return re.sub(ur'(?m)^[ \t]*\r?\n', u'', s) if mode == "collapse": # collapse runs of blank/whitespace-only lines to a single newline return re.sub(ur'(?m)(?:^[ \t]*\r?\n){2,}', u'\r\n', s) # fallback return s def run_php_lint(): path = notepad.getCurrentFilename() is_real_file = path and os.path.isfile(path) # ask before saving (optional) if ASK_BEFORE_SAVE and editor.getModify(): ans = notepad.messageBox( "Save the current file before linting?", "PHP Lint", MB_YESNO | MB_ICONQ ) if ans != IDYES: console.show() _nl(u"Cancelled: file not saved, lint not run.") return # save (will open "Save As..." for new/unsaved docs) notepad.save() # re-evaluate path after save (user may have cancelled Save As) path = notepad.getCurrentFilename() if not path or not os.path.isfile(path): console.show() _nl(u"No saved file to lint (did you cancel Save As?)") return # make sure we use a unicode path for display and pass bytes to Popen cmd = ["php", "-l", path] try: p = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False ) out_b, err_b = p.communicate() rc = p.returncode except Exception as e: console.show() _nl(u"Failed to execute php -l:") _nl(unicode(e)) return raw = _to_unicode(out_b or u"") + _to_unicode(err_b or u"") output = normalize_output(raw, NEWLINE_MODE) console.show() _nl(u"-" * 80) # show the command we ran _nl(u">>> " + u" ".join([_quote_arg(a if isinstance(a, unicode) else a.decode('utf-8','ignore')) for a in cmd])) if output.strip(): # write exactly what PHP printed console.write(output if isinstance(output, unicode) else _to_unicode(output)) # result summary if DISPLAY_RESULT: prefix = u"" if NEWLINE_MODE == "remove" else u"\n" if rc == 0: _nl(prefix + u"Result: OK (exit code 0)") elif rc == 255: _nl(prefix + u"Result: Syntax error (exit code 255)") else: _nl(prefix + u"Result: php -l exited with code {0}".format(rc)) # jump to the reported line if present m = re.search(ur'on line (\d+)', raw) if m: try: line = int(m.group(1)) - 1 if line >= 0: editor.gotoLine(line) except Exception: pass if __name__ == '__main__': run_php_lint() # give keyboard focus back to the editing pane editor.grabFocus()