diff --git a/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md b/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md index cf81840e..a0f8fb72 100644 --- a/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md +++ b/ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md @@ -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=` +## 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. diff --git a/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md b/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md index 60ceb84c..ef1fa31f 100644 --- a/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md +++ b/ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md @@ -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 diff --git a/docs/zh-CN/contributing.md b/docs/zh-CN/contributing.md index 1628bed1..ee760bcc 100644 --- a/docs/zh-CN/contributing.md +++ b/docs/zh-CN/contributing.md @@ -218,14 +218,6 @@ git push origin your-branch ### 命名规范 -### 代码生成器边界 - -- 框架内部的运行时模块、抽象层模块和顶层元包不得依赖 `*.SourceGenerators*` 项目或包。 -- `GenerateEnumExtensions`、`ContextAware`、`GetModel`、`GetService` 等代码生成器 attribute 只允许出现在消费端项目、 - 生成器项目本身、专门验证生成器行为的测试项目,或明确用于演示生成器接入的示例中。 -- 如果某个运行时模块为了编译而需要引入生成器 attribute,说明模块边界已经漂移;应优先移除该 attribute 使用,而不是把 - generator abstractions 暴露成新的运行时依赖。 - 遵循 C# 标准命名约定: - **类、接口、方法**:PascalCase diff --git a/scripts/test_validate_runtime_generator_boundaries.py b/scripts/test_validate_runtime_generator_boundaries.py new file mode 100644 index 00000000..41899213 --- /dev/null +++ b/scripts/test_validate_runtime_generator_boundaries.py @@ -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() diff --git a/scripts/validate-runtime-generator-boundaries.py b/scripts/validate-runtime-generator-boundaries.py index 75924d66..f27e237e 100644 --- a/scripts/validate-runtime-generator-boundaries.py +++ b/scripts/validate-runtime-generator-boundaries.py @@ -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[^\]]*?(?:^|[\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(