V6_GENERICS.md
This document tracks the implementation of enhanced generics support in Phan v6 while maintaining Phan's core goals:
@template T - Basic template declarations@template T of SomeClass - Constraints with enforcement across class hierarchies and function calls@extends ParentClass<Type> - Template inheritance for classesclass-string<T> - Type-safe class stringsarray{key:type}@template-covariant / @template-contravariant enforcement for array shapes and other compound types (difficult)Goal: Enable generic interfaces and traits
Priority: P1 (High Value, Moderate Effort)
Performance Impact: Minimal - similar to existing @extends implementation
Status: Task 1.1 (interfaces) ✅ Complete | Task 1.2 (traits) ✅ Complete
@template-implements ✅Status: COMPLETE Actual Effort: 5 hours total (Parsing: 2 hours | Resolution: 3 hours) Files Modified:
src/Phan/Language/Element/Comment/Builder.php - Parse annotationsrc/Phan/Language/Element/Comment.php - Store implements listsrc/Phan/Language/Element/Clazz.php - Process template parameters and resolutionsrc/Phan/Parse/ParseVisitor.php - Populate interface type mappingsrc/Phan/Analysis/ParameterTypesAnalyzer.php - Override compatibility with templatestests/files/src/1100_template_implements.php - Test casestests/files/expected/1100_template_implements.php.expected - Expected outputPhase 1 - Parsing (Complete):
$implemented_types field to Comment.phpgetImplementedTypes() getter method in Comment.phpapplyOverride() support for 'implements' key in Comment.php$implemented_types field to Builder.phpimplementsFromCommentLine() parsing method in Builder.phpmaybeParseImplements() handler in Builder.phpmaybeParsePhanImplements() handler in Builder.phpparseCommentLine() switchmaybeParsePhanCustomAnnotation()Phase 2 - Template Resolution (Complete):
$interface_type_map field to Clazz.phpsetInterfaceType() method to store interface typesgetInterfaceType() method to retrieve interface typesimportAncestorClasses() to use mapped interface types./vendor/bin/phpunit tests/Phan/PhanTestNew.php — 104 tests, 0 failuresWhat Works Now:
@implements Interface<Type> and @phan-implements Interface<Type>@implements Repository<array<string, User>>)Test Cases Implemented (tests/files/src/1100_template_implements.php):
// Basic implementation with concrete type
/**
* @implements Repository<User>
*/
class UserRepository implements Repository {
public function find(int $id): User { return new User(); }
public function save($entity): void { }
}
// Multiple interfaces with template parameters
/**
* @template T
* @implements Iterator<int, T>
* @implements Countable
*/
class UserCollection implements Iterator, Countable { ... }
// Nested generic types
/**
* @implements Repository<array<string, User>>
*/
class UserMapRepository implements Repository {
public function find(int $id): array { return []; }
public function save($entity): void { }
}
// Using @phan-implements variant
/**
* @template T
* @phan-implements Repository<T>
*/
class GenericRepository implements Repository { ... }
All test cases pass with correct template resolution and zero false positives.
Additional negative coverage: tests/files/src/1102_template_parameter_mismatch.php exercises missing and extra template parameter diagnostics across @implements, @use, and @extends, and now includes scaffolding for future variance tests (@template-covariant).
Performance Considerations:
Technical Implementation Details:
The implementation follows the same pattern as @extends for parent classes:
Storage Pattern (Clazz.php):
$interface_type_map stores FQSEN → Type mappings$parent_type but supports multiple interfacesPopulation Flow (ParseVisitor.php):
implements clause from AST@implements from comment via getImplementedTypes()setInterfaceType()Resolution Flow (Clazz.php → importAncestorClasses()):
getInterfaceType()importAncestorClass() which calls addMethod()addMethod() applies template substitution via cloneWithTemplateParameterTypeMap()Override Checking (ParameterTypesAnalyzer.php):
analyzeOverrideSignatureForOverriddenMethod()Key Design Decisions:
cloneWithTemplateParameterTypeMap)@template-use ✅Status: COMPLETE Actual Effort: 3 hours total (Parsing: 1 hour | Resolution: 2 hours) Files Modified:
src/Phan/Language/Element/Comment/Builder.php - Parse annotationsrc/Phan/Language/Element/Comment.php - Store trait typessrc/Phan/Language/Element/Clazz.php - Process and resolve trait templatessrc/Phan/Parse/ParseVisitor.php - Populate trait type mappingtests/files/src/1101_template_use.php - Test casestests/files/expected/1101_template_use.php.expected - Expected outputPhase 1 - Parsing (Complete):
$used_trait_types field to Comment.phpgetUsedTraitTypes() getter method in Comment.phpapplyOverride() support for 'use' key in Comment.php$used_trait_types field to Builder.phpuseFromCommentLine() parsing method in Builder.phpmaybeParseUse() handler in Builder.phpmaybeParsePhanUse() handler in Builder.phpparseCommentLine() switchmaybeParsePhanCustomAnnotation()Phase 2 - Template Resolution (Complete):
$trait_type_map field to Clazz.phpsetTraitType() method to store trait typesgetTraitType() method to retrieve trait typesimportAncestorClasses() to use mapped trait typesadaptInheritedMethodFromTrait()
createUseAlias, preventing later resolutionNone::instance() to addMethod() for traits to avoid double resolution./vendor/bin/phpunit tests/Phan/PhanTestNew.phpWhat Works Now:
@use Trait<Type> and @phan-use Trait<Type>@use Repository<array<string, User>>)@template T @phan-use Trait<T>)Test Cases Implemented (tests/files/src/1101_template_use.php):
// Basic trait usage with concrete type
/**
* @use Repository<User>
*/
class UserService {
use Repository;
// getEntity() correctly returns User instead of T
}
// Multiple traits with different template parameters
/**
* @use Repository<Article>
* @use Timestamped<\DateTimeImmutable>
*/
class ArticleService {
use Repository;
use Timestamped;
// getEntity() returns Article, getTimestamp() returns DateTimeImmutable
}
// Nested generic types
/**
* @use Repository<array<string, User>>
*/
class UserMapService {
use Repository;
// getEntity() returns array<string, User>
}
// Class template with trait template
/**
* @template T
* @phan-use Repository<T>
*/
class GenericService {
use Repository;
// get() correctly returns T (class's template parameter)
}
All test cases pass with correct template resolution:
Performance Considerations:
Technical Implementation Details:
The implementation follows the exact same pattern as @template-implements:
Storage Pattern (Clazz.php):
$trait_type_map stores FQSEN → Type mappings$interface_type_mapPopulation Flow (ParseVisitor.php):
use clause from AST@use from comment via getUsedTraitTypes()setTraitType()Resolution Flow (Clazz.php → importAncestorClasses()):
getTraitType()adaptInheritedMethodFromTrait()
adaptInheritedMethodFromTrait() changes method FQSENaddMethod()None::instance() to addMethod() to prevent double resolutionKey Difference from Interfaces:
addMethod()createUseAlias → must resolve earlierKey Design Decisions:
cloneWithTemplateParameterTypeMap)Goal: Make declared bounds (@template T of Foo) enforceable for inheritance hierarchies and template-aware call sites.
Priority: P1 (High Value, High Effort)
Status: COMPLETE
Highlights:
Comment/Builder now captures optional of ... constraints and instantiates TemplateType instances with bound union types, including support for @template-contravariant tokens (still ignored for enforcement).TemplateType caches keyed by identifier + bound, exposes getBoundUnionType()/hasBound(), and memoizes unionTypeSatisfiesBound() for reuse.Clazz::enforceTemplateConstraintForAncestor() validates template arguments supplied via @extends, @implements, and @use, emitting the new PhanTemplateTypeConstraintViolation (ID 14013) with precise source locations.ArgumentType::analyzeParameter() inspects template-bearing parameters to verify argument unions respect declared bounds; FunctionTrait helpers share argument-to-union extraction logic.PhanTemplateTypeConstraintViolation to Issue.php for downstream suppression/configuration.tests/files/src/1103_template_constraint_enforcement.php covers positive and negative scenarios across classes, traits, and function calls with expected output captured in tests/files/expected/1103_template_constraint_enforcement.php.expected.tests/files/src/1104_template_variance_parsing.php asserts covariant/contravariant enforcement rules via tests/files/expected/1104_template_variance_parsing.php.expected.1102_template_parameter_mismatch.php) now doubles as a property variance regression, verifying that mutable properties reject covariant templates while read-only ones remain valid.1105_template_property_variance.php) ensures covariant templates remain permitted on read-only properties while write-only annotations still trigger violations.1102_template_parameter_mismatch.php to include variance scaffolding and ensure constraint enforcement coexists with count validation../vendor/bin/phpunit tests/Phan/PhanTestNew.php now exercises 105 tests (210 assertions) including the new constraint suite.What Works Now:
@extends, @implements, @use).T with its own bound).PhanTemplateTypeVarianceViolation when templates appear in incompatible positions.readonly or doc @phan-read-only) and disallowing contravariant templates entirely on properties.New Issue Type:
PhanTemplateTypeConstraintViolation (ID 14013) — emitted when a template argument does not satisfy its declared of constraint.PhanTemplateTypeVarianceViolation (ID 14014) — emitted when variance annotations conflict with usage positions (e.g., covariant templates in parameters).Performance Considerations:
Goal: Make @template T of Foo actually validate types
Priority: P2 (High Value, Moderate-High Effort)
Performance Impact: Moderate - requires validation at instantiation points
Status: Complete Notes:
TemplateType now caches bounds and variance metadata, keyed by identifier + constraint.Comment/Builder captures the optional of ... clause (and variance keywords) when parsing @template tags.TemplateScope/scope maps preserve the enriched TemplateType instances for use during analysis.Implementation Notes:
// Default invariant template without bounds
TemplateType::instanceForId($template_type_identifier, false, null, TemplateType::VARIANCE_INVARIANT);
// Template with explicit bound and variance metadata
TemplateType::instanceForId($template_type_identifier, false, $constraint_union_type, TemplateType::VARIANCE_CONTRAVARIANT);
Constraint Parsing:
// Builder.php now captures both bounds and variance modifiers
'/@(?:phan-)?template(?:-(?:co|contra)variant)?\s+(?P<identifier>' . self::WORD_REGEX . ')(?:\s+of\s+(?P<constraint>' . UnionType::union_type_regex . '))?/'
Performance Considerations:
Status: Complete Notes:
Clazz::enforceTemplateConstraintForAncestor() checks docblock instantiations for @extends, @implements, and @use, emitting PhanTemplateTypeConstraintViolation on mismatches.ArgumentType::analyzeParameter() reuses the same logic for template-bearing parameters at call sites, ensuring inferred arguments respect declared bounds.tests/files/src/1103_template_constraint_enforcement.php) exercises class, trait, and function scenarios.Validation Points:
@extends Generic<ConcreteType>@implements Repository<User>@use Timestamped<DateTime>Test Cases:
/**
* @template T of \DateTimeInterface
*/
class DateProcessor {
/** @var T */
private $date;
}
// OK
/** @extends DateProcessor<\DateTime> */
class Processor1 extends DateProcessor {}
// ERROR: string is not DateTimeInterface
/** @extends DateProcessor<string> */
class Processor2 extends DateProcessor {}
Performance Considerations:
Goal: Enforce @template-covariant and @template-contravariant semantics
Priority: P2 (Very High Value, High Effort)
Performance Impact: Moderate - requires tracking read/write positions
@template-contravariant ✅ COMPLETEStatus: Complete Notes:
Comment/Builder and TemplateType::instanceForId() now understand the -(?:co|contra)variant suffix and cache variance alongside bounds.Implementation:
enum TemplateVariance {
INVARIANT, // default @template
COVARIANT, // @template-covariant
CONTRAVARIANT // @template-contravariant
}
Status: Complete Notes:
PhanTemplateTypeVarianceViolation when covariant templates appear in parameter types or contravariant templates appear in return types.Position Tracking:
Read positions (covariant OK):
Write positions (contravariant OK):
Invariant positions (both):
Test Cases:
/**
* @template-covariant T
*/
class Box {
/** @var T */
private $value;
/** @return T */
public function get() { return $this->value; } // OK - read position
/** @param T $value */
public function set($value): void { } // ERROR - write position with covariant
}
/**
* @template-contravariant T
*/
class Sink {
/** @param T $value */
public function consume($value): void { } // OK - write position
/** @return T */
public function produce() { } // ERROR - read position with contravariant
}
Performance Considerations:
Status: Complete Estimated Effort: 5 minutes
Remove the "XXX" comment acknowledging lack of support:
// BEFORE:
case 'template-covariant': // XXX Phan does not actually support @template-covariant semantics
// AFTER:
case 'template-covariant': // Enforces covariant template variance
Goal: Add Psalm/PHPStan utility types
Priority: P4 (Medium Value, Medium Effort)
Performance Impact: Low - these are parse-time transformations
key-of<T> and value-of<T>Status: Complete Highlights:
KeyOfType/ValueOfType implementations that expand to literal/int/string unions, falling back to array-key/mixed when source containers are imprecise.Type::fromStringInContext, allowing template substitution downstream.tests/files/src/1106_key_value_utility_types.php asserts both positive and negative scenarios (expected output in tests/files/expected/1106_key_value_utility_types.php.expected).Example:
/** @var key-of<array{foo: int, bar: string}> */ // Resolves to 'foo'|'bar'
/** @var value-of<array{foo: int, bar: string}> */ // Resolves to int|string
int-range<min, max>Status: Complete Highlights:
IntRangeType, tracking inclusive bounds on specialized integers and interoperating with literal ints during casting.int-range template parameters and degrades gracefully when malformed hints are encountered.tests/files/src/1109_int_range.php exercises argument/return enforcement against literal values (expected output in tests/files/expected/1109_int_range.php.expected).Example:
/** @param int-range<1, 100> $percentage */
function setOpacity(int $percentage): void { }
positive-int, negative-intStatus: Complete Files to Modify:
PositiveIntType, NegativeIntTypetests/files/src/XXXX_template_<feature>.php - Test implementationtests/files/expected/XXXX_template_<feature>.php.expected - Expected errors@template - Already covered (0597, 0993, etc.)@template-implements - Test 1100 (Complete with 4 scenarios)@template-use - Test 1101 (Complete with 4 scenarios)key-of/value-of - Test 1106 (shape + generic coverage)int-range - Test 1109 (literal-bound, negative ranges, reversed bounds)positive-int/negative-int - Test 1110 (strict literal enforcement)key-of without generics - Test 1114 (lenient parsing)README.mdShould we support @psalm- and @phpstan- prefixed versions?
How strict should constraint validation be?
PhanTemplateTypeConstraintViolationShould variance checking be opt-in or always on?
Status: Not implemented (by design)
Child class templates do not automatically inherit bounds declared on parent templates:
/** @template T of Foo */
class Base {}
/** @template T */ // T is unconstrained
class Child extends Base {}
Workaround: redeclare the bound explicitly
/** @template T of Foo */
class Child extends Base {}
Rationale: keeping constraints explicit avoids hidden coupling to parent changes and keeps template contracts self-documenting.
# Phan analyzing itself on branch v6 (68a899b8d) — measured 2024-05-16
time ./phan --no-progress-bar
# Output: real 12.93