diff --git a/config.xsd b/config.xsd index 4caa065d2e5..a2e2e9a6fe1 100644 --- a/config.xsd +++ b/config.xsd @@ -36,6 +36,7 @@ + diff --git a/src/Psalm/Checker/Statements/Expression/AssertionFinder.php b/src/Psalm/Checker/Statements/Expression/AssertionFinder.php index 750349d0690..36c8305c3a3 100644 --- a/src/Psalm/Checker/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Checker/Statements/Expression/AssertionFinder.php @@ -83,6 +83,27 @@ public static function getAssertions( return $if_types; } + if ($conditional instanceof PhpParser\Node\Expr\MethodCall + && $conditional->name instanceof PhpParser\Node\Identifier + && !$conditional->args + ) { + $config = \Psalm\Config::getInstance(); + + if ($config->memoize_method_calls) { + $lhs_var_name = ExpressionChecker::getArrayVarId( + $conditional->var, + $this_class_name, + $source + ); + + if ($lhs_var_name) { + $method_var_id = $lhs_var_name . '->' . strtolower($conditional->name->name) . '()'; + $if_types[$method_var_id] = '!falsy'; + return $if_types; + } + } + } + if ($conditional instanceof PhpParser\Node\Expr\BooleanNot) { $if_types_to_negate = self::getAssertions( $conditional->expr, diff --git a/src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php b/src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php index cef667d589c..d033df6f698 100644 --- a/src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php +++ b/src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php @@ -582,6 +582,18 @@ public static function analyze( $context->getPhantomClasses() ); } + + if (!$stmt->args && $var_id) { + if ($config->memoize_method_calls) { + $method_var_id = $var_id . '->' . $method_name_lc . '()'; + + if (isset($context->vars_in_scope[$method_var_id])) { + $return_type_candidate = clone $context->vars_in_scope[$method_var_id]; + } else { + $context->vars_in_scope[$method_var_id] = $return_type_candidate; + } + } + } } else { $returns_by_ref = $returns_by_ref @@ -614,8 +626,10 @@ public static function analyze( $method_id, $appearing_method_id, $declaring_method_id, + $var_id, $stmt->args, $code_location, + $context, $file_manipulations, $return_type_candidate ); diff --git a/src/Psalm/Checker/Statements/Expression/Call/StaticCallChecker.php b/src/Psalm/Checker/Statements/Expression/Call/StaticCallChecker.php index 28abb5dc500..c5a89e456a1 100644 --- a/src/Psalm/Checker/Statements/Expression/Call/StaticCallChecker.php +++ b/src/Psalm/Checker/Statements/Expression/Call/StaticCallChecker.php @@ -394,8 +394,10 @@ public static function analyze( $method_id, $appearing_method_id, $declaring_method_id, + null, $stmt->args, $code_location, + $context, $file_manipulations, $return_type_candidate ); diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 7a9e1c7974d..24cc28504c4 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -203,6 +203,11 @@ class Config */ public $use_phpdoc_methods_without_call = false; + /** + * @var bool + */ + public $memoize_method_calls = false; + /** * @var string[] */ @@ -499,6 +504,11 @@ public static function loadFromXML($base_dir, $file_contents) $config->use_phpdoc_methods_without_call = $attribute_text === 'true' || $attribute_text === '1'; } + if (isset($config_xml['memoizeMethodCallResults'])) { + $attribute_text = (string) $config_xml['memoizeMethodCallResults']; + $config->memoize_method_calls = $attribute_text === 'true' || $attribute_text === '1'; + } + if (isset($config_xml->projectFiles)) { $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true); } diff --git a/src/Psalm/Plugin.php b/src/Psalm/Plugin.php index ed24d0ccae5..1df81e6d927 100644 --- a/src/Psalm/Plugin.php +++ b/src/Psalm/Plugin.php @@ -86,7 +86,9 @@ public static function afterClassLikeExistsCheck( /** * @param string $method_id - the method id being checked + * @param string $appearing_method_id - the method id of the class that the method appears in * @param string $declaring_method_id - the method id of the class or trait that declares the method + * @param string|null $var_id - a reference to the LHS of the variable * @param PhpParser\Node\Arg[] $args * @param FileManipulation[] $file_replacements * @@ -95,9 +97,12 @@ public static function afterClassLikeExistsCheck( public static function afterMethodCallCheck( StatementsSource $statements_source, $method_id, + $appearing_method_id, $declaring_method_id, + $var_id, array $args, CodeLocation $code_location, + Context $context, array &$file_replacements = [], Union &$return_type_candidate = null ) { diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 67cedef8fd0..eff2407869e 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -732,6 +732,47 @@ function foo() {}' $this->analyzeFile($file_path, new Context()); } + /** + * @return void + */ + public function testMethodCallMemoize() + { + $this->project_checker = $this->getProjectCheckerWithConfig( + TestConfig::loadFromXML( + dirname(__DIR__), + ' + + + + + ' + ) + ); + + $file_path = getcwd() . '/src/somefile.php'; + + $this->addFile( + $file_path, + 'getFoo()) { + $a->getFoo()->bar(); + }' + ); + + $this->analyzeFile($file_path, new Context()); + } + /** * @return void */