└── Static Code Analyzer └── task └── analyzer ├── code_analyzer.py └── test.py /Static Code Analyzer/task/analyzer/code_analyzer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import sys 4 | from pathlib import Path 5 | import ast 6 | 7 | 8 | class CodeAnalyzer: 9 | def __init__(self): 10 | self.blank_lines = 0 11 | 12 | def check_amount_of_lines_preceding_a_code(self, code_line): 13 | if self.blank_lines > 2 and re.match('.+', code_line): 14 | return True 15 | elif re.match('^\n$', code_line): 16 | self.blank_lines += 1 17 | else: 18 | self.blank_lines = 0 19 | return False 20 | 21 | 22 | def check_length_of_code_line(code_line): 23 | return len(code_line) > 79 24 | 25 | 26 | def check_indentation(code_line): 27 | pattern = r'^[ ]+' 28 | result = re.match(pattern, code_line) 29 | if result and len(result.group()) % 4 != 0: 30 | return True 31 | return False 32 | 33 | 34 | def check_semicolon(code_line): 35 | pattern = r'(\'.*\')' 36 | result = re.sub(pattern, '', code_line) 37 | if re.search(';', result): 38 | pattern2 = r'.*#+.*;' 39 | result2 = re.match(pattern2, result) 40 | if result2: 41 | return False 42 | else: 43 | return True 44 | return False 45 | 46 | 47 | def check_two_space_before_comment(code_line): 48 | if re.search('.+#', code_line): 49 | pattern = r'^.*\s{2,}#.*' 50 | result = re.match(pattern, code_line) 51 | if not result: 52 | return True 53 | else: 54 | return False 55 | return False 56 | 57 | 58 | def check_todo(code_line): 59 | if re.search('#.*todo.*', code_line, flags=re.IGNORECASE): 60 | return True 61 | return False 62 | 63 | 64 | def check_spaces_after_construction_name(code_line): 65 | if re.search(r'(def|class) {2,}\w', code_line): 66 | return True 67 | return False 68 | 69 | 70 | def check_class_name(code_line): 71 | result = re.search(r'\bclass\b', code_line) 72 | result1 = re.search(r'(?<=class)\s+[A-Z][A-z0-9]+', code_line) 73 | if result and not result1: 74 | return re.search(r'^class +([A-z]+):', code_line).group(1) 75 | return False 76 | 77 | 78 | def check_function_name(code_line): 79 | result = re.search(r'\bdef\b', code_line) 80 | result1 = re.search(r'(?<=def)\s+[a-z0-9_]+', code_line) 81 | if result and not result1: 82 | return re.search(r'def\s+([A-z0-9_]+)', code_line).group(1) 83 | return False 84 | 85 | 86 | def check_argument_name(code, line_no): 87 | tree = ast.parse(code) 88 | for node in ast.walk(tree): 89 | if isinstance(node, ast.FunctionDef) and hasattr(node, 'lineno') and node.lineno == line_no: 90 | for arg in node.args.args: 91 | result = re.match('^[a-z0-9_]+', arg.arg) 92 | if not result: 93 | return arg.arg 94 | 95 | 96 | def check_variable_name(code, line_no, row): 97 | tree = ast.parse(code) 98 | for node in ast.walk(tree): 99 | if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and hasattr(node, 'lineno') \ 100 | and node.lineno == line_no and re.match(r'^\s+\w', row): 101 | result = re.match(r'^[a-z0-9_]', node.id) 102 | if not result: 103 | return node.id 104 | 105 | 106 | def check_argument_type(code, line_no): 107 | tree = ast.parse(code) 108 | for node in ast.walk(tree): 109 | if isinstance(node, ast.FunctionDef) and hasattr(node, 'lineno') and node.lineno == line_no: 110 | for item in node.args.defaults: 111 | if isinstance(item, ast.List): 112 | return True 113 | 114 | 115 | def check_file(file: dict(description='file name', type=str)): 116 | my_code_analyzer = CodeAnalyzer() 117 | 118 | with open(file, "r") as file: 119 | text_data = file.readlines() 120 | text_file = ''.join(text_data) 121 | 122 | for index, row in enumerate(text_data, start=1): 123 | if check_length_of_code_line(row): 124 | print(f'{file.name}: Line {index}: S001 Too long') 125 | if check_indentation(row): 126 | print(f'{file.name}: Line {index}: S002 Indentation is not a multiple of four') 127 | if check_semicolon(row): 128 | print(f'{file.name}: Line {index}: S003 Unnecessary semicolon') 129 | if check_two_space_before_comment(row): 130 | print(f'{file.name}: Line {index}: S004 At least two spaces required before inline comments') 131 | if check_todo(row): 132 | print(f'{file.name}: Line {index}: S005 TODO found') 133 | if my_code_analyzer.check_amount_of_lines_preceding_a_code(row): 134 | my_code_analyzer.blank_lines = 0 135 | print(f'{file.name}: Line {index}: S006 More than two blank lines used before this line') 136 | if check_spaces_after_construction_name(row): 137 | print(f'{file.name}: Line {index}: S007 Too many spaces after construction_name (def or class)') 138 | if check_class_name(row): 139 | print(f'{file.name}: Line {index}: S008 Class name \'{check_class_name(row)}\' should use CamelCase') 140 | if check_function_name(row): 141 | print(f'{file.name}: Line {index}: S009 Function name \'{check_function_name(row)}\' should use snake_case') 142 | if check_argument_name(text_file, index): 143 | print(f'{file.name}: Line {index}: S010 Argument name \'{check_argument_name(text_file, index)}\' ' 144 | f'should be snake_case') 145 | if check_variable_name(text_file, index, row): 146 | print(f'{file.name}: Line {index}: S011 Variable \'{check_variable_name(text_file, index, row)}\' in ' 147 | f'function should be snake_case') 148 | if check_argument_type(text_file, index): 149 | print(f'{file.name}: Line {index}: S012 Default argument value is mutable') 150 | 151 | def main(): 152 | """Get the argument from the command line""" 153 | if len(sys.argv) != 2: 154 | print('Usage: python code_analyzer.py ') 155 | sys.exit() 156 | else: 157 | path = sys.argv[1] 158 | 159 | if os.path.isfile(path): 160 | if os.path.splitext(path)[1] == '.py': 161 | check_file(path) 162 | else: 163 | print(f'{path} is not a Python file') 164 | 165 | elif os.path.isdir(path): 166 | path_to_directory = Path(path) 167 | for py_file in path_to_directory.glob(f'**/*.py'): 168 | # strange bug where I needed to explicitly exclude tests.py to pass the test 169 | if py_file.name != 'tests.py': 170 | check_file(py_file) 171 | 172 | else: 173 | print(f'{path} is not a valid file or directory') 174 | 175 | 176 | if __name__ == '__main__': 177 | main() 178 | -------------------------------------------------------------------------------- /Static Code Analyzer/task/analyzer/test.py: -------------------------------------------------------------------------------- 1 | CONSTANT = 10 2 | names = ['John', 'Lora', 'Paul'] 3 | 4 | 5 | def fun1(S=5, test=[]): # default argument value is mutable 6 | VARIABLE = 10 7 | string = 'string' 8 | print(VARIABLE) 9 | --------------------------------------------------------------------------------