minerva-plugin 2.1.0__tar.gz

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.
@@ -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,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pyproject.toml
4
+ recursive-include settings *.json
@@ -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,69 @@
1
+ # Minerva - Flake8 Plugin
2
+
3
+ Minerva — это плагин для Flake8, который проверяет качество кода на Python.
4
+
5
+ ## Возможности
6
+
7
+ ### MN001 - MN003: Проверка имён переменных
8
+ - Минимальная длина имени переменной
9
+ - Максимальная длина имени переменной
10
+ - Требование snake_case для имён
11
+
12
+ ### MN004: Проверка импортов
13
+ - Чёрный список запрещённых модулей
14
+
15
+ ### MN005 - MN006: Проверка строк
16
+ - Максимальная длина строки
17
+ - Запрет символов с кодом > 1000
18
+
19
+ ### MN007 - MN009: Проверка коллекций
20
+ - Запрет конструкторов (list(), dict(), set())
21
+ - Белый список разрешённых типов коллекций
22
+ - Требование создания через литералы
23
+
24
+ ## Установка
25
+
26
+ ```bash
27
+ pip install minerva-plugin
28
+ ```
29
+
30
+ ## Использование
31
+
32
+ Плагин автоматически интегрируется с Flake8:
33
+
34
+ ```bash
35
+ flake8 your_code.py
36
+ ```
37
+
38
+ ## Правила
39
+
40
+ | Код | Описание |
41
+ |-----|----------|
42
+ | MN001 | Имя переменной слишком короткое |
43
+ | MN002 | Имя переменной слишком длинное |
44
+ | MN003 | Имя не в snake_case |
45
+ | MN004 | Импорт запрещённого модуля |
46
+ | MN005 | Строка длиннее лимита |
47
+ | MN006 | Символ с кодом > лимита |
48
+ | MN007 | Использование запрещённого конструктора |
49
+ | MN008 | Использование неразрешённого типа коллекции |
50
+ | MN009 | Коллекция создана через конструктор вместо литерала |
51
+
52
+ ## Настройка
53
+
54
+ Настройки хранятся в `settings/plugin.json`:
55
+
56
+ ```json
57
+ {
58
+ "min_length": 2,
59
+ "max_length": 40,
60
+ "allowed_single_letters": "i,j,x,y,e",
61
+ "enforce_snake_case": true,
62
+ "prohibited_modules": "math,re",
63
+ "max_line_length": 123,
64
+ "max_char_code": 1000,
65
+ "prohibited_constructors": "list,set",
66
+ "allowed_collections": "dict",
67
+ "check_constructor": "list,dict"
68
+ }
69
+ ```
@@ -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,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,13 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ minerva_plugin/__init__.py
7
+ minerva_plugin.egg-info/PKG-INFO
8
+ minerva_plugin.egg-info/SOURCES.txt
9
+ minerva_plugin.egg-info/dependency_links.txt
10
+ minerva_plugin.egg-info/entry_points.txt
11
+ minerva_plugin.egg-info/requires.txt
12
+ minerva_plugin.egg-info/top_level.txt
13
+ settings/plugin.json
@@ -0,0 +1,2 @@
1
+ [flake8.extension]
2
+ MN = minerva_plugin:Minerva
@@ -0,0 +1 @@
1
+ flake8>=3.8.0
@@ -0,0 +1 @@
1
+ minerva_plugin
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "minerva-plugin"
7
+ version = "2.1.0"
8
+ description = "Minerva - Flake8 plugin for Python code quality checks (SAST)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "pascal65536", email = "pascal65536@gmail.com"}
14
+ ]
15
+ keywords = ["flake8", "linting", "code-quality", "static-analysis", "sast"]
16
+ classifiers = [
17
+ "Framework :: Flake8",
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Developers",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ ]
25
+ dependencies = [
26
+ "flake8>=3.8.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/pascal65536/minerva-plugin"
31
+ Issues = "https://github.com/pascal65536/minerva-plugin/issues"
32
+
33
+ [project.entry-points."flake8.extension"]
34
+ MN = "minerva_plugin:Minerva"
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,54 @@
1
+ from setuptools import setup, find_packages
2
+ from pathlib import Path
3
+
4
+ this_directory = Path(__file__).parent
5
+ long_description = ""
6
+ readme_path = this_directory / "README.md"
7
+ if readme_path.exists():
8
+ long_description = readme_path.read_text(encoding="utf-8")
9
+
10
+ setup(
11
+ name="minerva-plugin",
12
+ version="2.1.0",
13
+ description="Minerva - Flake8 plugin for Python code quality checks (SAST)",
14
+ long_description=long_description,
15
+ long_description_content_type="text/markdown",
16
+ author="pascal65536",
17
+ author_email="pascal65536@gmail.com",
18
+ url="https://github.com/pascal65536/minerva-plugin",
19
+ packages=find_packages(),
20
+ include_package_data=True,
21
+ package_data={
22
+ "": ["settings/*.json"],
23
+ },
24
+ data_files=[
25
+ ("settings", ["settings/plugin.json"]),
26
+ ],
27
+ entry_points={
28
+ "flake8.extension": [
29
+ "MN = minerva_plugin:Minerva",
30
+ ],
31
+ },
32
+ install_requires=[
33
+ "flake8>=3.8.0",
34
+ ],
35
+ python_requires=">=3.8",
36
+ classifiers=[
37
+ "Framework :: Flake8",
38
+ "Programming Language :: Python :: 3",
39
+ "Programming Language :: Python :: 3.8",
40
+ "Programming Language :: Python :: 3.9",
41
+ "Programming Language :: Python :: 3.10",
42
+ "Programming Language :: Python :: 3.11",
43
+ "Programming Language :: Python :: 3.12",
44
+ "Programming Language :: Python :: 3.13",
45
+ "Programming Language :: Python :: 3.14",
46
+ "Programming Language :: Python :: 3.15",
47
+ "License :: OSI Approved :: MIT License",
48
+ "Operating System :: OS Independent",
49
+ "Development Status :: 4 - Beta",
50
+ "Intended Audience :: Developers",
51
+ "Topic :: Software Development :: Quality Assurance",
52
+ ],
53
+ keywords="flake8 linting code-quality static-analysis sast",
54
+ )