fix(runtime-generator-boundary): 修复边界校验回归问题

- 修复 runtime-generator 边界校验对独立与带参数 attribute 的漏报问题,并过滤注释示例误报

- 新增 Python 回归测试覆盖独立、限定名、多 attribute 与文档示例场景

- 更新贡献文档与 ai-plan 记录,移除面向用户文档中的内部治理段落并补充验证结果
This commit is contained in:
gewuyou 2026-05-05 13:06:18 +08:00
parent 7288114e33
commit d9ceb83c2c
5 changed files with 110 additions and 14 deletions

View File

@ -26,15 +26,31 @@ attributes, and leaked NuGet dependencies.
`GFramework.Core.SourceGenerators.Abstractions` into its nuspec dependency graph. `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 - Confirmed the two `[GenerateEnumExtensions]` usages inside `GFramework.Game` do not need generated output and can be
removed outright. 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 ## Validation Target
- `python3 scripts/validate-runtime-generator-boundaries.py` - `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.Game/GFramework.Game.csproj -c Release`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release` - `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- `dotnet pack GFramework.sln -c Release -p:PackageVersion=<local>` - `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 ## Next Recommended Resume Step
Run the new boundary validator plus minimal Release build and pack validation, then inspect `GeWuYou.GFramework.Game` Run the boundary validator, the new Python regression tests, and the minimal Release build/pack validation; then push
and transitive runtime packages to confirm no `SourceGenerators` dependency remains. the follow-up commit so the open PR review threads can be resolved against fresh CI.

View File

@ -17,5 +17,20 @@
- `GFramework.Game` removes the generator abstractions project reference - `GFramework.Game` removes the generator abstractions project reference
- `GFramework.Game` removes the two unused enum generator attributes - `GFramework.Game` removes the two unused enum generator attributes
- CI and publish workflows run a dedicated boundary validator script - 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: - Immediate next step:
- complete implementation and run Release build plus pack verification - push the PR follow-up commit and resolve the remaining review threads

View File

@ -218,14 +218,6 @@ git push origin your-branch
### 命名规范 ### 命名规范
### 代码生成器边界
- 框架内部的运行时模块、抽象层模块和顶层元包不得依赖 `*.SourceGenerators*` 项目或包。
- `GenerateEnumExtensions``ContextAware``GetModel``GetService` 等代码生成器 attribute 只允许出现在消费端项目、
生成器项目本身、专门验证生成器行为的测试项目,或明确用于演示生成器接入的示例中。
- 如果某个运行时模块为了编译而需要引入生成器 attribute说明模块边界已经漂移应优先移除该 attribute 使用,而不是把
generator abstractions 暴露成新的运行时依赖。
遵循 C# 标准命名约定: 遵循 C# 标准命名约定:
- **类、接口、方法**PascalCase - **类、接口、方法**PascalCase

View 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()

View File

@ -40,8 +40,8 @@ FORBIDDEN_ATTRIBUTE_NAMES = (
"GetService", "GetService",
"GetServices", "GetServices",
"GetAll", "GetAll",
"Log", "Log", # GFramework.Core.SourceGenerators.Abstractions.Logging.LogAttribute
"Priority", "Priority", # GFramework.Core.SourceGenerators.Abstractions.Bases.PriorityAttribute
) )
FORBIDDEN_PROJECT_REFERENCE_PREFIX = "GFramework." FORBIDDEN_PROJECT_REFERENCE_PREFIX = "GFramework."
@ -145,8 +145,9 @@ def validate_project_references() -> list[Violation]:
def compile_attribute_patterns() -> dict[str, re.Pattern[str]]: def compile_attribute_patterns() -> dict[str, re.Pattern[str]]:
patterns: dict[str, re.Pattern[str]] = {} patterns: dict[str, re.Pattern[str]] = {}
for attribute_name in FORBIDDEN_ATTRIBUTE_NAMES: for attribute_name in FORBIDDEN_ATTRIBUTE_NAMES:
escaped_attribute_name = re.escape(attribute_name)
patterns[attribute_name] = re.compile( 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, re.MULTILINE,
) )
@ -157,6 +158,12 @@ def line_number_for_offset(text: str, offset: int) -> int:
return text.count("\n", 0, offset) + 1 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]: def validate_source_attributes() -> list[Violation]:
violations: list[Violation] = [] violations: list[Violation] = []
patterns = compile_attribute_patterns() patterns = compile_attribute_patterns()
@ -173,6 +180,9 @@ def validate_source_attributes() -> list[Violation]:
text = file_path.read_text(encoding="utf-8-sig") text = file_path.read_text(encoding="utf-8-sig")
for attribute_name, pattern in patterns.items(): for attribute_name, pattern in patterns.items():
for match in pattern.finditer(text): for match in pattern.finditer(text):
if is_comment_attribute_match(text, match.start()):
continue
line_number = line_number_for_offset(text, match.start()) line_number = line_number_for_offset(text, match.start())
relative_path = file_path.relative_to(REPO_ROOT) relative_path = file_path.relative_to(REPO_ROOT)
violations.append( violations.append(