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() -
@pbarney ,
That’s similar to my PythonScript that will compile/error-check Python or Perl source code whenever those filetypes are saved:
nppOnSaveSyntaxCheck.py– it was inspired by @dail’s script for Lua linting using LuaScript here (thanks to a mention by @Alan-Kilborn here).For Perl, I use
perl -c(or, actually,wperl -c, to not spawn a temporary console window). For Python – back in the time I wrote it, I only made it work on Python2 code, because I was using PythonScript 2. (At some point, I should modify the logic, since I use PythonScript3 almost-exclusively now.) But in that code, I use the compile function (which should still work in Python 3 code, per here)Yours doesn’t appear to use Annotations, but as shown in Alan’s screenshot, showing the linter errors inline with the code makes it especially useful (IMO).

-
Very useful script — thanks for sharing! 👏 Running the PHP linter directly from Notepad++ saves a lot of time. Maybe adding an option to auto-detect php.exe path or handle PHP 8+ output formatting could make it even more versatile. Great work overall!
-
@pbarney said:
it needs a lot of scaffolding to handle all the edge cases (saved/unsaved file, command line argument quoting, font encoding
Re unsaved file: You could have simply had the script abort if the active file is unsaved; with a message to the user of course.
Re encoding: If you’d have used PythonScript3 I think encoding issues would have been minimal.
The above are not complaints (negatives) against your script, just comments.
The script works for you…so Great Work!
-
@PeterJones Oh, thank you! I had no idea about Annotations. Can’t believe I’ve used NP++ this long and have never run into it.
It’s perfect because with the way it’s currently written, it opens up the Pythonscript console window to report results. But since there’s no keyboard shortcut to close the console, I need to grab my mouse and find that little X and click on it to get my screen real estate back. With Annotations. there appears to be none of that. By the way, how do you close annotations?
I also like your idea for linting on save, that’s better than my implementation.
The only issue I see with using Annotations is if the linter runs and everything passes, the user won’t get any feedback, which is fine I suppose, but I might find myself wondering from time to time if the linter was actually running.
I first began writing in Python 3, but from what I understand, Pythonscript comes with 2.8 by default (I may be wrong about this), so I opted to recode for that.
Thank you for the feedback!
-
@Thomas-Anderson said in PHP Linter PythonScript:
Maybe adding an option to auto-detect php.exe path
In a way, it more or less does by just using the
php.exethat’s in the PATH (it mentions this in the comments). But I think I get your point: if PHP isn’t in your path, you need to modify the script.I suppose it would be useful to be able to have the user specify another location in case they’re testing multiple versions of PHP, but I doubt I’ll take it that far.
or handle PHP 8+ output formatting could make it even more versatile. Great work overall!
What do you mean by this?
Thank you for the feedback!
-
@Alan-Kilborn said:
Re unsaved file: You could have simply had the script abort if the active file is unsaved; with a message to the user of course.
It does do this, and the code is minimal. What I meant by all the extra “scaffolding” is covering the edge cases like this. At first glance, it seems that all you’d really need for a linter to work is a single line that passes the path to
php -land echoing the output, but the script has more than one line, and that’s what I meant.Re encoding: If you’d have used PythonScript3 I think encoding issues would have been minimal.
I may misunderstand, but my impression is that Pythonscript ships with 2.8, and I wanted to be able to cover as many installations as possible right out of the box.
The above are not complaints
All the same, complaints are welcome as well, as always. Since I’ve always been a solo developer, negative feedback from better devs is one of the main ways I can grow, and I always appreciate it.
Thank you for your insights!
-
@pbarney said in PHP Linter PythonScript:
my impression is that Pythonscript ships with 2.8
From Plugins Admin, PythonScript v2.1.0 uses Python v2.7.18 (as far as I am aware, there is no such thing as Python v2.8).
However, if you go to the PythonScript releases page, you will see that there is also an “alpha” track of PythonScript v3.0.x (currently at 3.0.24), which use Python v3.12.xx. This is the route I personally recommend (caveat below).
The maintainer of PythonScript has chosen not to take PythonScript 3 out of “alpha” until it fully supports non-unicode encodings (ie, until it supports the 1980s-style character set encodings), so until that point, to get a maintained version of the Python interpreter for PythonScript, you have to manually install an alpha version of PythonScript – essentially, you have to put up with legacy syntax of Python if you want to deal with legacy non-unicode encodings of text. I personally have no need of supporting 80s technology for file encodings (and I pity anyone for whom their circumstances demand that they deal with file encodings that haven’t been brought into this millennium anytime in the last quarter century), so I use the 2020s version of Python for my PythonScript. (My personal opinion is that PythonScript should start shipping PythonScript 3 as non-alpha, and that they just prominently make it clear that if you have to deal with the old character sets that you should instead install the outdated PythonScript2 version from the releases page. But my opinion on such things holds no weight.)