Add Describable interface for human-readable source descriptions#53
Conversation
Introduces a new optional Describable interface with a describe(): string method that produces human-readable descriptions of Source trees. All five core Sources implement it: StaticSource, SymbolSource, TypeDefinition, InfixExpression, and UnaryExpression. Composite sources recursively describe their children, falling back to the class short name for non-Describable sources. https://claude.ai/code/session_011wv1MvWN6N2u8bYZZz8QA8
- Replace preg_replace with str_ends_with/substr in TypeDefinition to avoid PHPStan error from preg_replace's nullable return type - Add UsesClass attributes for Type classes used in DescribableTest - Add tests for non-Describable fallback branches in TypeDefinition, InfixExpression (both left and right), and UnaryExpression https://claude.ai/code/session_011wv1MvWN6N2u8bYZZz8QA8
Rework all Describable implementations to produce human-readable natural language descriptions instead of terse structured notation: - StaticSource: "the value 42" - SymbolSource: "the symbol 'price'" - TypeDefinition: "the symbol 'price' as a number" - InfixExpression: "the value 1 plus the value 2" with operator word mappings and parenthetical grouping for nested expressions - UnaryExpression: "the negation of the symbol 'active'" https://claude.ai/code/session_011wv1MvWN6N2u8bYZZz8QA8
Use raw values and operator symbols instead of verbose natural language: - StaticSource: 42, 'hello', null - SymbolSource: price, math.pi - TypeDefinition: price (as number) - InfixExpression: price * quantity, with parens for nesting - UnaryExpression: !active, -5 Complex example: price (as number) * (1 - rates.discount (as number)) https://claude.ai/code/session_011wv1MvWN6N2u8bYZZz8QA8
Adds describe() to MemberAccessSource, MatchExpression, MatchArm, LiteralPattern, WildcardPattern and ExpressionPattern so the full source tree can be textualised for AI consumption. https://claude.ai/code/session_01KErFD38kaqTb4JGusuSirg
| public function describe(): string | ||
| { | ||
| $operand = $this->operand instanceof Describable | ||
| ? $this->operand->describe() | ||
| : (new \ReflectionClass($this->operand))->getShortName(); | ||
|
|
||
| return sprintf('%s%s', $this->operator, $operand); | ||
| } |
There was a problem hiding this comment.
This could get confusing with infix.
! ($a || $b) would show as ! $a || $b
| public function describe(): string | |
| { | |
| $operand = $this->operand instanceof Describable | |
| ? $this->operand->describe() | |
| : (new \ReflectionClass($this->operand))->getShortName(); | |
| return sprintf('%s%s', $this->operator, $operand); | |
| } | |
| public function describe(): string | |
| { | |
| $operand = $this->operand instanceof Describable | |
| ? $this->operand->describe() | |
| : (new \ReflectionClass($this->operand))->getShortName(); | |
| if ($this->operand instanceof InfixExpression) { | |
| $operand = sprintf('(%s)', $operand); | |
| } | |
| return sprintf('%s%s', $this->operator, $operand); | |
| } |
There was a problem hiding this comment.
Good catch — applied the suggestion in bfce70d and added a regression test (unary_wraps_infix_operand_in_parentheses) asserting !(a || b).
Generated by Claude Code
| } | ||
|
|
||
| #[Test] | ||
| public function type_definition_with_non_describable_source_falls_back_to_class_name(): void |
There was a problem hiding this comment.
None of the falls_back_to_class_name tests actually test it falls back to class name. I could change the fallback logic and tests still pass.
There was a problem hiding this comment.
You're right. Replaced the anonymous classes with named fixtures (UndescribableSource, UndescribablePattern) and switched the assertions to assertSame against the full expected output (e.g. 'UndescribableSource + 1', '!UndescribableSource'), so the class-name fallback is now actually verified. Done in bfce70d.
Generated by Claude Code
- Wrap UnaryExpression's operand in parens when it's an InfixExpression so !($a || $b) renders as !(a || b) instead of !a || b. - Replace anonymous-class fallback tests with named fixtures (UndescribableSource, UndescribablePattern) and assert exact output so the class-name fallback is actually verified. https://claude.ai/code/session_01KErFD38kaqTb4JGusuSirg
Description
This PR introduces a new
Describableinterface that enables human-readable descriptions of the source tree, suitable for textualizing AST structures for AI consumption. All major source and pattern classes now implement this interface with appropriatedescribe()methods.Type of Change
Changes Made
Describableinterface insrc/Describable.phpwith adescribe(): stringmethodDescribableon all source classes:StaticSource- exports values usingSebastianBergmann\Exporter(e.g.,'hello',42,null,true)SymbolSource- describes names with optional namespace (e.g.,price,math.pi)TypeDefinition- describes source with type annotation (e.g.,price (as number))InfixExpression- describes binary operations with parentheses for nested expressions (e.g.,price * (1 - discount))UnaryExpression- describes unary operations (e.g.,!active,-5)MemberAccessSource- describes property access chains (e.g.,user.address.city)MatchExpression- describes match statements (e.g.,match status { 'on' => true, _ => false })MatchArm- describes pattern-to-expression mappings (e.g.,1 => 'one')Describableon all pattern classes:LiteralPattern- exports literal valuesWildcardPattern- describes as underscore (_)ExpressionPattern- delegates to inner source descriptionDescribableTest.php) with 40+ test cases covering all classes and edge casesTesting
DescribableTest.php)Code Quality Checklist
Documentation
Breaking Changes
N/A - This is a purely additive change. All new implementations are on existing classes but don't modify their public API or behavior.
Additional Notes
The implementation uses reflection as a fallback mechanism when sources don't implement
Describable, ensuring robustness when dealing with custom or third-party source implementations. TheSebastianBergmann\Exporteris used for consistent value formatting acrossStaticSourceandLiteralPattern.By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.
https://claude.ai/code/session_01KErFD38kaqTb4JGusuSirg