diff --git a/scripts/snippets/model.py b/scripts/snippets/model.py index c720c458f..9eb1b6984 100644 --- a/scripts/snippets/model.py +++ b/scripts/snippets/model.py @@ -173,3 +173,9 @@ class SnippetAttributeIssue: rule: SnippetAttributeRule span: Span message: str + + +@dataclass(frozen=True) +class SnippetConditionContext: + snippet: SnippetTag + condition: IfVersionCondition | None diff --git a/scripts/snippets/semantics.py b/scripts/snippets/semantics.py index 1539bdddf..102d577d4 100644 --- a/scripts/snippets/semantics.py +++ b/scripts/snippets/semantics.py @@ -1,12 +1,16 @@ from __future__ import annotations from .model import ( + ElseTag, + IfVersionCondition, + IfVersionTag, ImmutableSourceReference, LocalSourceReference, PullRequestSnippetSource, PullRequestSourceReference, SnippetAttributeIssue, SnippetAttributeRule, + SnippetConditionContext, SnippetSourceAttributeIssue, SnippetSourceAttributeRule, SnippetSourceAttributeValidation, @@ -136,3 +140,35 @@ def validate_snippet_basic_attributes( ) ) return tuple(issues) + + +def map_snippet_condition_contexts( + snippets: tuple[SnippetTag, ...], + conditional_tags: tuple[IfVersionTag | ElseTag, ...], + conditions: tuple[IfVersionCondition, ...], +) -> tuple[SnippetConditionContext, ...]: + """Associate snippets with their innermost enclosing IfVersion condition.""" + + conditions_by_start = { + condition.span.start: condition for condition in conditions + } + events = sorted( + (*snippets, *conditional_tags), key=lambda event: event.span.start + ) + stack: list[IfVersionCondition | None] = [] + contexts: list[SnippetConditionContext] = [] + for event in events: + if isinstance(event, IfVersionTag): + if event.closing: + if stack: + stack.pop() + else: + stack.append(conditions_by_start.get(event.span.start)) + elif isinstance(event, SnippetTag): + contexts.append( + SnippetConditionContext( + snippet=event, + condition=stack[-1] if stack else None, + ) + ) + return tuple(contexts) diff --git a/tests/test_snippet_condition_context.py b/tests/test_snippet_condition_context.py new file mode 100644 index 000000000..768f05c9b --- /dev/null +++ b/tests/test_snippet_condition_context.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from scripts.snippets.model import IfVersionCondition, IfVersionTag +from scripts.snippets.references import parse_github_repository_url +from scripts.snippets.semantics import map_snippet_condition_contexts +from scripts.snippets.syntax import ( + parse_if_version_tags, + parse_snippet_tags, +) + + +def conditions(text: str) -> tuple[IfVersionCondition, ...]: + result: list[IfVersionCondition] = [] + for tag in parse_if_version_tags(text): + if not isinstance(tag, IfVersionTag) or tag.closing: + continue + repository_value = tag.attribute("repository") + candidate = tag.attribute("containsPullRequest") + assert isinstance(repository_value, str) + repository = parse_github_repository_url(repository_value) + assert repository is not None + assert isinstance(candidate, int) + result.append( + IfVersionCondition( + repository=repository, + contains_pull_request=candidate, + span=tag.span, + ) + ) + return tuple(result) + + +def contexts(text: str): + return map_snippet_condition_contexts( + parse_snippet_tags(text), + parse_if_version_tags(text), + conditions(text), + ) + + +def test_maps_unconditional_snippet_to_no_condition() -> None: + result = contexts('') + + assert len(result) == 1 + assert result[0].condition is None + + +def test_maps_both_branches_to_enclosing_condition() -> None: + result = contexts( + """ + + + + +""" + ) + + assert [context.condition.contains_pull_request for context in result] == [ + 12, + 12, + ] + + +def test_maps_nested_snippet_to_innermost_condition() -> None: + result = contexts( + """ + + + +""" + ) + + assert result[0].condition is not None + assert result[0].condition.repository == "example/inner" + assert result[0].condition.contains_pull_request == 34