timeit

Програмиране с Python

Курс във Факултета по Математика и Информатика към СУ

Решение на Статичен анализ на python код от Кристофър Митов

Обратно към всички решения

Към профила на Кристофър Митов

Резултати

  • 8 точки от тестове
  • 0 бонус точки
  • 8 точки общо
  • 9 успешни тест(а)
  • 2 неуспешни тест(а)

Код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import ast
import re
from collections import defaultdict


ERRORS = {'too_long_line': 'line too long ({0} > {1})',
          'mult_expr': 'multiple expressions on the same line',
          'deep_nesting': 'nesting too deep ({0} > {1})',
          'indent_problem': 'indentation is {0} instead of {1}',
          'method_count': 'too many methods in class({0} > {1})',
          'arity_error': 'too many arguments({0} > {1})',
          'trailing_ws': 'trailing whitespace',
          'line_count': 'method with too many lines ({0} > {1})'}


MORE_INDENT = ['body', 'orelse']


def check_length(line, max_length):
    length = len(line)
    if length > max_length:
        return False, length

    return True, length


def strip(code):
    regexes = [(r'(\"|\').*?\1', "''"),
               (r'#.*?$', '')]

    multiline_regex = r'(\"\"\"|\'\'\'){1}.*?\1'
    newlines = tuple()
    for match in re.finditer(multiline_regex, code, re.S):
        count = len(code[match.start():match.end()].split('\n'))
        newlines += ((count-1)*'\n', )

    code = re.sub(multiline_regex, '"""{}"""', code, 0, re.S)
    code = code.format(*newlines)

    for regex, repl in regexes[1:]:
        code = re.sub(regex, repl, code, 0, re.M | re.S)

    return code


def get_linenos_of_node(node):
    result = []
    bodies = []

    bodies.extend([getattr(node, attr) for attr in MORE_INDENT if
                   hasattr(node, attr)])
    if isinstance(node, ast.Try):
        bodies.extend([h.body for h in node.handlers])
        bodies.append(node.finalbody)

    for body in bodies:
        for element in body:
            if hasattr(element, 'body'):
                result.extend(get_linenos_of_node(element))
            if hasattr(element, 'lineno'):
                result.append(element.lineno)
    return result


def check_statement(statement):
    if re.search(r'^ *$', statement):
        return False

    try:
        ast.parse(statement)
    except:
        if re.search(r':.*$', statement):
            return True
        return False

    return True


def check_line(last_stmt, line):
    line = last_stmt + line
    line = re.sub(r' *; *', ';', line)
    line = re.sub(r'; *$', '',  line)
    statements = line.split(';')
    statements = list(map(lambda x: re.sub(r'^ *', '', x), statements))
    if check_statement(statements[-1]):
        return 1, ''
    else:
        return 0, statements[-1]


def check_logical_lines(f, lines):
    count = 0

    linenos = sorted(get_linenos_of_node(f))
    last_stmt = ''
    for line in lines[linenos[0]-1:linenos[-1]]:
        c, last_stmt = check_line(last_stmt, line)
        count += c

    return count


def check_body(parent, gate):
    statements = getattr(parent, gate) if hasattr(parent, gate) else []

    if statements:
        stms = [hasattr(statements[0], 'lineno'),
                statements[0].lineno == parent.lineno]
        if all(stms):
            return []

    return statements


def check_indent(body, size, curr, lines):
    bad_indentations = []
    bodies = []

    stms = [(x.lineno, x.col_offset) for x in body if hasattr(x, 'lineno') and
            hasattr(x, 'col_offset') if x.col_offset >= 0]
    correct = list(map(lambda x: x[1] == curr, stms))
    for x in range(1, len(stms)):
        if not correct[x]:
            if stms[x - 1][0] == stms[x][0]:
                correct[x] = True
            if re.search(r';.*$', lines[stms[x][0] - 1], re.M):
                correct[x] = True

    # (row, actual, correct)
    errors = [(stms[x][0], stms[x][1], curr) for x in range(0, len(stms))
              if not correct[x]]
    bad_indentations.extend(errors)

    try_blocks = list(filter(lambda x: isinstance(x, ast.Try), body))
    for block in try_blocks:
        bodies.append(check_body(block, 'finalbody'))
        bodies.extend([check_body(x, 'body') for x in block.handlers])

    for gate in MORE_INDENT:
        bodies.extend([check_body(x, gate) for x in body])

    for x in bodies:
        bad_indentations.extend(check_indent(x, size, curr + size, lines))

    return bad_indentations


def check_nesting(f, curr, limit):
    bad_nestings = []
    if curr > limit:
        bad_nestings.append((curr, f.lineno + 1))

    for child in f.body:
        if isinstance(child, ast.If):
            bad_nestings.extend(check_nesting(child, curr + 1, limit))
        elif isinstance(child, ast.While) or isinstance(child, ast.For):
            bad_nestings.extend(check_nesting(child, curr + 1, limit))
        elif isinstance(child, ast.With):
            bad_nestings.extend(check_nesting(child, curr + 1, limit))
        elif isinstance(child, ast.Try):
            bad_nestings.extend(check_nesting(child, curr + 1, limit))
            for handler in child.handlers:
                bad_nestings.extend(check_nesting(handler, curr + 1, limit))

    if hasattr(f, 'orelse'):
        for child in f.orelse:
            if isinstance(child, ast.If):
                bad_nestings.extend(check_nesting(child, curr + 1, limit))
            elif isinstance(child, ast.While) or isinstance(child, ast.For):
                bad_nestings.extend(check_nesting(child, curr + 1, limit))
            elif isinstance(child, ast.With):
                bad_nestings.extend(check_nesting(child, curr + 1, limit))
            elif isinstance(child, ast.Try):
                bad_nestings.extend(check_nesting(child, curr + 1, limit))
                for h in child.handlers:
                    bad_nestings.extend(check_nesting(h, curr + 1, limit))

    return bad_nestings


def critic(code, line_length=79, forbid_semicolons=True, max_nesting=None,
           indentation_size=4, methods_per_class=None, max_arity=None,
           forbid_trailing_whitespace=True, max_lines_per_function=None):
    # Function critic starts here
    result = defaultdict(list)

    # Check for each line's length and trailing whitespaces
    lines = code.split('\n')
    for number, line in enumerate(lines, 1):
        length_ok, length = check_length(line, line_length)
        if not length_ok:
            result[number].append(
                    ERRORS['too_long_line'].format(length, line_length))

        if forbid_trailing_whitespace and re.search(r' +$', line):
            result[number].append(ERRORS['trailing_ws'])

    # strip the code from strings and comments
    code = strip(code)
    # end of stripping

    # Check for multiple expressions
    # We have to split the code again after the stripping
    lines = code.split("\n")
    for number, line in enumerate(lines, 1):
        if forbid_semicolons and re.search(r'^.*?;.*$', line):
            result[number].append(ERRORS['mult_expr'])

    tree = ast.parse(code)
    funcs = [f for f in ast.walk(tree) if isinstance(f, ast.FunctionDef)]
    # Check for each funcion/method's logical lines number
    if max_lines_per_function:
        for f in funcs:
            count = check_logical_lines(f, lines)
            if count > max_lines_per_function:
                result[f.lineno].append(
                      ERRORS['line_count'].format(count,
                                                  max_lines_per_function))

    # Get the bad intendations if there are any
    bad_indentations = check_indent(tree.body, indentation_size, 0, lines)
    for x in bad_indentations:
        result[x[0]].append(ERRORS['indent_problem'].format(x[1], x[2]))

    # Check for max methods per class
    if methods_per_class:
        classes = [c for c in ast.walk(tree) if isinstance(c, ast.ClassDef)]
        for c in classes:
            count = len([x for x in c.body if isinstance(x, ast.FunctionDef)])
            if count > methods_per_class:
                result[c.lineno].append(ERRORS['method_count'].format(
                    count, methods_per_class))

    # Check for arguments count
    if max_arity:
        all_functions = funcs +\
                [x for x in ast.walk(tree) if isinstance(x, ast.Lambda)]
        for f in all_functions:
            args_count = len(f.args.args)
            if args_count > max_arity:
                result[f.lineno].append(ERRORS['arity_error'].format(
                    args_count, max_arity))

    # Check for deep nesting
    if max_nesting:
        for f in funcs:
            max_indents = check_nesting(f, 1, max_nesting)
            for ind in max_indents:
                if ind[0] > max_nesting:
                    result[ind[1]].append(ERRORS['deep_nesting'].format(
                        ind[0], max_nesting))

    return result

Лог от изпълнението

E.....F....
======================================================================
ERROR: test_dict_nesting (test.TestCritic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/data/rails/pyfmi-2016/releases/20160307095126/lib/language/python/runner.py", line 67, in thread
    raise result
KeyError: "\n    'because'"

======================================================================
FAIL: test_multiple_issues_all_over_the_place (test.TestCritic)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/data/rails/pyfmi-2016/releases/20160307095126/lib/language/python/runner.py", line 67, in thread
    raise result
AssertionError: Items in the first set but not the second:
'method with too many lines (12 > 5)'

----------------------------------------------------------------------
Ran 11 tests in 0.114s

FAILED (failures=1, errors=1)

История (1 версия и 0 коментара)

Кристофър обнови решението на 18.05.2016 13:26 (преди над 1 година)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import ast
import re
from collections import defaultdict


ERRORS = {'too_long_line': 'line too long ({0} > {1})',
          'mult_expr': 'multiple expressions on the same line',
          'deep_nesting': 'nesting too deep ({0} > {1})',
          'indent_problem': 'indentation is {0} instead of {1}',
          'method_count': 'too many methods in class({0} > {1})',
          'arity_error': 'too many arguments({0} > {1})',
          'trailing_ws': 'trailing whitespace',
          'line_count': 'method with too many lines ({0} > {1})'}


MORE_INDENT = ['body', 'orelse']


def check_length(line, max_length):
    length = len(line)
    if length > max_length:
        return False, length

    return True, length


def strip(code):
    regexes = [(r'(\"|\').*?\1', "''"),
               (r'#.*?$', '')]

    multiline_regex = r'(\"\"\"|\'\'\'){1}.*?\1'
    newlines = tuple()
    for match in re.finditer(multiline_regex, code, re.S):
        count = len(code[match.start():match.end()].split('\n'))
        newlines += ((count-1)*'\n', )

    code = re.sub(multiline_regex, '"""{}"""', code, 0, re.S)
    code = code.format(*newlines)

    for regex, repl in regexes[1:]:
        code = re.sub(regex, repl, code, 0, re.M | re.S)

    return code


def get_linenos_of_node(node):
    result = []
    bodies = []

    bodies.extend([getattr(node, attr) for attr in MORE_INDENT if
                   hasattr(node, attr)])
    if isinstance(node, ast.Try):
        bodies.extend([h.body for h in node.handlers])
        bodies.append(node.finalbody)

    for body in bodies:
        for element in body:
            if hasattr(element, 'body'):
                result.extend(get_linenos_of_node(element))
            if hasattr(element, 'lineno'):
                result.append(element.lineno)
    return result


def check_statement(statement):
    if re.search(r'^ *$', statement):
        return False

    try:
        ast.parse(statement)
    except:
        if re.search(r':.*$', statement):
            return True
        return False

    return True


def check_line(last_stmt, line):
    line = last_stmt + line
    line = re.sub(r' *; *', ';', line)
    line = re.sub(r'; *$', '',  line)
    statements = line.split(';')
    statements = list(map(lambda x: re.sub(r'^ *', '', x), statements))
    if check_statement(statements[-1]):
        return 1, ''
    else:
        return 0, statements[-1]


def check_logical_lines(f, lines):
    count = 0

    linenos = sorted(get_linenos_of_node(f))
    last_stmt = ''
    for line in lines[linenos[0]-1:linenos[-1]]:
        c, last_stmt = check_line(last_stmt, line)
        count += c

    return count


def check_body(parent, gate):
    statements = getattr(parent, gate) if hasattr(parent, gate) else []

    if statements:
        stms = [hasattr(statements[0], 'lineno'),
                statements[0].lineno == parent.lineno]
        if all(stms):
            return []

    return statements


def check_indent(body, size, curr, lines):
    bad_indentations = []
    bodies = []

    stms = [(x.lineno, x.col_offset) for x in body if hasattr(x, 'lineno') and
            hasattr(x, 'col_offset') if x.col_offset >= 0]
    correct = list(map(lambda x: x[1] == curr, stms))
    for x in range(1, len(stms)):
        if not correct[x]:
            if stms[x - 1][0] == stms[x][0]:
                correct[x] = True
            if re.search(r';.*$', lines[stms[x][0] - 1], re.M):
                correct[x] = True

    # (row, actual, correct)
    errors = [(stms[x][0], stms[x][1], curr) for x in range(0, len(stms))
              if not correct[x]]
    bad_indentations.extend(errors)

    try_blocks = list(filter(lambda x: isinstance(x, ast.Try), body))
    for block in try_blocks:
        bodies.append(check_body(block, 'finalbody'))
        bodies.extend([check_body(x, 'body') for x in block.handlers])

    for gate in MORE_INDENT:
        bodies.extend([check_body(x, gate) for x in body])

    for x in bodies:
        bad_indentations.extend(check_indent(x, size, curr + size, lines))

    return bad_indentations


def check_nesting(f, curr, limit):
    bad_nestings = []
    if curr > limit:
        bad_nestings.append((curr, f.lineno + 1))

    for child in f.body:
        if isinstance(child, ast.If):
            bad_nestings.extend(check_nesting(child, curr + 1, limit))
        elif isinstance(child, ast.While) or isinstance(child, ast.For):
            bad_nestings.extend(check_nesting(child, curr + 1, limit))
        elif isinstance(child, ast.With):
            bad_nestings.extend(check_nesting(child, curr + 1, limit))
        elif isinstance(child, ast.Try):
            bad_nestings.extend(check_nesting(child, curr + 1, limit))
            for handler in child.handlers:
                bad_nestings.extend(check_nesting(handler, curr + 1, limit))

    if hasattr(f, 'orelse'):
        for child in f.orelse:
            if isinstance(child, ast.If):
                bad_nestings.extend(check_nesting(child, curr + 1, limit))
            elif isinstance(child, ast.While) or isinstance(child, ast.For):
                bad_nestings.extend(check_nesting(child, curr + 1, limit))
            elif isinstance(child, ast.With):
                bad_nestings.extend(check_nesting(child, curr + 1, limit))
            elif isinstance(child, ast.Try):
                bad_nestings.extend(check_nesting(child, curr + 1, limit))
                for h in child.handlers:
                    bad_nestings.extend(check_nesting(h, curr + 1, limit))

    return bad_nestings


def critic(code, line_length=79, forbid_semicolons=True, max_nesting=None,
           indentation_size=4, methods_per_class=None, max_arity=None,
           forbid_trailing_whitespace=True, max_lines_per_function=None):
    # Function critic starts here
    result = defaultdict(list)

    # Check for each line's length and trailing whitespaces
    lines = code.split('\n')
    for number, line in enumerate(lines, 1):
        length_ok, length = check_length(line, line_length)
        if not length_ok:
            result[number].append(
                    ERRORS['too_long_line'].format(length, line_length))

        if forbid_trailing_whitespace and re.search(r' +$', line):
            result[number].append(ERRORS['trailing_ws'])

    # strip the code from strings and comments
    code = strip(code)
    # end of stripping

    # Check for multiple expressions
    # We have to split the code again after the stripping
    lines = code.split("\n")
    for number, line in enumerate(lines, 1):
        if forbid_semicolons and re.search(r'^.*?;.*$', line):
            result[number].append(ERRORS['mult_expr'])

    tree = ast.parse(code)
    funcs = [f for f in ast.walk(tree) if isinstance(f, ast.FunctionDef)]
    # Check for each funcion/method's logical lines number
    if max_lines_per_function:
        for f in funcs:
            count = check_logical_lines(f, lines)
            if count > max_lines_per_function:
                result[f.lineno].append(
                      ERRORS['line_count'].format(count,
                                                  max_lines_per_function))

    # Get the bad intendations if there are any
    bad_indentations = check_indent(tree.body, indentation_size, 0, lines)
    for x in bad_indentations:
        result[x[0]].append(ERRORS['indent_problem'].format(x[1], x[2]))

    # Check for max methods per class
    if methods_per_class:
        classes = [c for c in ast.walk(tree) if isinstance(c, ast.ClassDef)]
        for c in classes:
            count = len([x for x in c.body if isinstance(x, ast.FunctionDef)])
            if count > methods_per_class:
                result[c.lineno].append(ERRORS['method_count'].format(
                    count, methods_per_class))

    # Check for arguments count
    if max_arity:
        all_functions = funcs +\
                [x for x in ast.walk(tree) if isinstance(x, ast.Lambda)]
        for f in all_functions:
            args_count = len(f.args.args)
            if args_count > max_arity:
                result[f.lineno].append(ERRORS['arity_error'].format(
                    args_count, max_arity))

    # Check for deep nesting
    if max_nesting:
        for f in funcs:
            max_indents = check_nesting(f, 1, max_nesting)
            for ind in max_indents:
                if ind[0] > max_nesting:
                    result[ind[1]].append(ERRORS['deep_nesting'].format(
                        ind[0], max_nesting))

    return result