Skip to content

Commit

Permalink
Allow ability to memoize empty method calls based on config
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed Apr 28, 2018
1 parent a0984cd commit da62092
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 0 deletions.
1 change: 1 addition & 0 deletions config.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<xs:attribute name="allowCoercionFromStringToClassConst" type="xs:string" />
<xs:attribute name="allowStringToStandInForClass" type="xs:string" />
<xs:attribute name="usePhpDocMethodsWithoutMagicCall" type="xs:string" />
<xs:attribute name="memoizeMethodCallResults" type="xs:string" />
</xs:complexType>

<xs:complexType name="ProjectFilesType">
Expand Down
21 changes: 21 additions & 0 deletions src/Psalm/Checker/Statements/Expression/AssertionFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
10 changes: 10 additions & 0 deletions src/Psalm/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ class Config
*/
public $use_phpdoc_methods_without_call = false;

/**
* @var bool
*/
public $memoize_method_calls = false;

/**
* @var string[]
*/
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions src/Psalm/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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
) {
Expand Down
41 changes: 41 additions & 0 deletions tests/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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__),
'<?xml version="1.0"?>
<psalm memoizeMethodCallResults="true">
<projectFiles>
<directory name="src" />
</projectFiles>
</psalm>'
)
);

$file_path = getcwd() . '/src/somefile.php';

$this->addFile(
$file_path,
'<?php
class A {
function getFoo() : ?Foo {
return rand(0, 1) ? new Foo : null;
}
}
class Foo {
public function bar() : void {}
};
$a = new A();
if ($a->getFoo()) {
$a->getFoo()->bar();
}'
);

$this->analyzeFile($file_path, new Context());
}

/**
* @return void
*/
Expand Down

0 comments on commit da62092

Please sign in to comment.