diff --git a/.claude/hooks/php-ordering-conformance.py b/.claude/hooks/php-ordering-conformance.py new file mode 100644 index 0000000..21a3ec0 --- /dev/null +++ b/.claude/hooks/php-ordering-conformance.py @@ -0,0 +1,607 @@ +#!/usr/bin/env python3 +"""PHP ordering conformance hook for tiny-blocks PHP libraries. + +Self-contained PostToolUse hook on Edit|Write|MultiEdit. Verifies the deterministic +ordering conventions for PHP declarations: + +- Parameter ordering: declaration parameters (constructors, factories, methods, + property promotion) in three tiers, required parameters first, then defaulted + parameters, then a variadic, each tier by identifier length ascending, + alphabetical tie-breaker, semantic pairs preserved. A PHPUnit test method fed by + a data provider is exempt, its parameters are the columns of its data set. +- Member ordering: constants, enum cases, constructor, static methods, instance + methods, in that group order, each group length-ascending with alphabetical + tie-breaker. PHPUnit test classes instead order methods as lifecycle hooks (in + execution order), then other methods, then data providers. + +The analysis is pure (FileUnit in, Violation out) and runs in three passes over +well-formed PHP: a lexical pass blanks every comment, string, and heredoc/nowdoc +body (LITERALS), a structural pass maps every bracket to its pair (bracket_spans); +extraction assigns tokens of interest to their containers by flat walks. Control +flow uses guard clauses only and nesting never exceeds two levels. Reports +violations to stderr and exits 2 to prompt Claude, exits 0 silently if no violations +or the file is out of scope. +""" + +import json +import re +import sys +from dataclasses import dataclass +from enum import Enum +from functools import cached_property +from pathlib import Path +from typing import Final + +# --- Configuration ---------------------------------------------------------- + +# In-scope files: PHP sources under src/ or tests/. +SCOPE_PATTERN: Final = re.compile(r"(^|/)(src|tests)/.+\.php$") + +# Semantic pairs (exhaustive). Natural order wins between +# the two members when both appear in the same parameter list. +SEMANTIC_PAIRS: Final = ( + ("start", "end"), + ("from", "to"), + ("startAt", "endAt"), + ("createdAt", "updatedAt"), + ("before", "after"), + ("min", "max"), +) + +# Each member maps to (first, second, position). Both members keep their natural +# order only when both are present, sorting as a unit at the lead member's key. +PAIR_MEMBER: Final = { + member: (first, second, position) + for first, second in SEMANTIC_PAIRS + for position, member in enumerate((first, second)) +} + +MODIFIERS: Final = ("abstract", "final", "private", "protected", "public", "static") + +# The lexical grammar: every PHP construct that must not be scanned as code. +# Alternatives are ordered, the heredoc label closes via backreference. +LITERALS: Final = re.compile( + r""" + /\*.*?\*/ # block comment + | //[^\n]* # line comment + | \#(?!\[)[^\n]* # hash comment, never a #[ attribute + | <<<[ \t]*(?P['"]?)(?P