mofox-plugin-dev-toolkit 0.3.3__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.
- mofox_plugin_dev_toolkit-0.3.3.dist-info/METADATA +730 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/RECORD +46 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/WHEEL +5 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/entry_points.txt +2 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/licenses/LICENSE +674 -0
- mofox_plugin_dev_toolkit-0.3.3.dist-info/top_level.txt +1 -0
- mpdt/__init__.py +15 -0
- mpdt/__main__.py +8 -0
- mpdt/cli.py +316 -0
- mpdt/commands/__init__.py +9 -0
- mpdt/commands/check.py +498 -0
- mpdt/commands/dev.py +318 -0
- mpdt/commands/generate.py +448 -0
- mpdt/commands/init.py +686 -0
- mpdt/dev/bridge_plugin/__init__.py +17 -0
- mpdt/dev/bridge_plugin/cleanup_handler.py +65 -0
- mpdt/dev/bridge_plugin/dev_config.py +24 -0
- mpdt/dev/bridge_plugin/file_watcher.py +169 -0
- mpdt/dev/bridge_plugin/plugin.py +219 -0
- mpdt/templates/__init__.py +165 -0
- mpdt/templates/action_template.py +102 -0
- mpdt/templates/adapter_template.py +129 -0
- mpdt/templates/chatter_template.py +103 -0
- mpdt/templates/event_template.py +116 -0
- mpdt/templates/plus_command_template.py +150 -0
- mpdt/templates/prompt_template.py +92 -0
- mpdt/templates/router_template.py +175 -0
- mpdt/templates/tool_template.py +98 -0
- mpdt/utils/__init__.py +10 -0
- mpdt/utils/code_parser.py +401 -0
- mpdt/utils/color_printer.py +99 -0
- mpdt/utils/config_loader.py +171 -0
- mpdt/utils/config_manager.py +297 -0
- mpdt/utils/file_ops.py +207 -0
- mpdt/utils/license_generator.py +980 -0
- mpdt/utils/plugin_parser.py +195 -0
- mpdt/utils/template_engine.py +112 -0
- mpdt/validators/__init__.py +26 -0
- mpdt/validators/auto_fix_validator.py +990 -0
- mpdt/validators/base.py +129 -0
- mpdt/validators/component_validator.py +842 -0
- mpdt/validators/config_validator.py +119 -0
- mpdt/validators/metadata_validator.py +107 -0
- mpdt/validators/structure_validator.py +72 -0
- mpdt/validators/style_validator.py +117 -0
- mpdt/validators/type_validator.py +206 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置文件验证器
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from ..utils.code_parser import CodeParser
|
|
6
|
+
from .base import BaseValidator, ValidationResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigValidator(BaseValidator):
|
|
10
|
+
"""配置文件验证器
|
|
11
|
+
|
|
12
|
+
仅检查插件类中定义的 config_schema 是否正确
|
|
13
|
+
不验证 config.toml 文件(因为它会在运行时自动生成)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def validate(self) -> ValidationResult:
|
|
17
|
+
"""执行配置验证
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
ValidationResult: 验证结果
|
|
21
|
+
"""
|
|
22
|
+
# 获取插件名称
|
|
23
|
+
plugin_name = self._get_plugin_name()
|
|
24
|
+
if not plugin_name:
|
|
25
|
+
self.result.add_error("无法确定插件名称")
|
|
26
|
+
return self.result
|
|
27
|
+
|
|
28
|
+
plugin_file = self.plugin_path / "plugin.py"
|
|
29
|
+
|
|
30
|
+
if not plugin_file.exists():
|
|
31
|
+
self.result.add_error("插件文件不存在: plugin.py")
|
|
32
|
+
return self.result
|
|
33
|
+
|
|
34
|
+
# 解析 plugin.py 查找 config_schema
|
|
35
|
+
config_schema = self._extract_config_schema(plugin_file, plugin_name)
|
|
36
|
+
|
|
37
|
+
if config_schema is None:
|
|
38
|
+
# 没有定义 config_schema,这是正常的
|
|
39
|
+
self.result.add_warning(
|
|
40
|
+
"插件未定义配置 schema",
|
|
41
|
+
file_path="plugin.py",
|
|
42
|
+
suggestion="最好定义config_schema启用插件配置系统",
|
|
43
|
+
)
|
|
44
|
+
return self.result
|
|
45
|
+
|
|
46
|
+
# 验证 config_schema 的结构
|
|
47
|
+
if not config_schema:
|
|
48
|
+
self.result.add_warning(
|
|
49
|
+
"config_schema 已定义但为空",
|
|
50
|
+
file_path="plugin.py",
|
|
51
|
+
suggestion="最好往里面加入一些配置项",
|
|
52
|
+
)
|
|
53
|
+
return self.result
|
|
54
|
+
|
|
55
|
+
# 检查是否定义了 config_file_name
|
|
56
|
+
has_config_file_name = self._check_config_file_name(plugin_file, plugin_name)
|
|
57
|
+
if not has_config_file_name:
|
|
58
|
+
self.result.add_warning(
|
|
59
|
+
"定义了 config_schema 但未定义 config_file_name",
|
|
60
|
+
file_path="plugin.py",
|
|
61
|
+
suggestion="请在插件类中添加: config_file_name = 'config.toml'",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 验证每个配置节
|
|
65
|
+
for section_name, section_content in config_schema.items():
|
|
66
|
+
if not section_name:
|
|
67
|
+
self.result.add_error(
|
|
68
|
+
"config_schema 中存在空的配置节名",
|
|
69
|
+
file_path="plugin.py",
|
|
70
|
+
)
|
|
71
|
+
elif not isinstance(section_content, dict):
|
|
72
|
+
self.result.add_warning(
|
|
73
|
+
f"config_schema 中的 [{section_name}] 节格式不正确",
|
|
74
|
+
file_path="plugin.py",
|
|
75
|
+
suggestion="每个配置节应该是一个字典,包含 ConfigField 定义",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.result.add_info(f"config_schema 定义了 {len(config_schema)} 个配置节")
|
|
79
|
+
return self.result
|
|
80
|
+
|
|
81
|
+
def _check_config_file_name(self, plugin_file, plugin_name: str) -> bool:
|
|
82
|
+
"""检查是否定义了 config_file_name
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
plugin_file: plugin.py 文件路径
|
|
86
|
+
plugin_name: 插件名称
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
是否定义了 config_file_name
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
parser = CodeParser.from_file(plugin_file)
|
|
93
|
+
return parser.has_class_attribute(
|
|
94
|
+
attribute_name="config_file_name",
|
|
95
|
+
base_class="BasePlugin"
|
|
96
|
+
)
|
|
97
|
+
except Exception:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def _extract_config_schema(self, plugin_file, plugin_name: str) -> dict | None:
|
|
101
|
+
"""从 plugin.py 中提取 config_schema 定义
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
plugin_file: plugin.py 文件路径
|
|
105
|
+
plugin_name: 插件名称
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
config_schema 字典,如果未定义返回 None
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
parser = CodeParser.from_file(plugin_file)
|
|
112
|
+
config_schema = parser.find_class_attribute(
|
|
113
|
+
base_class="BasePlugin",
|
|
114
|
+
attribute_name="config_schema"
|
|
115
|
+
)
|
|
116
|
+
return config_schema
|
|
117
|
+
except Exception as e:
|
|
118
|
+
self.result.add_error(f"解析 plugin.py 失败: {e}")
|
|
119
|
+
return None
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
插件元数据验证器
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from ..utils.code_parser import CodeParser
|
|
6
|
+
from .base import BaseValidator, ValidationResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MetadataValidator(BaseValidator):
|
|
10
|
+
"""插件元数据验证器
|
|
11
|
+
|
|
12
|
+
检查 plugin.py 中的 PluginMetadata 是否完整
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# 必需的元数据字段
|
|
16
|
+
REQUIRED_FIELDS = ["name", "description", "usage"]
|
|
17
|
+
|
|
18
|
+
# 推荐的元数据字段
|
|
19
|
+
RECOMMENDED_FIELDS = ["version", "author", "license"]
|
|
20
|
+
|
|
21
|
+
def validate(self) -> ValidationResult:
|
|
22
|
+
"""执行元数据验证
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
ValidationResult: 验证结果
|
|
26
|
+
"""
|
|
27
|
+
# 获取插件名称
|
|
28
|
+
plugin_name = self._get_plugin_name()
|
|
29
|
+
if not plugin_name:
|
|
30
|
+
self.result.add_error("无法确定插件名称")
|
|
31
|
+
return self.result
|
|
32
|
+
|
|
33
|
+
# 元数据在 __init__.py 中
|
|
34
|
+
init_file = self.plugin_path / "__init__.py"
|
|
35
|
+
if not init_file.exists():
|
|
36
|
+
self.result.add_error(
|
|
37
|
+
"__init__.py 文件不存在",
|
|
38
|
+
suggestion="请创建 __init__.py 文件并定义 __plugin_meta__",
|
|
39
|
+
)
|
|
40
|
+
return self.result
|
|
41
|
+
|
|
42
|
+
# 使用 CodeParser 解析
|
|
43
|
+
try:
|
|
44
|
+
parser = CodeParser.from_file(init_file)
|
|
45
|
+
except SyntaxError as e:
|
|
46
|
+
self.result.add_error(
|
|
47
|
+
f"__init__.py 存在语法错误: {e.msg}",
|
|
48
|
+
file_path="__init__.py",
|
|
49
|
+
line_number=e.lineno if hasattr(e, "lineno") else None,
|
|
50
|
+
)
|
|
51
|
+
return self.result
|
|
52
|
+
except Exception as e:
|
|
53
|
+
self.result.add_error(f"读取 __init__.py 失败: {e}")
|
|
54
|
+
return self.result
|
|
55
|
+
|
|
56
|
+
# 查找 __plugin_meta__ 赋值
|
|
57
|
+
metadata_values = parser.find_assignments("__plugin_meta__")
|
|
58
|
+
|
|
59
|
+
if not metadata_values:
|
|
60
|
+
self.result.add_error(
|
|
61
|
+
"未找到 __plugin_meta__ 变量或 PluginMetadata 实例",
|
|
62
|
+
file_path="__init__.py",
|
|
63
|
+
suggestion="请在 __init__.py 中定义: __plugin_meta__ = PluginMetadata(...) | 可运行 'mpdt check --fix' 自动修复",
|
|
64
|
+
)
|
|
65
|
+
return self.result
|
|
66
|
+
|
|
67
|
+
# 使用增强的 CodeParser 解析 PluginMetadata 的参数
|
|
68
|
+
metadata_args = parser.find_call_arguments("__plugin_meta__", "PluginMetadata")
|
|
69
|
+
|
|
70
|
+
if metadata_args is None:
|
|
71
|
+
self.result.add_error(
|
|
72
|
+
"未找到 __plugin_meta__ 的 PluginMetadata 调用",
|
|
73
|
+
file_path="__init__.py",
|
|
74
|
+
suggestion="请使用 PluginMetadata(...) 构造 __plugin_meta__ | 可运行 'mpdt check --fix' 自动修复",
|
|
75
|
+
)
|
|
76
|
+
return self.result
|
|
77
|
+
|
|
78
|
+
self.result.add_info("找到 __plugin_meta__ 定义")
|
|
79
|
+
|
|
80
|
+
# 检查必需字段
|
|
81
|
+
missing_required = parser.get_missing_call_arguments("__plugin_meta__", self.REQUIRED_FIELDS, "PluginMetadata")
|
|
82
|
+
|
|
83
|
+
if missing_required:
|
|
84
|
+
for field in missing_required:
|
|
85
|
+
self.result.add_error(
|
|
86
|
+
f"PluginMetadata 缺少必需字段: {field}",
|
|
87
|
+
file_path="__init__.py",
|
|
88
|
+
suggestion=f'请在 PluginMetadata 中添加 {field}="..." 参数 | 可运行 \'mpdt check --fix\' 自动修复',
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
self.result.add_info("所有必需的元数据字段都已提供")
|
|
92
|
+
|
|
93
|
+
# 检查推荐字段
|
|
94
|
+
missing_recommended = []
|
|
95
|
+
for field in self.RECOMMENDED_FIELDS:
|
|
96
|
+
if field not in metadata_args or not metadata_args[field]:
|
|
97
|
+
missing_recommended.append(field)
|
|
98
|
+
|
|
99
|
+
if missing_recommended:
|
|
100
|
+
fields_str = ", ".join(f'{f}="..."' for f in missing_recommended)
|
|
101
|
+
self.result.add_warning(
|
|
102
|
+
f"建议添加以下元数据字段: {', '.join(missing_recommended)}",
|
|
103
|
+
file_path="__init__.py",
|
|
104
|
+
suggestion=f"在 PluginMetadata 中添加: {fields_str}",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return self.result
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
插件结构验证器
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import BaseValidator, ValidationResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StructureValidator(BaseValidator):
|
|
9
|
+
"""插件结构验证器
|
|
10
|
+
|
|
11
|
+
检查插件的目录结构和必需文件
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# 必需的文件
|
|
15
|
+
REQUIRED_FILES = ["__init__.py", "plugin.py"]
|
|
16
|
+
|
|
17
|
+
# 推荐的文件
|
|
18
|
+
RECOMMENDED_FILES = ["README.md"]
|
|
19
|
+
|
|
20
|
+
# 推荐的目录
|
|
21
|
+
RECOMMENDED_DIRS = ["tests", "docs"]
|
|
22
|
+
|
|
23
|
+
def validate(self) -> ValidationResult:
|
|
24
|
+
"""执行结构验证
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
ValidationResult: 验证结果
|
|
28
|
+
"""
|
|
29
|
+
# 获取插件名称
|
|
30
|
+
plugin_name = self._get_plugin_name()
|
|
31
|
+
if not plugin_name:
|
|
32
|
+
self.result.add_error(
|
|
33
|
+
"无法确定插件名称,请确保插件目录结构正确",
|
|
34
|
+
suggestion="插件应该有 plugin.py 文件",
|
|
35
|
+
)
|
|
36
|
+
return self.result
|
|
37
|
+
|
|
38
|
+
# 插件代码目录就是根目录
|
|
39
|
+
plugin_code_dir = self.plugin_path
|
|
40
|
+
|
|
41
|
+
# 检查必需的文件
|
|
42
|
+
for file_name in self.REQUIRED_FILES:
|
|
43
|
+
file_path = plugin_code_dir / file_name
|
|
44
|
+
if not file_path.exists():
|
|
45
|
+
self.result.add_error(
|
|
46
|
+
f"缺少必需文件: {file_name}",
|
|
47
|
+
file_path=str(file_path.relative_to(self.plugin_path)),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 检查推荐的文件
|
|
51
|
+
for file_name in self.RECOMMENDED_FILES:
|
|
52
|
+
file_path = self.plugin_path / file_name
|
|
53
|
+
parent_file_path = self.plugin_path.parent / file_name
|
|
54
|
+
if not file_path.exists() and not parent_file_path.exists():
|
|
55
|
+
self.result.add_warning(
|
|
56
|
+
f"缺少推荐文件: {file_name}",
|
|
57
|
+
file_path=file_name,
|
|
58
|
+
suggestion=f"建议添加 {file_name} 以提供更好的文档或依赖管理",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# 检查推荐的目录
|
|
62
|
+
for dir_name in self.RECOMMENDED_DIRS:
|
|
63
|
+
dir_path = self.plugin_path / dir_name
|
|
64
|
+
parent_dir_path = self.plugin_path.parent / dir_name
|
|
65
|
+
if not dir_path.exists() and not parent_dir_path.exists():
|
|
66
|
+
self.result.add_warning(
|
|
67
|
+
f"缺少推荐目录: {dir_name}/",
|
|
68
|
+
file_path=dir_name,
|
|
69
|
+
suggestion=f"建议添加 {dir_name}/ 目录",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return self.result
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""代码风格验证器
|
|
2
|
+
|
|
3
|
+
使用 ruff 检查代码风格问题
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .base import BaseValidator, ValidationResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StyleValidator(BaseValidator):
|
|
15
|
+
"""代码风格验证器
|
|
16
|
+
|
|
17
|
+
使用 ruff 检查代码风格和代码质量问题
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, plugin_path: Path):
|
|
21
|
+
super().__init__(plugin_path)
|
|
22
|
+
|
|
23
|
+
def validate(self) -> ValidationResult:
|
|
24
|
+
"""执行代码风格检查"""
|
|
25
|
+
result = ValidationResult(
|
|
26
|
+
validator_name="StyleValidator",
|
|
27
|
+
success=True
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
plugin_name = self._get_plugin_name()
|
|
31
|
+
if not plugin_name:
|
|
32
|
+
result.add_error("无法确定插件名称")
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
# 检查 ruff 是否安装
|
|
36
|
+
if not self._is_ruff_installed():
|
|
37
|
+
result.add_warning("未安装 ruff,跳过代码风格检查", suggestion="运行 'pip install ruff' 安装")
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
# 运行 ruff check
|
|
41
|
+
issues = self._run_ruff_check()
|
|
42
|
+
|
|
43
|
+
if issues:
|
|
44
|
+
for issue in issues:
|
|
45
|
+
result.add_warning(
|
|
46
|
+
issue["message"],
|
|
47
|
+
file_path=issue.get("file"),
|
|
48
|
+
line_number=issue.get("line"),
|
|
49
|
+
suggestion=issue.get("suggestion"),
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
result.add_info("代码风格检查通过,未发现问题")
|
|
53
|
+
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
def _is_ruff_installed(self) -> bool:
|
|
57
|
+
"""检查 ruff 是否安装"""
|
|
58
|
+
try:
|
|
59
|
+
subprocess.run(["ruff", "--version"], capture_output=True, check=True, encoding='utf-8', errors='ignore')
|
|
60
|
+
return True
|
|
61
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def _run_ruff_check(self) -> list[dict[str, Any]]:
|
|
65
|
+
"""运行 ruff 检查
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
问题列表
|
|
69
|
+
"""
|
|
70
|
+
issues = []
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# 构建命令
|
|
74
|
+
cmd = ["ruff", "check", "--output-format", "json", str(self.plugin_path)]
|
|
75
|
+
|
|
76
|
+
# 运行 ruff
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
cmd,
|
|
79
|
+
capture_output=True,
|
|
80
|
+
text=True,
|
|
81
|
+
encoding='utf-8',
|
|
82
|
+
errors='ignore'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# 解析输出
|
|
86
|
+
if result.stdout.strip():
|
|
87
|
+
try:
|
|
88
|
+
ruff_output = json.loads(result.stdout)
|
|
89
|
+
for item in ruff_output:
|
|
90
|
+
issues.append(
|
|
91
|
+
{
|
|
92
|
+
"file": str(Path(item["filename"]).relative_to(self.plugin_path)),
|
|
93
|
+
"line": item["location"]["row"],
|
|
94
|
+
"message": f"{item['code']}: {item['message']}",
|
|
95
|
+
"suggestion": self._get_fix_suggestion(item),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
except json.JSONDecodeError:
|
|
99
|
+
# 如果不是 JSON 格式,尝试解析纯文本
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
# 不抛出异常,只记录问题
|
|
104
|
+
issues.append({
|
|
105
|
+
"file": None,
|
|
106
|
+
"line": None,
|
|
107
|
+
"message": f"运行 ruff 时出错: {e}",
|
|
108
|
+
"suggestion": None
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return issues
|
|
112
|
+
|
|
113
|
+
def _get_fix_suggestion(self, item: dict) -> str | None:
|
|
114
|
+
"""获取修复建议"""
|
|
115
|
+
if item.get("fix"):
|
|
116
|
+
return "可自动修复,使用 --fix 选项"
|
|
117
|
+
return None
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""类型检查验证器
|
|
2
|
+
|
|
3
|
+
使用 mypy 进行类型检查
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from mpdt.utils.config_manager import MPDTConfig
|
|
11
|
+
|
|
12
|
+
from .base import BaseValidator, ValidationResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TypeValidator(BaseValidator):
|
|
16
|
+
"""类型检查验证器
|
|
17
|
+
|
|
18
|
+
使用 mypy 进行静态类型检查
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, plugin_path: Path):
|
|
22
|
+
super().__init__(plugin_path)
|
|
23
|
+
# 尝试找到 MoFox 主项目路径
|
|
24
|
+
self.MoFox_root = MPDTConfig().mofox_path
|
|
25
|
+
|
|
26
|
+
def validate(self) -> ValidationResult:
|
|
27
|
+
"""执行类型检查"""
|
|
28
|
+
result = ValidationResult(
|
|
29
|
+
validator_name="TypeValidator",
|
|
30
|
+
success=True
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
plugin_name = self._get_plugin_name()
|
|
34
|
+
if not plugin_name:
|
|
35
|
+
result.add_error("无法确定插件名称")
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
# 检查 mypy 是否安装
|
|
39
|
+
if not self._is_mypy_installed():
|
|
40
|
+
result.add_warning(
|
|
41
|
+
"未安装 mypy,跳过类型检查",
|
|
42
|
+
suggestion="运行 'pip install mypy' 安装"
|
|
43
|
+
)
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
# 运行 mypy
|
|
47
|
+
issues = self._run_mypy_check()
|
|
48
|
+
|
|
49
|
+
if issues:
|
|
50
|
+
for issue in issues:
|
|
51
|
+
# 根据严重程度决定是错误还是警告
|
|
52
|
+
if issue.get("severity") == "error":
|
|
53
|
+
result.add_error(
|
|
54
|
+
issue["message"],
|
|
55
|
+
file_path=issue.get("file"),
|
|
56
|
+
line_number=issue.get("line"),
|
|
57
|
+
suggestion=issue.get("suggestion")
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
result.add_warning(
|
|
61
|
+
issue["message"],
|
|
62
|
+
file_path=issue.get("file"),
|
|
63
|
+
line_number=issue.get("line"),
|
|
64
|
+
suggestion=issue.get("suggestion")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def _is_mypy_installed(self) -> bool:
|
|
70
|
+
"""检查 mypy 是否安装"""
|
|
71
|
+
try:
|
|
72
|
+
subprocess.run(
|
|
73
|
+
["mypy", "--version"],
|
|
74
|
+
capture_output=True,
|
|
75
|
+
check=True,
|
|
76
|
+
encoding='utf-8',
|
|
77
|
+
errors='ignore'
|
|
78
|
+
)
|
|
79
|
+
return True
|
|
80
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def _run_mypy_check(self) -> list[dict]:
|
|
86
|
+
"""运行 mypy 检查
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
问题列表
|
|
90
|
+
"""
|
|
91
|
+
issues = []
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# 构建命令
|
|
95
|
+
cmd = [
|
|
96
|
+
"mypy",
|
|
97
|
+
str(self.plugin_path),
|
|
98
|
+
"--no-error-summary",
|
|
99
|
+
"--show-column-numbers",
|
|
100
|
+
"--show-error-codes",
|
|
101
|
+
"--no-namespace-packages", # 避免包命名空间问题
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# 如果找到了 MoFox-Bot 主项目,添加到 Python 路径
|
|
105
|
+
if self.MoFox_root:
|
|
106
|
+
cmd.extend(["--python-path", str(self.MoFox_root)])
|
|
107
|
+
|
|
108
|
+
# 运行 mypy
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
cmd,
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
encoding='utf-8',
|
|
114
|
+
errors='ignore'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# 解析输出
|
|
118
|
+
if result.stdout.strip():
|
|
119
|
+
for line in result.stdout.strip().split('\n'):
|
|
120
|
+
issue = self._parse_mypy_line(line)
|
|
121
|
+
if issue:
|
|
122
|
+
# 如果找不到 MoFox 根目录,过滤掉所有 src.* 模块的导入错误
|
|
123
|
+
if not self.MoFox_root:
|
|
124
|
+
# 检查是否是 src.* 模块的导入错误
|
|
125
|
+
if "Cannot find implementation" in issue["message"] and '"src.' in issue["message"]:
|
|
126
|
+
continue # 跳过这个错误
|
|
127
|
+
# 检查是否是因为基类是 Any 导致的错误(因为导入失败)
|
|
128
|
+
if "has type \"Any\"" in issue["message"]:
|
|
129
|
+
continue # 跳过这个错误
|
|
130
|
+
issues.append(issue)
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
issues.append({
|
|
134
|
+
"file": None,
|
|
135
|
+
"line": None,
|
|
136
|
+
"message": f"运行 mypy 时出错: {e}",
|
|
137
|
+
"severity": "error",
|
|
138
|
+
"suggestion": None
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return issues
|
|
142
|
+
|
|
143
|
+
def _parse_mypy_line(self, line: str) -> dict | None:
|
|
144
|
+
"""解析 mypy 输出的一行
|
|
145
|
+
|
|
146
|
+
格式: file.py:123:45: error: Message [error-code]
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
line: mypy 输出的一行
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
解析后的问题字典,如果解析失败返回 None
|
|
153
|
+
"""
|
|
154
|
+
# 匹配 mypy 输出格式
|
|
155
|
+
pattern = r'^(.+?):(\d+):(?:\d+:)?\s+(error|warning|note):\s+(.+?)(?:\s+\[(.+?)\])?$'
|
|
156
|
+
match = re.match(pattern, line)
|
|
157
|
+
|
|
158
|
+
if not match:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
file_path, line_num, severity, message, error_code = match.groups()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# 转换为相对路径
|
|
165
|
+
rel_path = str(Path(file_path).relative_to(self.plugin_path))
|
|
166
|
+
except ValueError:
|
|
167
|
+
rel_path = file_path
|
|
168
|
+
|
|
169
|
+
issue = {
|
|
170
|
+
"file": rel_path,
|
|
171
|
+
"line": int(line_num),
|
|
172
|
+
"message": message.strip(),
|
|
173
|
+
"severity": severity,
|
|
174
|
+
"suggestion": None
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# 添加错误代码到消息中
|
|
178
|
+
if error_code:
|
|
179
|
+
issue["message"] = f"[{error_code}] {issue['message']}"
|
|
180
|
+
|
|
181
|
+
# 根据常见错误提供建议
|
|
182
|
+
issue["suggestion"] = self._get_type_hint_suggestion(message, error_code)
|
|
183
|
+
|
|
184
|
+
return issue
|
|
185
|
+
|
|
186
|
+
def _get_type_hint_suggestion(self, message: str, error_code: str | None) -> str | None:
|
|
187
|
+
"""根据错误消息提供类型提示建议"""
|
|
188
|
+
if not error_code:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
suggestions = {
|
|
192
|
+
"no-untyped-def": "为函数添加类型注解",
|
|
193
|
+
"no-untyped-call": "被调用的函数缺少类型注解",
|
|
194
|
+
"assignment": "检查赋值的类型是否匹配",
|
|
195
|
+
"return-value": "检查返回值类型是否与声明一致",
|
|
196
|
+
"arg-type": "检查参数类型是否正确",
|
|
197
|
+
"attr-defined": "检查属性是否存在",
|
|
198
|
+
"name-defined": "检查名称是否定义",
|
|
199
|
+
"import": "检查导入是否正确"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for code, suggestion in suggestions.items():
|
|
203
|
+
if code in error_code:
|
|
204
|
+
return suggestion
|
|
205
|
+
|
|
206
|
+
return None
|