diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 14f91cdd161..898b4143837 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -163,6 +163,24 @@ parameters: count: 1 path: src/Analyser/StmtHandler/ClassLikeHandler.php + - + rawMessage: ''' + Call to method __construct() of deprecated class PHPStan\Node\CatchWithUnthrownExceptionNode: + Use CatchTypeNode, emitted for every catch type, and check isThrown(). + ''' + identifier: method.deprecatedClass + count: 1 + path: src/Analyser/StmtHandler/TryCatchHandler.php + + - + rawMessage: ''' + Instantiation of deprecated class PHPStan\Node\CatchWithUnthrownExceptionNode: + Use CatchTypeNode, emitted for every catch type, and check isThrown(). + ''' + identifier: new.deprecatedClass + count: 1 + path: src/Analyser/StmtHandler/TryCatchHandler.php + - rawMessage: 'Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\Collectors\Collector::processNode().' identifier: generics.variance diff --git a/src/Analyser/StmtHandler/TryCatchHandler.php b/src/Analyser/StmtHandler/TryCatchHandler.php index c41c2168982..fdf8b4ad7f9 100644 --- a/src/Analyser/StmtHandler/TryCatchHandler.php +++ b/src/Analyser/StmtHandler/TryCatchHandler.php @@ -21,6 +21,7 @@ use PHPStan\Analyser\VariableFlowBuilder; use PHPStan\Dependency\Dependencies; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\CatchTypeNode; use PHPStan\Node\CatchWithUnthrownExceptionNode; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\FinallyExitPointsNode; @@ -112,9 +113,11 @@ public function processStmt( } $dependencies[] = Dependencies::create($scope->getFile(), classNames: $caughtClassNames); + $catchTypeNodes = []; $originalCatchTypes = []; $catchTypes = []; foreach ($catchNode->types as $catchNodeType) { + $catchTypeNodes[] = $catchNodeType; $catchType = new ObjectType($catchNodeType->toString()); $originalCatchTypes[] = $catchType; $catchTypes[] = TypeCombinator::remove($catchType, $pastCatchTypes); @@ -207,6 +210,7 @@ public function processStmt( // emit error foreach ($matchingCatchTypes as $catchTypeIndex => $matched) { + $nodeScopeResolver->callNodeCallback($nodeCallback, new CatchTypeNode($catchNode, $catchTypeNodes[$catchTypeIndex], $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex], $matched), $scope, $storage); if ($matched) { continue; } diff --git a/src/Node/CatchTypeNode.php b/src/Node/CatchTypeNode.php new file mode 100644 index 00000000000..8ded0b8f11e --- /dev/null +++ b/src/Node/CatchTypeNode.php @@ -0,0 +1,79 @@ +getAttributes()); + } + + public function getOriginalNode(): Catch_ + { + return $this->originalNode; + } + + public function getTypeNode(): Name + { + return $this->typeNode; + } + + /** + * The caught type without the types caught by the preceding catch clauses; + * NeverType when they already catch all of it. + */ + public function getCaughtType(): Type + { + return $this->caughtType; + } + + public function getOriginalCaughtType(): Type + { + return $this->originalCaughtType; + } + + public function isThrown(): bool + { + return $this->thrown; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_CatchTypeNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/CatchWithUnthrownExceptionNode.php b/src/Node/CatchWithUnthrownExceptionNode.php index 8d9c17f5f05..c161e67741c 100644 --- a/src/Node/CatchWithUnthrownExceptionNode.php +++ b/src/Node/CatchWithUnthrownExceptionNode.php @@ -10,6 +10,7 @@ /** * @api + * @deprecated Use CatchTypeNode, emitted for every catch type, and check isThrown(). */ #[ReferencedByTurboExtension(key: 'catchWithUnthrownExceptionNode')] final class CatchWithUnthrownExceptionNode extends NodeAbstract implements VirtualNode diff --git a/src/Rules/Comparison/ConstantConditionInTraitHelper.php b/src/Rules/Comparison/ConstantConditionInTraitHelper.php index 145c48919c1..3918e55f000 100644 --- a/src/Rules/Comparison/ConstantConditionInTraitHelper.php +++ b/src/Rules/Comparison/ConstantConditionInTraitHelper.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PhpParser\Node\Name; use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\RuleErrorTransformer; @@ -27,9 +28,9 @@ public function __construct( { } - private function exprString(Expr $expr): string + private function exprString(Expr|Name $expr): string { - return sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine()); + return sprintf('%s:%d', $expr instanceof Name ? $expr->toString() : $this->exprPrinter->printExpr($expr), $expr->getStartLine()); } /** @@ -38,7 +39,7 @@ private function exprString(Expr $expr): string public function emitNoError( string $ruleName, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, - Expr $expr, + Expr|Name $expr, ): void { if (!$scope->isInTrait()) { @@ -59,7 +60,7 @@ public function emitNoError( public function emitError( string $ruleName, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, - Expr $expr, + Expr|Name $expr, bool $value, RuleError $ruleError, ): void diff --git a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php index f4e2479a2ad..b9747e86a10 100644 --- a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php +++ b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php @@ -3,10 +3,13 @@ namespace PHPStan\Rules\Exceptions; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; -use PHPStan\Node\CatchWithUnthrownExceptionNode; +use PHPStan\Node\CatchTypeNode; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; @@ -14,7 +17,7 @@ use function sprintf; /** - * @implements Rule + * @implements Rule */ #[RegisteredRule(level: 4)] final class CatchWithUnthrownExceptionRule implements Rule @@ -23,6 +26,7 @@ final class CatchWithUnthrownExceptionRule implements Rule public function __construct( #[AutowiredParameter(ref: '@exceptionTypeResolver')] private ExceptionTypeResolver $exceptionTypeResolver, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter(ref: '%exceptions.reportUncheckedExceptionDeadCatch%')] private bool $reportUncheckedExceptionDeadCatch, ) @@ -31,44 +35,55 @@ public function __construct( public function getNodeType(): string { - return CatchWithUnthrownExceptionNode::class; + return CatchTypeNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { - if ($node->getCaughtType() instanceof NeverType) { - return [ - RuleErrorBuilder::message( - sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())), - ) - ->line($node->getStartLine()) - ->identifier('catch.alreadyCaught') - ->build(), - ]; + if ($node->isThrown()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getTypeNode()); + return []; } - if (!$this->reportUncheckedExceptionDeadCatch) { - $isCheckedException = false; - foreach ($node->getCaughtType()->getObjectClassNames() as $objectClassName) { - if ($this->exceptionTypeResolver->isCheckedException($objectClassName, $scope)) { - $isCheckedException = true; - break; + if ($node->getCaughtType() instanceof NeverType) { + $ruleError = RuleErrorBuilder::message( + sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())), + ) + ->line($node->getStartLine()) + ->identifier('catch.alreadyCaught') + ->build(); + } else { + if (!$this->reportUncheckedExceptionDeadCatch) { + $isCheckedException = false; + foreach ($node->getCaughtType()->getObjectClassNames() as $objectClassName) { + if ($this->exceptionTypeResolver->isCheckedException($objectClassName, $scope)) { + $isCheckedException = true; + break; + } } - } - if (!$isCheckedException) { - return []; + if (!$isCheckedException) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getTypeNode()); + return []; + } } - } - return [ - RuleErrorBuilder::message( + $ruleError = RuleErrorBuilder::message( sprintf('Dead catch - %s is never thrown in the try block.', $node->getCaughtType()->describe(VerbosityLevel::typeOnly())), ) ->line($node->getStartLine()) ->identifier('catch.neverThrown') - ->build(), - ]; + ->build(); + } + + if ($scope->isInTrait()) { + // The message already distinguishes the possible outcomes, + // so the contexts only need to be told apart by error/no error. + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->getTypeNode(), true, $ruleError); + return []; + } + + return [$ruleError]; } } diff --git a/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php index 1052fa1991a..91276cd48e3 100644 --- a/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php +++ b/tests/PHPStan/Rules/Exceptions/AbilityToDisableImplicitThrowsTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Exceptions; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -21,7 +22,7 @@ protected function getRule(): Rule [], [], [], - ), true); + ), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhp73Test.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhp73Test.php index 62a4081ceaf..0ea8f001637 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhp73Test.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhp73Test.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Exceptions; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -19,7 +20,7 @@ protected function getRule(): Rule [], [], [], - ), true); + ), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true); } public function testRuleInPhpVersionRange(): void diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php index 71bea413312..1e394768292 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleConfigPhpTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Exceptions; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; @@ -20,7 +21,7 @@ protected function getRule(): Rule [], [], [], - ), true); + ), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true); } public function testThrowTypesInPhpVersionRange(): void diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php index af9dbc277b8..40bf31d62b8 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleGetClassConfigPhpTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Exceptions; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -19,7 +20,7 @@ protected function getRule(): Rule [], [], [], - ), true); + ), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true); } public function testGetClassThrowTypeInPhpVersionRange(): void diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php index 421fdb719d0..d0af9bda36b 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleStubsTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Exceptions; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -19,7 +20,7 @@ protected function getRule(): Rule [], [], [], - ), true); + ), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index d264aac9a30..ba655b4b01b 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -4,12 +4,15 @@ use Error; use InvalidArgumentException; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; +use PHPStan\Rules\Comparison\ConstantConditionInTraitRule; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class CatchWithUnthrownExceptionRuleTest extends RuleTestCase { @@ -26,13 +29,41 @@ public static function getComposerAutoloaderProjectPaths(): array protected function getRule(): Rule { - return new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( - self::createReflectionProvider(), - [], - $this->uncheckedExceptionClasses, - [], - [], - ), $this->reportUncheckedExceptionDeadCatch); + // @phpstan-ignore argument.type + return new CompositeRule([ + new CatchWithUnthrownExceptionRule(new DefaultExceptionTypeResolver( + self::createReflectionProvider(), + [], + $this->uncheckedExceptionClasses, + [], + [], + ), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->reportUncheckedExceptionDeadCatch), + new ConstantConditionInTraitRule(), + ]); + } + + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/dead-catch-in-trait.php'], [ + [ + 'Dead catch - DeadCatchInTrait\BarException is never thrown in the try block.', + 33, + ], + [ + 'Dead catch - DeadCatchInTrait\BarException is never thrown in the try block.', + 43, + ], + [ + 'Dead catch - DeadCatchInTrait\FooException is already caught above.', + 55, + ], + ]); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug10315(): void + { + $this->analyse([__DIR__ . '/data/bug-10315.php'], []); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-10315.php b/tests/PHPStan/Rules/Exceptions/data/bug-10315.php new file mode 100644 index 00000000000..055b98e0247 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-10315.php @@ -0,0 +1,47 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug10315; + +trait DriverPoolAbstractTrait +{ + /** + * @return array + */ + protected function driverReadMultiple(): array + { + try { + return $this->doThings(); + } catch (\Exception) { + return []; + } + } +} + +class Foo +{ + use DriverPoolAbstractTrait; + + /** + * @return array + * @throws void + */ + public function doThings(): array + { + return []; + } +} + +class Foo2 +{ + use DriverPoolAbstractTrait; + + /** + * @throws \Exception + */ + public function doThings(): never + { + throw new \Exception(); + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/dead-catch-in-trait.php b/tests/PHPStan/Rules/Exceptions/data/dead-catch-in-trait.php new file mode 100644 index 00000000000..441a8bfce8a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/dead-catch-in-trait.php @@ -0,0 +1,90 @@ +doBar(); + } catch (FooException $e) { + + } + } + + public function doFoo2(): void + { + // never thrown + try { + $this->doBar(); + } catch (BarException $e) { + + } + } + + public function doFoo3(): void + { + // FooException sometimes thrown, BarException never + try { + $this->doBar(); + } catch (FooException | BarException $e) { + + } + } + + public function doFoo4(): void + { + // already caught above in every context + try { + $this->doBar(); + } catch (\Exception $e) { + + } catch (FooException $e) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + /** + * @throws FooException + */ + public function doBar(): void + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + /** + * @throws void + */ + public function doBar(): void + { + + } + +} diff --git a/turbo-ext/src/TryCatchHandler.cpp b/turbo-ext/src/TryCatchHandler.cpp index 78819f6485a..fa15903f4d7 100644 --- a/turbo-ext/src/TryCatchHandler.cpp +++ b/turbo-ext/src/TryCatchHandler.cpp @@ -266,6 +266,7 @@ class TryCatchHandler if (UNEXPECTED(!pt_node_scope_resolver_call_node_callback(nodeScopeResolver, nodeCallback, catchNode, scope, storage))) return zv::Val(); if (UNEXPECTED(!pushCaughtClassDependencies(dependencies, catchNode, scope))) return zv::Val(); + zv::Arr catchTypeNodes = zv::Arr::empty(); zv::Arr originalCatchTypes = zv::Arr::empty(); zv::Arr catchTypes = zv::Arr::empty(); { @@ -289,6 +290,7 @@ class TryCatchHandler zend_type_error("PHPStan\\Type\\ObjectType::__construct(): Argument #1 ($className) must be of type string, %s given", zend_zval_value_name(className.raw())); return zv::Val(); } + catchTypeNodes.push(zv::Ref(catchNodeType)); zv::Val catchType = newObjectType(Z_STR_P(className.raw())); if (UNEXPECTED(catchType.isUndef())) return zv::Val(); originalCatchTypes.push(zv::Ref(catchType.raw())); @@ -427,9 +429,16 @@ class TryCatchHandler // emit error for (uint32_t catchTypeIndex = 0; catchTypeIndex < catchTypeCount; catchTypeIndex++) { - if (matchingCatchTypes[catchTypeIndex]) continue; zval *caughtType = zend_hash_index_find(catchTypes.table(), catchTypeIndex); zval *originalCaughtType = zend_hash_index_find(originalCatchTypes.table(), catchTypeIndex); + { + zv::Val thrown = zv::Val::boolean(matchingCatchTypes[catchTypeIndex]); + zv::Args typeNodeArgv{catchNode, zend_hash_index_find(catchTypeNodes.table(), catchTypeIndex), caughtType, originalCaughtType, thrown.raw()}; + zv::Val catchTypeNode = pt_type_new(PT_CLASS_CATCH_TYPE_NODE, 5, typeNodeArgv); + if (UNEXPECTED(catchTypeNode.isUndef())) return zv::Val(); + if (UNEXPECTED(!pt_node_scope_resolver_call_node_callback(nodeScopeResolver, nodeCallback, catchTypeNode.raw(), scope, storage))) return zv::Val(); + } + if (matchingCatchTypes[catchTypeIndex]) continue; zv::Args nodeArgv{catchNode, caughtType, originalCaughtType}; zv::Val unthrownNode = pt_type_new(PT_CLASS_CATCH_WITH_UNTHROWN_EXCEPTION_NODE, 3, nodeArgv); if (UNEXPECTED(unthrownNode.isUndef())) return zv::Val(); diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index 315fde4f690..8869fbea4a0 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -340,6 +340,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_DO_WHILE_LOOP_CONDITION_NODE */ {"doWhileLoopConditionNode", "PHPStan\\Node\\DoWhileLoopConditionNode"}, /* PT_CLASS_SWITCH_CONDITION_ARM */ {"switchConditionArm", "PHPStan\\Node\\SwitchConditionArm"}, /* PT_CLASS_SWITCH_CONDITION_NODE */ {"switchConditionNode", "PHPStan\\Node\\SwitchConditionNode"}, + /* PT_CLASS_CATCH_TYPE_NODE */ {"catchTypeNode", "PHPStan\\Node\\CatchTypeNode"}, /* PT_CLASS_CATCH_WITH_UNTHROWN_EXCEPTION_NODE */ {"catchWithUnthrownExceptionNode", "PHPStan\\Node\\CatchWithUnthrownExceptionNode"}, /* PT_CLASS_FINALLY_EXIT_POINTS_NODE */ {"finallyExitPointsNode", "PHPStan\\Node\\FinallyExitPointsNode"}, /* PT_CLASS_IN_FOREACH_NODE */ {"inForeachNode", "PHPStan\\Node\\InForeachNode"}, diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 4646c3696ff..d22b6259ec6 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -435,6 +435,7 @@ enum { PT_CLASS_DO_WHILE_LOOP_CONDITION_NODE, PT_CLASS_SWITCH_CONDITION_ARM, PT_CLASS_SWITCH_CONDITION_NODE, + PT_CLASS_CATCH_TYPE_NODE, PT_CLASS_CATCH_WITH_UNTHROWN_EXCEPTION_NODE, PT_CLASS_FINALLY_EXIT_POINTS_NODE, /* ForeachHandler.cpp */