diff --git a/flake8/checker.py b/flake8/checker.py index c449a9b..905cade 100644 --- a/flake8/checker.py +++ b/flake8/checker.py @@ -326,6 +326,37 @@ class FileChecker(object): self.processor.keyword_arguments_for(plugin.parameters, arguments) return plugin.execute(**arguments) + def run_ast_checks(self): + """Run all checks expecting an abstract syntax tree.""" + try: + ast = self.processor.build_ast() + except (ValueError, SyntaxError, TypeError): + (exc_type, exception) = sys.exc_info()[:2] + if len(exception.args) > 1: + offset = exception.args[1] + if len(offset) > 2: + offset = offset[1:3] + else: + offset = (1, 0) + + self.report('E999', offset[0], offset[1], '%s: %s' % + (exc_type.__name__, exception.args[0])) + return + + for plugin in self.checks.ast_plugins: + checker = self.run_check(plugin, tree=ast) + # NOTE(sigmavirus24): If we want to allow for AST plugins that are + # not classes exclusively, we can do the following: + # retrieve_results = getattr(checker, 'run', lambda: checker) + # Otherwise, we just call run on the checker + for (line_number, offset, text, check) in checker.run(): + self.report( + error_code=None, + line_number=line_number, + column=offset, + text=text, + ) + def run_logical_checks(self): """Run all checks expecting a logical line.""" comments, logical_line, mapping = self.processor.build_logical_line() @@ -404,6 +435,8 @@ class FileChecker(object): self.report(exc.error_code, exc.line_number, exc.column_number, exc.error_message) + self.run_ast_checks() + if results_queue is not None: results_queue.put((self.filename, self.results)) diff --git a/flake8/main/cli.py b/flake8/main/cli.py index 5bf6144..dbd9603 100644 --- a/flake8/main/cli.py +++ b/flake8/main/cli.py @@ -227,6 +227,9 @@ class Application(object): self.option_manager, argv ) + self.check_plugins.provide_options(self.option_manager, self.options, + self.args) + def make_formatter(self): # type: () -> NoneType """Initialize a formatter based on the parsed options.""" diff --git a/flake8/processor.py b/flake8/processor.py index d75256b..1dc27a1 100644 --- a/flake8/processor.py +++ b/flake8/processor.py @@ -10,6 +10,7 @@ from flake8 import defaults from flake8 import exceptions from flake8 import utils +PyCF_ONLY_AST = 1024 NEWLINE = frozenset([tokenize.NL, tokenize.NEWLINE]) # Work around Python < 2.6 behaviour, which does not generate NL after # a comment which is on a line by itself. @@ -172,6 +173,10 @@ class FileProcessor(object): (previous_row, previous_column) = end return comments, logical, mapping + def build_ast(self): + """Build an abstract syntax tree from the list of lines.""" + return compile(''.join(self.lines), '', 'exec', PyCF_ONLY_AST) + def build_logical_line(self): """Build a logical line from the current tokens list.""" comments, logical, mapping_list = self.build_logical_line_tokens() @@ -223,6 +228,10 @@ class FileProcessor(object): except tokenize.TokenError as exc: raise exceptions.InvalidSyntax(exc.message, exception=exc) + def line_for(self, line_number): + """Retrieve the physical line at the specified line number.""" + return self.lines[line_number - 1] + def next_line(self): """Get the next line from the list.""" if self.line_number >= self.total_lines: diff --git a/flake8/style_guide.py b/flake8/style_guide.py index 51c3a8e..2c18c9d 100644 --- a/flake8/style_guide.py +++ b/flake8/style_guide.py @@ -187,12 +187,13 @@ class StyleGuide(object): error, codes_str) return False - def handle_error(self, code, filename, line_number, column_number, text): + def handle_error(self, code, filename, line_number, column_number, text, + physical_line=None): # type: (str, str, int, int, str) -> NoneType """Handle an error reported by a check.""" error = Error(code, filename, line_number, column_number, text) if (self.should_report_error(error.code) is Decision.Selected and - self.is_inline_ignored(error) is False): + self.is_inline_ignored(error, physical_line) is False): self.formatter.handle(error) self.listener.notify(error.code, error)