diff --git a/apigen/apigen.neon b/apigen/apigen.neon index 351f94357ed..908f11227b9 100644 --- a/apigen/apigen.neon +++ b/apigen/apigen.neon @@ -9,5 +9,8 @@ services: renderer.filter: factory: PHPStan\ApiGen\RendererFilter + analyze.phpDocResolver: + factory: PHPStan\ApiGen\PhpDocResolver + commonMark: factory: PHPStan\ApiGen\MarkdownConverter diff --git a/apigen/src/MarkdownConverter.php b/apigen/src/MarkdownConverter.php index 8d8c8c05445..4e616103255 100644 --- a/apigen/src/MarkdownConverter.php +++ b/apigen/src/MarkdownConverter.php @@ -7,8 +7,10 @@ use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use League\CommonMark\MarkdownConverter as BaseMarkdownConverter; +use League\CommonMark\Output\RenderedContentInterface; +use function str_replace; -/** GitHub-flavored Markdown converter that escapes generic types written in PHPDoc descriptions. */ +/** GitHub-flavored Markdown converter for PHPDoc descriptions that escapes generic types and keeps paragraphs apart. */ final class MarkdownConverter extends BaseMarkdownConverter { @@ -22,4 +24,11 @@ public function __construct() parent::__construct($environment); } + public function convert(string $input): RenderedContentInterface + { + // ApiGen joins description nodes with a space, which would indent the paragraph + // after each separator and break code fences that start it. + return parent::convert(str_replace(' ' . PhpDocResolver::PARAGRAPH_SEPARATOR . ' ', PhpDocResolver::PARAGRAPH_SEPARATOR, $input)); + } + } diff --git a/apigen/src/PhpDocResolver.php b/apigen/src/PhpDocResolver.php new file mode 100644 index 00000000000..58c4b48709b --- /dev/null +++ b/apigen/src/PhpDocResolver.php @@ -0,0 +1,35 @@ +children as $child) { + if ($child instanceof PhpDocTextNode && $child->text === '') { + $children[] = new PhpDocTextNode(self::PARAGRAPH_SEPARATOR); + continue; + } + + $children = array_merge($children, parent::resolvePhpDoc(new PhpDocNode([$child]))->children); + } + + return new PhpDocNode($children); + } + +} diff --git a/src/DependencyInjection/ContainerExtensionsExtension.php b/src/DependencyInjection/ContainerExtensionsExtension.php index 383c65f2a04..08e07609449 100644 --- a/src/DependencyInjection/ContainerExtensionsExtension.php +++ b/src/DependencyInjection/ContainerExtensionsExtension.php @@ -18,7 +18,9 @@ * * Extending ExtensionsExtension is required, not cosmetic. Compiler::processExtensions() starts with * - * $first = $this->getExtensions(ParametersExtension::class) + $this->getExtensions(ExtensionsExtension::class); + * ```php + * $first = $this->getExtensions(ParametersExtension::class) + $this->getExtensions(ExtensionsExtension::class); + * ``` * * and only extensions matching those types by instanceof have their loadConfiguration() called before * the compiler snapshots its extension list. Everything registered after that snapshot is rejected with diff --git a/src/TrinaryLogic.php b/src/TrinaryLogic.php index 1707443fdaf..15a46e60815 100644 --- a/src/TrinaryLogic.php +++ b/src/TrinaryLogic.php @@ -20,15 +20,17 @@ * 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 - * } + * ```php + * 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 diff --git a/src/Type/Accessory/AccessoryType.php b/src/Type/Accessory/AccessoryType.php index e067a4bf012..3dbb81b36b7 100644 --- a/src/Type/Accessory/AccessoryType.php +++ b/src/Type/Accessory/AccessoryType.php @@ -15,11 +15,15 @@ * * For example, `non-empty-string` is represented as: * - * IntersectionType([StringType, AccessoryNonEmptyStringType]) + * ``` + * IntersectionType([StringType, AccessoryNonEmptyStringType]) + * ``` * * And `non-empty-list` is represented as: * - * IntersectionType([ArrayType(int, int), NonEmptyArrayType, AccessoryArrayListType]) + * ``` + * 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 diff --git a/src/Type/GeneralizePrecision.php b/src/Type/GeneralizePrecision.php index 5947f889ba3..5a9b45e9f9e 100644 --- a/src/Type/GeneralizePrecision.php +++ b/src/Type/GeneralizePrecision.php @@ -22,7 +22,9 @@ * * Used as a parameter to Type::generalize(): * - * $type->generalize(GeneralizePrecision::lessSpecific()) + * ```php + * $type->generalize(GeneralizePrecision::lessSpecific()) + * ``` */ #[ReferencedByTurboExtension(key: 'generalizePrecision')] final class GeneralizePrecision diff --git a/src/Type/Type.php b/src/Type/Type.php index ba36621e95b..d46008babbd 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -36,11 +36,13 @@ * 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) { ... } + * ```php + * // Wrong: + * if ($type instanceof StringType) { ... } * - * // Correct: - * if ($type->isString()->yes()) { ... } + * // Correct: + * if ($type->isString()->yes()) { ... } + * ``` * * @api * @api-do-not-implement @@ -563,14 +565,13 @@ public function getGreaterOrEqualType(PhpVersion $phpVersion): Type; /** * Returns actual template type for a given object. * - * Example: - * - * @-template T - * class Foo {} + * For example, with a class `Foo` that declares `@template T`: * + * ```php * // $fooType is Foo * $t = $fooType->getTemplateType(Foo::class, 'T'); * $t->isInteger(); // yes + * ``` * * Returns ErrorType in case of a missing type. * diff --git a/src/Type/TypeTraverser.php b/src/Type/TypeTraverser.php index 49017dc9da2..836431ad8c2 100644 --- a/src/Type/TypeTraverser.php +++ b/src/Type/TypeTraverser.php @@ -30,6 +30,7 @@ final class TypeTraverser * The following example converts constant strings to objects, while * preserving unions and intersections: * + * ```php * TypeTraverser::map($type, function (Type $type, callable $traverse): Type { * if ($type instanceof UnionType || $type instanceof IntersectionType) { * // Traverse inner types @@ -42,6 +43,7 @@ final class TypeTraverser * // Replaces the current type, and don't traverse * return new MixedType(); * }); + * ``` * * @api * @param TypeTraverserCallable|callable(Type $type, callable(Type): Type $traverse): Type $cb diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index fb760a80bfb..662da1cbda1 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -32,9 +32,11 @@ * * 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" + * ```php + * $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