mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-14 06:34:30 +08:00
Compare commits
No commits in common. "a8c6c11e9ef02235f6081bdfa48659a00747e74b" and "c69942d66e7fa597ef6af7dd1aace6515aa56dbf" have entirely different histories.
a8c6c11e9e
...
c69942d66e
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -35,9 +35,6 @@ jobs:
|
|||||||
- name: Validate license headers
|
- name: Validate license headers
|
||||||
run: python3 scripts/license-header.py --check
|
run: python3 scripts/license-header.py --check
|
||||||
|
|
||||||
- name: Validate runtime-generator boundaries
|
|
||||||
run: python3 scripts/validate-runtime-generator-boundaries.py
|
|
||||||
|
|
||||||
# 缓存MegaLinter
|
# 缓存MegaLinter
|
||||||
- name: Cache MegaLinter
|
- name: Cache MegaLinter
|
||||||
uses: actions/cache@v5
|
uses: actions/cache@v5
|
||||||
|
|||||||
3
.github/workflows/publish.yml
vendored
3
.github/workflows/publish.yml
vendored
@ -118,9 +118,6 @@ jobs:
|
|||||||
|
|
||||||
diff -u expected-packages.txt actual-packages.txt
|
diff -u expected-packages.txt actual-packages.txt
|
||||||
|
|
||||||
- name: Validate runtime-generator package boundaries
|
|
||||||
run: python3 scripts/validate-runtime-generator-boundaries.py --package-dir ./packages
|
|
||||||
|
|
||||||
- name: Show packages
|
- name: Show packages
|
||||||
run: ls -la ./packages || true
|
run: ls -la ./packages || true
|
||||||
|
|
||||||
|
|||||||
@ -212,9 +212,6 @@ All generated or modified code MUST include clear and meaningful comments where
|
|||||||
- Private fields: `_camelCase`
|
- Private fields: `_camelCase`
|
||||||
- Keep abstractions projects free of implementation details and engine-specific dependencies.
|
- Keep abstractions projects free of implementation details and engine-specific dependencies.
|
||||||
- Preserve existing module boundaries. Do not introduce new cross-module dependencies without clear architectural need.
|
- Preserve existing module boundaries. Do not introduce new cross-module dependencies without clear architectural need.
|
||||||
- Framework runtime, abstractions, and meta-package projects MUST NOT reference `*.SourceGenerators*` projects or packages,
|
|
||||||
and MUST NOT use source-generator attributes such as `GenerateEnumExtensions` or `ContextAware`. Those capabilities are
|
|
||||||
reserved for consumer projects, generator projects, examples explicitly meant to demonstrate generator usage, and related tests.
|
|
||||||
|
|
||||||
### Formatting
|
### Formatting
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) 2025-2026 GeWuYou
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using GFramework.Core.SourceGenerators.Abstractions.Enums;
|
||||||
|
|
||||||
namespace GFramework.Game.Config;
|
namespace GFramework.Game.Config;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示当前运行时 schema 校验器支持的属性类型。
|
/// 表示当前运行时 schema 校验器支持的属性类型。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[GenerateEnumExtensions]
|
||||||
internal enum YamlConfigSchemaPropertyType
|
internal enum YamlConfigSchemaPropertyType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
// Copyright (c) 2025-2026 GeWuYou
|
// Copyright (c) 2025-2026 GeWuYou
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
using GFramework.Core.SourceGenerators.Abstractions.Enums;
|
||||||
|
|
||||||
namespace GFramework.Game.Config;
|
namespace GFramework.Game.Config;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。
|
/// 表示当前 Runtime / Generator / Tooling 共享支持的字符串 format 子集。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[GenerateEnumExtensions]
|
||||||
internal enum YamlConfigStringFormatKind
|
internal enum YamlConfigStringFormatKind
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
|
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GFramework.Core.SourceGenerators.Abstractions\GFramework.Core.SourceGenerators.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||||
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
|
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -42,10 +42,6 @@ help the current worktree land on the right recovery documents without scanning
|
|||||||
- Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow.
|
- Purpose: migrate release version calculation from fixed patch bumps to semantic-release while keeping the existing tag-driven NuGet publish flow.
|
||||||
- Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md`
|
- Tracking: `ai-plan/public/semantic-release-versioning/todos/semantic-release-versioning-tracking.md`
|
||||||
- Trace: `ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md`
|
- Trace: `ai-plan/public/semantic-release-versioning/traces/semantic-release-versioning-trace.md`
|
||||||
- `runtime-generator-boundary`
|
|
||||||
- Purpose: keep runtime and abstractions packages isolated from source-generator dependencies, packaging leaks, and attribute usage.
|
|
||||||
- Tracking: `ai-plan/public/runtime-generator-boundary/todos/runtime-generator-boundary-tracking.md`
|
|
||||||
- Trace: `ai-plan/public/runtime-generator-boundary/traces/runtime-generator-boundary-trace.md`
|
|
||||||
|
|
||||||
## Worktree To Active Topic Map
|
## Worktree To Active Topic Map
|
||||||
|
|
||||||
@ -72,9 +68,6 @@ help the current worktree land on the right recovery documents without scanning
|
|||||||
- Branch: `fix/release-notes-pr-links`
|
- Branch: `fix/release-notes-pr-links`
|
||||||
- Worktree hint: `GFramework`
|
- Worktree hint: `GFramework`
|
||||||
- Priority 1: `semantic-release-versioning`
|
- Priority 1: `semantic-release-versioning`
|
||||||
- Branch: `fix/runtime-generator-boundary`
|
|
||||||
- Worktree hint: `GFramework`
|
|
||||||
- Priority 1: `runtime-generator-boundary`
|
|
||||||
- Branch: `docs/sdk-update-documentation`
|
- Branch: `docs/sdk-update-documentation`
|
||||||
- Worktree hint: `GFramework-update-documentation`
|
- Worktree hint: `GFramework-update-documentation`
|
||||||
- Priority 1: `documentation-full-coverage-governance`
|
- Priority 1: `documentation-full-coverage-governance`
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
# Runtime / Generator Boundary Tracking
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Keep runtime, abstractions, and meta-package modules free from source-generator project references, source-generator
|
|
||||||
attributes, and leaked NuGet dependencies.
|
|
||||||
|
|
||||||
## Current Recovery Point
|
|
||||||
|
|
||||||
- Recovery point: RGB-RP-001
|
|
||||||
- Phase: remove `GFramework.Game` generator coupling and add repository guardrails
|
|
||||||
- Focus:
|
|
||||||
- delete `GFramework.Game`'s dependency on `GFramework.Core.SourceGenerators.Abstractions`
|
|
||||||
- remove unused `[GenerateEnumExtensions]` usage from `GFramework.Game`
|
|
||||||
- add static and packed-package validation so runtime packages cannot regress
|
|
||||||
|
|
||||||
## Active Risks
|
|
||||||
|
|
||||||
- A runtime package can still compile locally if it references a non-packable generator helper project, so regressions are
|
|
||||||
easy to miss without an explicit guard.
|
|
||||||
- A leaked package dependency may only surface when a consumer restores from NuGet, not during normal repository builds.
|
|
||||||
|
|
||||||
## Completed In This Stage
|
|
||||||
|
|
||||||
- Confirmed `GFramework.Game` was the direct runtime offender and `GeWuYou.GFramework.Game` leaked
|
|
||||||
`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 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.
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
# Runtime / Generator Boundary Trace
|
|
||||||
|
|
||||||
## 2026-05-05
|
|
||||||
|
|
||||||
### RGB-RP-001 Runtime package boundary repair
|
|
||||||
|
|
||||||
- Trigger:
|
|
||||||
- external consumers restoring `GeWuYou.GFramework.Game` failed because NuGet looked for
|
|
||||||
`GFramework.Core.SourceGenerators.Abstractions`
|
|
||||||
- repository inspection showed `GFramework.Game` had a direct project reference to a non-packable generator
|
|
||||||
abstractions project and used `[GenerateEnumExtensions]`
|
|
||||||
- Decisions:
|
|
||||||
- treat the issue as a runtime/generator boundary violation, not as a missing publish target
|
|
||||||
- remove the runtime-side attribute usage instead of turning generator abstractions into public runtime packages
|
|
||||||
- add repository guardrails at both source-validation time and packed-package validation time
|
|
||||||
- Expected implementation:
|
|
||||||
- `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:
|
|
||||||
- push the PR follow-up commit and resolve the remaining review threads
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@ -1,296 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# Copyright (c) 2025-2026 GeWuYou
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import zipfile
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
RUNTIME_PROJECTS = (
|
|
||||||
"GFramework",
|
|
||||||
"GFramework.Core",
|
|
||||||
"GFramework.Core.Abstractions",
|
|
||||||
"GFramework.Cqrs",
|
|
||||||
"GFramework.Cqrs.Abstractions",
|
|
||||||
"GFramework.Game",
|
|
||||||
"GFramework.Game.Abstractions",
|
|
||||||
"GFramework.Godot",
|
|
||||||
"GFramework.Ecs.Arch",
|
|
||||||
"GFramework.Ecs.Arch.Abstractions",
|
|
||||||
)
|
|
||||||
|
|
||||||
FORBIDDEN_ATTRIBUTE_NAMES = (
|
|
||||||
"GenerateEnumExtensions",
|
|
||||||
"ContextAware",
|
|
||||||
"GetModel",
|
|
||||||
"GetModels",
|
|
||||||
"GetSystem",
|
|
||||||
"GetSystems",
|
|
||||||
"GetUtility",
|
|
||||||
"GetUtilities",
|
|
||||||
"GetService",
|
|
||||||
"GetServices",
|
|
||||||
"GetAll",
|
|
||||||
"Log", # GFramework.Core.SourceGenerators.Abstractions.Logging.LogAttribute
|
|
||||||
"Priority", # GFramework.Core.SourceGenerators.Abstractions.Bases.PriorityAttribute
|
|
||||||
)
|
|
||||||
|
|
||||||
FORBIDDEN_PROJECT_REFERENCE_PREFIX = "GFramework."
|
|
||||||
FORBIDDEN_PACKAGE_REFERENCE_PREFIX = "GeWuYou.GFramework."
|
|
||||||
PACKAGE_NAMESPACE = "http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"
|
|
||||||
PACKAGE_NAMESPACE_2012 = "http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Violation:
|
|
||||||
location: str
|
|
||||||
message: str
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Validate that runtime and abstractions modules do not depend on source-generator packages or attributes."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--package-dir",
|
|
||||||
type=Path,
|
|
||||||
help="Optional package directory. When supplied, validate packed runtime nuspec dependencies as well.",
|
|
||||||
)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def get_project_file(project_name: str) -> Path:
|
|
||||||
if project_name == "GFramework":
|
|
||||||
return REPO_ROOT / "GFramework.csproj"
|
|
||||||
|
|
||||||
return REPO_ROOT / project_name / f"{project_name}.csproj"
|
|
||||||
|
|
||||||
|
|
||||||
def get_source_root(project_name: str) -> Path | None:
|
|
||||||
if project_name == "GFramework":
|
|
||||||
return None
|
|
||||||
|
|
||||||
return REPO_ROOT / project_name
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_name(tag: str) -> str:
|
|
||||||
return tag.split("}", 1)[-1]
|
|
||||||
|
|
||||||
|
|
||||||
def get_xml_text(element: ET.Element) -> str:
|
|
||||||
return (element.text or "").strip()
|
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_package_ids() -> set[str]:
|
|
||||||
package_ids: set[str] = set()
|
|
||||||
for project_name in RUNTIME_PROJECTS:
|
|
||||||
project_file = get_project_file(project_name)
|
|
||||||
tree = ET.parse(project_file)
|
|
||||||
root = tree.getroot()
|
|
||||||
package_id = None
|
|
||||||
assembly_name = None
|
|
||||||
|
|
||||||
for element in root.iter():
|
|
||||||
local_name = get_local_name(element.tag)
|
|
||||||
if local_name == "PackageId" and not package_id:
|
|
||||||
package_id = get_xml_text(element)
|
|
||||||
elif local_name == "AssemblyName" and not assembly_name:
|
|
||||||
assembly_name = get_xml_text(element)
|
|
||||||
|
|
||||||
resolved_package_id = package_id or f"GeWuYou.{assembly_name or project_name}"
|
|
||||||
package_ids.add(resolved_package_id)
|
|
||||||
|
|
||||||
return package_ids
|
|
||||||
|
|
||||||
|
|
||||||
def validate_project_references() -> list[Violation]:
|
|
||||||
violations: list[Violation] = []
|
|
||||||
|
|
||||||
for project_name in RUNTIME_PROJECTS:
|
|
||||||
project_file = get_project_file(project_name)
|
|
||||||
tree = ET.parse(project_file)
|
|
||||||
|
|
||||||
for element in tree.getroot().iter():
|
|
||||||
local_name = get_local_name(element.tag)
|
|
||||||
if local_name not in {"ProjectReference", "PackageReference"}:
|
|
||||||
continue
|
|
||||||
|
|
||||||
include = element.attrib.get("Include", "").strip()
|
|
||||||
if local_name == "ProjectReference":
|
|
||||||
if FORBIDDEN_PROJECT_REFERENCE_PREFIX not in include or "SourceGenerators" not in include:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if not include.startswith(FORBIDDEN_PACKAGE_REFERENCE_PREFIX) or "SourceGenerators" not in include:
|
|
||||||
continue
|
|
||||||
|
|
||||||
violations.append(
|
|
||||||
Violation(
|
|
||||||
location=str(project_file.relative_to(REPO_ROOT)),
|
|
||||||
message=f"forbidden {local_name} -> {include}",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return violations
|
|
||||||
|
|
||||||
|
|
||||||
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"\[[^\]]*(?:(?<=\[)|(?<=[\s,(]))(?:global::)?(?:[A-Za-z_][A-Za-z0-9_]*\.)*{escaped_attribute_name}(?:Attribute)?(?=\s*(?:\(|,|\]))[^\]]*\]",
|
|
||||||
re.MULTILINE,
|
|
||||||
)
|
|
||||||
|
|
||||||
return patterns
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
for project_name in RUNTIME_PROJECTS:
|
|
||||||
source_root = get_source_root(project_name)
|
|
||||||
if source_root is None or not source_root.is_dir():
|
|
||||||
continue
|
|
||||||
|
|
||||||
for file_path in source_root.rglob("*.cs"):
|
|
||||||
if any(part in {"bin", "obj"} for part in file_path.parts):
|
|
||||||
continue
|
|
||||||
|
|
||||||
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(
|
|
||||||
Violation(
|
|
||||||
location=f"{relative_path}:{line_number}",
|
|
||||||
message=f"forbidden source-generator attribute [{attribute_name}] in runtime module",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return violations
|
|
||||||
|
|
||||||
|
|
||||||
def iter_dependency_ids(nuspec_root: ET.Element) -> list[str]:
|
|
||||||
dependency_ids: list[str] = []
|
|
||||||
for namespace in (PACKAGE_NAMESPACE, PACKAGE_NAMESPACE_2012):
|
|
||||||
dependency_ids.extend(
|
|
||||||
element.attrib["id"]
|
|
||||||
for element in nuspec_root.findall(f".//{{{namespace}}}dependency")
|
|
||||||
if "id" in element.attrib
|
|
||||||
)
|
|
||||||
|
|
||||||
if dependency_ids:
|
|
||||||
return dependency_ids
|
|
||||||
|
|
||||||
dependency_ids.extend(
|
|
||||||
element.attrib["id"]
|
|
||||||
for element in nuspec_root.findall(".//dependency")
|
|
||||||
if "id" in element.attrib
|
|
||||||
)
|
|
||||||
return dependency_ids
|
|
||||||
|
|
||||||
|
|
||||||
def validate_packed_dependencies(package_dir: Path) -> list[Violation]:
|
|
||||||
violations: list[Violation] = []
|
|
||||||
runtime_package_ids = get_runtime_package_ids()
|
|
||||||
|
|
||||||
for package_path in sorted(package_dir.glob("*.nupkg")):
|
|
||||||
with zipfile.ZipFile(package_path) as archive:
|
|
||||||
nuspec_entries = [name for name in archive.namelist() if name.endswith(".nuspec")]
|
|
||||||
if not nuspec_entries:
|
|
||||||
violations.append(
|
|
||||||
Violation(
|
|
||||||
location=str(package_path.relative_to(REPO_ROOT if package_path.is_relative_to(REPO_ROOT) else package_dir.parent)),
|
|
||||||
message="missing nuspec entry",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
nuspec_root = ET.fromstring(archive.read(nuspec_entries[0]))
|
|
||||||
|
|
||||||
package_id_element = nuspec_root.find(f".//{{{PACKAGE_NAMESPACE}}}id")
|
|
||||||
if package_id_element is None:
|
|
||||||
package_id_element = nuspec_root.find(f".//{{{PACKAGE_NAMESPACE_2012}}}id")
|
|
||||||
if package_id_element is None:
|
|
||||||
package_id_element = nuspec_root.find(".//id")
|
|
||||||
|
|
||||||
package_id = get_xml_text(package_id_element) if package_id_element is not None else package_path.stem
|
|
||||||
if package_id not in runtime_package_ids:
|
|
||||||
continue
|
|
||||||
|
|
||||||
dependency_ids = iter_dependency_ids(nuspec_root)
|
|
||||||
for dependency_id in dependency_ids:
|
|
||||||
if not dependency_id.startswith(FORBIDDEN_PACKAGE_REFERENCE_PREFIX) and not dependency_id.startswith(
|
|
||||||
FORBIDDEN_PROJECT_REFERENCE_PREFIX
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "SourceGenerators" not in dependency_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
violations.append(
|
|
||||||
Violation(
|
|
||||||
location=str(package_path),
|
|
||||||
message=f"runtime package {package_id} depends on forbidden package {dependency_id}",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return violations
|
|
||||||
|
|
||||||
|
|
||||||
def print_violations(violations: list[Violation]) -> None:
|
|
||||||
for violation in violations:
|
|
||||||
print(f"- {violation.location}: {violation.message}")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
args = parse_args()
|
|
||||||
|
|
||||||
violations: list[Violation] = []
|
|
||||||
violations.extend(validate_project_references())
|
|
||||||
violations.extend(validate_source_attributes())
|
|
||||||
|
|
||||||
if args.package_dir is not None:
|
|
||||||
package_dir = args.package_dir if args.package_dir.is_absolute() else REPO_ROOT / args.package_dir
|
|
||||||
if not package_dir.is_dir():
|
|
||||||
print(f"Package directory does not exist: {package_dir}", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
violations.extend(validate_packed_dependencies(package_dir))
|
|
||||||
|
|
||||||
if violations:
|
|
||||||
print("Runtime/source-generator boundary validation failed.")
|
|
||||||
print_violations(violations)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print("Runtime/source-generator boundary validation passed.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
Loading…
x
Reference in New Issue
Block a user