Community
    • Login

    PHP Linter PythonScript

    Scheduled Pinned Locked Moved General Discussion
    8 Posts 4 Posters 380 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • pbarneyP
      pbarney
      last edited by

      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()
      
      PeterJonesP Alan KilbornA 2 Replies Last reply Reply Quote 0
      • PeterJonesP
        PeterJones @pbarney
        last edited by

        @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).

        pbarneyP 1 Reply Last reply Reply Quote 4
        • Thomas AndersonT
          Thomas Anderson
          last edited by

          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!

          pbarneyP 1 Reply Last reply Reply Quote 1
          • Alan KilbornA
            Alan Kilborn @pbarney
            last edited by

            @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!

            pbarneyP 1 Reply Last reply Reply Quote 1
            • pbarneyP
              pbarney @PeterJones
              last edited by

              @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!

              1 Reply Last reply Reply Quote 0
              • pbarneyP
                pbarney @Thomas Anderson
                last edited by

                @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.exe that’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!

                1 Reply Last reply Reply Quote 0
                • pbarneyP
                  pbarney @Alan Kilborn
                  last edited by

                  @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 -l and 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!

                  PeterJonesP 1 Reply Last reply Reply Quote 0
                  • PeterJonesP
                    PeterJones @pbarney
                    last edited by PeterJones

                    @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.)

                    1 Reply Last reply Reply Quote 2
                    • First post
                      Last post
                    The Community of users of the Notepad++ text editor.
                    Powered by NodeBB | Contributors