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
*/