mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(runtime-generator-boundary): 修复边界校验回归问题
- 修复 runtime-generator 边界校验对独立与带参数 attribute 的漏报问题,并过滤注释示例误报 - 新增 Python 回归测试覆盖独立、限定名、多 attribute 与文档示例场景 - 更新贡献文档与 ai-plan 记录,移除面向用户文档中的内部治理段落并补充验证结果
This commit is contained in:
parent
7288114e33
commit
d9ceb83c2c
@ -26,15 +26,31 @@ attributes, and leaked NuGet dependencies.
|
||||
`GFramework.Core.SourceGenerators.Abstractions` into its nuspec dependency graph.
|
||||
- Confirmed the two `[GenerateEnumExtensions]` usages inside `GFramework.Game` do not need generated output and can be
|
||||
removed outright.
|
||||
- Verified current PR review findings locally: the validator regex still missed standalone attributes, while the
|
||||
`docs/zh-CN/contributing.md` generator-boundary text should be removed instead of repositioned because it is
|
||||
maintainer-facing governance rather than reader-facing contribution guidance.
|
||||
- Added a Python regression test for standalone, parameterized, fully qualified, and multi-attribute declarations so
|
||||
future validator edits cannot silently reintroduce the false negative.
|
||||
- Added comment-line filtering for the validator after the first regex fix started matching XML documentation examples
|
||||
such as `/// [ContextAware]`, which would otherwise create false CI failures for reader-facing code comments.
|
||||
|
||||
## Validation Target
|
||||
|
||||
- `python3 scripts/validate-runtime-generator-boundaries.py`
|
||||
- `python3 scripts/test_validate_runtime_generator_boundaries.py`
|
||||
- `python3 scripts/license-header.py --check`
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
|
||||
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
|
||||
- `dotnet pack GFramework.sln -c Release -p:PackageVersion=<local>`
|
||||
|
||||
## Latest Validation Result
|
||||
|
||||
- `python3 scripts/test_validate_runtime_generator_boundaries.py` passed on 2026-05-05.
|
||||
- `python3 scripts/validate-runtime-generator-boundaries.py` passed on 2026-05-05.
|
||||
- `python3 scripts/license-header.py --check` passed on 2026-05-05.
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` passed on 2026-05-05 with 0 warnings and 0 errors.
|
||||
|
||||
## Next Recommended Resume Step
|
||||
|
||||
Run the new boundary validator plus minimal Release build and pack validation, then inspect `GeWuYou.GFramework.Game`
|
||||
and transitive runtime packages to confirm no `SourceGenerators` dependency remains.
|
||||
Run the boundary validator, the new Python regression tests, and the minimal Release build/pack validation; then push
|
||||
the follow-up commit so the open PR review threads can be resolved against fresh CI.
|
||||
|
||||
@ -17,5 +17,20 @@
|
||||
- `GFramework.Game` removes the generator abstractions project reference
|
||||
- `GFramework.Game` removes the two unused enum generator attributes
|
||||
- CI and publish workflows run a dedicated boundary validator script
|
||||
- PR review follow-up:
|
||||
- verified CodeRabbit and Greptile findings against local source before acting on them
|
||||
- accepted the validator regex finding because the original pattern missed standalone
|
||||
`[GenerateEnumExtensions]` declarations in runtime code
|
||||
- added comment-line filtering after the first regex repair surfaced false positives from XML documentation examples
|
||||
such as `/// [ContextAware]`
|
||||
- rejected the documentation reposition suggestion as stated and removed the
|
||||
`代码生成器边界` block from `docs/zh-CN/contributing.md` because it documents internal governance rather than
|
||||
reader-facing contributor guidance
|
||||
- added a Python regression test covering standalone, parameterized, fully qualified, and multi-attribute matches
|
||||
- Validation milestone:
|
||||
- `python3 scripts/test_validate_runtime_generator_boundaries.py` passed
|
||||
- `python3 scripts/validate-runtime-generator-boundaries.py` passed
|
||||
- `python3 scripts/license-header.py --check` passed
|
||||
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release` passed with 0 warnings and 0 errors
|
||||
- Immediate next step:
|
||||
- complete implementation and run Release build plus pack verification
|
||||
- push the PR follow-up commit and resolve the remaining review threads
|
||||
|
||||
@ -218,14 +218,6 @@ git push origin your-branch
|
||||
|
||||
### 命名规范
|
||||
|
||||
### 代码生成器边界
|
||||
|
||||
- 框架内部的运行时模块、抽象层模块和顶层元包不得依赖 `*.SourceGenerators*` 项目或包。
|
||||
- `GenerateEnumExtensions`、`ContextAware`、`GetModel`、`GetService` 等代码生成器 attribute 只允许出现在消费端项目、
|
||||
生成器项目本身、专门验证生成器行为的测试项目,或明确用于演示生成器接入的示例中。
|
||||
- 如果某个运行时模块为了编译而需要引入生成器 attribute,说明模块边界已经漂移;应优先移除该 attribute 使用,而不是把
|
||||
generator abstractions 暴露成新的运行时依赖。
|
||||
|
||||
遵循 C# 标准命名约定:
|
||||
|
||||
- **类、接口、方法**:PascalCase
|
||||
|
||||
63
scripts/test_validate_runtime_generator_boundaries.py
Normal file
63
scripts/test_validate_runtime_generator_boundaries.py
Normal file
@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025-2026 GeWuYou
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""Regression tests for runtime/source-generator boundary validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MODULE_PATH = Path(__file__).resolve().parent / "validate-runtime-generator-boundaries.py"
|
||||
MODULE_SPEC = importlib.util.spec_from_file_location("validate_runtime_generator_boundaries", MODULE_PATH)
|
||||
if MODULE_SPEC is None or MODULE_SPEC.loader is None:
|
||||
raise RuntimeError(f"Unable to load module spec from {MODULE_PATH}")
|
||||
|
||||
validate_runtime_generator_boundaries = importlib.util.module_from_spec(MODULE_SPEC)
|
||||
sys.modules[MODULE_SPEC.name] = validate_runtime_generator_boundaries
|
||||
MODULE_SPEC.loader.exec_module(validate_runtime_generator_boundaries)
|
||||
|
||||
|
||||
class ValidateRuntimeGeneratorBoundariesTests(unittest.TestCase):
|
||||
"""Covers attribute matching edge cases that previously caused false negatives."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.patterns = validate_runtime_generator_boundaries.compile_attribute_patterns()
|
||||
|
||||
def test_matches_standalone_attribute(self) -> None:
|
||||
pattern = self.patterns["GenerateEnumExtensions"]
|
||||
|
||||
self.assertIsNotNone(pattern.search("[GenerateEnumExtensions]"))
|
||||
|
||||
def test_matches_parameterized_attribute(self) -> None:
|
||||
pattern = self.patterns["GenerateEnumExtensions"]
|
||||
|
||||
self.assertIsNotNone(pattern.search("[GenerateEnumExtensions(typeof(string))]"))
|
||||
|
||||
def test_matches_non_leading_attribute_in_attribute_list(self) -> None:
|
||||
pattern = self.patterns["GenerateEnumExtensions"]
|
||||
|
||||
self.assertIsNotNone(pattern.search("[Serializable, GenerateEnumExtensions]"))
|
||||
|
||||
def test_matches_fully_qualified_attribute(self) -> None:
|
||||
pattern = self.patterns["Priority"]
|
||||
|
||||
self.assertIsNotNone(
|
||||
pattern.search("[global::GFramework.Core.SourceGenerators.Abstractions.Bases.PriorityAttribute(10)]")
|
||||
)
|
||||
|
||||
def test_ignores_xml_doc_example_attribute(self) -> None:
|
||||
text = "/// [ContextAware]\npublic interface IController;\n"
|
||||
pattern = self.patterns["ContextAware"]
|
||||
match = pattern.search(text)
|
||||
|
||||
self.assertIsNotNone(match)
|
||||
self.assertTrue(validate_runtime_generator_boundaries.is_comment_attribute_match(text, match.start()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -40,8 +40,8 @@ FORBIDDEN_ATTRIBUTE_NAMES = (
|
||||
"GetService",
|
||||
"GetServices",
|
||||
"GetAll",
|
||||
"Log",
|
||||
"Priority",
|
||||
"Log", # GFramework.Core.SourceGenerators.Abstractions.Logging.LogAttribute
|
||||
"Priority", # GFramework.Core.SourceGenerators.Abstractions.Bases.PriorityAttribute
|
||||
)
|
||||
|
||||
FORBIDDEN_PROJECT_REFERENCE_PREFIX = "GFramework."
|
||||
@ -145,8 +145,9 @@ def validate_project_references() -> list[Violation]:
|
||||
def compile_attribute_patterns() -> dict[str, re.Pattern[str]]:
|
||||
patterns: dict[str, re.Pattern[str]] = {}
|
||||
for attribute_name in FORBIDDEN_ATTRIBUTE_NAMES:
|
||||
escaped_attribute_name = re.escape(attribute_name)
|
||||
patterns[attribute_name] = re.compile(
|
||||
rf"\[(?P<body>[^\]]*?(?:^|[\s,(])(?:global::)?(?:[A-Za-z_][A-Za-z0-9_]*\.)*{attribute_name}(?:Attribute)?(?=[\s,)\]])[^\]]*)\]",
|
||||
rf"\[[^\]]*(?:(?<=\[)|(?<=[\s,(]))(?:global::)?(?:[A-Za-z_][A-Za-z0-9_]*\.)*{escaped_attribute_name}(?:Attribute)?(?=\s*(?:\(|,|\]))[^\]]*\]",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
@ -157,6 +158,12 @@ def line_number_for_offset(text: str, offset: int) -> int:
|
||||
return text.count("\n", 0, offset) + 1
|
||||
|
||||
|
||||
def is_comment_attribute_match(text: str, offset: int) -> bool:
|
||||
line_start = text.rfind("\n", 0, offset) + 1
|
||||
line_prefix = text[line_start:offset].lstrip()
|
||||
return line_prefix.startswith("///") or line_prefix.startswith("//") or line_prefix.startswith("/*") or line_prefix.startswith("*")
|
||||
|
||||
|
||||
def validate_source_attributes() -> list[Violation]:
|
||||
violations: list[Violation] = []
|
||||
patterns = compile_attribute_patterns()
|
||||
@ -173,6 +180,9 @@ def validate_source_attributes() -> list[Violation]:
|
||||
text = file_path.read_text(encoding="utf-8-sig")
|
||||
for attribute_name, pattern in patterns.items():
|
||||
for match in pattern.finditer(text):
|
||||
if is_comment_attribute_match(text, match.start()):
|
||||
continue
|
||||
|
||||
line_number = line_number_for_offset(text, match.start())
|
||||
relative_path = file_path.relative_to(REPO_ROOT)
|
||||
violations.append(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user