--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/eradicate.py Wed Jun 26 19:41:11 2019 +0200 @@ -0,0 +1,234 @@ +# Copyright (C) 2012-2015 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Removes commented-out Python code.""" + +from __future__ import print_function +from __future__ import unicode_literals + +import difflib +import io +import os +import re +import tokenize + +__version__ = '1.0' + + +MULTILINE_ASSIGNMENT_REGEX = re.compile(r'^\s*\w+\s*=.*[(\[{]$') +PARTIAL_DICTIONARY_REGEX = re.compile(r'^\s*[\'"]\w+[\'"]\s*:.+[,{]\s*$') + + +def comment_contains_code(line, aggressive=True): + """Return True comment contains code.""" + line = line.lstrip() + if not line.startswith('#'): + return False + + line = line.lstrip(' \t\v\n#').strip() + + # Ignore non-comment related hashes. For example, "# Issue #999". + if re.search('#[0-9]', line): + return False + + if line.startswith('pylint:'): + return False + + if re.match(r'.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)', line): + return False + + # Check that this is possibly code. + for symbol in list('()[]{}:=%') + ['print', 'return', 'break', 'continue', + 'import']: + if symbol in line: + break + else: + return False + + if multiline_case(line, aggressive=aggressive): + return True + + symbol_list = [r'elif\s+.*', 'else', 'try', + 'finally', r'except\s+.*'] + if aggressive: + symbol_list.append(r'if\s+.*') + + for symbol in symbol_list: + if re.match(r'^\s*' + symbol + r'\s*:\s*$', line): + return True + + line = re.sub(r'^(print|return)\b\s*', '', line) + + if re.match(PARTIAL_DICTIONARY_REGEX, line): + return True + + try: + compile(line, '<string>', 'exec') + return True + except (SyntaxError, TypeError, UnicodeDecodeError): + return False + + +def multiline_case(line, aggressive=True): + """Return True if line is probably part of some multiline code.""" + if aggressive: + for ending in ')]}': + if line.endswith(ending + ':'): + return True + + if line.strip() == ending + ',': + return True + + # Check whether a function/method definition with return value + # annotation + if re.search(r"def .+\)[\s]+->[\s]+[a-zA-Z_][a-zA-Z0-9_]*:$", line): + return True + + # Check weather a with statement + if re.search(r"with .+ as [a-zA-Z_][a-zA-Z0-9_]*:$", line): + return True + + # Check weather a for statement + if re.search(r"for [a-zA-Z_][a-zA-Z0-9_]* in .+:$", line): + return True + + if line.endswith('\\'): + return True + + if re.match(MULTILINE_ASSIGNMENT_REGEX, line): + return True + + if re.match(r'^[()\[\]{}\s]+$', line): + return True + + return False + + +def commented_out_code_line_numbers(source, aggressive=True): + """Yield line numbers of commented-out code.""" + sio = io.StringIO(source) + try: + for token in tokenize.generate_tokens(sio.readline): + token_type = token[0] + start_row = token[2][0] + line = token[4] + + if (token_type == tokenize.COMMENT and + line.lstrip().startswith('#') and + not line.lstrip().startswith('##') and + # modified from original file (line added) + comment_contains_code(line, aggressive)): + yield start_row + except (tokenize.TokenError, IndentationError): + pass + + +def filter_commented_out_code(source, aggressive=True): + """Yield code with commented out code removed.""" + marked_lines = list(commented_out_code_line_numbers(source, + aggressive)) + sio = io.StringIO(source) + previous_line = '' + for line_number, line in enumerate(sio.readlines(), start=1): + if (line_number not in marked_lines or + previous_line.rstrip().endswith('\\')): + yield line + previous_line = line + + +def fix_file(filename, args, standard_out): + """Run filter_commented_out_code() on file.""" + encoding = detect_encoding(filename) + with open_with_encoding(filename, encoding=encoding) as input_file: + source = input_file.read() + + filtered_source = ''.join(filter_commented_out_code(source, + args.aggressive)) + + if source != filtered_source: + if args.in_place: + with open_with_encoding(filename, mode='w', + encoding=encoding) as output_file: + output_file.write(filtered_source) + else: + diff = difflib.unified_diff( + source.splitlines(), + filtered_source.splitlines(), + 'before/' + filename, + 'after/' + filename, + lineterm='') + standard_out.write('\n'.join(list(diff) + [''])) + + +def open_with_encoding(filename, encoding, mode='r'): + """Return opened file with a specific encoding.""" + return io.open(filename, mode=mode, encoding=encoding, + newline='') # Preserve line endings + + +def detect_encoding(filename): + """Return file encoding.""" + try: + with open(filename, 'rb') as input_file: + from lib2to3.pgen2 import tokenize as lib2to3_tokenize + encoding = lib2to3_tokenize.detect_encoding(input_file.readline)[0] + + # Check for correctness of encoding. + with open_with_encoding(filename, encoding) as input_file: + input_file.read() + + return encoding + except (SyntaxError, LookupError, UnicodeDecodeError): + return 'latin-1' + + +def main(argv, standard_out, standard_error): + """Main entry point.""" + import argparse + parser = argparse.ArgumentParser(description=__doc__, prog='eradicate') + parser.add_argument('-i', '--in-place', action='store_true', + help='make changes to files instead of printing diffs') + parser.add_argument('-r', '--recursive', action='store_true', + help='drill down directories recursively') + parser.add_argument('-a', '--aggressive', action='store_true', + help='make more aggressive changes; ' + 'this may result in false positives') + parser.add_argument('--version', action='version', + version='%(prog)s ' + __version__) + parser.add_argument('files', nargs='+', help='files to format') + + args = parser.parse_args(argv[1:]) + + filenames = list(set(args.files)) + while filenames: + name = filenames.pop(0) + if args.recursive and os.path.isdir(name): + for root, directories, children in os.walk('{}'.format(name)): + filenames += [os.path.join(root, f) for f in children + if f.endswith('.py') and + not f.startswith('.')] + directories[:] = [d for d in directories + if not d.startswith('.')] + else: + try: + fix_file(name, args=args, standard_out=standard_out) + except IOError as exception: + print('{}'.format(exception), file=standard_error)