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