413 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			413 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # -*- coding: utf-8 -*-
 | |
| # base_linter.py - base class for linters
 | |
| 
 | |
| import os
 | |
| import os.path
 | |
| import json
 | |
| import re
 | |
| import subprocess
 | |
| 
 | |
| import sublime
 | |
| 
 | |
| # If the linter uses an executable that takes stdin, use this input method.
 | |
| INPUT_METHOD_STDIN = 1
 | |
| 
 | |
| # If the linter uses an executable that does not take stdin but you wish to use
 | |
| # a temp file so that the current view can be linted interactively, use this input method.
 | |
| # If the current view has been saved, the tempfile will have the same name as the
 | |
| # view's file, which is necessary for some linters.
 | |
| INPUT_METHOD_TEMP_FILE = 2
 | |
| 
 | |
| # If the linter uses an executable that does not take stdin and you wish to have
 | |
| # linting occur only on file load and save, use this input method.
 | |
| INPUT_METHOD_FILE = 3
 | |
| 
 | |
| CONFIG = {
 | |
|     # The display language name for this linter.
 | |
|     'language': '',
 | |
| 
 | |
|     # Linters may either use built in code or use an external executable. This item may have
 | |
|     # one of the following values:
 | |
|     #
 | |
|     #   string - An external command (or path to a command) to execute
 | |
|     #   None - The linter is considered to be built in
 | |
|     #
 | |
|     # Alternately, your linter class may define the method get_executable(),
 | |
|     # which should return the three-tuple (<enabled>, <executable>, <message>):
 | |
|     #   <enabled> must be a boolean than indicates whether the executable is available and usable.
 | |
|     #   If <enabled> is True, <executable> must be one of:
 | |
|     #       - A command string (or path to a command) if an external executable will be used
 | |
|     #       - None if built in code will be used
 | |
|     #       - False if no suitable executable can be found or the linter should be disabled
 | |
|     #         for some other reason.
 | |
|     #   <message> is the message that will be shown in the console when the linter is
 | |
|     #   loaded, to aid the user in knowing what the status of the linter is. If None or an empty string,
 | |
|     #   a default message will be returned based on the value of <executable>. Otherwise it
 | |
|     #   must be a string.
 | |
|     'executable': None,
 | |
| 
 | |
|     # If an external executable is being used, this item specifies the arguments
 | |
|     # used when checking the existence of the executable to determine if the linter can be enabled.
 | |
|     # If more than one argument needs to be passed, use a tuple/list.
 | |
|     # Defaults to '-v' if this item is missing.
 | |
|     'test_existence_args': '-v',
 | |
| 
 | |
|     # If an external executable is being used, this item specifies the arguments to be passed
 | |
|     # when linting. If there is more than one argument, use a tuple/list.
 | |
|     # If the input method is anything other than INPUT_METHOD_STDIN, put a {filename} placeholder in
 | |
|     # the args where the filename should go.
 | |
|     #
 | |
|     # Alternately, if your linter class may define the method get_lint_args(), which should return
 | |
|     # None for no arguments or a tuple/list for one or more arguments.
 | |
|     'lint_args': None,
 | |
| 
 | |
|     # If an external executable is being used, the method used to pass input to it. Defaults to STDIN.
 | |
|     'input_method': INPUT_METHOD_STDIN
 | |
| }
 | |
| 
 | |
| TEMPFILES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__.encode('utf-8')), u'..', u'.tempfiles'))
 | |
| 
 | |
| JSON_MULTILINE_COMMENT_RE = re.compile(r'\/\*[\s\S]*?\*\/')
 | |
| JSON_SINGLELINE_COMMENT_RE = re.compile(r'\/\/[^\n\r]*')
 | |
| 
 | |
| if not os.path.exists(TEMPFILES_DIR):
 | |
|     os.mkdir(TEMPFILES_DIR)
 | |
| 
 | |
| 
 | |
| class BaseLinter(object):
 | |
|     '''A base class for linters. Your linter module needs to do the following:
 | |
| 
 | |
|             - Set the relevant values in CONFIG
 | |
|             - Override built_in_check() if it uses a built in linter. You may return
 | |
|               whatever value you want, this value will be passed to parse_errors().
 | |
|             - Override parse_errors() and populate the relevant lists/dicts. The errors
 | |
|               argument passed to parse_errors() is the output of the executable run through strip().
 | |
| 
 | |
|        If you do subclass and override __init__, be sure to call super(MyLinter, self).__init__(config).
 | |
|     '''
 | |
| 
 | |
|     JSC_PATH = '/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc'
 | |
| 
 | |
|     LIB_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__.encode('utf-8')), u'libs'))
 | |
| 
 | |
|     JAVASCRIPT_ENGINES = ['node', 'jsc']
 | |
|     JAVASCRIPT_ENGINE_NAMES = {'node': 'node.js', 'jsc': 'JavaScriptCore'}
 | |
|     JAVASCRIPT_ENGINE_WRAPPERS_PATH = os.path.join(LIB_PATH, 'jsengines')
 | |
| 
 | |
|     def __init__(self, config):
 | |
|         self.language = config['language']
 | |
|         self.enabled = False
 | |
|         self.executable = config.get('executable', None)
 | |
|         self.test_existence_args = config.get('test_existence_args', ['-v'])
 | |
|         self.js_engine = None
 | |
| 
 | |
|         if isinstance(self.test_existence_args, basestring):
 | |
|             self.test_existence_args = (self.test_existence_args,)
 | |
| 
 | |
|         self.input_method = config.get('input_method', INPUT_METHOD_STDIN)
 | |
|         self.filename = None
 | |
|         self.lint_args = config.get('lint_args', [])
 | |
| 
 | |
|         if isinstance(self.lint_args, basestring):
 | |
|             self.lint_args = [self.lint_args]
 | |
| 
 | |
|     def check_enabled(self, view):
 | |
|         if hasattr(self, 'get_executable'):
 | |
|             try:
 | |
|                 self.enabled, self.executable, message = self.get_executable(view)
 | |
| 
 | |
|                 if self.enabled and not message:
 | |
|                     message = 'using "{0}"'.format(self.executable) if self.executable else 'built in'
 | |
|             except Exception as ex:
 | |
|                 self.enabled = False
 | |
|                 message = unicode(ex)
 | |
|         else:
 | |
|             self.enabled, message = self._check_enabled(view)
 | |
| 
 | |
|         return (self.enabled, message or '<unknown reason>')
 | |
| 
 | |
|     def _check_enabled(self, view):
 | |
|         if self.executable is None:
 | |
|             return (True, 'built in')
 | |
|         elif isinstance(self.executable, basestring):
 | |
|             self.executable = self.get_mapped_executable(view, self.executable)
 | |
|         elif isinstance(self.executable, bool) and self.executable == False:
 | |
|             return (False, 'unknown error')
 | |
|         else:
 | |
|             return (False, 'bad type for CONFIG["executable"]')
 | |
| 
 | |
|         # If we get this far, the executable is external. Test that it can be executed
 | |
|         # and capture stdout and stderr so they don't end up in the system log.
 | |
|         try:
 | |
|             args = [self.executable]
 | |
|             args.extend(self.test_existence_args)
 | |
|             subprocess.Popen(args, startupinfo=self.get_startupinfo(),
 | |
|                              stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()
 | |
|         except OSError:
 | |
|             return (False, '"{0}" cannot be found'.format(self.executable))
 | |
| 
 | |
|         return (True, 'using "{0}" for executable'.format(self.executable))
 | |
| 
 | |
|     def _get_lint_args(self, view, code, filename):
 | |
|         if hasattr(self, 'get_lint_args'):
 | |
|             return self.get_lint_args(view, code, filename) or []
 | |
|         else:
 | |
|             lintArgs = self.lint_args or []
 | |
|             settings = view.settings().get('SublimeLinter', {}).get(self.language, {})
 | |
| 
 | |
|             if settings:
 | |
|                 args = settings.get('lint_args', [])
 | |
|                 lintArgs.extend(args)
 | |
| 
 | |
|                 cwd = settings.get('working_directory').encode('utf-8')
 | |
| 
 | |
|                 if cwd and os.path.isabs(cwd) and os.path.isdir(cwd):
 | |
|                     os.chdir(cwd)
 | |
| 
 | |
|             return [arg.format(filename=filename) for arg in lintArgs]
 | |
| 
 | |
|     def built_in_check(self, view, code, filename):
 | |
|         return ''
 | |
| 
 | |
|     def executable_check(self, view, code, filename):
 | |
|         args = [self.executable]
 | |
|         tempfilePath = None
 | |
| 
 | |
|         if self.input_method == INPUT_METHOD_STDIN:
 | |
|             args.extend(self._get_lint_args(view, code, filename))
 | |
| 
 | |
|         elif self.input_method == INPUT_METHOD_TEMP_FILE:
 | |
|             if filename:
 | |
|                 filename = os.path.basename(filename)
 | |
|             else:
 | |
|                 filename = u'view{0}'.format(view.id())
 | |
| 
 | |
|             tempfilePath = os.path.join(TEMPFILES_DIR, filename)
 | |
| 
 | |
|             with open(tempfilePath, 'w') as f:
 | |
|                 f.write(code)
 | |
| 
 | |
|             args.extend(self._get_lint_args(view, code, tempfilePath))
 | |
|             code = u''
 | |
| 
 | |
|         elif self.input_method == INPUT_METHOD_FILE:
 | |
|             args.extend(self._get_lint_args(view, code, filename))
 | |
|             code = u''
 | |
| 
 | |
|         else:
 | |
|             return u''
 | |
| 
 | |
|         try:
 | |
|             process = subprocess.Popen(args,
 | |
|                                        stdin=subprocess.PIPE,
 | |
|                                        stdout=subprocess.PIPE,
 | |
|                                        stderr=subprocess.STDOUT,
 | |
|                                        startupinfo=self.get_startupinfo())
 | |
|             process.stdin.write(code)
 | |
|             result = process.communicate()[0]
 | |
|         finally:
 | |
|             if tempfilePath:
 | |
|                 os.remove(tempfilePath)
 | |
| 
 | |
|         return result.strip()
 | |
| 
 | |
|     def parse_errors(self, view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages):
 | |
|         pass
 | |
| 
 | |
|     def add_message(self, lineno, lines, message, messages):
 | |
|         # Assume lineno is one-based, ST2 wants zero-based line numbers
 | |
|         lineno -= 1
 | |
|         lines.add(lineno)
 | |
|         message = message[0].upper() + message[1:]
 | |
| 
 | |
|         # Remove trailing period from error message
 | |
|         if message[-1] == '.':
 | |
|             message = message[:-1]
 | |
| 
 | |
|         if lineno in messages:
 | |
|             messages[lineno].append(message)
 | |
|         else:
 | |
|             messages[lineno] = [message]
 | |
| 
 | |
|     def underline_range(self, view, lineno, position, underlines, length=1):
 | |
|         # Assume lineno is one-based, ST2 wants zero-based line numbers
 | |
|         lineno -= 1
 | |
|         line = view.full_line(view.text_point(lineno, 0))
 | |
|         position += line.begin()
 | |
| 
 | |
|         for i in xrange(length):
 | |
|             underlines.append(sublime.Region(position + i))
 | |
| 
 | |
|     def underline_regex(self, view, lineno, regex, lines, underlines, wordmatch=None, linematch=None):
 | |
|         # Assume lineno is one-based, ST2 wants zero-based line numbers
 | |
|         lineno -= 1
 | |
|         lines.add(lineno)
 | |
|         offset = 0
 | |
|         line = view.full_line(view.text_point(lineno, 0))
 | |
|         lineText = view.substr(line)
 | |
| 
 | |
|         if linematch:
 | |
|             match = re.match(linematch, lineText)
 | |
| 
 | |
|             if match:
 | |
|                 lineText = match.group('match')
 | |
|                 offset = match.start('match')
 | |
|             else:
 | |
|                 return
 | |
| 
 | |
|         iters = re.finditer(regex, lineText)
 | |
|         results = [(result.start('underline'), result.end('underline')) for result in iters
 | |
|                     if not wordmatch or result.group('underline') == wordmatch]
 | |
| 
 | |
|         # Make the lineno one-based again for underline_range
 | |
|         lineno += 1
 | |
| 
 | |
|         for start, end in results:
 | |
|             self.underline_range(view, lineno, start + offset, underlines, end - start)
 | |
| 
 | |
|     def underline_word(self, view, lineno, position, underlines):
 | |
|         # Assume lineno is one-based, ST2 wants zero-based line numbers
 | |
|         lineno -= 1
 | |
|         line = view.full_line(view.text_point(lineno, 0))
 | |
|         position += line.begin()
 | |
| 
 | |
|         word = view.word(position)
 | |
|         underlines.append(word)
 | |
| 
 | |
|     def run(self, view, code, filename=None):
 | |
|         self.filename = filename
 | |
| 
 | |
|         if self.executable is None:
 | |
|             errors = self.built_in_check(view, code, filename)
 | |
|         else:
 | |
|             errors = self.executable_check(view, code, filename)
 | |
| 
 | |
|         lines = set()
 | |
|         errorUnderlines = []  # leave this here for compatibility with original plugin
 | |
|         errorMessages = {}
 | |
|         violationUnderlines = []
 | |
|         violationMessages = {}
 | |
|         warningUnderlines = []
 | |
|         warningMessages = {}
 | |
| 
 | |
|         self.parse_errors(view, errors, lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages)
 | |
|         return lines, errorUnderlines, violationUnderlines, warningUnderlines, errorMessages, violationMessages, warningMessages
 | |
| 
 | |
|     def get_mapped_executable(self, view, default):
 | |
|         map = view.settings().get('sublimelinter_executable_map')
 | |
| 
 | |
|         if map:
 | |
|             lang = self.language.lower()
 | |
| 
 | |
|             if lang in map:
 | |
|                 return map[lang].encode('utf-8')
 | |
| 
 | |
|         return default
 | |
| 
 | |
|     def get_startupinfo(self):
 | |
|         info = None
 | |
| 
 | |
|         if os.name == 'nt':
 | |
|             info = subprocess.STARTUPINFO()
 | |
|             info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
 | |
|             info.wShowWindow = subprocess.SW_HIDE
 | |
| 
 | |
|         return info
 | |
| 
 | |
|     def execute_get_output(self, args):
 | |
|         try:
 | |
|             return subprocess.Popen(args, self.get_startupinfo()).communicate()[0]
 | |
|         except:
 | |
|             return ''
 | |
| 
 | |
|     def jsc_path(self):
 | |
|         '''Return the path to JavaScriptCore. Use this method in case the path
 | |
|            has to be dynamically calculated in the future.'''
 | |
|         return self.JSC_PATH
 | |
| 
 | |
|     def find_file(self, filename, view):
 | |
|         '''Find a file with the given name, starting in the view's directory,
 | |
|            then ascending the file hierarchy up to root.'''
 | |
|         path = view.file_name().encode('utf-8')
 | |
| 
 | |
|         # quit if the view is temporary
 | |
|         if not path:
 | |
|             return None
 | |
| 
 | |
|         dirname = os.path.dirname(path)
 | |
| 
 | |
|         while True:
 | |
|             path = os.path.join(dirname, filename)
 | |
| 
 | |
|             if os.path.isfile(path):
 | |
|                 with open(path, 'r') as f:
 | |
|                     return f.read()
 | |
| 
 | |
|             # if we hit root, quit
 | |
|             parent = os.path.dirname(dirname)
 | |
| 
 | |
|             if parent == dirname:
 | |
|                 return None
 | |
|             else:
 | |
|                 dirname = parent
 | |
| 
 | |
|     def strip_json_comments(self, json_str):
 | |
|         stripped_json = JSON_MULTILINE_COMMENT_RE.sub('', json_str)
 | |
|         stripped_json = JSON_SINGLELINE_COMMENT_RE.sub('', stripped_json)
 | |
|         return json.dumps(json.loads(stripped_json))
 | |
| 
 | |
|     def get_javascript_args(self, view, linter, code):
 | |
|         path = os.path.join(self.LIB_PATH, linter)
 | |
|         options = self.get_javascript_options(view)
 | |
| 
 | |
|         if options == None:
 | |
|             options = json.dumps(view.settings().get('%s_options' % linter) or {})
 | |
| 
 | |
|         self.get_javascript_engine(view)
 | |
|         engine = self.js_engine
 | |
| 
 | |
|         if (engine['name'] == 'jsc'):
 | |
|             args = [engine['wrapper'], '--', path + os.path.sep, str(code.count('\n')), options]
 | |
|         else:
 | |
|             args = [engine['wrapper'], path + os.path.sep, options]
 | |
| 
 | |
|         return args
 | |
| 
 | |
|     def get_javascript_options(self, view):
 | |
|         '''Subclasses should override this if they want to provide options
 | |
|            for a Javascript-based linter. If the subclass cannot provide
 | |
|            options, it should return None (or not return anything).'''
 | |
|         return None
 | |
| 
 | |
|     def get_javascript_engine(self, view):
 | |
|         if self.js_engine == None:
 | |
|             for engine in self.JAVASCRIPT_ENGINES:
 | |
|                 if engine == 'node':
 | |
|                     try:
 | |
|                         path = self.get_mapped_executable(view, 'node')
 | |
|                         subprocess.call([path, u'-v'], startupinfo=self.get_startupinfo())
 | |
|                         self.js_engine = {
 | |
|                             'name': engine,
 | |
|                             'path': path,
 | |
|                             'wrapper': os.path.join(self.JAVASCRIPT_ENGINE_WRAPPERS_PATH, engine + '.js'),
 | |
|                         }
 | |
|                         break
 | |
|                     except OSError:
 | |
|                         pass
 | |
| 
 | |
|                 elif engine == 'jsc':
 | |
|                     if os.path.exists(self.jsc_path()):
 | |
|                         self.js_engine = {
 | |
|                             'name': engine,
 | |
|                             'path': self.jsc_path(),
 | |
|                             'wrapper': os.path.join(self.JAVASCRIPT_ENGINE_WRAPPERS_PATH, engine + '.js'),
 | |
|                         }
 | |
|                         break
 | |
| 
 | |
|         if self.js_engine != None:
 | |
|             return (True, self.js_engine['path'], 'using {0}'.format(self.JAVASCRIPT_ENGINE_NAMES[self.js_engine['name']]))
 | |
| 
 | |
|         # Didn't find an engine, tell the user
 | |
|         engine_list = ', '.join(self.JAVASCRIPT_ENGINE_NAMES.values())
 | |
|         return (False, '', 'One of the following Javascript engines must be installed: ' + engine_list)
 |