From 0cbef6db8bb51440e73936693fb810a7327da03a Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Sun, 20 Sep 2026 17:36:37 +0800 Subject: [PATCH 01/10] Fix bug on generics eager validation of generic type ingoring inline generics var toggle --- src/Internal/Ast/PropertyHookInjector.php | 4 +- src/Internal/Checker/InlineChecker.php | 71 +++++++---------- src/Internal/Docblock/DocblockParser.php | 6 +- src/Internal/Generics/TemplateManager.php | 2 +- src/Internal/RuntimeTypeChecker.php | 4 + src/Internal/Util/Config.php | 92 +++++++++++++++++++++++ 6 files changed, 129 insertions(+), 50 deletions(-) diff --git a/src/Internal/Ast/PropertyHookInjector.php b/src/Internal/Ast/PropertyHookInjector.php index 93f62784..1750726e 100644 --- a/src/Internal/Ast/PropertyHookInjector.php +++ b/src/Internal/Ast/PropertyHookInjector.php @@ -44,9 +44,7 @@ private static function shouldSkipInjection(Node\Stmt\Property $node): bool return false; } - $shouldRespectIgnore = (bool) (Config::get()['respect_ignore_tags'] ?? true); - - return $shouldRespectIgnore && (str_contains($doc->getText(), '@typephp-ignore') || str_contains($doc->getText(), '@typephp-disable')); + return Config::isRespectIgnoreTagsEnabled() && (str_contains($doc->getText(), '@typephp-ignore') || str_contains($doc->getText(), '@typephp-disable')); } private static function processGetHook(Node\PropertyHook $hook, string $propertyName): void diff --git a/src/Internal/Checker/InlineChecker.php b/src/Internal/Checker/InlineChecker.php index c8a87e4b..9ef77f63 100644 --- a/src/Internal/Checker/InlineChecker.php +++ b/src/Internal/Checker/InlineChecker.php @@ -130,11 +130,7 @@ public static function checkVariable( ?string $caller = null, mixed $thisOrClass = null ): mixed { - $rawConfig = Config::get()['inline_vars'] ?? []; - /** @var array $config */ - $config = \is_array($rawConfig) ? $rawConfig : []; - - if (! self::hasActiveInlineChecks($config)) { + if (! Config::hasActiveInlineChecks()) { return $value; } @@ -150,7 +146,7 @@ public static function checkVariable( $typeNode = self::resolveCallerContext($typeNode, $caller, $thisOrClass); } - if (! self::shouldValidateType($typeNode, $config)) { + if (! self::shouldValidateType($typeNode)) { return $value; } @@ -166,8 +162,20 @@ public static function checkVariable( return CallableWrapper::wrapTypeNode($typeNode, $value, $cbPrefix, $registry); } - $checkGenerics = (bool) ($config['generics'] ?? true); - if ($typeNode instanceof GenericTypeNode && $checkGenerics && \is_object($value)) { + if ($typeNode instanceof GenericTypeNode) { + $baseName = strtolower($typeNode->type->name); + $isCollection = isset(self::ARRAY_TYPES[$baseName]); + + if (! $isCollection) { + if (! Config::isInlineGenericsEnabled() && Config::isInlineObjectsEnabled()) { + $typeNode = $typeNode->type; + } elseif (! Config::isInlineGenericsEnabled() && ! Config::isInlineObjectsEnabled()) { + return $value; + } + } + } + + if ($typeNode instanceof GenericTypeNode && Config::isInlineGenericsEnabled() && \is_object($value)) { $err = TemplateManager::bindInstanceFromNode($value, $typeNode, $context); if ($err !== null) { return $err; @@ -225,11 +233,7 @@ public static function checkProperty(mixed $value, mixed $objectOrClass, string return $value; } - $rawConfig = Config::get()['inline_vars'] ?? []; - /** @var array $config */ - $config = \is_array($rawConfig) ? $rawConfig : []; - - if (! ($config['properties'] ?? true)) { + if (! Config::isInlinePropertiesEnabled()) { return $value; } @@ -240,7 +244,7 @@ public static function checkProperty(mixed $value, mixed $objectOrClass, string return $value; } - if (! self::shouldValidateType($typeNode, $config)) { + if (! self::shouldValidateType($typeNode)) { return $value; } @@ -260,20 +264,6 @@ public static function checkProperty(mixed $value, mixed $objectOrClass, string return $value; } - /** - * Checks if at least one inline variable category is active. - * - * @param array $config - */ - private static function hasActiveInlineChecks(array $config): bool - { - return (bool) ($config['generics'] ?? true) - || (bool) ($config['callables'] ?? true) - || (bool) ($config['scalars'] ?? false) - || (bool) ($config['arrays'] ?? false) - || (bool) ($config['objects'] ?? false); - } - /** * Resolves caller class or function context and applies templates & type aliases to the AST. */ @@ -550,15 +540,12 @@ private static function getTypeParserComponents(): array return [$typeParser, $lexer]; } - /** - * @param array $config - */ - private static function shouldValidateType(TypeNode $node, array $config): bool + private static function shouldValidateType(TypeNode $node): bool { - $checkArrays = (bool) ($config['arrays'] ?? false); + $checkArrays = Config::isInlineArraysEnabled(); if ($node instanceof CallableTypeNode) { - return (bool) ($config['callables'] ?? true); + return Config::isInlineCallablesEnabled(); } if ($node instanceof ObjectShapeNode || $node instanceof ArrayShapeNode || $node instanceof ArrayTypeNode) { @@ -573,7 +560,7 @@ private static function shouldValidateType(TypeNode $node, array $config): bool } if ($lower === 'callable') { - return (bool) ($config['callables'] ?? true); + return Config::isInlineCallablesEnabled(); } if (isset(self::ARRAY_TYPES[$lower])) { @@ -581,10 +568,10 @@ private static function shouldValidateType(TypeNode $node, array $config): bool } if (isset(self::SCALAR_TYPES[$lower])) { - return (bool) ($config['scalars'] ?? false); + return Config::isInlineScalarsEnabled(); } - return (bool) ($config['objects'] ?? false); + return Config::isInlineObjectsEnabled(); } if ($node instanceof GenericTypeNode) { @@ -593,20 +580,20 @@ private static function shouldValidateType(TypeNode $node, array $config): bool return $checkArrays; } - if ((bool) ($config['generics'] ?? true)) { + if (Config::isInlineGenericsEnabled()) { return true; } - return (bool) ($config['objects'] ?? false); + return Config::isInlineObjectsEnabled(); } if ($node instanceof NullableTypeNode) { - return self::shouldValidateType($node->type, $config); + return self::shouldValidateType($node->type); } if ($node instanceof UnionTypeNode || $node instanceof IntersectionTypeNode) { foreach ($node->types as $t) { - if (self::shouldValidateType($t, $config)) { + if (self::shouldValidateType($t)) { return true; } } @@ -614,6 +601,6 @@ private static function shouldValidateType(TypeNode $node, array $config): bool return false; } - return (bool) ($config['scalars'] ?? false); + return Config::isInlineScalarsEnabled(); } } diff --git a/src/Internal/Docblock/DocblockParser.php b/src/Internal/Docblock/DocblockParser.php index 9cb3be93..d10f7f4b 100644 --- a/src/Internal/Docblock/DocblockParser.php +++ b/src/Internal/Docblock/DocblockParser.php @@ -472,7 +472,7 @@ public static function parseProperty(string $className, string $propertyName): ? $typeNode = null; $isMagicProperty = false; - if ($doc === false && (bool) (Config::get()['magic_properties'] ?? true)) { + if ($doc === false && Config::isMagicPropertiesEnabled()) { $magicResolved = self::findMagicPropertyDoc($refClass, $propertyName); if ($magicResolved !== null) { $doc = $magicResolved['doc']; @@ -764,9 +764,7 @@ private static function extractRawParamName(object $paramNode): string private static function shouldIgnoreDoc(string $doc): bool { - $shouldRespectIgnore = (bool) (Config::get()['respect_ignore_tags'] ?? true); - - return $shouldRespectIgnore && (str_contains($doc, '@typephp-ignore') || str_contains($doc, '@typephp-disable')); + return Config::isRespectIgnoreTagsEnabled() && (str_contains($doc, '@typephp-ignore') || str_contains($doc, '@typephp-disable')); } /** diff --git a/src/Internal/Generics/TemplateManager.php b/src/Internal/Generics/TemplateManager.php index 41ba807f..1eb2704c 100644 --- a/src/Internal/Generics/TemplateManager.php +++ b/src/Internal/Generics/TemplateManager.php @@ -414,7 +414,7 @@ public static function popPendingInstantiation(): void */ public static function applyPendingInstantiation(object $instance): void { - if (self::$pendingInstantiations === []) { + if (! Config::isInlineGenericsEnabled() || self::$pendingInstantiations === []) { return; } diff --git a/src/Internal/RuntimeTypeChecker.php b/src/Internal/RuntimeTypeChecker.php index e54f0e34..90700904 100644 --- a/src/Internal/RuntimeTypeChecker.php +++ b/src/Internal/RuntimeTypeChecker.php @@ -62,6 +62,10 @@ public static function isEnabled(): bool */ public static function withPendingGeneric(string $typeString, \Closure $factory, string $file = ''): mixed { + if (! Config::isEnabled() || ! Config::isInlineGenericsEnabled()) { + return $factory(); + } + TemplateManager::pushPendingInstantiation($typeString, $file); try { diff --git a/src/Internal/Util/Config.php b/src/Internal/Util/Config.php index bcff1a3f..9a9138af 100644 --- a/src/Internal/Util/Config.php +++ b/src/Internal/Util/Config.php @@ -64,6 +64,18 @@ final class Config private static string $arrayValidation = 'full'; + private static bool $inlineProperties = true; + + private static bool $inlineGenerics = true; + + private static bool $inlineCallables = true; + + private static bool $inlineScalars = true; + + private static bool $inlineArrays = true; + + private static bool $inlineObjects = true; + public static function isEnabled(): bool { if (self::$cachedConfig === null) { @@ -73,6 +85,73 @@ public static function isEnabled(): bool return self::$enabled; } + public static function isInlinePropertiesEnabled(): bool + { + if (self::$cachedConfig === null) { + self::get(); + } + + return self::$inlineProperties; + } + + public static function isInlineGenericsEnabled(): bool + { + if (self::$cachedConfig === null) { + self::get(); + } + + return self::$inlineGenerics; + } + + public static function isInlineCallablesEnabled(): bool + { + if (self::$cachedConfig === null) { + self::get(); + } + + return self::$inlineCallables; + } + + public static function isInlineScalarsEnabled(): bool + { + if (self::$cachedConfig === null) { + self::get(); + } + + return self::$inlineScalars; + } + + public static function isInlineArraysEnabled(): bool + { + if (self::$cachedConfig === null) { + self::get(); + } + + return self::$inlineArrays; + } + + public static function isInlineObjectsEnabled(): bool + { + if (self::$cachedConfig === null) { + self::get(); + } + + return self::$inlineObjects; + } + + public static function hasActiveInlineChecks(): bool + { + if (self::$cachedConfig === null) { + self::get(); + } + + return self::$inlineGenerics + || self::$inlineCallables + || self::$inlineScalars + || self::$inlineArrays + || self::$inlineObjects; + } + public static function isCacheCheckMtimeEnabled(): bool { if (self::$cachedConfig === null) { @@ -415,6 +494,12 @@ public static function reset(): void self::$arrayValidation = 'full'; self::$cacheCheckMtime = true; self::$paramsOut = true; + self::$inlineProperties = true; + self::$inlineGenerics = true; + self::$inlineCallables = true; + self::$inlineScalars = true; + self::$inlineArrays = true; + self::$inlineObjects = true; DocblockParser::reset(); ParamChecker::reset(); @@ -452,5 +537,12 @@ private static function syncFlags(array $config): void self::$vendorBoundaryOnly = (bool) ($config['vendor_boundary_only'] ?? true); self::$arrayValidation = \is_string($config['array_validation'] ?? null) ? $config['array_validation'] : 'full'; self::$cacheCheckMtime = (bool) ($config['cache_check_mtime'] ?? true); + $inline = \is_array($config['inline_vars'] ?? null) ? $config['inline_vars'] : []; + self::$inlineProperties = (bool) ($inline['properties'] ?? true); + self::$inlineGenerics = (bool) ($inline['generics'] ?? true); + self::$inlineCallables = (bool) ($inline['callables'] ?? true); + self::$inlineScalars = (bool) ($inline['scalars'] ?? true); + self::$inlineArrays = (bool) ($inline['arrays'] ?? true); + self::$inlineObjects = (bool) ($inline['objects'] ?? true); } } From 13278669f5a50a9b6d3b2be8def2ab70dea1869c Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Sun, 20 Sep 2026 17:42:41 +0800 Subject: [PATCH 02/10] Add tests for preserving line numbers in multi-line array returns --- .../ConstructorGenericPrebindingTest.php | 40 ++++++++ tests/Unit/LineNumberPreservationTest.php | 93 +++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/tests/TypeChecking/Generics/ConstructorGenericPrebindingTest.php b/tests/TypeChecking/Generics/ConstructorGenericPrebindingTest.php index f9346f74..3c920618 100644 --- a/tests/TypeChecking/Generics/ConstructorGenericPrebindingTest.php +++ b/tests/TypeChecking/Generics/ConstructorGenericPrebindingTest.php @@ -5,6 +5,7 @@ namespace TypePHP\Tests\TypeChecking\Generics; use TypePHP\Exception\TypeError; +use TypePHP\Internal\Util\Config; use TypePHP\TypePHP; class ConstructorPrebindAnimal @@ -82,3 +83,42 @@ public function getContent(): array ); }); }); + +describe('Configuration Toggles (inline_vars.generics => false)', function () { + afterEach(function () { + Config::reset(); + }); + + test('bypasses eager constructor prebinding and allows dynamic inference when generics toggle is false', function () { + Config::set([ + 'inline_vars' => [ + 'generics' => false, + 'objects' => true, + ], + ]); + + /** @var ConstructorPrebindBox $box */ + $box = new ConstructorPrebindBox(['1', '2', '3']); + + expect($box)->toBeInstanceOf(ConstructorPrebindBox::class) + ->and($box->getContent())->toBe(['1', '2', '3']) + ; + }); + + test('still enforces object class check when generics is false and objects is true', function () { + Config::set([ + 'inline_vars' => [ + 'generics' => false, + 'objects' => true, + ], + ]); + + expect(function () { + /** @var ConstructorPrebindBox $box */ + $box = new ConstructorPrebindCar(); + })->toThrow( + TypeError::class, + 'must be of type TypePHP\Tests\TypeChecking\Generics\ConstructorPrebindBox' + ); + }); +}); diff --git a/tests/Unit/LineNumberPreservationTest.php b/tests/Unit/LineNumberPreservationTest.php index c55b77e6..3788c830 100644 --- a/tests/Unit/LineNumberPreservationTest.php +++ b/tests/Unit/LineNumberPreservationTest.php @@ -208,4 +208,97 @@ public function show(): void ->and($transformed)->not()->toContain('/*') ; }); + + test('preserves exact line numbers when a multi-line array return is followed by subsequent method calls', function () { + $source = <<<'PHP' +> + */ + public function rules(): array + { + return [ + 'title' => ['required', 'string', 'max:255'], + 'body' => ['required', 'string'], + 'is_locked' => ['boolean'], + ]; + } + + /** + * @param array $data + */ + public function insert(array $data): void + { + // logic + } +} + +$targetCall = true; +PHP; + + $transformed = StreamWrapper::transformSource($source, 'test_multiline_return_drift.php'); + + $origLines = explode("\n", str_replace("\r\n", "\n", $source)); + $transLines = explode("\n", str_replace("\r\n", "\n", $transformed)); + + expect(\count($transLines))->toBe(\count($origLines)); + + $origIndex = array_search('$targetCall = true;', array_map('trim', $origLines), true); + $transIndex = array_search('$targetCall = true;', array_map('trim', $transLines), true); + + expect($transIndex)->toBe($origIndex) + ->and($origIndex)->toBe(27) + ; + }); + + test('points to exact return statement start line when multi-line return array contract fails', function () { + $tempDir = sys_get_temp_dir() . '/typephp_multiline_test_' . uniqid(); + mkdir($tempDir, 0777, true); + $scriptPath = $tempDir . '/multiline_return_test.php'; + + $code = <<<'PHP' +> + */ + public function rules(): array + { + return [ + 'title' => ['required', 'string'], + ]; + } +} + +(new ReturnLineCheckFixture())->rules(); +PHP; + + file_put_contents($scriptPath, $code); + + try { + require $scriptPath; + $caught = false; + } catch (TypePHP\Exception\TypeError $e) { + $caught = true; + expect($e->getLine())->toBe(12); + } finally { + if (file_exists($scriptPath)) { + @unlink($scriptPath); + } + if (is_dir($tempDir)) { + @rmdir($tempDir); + } + } + + expect($caught)->toBeTrue(); + }); }); From 24a706acc2fcb2992e98873e42a00da5247059a5 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Sun, 20 Sep 2026 19:29:03 +0800 Subject: [PATCH 03/10] Enhance ContractVisitor and RuntimeTypeChecker with property validation and error handling improvements; add tests for property default value validation --- src/Internal/Ast/ContractVisitor.php | 132 ++++++++++++++++++ src/Internal/Diagnostic/ErrorFactory.php | 5 +- src/Internal/RuntimeTypeChecker.php | 40 ++++++ src/Internal/Util/Config.php | 10 +- .../PropertyDefaultValidationTest.php | 67 +++++++++ 5 files changed, 247 insertions(+), 7 deletions(-) create mode 100644 tests/TypeChecking/Boundaries/PropertyDefaultValidationTest.php diff --git a/src/Internal/Ast/ContractVisitor.php b/src/Internal/Ast/ContractVisitor.php index 422ac616..54625247 100644 --- a/src/Internal/Ast/ContractVisitor.php +++ b/src/Internal/Ast/ContractVisitor.php @@ -7,6 +7,7 @@ use PhpParser\Node; use PhpParser\NodeVisitorAbstract; use TypePHP\Internal\Docblock\DocblockExtractor; +use TypePHP\Internal\Util\Config; /** * @internal AST Node Visitor that injects contract checks, scope tracking, property hook validation, and parameter/return wrappers into functions and methods. @@ -71,6 +72,20 @@ public function enterNode(Node $node): ?array $this->scopeManager->pushScope(); } + if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) { + $this->markWriteContext($node->var); + } elseif ($node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc || $node instanceof Node\Expr\PreDec || $node instanceof Node\Expr\PostDec) { + $this->markWriteContext($node->var); + } elseif ($node instanceof Node\Stmt\Unset_) { + foreach ($node->vars as $v) { + $this->markWriteContext($v); + } + } + + if ($node instanceof Node\Stmt\Class_) { + $this->processClassPropertyDefaults($node); + } + if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) { $classContext = $this->classStack !== [] ? end($this->classStack) : null; FunctionContractInjector::inject($node, $classContext); @@ -135,6 +150,29 @@ public function leaveNode(Node $node): ?Node } } + if ($node instanceof Node\Expr\StaticPropertyFetch && $node->name instanceof Node\VarLikeIdentifier) { + $propName = $node->name->toString(); + + if ($node->getAttribute('typephp_checked') !== true && $node->getAttribute('typephp_write_context') !== true) { + $node->setAttribute('typephp_checked', true); + $classArg = $node->class instanceof Node\Name + ? new Node\Expr\ClassConstFetch($node->class, 'class') + : $node->class; + + return new Node\Expr\StaticCall( + new Node\Name\FullyQualified('TypePHP\Internal\RuntimeTypeChecker'), + 'checkStaticProperty', + [ + new Node\Arg($classArg), + new Node\Arg(new Node\Scalar\String_($propName)), + new Node\Arg($node), + new Node\Arg(new Node\Scalar\MagicConst\File()), + new Node\Arg(new Node\Scalar\LNumber($node->getStartLine())), + ] + ); + } + } + if ($this->isScopeBoundary($node)) { $this->scopeManager->popScope(); } @@ -142,6 +180,100 @@ public function leaveNode(Node $node): ?Node return null; } + private function markWriteContext(Node $node): void + { + $node->setAttribute('typephp_write_context', true); + if ($node instanceof Node\Expr\ArrayDimFetch || $node instanceof Node\Expr\PropertyFetch) { + $this->markWriteContext($node->var); + } + } + + private function processClassPropertyDefaults(Node\Stmt\Class_ $node): void + { + if (! Config::isInlinePropertiesEnabled()) { + return; + } + + $defaultProps = []; + $hasConstructor = false; + + foreach ($node->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\ClassMethod && strtolower($stmt->name->toString()) === '__construct') { + $hasConstructor = true; + } elseif ($stmt instanceof Node\Stmt\Property && ! $stmt->isStatic()) { + $doc = $stmt->getDocComment(); + if ($doc !== null && str_contains($doc->getText(), '@var') && ! str_contains($doc->getText(), '@typephp-ignore')) { + foreach ($stmt->props as $p) { + $isExplicitNull = $p->default instanceof Node\Expr\ConstFetch && strtolower($p->default->name->toString()) === 'null'; + if ($p->default !== null && ! $isExplicitNull) { + $defaultProps[] = [ + 'name' => $p->name->toString(), + 'line' => $p->getStartLine(), + ]; + } + } + } + } + } + + if ($defaultProps === []) { + return; + } + + if (! $hasConstructor) { + $ctorStmts = []; + if ($node->extends !== null) { + $ctorStmts[] = new Node\Stmt\If_( + new Node\Expr\FuncCall(new Node\Name('method_exists'), [ + new Node\Arg(new Node\Expr\ClassConstFetch(new Node\Name('parent'), 'class')), + new Node\Arg(new Node\Scalar\String_('__construct')), + ]), + ['stmts' => [ + new Node\Stmt\Expression(new Node\Expr\StaticCall(new Node\Name('parent'), '__construct', [new Node\Arg(new Node\Expr\Variable('_typephp_ctor_args'), false, true)])), + ]] + ); + } + + foreach ($defaultProps as $dp) { + $checkCall = NodeBuilder::createPropertyCheckCall( + new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $dp['name']), + new Node\Expr\Variable('this'), + $dp['name'] + ); + $stmt = new Node\Stmt\Expression(NodeBuilder::createTernaryThrowExpr($checkCall, $dp['line'])); + $stmt->setAttribute('typephp_injected', true); + $ctorStmts[] = $stmt; + } + + $ctor = new Node\Stmt\ClassMethod('__construct', [ + 'flags' => Node\Stmt\Class_::MODIFIER_PUBLIC, + 'params' => [new Node\Param(new Node\Expr\Variable('_typephp_ctor_args'), null, null, false, true)], + 'stmts' => $ctorStmts, + ]); + $ctor->setAttribute('typephp_injected', true); + $node->stmts[] = $ctor; + } else { + foreach ($node->stmts as $stmt) { + if ($stmt instanceof Node\Stmt\ClassMethod && strtolower($stmt->name->toString()) === '__construct') { + $injected = []; + foreach ($defaultProps as $dp) { + $checkCall = NodeBuilder::createPropertyCheckCall( + new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), $dp['name']), + new Node\Expr\Variable('this'), + $dp['name'] + ); + $checkStmt = new Node\Stmt\Expression(NodeBuilder::createTernaryThrowExpr($checkCall, $dp['line'])); + $checkStmt->setAttribute('typephp_injected', true); + $injected[] = $checkStmt; + } + $stmt->stmts = [...$injected, ...($stmt->stmts ?? [])]; + + break; + } + } + } + } + private function trackDeclarationEntry(Node $node): void { if ($node instanceof Node\Stmt\Namespace_) { diff --git a/src/Internal/Diagnostic/ErrorFactory.php b/src/Internal/Diagnostic/ErrorFactory.php index 3898544f..0d9b8a50 100644 --- a/src/Internal/Diagnostic/ErrorFactory.php +++ b/src/Internal/Diagnostic/ErrorFactory.php @@ -25,6 +25,7 @@ final class ErrorFactory 'iterator $', 'return iterator', 'generator sent value', + 'property ', ]; /** @@ -47,9 +48,9 @@ public static function createError(string $message): ErrorMessage * Prepares a TypeError exception before throwing by filtering internal library frames * and repointing the exception to the actual application caller location. */ - public static function prepareException(TypeError $e, ?int $line = null): TypeError + public static function prepareException(TypeError $e, ?int $line = null, ?string $file = null): TypeError { - $targetFile = null; + $targetFile = $file; $targetLine = $line; $message = $e->getMessage(); diff --git a/src/Internal/RuntimeTypeChecker.php b/src/Internal/RuntimeTypeChecker.php index 90700904..3f1f42bc 100644 --- a/src/Internal/RuntimeTypeChecker.php +++ b/src/Internal/RuntimeTypeChecker.php @@ -13,6 +13,7 @@ use TypePHP\Internal\Checker\ParamOutChecker; use TypePHP\Internal\Checker\ReturnChecker; use TypePHP\Internal\Checker\SelfOutChecker; +use TypePHP\Internal\Diagnostic\ErrorFactory; use TypePHP\Internal\Diagnostic\ErrorMessage; use TypePHP\Internal\Docblock\DocblockParser; use TypePHP\Internal\Generics\TemplateManager; @@ -35,12 +36,18 @@ final class RuntimeTypeChecker */ public static array $hasMethodTemplatesCache = []; + /** + * @var array + */ + private static array $checkedStaticProperties = []; + /** * Resets runtime caches. */ public static function reset(): void { self::$hasMethodTemplatesCache = []; + self::$checkedStaticProperties = []; IgnoreManager::reset(); CallerBoundaryResolver::reset(); ParamOutChecker::reset(); @@ -75,6 +82,39 @@ public static function withPendingGeneric(string $typeString, \Closure $factory, } } + /** + * Evaluates inline property validation dynamically based on configuration. + */ + public static function checkStaticProperty( + mixed $objectOrClass, + string $propName, + mixed $value, + string $file = '', + ?int $line = null + ): mixed { + if (! Config::isEnabled() || ! Config::isInlinePropertiesEnabled()) { + return $value; + } + + $className = \is_object($objectOrClass) ? $objectOrClass::class : (\is_string($objectOrClass) ? $objectOrClass : ''); + $key = $className . '::$' . $propName; + + if (isset(self::$checkedStaticProperties[$key])) { + return $value; + } + + self::$checkedStaticProperties[$key] = true; + + $res = self::checkProperty($value, $objectOrClass, $propName, $file); + if ($res instanceof ErrorMessage) { + $typeError = new \TypePHP\Exception\TypeError($res->getMessage()); + + throw ErrorFactory::prepareException($typeError, $line, $file !== '' ? $file : null); + } + + return $value; + } + /** * Delegates generic template binding for class instances. */ diff --git a/src/Internal/Util/Config.php b/src/Internal/Util/Config.php index 9a9138af..a2c0022d 100644 --- a/src/Internal/Util/Config.php +++ b/src/Internal/Util/Config.php @@ -392,8 +392,8 @@ public static function get(): array /** @var array $currentStubs */ $currentStubs = \is_array($mergedConfig['stubs'] ?? null) ? $mergedConfig['stubs'] : []; - $mergedConfig['include'] = array_values(array_unique(array_merge($currentIncludes, $extensionIncludes))); - $mergedConfig['stubs'] = array_values(array_unique(array_merge($currentStubs, $extensionStubs))); + $mergedConfig['include'] = array_values(array_unique([...$currentIncludes, ...$extensionIncludes])); + $mergedConfig['stubs'] = array_values(array_unique([...$currentStubs, ...$extensionStubs])); self::syncFlags($mergedConfig); @@ -421,8 +421,8 @@ public static function set(array $config): void /** @var array $currentStubs */ $currentStubs = \is_array($mergedConfig['stubs'] ?? null) ? $mergedConfig['stubs'] : []; - $mergedConfig['include'] = array_values(array_unique(array_merge($currentIncludes, $extensionIncludes))); - $mergedConfig['stubs'] = array_values(array_unique(array_merge($currentStubs, $extensionStubs))); + $mergedConfig['include'] = array_values(array_unique([...$currentIncludes, ...$extensionIncludes])); + $mergedConfig['stubs'] = array_values(array_unique([...$currentStubs, ...$extensionStubs])); } self::$cachedConfig = $mergedConfig; @@ -463,7 +463,7 @@ private static function mergeConfig(array $base, array $overrides): array $baseInlineVars = $base['inline_vars']; /** @var array $overrideInlineVars */ $overrideInlineVars = $value; - $merged['inline_vars'] = array_merge($baseInlineVars, $overrideInlineVars); + $merged['inline_vars'] = [...$baseInlineVars, ...$overrideInlineVars]; } elseif (\in_array($key, ['include', 'exclude', 'extensions', 'stubs'], true) && \is_array($value)) { $merged[$key] = array_values($value); } else { diff --git a/tests/TypeChecking/Boundaries/PropertyDefaultValidationTest.php b/tests/TypeChecking/Boundaries/PropertyDefaultValidationTest.php new file mode 100644 index 00000000..a15aa94a --- /dev/null +++ b/tests/TypeChecking/Boundaries/PropertyDefaultValidationTest.php @@ -0,0 +1,67 @@ + + */ + public static array $protected = [ + 'name', + 'email', + 12345, + ]; +} + +class InstancePropertyDefaultInvalid +{ + /** + * @var positive-int + */ + public int $score = -1; +} + +class InstancePropertyDefaultValid +{ + /** + * @var positive-int + */ + public int $score = 100; +} + +describe('Property Default Value Validation at Declaration & Instantiation', function () { + test('rejects invalid static property default integer upon access or loading', function () { + expect(fn () => StaticPropertyDefaultInvalid::$score) + ->toThrow(TypeError::class, 'positive-int') + ; + }); + + test('rejects invalid static property default array list upon access or loading', function () { + expect(fn () => StaticPropertyDefaultListInvalid::$protected) + ->toThrow(TypeError::class, 'string') + ; + }); + + test('rejects invalid instance property default value upon instantiation', function () { + expect(fn () => new InstancePropertyDefaultInvalid()) + ->toThrow(TypeError::class, 'positive-int') + ; + }); + + test('accepts valid instance property default values cleanly', function () { + $user = new InstancePropertyDefaultValid(); + + expect($user->score)->toBe(100); + }); +}); From a56fa5a4ea7ec91f5fd094f092521769fc603e1d Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Sun, 20 Sep 2026 19:58:37 +0800 Subject: [PATCH 04/10] Refactor LineNumberPreservationTest to improve temporary directory handling and add configuration setup for StreamWrapper --- tests/Unit/LineNumberPreservationTest.php | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/Unit/LineNumberPreservationTest.php b/tests/Unit/LineNumberPreservationTest.php index 3788c830..3300dbab 100644 --- a/tests/Unit/LineNumberPreservationTest.php +++ b/tests/Unit/LineNumberPreservationTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use TypePHP\Internal\Io\StreamWrapper; +use TypePHP\Internal\Util\Config; describe('Line Number Preservation', function () { test('transforms code without shifting original line numbers for parameter checks', function () { @@ -257,9 +258,13 @@ public function insert(array $data): void }); test('points to exact return statement start line when multi-line return array contract fails', function () { - $tempDir = sys_get_temp_dir() . '/typephp_multiline_test_' . uniqid(); + $sysTemp = realpath(sys_get_temp_dir()); + $baseTemp = str_replace('\\', '/', $sysTemp !== false ? $sysTemp : sys_get_temp_dir()); + $tempDir = $baseTemp . '/typephp_multiline_test_' . uniqid(); mkdir($tempDir, 0777, true); - $scriptPath = $tempDir . '/multiline_return_test.php'; + + $canonicalDir = str_replace('\\', '/', realpath($tempDir) ?: $tempDir); + $scriptPath = $canonicalDir . '/multiline_return_test.php'; $code = <<<'PHP' [ + $canonicalDir . '/**', + ], + 'exclude' => [ + 'vendor/**', + ], + ]); + StreamWrapper::register(); + require $scriptPath; $caught = false; - } catch (TypePHP\Exception\TypeError $e) { + } catch (\TypePHP\Exception\TypeError $e) { $caught = true; expect($e->getLine())->toBe(12); } finally { + Config::reset(); if (file_exists($scriptPath)) { @unlink($scriptPath); } From 55045aec184e9aa1ed8ebd406be604e0bbecd0e8 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 21 Sep 2026 01:45:32 +0800 Subject: [PATCH 05/10] Refactor path validation logic in PathMatcher to improve handling of system variable paths for improve MacOS temp director handling --- src/Internal/Util/PathMatcher.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Internal/Util/PathMatcher.php b/src/Internal/Util/PathMatcher.php index 44814802..6fbd2728 100644 --- a/src/Internal/Util/PathMatcher.php +++ b/src/Internal/Util/PathMatcher.php @@ -290,9 +290,15 @@ public static function mayPathBeIncluded(string $normalizedPath): bool } } - if (str_starts_with($canon, 'var/') - || (! str_starts_with($canon, '/var/') && str_contains($canon, '/var/')) - || (str_starts_with($canon, '/var/') && str_contains(substr($canon, 4), '/var/')) + $isSystemVar = str_starts_with($canon, '/var/') || str_starts_with($canon, '/private/var/'); + $canonAfterSystemVar = str_starts_with($canon, '/private/var/') + ? substr($canon, 12) + : (str_starts_with($canon, '/var/') ? substr($canon, 4) : $canon); + + if ( + str_starts_with($canon, 'var/') + || (! $isSystemVar && str_contains($canon, '/var/')) + || ($isSystemVar && str_contains($canonAfterSystemVar, '/var/')) ) { if (! self::hasIncludeMatchingPrefix('var/', $includes)) { return false; From 62b9da678ef618a10a32cdcd48b82cb1a2fb06c2 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 21 Sep 2026 02:09:33 +0800 Subject: [PATCH 06/10] Add DeepIgnoreTraceDepthTest to validate @typephp-ignore behavior in deep stack traces --- .../DeepIgnoreTraceDepthTest.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php diff --git a/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php b/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php new file mode 100644 index 00000000..0014471f --- /dev/null +++ b/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php @@ -0,0 +1,101 @@ += $targetDepth) { + return $leaf(); + } + + return self::recurse($currentDepth + 1, $targetDepth, $leaf); + } +} + +class DeepIgnoringCaller +{ + /** + * @typephp-ignore + */ + public function runDeep(int $framesDeep): int + { + $target = new DeepIgnoreTargetService(); + + return DeepPipelineRunner::recurse(1, $framesDeep, function () use ($target) { + return $target->executeLeaf(-1); + }); + } +} + +class DeepNormalCaller +{ + public function runDeep(int $framesDeep): int + { + $target = new DeepIgnoreTargetService(); + + return DeepPipelineRunner::recurse(1, $framesDeep, function () use ($target) { + return $target->executeLeaf(-1); + }); + } +} + +describe('Deep Stack Trace @typephp-ignore Resolution (11+ Frames Deep)', function () { + beforeEach(function () { + Config::reset(); + }); + + afterEach(function () { + Config::reset(); + }); + + test('honors @typephp-ignore when caller is 12 frames above the failing check', function () { + $caller = new DeepIgnoringCaller(); + + $result = $caller->runDeep(12); + + expect($result)->toBe(-1); + }); + + test('still throws TypeError when caller 12 frames deep does not have ignore tag', function () { + $caller = new DeepNormalCaller(); + + expect(fn () => $caller->runDeep(12)) + ->toThrow(TypeError::class, 'positive-int') + ; + }); + + test('allows custom ignore_trace_depth configuration', function () { + Config::set(['ignore_trace_depth' => 30]); + + $caller = new DeepIgnoringCaller(); + + $result = $caller->runDeep(20); + expect($result)->toBe(-1); + }); + + test('shallow configured depth throws when ignore tag is beyond window', function () { + Config::set(['ignore_trace_depth' => 5]); + + $caller = new DeepIgnoringCaller(); + + expect(fn () => $caller->runDeep(10)) + ->toThrow(TypeError::class, 'positive-int') + ; + }); +}); \ No newline at end of file From 70cfa8c001b9c96fe7d631108feb7a2c36ba5645 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 21 Sep 2026 02:09:46 +0800 Subject: [PATCH 07/10] Add ignore_trace_depth configuration to control stack trace depth for @typephp-ignore tags --- src/Internal/Cli/ConfigInitCommand.php | 11 +++++++++++ src/Internal/Util/Config.php | 16 ++++++++++++++++ src/Internal/Util/IgnoreManager.php | 3 ++- .../Configuration/DeepIgnoreTraceDepthTest.php | 6 +++--- tests/Unit/LineNumberPreservationTest.php | 2 +- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/Internal/Cli/ConfigInitCommand.php b/src/Internal/Cli/ConfigInitCommand.php index f8baed83..23b62689 100644 --- a/src/Internal/Cli/ConfigInitCommand.php +++ b/src/Internal/Cli/ConfigInitCommand.php @@ -113,6 +113,17 @@ private static function getTemplate(): string */ 'respect_ignore_tags' => true, + /* + |-------------------------------------------------------------------------- + | Ignore Tag Stack Trace Depth + |-------------------------------------------------------------------------- + | Controls how many stack frames above a failing type check TypePHP will + | inspect to find an enclosing @typephp-ignore or @typephp-disable tag. + | Default is 25 frames. Increase this if your application or test suite + | uses deep call stacks (e.g. pipelines, middlewares, or nested callers). + */ + 'ignore_trace_depth' => 25, + /* |-------------------------------------------------------------------------- | Array Validation Strategy diff --git a/src/Internal/Util/Config.php b/src/Internal/Util/Config.php index a2c0022d..1e60b101 100644 --- a/src/Internal/Util/Config.php +++ b/src/Internal/Util/Config.php @@ -76,6 +76,8 @@ final class Config private static bool $inlineObjects = true; + private static int $ignoreTraceDepth = 25; + public static function isEnabled(): bool { if (self::$cachedConfig === null) { @@ -85,6 +87,15 @@ public static function isEnabled(): bool return self::$enabled; } + public static function getIgnoreTraceDepth(): int + { + if (self::$cachedConfig === null) { + self::get(); + } + + return self::$ignoreTraceDepth; + } + public static function isInlinePropertiesEnabled(): bool { if (self::$cachedConfig === null) { @@ -345,6 +356,7 @@ public static function get(): array 'magic_properties' => true, 'magic_methods' => true, 'respect_ignore_tags' => true, + 'ignore_trace_depth' => 25, 'respect_native_nullability' => true, 'vendor_boundary_only' => true, 'array_validation' => 'full', @@ -489,6 +501,7 @@ public static function reset(): void self::$magicProperties = true; self::$magicMethods = true; self::$respectIgnoreTags = true; + self::$ignoreTraceDepth = 25; self::$respectNativeNullability = true; self::$vendorBoundaryOnly = true; self::$arrayValidation = 'full'; @@ -533,6 +546,9 @@ private static function syncFlags(array $config): void self::$magicProperties = (bool) ($config['magic_properties'] ?? true); self::$magicMethods = (bool) ($config['magic_methods'] ?? true); self::$respectIgnoreTags = (bool) ($config['respect_ignore_tags'] ?? true); + self::$ignoreTraceDepth = isset($config['ignore_trace_depth']) && is_numeric($config['ignore_trace_depth']) && (int) $config['ignore_trace_depth'] > 0 + ? (int) $config['ignore_trace_depth'] + : 25; self::$respectNativeNullability = (bool) ($config['respect_native_nullability'] ?? true); self::$vendorBoundaryOnly = (bool) ($config['vendor_boundary_only'] ?? true); self::$arrayValidation = \is_string($config['array_validation'] ?? null) ? $config['array_validation'] : 'full'; diff --git a/src/Internal/Util/IgnoreManager.php b/src/Internal/Util/IgnoreManager.php index e41a262d..639dd4fa 100644 --- a/src/Internal/Util/IgnoreManager.php +++ b/src/Internal/Util/IgnoreManager.php @@ -90,7 +90,8 @@ public static function isCallerIgnored(?string $callerClass = null, ?string $cal return self::$callerCache[$callerFunction] = self::checkFunctionIgnored($callerFunction); } - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 7); + $depth = Config::getIgnoreTraceDepth(); + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $depth); for ($i = 1; $i < \count($trace); $i++) { $frame = $trace[$i]; diff --git a/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php b/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php index 0014471f..50cfb828 100644 --- a/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php +++ b/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php @@ -38,7 +38,7 @@ public function runDeep(int $framesDeep): int $target = new DeepIgnoreTargetService(); return DeepPipelineRunner::recurse(1, $framesDeep, function () use ($target) { - return $target->executeLeaf(-1); + return $target->executeLeaf(-1); }); } } @@ -50,7 +50,7 @@ public function runDeep(int $framesDeep): int $target = new DeepIgnoreTargetService(); return DeepPipelineRunner::recurse(1, $framesDeep, function () use ($target) { - return $target->executeLeaf(-1); + return $target->executeLeaf(-1); }); } } @@ -98,4 +98,4 @@ public function runDeep(int $framesDeep): int ->toThrow(TypeError::class, 'positive-int') ; }); -}); \ No newline at end of file +}); diff --git a/tests/Unit/LineNumberPreservationTest.php b/tests/Unit/LineNumberPreservationTest.php index 3300dbab..bcb7d66b 100644 --- a/tests/Unit/LineNumberPreservationTest.php +++ b/tests/Unit/LineNumberPreservationTest.php @@ -302,7 +302,7 @@ public function rules(): array require $scriptPath; $caught = false; - } catch (\TypePHP\Exception\TypeError $e) { + } catch (TypePHP\Exception\TypeError $e) { $caught = true; expect($e->getLine())->toBe(12); } finally { From 8e060b88bc5637beda09ea3c7f0b002bf8e2186d Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 21 Sep 2026 02:13:24 +0800 Subject: [PATCH 08/10] Remove redundant beforeEach reset in DeepIgnoreTraceDepthTest --- tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php b/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php index 50cfb828..8a080707 100644 --- a/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php +++ b/tests/TypeChecking/Configuration/DeepIgnoreTraceDepthTest.php @@ -56,10 +56,6 @@ public function runDeep(int $framesDeep): int } describe('Deep Stack Trace @typephp-ignore Resolution (11+ Frames Deep)', function () { - beforeEach(function () { - Config::reset(); - }); - afterEach(function () { Config::reset(); }); From 10fe5337917e11f59270d5eef20b9707d40ebefc Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 21 Sep 2026 03:14:42 +0800 Subject: [PATCH 09/10] fix 8.2 ci failing test --- src/Internal/Docblock/DocblockParser.php | 11 +++-- src/Internal/Util/IgnoreManager.php | 60 ++++++++++++++++-------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/src/Internal/Docblock/DocblockParser.php b/src/Internal/Docblock/DocblockParser.php index d10f7f4b..dfb2667e 100644 --- a/src/Internal/Docblock/DocblockParser.php +++ b/src/Internal/Docblock/DocblockParser.php @@ -28,6 +28,7 @@ use TypePHP\Internal\Resolver\SpecialTypeResolver; use TypePHP\Internal\Util\Config; use TypePHP\Internal\Util\FileFilter; +use TypePHP\Internal\Util\IgnoreManager; use TypePHP\Internal\Util\StubManager; use TypePHP\Internal\Validator\TypeValidatorRegistry; @@ -764,7 +765,7 @@ private static function extractRawParamName(object $paramNode): string private static function shouldIgnoreDoc(string $doc): bool { - return Config::isRespectIgnoreTagsEnabled() && (str_contains($doc, '@typephp-ignore') || str_contains($doc, '@typephp-disable')); + return Config::isRespectIgnoreTagsEnabled() && IgnoreManager::hasIgnoreDocTag($doc); } /** @@ -1626,7 +1627,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof CallableTypeNode) { $parameters = array_map( - fn (CallableTypeParameterNode $param) => new CallableTypeParameterNode( + fn(CallableTypeParameterNode $param) => new CallableTypeParameterNode( self::substituteAliases($param->type, $aliases), $param->isReference, $param->isVariadic, @@ -1660,7 +1661,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof GenericTypeNode) { $genericType = self::substituteAliases($node->type, $aliases); $genericTypes = array_map( - fn ($t) => self::substituteAliases($t, $aliases), + fn($t) => self::substituteAliases($t, $aliases), $node->genericTypes ); @@ -1677,7 +1678,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof UnionTypeNode) { $types = array_map( - fn ($t) => self::substituteAliases($t, $aliases), + fn($t) => self::substituteAliases($t, $aliases), $node->types ); @@ -1692,7 +1693,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof IntersectionTypeNode) { $types = array_map( - fn ($t) => self::substituteAliases($t, $aliases), + fn($t) => self::substituteAliases($t, $aliases), $node->types ); diff --git a/src/Internal/Util/IgnoreManager.php b/src/Internal/Util/IgnoreManager.php index 639dd4fa..d25e1379 100644 --- a/src/Internal/Util/IgnoreManager.php +++ b/src/Internal/Util/IgnoreManager.php @@ -99,7 +99,20 @@ public static function isCallerIgnored(?string $callerClass = null, ?string $cal $function = $frame['function']; $file = $frame['file'] ?? ''; - if ($class !== '' && (str_starts_with($class, 'TypePHP\\Internal\\') || $class === 'TypePHP\\TypePHP')) { + // Skip internal library frames and test runner framework internals + if ( + $class !== '' && ( + str_starts_with($class, 'TypePHP\\Internal\\') + || $class === 'TypePHP\\TypePHP' + || str_starts_with($class, 'PHPUnit\\') + || str_starts_with($class, 'Pest\\') + || str_starts_with($class, 'P\\') + ) + ) { + continue; + } + + if (str_starts_with($function, '__pest_')) { continue; } @@ -146,18 +159,37 @@ public static function isCallerIgnored(?string $callerClass = null, ?string $cal return false; } + /** + * Checks if a docblock contains a standalone @typephp-ignore or @typephp-disable tag. + */ + public static function hasIgnoreDocTag(string|false|null $doc): bool + { + if ($doc === null || $doc === false || $doc === '') { + return false; + } + + if (! str_contains($doc, '@typephp-ignore') && ! str_contains($doc, '@typephp-disable')) { + return false; + } + + return (bool) preg_match( + '/(?:^\s*\*\s*|\/\*\*\s*|(?:\/\/|#)\s*)@(typephp-ignore|typephp-disable)(?:\s|\*\/|$)/m', + $doc + ); + } + private static function checkMethodIgnored(string $class, string $method): bool { if (StubManager::hasMethodStub($class, $method)) { $stubDoc = StubManager::getMethodDoc($class, $method); - if ($stubDoc !== null && (str_contains($stubDoc, '@typephp-ignore') || str_contains($stubDoc, '@typephp-disable'))) { + if (self::hasIgnoreDocTag($stubDoc)) { return true; } } if (StubManager::hasClassStub($class)) { $classStubDoc = StubManager::getClassDoc($class); - if ($classStubDoc !== null && (str_contains($classStubDoc, '@typephp-ignore') || str_contains($classStubDoc, '@typephp-disable'))) { + if (self::hasIgnoreDocTag($classStubDoc)) { return true; } } @@ -172,20 +204,12 @@ private static function checkMethodIgnored(string $class, string $method): bool if ($refClass->hasMethod($method)) { $refMethod = $refClass->getMethod($method); - $doc = $refMethod->getDocComment(); - if ($doc !== false && $doc !== null && ( - str_contains($doc, '@typephp-ignore') - || str_contains($doc, '@typephp-disable') - )) { + if (self::hasIgnoreDocTag($refMethod->getDocComment())) { return true; } } - $classDoc = $refClass->getDocComment(); - if ($classDoc !== false && $classDoc !== null && ( - str_contains($classDoc, '@typephp-ignore') - || str_contains($classDoc, '@typephp-disable') - )) { + if (self::hasIgnoreDocTag($refClass->getDocComment())) { return true; } } catch (Throwable $e) { @@ -199,7 +223,7 @@ private static function checkFunctionIgnored(string $function): bool { if (StubManager::hasFunctionStub($function)) { $stubDoc = StubManager::getFunctionDoc($function); - if ($stubDoc !== null && (str_contains($stubDoc, '@typephp-ignore') || str_contains($stubDoc, '@typephp-disable'))) { + if (self::hasIgnoreDocTag($stubDoc)) { return true; } } @@ -210,11 +234,7 @@ private static function checkFunctionIgnored(string $function): bool try { $refFunc = new ReflectionFunction($function); - $doc = $refFunc->getDocComment(); - if ($doc !== false && $doc !== null && ( - str_contains($doc, '@typephp-ignore') - || str_contains($doc, '@typephp-disable') - )) { + if (self::hasIgnoreDocTag($refFunc->getDocComment())) { return true; } } catch (Throwable $e) { @@ -223,4 +243,4 @@ private static function checkFunctionIgnored(string $function): bool return false; } -} +} \ No newline at end of file From f5964257032ab874edca53c22fb684ca0381f7d2 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 21 Sep 2026 03:16:59 +0800 Subject: [PATCH 10/10] fix code styling --- src/Internal/Docblock/DocblockParser.php | 8 ++++---- src/Internal/Util/IgnoreManager.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Internal/Docblock/DocblockParser.php b/src/Internal/Docblock/DocblockParser.php index dfb2667e..105a3ebe 100644 --- a/src/Internal/Docblock/DocblockParser.php +++ b/src/Internal/Docblock/DocblockParser.php @@ -1627,7 +1627,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof CallableTypeNode) { $parameters = array_map( - fn(CallableTypeParameterNode $param) => new CallableTypeParameterNode( + fn (CallableTypeParameterNode $param) => new CallableTypeParameterNode( self::substituteAliases($param->type, $aliases), $param->isReference, $param->isVariadic, @@ -1661,7 +1661,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof GenericTypeNode) { $genericType = self::substituteAliases($node->type, $aliases); $genericTypes = array_map( - fn($t) => self::substituteAliases($t, $aliases), + fn ($t) => self::substituteAliases($t, $aliases), $node->genericTypes ); @@ -1678,7 +1678,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof UnionTypeNode) { $types = array_map( - fn($t) => self::substituteAliases($t, $aliases), + fn ($t) => self::substituteAliases($t, $aliases), $node->types ); @@ -1693,7 +1693,7 @@ public static function substituteAliases(TypeNode $node, array $aliases): TypeNo if ($node instanceof IntersectionTypeNode) { $types = array_map( - fn($t) => self::substituteAliases($t, $aliases), + fn ($t) => self::substituteAliases($t, $aliases), $node->types ); diff --git a/src/Internal/Util/IgnoreManager.php b/src/Internal/Util/IgnoreManager.php index d25e1379..56e97a23 100644 --- a/src/Internal/Util/IgnoreManager.php +++ b/src/Internal/Util/IgnoreManager.php @@ -243,4 +243,4 @@ private static function checkFunctionIgnored(string $function): bool return false; } -} \ No newline at end of file +}