diff --git a/scripts/snippets/model.py b/scripts/snippets/model.py index 9eb1b6984..4c7c4f913 100644 --- a/scripts/snippets/model.py +++ b/scripts/snippets/model.py @@ -179,3 +179,15 @@ class SnippetAttributeIssue: class SnippetConditionContext: snippet: SnippetTag condition: IfVersionCondition | None + + +class CandidateConditionRule(str, Enum): + CONDITION_REQUIRED = "condition_required" + IDENTITY_MISMATCH = "identity_mismatch" + + +@dataclass(frozen=True) +class CandidateConditionIssue: + rule: CandidateConditionRule + span: Span + message: str diff --git a/scripts/snippets/semantics.py b/scripts/snippets/semantics.py index 102d577d4..eb6fd7ea7 100644 --- a/scripts/snippets/semantics.py +++ b/scripts/snippets/semantics.py @@ -1,6 +1,8 @@ from __future__ import annotations from .model import ( + CandidateConditionIssue, + CandidateConditionRule, ElseTag, IfVersionCondition, IfVersionTag, @@ -172,3 +174,39 @@ def map_snippet_condition_contexts( ) ) return tuple(contexts) + + +def validate_candidate_condition( + source: ( + ImmutableSourceReference | PullRequestSnippetSource | LocalSourceReference + ), + context: SnippetConditionContext, +) -> tuple[CandidateConditionIssue, ...]: + """Require candidate source identity to match its enclosing condition.""" + + if not isinstance(source, PullRequestSnippetSource): + return () + condition = context.condition + if condition is None: + return ( + CandidateConditionIssue( + rule=CandidateConditionRule.CONDITION_REQUIRED, + span=context.snippet.span, + message="Candidate pull-request snippet must be inside IfVersion", + ), + ) + if ( + source.repository != condition.repository + or source.pull_request != condition.contains_pull_request + ): + return ( + CandidateConditionIssue( + rule=CandidateConditionRule.IDENTITY_MISMATCH, + span=context.snippet.span, + message=( + "Candidate snippet does not match its enclosing IfVersion " + f"({condition.repository}#{condition.contains_pull_request})" + ), + ), + ) + return () diff --git a/tests/test_candidate_snippet_condition_validation.py b/tests/test_candidate_snippet_condition_validation.py new file mode 100644 index 000000000..d2194eb10 --- /dev/null +++ b/tests/test_candidate_snippet_condition_validation.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from scripts.snippets.model import ( + CandidateConditionRule, + IfVersionCondition, + ImmutableSourceReference, + PullRequestSnippetSource, + SnippetConditionContext, + SnippetTag, + Span, +) +from scripts.snippets.semantics import validate_candidate_condition + +SPAN = Span(start=0, end=1, line=1, column=1) +SNIPPET = SnippetTag(attributes=(), span=SPAN) +CONDITION = IfVersionCondition( + repository="canton-network/splice", + contains_pull_request=6123, + span=SPAN, +) +CANDIDATE = PullRequestSnippetSource( + repository="canton-network/splice", + pull_request=6123, + path="apps/file.yaml", +) + + +def test_accepts_candidate_matching_enclosing_condition() -> None: + context = SnippetConditionContext(snippet=SNIPPET, condition=CONDITION) + + assert validate_candidate_condition(CANDIDATE, context) == () + + +def test_requires_condition_around_candidate() -> None: + context = SnippetConditionContext(snippet=SNIPPET, condition=None) + + issues = validate_candidate_condition(CANDIDATE, context) + assert {issue.rule for issue in issues} == { + CandidateConditionRule.CONDITION_REQUIRED + } + + +def test_requires_repository_and_pr_identity_match() -> None: + context = SnippetConditionContext(snippet=SNIPPET, condition=CONDITION) + other_candidate = PullRequestSnippetSource( + repository="canton-network/splice", + pull_request=6124, + path="apps/file.yaml", + ) + + issues = validate_candidate_condition(other_candidate, context) + assert {issue.rule for issue in issues} == { + CandidateConditionRule.IDENTITY_MISMATCH + } + + +def test_does_not_restrict_non_candidate_sources() -> None: + immutable = ImmutableSourceReference( + repository="canton-network/splice", + commit="2c941ea9e834d7602d388f3271c0f864025ea756", + path="apps/file.yaml", + ) + context = SnippetConditionContext(snippet=SNIPPET, condition=None) + + assert validate_candidate_condition(immutable, context) == ()