diff --git a/CLAUDE.md b/CLAUDE.md index d4e7bdc1e9..3093b8d0aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,6 +210,24 @@ Based on analysis of recent releases (2.1.30-2.1.38), these are the recurring pa A recurring cleanup theme: never use `$type instanceof StringType` or similar. This misses union types, intersection types with accessory types, and other composite forms. Always use `$type->isString()->yes()` or `(new StringType())->isSuperTypeOf($type)`. Multiple PRs have systematically replaced `instanceof *Type` checks throughout the codebase. +### Type system: add methods to the Type interface instead of one-offing conditions + +When a bug requires checking a type property across the codebase, the fix is often to add a new method to the `Type` interface rather than scattering `instanceof` checks or utility function calls throughout rules and extensions. This ensures every type implementation handles the query correctly (including union/intersection types which delegate to their inner types) and keeps the logic centralized. + +Historical analysis of `Type.php` via `git blame` shows that new methods are added for several recurring reasons: + +- **Replacing scattered `instanceof` checks (~30%)**: Methods like `isNull()`, `isTrue()`, `isFalse()`, `isString()`, `isInteger()`, `isFloat()`, `isBoolean()`, `isArray()`, `isScalar()`, `isObject()`, `isEnum()`, `getClassStringObjectType()`, `getObjectClassNames()`, `getObjectClassReflections()` were added to replace `$type instanceof ConstantBooleanType`, `$type instanceof StringType`, etc. Each type implements the method correctly — e.g., `UnionType::isNull()` returns `yes` only if all members are null, `maybe` if some are, `no` if none are. This is impossible to get right with a single `instanceof` check. + +- **Moving logic from TypeUtils/extensions into Type (~35%)**: Methods like `toArrayKey()`, `toBoolean()`, `toNumber()`, `toFloat()`, `toInteger()`, `toString()`, `toArray()`, `flipArray()`, `getKeysArray()`, `getValuesArray()`, `popArray()`, `shiftArray()`, `shuffleArray()`, `reverseSortArray()`, `getEnumCases()`, `isCallable()`, `getCallableParametersAcceptors()`, `isList()` moved scattered utility logic into polymorphic dispatch. When logic lives in a utility function it typically uses a chain of `if ($type instanceof X) ... elseif ($type instanceof Y) ...` which breaks when new type classes are added or misses edge cases in composite types. + +- **Supporting new type features (~15%)**: Methods like `isNonEmptyString()`, `isNonFalsyString()`, `isLiteralString()`, `isClassString()`, `isNonEmptyArray()`, `isIterableAtLeastOnce()` were added as PHPStan gained support for more refined types (accessory types in intersections). These enable rules to query refined properties without knowing how the refinement is represented internally. + +- **Bug fixes through better polymorphism (~10%)**: Some bugs are directly fixed by adding a new Type method. For example, `isOffsetAccessLegal()` fixed false positives about illegal offset access by letting each type declare whether `$x[...]` is valid. `setExistingOffsetValueType()` (distinct from `setOffsetValueType()`) fixed array list type preservation bugs. `toCoercedArgumentType()` fixed parameter type contravariance issues during type coercion. + +- **Richer return types (~5%)**: Methods that returned `TrinaryLogic` were changed to return `AcceptsResult` or `IsSuperTypeOfResult`, which carry human-readable reasons for why a type relationship holds or doesn't. This enabled better error messages without changing the call sites significantly. + +When considering a bug fix that involves checking "is this type a Foo?", first check whether an appropriate method already exists on `Type`. If not, consider whether adding one would be the right fix — especially if the check is needed in more than one place or involves logic that varies by type class. + ### MutatingScope: expression invalidation during scope merging When two scopes are merged (e.g. after if/else branches), `MutatingScope::generalizeWith()` must invalidate dependent expressions. If variable `$i` changes, then `$locations[$i]` must be invalidated too. Bugs arise when stale `ExpressionTypeHolder` entries survive scope merges. Fix pattern: in `MutatingScope`, when a root expression changes, skip/invalidate all deep expressions that depend on it. @@ -291,6 +309,32 @@ Recent work on PHP 8.5 support shows the pattern: - **PhpVersion**: Add detection methods like `supportsPropertyHooks()`, `supportsPipeOperator()`, etc. - **Stubs**: Update function/class stubs for new built-in functions and changed signatures +## Writing PHPDocs + +When adding or editing PHPDoc comments in this codebase, follow these guidelines: + +### What to document + +- **Class-level docs on interfaces and key abstractions**: Explain the role of the interface, what implements it, and how it fits into the architecture. Mention non-obvious patterns like double-dispatch (CompoundType), the intersection-with-base-type requirement (AccessoryType), or the instanceof-avoidance rule (TypeWithClassName). +- **Non-obvious behavior**: Document when a method's behavior differs from what its name suggests, or when there are subtle contracts. For example: `getDeclaringClass()` returning the declaring class even for inherited members, `setExistingOffsetValueType()` vs `setOffsetValueType()` preserving list types differently, or `getWritableType()` potentially differing from `getReadableType()` due to asymmetric visibility. +- **`@api` tags**: Keep these — they mark the public API for extension developers. +- **`@phpstan-assert` tags**: Keep these — they provide type narrowing information that PHPStan uses. +- **`@return`, `@param`, `@template` tags**: Keep when they provide type information not expressible in native PHP types (e.g. `@return self::SOURCE_*`, `@param array`). + +### What NOT to document + +- **Obvious from the method name**: Do not write "Returns the name" above `getName()`, "Returns the value type" above `getValueType()`, or "Returns whether deprecated" above `isDeprecated()`. If the method name says it all, add no description. +- **Obvious to experienced PHP developers**: Do not explain standard visibility rules ("public methods are always callable, protected methods are callable from subclasses..."), standard PHP semantics, or basic design patterns. +- **Obvious from tags**: Do not add prose that restates what `@return`, `@phpstan-assert`, or `@param` tags already say. If `@return non-empty-string|null` is present, do not also write "Returns a non-empty string or null". +- **Factory method descriptions that repeat the class-level doc**: If the class doc already explains the levels/variants (like VerbosityLevel or GeneralizePrecision), don't repeat those descriptions on each factory method. A bare `@api` tag is sufficient. +- **Getter/setter/query methods on value objects**: Methods like `isInvariant()`, `isCovariant()`, `isEmpty()`, `count()`, `getType()`, `hasType()` on simple value objects need no PHPDoc. + +### Style + +- Keep descriptions concise — one or two sentences for method docs when needed. +- Use imperative voice without "Returns the..." preambles when a brief note suffices. Prefer `/** Replaces unresolved TemplateTypes with their bounds. */` over a multi-line block. +- Preserve `@api` and type tags on their own lines, with no redundant description alongside them. + ## Important dependencies - `nikic/php-parser` ^5.7.0 - PHP AST parsing diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 0e2bac3224..5afda44c70 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -22,7 +22,27 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeWithClassName; -/** @api */ +/** + * Represents the state of the analyser at a specific position in the AST. + * + * The Scope tracks everything PHPStan knows at a given point in code: variable types, + * the current class/function/method context, whether strict_types is enabled, and more. + * It is the primary interface through which rules and extensions query information + * about the analysed code. + * + * The Scope is passed as a parameter to: + * - Custom rules (2nd parameter of processNode()) + * - Dynamic return type extensions (last parameter of getTypeFrom*Call()) + * - Dynamic throw type extensions + * - Type-specifying extensions (3rd parameter of specifyTypes()) + * + * The Scope is immutable from the extension's perspective. Each AST node gets + * its own Scope reflecting the analysis state at that point. For example, after + * an `if ($x instanceof Foo)` check, the Scope inside the if-branch knows that + * $x is of type Foo. + * + * @api + */ interface Scope extends ClassMemberAccessAnswerer, NamespaceAnswerer { @@ -38,8 +58,16 @@ interface Scope extends ClassMemberAccessAnswerer, NamespaceAnswerer '_ENV', ]; + /** + * When analysing a trait, returns the file where the trait is used, + * not the trait file itself. Use getFileDescription() for the trait file path. + */ public function getFile(): string; + /** + * For traits, returns the trait file path with the using class context, + * e.g. "TraitFile.php (in context of class MyClass)". + */ public function getFileDescription(): string; public function isDeclareStrictTypes(): bool; @@ -49,6 +77,10 @@ public function isDeclareStrictTypes(): bool; */ public function isInTrait(): bool; + /** + * Returns the trait itself, not the class using the trait. + * Use getClassReflection() for the using class. + */ public function getTraitReflection(): ?ClassReflection; public function getFunction(): ?PhpFunctionFromParserNodeReflection; @@ -61,21 +93,27 @@ public function hasVariableType(string $variableName): TrinaryLogic; public function getVariableType(string $variableName): Type; - public function canAnyVariableExist(): bool; - /** - * @return array + * True at the top level of a file or after extract() — contexts where + * arbitrary variables may exist. */ + public function canAnyVariableExist(): bool; + + /** @return array */ public function getDefinedVariables(): array; /** + * Variables with TrinaryLogic::Maybe certainty — defined in some code paths but not others. + * * @return array */ public function getMaybeDefinedVariables(): array; public function hasConstant(Name $name): bool; - /** @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead */ + /** + * @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead + */ public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; public function getInstancePropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection; @@ -102,69 +140,200 @@ public function getAnonymousFunctionReflection(): ?ClosureType; public function getAnonymousFunctionReturnType(): ?Type; + /** + * Returns the PHPDoc-enhanced type. Use getNativeType() for native types only. + */ public function getType(Expr $node): Type; + /** + * Returns only what PHP's native type system knows, ignoring PHPDoc. + */ public function getNativeType(Expr $expr): Type; + /** + * Like getType(), but preserves void for function/method calls + * (normally getType() replaces void with null). + */ public function getKeepVoidType(Expr $node): Type; /** - * The `getType()` method along with FNSR enabled - * waits for the Expr analysis to be completed - * in order to evaluate the type at the right place in the code. - * - * This prevents tricky bugs when reasoning about code like - * `doFoo($a = 1, $a)`. - * - * Sometimes this is counter-productive because we actually want - * to use the current Scope object contents to resolve the Expr type. - * - * In these cases use `getScopeType()`. + * Unlike getType() which may defer evaluation, this uses the scope's + * current state immediately. */ public function getScopeType(Expr $expr): Type; public function getScopeNativeType(Expr $expr): Type; + /** + * Resolves a Name AST node to a fully qualified class name string. + * + * Handles special names: `self` and `static` resolve to the current class, + * `parent` resolves to the parent class. Other names are returned as-is + * (they should already be fully qualified by the PHP parser's name resolver). + * + * Inside a Closure::bind() context, `self`/`static` resolve to the bound class. + */ public function resolveName(Name $name): string; + /** + * Resolves a Name AST node to a TypeWithClassName. + * + * Unlike resolveName() which returns a plain string, this returns a proper + * Type object that preserves late-static-binding information: + * - `static` returns a StaticType (preserves LSB in subclasses) + * - `self` returns a ThisType when inside the same class hierarchy + * - Other names return an ObjectType + */ public function resolveTypeByName(Name $name): TypeWithClassName; /** + * Returns the PHPStan Type representing a given PHP value. + * + * Converts runtime PHP values to their corresponding constant types: + * integers become ConstantIntegerType, strings become ConstantStringType, + * arrays become ConstantArrayType (if small enough), etc. + * * @param mixed $value */ public function getTypeFromValue($value): Type; + /** + * Returns whether an expression has a tracked type in this scope. + * + * Returns TrinaryLogic::Yes if the expression's type is definitely known, + * TrinaryLogic::Maybe if it might be known, and TrinaryLogic::No if there + * is no type information for it. + * + * This checks the scope's expression type map without computing the type + * (unlike getType() which always computes a type). + */ public function hasExpressionType(Expr $node): TrinaryLogic; + /** + * Returns whether the given class name is being checked inside a + * class_exists(), interface_exists(), or trait_exists() call. + * + * When true, rules should suppress "class not found" errors because + * the code is explicitly checking for the class's existence. + */ public function isInClassExists(string $className): bool; + /** + * Returns whether the given function name is being checked inside a + * function_exists() call. + * + * When true, rules should suppress "function not found" errors because + * the code is explicitly checking for the function's existence. + */ public function isInFunctionExists(string $functionName): bool; + /** + * Returns whether the current analysis context is inside a Closure::bind() + * or Closure::bindTo() call. + * + * When true, the closure's $this and self/static may refer to a different + * class than the one where the closure was defined. + */ public function isInClosureBind(): bool; - /** @return list */ + /** + * Returns the stack of function/method calls that are currently being analysed. + * + * When analysing arguments of a function call, this returns the chain of + * enclosing calls. Used by extensions that need to know the calling context, + * such as type-specifying extensions for functions like class_exists(). + * + * @return list + */ public function getFunctionCallStack(): array; - /** @return list */ + /** + * Like getFunctionCallStack(), but also includes the parameter being passed to. + * + * Each entry is a tuple of the function/method reflection and the parameter + * reflection for the argument position being analysed (or null if unknown). + * + * @return list + */ public function getFunctionCallStackWithParameters(): array; + /** + * Returns whether a function parameter has a default value of null. + * + * Checks the parameter's default value AST node to determine if + * `= null` was specified. Used by function definition checks. + */ public function isParameterValueNullable(Param $parameter): bool; /** + * Resolves a type AST node (from a parameter/return type declaration) to a Type. + * + * Handles named types, identifier types (int, string, etc.), union types, + * intersection types, and nullable types. The $isNullable flag adds null + * to the type, and $isVariadic wraps the type in an array. + * * @param Node\Name|Node\Identifier|Node\ComplexType|null $type */ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type; + /** + * Returns whether the given expression is currently being assigned to. + * + * Returns true during the analysis of the right-hand side of an assignment + * to this expression. For example, when analysing `$a = expr`, this returns + * true for the $a variable during the analysis of `expr`. + * + * Used to prevent infinite recursion when resolving types during assignment. + */ public function isInExpressionAssign(Expr $expr): bool; + /** + * Returns whether accessing the given expression in an undefined state is allowed. + * + * Returns true when the expression is on the left-hand side of an assignment + * or in similar contexts where it's valid for the expression to be undefined + * (e.g. `$a['key'] = value` where $a['key'] doesn't need to exist yet). + */ public function isUndefinedExpressionAllowed(Expr $expr): bool; + /** + * Returns a new Scope with types narrowed by assuming the expression is truthy. + * + * Given an expression like `$x instanceof Foo`, returns a scope where + * $x is known to be of type Foo. This is the scope used inside the + * if-branch of `if ($x instanceof Foo)`. + * + * Uses the TypeSpecifier internally to determine type narrowing. + */ public function filterByTruthyValue(Expr $expr): self; + /** + * Returns a new Scope with types narrowed by assuming the expression is falsy. + * + * The opposite of filterByTruthyValue(). Given `$x instanceof Foo`, returns + * a scope where $x is known NOT to be of type Foo. This is the scope used + * in the else-branch of `if ($x instanceof Foo)`. + */ public function filterByFalseyValue(Expr $expr): self; + /** + * Returns whether the current statement is a "first-level" statement. + * + * A first-level statement is one that is directly inside a function/method + * body, not nested inside control structures like if/else, loops, or + * try/catch. Used to determine whether certain checks should be more + * or less strict. + */ public function isInFirstLevelStatement(): bool; + /** + * Returns the PHP version(s) being analysed against. + * + * Returns a PhpVersions object that can represent a range of PHP versions + * (when the exact version is not known). Use its methods like + * supportsEnums(), supportsReadonlyProperties(), etc. to check for + * version-specific features. + */ public function getPhpVersion(): PhpVersions; /** @internal */ diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 6a555dff73..8c8ef1041b 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -6,6 +6,11 @@ use function floor; /** + * Represents a specific PHP version for version-dependent analysis behavior. + * + * The version is stored as PHP_VERSION_ID format (e.g. 80100 for PHP 8.1.0). + * Extension developers can access it by injecting PhpVersion via constructor injection. + * * @api */ #[AutowiredService(factory: '@PHPStan\Php\PhpVersionFactory::create')] @@ -19,7 +24,6 @@ final class PhpVersion /** * @api - * * @param self::SOURCE_* $source */ public function __construct(private int $versionId, private int $source = self::SOURCE_UNKNOWN) diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 993ee23acf..a6849c0584 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -7,6 +7,14 @@ use PHPStan\Type\Type; /** + * Range-aware PHP version check that handles version uncertainty. + * + * Unlike PhpVersion (which represents a single known version), PhpVersions wraps + * a Type representing the possible PHP versions. When the exact version is known, + * queries return Yes/No. When a range of versions is possible, queries return Maybe. + * + * This is the return type of Scope::getPhpVersion(). + * * @api */ final class PhpVersions diff --git a/src/Reflection/Assertions.php b/src/Reflection/Assertions.php index a1f7ebfa6d..30f9106b77 100644 --- a/src/Reflection/Assertions.php +++ b/src/Reflection/Assertions.php @@ -11,6 +11,19 @@ use function count; /** + * Collection of @phpstan-assert annotations on a function or method. + * + * PHPStan supports type assertions via PHPDoc annotations: + * - `@phpstan-assert Type $param` — narrows the parameter type unconditionally + * - `@phpstan-assert-if-true Type $param` — narrows when the method returns true + * - `@phpstan-assert-if-false Type $param` — narrows when the method returns false + * + * This class collects all such assertions and provides methods to retrieve them + * by condition type. It also handles negation: an `@phpstan-assert-if-true` assertion + * is automatically negated and included in the `getAssertsIfFalse()` result. + * + * Returned by ExtendedMethodReflection::getAsserts() and FunctionReflection::getAsserts(). + * * @api */ final class Assertions @@ -25,15 +38,15 @@ private function __construct(private array $asserts) { } - /** - * @return AssertTag[] - */ + /** @return AssertTag[] */ public function getAll(): array { return $this->asserts; } /** + * Unconditional assertions — narrow parameter types regardless of the method's return value. + * * @return AssertTag[] */ public function getAsserts(): array @@ -42,6 +55,8 @@ public function getAsserts(): array } /** + * Includes @phpstan-assert-if-true tags and negated @phpstan-assert-if-false tags. + * * @return AssertTag[] */ public function getAssertsIfTrue(): array @@ -56,6 +71,8 @@ public function getAssertsIfTrue(): array } /** + * Includes @phpstan-assert-if-false tags and negated @phpstan-assert-if-true tags. + * * @return AssertTag[] */ public function getAssertsIfFalse(): array @@ -69,9 +86,7 @@ public function getAssertsIfFalse(): array ); } - /** - * @param callable(Type): Type $callable - */ + /** @param callable(Type): Type $callable */ public function mapTypes(callable $callable): self { $assertTagsCallback = static fn (AssertTag $tag): AssertTag => $tag->withType($callable($tag->getType())); diff --git a/src/Reflection/AttributeReflection.php b/src/Reflection/AttributeReflection.php index 74d6874223..34be236c67 100644 --- a/src/Reflection/AttributeReflection.php +++ b/src/Reflection/AttributeReflection.php @@ -5,13 +5,22 @@ use PHPStan\Type\Type; /** + * Reflection for a PHP attribute (PHP 8.0+). + * + * Represents a single attribute applied to a class, method, property, function, + * parameter, or constant. Provides the attribute's class name and the types of + * its constructor arguments. + * + * Returned by the getAttributes() method on ExtendedMethodReflection, + * ExtendedPropertyReflection, FunctionReflection, ClassConstantReflection, etc. + * * @api */ final class AttributeReflection { /** - * @param array $argumentTypes + * @param array $argumentTypes Argument types keyed by parameter name */ public function __construct(private string $name, private array $argumentTypes) { @@ -22,9 +31,7 @@ public function getName(): string return $this->name; } - /** - * @return array - */ + /** @return array */ public function getArgumentTypes(): array { return $this->argumentTypes; diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php index 11a22e40cf..45ca73857c 100644 --- a/src/Reflection/Callables/CallableParametersAcceptor.php +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -7,39 +7,52 @@ use PHPStan\TrinaryLogic; /** + * A ParametersAcceptor for callable types (closures, first-class callables). + * + * Extends ParametersAcceptor with information about side effects, exceptions, + * and other runtime behavior of callable values. This is what PHPStan knows + * about a closure or callable when it's passed as a parameter or stored in a variable. + * + * Implemented by ClosureType and used as the return type of + * Type::getCallableParametersAcceptors(). + * + * Provides: + * - Throw points (what exceptions the callable may throw) + * - Impure points (what side effects the callable may have) + * - Purity information + * - Variables captured from outer scope (used variables) + * - Expressions that are invalidated by calling this callable + * * @api */ interface CallableParametersAcceptor extends ParametersAcceptor { - /** - * @return SimpleThrowPoint[] - */ + /** @return SimpleThrowPoint[] */ public function getThrowPoints(): array; public function isPure(): TrinaryLogic; public function acceptsNamedArguments(): TrinaryLogic; - /** - * @return SimpleImpurePoint[] - */ + /** @return SimpleImpurePoint[] */ public function getImpurePoints(): array; /** + * Tracks when calling a closure invalidates cached type information + * for variables it captures by reference. + * * @return InvalidateExprNode[] */ public function getInvalidateExpressions(): array; - /** - * @return string[] - */ + /** @return string[] */ public function getUsedVariables(): array; /** - * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return - * value is unused at runtime a warning is emitted, PHPStan will emit the - * warning during analysis and on older PHP versions too + * Whether the callable is marked with the `#[\NoDiscard]` attribute. + * On PHP 8.5+ if the return value is unused at runtime, a warning is emitted. + * PHPStan reports this during analysis regardless of PHP version. */ public function mustUseReturnValue(): TrinaryLogic; diff --git a/src/Reflection/Callables/SimpleImpurePoint.php b/src/Reflection/Callables/SimpleImpurePoint.php index 8d6ed95b4c..6274d75ed1 100644 --- a/src/Reflection/Callables/SimpleImpurePoint.php +++ b/src/Reflection/Callables/SimpleImpurePoint.php @@ -12,6 +12,17 @@ use function sprintf; /** + * Represents a point where a callable may have side effects (impure behavior). + * + * Used by CallableParametersAcceptor::getImpurePoints() to describe what side effects + * a closure or callable value may have. Each impure point has an identifier (e.g. + * "functionCall", "methodCall"), a human-readable description, and a certainty flag. + * + * PHPStan uses impure points to: + * - Detect calls to impure functions inside @phpstan-pure contexts + * - Report unused return values of pure functions (expr.resultUnused) + * - Determine whether expressions have side effects + * * @phpstan-import-type ImpurePointIdentifier from ImpurePoint */ final class SimpleImpurePoint @@ -36,6 +47,8 @@ public function __construct( } /** + * Returns null if the function is known to be pure (no side effects). + * * @param Arg[] $args */ public static function createFromVariant(FunctionReflection|ExtendedMethodReflection $function, ?ParametersAcceptor $variant, ?Scope $scope = null, array $args = []): ?self @@ -103,9 +116,7 @@ public static function createFromVariant(FunctionReflection|ExtendedMethodReflec return null; } - /** - * @return ImpurePointIdentifier - */ + /** @return ImpurePointIdentifier */ public function getIdentifier(): string { return $this->identifier; diff --git a/src/Reflection/Callables/SimpleThrowPoint.php b/src/Reflection/Callables/SimpleThrowPoint.php index 5cde43a155..3e7c510f81 100644 --- a/src/Reflection/Callables/SimpleThrowPoint.php +++ b/src/Reflection/Callables/SimpleThrowPoint.php @@ -6,6 +6,17 @@ use PHPStan\Type\Type; use Throwable; +/** + * Represents a point where a callable may throw an exception. + * + * Used by CallableParametersAcceptor::getThrowPoints() to describe what exceptions + * a closure or callable value may throw. This is a simplified version of the full + * ThrowPoint used in the analyser — it carries just the exception type, whether the + * throw was explicitly declared (@throws), and whether it could be any Throwable. + * + * Explicit throw points come from @throws annotations. Implicit throw points represent + * the possibility that any function call could throw. + */ final class SimpleThrowPoint { diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index dc30920809..9c5b9e18ff 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -6,7 +6,20 @@ use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Type\Type; -/** @api */ +/** + * Reflection for a class constant. + * + * Combines ClassMemberReflection (declaring class, visibility) with + * ConstantReflection (name, value type, deprecation) and adds class-constant-specific + * features: the value expression AST, final modifier, and separate PHPDoc/native types. + * + * PHP 8.3+ supports native type declarations on class constants, so this interface + * provides both PHPDoc and native type accessors (similar to property reflection). + * + * This is the return type of Type::getConstant() and Scope::getConstantReflection(). + * + * @api + */ interface ClassConstantReflection extends ClassMemberReflection, ConstantReflection { diff --git a/src/Reflection/ClassMemberAccessAnswerer.php b/src/Reflection/ClassMemberAccessAnswerer.php index 9eeb979821..5d19503e8e 100644 --- a/src/Reflection/ClassMemberAccessAnswerer.php +++ b/src/Reflection/ClassMemberAccessAnswerer.php @@ -2,7 +2,20 @@ namespace PHPStan\Reflection; -/** @api */ +/** + * Answers questions about visibility and access rights for class members + * (properties, methods, constants) from the current analysis context. + * + * This interface is the Scope's role as an access control checker. It is + * passed as a parameter to Type methods like getMethod(), getProperty(), + * getConstant(), etc., so the type system can enforce visibility rules + * (public/protected/private) based on where the access occurs. + * + * The primary implementation is MutatingScope. A secondary implementation, + * OutOfClassScope, is used when accessing members from outside any class. + * + * @api + */ interface ClassMemberAccessAnswerer { diff --git a/src/Reflection/ClassMemberReflection.php b/src/Reflection/ClassMemberReflection.php index da2274a063..eefe052541 100644 --- a/src/Reflection/ClassMemberReflection.php +++ b/src/Reflection/ClassMemberReflection.php @@ -2,10 +2,25 @@ namespace PHPStan\Reflection; -/** @api */ +/** + * Base interface for all class members: properties, methods, and constants. + * + * Provides common metadata shared by all class members — their declaring class, + * visibility (public/private/protected), static-ness, and raw PHPDoc comment. + * + * This is the parent interface for PropertyReflection, MethodReflection, and + * (via ConstantReflection) ClassConstantReflection. Extension developers typically + * work with the more specific child interfaces. + * + * @api + */ interface ClassMemberReflection { + /** + * For inherited members, this returns the original declaring class, + * not the class where the member was accessed. + */ public function getDeclaringClass(): ClassReflection; public function isStatic(): bool; diff --git a/src/Reflection/ConstantReflection.php b/src/Reflection/ConstantReflection.php index 9679c72542..c8dcca1a2b 100644 --- a/src/Reflection/ConstantReflection.php +++ b/src/Reflection/ConstantReflection.php @@ -5,7 +5,15 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -/** @api */ +/** + * Reflection for a constant (class constant or global constant). + * + * Provides the constant's name, resolved value type, deprecation status, and + * metadata. This is the base interface — ClassConstantReflection extends it + * with class-specific features (declaring class, value expression, native type). + * + * @api + */ interface ConstantReflection { @@ -21,9 +29,7 @@ public function isInternal(): TrinaryLogic; public function getFileName(): ?string; - /** - * @return list - */ + /** @return list */ public function getAttributes(): array; } diff --git a/src/Reflection/ExtendedMethodReflection.php b/src/Reflection/ExtendedMethodReflection.php index a13cd47c1f..b959ae79d5 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -7,33 +7,39 @@ use PHPStan\Type\Type; /** - * The purpose of this interface is to be able to - * answer more questions about methods - * without breaking backward compatibility - * with existing MethodsClassReflectionExtension. + * Extended method reflection with additional metadata beyond MethodReflection. * - * Developers are meant to only implement MethodReflection - * and its methods in their code. + * This interface exists to allow PHPStan to add new method query methods in minor + * versions without breaking existing MethodsClassReflectionExtension implementations. + * Extension developers should implement MethodReflection, not this interface — PHPStan + * wraps MethodReflection implementations to provide ExtendedMethodReflection. * - * New methods on ExtendedMethodReflection will be added - * in minor versions. + * Provides access to: + * - Extended parameter signatures (ExtendedParametersAcceptor with PHPDoc/native types) + * - Named argument variants (different signatures when using named arguments) + * - Type assertions (@phpstan-assert annotations) + * - Self-out types (@phpstan-self-out for fluent interfaces) + * - Purity information (@phpstan-pure/@phpstan-impure) + * - PHP attributes (including #[\NoDiscard]) + * - Resolved PHPDoc block + * + * This is the return type of Type::getMethod() and Scope::getMethodReflection(). * * @api */ interface ExtendedMethodReflection extends MethodReflection { - /** - * @return list - */ + /** @return list */ public function getVariants(): array; - /** - * @internal - */ + /** @internal */ public function getOnlyVariant(): ExtendedParametersAcceptor; /** + * Returns alternative signatures used when the method is called with named arguments. + * Returns null if the named argument variants are the same as regular variants. + * * @return list|null */ public function getNamedArgumentsVariants(): ?array; @@ -42,6 +48,10 @@ public function acceptsNamedArguments(): TrinaryLogic; public function getAsserts(): Assertions; + /** + * Used for fluent interfaces where calling a method changes the generic + * type parameters of $this (e.g. a builder pattern). + */ public function getSelfOutType(): ?Type; public function returnsByReference(): TrinaryLogic; @@ -53,23 +63,17 @@ public function isAbstract(): TrinaryLogic|bool; public function isBuiltin(): TrinaryLogic|bool; /** - * This indicates whether the method has phpstan-pure - * or phpstan-impure annotation above it. - * - * In most cases asking hasSideEffects() is much more practical - * as it also accounts for void return type (method being always impure). + * In most cases hasSideEffects() is more practical as it also accounts + * for void return type (methods returning void are always impure). */ public function isPure(): TrinaryLogic; - /** - * @return list - */ + /** @return list */ public function getAttributes(): array; /** - * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return - * value is unused at runtime a warning is emitted, PHPStan will emit the - * warning during analysis and on older PHP versions too + * On PHP 8.5+ if the return value is unused at runtime, a warning is emitted. + * PHPStan reports this during analysis regardless of PHP version. */ public function mustUseReturnValue(): TrinaryLogic; diff --git a/src/Reflection/ExtendedParametersAcceptor.php b/src/Reflection/ExtendedParametersAcceptor.php index 77fb213b49..43ebd06461 100644 --- a/src/Reflection/ExtendedParametersAcceptor.php +++ b/src/Reflection/ExtendedParametersAcceptor.php @@ -5,13 +5,23 @@ use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; -/** @api */ +/** + * Extended function/method signature with separate PHPDoc and native types. + * + * Extends ParametersAcceptor with: + * - Extended parameter reflections (separate PHPDoc/native types per parameter) + * - Separate PHPDoc and native return types (vs the combined return type from ParametersAcceptor) + * - Call-site variance map for template type parameters + * + * This is the return type of FunctionReflection::getVariants() and + * ExtendedMethodReflection::getVariants(). + * + * @api + */ interface ExtendedParametersAcceptor extends ParametersAcceptor { - /** - * @return list - */ + /** @return list */ public function getParameters(): array; public function getPhpDocReturnType(): Type; diff --git a/src/Reflection/ExtendedPropertyReflection.php b/src/Reflection/ExtendedPropertyReflection.php index 16d0f895ea..b8249987d2 100644 --- a/src/Reflection/ExtendedPropertyReflection.php +++ b/src/Reflection/ExtendedPropertyReflection.php @@ -6,16 +6,22 @@ use PHPStan\Type\Type; /** - * The purpose of this interface is to be able to - * answer more questions about properties - * without breaking backward compatibility - * with existing PropertiesClassReflectionExtension. + * Extended property reflection with additional metadata beyond PropertyReflection. * - * Developers are meant to only implement PropertyReflection - * and its methods in their code. + * This interface exists to allow PHPStan to add new property query methods in minor + * versions without breaking existing PropertiesClassReflectionExtension implementations. + * Extension developers should implement PropertyReflection, not this interface — PHPStan + * wraps PropertyReflection implementations to provide ExtendedPropertyReflection. * - * New methods on ExtendedPropertyReflection will be added - * in minor versions. + * Provides access to: + * - Separate PHPDoc type vs native type (for resolving the effective type) + * - Property hooks (PHP 8.4+) — get/set hooks with their own method reflections + * - Asymmetric visibility (PHP 8.4+) — different read/write visibility + * - Abstract/final/virtual modifiers + * - PHP attributes + * + * This is the return type of Type::getProperty(), Type::getInstanceProperty(), + * and Type::getStaticProperty(). * * @api */ @@ -42,14 +48,18 @@ public function isFinalByKeyword(): TrinaryLogic; public function isFinal(): TrinaryLogic; - public function isVirtual(): TrinaryLogic; - /** - * @param self::HOOK_* $hookType + * Virtual properties (PHP 8.4+) exist only through their get/set hooks + * and don't occupy memory in the object. */ + public function isVirtual(): TrinaryLogic; + + /** @param self::HOOK_* $hookType */ public function hasHook(string $hookType): bool; /** + * Property hooks (PHP 8.4+) are internally represented as methods. + * * @param self::HOOK_* $hookType */ public function getHook(string $hookType): ExtendedMethodReflection; @@ -58,16 +68,13 @@ public function isProtectedSet(): bool; public function isPrivateSet(): bool; - /** - * @return list - */ + /** @return list */ public function getAttributes(): array; /** - * If property has been declared in code then this returns `no()` - * - * Returns `yes()` if the property represents possibly-defined property - * in non-final classes, on mixed, on object etc. + * Returns yes() for properties that represent possibly-defined properties + * on non-final classes, mixed, object, etc. — placeholders PHPStan creates + * when it cannot prove a property doesn't exist. */ public function isDummy(): TrinaryLogic; diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index b99209c628..a45c802d3f 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -5,7 +5,21 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -/** @api */ +/** + * Reflection for a standalone function (not a class method). + * + * Represents both built-in PHP functions and user-defined functions. Like methods, + * functions can have multiple "variants" (overloaded signatures) — particularly + * common for built-in functions where the return type depends on argument types. + * + * Extension developers encounter this interface when implementing + * DynamicFunctionReturnTypeExtension or FunctionTypeSpecifyingExtension. + * + * Functions referenced in Scope::getFunctionCallStack() may be either + * FunctionReflection or MethodReflection. + * + * @api + */ interface FunctionReflection { @@ -13,17 +27,16 @@ public function getName(): string; public function getFileName(): ?string; - /** - * @return list - */ + /** @return list */ public function getVariants(): array; - /** - * @internal - */ + /** @internal */ public function getOnlyVariant(): ExtendedParametersAcceptor; /** + * Returns alternative signatures used when the function is called with named arguments. + * Returns null if the named argument variants are the same as regular variants. + * * @return list|null */ public function getNamedArgumentsVariants(): ?array; @@ -49,23 +62,17 @@ public function getDocComment(): ?string; public function returnsByReference(): TrinaryLogic; /** - * This indicates whether the function has phpstan-pure - * or phpstan-impure annotation above it. - * - * In most cases asking hasSideEffects() is much more practical - * as it also accounts for void return type (method being always impure). + * In most cases hasSideEffects() is more practical as it also accounts + * for void return type (functions returning void are always impure). */ public function isPure(): TrinaryLogic; - /** - * @return list - */ + /** @return list */ public function getAttributes(): array; /** - * Has the #[\NoDiscard] attribute - on PHP 8.5+ if the function's return - * value is unused at runtime a warning is emitted, PHPStan will emit the - * warning during analysis and on older PHP versions too + * On PHP 8.5+ if the return value is unused at runtime, a warning is emitted. + * PHPStan reports this during analysis regardless of PHP version. */ public function mustUseReturnValue(): TrinaryLogic; diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index 529a5011dd..524fa98bfc 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -5,15 +5,36 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -/** @api */ +/** + * Reflection for a class method. + * + * This is the interface extension developers should implement when creating custom + * MethodsClassReflectionExtension implementations for magic methods (__call, etc.). + * + * Methods can have multiple "variants" (overloaded signatures) — for example, + * built-in functions like `array_map` have different signatures depending on + * the number of arguments. Each variant is a ParametersAcceptor. + * + * For additional method metadata (assertions, purity, named arguments, attributes), + * see ExtendedMethodReflection which extends this interface. + * + * @api + */ interface MethodReflection extends ClassMemberReflection { public function getName(): string; + /** + * For methods that override a parent method, this returns the parent's + * method reflection. For methods with no parent, returns itself. + */ public function getPrototype(): ClassMemberReflection; /** + * Most methods have a single variant. Built-in PHP functions with overloaded + * signatures (e.g. different return types based on argument count) have multiple. + * * @return list */ public function getVariants(): array; @@ -28,6 +49,10 @@ public function isInternal(): TrinaryLogic; public function getThrowType(): ?Type; + /** + * Void methods are always considered impure since they must do something + * to be useful. + */ public function hasSideEffects(): TrinaryLogic; } diff --git a/src/Reflection/NamespaceAnswerer.php b/src/Reflection/NamespaceAnswerer.php index 4e908a6d8e..2a3ac2d40a 100644 --- a/src/Reflection/NamespaceAnswerer.php +++ b/src/Reflection/NamespaceAnswerer.php @@ -2,13 +2,18 @@ namespace PHPStan\Reflection; -/** @api */ +/** + * Provides the current namespace context. + * + * Used by the type resolver and PHPDoc parser to resolve relative class names + * against the current namespace and use statements. + * + * @api + */ interface NamespaceAnswerer { - /** - * @return non-empty-string|null - */ + /** @return non-empty-string|null */ public function getNamespace(): ?string; } diff --git a/src/Reflection/ParameterReflection.php b/src/Reflection/ParameterReflection.php index 8efbfa7c06..bebd231c94 100644 --- a/src/Reflection/ParameterReflection.php +++ b/src/Reflection/ParameterReflection.php @@ -4,7 +4,19 @@ use PHPStan\Type\Type; -/** @api */ +/** + * Reflection for a function/method parameter. + * + * Represents a single parameter in a function or method signature. Each parameter + * has a name, type, and metadata about optionality, variadicity, and pass-by-reference. + * + * The type returned by getType() is the combined PHPDoc + native type. + * For separate PHPDoc and native types, see ExtendedParameterReflection. + * + * Part of a ParametersAcceptor which describes a complete function signature. + * + * @api + */ interface ParameterReflection { diff --git a/src/Reflection/ParametersAcceptor.php b/src/Reflection/ParametersAcceptor.php index b5fa5f1a2d..53580963c4 100644 --- a/src/Reflection/ParametersAcceptor.php +++ b/src/Reflection/ParametersAcceptor.php @@ -5,7 +5,22 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; -/** @api */ +/** + * Describes one signature variant of a function or method. + * + * A function/method may have multiple ParametersAcceptor variants — for example, + * the built-in `strtok` function has different signatures depending on argument count. + * Each variant describes the template type parameters, positional parameters, variadicity, + * and return type. + * + * This is the base interface. ExtendedParametersAcceptor adds separate PHPDoc/native + * return types and extended parameter reflection. CallableParametersAcceptor adds + * throw points, impure points, and purity information. + * + * Use ParametersAcceptorSelector to choose the best variant for a given call site. + * + * @api + */ interface ParametersAcceptor { @@ -17,11 +32,13 @@ interface ParametersAcceptor public function getTemplateTypeMap(): TemplateTypeMap; - public function getResolvedTemplateTypeMap(): TemplateTypeMap; - /** - * @return list + * After template type inference at a call site, this map contains the + * concrete types inferred for each template parameter. */ + public function getResolvedTemplateTypeMap(): TemplateTypeMap; + + /** @return list */ public function getParameters(): array; public function isVariadic(): bool; diff --git a/src/Reflection/PassedByReference.php b/src/Reflection/PassedByReference.php index 804d049b43..6e31c54f1f 100644 --- a/src/Reflection/PassedByReference.php +++ b/src/Reflection/PassedByReference.php @@ -5,6 +5,22 @@ use function array_key_exists; /** + * Describes how a function/method parameter is passed: by value or by reference. + * + * Three modes: + * - **No**: Passed by value — the argument expression is evaluated and its value is copied. + * - **ReadsArgument**: Passed by reference, but the function reads the existing variable. + * The variable must already exist. Example: `sort(&$array)`. + * - **CreatesNewVariable**: Passed by reference, and the function may create the variable + * if it doesn't exist. Example: `preg_match($pattern, $subject, &$matches)` where + * `$matches` doesn't need to be defined beforehand. + * + * This distinction matters for PHPStan's scope analysis — when a function takes a + * parameter by reference with "creates new variable" semantics, PHPStan knows the + * variable will exist after the call even if it wasn't defined before. + * + * Used as the return type of ParameterReflection::passedByReference(). + * * @api */ final class PassedByReference @@ -65,6 +81,7 @@ public function createsNewVariable(): bool return $this->value === self::CREATES_NEW_VARIABLE; } + /** CreatesNewVariable > ReadsArgument > No. */ public function combine(self $other): self { if ($this->value > $other->value) { diff --git a/src/Reflection/PropertyReflection.php b/src/Reflection/PropertyReflection.php index cc22cc5610..a301e42225 100644 --- a/src/Reflection/PropertyReflection.php +++ b/src/Reflection/PropertyReflection.php @@ -5,14 +5,39 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -/** @api */ +/** + * Reflection for a class property. + * + * This is the interface extension developers should implement when creating + * custom PropertiesClassReflectionExtension implementations for magic properties. + * + * Properties have separate readable and writable types to support: + * - Asymmetric types (PHP 8.4+ property hooks with different get/set types) + * - Read-only properties (readable but not writable) + * - Write-only properties (writable but not readable, rare) + * + * For additional property metadata (native types, PHPDoc types, hooks, attributes), + * see ExtendedPropertyReflection which extends this interface. + * + * @api + */ interface PropertyReflection extends ClassMemberReflection { public function getReadableType(): Type; + /** + * May differ from the readable type for properties with asymmetric visibility + * or property hooks with different get/set types. + */ public function getWritableType(): Type; + /** + * Returns true when the readable and writable types are the same and no property hooks + * transform the value — PHPStan can then narrow the property's type based on assignments. + * Returns false when read and write types differ (e.g. `@property` with asymmetric types, + * property hooks, virtual properties). + */ public function canChangeTypeAfterAssignment(): bool; public function isReadable(): bool; diff --git a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php index 4665670cb4..e2d3f01801 100644 --- a/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnresolvedMethodPrototypeReflection.php @@ -5,6 +5,26 @@ use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Type\Type; +/** + * Lazy method reflection that defers template type and static type resolution. + * + * When calling a method on a generic type, the method's parameter and return types + * need to be transformed by substituting template type parameters with their concrete + * arguments. This interface allows that resolution to be deferred and configured: + * + * - getNakedMethod() returns the method as declared (before template substitution) + * - getTransformedMethod() returns the method with templates resolved + * - doNotResolveTemplateTypeMapToBounds() prevents falling back to template bounds + * when concrete types are unknown (used during type inference) + * - withCalledOnType() sets the type the method is being called on + * + * This exists primarily because of StaticType. ObjectType uses + * CalledOnTypeUnresolvedMethodPrototypeReflection which has hardcoded logic + * to transform static types. StaticType uses CallbackUnresolvedMethodPrototypeReflection + * which accepts a custom callback for context-aware static type transformation. + * + * This is the return type of Type::getUnresolvedMethodPrototype(). + */ interface UnresolvedMethodPrototypeReflection { @@ -12,6 +32,10 @@ public function doNotResolveTemplateTypeMapToBounds(): self; public function getNakedMethod(): ExtendedMethodReflection; + /** + * Returns the method reflection with template types substituted from the + * called-on type's generic arguments. + */ public function getTransformedMethod(): ExtendedMethodReflection; public function withCalledOnType(Type $type): self; diff --git a/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php index 441d4a36c3..c0206bce65 100644 --- a/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/UnresolvedPropertyPrototypeReflection.php @@ -5,6 +5,28 @@ use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Type\Type; +/** + * Lazy property reflection that defers template type and static type resolution. + * + * When accessing a property on a generic type, the property's types need to be + * transformed by substituting template type parameters with their concrete arguments. + * This interface allows that resolution to be deferred and configured: + * + * - getNakedProperty() returns the property as declared (before template substitution) + * - getTransformedProperty() returns the property with templates resolved + * - doNotResolveTemplateTypeMapToBounds() prevents falling back to template bounds + * when concrete types are unknown (used during type inference) + * - withFechedOnType() sets the type the property is being accessed on + * + * This exists primarily because of StaticType. ObjectType uses + * CalledOnTypeUnresolvedPropertyPrototypeReflection which has hardcoded logic + * to transform static types. StaticType uses CallbackUnresolvedPropertyPrototypeReflection + * which accepts a custom callback for context-aware static type transformation. + * + * This is the return type of Type::getUnresolvedPropertyPrototype(), + * Type::getUnresolvedInstancePropertyPrototype(), and + * Type::getUnresolvedStaticPropertyPrototype(). + */ interface UnresolvedPropertyPrototypeReflection { @@ -12,6 +34,10 @@ public function doNotResolveTemplateTypeMapToBounds(): self; public function getNakedProperty(): ExtendedPropertyReflection; + /** + * Returns the property reflection with template types substituted from the + * fetched-on type's generic arguments. + */ public function getTransformedProperty(): ExtendedPropertyReflection; public function withFechedOnType(Type $type): self; diff --git a/src/TrinaryLogic.php b/src/TrinaryLogic.php index fe08c0aa69..e628893e96 100644 --- a/src/TrinaryLogic.php +++ b/src/TrinaryLogic.php @@ -9,6 +9,30 @@ use function min; /** + * Three-valued logic used throughout PHPStan's type system. + * + * Unlike boolean logic, TrinaryLogic has three states: Yes, No, and Maybe. + * This is essential for static analysis because type relationships aren't always + * certain. For example, a `mixed` type *might* be a string — that's `Maybe`. + * + * Many Type methods return TrinaryLogic instead of bool because the answer may + * depend on runtime values that can't be known statically. Extension developers + * encounter TrinaryLogic extensively when querying type properties: + * + * if ($type->isString()->yes()) { + * // Definitely a string + * } + * if ($type->isString()->maybe()) { + * // Could be a string (e.g. mixed) + * } + * if ($type->isString()->no()) { + * // Definitely not a string + * } + * + * TrinaryLogic supports logical operations (and, or, negate) that propagate + * uncertainty correctly. It is used as a flyweight — instances are cached and + * compared by identity. + * * @api * @see https://phpstan.org/developing-extensions/trinary-logic */ @@ -169,6 +193,9 @@ public function lazyOr( return $this->or(...$results); } + /** + * Returns the operands' value if they all agree, Maybe if any differ. + */ public static function extremeIdentity(self ...$operands): self { if ($operands === []) { @@ -211,6 +238,9 @@ public static function lazyExtremeIdentity( return $lastResult; } + /** + * Returns Yes if any operand is Yes, otherwise the minimum. + */ public static function maxMin(self ...$operands): self { if ($operands === []) { @@ -253,6 +283,9 @@ public function equals(self $other): bool return $this === $other; } + /** + * Returns the stronger of the two values, or null if they are equal (Yes > Maybe > No). + */ public function compareTo(self $other): ?self { if ($this->value > $other->value) { diff --git a/src/Type/AcceptsResult.php b/src/Type/AcceptsResult.php index 36a3b598cf..3696e43717 100644 --- a/src/Type/AcceptsResult.php +++ b/src/Type/AcceptsResult.php @@ -10,6 +10,20 @@ use function array_values; /** + * Result of a Type::accepts() check — whether one type accepts another. + * + * Wraps a TrinaryLogic result together with human-readable reasons explaining + * why the acceptance failed. These reasons are surfaced in PHPStan error messages + * to help developers understand type mismatches. + * + * For example, when checking if `int` accepts `string`, the result would be No + * with a reason like "string is not a subtype of int". + * + * The `accepts()` method is used to check assignability — whether a value of one + * type can be assigned to a variable/parameter of another type. This is stricter + * than `isSuperTypeOf()` because it accounts for PHPStan's rule level and + * generics variance. + * * @api */ final class AcceptsResult @@ -17,7 +31,7 @@ final class AcceptsResult /** * @api - * @param list $reasons + * @param list $reasons Human-readable explanations of why acceptance failed */ public function __construct( public readonly TrinaryLogic $result, @@ -58,9 +72,7 @@ public static function createYes(): self return new self(TrinaryLogic::createYes(), []); } - /** - * @param list $reasons - */ + /** @param list $reasons */ public static function createNo(array $reasons = []): self { return new self(TrinaryLogic::createNo(), $reasons); @@ -92,9 +104,7 @@ public function or(self $other): self ); } - /** - * @param callable(string): string $cb - */ + /** @param callable(string): string $cb */ public function decorateReasons(callable $cb): self { $reasons = []; @@ -105,6 +115,7 @@ public function decorateReasons(callable $cb): self return new self($this->result, $reasons); } + /** @see TrinaryLogic::extremeIdentity() */ public static function extremeIdentity(self ...$operands): self { if ($operands === []) { @@ -122,6 +133,7 @@ public static function extremeIdentity(self ...$operands): self return new self($result, array_values(array_unique($reasons))); } + /** @see TrinaryLogic::maxMin() */ public static function maxMin(self ...$operands): self { if ($operands === []) { diff --git a/src/Type/Accessory/AccessoryType.php b/src/Type/Accessory/AccessoryType.php index 734ee43759..3b0992e0e9 100644 --- a/src/Type/Accessory/AccessoryType.php +++ b/src/Type/Accessory/AccessoryType.php @@ -4,6 +4,54 @@ use PHPStan\Type\Type; +/** + * Marker interface for types that refine a base type with additional constraints. + * + * An AccessoryType never stands alone — it always exists inside an `IntersectionType` + * alongside a base type (like `StringType` or `ArrayType`). The base type provides + * the fundamental type identity, and the AccessoryType adds a narrowing guarantee. + * + * For example, `non-empty-string` is represented as: + * + * IntersectionType([StringType, AccessoryNonEmptyStringType]) + * + * And `non-empty-list` is represented as: + * + * IntersectionType([ArrayType(int, int), NonEmptyArrayType, AccessoryArrayListType]) + * + * Each AccessoryType implementation has a corresponding query method on the `Type` interface + * that returns `TrinaryLogic`. This lets any code query the refinement without knowing + * how it is represented internally: + * + * | AccessoryType class | Corresponding Type method | + * |----------------------------------|---------------------------------| + * | AccessoryNonEmptyStringType | `isNonEmptyString()` | + * | AccessoryNonFalsyStringType | `isNonFalsyString()` | + * | AccessoryLiteralStringType | `isLiteralString()` | + * | AccessoryNumericStringType | `isNumericString()` | + * | AccessoryLowercaseStringType | `isLowercaseString()` | + * | AccessoryUppercaseStringType | `isUppercaseString()` | + * | AccessoryArrayListType | `isList()` | + * | NonEmptyArrayType | `isIterableAtLeastOnce()` | + * | OversizedArrayType | `isOversizedArray()` | + * | HasMethodType | `hasMethod(string)` | + * | HasPropertyType | `hasProperty(string)` | + * | HasOffsetType | `hasOffsetValueType(Type)` | + * | HasOffsetValueType | `getOffsetValueType(Type)` | + * + * All implementations also implement `CompoundType`, so they participate in the + * double-dispatch protocol for type comparison — simple types delegate to + * `$type->isAcceptedBy()`/`$type->isSubTypeOf()` when they encounter an AccessoryType. + * + * The `instanceof AccessoryType` check is used in a few specific places: + * - `IntersectionType::describe()` — skips AccessoryTypes when building base type names + * (they are rendered as type-level qualifiers like `non-empty-` instead) + * - `IntersectionType::describeItself()` — separates base types from accessory types + * when composing the human-readable type description + * - `UnionTypeHelper::sortTypes()` — sorts AccessoryTypes after base types + * - `TypeCombinator` — handles AccessoryType intersection/union normalization + * - `MissingTypehintCheck` — skips AccessoryTypes in typehint analysis + */ interface AccessoryType extends Type { diff --git a/src/Type/CompoundType.php b/src/Type/CompoundType.php index f66e10c091..8f3f0f97cc 100644 --- a/src/Type/CompoundType.php +++ b/src/Type/CompoundType.php @@ -5,16 +5,82 @@ use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; -/** @api */ +/** + * Marker interface for types that require bidirectional type comparison. + * + * Simple types like `StringType` or `IntegerType` can answer `isSuperTypeOf()` + * and `accepts()` on their own — they check whether the incoming type fits. + * But compound types (unions, intersections, mixed, never, accessory types, + * integer ranges, callables, iterables, conditionals, etc.) need to be asked + * from the other direction, because they carry internal structure that the + * simple type on the other side knows nothing about. + * + * The protocol works like a double dispatch: + * + * 1. A simple type's `accepts()`/`isSuperTypeOf()` receives an argument. + * 2. It checks `if ($type instanceof CompoundType)`. + * 3. If true, it delegates to `$type->isAcceptedBy($this, …)` or `$type->isSubTypeOf($this)`. + * 4. The compound type then decomposes itself (e.g., iterates union members) + * and calls back to the simple type for each component. + * + * This avoids the simple type having to understand union/intersection/mixed/never + * semantics. For example, `StringType::accepts()` doesn't need to know how to + * check a `UnionType` — it just delegates to `UnionType::isAcceptedBy()`, + * which iterates its members and asks `StringType::accepts()` for each one. + * + * Unlike `instanceof SomeSpecificType` checks (which are discouraged in CLAUDE.md), + * `instanceof CompoundType` is the correct and intended pattern throughout the + * type system. It is part of the double-dispatch protocol, not a type query. + * + * Implementations include: + * - `UnionType` — `isSubTypeOf()` requires ALL members to be subtypes, `isAcceptedBy()` requires ALL to be accepted + * - `IntersectionType` — `isSubTypeOf()` requires at least ONE member to be a subtype (via `maxMin`) + * - `MixedType`, `NeverType` — terminal cases (mixed accepts everything, never is subtype of everything) + * - All `AccessoryType` implementations — refinement types that live inside intersections + * - `IntegerRangeType`, `CallableType`, `IterableType` — types with internal structure + * - `ConditionalType`, `KeyOfType`, `ValueOfType`, etc. — late-resolvable types + * + * @api + */ interface CompoundType extends Type { + /** + * Answers "is this compound type accepted by $acceptingType?" from the compound type's perspective. + * + * Called by simple types when they encounter a CompoundType argument in their `accepts()` method. + * The compound type decomposes itself and calls `$acceptingType->accepts()` for each component. + * + * For example, `UnionType(string|int)::isAcceptedBy(StringType)` asks StringType to accept + * `string` and `int` separately, then combines results with `extremeIdentity` (all must pass). + */ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult; + /** + * Answers "is this compound type a subtype of $otherType?" from the compound type's perspective. + * + * Called by simple types when they encounter a CompoundType argument in their `isSuperTypeOf()` method. + * The compound type decomposes itself and calls `$otherType->isSuperTypeOf()` for each component. + * + * For example, `UnionType(string|int)::isSubTypeOf(MixedType)` asks MixedType whether it is + * a supertype of `string` and `int` separately, then combines with `extremeIdentity` (all must pass). + */ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult; + /** + * Compares this compound type against $otherType using greater-than semantics. + * + * Used for comparison operators (`>`). Each compound type decomposes the comparison + * across its members (e.g., IntegerRangeType checks whether all values in the range + * are greater than the other type). + */ public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; + /** + * Compares this compound type against $otherType using greater-than-or-equal semantics. + * + * Used for comparison operators (`>=`). Same decomposition strategy as `isGreaterThan()`. + */ public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; } diff --git a/src/Type/ConstantScalarType.php b/src/Type/ConstantScalarType.php index b84b381717..bbd2c22e9e 100644 --- a/src/Type/ConstantScalarType.php +++ b/src/Type/ConstantScalarType.php @@ -2,13 +2,21 @@ namespace PHPStan\Type; -/** @api */ +/** + * A type whose value is known at analysis time — a compile-time constant scalar. + * + * Implemented by ConstantIntegerType, ConstantFloatType, ConstantStringType, + * ConstantBooleanType, and NullType. + * + * Use Type::isConstantValue() to check if a type is constant without instanceof, + * and Type::getConstantScalarTypes() to extract constant types from unions. + * + * @api + */ interface ConstantScalarType extends Type { - /** - * @return int|float|string|bool|null - */ + /** @return int|float|string|bool|null */ public function getValue(); } diff --git a/src/Type/GeneralizePrecision.php b/src/Type/GeneralizePrecision.php index 3f4d3be629..2b4595470b 100644 --- a/src/Type/GeneralizePrecision.php +++ b/src/Type/GeneralizePrecision.php @@ -2,6 +2,26 @@ namespace PHPStan\Type; +/** + * Controls how aggressively Type::generalize() widens a type. + * + * Generalization is the process of widening a specific type to a broader one. + * For example, generalizing ConstantStringType('hello') yields StringType. + * This is used when PHPStan needs to merge types across loop iterations or + * branches where tracking precise constant values is impractical. + * + * Three levels of precision: + * - **lessSpecific**: Aggressive generalization — constant values become their + * general type (e.g. 'hello' → string, array{foo: int} → array) + * - **moreSpecific**: Preserves more detail — e.g. non-empty-string stays + * non-empty-string instead of widening to string + * - **templateArgument**: Used when generalizing template type arguments, + * preserving template-specific structure + * + * Used as a parameter to Type::generalize(): + * + * $type->generalize(GeneralizePrecision::lessSpecific()) + */ final class GeneralizePrecision { diff --git a/src/Type/Generic/TemplateTypeMap.php b/src/Type/Generic/TemplateTypeMap.php index 49f3a08496..356772085a 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -11,6 +11,26 @@ use function count; /** + * Maps template type parameter names to their resolved types. + * + * This is the core data structure for PHPStan's generics support. When a class declares + * `@template T`, `@template U of object`, etc., the TemplateTypeMap tracks what concrete + * types T and U resolve to in a particular context. + * + * Two kinds of type bindings are tracked: + * - **types** (upper bounds): The concrete type inferred or declared for each template. + * For `@template T of Countable`, if T is inferred as `array`, types maps T → array. + * - **lowerBoundTypes**: Types inferred from contravariant positions (e.g. parameter types). + * Used during type inference to narrow template types from below. + * + * TemplateTypeMap supports set operations (union, intersect, benevolentUnion) that combine + * maps from different code paths, and resolveToBounds() which replaces unresolved template + * types with their declared bounds. + * + * Common usage: ParametersAcceptor::getTemplateTypeMap() returns the template declarations, + * and ParametersAcceptor::getResolvedTemplateTypeMap() returns inferred concrete types. + * Type::inferTemplateTypes() produces a TemplateTypeMap from a concrete type. + * * @api */ final class TemplateTypeMap @@ -22,8 +42,8 @@ final class TemplateTypeMap /** * @api - * @param array $types - * @param array $lowerBoundTypes + * @param array $types Concrete types for each template parameter (upper bounds) + * @param array $lowerBoundTypes Types inferred from contravariant positions */ public function __construct(private array $types, private array $lowerBoundTypes = []) { @@ -206,6 +226,9 @@ public function map(callable $cb): self return new self($types); } + /** + * Replaces unresolved TemplateType values with their declared bounds (or defaults). + */ public function resolveToBounds(): self { if ($this->resolvedToBounds !== null) { diff --git a/src/Type/Generic/TemplateTypeReference.php b/src/Type/Generic/TemplateTypeReference.php index 0be67d5e08..1bf0b087fa 100644 --- a/src/Type/Generic/TemplateTypeReference.php +++ b/src/Type/Generic/TemplateTypeReference.php @@ -2,6 +2,20 @@ namespace PHPStan\Type\Generic; +/** + * A reference to a template type together with its variance at the point of usage. + * + * When a type contains template type parameters (e.g. `array` or `Comparable`), + * this class pairs the TemplateType with its positional variance — whether T appears + * in a covariant position (return type), contravariant position (parameter type), + * invariant position, or bivariant position. + * + * Used by Type::getReferencedTemplateTypes() to report all template types within + * a type along with their variance context. This information is used for: + * - Template type inference (knowing the variance affects how types are inferred) + * - Variance validation (checking that @template-covariant types only appear in + * covariant positions) + */ final class TemplateTypeReference { diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index a630895bed..35536317dd 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -13,6 +13,24 @@ use function sprintf; /** + * Represents the variance of a template type parameter. + * + * Variance describes how subtyping of a generic type relates to subtyping of its + * type arguments. For a class `Box`: + * + * - **Invariant** (default): `Box` is NOT a subtype of `Box`, even though + * Cat extends Animal. The type argument must match exactly. Declared with `@template T`. + * - **Covariant**: `Box` IS a subtype of `Box`. Safe when T only appears + * in "output" positions (return types). Declared with `@template-covariant T`. + * - **Contravariant**: `Box` IS a subtype of `Box`. Safe when T only + * appears in "input" positions (parameter types). Declared with `@template-contravariant T`. + * - **Bivariant**: The type argument is ignored for subtyping purposes. Rarely used. + * - **Static**: Special variance for `static` return type in template context. + * + * Variance composition follows standard rules — e.g. covariant composed with + * contravariant yields contravariant. This is used when template types appear + * inside nested generic types. + * * @api */ final class TemplateTypeVariance @@ -37,26 +55,31 @@ private static function create(int $value): self return self::$registry[$value]; } + /** Type argument must match exactly. This is the default for @template T. */ public static function createInvariant(): self { return self::create(self::INVARIANT); } + /** Subtyping flows with the type argument: Cat <: Animal ⟹ Box <: Box. */ public static function createCovariant(): self { return self::create(self::COVARIANT); } + /** Subtyping flows against the type argument: Cat <: Animal ⟹ Box <: Box. */ public static function createContravariant(): self { return self::create(self::CONTRAVARIANT); } + /** Special variance for static return type in template context. */ public static function createStatic(): self { return self::create(self::STATIC); } + /** Type argument is ignored for subtyping — all types are compatible. */ public static function createBivariant(): self { return self::create(self::BIVARIANT); diff --git a/src/Type/IsSuperTypeOfResult.php b/src/Type/IsSuperTypeOfResult.php index 7fddea5c04..24bdebc5c7 100644 --- a/src/Type/IsSuperTypeOfResult.php +++ b/src/Type/IsSuperTypeOfResult.php @@ -10,6 +10,22 @@ use function array_values; /** + * Result of a Type::isSuperTypeOf() check — whether one type is a supertype of another. + * + * Wraps a TrinaryLogic result together with human-readable reasons explaining the + * relationship. This is the primary mechanism for comparing types in PHPStan's type system. + * + * `isSuperTypeOf()` answers: "Can all values of type B also be values of type A?" + * For example: + * - `(new StringType())->isSuperTypeOf(new ConstantStringType('hello'))` → Yes + * - `(new IntegerType())->isSuperTypeOf(new StringType())` → No + * - `(new StringType())->isSuperTypeOf(new MixedType())` → Maybe + * + * This is distinct from `accepts()` which also considers rule levels and PHPDoc context. + * Use `isSuperTypeOf()` for type-theoretic comparisons and `accepts()` for assignability checks. + * + * Can be converted to AcceptsResult via toAcceptsResult(). + * * @api */ final class IsSuperTypeOfResult @@ -17,7 +33,7 @@ final class IsSuperTypeOfResult /** * @api - * @param list $reasons + * @param list $reasons Human-readable explanations of the type relationship */ public function __construct( public readonly TrinaryLogic $result, @@ -58,9 +74,7 @@ public static function createYes(): self return new self(TrinaryLogic::createYes(), []); } - /** - * @param list $reasons - */ + /** @param list $reasons */ public static function createNo(array $reasons = []): self { return new self(TrinaryLogic::createNo(), $reasons); @@ -111,9 +125,7 @@ public function or(self ...$others): self ); } - /** - * @param callable(string): string $cb - */ + /** @param callable(string): string $cb */ public function decorateReasons(callable $cb): self { $reasons = []; @@ -124,6 +136,7 @@ public function decorateReasons(callable $cb): self return new self($this->result, $reasons); } + /** @see TrinaryLogic::extremeIdentity() */ public static function extremeIdentity(self ...$operands): self { if ($operands === []) { @@ -135,6 +148,7 @@ public static function extremeIdentity(self ...$operands): self return new self($result, self::mergeReasons($operands)); } + /** @see TrinaryLogic::maxMin() */ public static function maxMin(self ...$operands): self { if ($operands === []) { diff --git a/src/Type/Type.php b/src/Type/Type.php index 58d37a52a4..77f7682ca5 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -21,6 +21,25 @@ use PHPStan\Type\Generic\TemplateTypeVariance; /** + * Represents a PHPStan type in the type system. + * + * This is the central interface of PHPStan's type system. Every type that PHPStan + * can reason about implements this interface — from simple scalars like StringType + * to complex generics like GenericObjectType. + * + * Each Type knows what it accepts, what is a supertype of it, what properties/methods/constants + * it has, what operations it supports, and how to describe itself for error messages. + * + * Important: Never use `instanceof` to check types. For example, `$type instanceof StringType` + * will miss union types, intersection types with accessory types, and other composite forms. + * Always use the `is*()` methods or `isSuperTypeOf()` instead: + * + * // Wrong: + * if ($type instanceof StringType) { ... } + * + * // Correct: + * if ($type->isString()->yes()) { ... } + * * @api * @see https://phpstan.org/developing-extensions/type-system */ @@ -28,26 +47,35 @@ interface Type { /** + * Returns all class names referenced anywhere in this type, recursively + * (including generic arguments, callable signatures, etc.). + * + * @see Type::getObjectClassNames() for only direct object type class names + * * @return list */ public function getReferencedClasses(): array; - /** @return list */ - public function getObjectClassNames(): array; - /** - * @return list + * Returns class names of the object types this type directly represents. + * Unlike getReferencedClasses(), excludes classes in generic arguments, etc. + * + * @return list */ + public function getObjectClassNames(): array; + + /** @return list */ public function getObjectClassReflections(): array; /** - * Returns object type Foo for class-string and 'Foo' (if Foo is a valid class). + * Returns the object type for a class-string or literal class name string. + * For non-class-string types, returns ErrorType. */ public function getClassStringObjectType(): Type; /** - * Returns object type Foo for class-string, 'Foo' (if Foo is a valid class), - * and object type Foo. + * Like getClassStringObjectType(), but also returns object types as-is. + * Used for `$classOrObject::method()` where the left side can be either. */ public function getObjectTypeOrClassStringObjectType(): Type; @@ -58,14 +86,29 @@ public function isEnum(): TrinaryLogic; /** @return list */ public function getArrays(): array; - /** @return list */ + /** + * Only ConstantArrayType instances (array shapes with known keys). + * + * @return list + */ public function getConstantArrays(): array; /** @return list */ public function getConstantStrings(): array; + /** + * Unlike isSuperTypeOf(), accepts() takes into account PHP's implicit type coercion. + * With $strictTypes = false, int is accepted by float, and Stringable objects are + * accepted by string. + */ public function accepts(Type $type, bool $strictTypes): AcceptsResult; + /** + * "Does every value of $type belong to $this type?" + * + * Preferable to instanceof checks because it correctly handles + * union types, intersection types, and all other composite types. + */ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult; public function equals(Type $type): bool; @@ -87,6 +130,10 @@ public function hasInstanceProperty(string $propertyName): TrinaryLogic; public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection; + /** + * Unlike getInstanceProperty(), this defers template type resolution. + * Use getInstanceProperty() in most rule implementations. + */ public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection; public function hasStaticProperty(string $propertyName): TrinaryLogic; @@ -101,6 +148,10 @@ public function hasMethod(string $methodName): TrinaryLogic; public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection; + /** + * Unlike getMethod(), this defers template type and static type resolution. + * Use getMethod() in most rule implementations. + */ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection; public function canAccessConstants(): TrinaryLogic; @@ -113,8 +164,14 @@ public function isIterable(): TrinaryLogic; public function isIterableAtLeastOnce(): TrinaryLogic; + /** + * Returns the count of elements as a Type (typically IntegerRangeType). + */ public function getArraySize(): Type; + /** + * Works for both arrays and Traversable objects. + */ public function getIterableKeyType(): Type; /** @deprecated use getIterableKeyType */ @@ -135,61 +192,96 @@ public function isArray(): TrinaryLogic; public function isConstantArray(): TrinaryLogic; + /** + * An oversized array is a constant array shape that grew too large to track + * precisely and was degraded to a generic array type. + */ public function isOversizedArray(): TrinaryLogic; + /** + * A list is an array with sequential integer keys starting from 0 with no gaps. + */ public function isList(): TrinaryLogic; public function isOffsetAccessible(): TrinaryLogic; + /** + * Whether accessing a non-existent offset is safe (won't cause errors). + * Unlike isOffsetAccessible() which checks if offset access is supported at all. + */ public function isOffsetAccessLegal(): TrinaryLogic; public function hasOffsetValueType(Type $offsetType): TrinaryLogic; public function getOffsetValueType(Type $offsetType): Type; + /** + * May add a new key. When $offsetType is null, appends (like $a[] = $value). + * + * @see Type::setExistingOffsetValueType() for modifying an existing key without widening + */ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type; + /** + * Unlike setOffsetValueType(), assumes the key already exists. + * Preserves the array shape and list type. + */ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type; public function unsetOffset(Type $offsetType): Type; + /** Models array_keys($array, $searchValue, $strict). */ public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type; + /** Models array_keys($array). */ public function getKeysArray(): Type; + /** Models array_values($array). */ public function getValuesArray(): Type; + /** Models array_chunk($array, $length, $preserveKeys). */ public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type; + /** Models array_fill_keys($keys, $value). */ public function fillKeysArray(Type $valueType): Type; + /** Models array_flip($array). */ public function flipArray(): Type; + /** Models array_intersect_key($array, ...$otherArrays). */ public function intersectKeyArray(Type $otherArraysType): Type; + /** Models array_pop() effect on the array. */ public function popArray(): Type; + /** Models array_reverse($array, $preserveKeys). */ public function reverseArray(TrinaryLogic $preserveKeys): Type; + /** Models array_search($needle, $array, $strict). */ public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type; + /** Models array_shift() effect on the array. */ public function shiftArray(): Type; + /** Models shuffle() effect on the array. Result is always a list. */ public function shuffleArray(): Type; + /** Models array_slice($array, $offset, $length, $preserveKeys). */ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type; + /** Models array_splice() effect on the array (the modified array, not the removed portion). */ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type; - /** - * @return list - */ + /** @return list */ public function getEnumCases(): array; + /** + * Returns the single enum case this type represents, or null if not exactly one case. + */ public function getEnumCaseObject(): ?EnumCaseObjectType; /** - * Returns a list of finite values. + * Returns a list of finite values this type can take. * * Examples: * @@ -204,37 +296,51 @@ public function getEnumCaseObject(): ?EnumCaseObjectType; */ public function getFiniteTypes(): array; + /** Models the ** operator. */ public function exponentiate(Type $exponent): Type; public function isCallable(): TrinaryLogic; - /** - * @return list - */ + /** @return list */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array; public function isCloneable(): TrinaryLogic; + /** Models the (bool) cast. */ public function toBoolean(): BooleanType; + /** Models numeric coercion for arithmetic operators. */ public function toNumber(): Type; + /** Models the (int) cast. */ public function toInteger(): Type; + /** Models the (float) cast. */ public function toFloat(): Type; + /** Models the (string) cast. */ public function toString(): Type; + /** Models the (array) cast. */ public function toArray(): Type; + /** + * Models PHP's implicit array key coercion: floats truncated to int, + * booleans become 0/1, null becomes '', numeric strings become int. + */ public function toArrayKey(): Type; /** - * Tells how a type might change when passed to an argument + * Returns how this type might change when passed to a typed parameter * or assigned to a typed property. * - * Example: int is accepted by int|float with strict_types = 1 - * Stringable is accepted by string|Stringable even without strict_types. + * With $strictTypes = true: int widens to int|float (since int is accepted + * by float parameters in strict mode). + * With $strictTypes = false: additional coercions apply, e.g. Stringable + * objects are accepted by string parameters. + * + * Used internally to determine what types a value might be coerced to + * when checking parameter acceptance. */ public function toCoercedArgumentType(bool $strictTypes): self; @@ -244,22 +350,24 @@ public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): T /** * Is Type of a known constant value? Includes literal strings, integers, floats, true, false, null, and array shapes. + * + * Unlike isConstantScalarValue(), this also returns yes for constant array types (array shapes + * with known keys and values). Use this when you need to detect any constant value including arrays. */ public function isConstantValue(): TrinaryLogic; /** * Is Type of a known constant scalar value? Includes literal strings, integers, floats, true, false, and null. + * + * Unlike isConstantValue(), this does NOT return yes for array shapes. + * Use this when you specifically need scalar constants only. */ public function isConstantScalarValue(): TrinaryLogic; - /** - * @return list - */ + /** @return list */ public function getConstantScalarTypes(): array; - /** - * @return list - */ + /** @return list */ public function getConstantScalarValues(): array; public function isNull(): TrinaryLogic; @@ -280,8 +388,16 @@ public function isNumericString(): TrinaryLogic; public function isNonEmptyString(): TrinaryLogic; + /** + * Non-falsy string is a non-empty string that is also not '0'. + * Stricter subset of non-empty-string. + */ public function isNonFalsyString(): TrinaryLogic; + /** + * A literal-string is a string composed entirely from string literals + * in the source code (not from user input). Used for SQL injection prevention. + */ public function isLiteralString(): TrinaryLogic; public function isLowercaseString(): TrinaryLogic; @@ -296,6 +412,10 @@ public function isScalar(): TrinaryLogic; public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType; + /** + * Type narrowing methods for comparison operators. + * For example, for ConstantIntegerType(5), getSmallerType() returns int. + */ public function getSmallerType(PhpVersion $phpVersion): Type; public function getSmallerOrEqualType(PhpVersion $phpVersion): Type; @@ -323,15 +443,14 @@ public function getGreaterOrEqualType(PhpVersion $phpVersion): Type; public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type; /** - * Infers template types - * - * Infers the real Type of the TemplateTypes found in $this, based on - * the received Type. + * Infers the real types of TemplateTypes found in $this, based on + * the received Type. E.g. if $this is array and $receivedType + * is array, infers T = int. */ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap; /** - * Returns the template types referenced by this Type, recursively + * Returns the template types referenced by this Type, recursively. * * The return value is a list of TemplateTypeReferences, who contain the * referenced template type as well as the variance position in which it was @@ -348,20 +467,23 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap; */ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array; + /** Models abs(). */ public function toAbsoluteNumber(): Type; /** - * Traverses inner types + * Returns a new instance with all inner types mapped through $cb. + * Returns the same instance if inner types did not change. * - * Returns a new instance with all inner types mapped through $cb. Might - * return the same instance if inner types did not change. + * Not used directly — use TypeTraverser::map() instead. * * @param callable(Type):Type $cb */ public function traverse(callable $cb): Type; /** - * Traverses inner types while keeping the same context in another type. + * Like traverse(), but walks two types simultaneously. + * + * Not used directly — use SimultaneousTypeTraverser::map() instead. * * @param callable(Type $left, Type $right): Type $cb */ @@ -369,15 +491,18 @@ public function traverseSimultaneously(Type $right, callable $cb): Type; public function toPhpDocNode(): TypeNode; - /** - * Return the difference with another type, or null if it cannot be represented. - * - * @see TypeCombinator::remove() - */ + /** @see TypeCombinator::remove() */ public function tryRemove(Type $typeToRemove): ?Type; + /** + * Removes constant value information. E.g. 'foo' -> string, 1 -> int. + * Used when types become too complex to track precisely (e.g. loop iterations). + */ public function generalize(GeneralizePrecision $precision): Type; + /** + * Performance optimization to skip template resolution when no templates are present. + */ public function hasTemplateOrLateResolvableType(): bool; } diff --git a/src/Type/TypeWithClassName.php b/src/Type/TypeWithClassName.php index c2688af355..952c904cd1 100644 --- a/src/Type/TypeWithClassName.php +++ b/src/Type/TypeWithClassName.php @@ -4,12 +4,30 @@ use PHPStan\Reflection\ClassReflection; -/** @api */ +/** + * A Type that represents an object with a known class name. + * + * Implemented by ObjectType, StaticType, ThisType, EnumCaseObjectType, ClosureType, + * and GenericObjectType. Provides access to the class name and its ClassReflection. + * + * This interface is used when code needs to work with any object type that has a + * specific class — for example, Scope::resolveTypeByName() returns TypeWithClassName + * because the resolved type always has a known class. + * + * Note: Do not use `instanceof TypeWithClassName` to check if a type is an object. + * Use `$type->getObjectClassNames()` or `$type->isObject()` instead, which correctly + * handles union types and intersection types. + * + * @api + */ interface TypeWithClassName extends Type { public function getClassName(): string; + /** + * Returns this type projected onto an ancestor class, preserving generic type arguments. + */ public function getAncestorWithClassName(string $className): ?self; public function getClassReflection(): ?ClassReflection; diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 41ae162b7a..32be9683a8 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -14,6 +14,30 @@ use PHPStan\Type\Generic\GenericStaticType; use PHPStan\Type\Generic\TemplateType; +/** + * Controls the verbosity of type descriptions in error messages. + * + * When PHPStan describes a type for an error message, it uses VerbosityLevel to + * decide how much detail to include. Higher levels include more detail like constant + * values and array shapes. + * + * The four levels (from least to most verbose): + * - **typeOnly**: Just the type name, e.g. "string", "array", "Foo" + * - **value**: Includes constant values, e.g. "'hello'", "array{foo: int}", "non-empty-string" + * - **precise**: Maximum detail — adds subtracted types on object/mixed (e.g. "object~Bar"), + * lowercase/uppercase string distinctions, untruncated array shapes, and template type scope + * - **cache**: Internal level used for generating cache keys + * + * Used as a parameter to Type::describe() to control output detail: + * + * $type->describe(VerbosityLevel::typeOnly()) // "string" + * $type->describe(VerbosityLevel::value()) // "'hello'" + * $type->describe(VerbosityLevel::precise()) // "non-empty-lowercase-string" + * + * The getRecommendedLevelByType() factory method automatically chooses the right level + * for error messages based on what types are involved — it picks the minimum verbosity + * needed to distinguish the accepting type from the accepted type. + */ final class VerbosityLevel { @@ -65,7 +89,11 @@ public static function precise(): self return self::create(self::PRECISE); } - /** @api */ + /** + * Internal level for generating unique cache keys — not for user-facing messages. + * + * @api + */ public static function cache(): self { return self::create(self::CACHE); @@ -91,7 +119,11 @@ public function isCache(): bool return $this->value === self::CACHE; } - /** @api */ + /** + * Chooses the minimum verbosity needed to distinguish the two types in error messages. + * + * @api + */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { $moreVerbose = false;