minerva-plugin 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- minerva_plugin/__init__.py +480 -0
- minerva_plugin-2.1.0.data/data/settings/plugin.json +12 -0
- minerva_plugin-2.1.0.dist-info/METADATA +96 -0
- minerva_plugin-2.1.0.dist-info/RECORD +8 -0
- minerva_plugin-2.1.0.dist-info/WHEEL +5 -0
- minerva_plugin-2.1.0.dist-info/entry_points.txt +2 -0
- minerva_plugin-2.1.0.dist-info/licenses/LICENSE +21 -0
- minerva_plugin-2.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import re
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_json(folder_name_lst, file_name, default=None):
|
|
8
|
+
if default is None:
|
|
9
|
+
default = {}
|
|
10
|
+
if isinstance(folder_name_lst, str):
|
|
11
|
+
folder_name = folder_name_lst
|
|
12
|
+
elif isinstance(folder_name_lst, list):
|
|
13
|
+
folder_name = os.path.join(*folder_name_lst)
|
|
14
|
+
if not os.path.exists(folder_name):
|
|
15
|
+
os.makedirs(folder_name)
|
|
16
|
+
filename = os.path.join(folder_name, file_name)
|
|
17
|
+
if not os.path.exists(filename):
|
|
18
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
19
|
+
json.dump(default, f, ensure_ascii=True)
|
|
20
|
+
with open(filename, encoding="utf-8") as f:
|
|
21
|
+
load_dct = json.load(f)
|
|
22
|
+
return load_dct
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def save_json(folder_name_lst, file_name, save_dct):
|
|
26
|
+
if isinstance(folder_name_lst, str):
|
|
27
|
+
folder_name = folder_name_lst
|
|
28
|
+
elif isinstance(folder_name_lst, list):
|
|
29
|
+
folder_name = os.path.join(*folder_name_lst)
|
|
30
|
+
if not os.path.exists(folder_name):
|
|
31
|
+
os.makedirs(folder_name)
|
|
32
|
+
filename = os.path.join(folder_name, file_name)
|
|
33
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
34
|
+
json.dump(save_dct, f, ensure_ascii=False, indent=4)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Minerva:
|
|
38
|
+
"""
|
|
39
|
+
Основной класс плагина Minerva.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name = "minerva"
|
|
43
|
+
version = "2.1.0"
|
|
44
|
+
directory = "settings"
|
|
45
|
+
filename = "plugin.json"
|
|
46
|
+
|
|
47
|
+
def __init__(self, tree: ast.AST, filename: str, lines=None):
|
|
48
|
+
self.tree = tree
|
|
49
|
+
self.filename = filename
|
|
50
|
+
if lines is None:
|
|
51
|
+
try:
|
|
52
|
+
with open(filename, "r", encoding="utf-8") as f:
|
|
53
|
+
self.lines = f.readlines()
|
|
54
|
+
except (OSError, IOError):
|
|
55
|
+
self.lines = []
|
|
56
|
+
else:
|
|
57
|
+
self.lines = lines
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def add_options(cls, parser):
|
|
61
|
+
"""
|
|
62
|
+
Регистрация настроек в flake8
|
|
63
|
+
"""
|
|
64
|
+
settings = cls.load_settings()
|
|
65
|
+
|
|
66
|
+
parser.add_option(
|
|
67
|
+
"--min-var-length",
|
|
68
|
+
action="store",
|
|
69
|
+
type=int,
|
|
70
|
+
default=settings.get("min_length", 2),
|
|
71
|
+
parse_from_config=True,
|
|
72
|
+
help="Минимальная длина имени переменной",
|
|
73
|
+
)
|
|
74
|
+
parser.add_option(
|
|
75
|
+
"--max-var-length",
|
|
76
|
+
action="store",
|
|
77
|
+
type=int,
|
|
78
|
+
default=settings.get("max_length", 40),
|
|
79
|
+
parse_from_config=True,
|
|
80
|
+
help="Максимальная длина имени переменной",
|
|
81
|
+
)
|
|
82
|
+
parser.add_option(
|
|
83
|
+
"--allowed-single-letters",
|
|
84
|
+
action="store",
|
|
85
|
+
type=str,
|
|
86
|
+
default=settings.get("allowed_single_letters", "i,j,x,y,e"),
|
|
87
|
+
parse_from_config=True,
|
|
88
|
+
help="Разрешенные однобуквенные имена через запятую",
|
|
89
|
+
)
|
|
90
|
+
parser.add_option(
|
|
91
|
+
"--enforce-snake-case",
|
|
92
|
+
action="store_true",
|
|
93
|
+
default=settings.get("enforce_snake_case", True),
|
|
94
|
+
parse_from_config=True,
|
|
95
|
+
help="Требовать snake_case для имен переменных",
|
|
96
|
+
)
|
|
97
|
+
parser.add_option(
|
|
98
|
+
"--prohibited-modules",
|
|
99
|
+
action="store",
|
|
100
|
+
type=str,
|
|
101
|
+
default=settings.get("prohibited_modules", "math,re"),
|
|
102
|
+
parse_from_config=True,
|
|
103
|
+
help="Запрещенные модули для импорта через запятую",
|
|
104
|
+
)
|
|
105
|
+
parser.add_option(
|
|
106
|
+
"--max-line-length-custom",
|
|
107
|
+
action="store",
|
|
108
|
+
type=int,
|
|
109
|
+
default=settings.get("max_line_length", 123),
|
|
110
|
+
parse_from_config=True,
|
|
111
|
+
help="Максимальная длина строки (MN005)",
|
|
112
|
+
)
|
|
113
|
+
parser.add_option(
|
|
114
|
+
"--max-char-code",
|
|
115
|
+
action="store",
|
|
116
|
+
type=int,
|
|
117
|
+
default=settings.get("max_char_code", 1000),
|
|
118
|
+
parse_from_config=True,
|
|
119
|
+
help="Максимальный допустимый код символа в строке (MN006)",
|
|
120
|
+
)
|
|
121
|
+
parser.add_option(
|
|
122
|
+
"--prohibited-constructors",
|
|
123
|
+
action="store",
|
|
124
|
+
type=str,
|
|
125
|
+
default=settings.get("prohibited_constructors", "list,set"),
|
|
126
|
+
parse_from_config=True,
|
|
127
|
+
help="Запрещенные конструкторы через запятую (MN007)",
|
|
128
|
+
)
|
|
129
|
+
parser.add_option(
|
|
130
|
+
"--allowed-collections",
|
|
131
|
+
action="store",
|
|
132
|
+
type=str,
|
|
133
|
+
default=settings.get("allowed_collections", ""),
|
|
134
|
+
parse_from_config=True,
|
|
135
|
+
help="Белый список коллекций через запятую (MN008)",
|
|
136
|
+
)
|
|
137
|
+
parser.add_option(
|
|
138
|
+
"--check-constructor",
|
|
139
|
+
action="store",
|
|
140
|
+
type=str,
|
|
141
|
+
default=settings.get("check_constructor", ""),
|
|
142
|
+
parse_from_config=True,
|
|
143
|
+
help="Типы коллекций, которые должны создаваться через литерал (MN009)",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def parse_options(cls, options):
|
|
148
|
+
"""
|
|
149
|
+
Парсинг полученных опций
|
|
150
|
+
"""
|
|
151
|
+
cls.min_length = options.min_var_length
|
|
152
|
+
cls.max_length = options.max_var_length
|
|
153
|
+
cls.allowed_single_letters = set(
|
|
154
|
+
letter.strip() for letter in options.allowed_single_letters.split(",")
|
|
155
|
+
)
|
|
156
|
+
cls.enforce_snake_case = options.enforce_snake_case
|
|
157
|
+
cls.prohibited_modules = set(
|
|
158
|
+
mod.strip() for mod in options.prohibited_modules.split(",") if mod.strip()
|
|
159
|
+
)
|
|
160
|
+
cls.max_line_length = options.max_line_length_custom
|
|
161
|
+
cls.max_char_code = options.max_char_code
|
|
162
|
+
cls.prohibited_constructors = set(
|
|
163
|
+
c.strip() for c in options.prohibited_constructors.split(",") if c.strip()
|
|
164
|
+
)
|
|
165
|
+
cls.allowed_collections = set(
|
|
166
|
+
c.strip() for c in options.allowed_collections.split(",") if c.strip()
|
|
167
|
+
)
|
|
168
|
+
cls.check_constructor = set(
|
|
169
|
+
c.strip() for c in options.check_constructor.split(",") if c.strip()
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def run(self):
|
|
173
|
+
"""
|
|
174
|
+
Генератор нарушений
|
|
175
|
+
"""
|
|
176
|
+
settings = self.load_settings()
|
|
177
|
+
visitor = MinervaVisitor(**settings)
|
|
178
|
+
visitor.check_lines(self.lines)
|
|
179
|
+
visitor.visit(self.tree)
|
|
180
|
+
for violation in visitor.violations:
|
|
181
|
+
yield violation
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def load_settings(cls):
|
|
185
|
+
"""
|
|
186
|
+
Загрузка настроек из файла
|
|
187
|
+
"""
|
|
188
|
+
settings = load_json(cls.directory, cls.filename)
|
|
189
|
+
if settings == {}:
|
|
190
|
+
settings["min_length"] = 2
|
|
191
|
+
settings["max_length"] = 40
|
|
192
|
+
settings["allowed_single_letters"] = "i,j,x,y,e"
|
|
193
|
+
settings["enforce_snake_case"] = True
|
|
194
|
+
settings["prohibited_modules"] = "math,re"
|
|
195
|
+
settings["max_line_length"] = 123
|
|
196
|
+
settings["max_char_code"] = 1000
|
|
197
|
+
settings["prohibited_constructors"] = "list,set"
|
|
198
|
+
settings["allowed_collections"] = "dict"
|
|
199
|
+
settings["check_constructor"] = "list,dict"
|
|
200
|
+
save_json(cls.directory, cls.filename, settings)
|
|
201
|
+
return settings
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class MinervaVisitor(ast.NodeVisitor):
|
|
205
|
+
"""
|
|
206
|
+
Визитор AST дерева для проверки имен, импортов, строк и коллекций
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(
|
|
210
|
+
self,
|
|
211
|
+
min_length,
|
|
212
|
+
max_length,
|
|
213
|
+
allowed_single_letters,
|
|
214
|
+
enforce_snake_case,
|
|
215
|
+
prohibited_modules,
|
|
216
|
+
max_line_length=123,
|
|
217
|
+
max_char_code=1000,
|
|
218
|
+
prohibited_constructors="list,set",
|
|
219
|
+
allowed_collections="",
|
|
220
|
+
check_constructor="",
|
|
221
|
+
):
|
|
222
|
+
self.violations = list()
|
|
223
|
+
self.min_length = min_length
|
|
224
|
+
self.max_length = max_length
|
|
225
|
+
self.max_line_length = max_line_length
|
|
226
|
+
self.max_char_code = max_char_code
|
|
227
|
+
|
|
228
|
+
self.allowed_single_letters = (
|
|
229
|
+
set(letter.strip() for letter in allowed_single_letters.split(","))
|
|
230
|
+
if isinstance(allowed_single_letters, str)
|
|
231
|
+
else allowed_single_letters
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.enforce_snake_case = enforce_snake_case
|
|
235
|
+
|
|
236
|
+
self.prohibited_modules = (
|
|
237
|
+
set(mod.strip() for mod in prohibited_modules.split(",") if mod.strip())
|
|
238
|
+
if isinstance(prohibited_modules, str)
|
|
239
|
+
else prohibited_modules
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
self.prohibited_constructors = (
|
|
243
|
+
set(c.strip() for c in prohibited_constructors.split(",") if c.strip())
|
|
244
|
+
if isinstance(prohibited_constructors, str)
|
|
245
|
+
else prohibited_constructors
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
self.allowed_collections = (
|
|
249
|
+
set(c.strip() for c in allowed_collections.split(",") if c.strip())
|
|
250
|
+
if isinstance(allowed_collections, str) and allowed_collections.strip()
|
|
251
|
+
else set()
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
self.check_constructor = (
|
|
255
|
+
set(c.strip() for c in check_constructor.split(",") if c.strip())
|
|
256
|
+
if isinstance(check_constructor, str) and check_constructor.strip()
|
|
257
|
+
else set()
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
self.snake_case_pattern = re.compile(r"^_?[a-z][a-z0-9_]*$")
|
|
261
|
+
|
|
262
|
+
def check_lines(self, lines):
|
|
263
|
+
"""
|
|
264
|
+
Проверка строк исходного кода (вне AST).
|
|
265
|
+
- MN005: строка длиннее max_line_length
|
|
266
|
+
- MN006: в строке есть символ с кодом > max_char_code
|
|
267
|
+
"""
|
|
268
|
+
if not lines:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
for lineno, line in enumerate(lines, start=1):
|
|
272
|
+
stripped = line.rstrip("\n\r")
|
|
273
|
+
if self.max_line_length and len(stripped) > self.max_line_length:
|
|
274
|
+
msg = (
|
|
275
|
+
f"MN005 line too long "
|
|
276
|
+
f"({len(stripped)} > {self.max_line_length} characters)"
|
|
277
|
+
)
|
|
278
|
+
candidate = (lineno, self.max_line_length, msg, Minerva)
|
|
279
|
+
if candidate not in self.violations:
|
|
280
|
+
self.violations.append(candidate)
|
|
281
|
+
|
|
282
|
+
if self.max_char_code and stripped:
|
|
283
|
+
max_code = max(ord(z) for z in stripped)
|
|
284
|
+
if max_code > self.max_char_code:
|
|
285
|
+
col = stripped.index(chr(max_code))
|
|
286
|
+
msg = (
|
|
287
|
+
f"MN006 suspicious character U+{max_code:04X} "
|
|
288
|
+
f"(code {max_code} > {self.max_char_code})"
|
|
289
|
+
)
|
|
290
|
+
candidate = (lineno, col, msg, Minerva)
|
|
291
|
+
if candidate not in self.violations:
|
|
292
|
+
self.violations.append(candidate)
|
|
293
|
+
|
|
294
|
+
def _check_name(self, name, lineno, col_offset):
|
|
295
|
+
if not name:
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
if name.startswith("__") and name.endswith("__"):
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
if len(name) < self.min_length:
|
|
302
|
+
if name not in self.allowed_single_letters:
|
|
303
|
+
msg = f"MN001 variable name too short (min {self.min_length} chars)"
|
|
304
|
+
candidate = (lineno, col_offset, msg, Minerva)
|
|
305
|
+
if candidate not in self.violations:
|
|
306
|
+
self.violations.append(candidate)
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if len(name) > self.max_length:
|
|
310
|
+
msg = f"MN002 variable name too long (max {self.max_length} chars)"
|
|
311
|
+
candidate = (lineno, col_offset, msg, Minerva)
|
|
312
|
+
if candidate not in self.violations:
|
|
313
|
+
self.violations.append(candidate)
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
if name.isupper():
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
if self.enforce_snake_case:
|
|
320
|
+
if not self.snake_case_pattern.match(name):
|
|
321
|
+
msg = "MN003 variable name must be in snake_case"
|
|
322
|
+
candidate = (lineno, col_offset, msg, Minerva)
|
|
323
|
+
if candidate not in self.violations:
|
|
324
|
+
self.violations.append(candidate)
|
|
325
|
+
|
|
326
|
+
def _check_import(self, module_name, lineno, col_offset):
|
|
327
|
+
"""
|
|
328
|
+
Проверка модуля на наличие в списке ЗАПРЕЩЕННЫХ (Black List)
|
|
329
|
+
"""
|
|
330
|
+
if not self.prohibited_modules:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
base_module = module_name.split(".")[0]
|
|
334
|
+
|
|
335
|
+
if base_module in self.prohibited_modules:
|
|
336
|
+
msg = f"MN004 import of '{base_module}' is prohibited"
|
|
337
|
+
candidate = (lineno, col_offset, msg, Minerva)
|
|
338
|
+
if candidate not in self.violations:
|
|
339
|
+
self.violations.append(candidate)
|
|
340
|
+
|
|
341
|
+
def _check_constructor_call(self, node, constructor_name):
|
|
342
|
+
"""
|
|
343
|
+
Проверка вызова конструктора коллекции.
|
|
344
|
+
- MN007: конструктор в списке запрещенных
|
|
345
|
+
- MN009: тип в check_constructor должен создаваться через литерал
|
|
346
|
+
"""
|
|
347
|
+
if constructor_name in self.prohibited_constructors:
|
|
348
|
+
msg = f"MN007 use of prohibited constructor: {constructor_name}()"
|
|
349
|
+
candidate = (node.lineno, node.col_offset, msg, Minerva)
|
|
350
|
+
if candidate not in self.violations:
|
|
351
|
+
self.violations.append(candidate)
|
|
352
|
+
|
|
353
|
+
if constructor_name in self.check_constructor:
|
|
354
|
+
msg = f"MN009 {constructor_name} must be created via literal, not constructor"
|
|
355
|
+
candidate = (node.lineno, node.col_offset, msg, Minerva)
|
|
356
|
+
if candidate not in self.violations:
|
|
357
|
+
self.violations.append(candidate)
|
|
358
|
+
|
|
359
|
+
def _check_literal(self, node, collection_type):
|
|
360
|
+
"""
|
|
361
|
+
Проверка литерала коллекции.
|
|
362
|
+
- MN008: тип не в белом списке (если задан)
|
|
363
|
+
"""
|
|
364
|
+
if self.allowed_collections:
|
|
365
|
+
if collection_type not in self.allowed_collections:
|
|
366
|
+
allowed_str = ", ".join(sorted(self.allowed_collections))
|
|
367
|
+
msg = (
|
|
368
|
+
f"MN008 use of non-allowed collection type '{collection_type}'. "
|
|
369
|
+
f"Allowed types: {allowed_str}"
|
|
370
|
+
)
|
|
371
|
+
candidate = (node.lineno, node.col_offset, msg, Minerva)
|
|
372
|
+
if candidate not in self.violations:
|
|
373
|
+
self.violations.append(candidate)
|
|
374
|
+
|
|
375
|
+
def visit_Import(self, node: ast.Import):
|
|
376
|
+
for alias in node.names:
|
|
377
|
+
self._check_import(alias.name, node.lineno, node.col_offset)
|
|
378
|
+
self.generic_visit(node)
|
|
379
|
+
|
|
380
|
+
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
381
|
+
if node.module:
|
|
382
|
+
self._check_import(node.module, node.lineno, node.col_offset)
|
|
383
|
+
self.generic_visit(node)
|
|
384
|
+
|
|
385
|
+
def visit_Name(self, node: ast.Name):
|
|
386
|
+
if isinstance(node.ctx, ast.Store):
|
|
387
|
+
self._check_name(node.id, node.lineno, node.col_offset)
|
|
388
|
+
else:
|
|
389
|
+
self.generic_visit(node)
|
|
390
|
+
|
|
391
|
+
def visit_arg(self, node: ast.arg):
|
|
392
|
+
self._check_name(node.arg, node.lineno, node.col_offset)
|
|
393
|
+
self.generic_visit(node)
|
|
394
|
+
|
|
395
|
+
def visit_For(self, node: ast.For):
|
|
396
|
+
self._visit_target(node.target)
|
|
397
|
+
self.generic_visit(node)
|
|
398
|
+
|
|
399
|
+
def visit_AsyncFor(self, node: ast.AsyncFor):
|
|
400
|
+
self._visit_target(node.target)
|
|
401
|
+
self.generic_visit(node)
|
|
402
|
+
|
|
403
|
+
def visit_Assign(self, node: ast.Assign):
|
|
404
|
+
for target in node.targets:
|
|
405
|
+
self._visit_target(target)
|
|
406
|
+
self.generic_visit(node)
|
|
407
|
+
|
|
408
|
+
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
409
|
+
if isinstance(node.target, ast.Name):
|
|
410
|
+
self._check_name(node.target.id, node.target.lineno, node.target.col_offset)
|
|
411
|
+
else:
|
|
412
|
+
self.generic_visit(node)
|
|
413
|
+
|
|
414
|
+
def visit_NamedExpr(self, node: ast.NamedExpr):
|
|
415
|
+
if isinstance(node.target, ast.Name):
|
|
416
|
+
self._check_name(node.target.id, node.target.lineno, node.target.col_offset)
|
|
417
|
+
else:
|
|
418
|
+
self.generic_visit(node)
|
|
419
|
+
|
|
420
|
+
def _visit_target(self, target: ast.expr):
|
|
421
|
+
"""
|
|
422
|
+
Рекурсивный обход целей присваивания
|
|
423
|
+
"""
|
|
424
|
+
if isinstance(target, ast.Name):
|
|
425
|
+
self._check_name(target.id, target.lineno, target.col_offset)
|
|
426
|
+
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
427
|
+
for elt in target.elts:
|
|
428
|
+
self._visit_target(elt)
|
|
429
|
+
|
|
430
|
+
def visit_Call(self, node: ast.Call):
|
|
431
|
+
"""
|
|
432
|
+
Вызовы конструкторов: list(), dict(), set()
|
|
433
|
+
"""
|
|
434
|
+
if isinstance(node.func, ast.Name) and node.func.id in (
|
|
435
|
+
"list", "dict", "set"
|
|
436
|
+
):
|
|
437
|
+
self._check_constructor_call(node, node.func.id)
|
|
438
|
+
self.generic_visit(node)
|
|
439
|
+
|
|
440
|
+
def visit_List(self, node: ast.List):
|
|
441
|
+
"""
|
|
442
|
+
Литерал списка: [1, 2, 3]
|
|
443
|
+
"""
|
|
444
|
+
self._check_literal(node, "list")
|
|
445
|
+
self.generic_visit(node)
|
|
446
|
+
|
|
447
|
+
def visit_Dict(self, node: ast.Dict):
|
|
448
|
+
"""
|
|
449
|
+
Литерал словаря: {"a": 1}
|
|
450
|
+
"""
|
|
451
|
+
self._check_literal(node, "dict")
|
|
452
|
+
self.generic_visit(node)
|
|
453
|
+
|
|
454
|
+
def visit_Set(self, node: ast.Set):
|
|
455
|
+
"""
|
|
456
|
+
Литерал множества: {1, 2, 3}
|
|
457
|
+
"""
|
|
458
|
+
self._check_literal(node, "set")
|
|
459
|
+
self.generic_visit(node)
|
|
460
|
+
|
|
461
|
+
def visit_ListComp(self, node: ast.ListComp):
|
|
462
|
+
"""
|
|
463
|
+
List comprehension: [x for x in ...]
|
|
464
|
+
"""
|
|
465
|
+
self._check_literal(node, "list")
|
|
466
|
+
self.generic_visit(node)
|
|
467
|
+
|
|
468
|
+
def visit_DictComp(self, node: ast.DictComp):
|
|
469
|
+
"""
|
|
470
|
+
Dict comprehension: {k: v for ...}
|
|
471
|
+
"""
|
|
472
|
+
self._check_literal(node, "dict")
|
|
473
|
+
self.generic_visit(node)
|
|
474
|
+
|
|
475
|
+
def visit_SetComp(self, node: ast.SetComp):
|
|
476
|
+
"""
|
|
477
|
+
Set comprehension: {x for x in ...}
|
|
478
|
+
"""
|
|
479
|
+
self._check_literal(node, "set")
|
|
480
|
+
self.generic_visit(node)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"min_length": 2,
|
|
3
|
+
"max_length": 40,
|
|
4
|
+
"allowed_single_letters": "i,j,x,y,e",
|
|
5
|
+
"enforce_snake_case": true,
|
|
6
|
+
"prohibited_modules": "math,re",
|
|
7
|
+
"max_line_length": 99,
|
|
8
|
+
"max_char_code": 1000,
|
|
9
|
+
"prohibited_constructors": "list,set",
|
|
10
|
+
"allowed_collections": "dict",
|
|
11
|
+
"check_constructor": "list,dict"
|
|
12
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minerva-plugin
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Minerva - Flake8 plugin for Python code quality checks (SAST)
|
|
5
|
+
Home-page: https://github.com/pascal65536/minerva-plugin
|
|
6
|
+
Author: pascal65536
|
|
7
|
+
Author-email: pascal65536 <pascal65536@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/pascal65536/minerva-plugin
|
|
10
|
+
Project-URL: Issues, https://github.com/pascal65536/minerva-plugin/issues
|
|
11
|
+
Keywords: flake8,linting,code-quality,static-analysis,sast
|
|
12
|
+
Classifier: Framework :: Flake8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Development Status :: 4 - Beta
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: flake8>=3.8.0
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-python
|
|
27
|
+
|
|
28
|
+
# Minerva - Flake8 Plugin
|
|
29
|
+
|
|
30
|
+
Minerva — это плагин для Flake8, который проверяет качество кода на Python.
|
|
31
|
+
|
|
32
|
+
## Возможности
|
|
33
|
+
|
|
34
|
+
### MN001 - MN003: Проверка имён переменных
|
|
35
|
+
- Минимальная длина имени переменной
|
|
36
|
+
- Максимальная длина имени переменной
|
|
37
|
+
- Требование snake_case для имён
|
|
38
|
+
|
|
39
|
+
### MN004: Проверка импортов
|
|
40
|
+
- Чёрный список запрещённых модулей
|
|
41
|
+
|
|
42
|
+
### MN005 - MN006: Проверка строк
|
|
43
|
+
- Максимальная длина строки
|
|
44
|
+
- Запрет символов с кодом > 1000
|
|
45
|
+
|
|
46
|
+
### MN007 - MN009: Проверка коллекций
|
|
47
|
+
- Запрет конструкторов (list(), dict(), set())
|
|
48
|
+
- Белый список разрешённых типов коллекций
|
|
49
|
+
- Требование создания через литералы
|
|
50
|
+
|
|
51
|
+
## Установка
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install minerva-plugin
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Использование
|
|
58
|
+
|
|
59
|
+
Плагин автоматически интегрируется с Flake8:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
flake8 your_code.py
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Правила
|
|
66
|
+
|
|
67
|
+
| Код | Описание |
|
|
68
|
+
|-----|----------|
|
|
69
|
+
| MN001 | Имя переменной слишком короткое |
|
|
70
|
+
| MN002 | Имя переменной слишком длинное |
|
|
71
|
+
| MN003 | Имя не в snake_case |
|
|
72
|
+
| MN004 | Импорт запрещённого модуля |
|
|
73
|
+
| MN005 | Строка длиннее лимита |
|
|
74
|
+
| MN006 | Символ с кодом > лимита |
|
|
75
|
+
| MN007 | Использование запрещённого конструктора |
|
|
76
|
+
| MN008 | Использование неразрешённого типа коллекции |
|
|
77
|
+
| MN009 | Коллекция создана через конструктор вместо литерала |
|
|
78
|
+
|
|
79
|
+
## Настройка
|
|
80
|
+
|
|
81
|
+
Настройки хранятся в `settings/plugin.json`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"min_length": 2,
|
|
86
|
+
"max_length": 40,
|
|
87
|
+
"allowed_single_letters": "i,j,x,y,e",
|
|
88
|
+
"enforce_snake_case": true,
|
|
89
|
+
"prohibited_modules": "math,re",
|
|
90
|
+
"max_line_length": 123,
|
|
91
|
+
"max_char_code": 1000,
|
|
92
|
+
"prohibited_constructors": "list,set",
|
|
93
|
+
"allowed_collections": "dict",
|
|
94
|
+
"check_constructor": "list,dict"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
minerva_plugin/__init__.py,sha256=ACD8HWICQxP-ghx6OdpJlWiX8sY_euarJVB8svnY6Zc,17704
|
|
2
|
+
minerva_plugin-2.1.0.data/data/settings/plugin.json,sha256=OFfy_j_QFqOc-nXjfUJX0LYsowq6Opm7DwN5wBILoc0,327
|
|
3
|
+
minerva_plugin-2.1.0.dist-info/licenses/LICENSE,sha256=BEakLgr_-0FLterMt6F017pa6f2YbZ9eUun_JFBCquI,1073
|
|
4
|
+
minerva_plugin-2.1.0.dist-info/METADATA,sha256=v365PuxaoUHz2iCthBWiCT-t9TaGKeB5Kq8qA9DzI9E,3307
|
|
5
|
+
minerva_plugin-2.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
minerva_plugin-2.1.0.dist-info/entry_points.txt,sha256=A0a6vVTATx8MFvPEDDti9kQvEtaPlS88WSozS2vwb7Y,47
|
|
7
|
+
minerva_plugin-2.1.0.dist-info/top_level.txt,sha256=HB_XJ3U2ftZ0vZgrPf_P4Zsv4gglc8OQzt8YfNz72qg,15
|
|
8
|
+
minerva_plugin-2.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergey Pakhtusov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
minerva_plugin
|