Repository: nonebot/nonebot2

Issue:
Bug: 插件在运行期间创建的 Matcher 没有 plugin_name 属性
**描述问题：**

插件若在 nonebot 启动后再动态创建 Matcher，这些 Matcher 的 plugin_name 属性为 None，而很多管理类插件需要用到 matcher.plugin_name

**如何复现？**

在 nonebot 启动后创建 Matcher，如在 on_startup 中创建 Matcher

示例：[nonebot-plugin-memes-api](https://github.com/noneplugin/nonebot-plugin-memes-api) 插件在 on_startup 中创建 Matcher： [code](https://github.com/noneplugin/nonebot-plugin-memes-api/blob/af07cda1f76691e6e41849c3aconebot-plugin-memes-api/blob/af07cda1f76691e6e41849c3ac93683fcbd9db23/nonebot_plugin_memes_api/__init__.py#L41293683fcbd9db23/nonebot_plugin_memes_api/__init__.py#L412)

有些 bot 直接使用 matcher.plugin_name，导致如下的报错：
![-797b4f1fd0a37362](https://user-images.githubusercontent.com/33149974/227731930-9acaf8d9-9b9e-40f8-9cf7-08fcf001af9b.jpg)


**期望的结果**

由于 nonebot 中 Matcher 的 plugin_name 属性为 Optional，不确定这一结果是否是预期行为？

如果是，有没有好的办法在动态创建 Matcher 时定义 plugin_name 呢？



Discussion:
plugin相关属性目前只有在插件导入时可以进行跟踪，但是module属性应该是有的，可以考虑通过module寻找plugin
这边遇到了一个小问题，运行时插件新建的matcher是否应该添加到`plugin.matcher`存储里

Resolving patch:
diff --git a/nonebot/plugin/on.py b/nonebot/plugin/on.py
--- a/nonebot/plugin/on.py
+++ b/nonebot/plugin/on.py
@@ -28,13 +28,26 @@
     shell_command,
 )
 
+from .plugin import Plugin
+from . import get_plugin_by_module_name
 from .manager import _current_plugin_chain
 
 
 def _store_matcher(matcher: Type[Matcher]) -> None:
-    # only store the matcher defined in the plugin
-    if plugins := _current_plugin_chain.get():
-        plugins[-1].matcher.add(matcher)
+    # only store the matcher defined when plugin loading
+    if plugin_chain := _current_plugin_chain.get():
+        plugin_chain[-1].matcher.add(matcher)
+
+
+def _get_matcher_plugin(depth: int = 1) -> Optional[Plugin]:
+    # matcher defined when plugin loading
+    if plugin_chain := _current_plugin_chain.get():
+        return plugin_chain[-1]
+
+    # matcher defined when plugin running
+    if module := _get_matcher_module(depth + 1):
+        if plugin := get_plugin_by_module_name(module.__name__):
+            return plugin
 
 
 def _get_matcher_module(depth: int = 1) -> Optional[ModuleType]:
@@ -71,7 +84,6 @@ def on(
         block: 是否阻止事件向更低优先级传递
         state: 默认 state
     """
-    plugin_chain = _current_plugin_chain.get()
     matcher = Matcher.new(
         type,
         Rule() & rule,
@@ -81,7 +93,7 @@ def on(
         priority=priority,
         block=block,
         handlers=handlers,
-        plugin=plugin_chain[-1] if plugin_chain else None,
+        plugin=_get_matcher_plugin(_depth + 1),
         module=_get_matcher_module(_depth + 1),
         default_state=state,
     )
diff --git a/nonebot/plugin/plugin.py b/nonebot/plugin/plugin.py
--- a/nonebot/plugin/plugin.py
+++ b/nonebot/plugin/plugin.py
@@ -47,7 +47,7 @@ class Plugin:
     manager: "PluginManager"
     """导入该插件的插件管理器"""
     matcher: Set[Type[Matcher]] = field(default_factory=set)
-    """插件内定义的 `Matcher`"""
+    """插件加载时定义的 `Matcher`"""
     parent_plugin: Optional["Plugin"] = None
     """父插件"""
     sub_plugins: Set["Plugin"] = field(default_factory=set)


Test patch:
diff --git a/tests/plugins/matcher/matcher_process.py b/tests/plugins/matcher/matcher_process.py
--- a/tests/plugins/matcher/matcher_process.py
+++ b/tests/plugins/matcher/matcher_process.py
@@ -90,3 +90,6 @@ async def overload(event: FakeEvent):
 @test_overload.handle()
 async def finish():
     await test_overload.finish()
+
+
+test_destroy = on_message()
diff --git a/tests/plugins/plugin/matchers.py b/tests/plugins/plugin/matchers.py
--- a/tests/plugins/plugin/matchers.py
+++ b/tests/plugins/plugin/matchers.py
@@ -1,6 +1,8 @@
+from typing import Type
 from datetime import datetime, timezone
 
 from nonebot.adapters import Event
+from nonebot.matcher import Matcher
 from nonebot import (
     CommandGroup,
     MatcherGroup,
@@ -50,6 +52,20 @@ async def handler():
 )
 
 
+def matcher_on_factory() -> Type[Matcher]:
+    return on(
+        "test",
+        rule=rule,
+        permission=permission,
+        handlers=[handler],
+        temp=True,
+        expire_time=expire_time,
+        priority=priority,
+        block=True,
+        state=state,
+    )
+
+
 matcher_on_metaevent = on_metaevent(
     rule=rule,
     handlers=[handler],
diff --git a/tests/test_matcher/test_matcher.py b/tests/test_matcher/test_matcher.py
--- a/tests/test_matcher/test_matcher.py
+++ b/tests/test_matcher/test_matcher.py
@@ -79,6 +79,20 @@ async def test_matcher(app: App):
         ctx.should_finished()
 
 
+@pytest.mark.asyncio
+async def test_matcher_destroy(app: App):
+    from plugins.matcher.matcher_process import test_destroy
+
+    async with app.test_matcher(test_destroy) as ctx:
+        assert len(matchers) == 1
+        assert len(matchers[test_destroy.priority]) == 1
+        assert matchers[test_destroy.priority][0] is test_destroy
+
+        test_destroy.destroy()
+
+        assert len(matchers[test_destroy.priority]) == 0
+
+
 @pytest.mark.asyncio
 async def test_type_updater(app: App):
     from plugins.matcher.matcher_type import test_type_updater, test_custom_updater
diff --git a/tests/test_plugin/test_on.py b/tests/test_plugin/test_on.py
--- a/tests/test_plugin/test_on.py
+++ b/tests/test_plugin/test_on.py
@@ -1,8 +1,9 @@
-from typing import Type, Optional
+from typing import Type, Callable, Optional
 
 import pytest
 
 import nonebot
+from nonebot.adapters import Event
 from nonebot.typing import T_RuleChecker
 from nonebot.matcher import Matcher, matchers
 from nonebot.rule import (
@@ -18,7 +19,74 @@
 
 
 @pytest.mark.asyncio
-async def test_on():
+@pytest.mark.parametrize(
+    "matcher_name, pre_rule_factory, has_permission",
+    [
+        pytest.param("matcher_on", None, True),
+        pytest.param("matcher_on_metaevent", None, False),
+        pytest.param("matcher_on_message", None, True),
+        pytest.param("matcher_on_notice", None, False),
+        pytest.param("matcher_on_request", None, False),
+        pytest.param(
+            "matcher_on_startswith", lambda e: StartswithRule(("test",)), True
+        ),
+        pytest.param("matcher_on_endswith", lambda e: EndswithRule(("test",)), True),
+        pytest.param("matcher_on_fullmatch", lambda e: FullmatchRule(("test",)), True),
+        pytest.param("matcher_on_keyword", lambda e: KeywordsRule("test"), True),
+        pytest.param("matcher_on_command", lambda e: CommandRule([("test",)]), True),
+        pytest.param(
+            "matcher_on_shell_command",
+            lambda e: ShellCommandRule([("test",)], None),
+            True,
+        ),
+        pytest.param("matcher_on_regex", lambda e: RegexRule("test"), True),
+        pytest.param("matcher_on_type", lambda e: IsTypeRule(e), True),
+        pytest.param("matcher_sub_cmd", lambda e: CommandRule([("test", "sub")]), True),
+        pytest.param(
+            "matcher_sub_shell_cmd",
+            lambda e: ShellCommandRule([("test", "sub")], None),
+            True,
+        ),
+        pytest.param("matcher_group_on", None, True),
+        pytest.param("matcher_group_on_metaevent", None, False),
+        pytest.param("matcher_group_on_message", None, True),
+        pytest.param("matcher_group_on_notice", None, False),
+        pytest.param("matcher_group_on_request", None, False),
+        pytest.param(
+            "matcher_group_on_startswith",
+            lambda e: StartswithRule(("test",)),
+            True,
+        ),
+        pytest.param(
+            "matcher_group_on_endswith",
+            lambda e: EndswithRule(("test",)),
+            True,
+        ),
+        pytest.param(
+            "matcher_group_on_fullmatch",
+            lambda e: FullmatchRule(("test",)),
+            True,
+        ),
+        pytest.param("matcher_group_on_keyword", lambda e: KeywordsRule("test"), True),
+        pytest.param(
+            "matcher_group_on_command",
+            lambda e: CommandRule([("test",)]),
+            True,
+        ),
+        pytest.param(
+            "matcher_group_on_shell_command",
+            lambda e: ShellCommandRule([("test",)], None),
+            True,
+        ),
+        pytest.param("matcher_group_on_regex", lambda e: RegexRule("test"), True),
+        pytest.param("matcher_group_on_type", lambda e: IsTypeRule(e), True),
+    ],
+)
+async def test_on(
+    matcher_name: str,
+    pre_rule_factory: Optional[Callable[[Type[Event]], T_RuleChecker]],
+    has_permission: bool,
+):
     import plugins.plugin.matchers as module
     from plugins.plugin.matchers import (
         TestEvent,
@@ -26,91 +94,56 @@ async def test_on():
         state,
         handler,
         priority,
-        matcher_on,
         permission,
         expire_time,
-        matcher_on_type,
-        matcher_sub_cmd,
-        matcher_group_on,
-        matcher_on_regex,
-        matcher_on_notice,
-        matcher_on_command,
-        matcher_on_keyword,
-        matcher_on_message,
-        matcher_on_request,
-        matcher_on_endswith,
-        matcher_on_fullmatch,
-        matcher_on_metaevent,
-        matcher_group_on_type,
-        matcher_on_startswith,
-        matcher_sub_shell_cmd,
-        matcher_group_on_regex,
-        matcher_group_on_notice,
-        matcher_group_on_command,
-        matcher_group_on_keyword,
-        matcher_group_on_message,
-        matcher_group_on_request,
-        matcher_on_shell_command,
-        matcher_group_on_endswith,
-        matcher_group_on_fullmatch,
-        matcher_group_on_metaevent,
-        matcher_group_on_startswith,
-        matcher_group_on_shell_command,
     )
 
+    matcher = getattr(module, matcher_name)
+    assert issubclass(matcher, Matcher), f"{matcher_name} should be a Matcher"
+
+    pre_rule = pre_rule_factory(TestEvent) if pre_rule_factory else None
+
     plugin = nonebot.get_plugin("plugin")
+    assert plugin, "plugin should be loaded"
 
-    def _check(
-        matcher: Type[Matcher],
-        pre_rule: Optional[T_RuleChecker],
-        has_permission: bool,
-    ):
-        assert {dependent.call for dependent in matcher.rule.checkers} == (
-            {pre_rule, rule} if pre_rule else {rule}
-        )
-        if has_permission:
-            assert {dependent.call for dependent in matcher.permission.checkers} == {
-                permission
-            }
-        else:
-            assert not matcher.permission.checkers
-        assert [dependent.call for dependent in matcher.handlers] == [handler]
-        assert matcher.temp is True
-        assert matcher.expire_time == expire_time
-        assert matcher in matchers[priority]
-        assert matcher.block is True
-        assert matcher._default_state == state
+    assert {dependent.call for dependent in matcher.rule.checkers} == (
+        {pre_rule, rule} if pre_rule else {rule}
+    )
+    if has_permission:
+        assert {dependent.call for dependent in matcher.permission.checkers} == {
+            permission
+        }
+    else:
+        assert not matcher.permission.checkers
+    assert [dependent.call for dependent in matcher.handlers] == [handler]
+    assert matcher.temp is True
+    assert matcher.expire_time == expire_time
+    assert matcher in matchers[priority]
+    assert matcher.block is True
+    assert matcher._default_state == state
 
+    assert matcher.plugin is plugin
+    assert matcher in plugin.matcher
+    assert matcher.module is module
+    assert matcher.plugin_name == "plugin"
+    assert matcher.module_name == "plugins.plugin.matchers"
+
+
+@pytest.mark.asyncio
+async def test_runtime_on():
+    import plugins.plugin.matchers as module
+    from plugins.plugin.matchers import matcher_on_factory
+
+    matcher = matcher_on_factory()
+
+    plugin = nonebot.get_plugin("plugin")
+    assert plugin, "plugin should be loaded"
+
+    try:
         assert matcher.plugin is plugin
+        assert matcher not in plugin.matcher
         assert matcher.module is module
         assert matcher.plugin_name == "plugin"
         assert matcher.module_name == "plugins.plugin.matchers"
-
-    _check(matcher_on, None, True)
-    _check(matcher_on_metaevent, None, False)
-    _check(matcher_on_message, None, True)
-    _check(matcher_on_notice, None, False)
-    _check(matcher_on_request, None, False)
-    _check(matcher_on_startswith, StartswithRule(("test",)), True)
-    _check(matcher_on_endswith, EndswithRule(("test",)), True)
-    _check(matcher_on_fullmatch, FullmatchRule(("test",)), True)
-    _check(matcher_on_keyword, KeywordsRule("test"), True)
-    _check(matcher_on_command, CommandRule([("test",)]), True)
-    _check(matcher_on_shell_command, ShellCommandRule([("test",)], None), True)
-    _check(matcher_on_regex, RegexRule("test"), True)
-    _check(matcher_on_type, IsTypeRule(TestEvent), True)
-    _check(matcher_sub_cmd, CommandRule([("test", "sub")]), True)
-    _check(matcher_sub_shell_cmd, ShellCommandRule([("test", "sub")], None), True)
-    _check(matcher_group_on, None, True)
-    _check(matcher_group_on_metaevent, None, False)
-    _check(matcher_group_on_message, None, True)
-    _check(matcher_group_on_notice, None, False)
-    _check(matcher_group_on_request, None, False)
-    _check(matcher_group_on_startswith, StartswithRule(("test",)), True)
-    _check(matcher_group_on_endswith, EndswithRule(("test",)), True)
-    _check(matcher_group_on_fullmatch, FullmatchRule(("test",)), True)
-    _check(matcher_group_on_keyword, KeywordsRule("test"), True)
-    _check(matcher_group_on_command, CommandRule([("test",)]), True)
-    _check(matcher_group_on_shell_command, ShellCommandRule([("test",)], None), True)
-    _check(matcher_group_on_regex, RegexRule("test"), True)
-    _check(matcher_group_on_type, IsTypeRule(TestEvent), True)
+    finally:
+        matcher.destroy()