diff --git a/scripts/snippets/model.py b/scripts/snippets/model.py index 7ab2b3608..b49f6dad9 100644 --- a/scripts/snippets/model.py +++ b/scripts/snippets/model.py @@ -227,3 +227,20 @@ class ConditionPageValidation: tags: tuple[IfVersionTag | ElseTag, ...] conditions: tuple[IfVersionCondition, ...] diagnostics: tuple[Diagnostic, ...] + + +@dataclass(frozen=True) +class ValidatedSnippet: + tag: SnippetTag + source: ( + ImmutableSourceReference + | PullRequestSnippetSource + | LocalSourceReference + ) + condition: IfVersionCondition | None + + +@dataclass(frozen=True) +class SnippetPageValidation: + snippets: tuple[ValidatedSnippet, ...] + diagnostics: tuple[Diagnostic, ...] diff --git a/scripts/snippets/page_validation.py b/scripts/snippets/page_validation.py index 10afe3aec..e98cd73c8 100644 --- a/scripts/snippets/page_validation.py +++ b/scripts/snippets/page_validation.py @@ -3,16 +3,38 @@ from pathlib import Path from .diagnostics import ( + candidate_condition_diagnostics, directive_syntax_diagnostic, if_version_attribute_diagnostics, if_version_structure_diagnostics, + local_source_policy_diagnostic, + snippet_attribute_diagnostics, + snippet_source_attribute_diagnostics, + snippet_source_safety_diagnostics, +) +from .model import ( + ConditionPageValidation, + IfVersionTag, + SnippetPageValidation, + ValidatedSnippet, ) -from .model import ConditionPageValidation, IfVersionTag from .registry import RepositoryRegistry -from .syntax import DirectiveSyntaxError, parse_if_version_tags +from .semantics import ( + map_snippet_condition_contexts, + resolve_snippet_source_attributes, + validate_candidate_condition, + validate_snippet_basic_attributes, +) +from .syntax import ( + DirectiveSyntaxError, + parse_if_version_tags, + parse_snippet_tags, +) from .validation import ( validate_if_version_attributes, validate_if_version_structure, + validate_local_source_policy, + validate_snippet_source_safety, ) @@ -48,3 +70,73 @@ def validate_condition_page( conditions=tuple(conditions), diagnostics=tuple(diagnostics), ) + + +def validate_snippet_page( + text: str, + *, + path: Path, + registry: RepositoryRegistry, + conditions: ConditionPageValidation, + allow_local: bool = False, +) -> SnippetPageValidation: + """Parse and validate snippets against prevalidated condition context.""" + + try: + tags = parse_snippet_tags(text) + except DirectiveSyntaxError as error: + return SnippetPageValidation( + snippets=(), + diagnostics=(directive_syntax_diagnostic(path, error),), + ) + + diagnostics = [] + snippets = [] + contexts = map_snippet_condition_contexts( + tags, conditions.tags, conditions.conditions + ) + for context in contexts: + tag = context.snippet + tag_diagnostics = list( + snippet_attribute_diagnostics( + path, validate_snippet_basic_attributes(tag) + ) + ) + source_result = resolve_snippet_source_attributes(tag) + tag_diagnostics.extend( + snippet_source_attribute_diagnostics(path, source_result.issues) + ) + source = source_result.source + if source is not None: + tag_diagnostics.extend( + snippet_source_safety_diagnostics( + path, + validate_snippet_source_safety( + source, span=tag.span, registry=registry + ), + ) + ) + local_issues = validate_local_source_policy( + source, span=tag.span, allow_local=allow_local + ) + tag_diagnostics.extend( + local_source_policy_diagnostic(path, issue) + for issue in local_issues + ) + tag_diagnostics.extend( + candidate_condition_diagnostics( + path, validate_candidate_condition(source, context) + ) + ) + diagnostics.extend(tag_diagnostics) + if source is not None and not tag_diagnostics: + snippets.append( + ValidatedSnippet( + tag=tag, + source=source, + condition=context.condition, + ) + ) + return SnippetPageValidation( + snippets=tuple(snippets), diagnostics=tuple(diagnostics) + ) diff --git a/tests/test_snippet_page_validation.py b/tests/test_snippet_page_validation.py new file mode 100644 index 000000000..34042f5d6 --- /dev/null +++ b/tests/test_snippet_page_validation.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from pathlib import Path + +from scripts.snippets.model import SnippetPageValidation +from scripts.snippets.page_validation import ( + validate_condition_page, + validate_snippet_page, +) +from scripts.snippets.registry import ( + RepositoryConfig, + RepositoryRegistry, + RepositoryVisibility, +) + +PATH = Path("docs-main/validator.source.mdx") +REGISTRY = RepositoryRegistry( + ( + RepositoryConfig( + name="canton-network/splice", + url="https://github.com/canton-network/splice", + default_branch="main", + visibility=RepositoryVisibility.PUBLIC, + ), + ) +) +COMMIT = "2c941ea9e834d7602d388f3271c0f864025ea756" + + +def validate( + text: str, *, allow_local: bool = False +) -> SnippetPageValidation: + conditions = validate_condition_page(text, path=PATH, registry=REGISTRY) + return validate_snippet_page( + text, + path=PATH, + registry=REGISTRY, + conditions=conditions, + allow_local=allow_local, + ) + + +def test_returns_validated_immutable_snippet() -> None: + result = validate( + f'' + ) + + assert result.diagnostics == () + assert len(result.snippets) == 1 + assert result.snippets[0].condition is None + + +def test_returns_candidate_matching_condition() -> None: + result = validate( + """ + +""" + ) + + assert result.diagnostics == () + assert result.snippets[0].condition is not None + + +def test_reports_candidate_identity_mismatch() -> None: + result = validate( + """ + +""" + ) + + assert "SNIP028" in { + diagnostic.code for diagnostic in result.diagnostics + } + + +def test_reports_local_ref_remediation_unless_preview_enabled() -> None: + text = ( + '' + ) + + committed = validate(text) + preview = validate(text, allow_local=True) + + assert committed.diagnostics[0].code == "SNIP007" + remediation = committed.diagnostics[0].remediation + assert remediation is not None + assert "snippets:resolve-local" in remediation + assert preview.diagnostics == () + + +def test_reports_snippet_syntax_error() -> None: + result = validate('') + + assert [diagnostic.code for diagnostic in result.diagnostics] == [ + "SNIP014" + ]