def parse_message(self, message): """ Parse message object into standard elements of an error and return them. """ error_message = message['message'] line = message['line'] - 1 col = message['col'] # set error and warning flags based on message type error = None warning = None if message['type'] == 'error': error = True warning = False elif message['type'] == 'warning': error = False warning = True elif message['type'] == 'info': # ignore info messages by setting message to None message = None persist.debug('message -- msg:"{}", line:{}, col:{}, error: {}, warning: {}, message_obj:{}'.format(error_message, line, col, error, warning, message)) return message, line, col, error, warning, error_message, None
def is_current_file(self, working_dir, matched_file): """ Return true if `matched_file` is logically the same file as `self.filename`. Cargo example demonstrating how matching is done: - os.getcwd() = '/Applications/Sublime Text.app/Contents/MacOS' - `working_dir` = '/path/to/project' - `matched_file` = 'src/foo.rs' - `self.filename` = '/path/to/project/src/foo.rs' The current OS directory is not considered at all -- comparison is only done relative to where Cargo.toml was found. `os.path.realpath` is used to normalize the filenames so that they can be directly compared after manipulation. """ abs_matched_file = os.path.join(working_dir, matched_file) persist.debug('Sublime Text cwd: ', os.getcwd()) persist.debug('Build cwd: ', working_dir) persist.debug('Current filename: ', self.filename) persist.debug('Matched filename: ', matched_file) persist.debug('Compared filename: ', abs_matched_file) return os.path.realpath(self.filename) == os.path.realpath( abs_matched_file)
def find_errors(self, output): """ Convert flow's json output into a set of matches SublimeLinter can process. I'm not sure why find_errors isn't exposed in SublimeLinter's docs, but this would normally attempt to parse a regex and then return a generator full of sanitized matches. Instead, this implementation returns a list of errors processed by _error_to_tuple, ready for SublimeLinter to unpack """ try: # calling flow in a matching syntax without a `flowconfig` will cause the # output of flow to be an error message. catch and return [] check, coverage = json.loads(output) except ValueError: persist.debug('flow {}'.format(output)) return [] errors = check.get('errors', []) persist.debug('flow {} errors. passed: {}'.format( len(errors), check.get('passed', True) )) return chain( map(self._error_to_tuple, errors), map(self._uncovered_to_tuple, coverage.get('expressions', {}).get('uncovered_locs', []), repeat(set())) )
def get_cmd(self): """Add condition before calling foodcritic.""" cmd = super(RubyLinter, self).get_cmd() # Do nothing for unsaved files if self.view.is_dirty(): persist.debug("foodcritic: Can't handle unsaved files, skipping") return False return cmd
def run(self, cmd, code): """ Flow lint code if `@flow` pragma is present. if not present, this method noops """ _flow_comment_re = r'\@flow' if not re.search(_flow_comment_re, code) \ and not self._inline_setting_bool('all'): persist.debug("did not find @flow pragma") return '' persist.debug("found flow pragma!") check = super().run(cmd, code) coverage = super().run(_build_coverage_cmd(cmd), code) \ if self._inline_setting_bool('coverage') else '{}' return '[%s,%s]' % (check, coverage)
def schema_path(self): self.schema_exists = True try: xsi = 'http://www.w3.org/2001/XMLSchema-instance' root = minidom.parseString(self.code).documentElement schema = root.getAttributeNS(xsi, 'noNamespaceSchemaLocation') if schema: online = schema.find('http://') == 0 or schema.find('https://') == 0 if not (online or os.path.isabs(schema)): schema = os.path.dirname(self.filename) + '/' + schema if not online and not os.path.isfile(schema): self.schema_exists = False return None return schema except Exception as e: persist.debug(e) return None
def lint(self, path, content): """Lint the content of a file.""" output = "" try: persist.debug("Connecting to Julia lint server ({}, {})".format(self.address, self.port)) # noqa output = self._lint(path, content) except Exception as e: persist.debug(e) if not self.auto_start: persist.printf("Julia lint server is not running") else: # if self.proc is not None: # if self.proc.poll() is None: # persist.debug("Local Julia lint server was started") # return # else: # raise subprocess.SubprocessError(self.proc.returncode) persist.printf("Launching Julia lint server on localhost port {}".format(self.port)) # noqa self.start() persist.printf("Julia lint server starting up, server will be operational shortly") # noqa try: # Wait to give Julia time to start sleep(5) output = self._lint(path, content) except Exception as e: persist.debug(e) persist.printf("Julia lint server failed to start") else: persist.printf("Julia lint server now operational") return output
def cmd(self): """Return a list with the command line to execute.""" cmd = [ self.executable_path, '*', '--follow-imports=silent', # or 'skip' '--ignore-missing-imports', '--show-column-numbers', '--hide-error-context', '@' ] if self.tempfile_suffix == "-": # --shadow-file SOURCE_FILE SHADOW_FILE # # '@' needs to be the shadow file, # while we request the normal filename # to be checked in its normal environment. # Trying to be smart about view.is_dirty and optimizing '--shadow-file' away # doesn't work well with SublimeLinter internals. cmd[-1:] = ['--shadow-file', self.filename, '@', self.filename] # Add a temporary cache dir to the command if none was specified. # Helps keep the environment clean # by not littering everything with `.mypy_cache` folders. settings = self.get_view_settings() if not settings.get('cache-dir'): cwd = os.getcwd() if cwd in tmpdirs: cache_dir = tmpdirs[cwd].name else: tmp_dir = tempfile.TemporaryDirectory(prefix=TMPDIR_PREFIX) tmpdirs[cwd] = tmp_dir cache_dir = tmp_dir.name persist.debug("Created temporary cache dir at: " + cache_dir) cmd[1:1] = ["--cache-dir", cache_dir] return cmd
def is_current_file(self, working_dir, matched_file): """ Return true if `matched_file` is logically the same file as `self.filename`. Cargo example demonstrating how matching is done: - os.getcwd() = '/Applications/Sublime Text.app/Contents/MacOS' - `working_dir` = '/path/to/project' - `matched_file` = 'src/foo.rs' - `self.filename` = '/path/to/project/src/foo.rs' The current OS directory is not considered at all -- comparison is only done relative to where Cargo.toml was found. `os.path.realpath` is used to normalize the filenames so that they can be directly compared after manipulation. """ abs_matched_file = os.path.join(working_dir, matched_file) persist.debug('Sublime Text cwd: ', os.getcwd()) persist.debug('Build cwd: ', working_dir) persist.debug('Current filename: ', self.filename) persist.debug('Matched filename: ', matched_file) persist.debug('Compared filename: ', abs_matched_file) return os.path.realpath(self.filename) == os.path.realpath(abs_matched_file)
def _error_to_tuple(self, error): """ Map an array of flow error messages to a fake regex match tuple. flow returns errors like this: {message: [{<msg>},..,]} where <msg>:Object { descr: str, level: str, path: str, line: number, endline: number, start: number, end: number } Which means we can mostly avoid dealing with regex parsing since the flow devs have already done that for us. Thanks flow devs! """ error_messages = error.get('message', []) match = self.filename == error_messages[0]['path'] # TODO(nsfmc): `line_col_base` won't work b/c we avoid `split_match`'s codepath line = error_messages[0]['line'] - 1 col = error_messages[0]['start'] - 1 level = error_messages[0]['level'] is_error = level == 'error' is_warning = level == 'warning' combined_message = " ".join([m.get('descr', '') for m in error_messages]) near_match = re.search(self.__flow_near_re, combined_message) near = near_match.group('near') if near_match else None persist.debug('flow line: {}, col: {}, level: {}, message: {}'.format( line, col, level, combined_message)) return (match, line, col, is_error, is_warning, combined_message, near)
def split_match(self, match): """ Pre-process the matchObject before passing it upstream. Several reasons for this: * unrelated library files can throw errors, and we only want errors from the linted file. * our regex contains more than just the basic capture groups (filename, line, message, etc.) but we still need to pass a match object that contains the above groups upstream. * Line is not reported for some macro errors * etc.. """ dummy_match = None if match: captures = match.groupdict() dummy_string = self.build_dummy_string(captures) dummy_match = re.match(self.dummy_regex, dummy_string) if dummy_match: filename = os.path.join(self.chdir, dummy_match.group('filename')) persist.debug("Linted file: %s" % self.filename) persist.debug("Error source: %s" % filename) if self.filename != filename: persist.debug( "Ignore error from %s (linted file: %s)" % (filename, self.filename) ) dummy_match = None return super().split_match(dummy_match)
def build_dummy_string(self, captures): """ Build a string to be matched against self.dummy_regex. It is used to ensure that a matchObject with the appropriate group names is passed upstream. Returns a string with the following format: {filename}:{line}:{error_type}:{message} """ if captures['e_file1'] is not None: persist.debug('Error type 1') dummy_str = '%s:%s:%s:%s' % (captures['e_file1'], captures['e_line1'], 'error', captures['e_msg1']) elif captures['e_file2'] is not None: persist.debug('Error type 2') dummy_str = "%s:%s:%s:%s" % (captures['e_file2'], captures['e_line2'], 'error', captures['e_msg2']) elif captures['e_file3'] is not None: persist.debug('Error type 3') dummy_str = "%s:%s:%s:%s" % (captures['e_file3'], captures['e_line3'], 'error', captures['e_msg3']) elif captures['w_file1'] is not None: persist.debug('Warning type 1') dummy_str = "%s:%s:%s:%s" % (captures['w_file1'], captures['w_line1'], 'warning', captures['w_msg1']) elif captures['w_file2'] is not None: persist.debug('Warning type 2') dummy_str = "%s:%s:%s:%s" % (captures['w_file2'], captures['w_line2'], 'warning', captures['w_msg2']) else: persist.debug('No match') dummy_str = "" persist.debug("Dummy string: %s" % dummy_str) return dummy_str
def _error_to_tuple(self, error): """ Map an array of flow error messages to a fake regex match tuple. this is described in `flow/tsrc/flowResult.js` in the flow repo flow returns errors like this type FlowError = { kind: string, level: string, message: Array<FlowMessage>, trace: ?Array<FlowMessage>, operation?: FlowMessage, extra?: FlowExtra, }; type FlowMessage = { descr: string, type: "Blame" | "Comment", context?: ?string, loc?: ?FlowLoc, indent?: number, }; Which means we can mostly avoid dealing with regex parsing since the flow devs have already done that for us. Thanks flow devs! """ error_messages = error.get('message', []) # TODO(nsfmc): `line_col_base` won't work b/c we avoid `split_match`'s # codepath operation = error.get('operation', {}) loc = operation.get('loc') or error_messages[0].get('loc', {}) message = self._find_matching_msg_for_file(error) if message is None: return (False, 0, 0, False, False, '', None) error_context = message.get('context', '') loc = message.get('loc') message_start = loc.get('start', {}) message_end = loc.get('end', {}) line = message_start.get('line', None) if line: line -= 1 col = message_start.get('column', None) if col: col -= 1 end = message_end.get('column', None) # slice the error message from the context and loc positions # If error spans multiple lines, though, don't highlight them all # but highlight the 1st error character by passing None as near # SublimeLinter will strip quotes of `near` strings as documented in # http://www.sublimelinter.com/en/latest/linter_attributes.html#regex # In order to preserve quotes, we have to wrap strings with more # quotes. if end and line == (message_end.get('line') - 1): near = '"' + error_context[col:end] + '"' else: near = None level = error.get('level', False) is_error = level == 'error' is_warning = level == 'warning' combined_message = " ".join( [self._format_message(msg) for msg in error_messages] ).strip() persist.debug('flow line: {}, col: {}, level: {}, message: {}'.format( line, col, level, combined_message)) return (True, line, col, is_error, is_warning, combined_message, near)
def build_dummy_string(self, captures): """ Build a string to be matched against self.dummy_regex. It is used to ensure that a matchObject with the appropriate group names is passed upstream. Returns a string with the following format: {filename}:{line}:{error_type}:{message} """ if captures['e_file1'] is not None: persist.debug('Error type 1') dummy_str = '%s:%s:%s:%s' % ( captures['e_file1'], captures['e_line1'], 'error', captures['e_msg1'] ) elif captures['e_file2'] is not None: persist.debug('Error type 2') dummy_str = "%s:%s:%s:%s" % ( captures['e_file2'], captures['e_line2'], 'error', captures['e_msg2'] ) elif captures['e_file3'] is not None: persist.debug('Error type 3') dummy_str = "%s:%s:%s:%s" % ( captures['e_file3'], captures['e_line3'], 'error', captures['e_msg3'] ) elif captures['w_file1'] is not None: persist.debug('Warning type 1') dummy_str = "%s:%s:%s:%s" % ( captures['w_file1'], captures['w_line1'], 'warning', captures['w_msg1'] ) else: persist.debug('No match') dummy_str = "" persist.debug("Dummy string: %s" % dummy_str) return dummy_str
def context_sensitive_executable_path(self, cmd): """Try to find an executable for a given cmd.""" settings = self.get_view_settings() # If the user explicitly set an executable, it takes precedence. # We expand environment variables. E.g. a user could have a project # structure where a virtual environment is always located within # the project structure. She could then simply specify # `${project_path}/venv/bin/flake8`. Note that setting `@python` # to a path will have a similar effect. executable = settings.get('executable', '') if executable: executable = util.expand_variables(executable) persist.debug("{}: wanted executable is '{}'".format( self.name, executable)) if util.can_exec(executable): return True, executable persist.printf("ERROR: {} deactivated, cannot locate '{}' ".format( self.name, executable)) # no fallback, the user specified something, so we err return True, None # `@python` can be number or a string. If it is a string it should # point to a python environment, NOT a python binary. # We expand environment variables. E.g. a user could have a project # structure where virtual envs are located always like such # `some/where/venvs/${project_base_name}` or she has the venv # contained in the project dir `${project_path}/venv`. She then # could edit the global settings once and can be sure that always the # right linter installed in the virtual environment gets executed. python = settings.get('@python', None) if isinstance(python, str): python = util.expand_variables(python) persist.debug("{}: wanted @python is '{}'".format(self.name, python)) cmd_name = cmd[0] if isinstance(cmd, (list, tuple)) else cmd if python: if isinstance(python, str): executable = util.find_script_by_python_env(python, cmd_name) if not executable: persist.printf( "WARNING: {} deactivated, cannot locate '{}' " "for given @python '{}'".format( self.name, cmd_name, python)) # Do not fallback, user specified something we didn't find return True, None return True, executable else: executable = util.find_script_by_python_version( cmd_name, str(python)) # If we didn't find anything useful, use the legacy # code from SublimeLinter for resolving that version. if executable is None: persist.debug("{}: Still trying to resolve {}, now trying " "SublimeLinter's legacy code.".format( self.name, python)) _, executable, *_ = util.find_python(str(python), cmd_name) if executable is None: persist.printf( "WARNING: {} deactivated, cannot locate '{}' " "for given @python '{}'".format( self.name, cmd_name, python)) return True, None persist.debug("{}: Using {} for given @python '{}'".format( self.name, executable, python)) return True, executable # If we're here the user didn't specify anything. This is the default # experience. So we kick in some 'magic' chdir = self.get_chdir(settings) executable = util.ask_pipenv(cmd[0], chdir) if executable: persist.debug("{}: Using {} according to 'pipenv'".format( self.name, executable)) return True, executable # Should we try a `pyenv which` as well? Problem: I don't have it, # it's MacOS only. persist.debug("{}: trying to use globally installed {}".format( self.name, cmd_name)) # fallback, similiar to a which(cmd) executable = util.find_executable(cmd_name) if executable is None: persist.printf( "WARNING: cannot locate '{}'. Fill in the '@python' or " "'executable' setting.".format(self.name)) return True, executable