Skip to content

Commit

Permalink
Fix #3236 - allow use-checking of more methods starting with __
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed May 13, 2020
1 parent 3f3a226 commit 2af0a17
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 58 deletions.
Expand Up @@ -354,8 +354,7 @@ public static function analyzeInstance(

$set_method_id = new \Psalm\Internal\MethodIdentifier($fq_class_name, '__set');

if ($codebase->methods->methodExists($set_method_id)
&& (!$codebase->properties->propertyExists($property_id, false, $statements_analyzer, $context)
if ((!$codebase->properties->propertyExists($property_id, false, $statements_analyzer, $context)
|| ($lhs_var_id !== '$this'
&& $fq_class_name !== $context->self
&& ClassLikeAnalyzer::checkPropertyVisibility(
Expand All @@ -367,6 +366,18 @@ public static function analyzeInstance(
false
) !== true)
)
&& $codebase->methods->methodExists(
$set_method_id,
$context->calling_method_id,
$codebase->collect_locations
? new CodeLocation($statements_analyzer->getSource(), $stmt)
: null,
!$context->collect_initializations
&& !$context->collect_mutations
? $statements_analyzer
: null,
$statements_analyzer->getFilePath()
)
) {
$has_magic_setter = true;
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
Expand Down
Expand Up @@ -1847,21 +1847,32 @@ public static function analyzeConcatOp(
}
}

if ($context->mutation_free) {
foreach ($left_type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
foreach ($left_type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
$to_string_method_id = new \Psalm\Internal\MethodIdentifier(
$atomic_type->value,
'__tostring'
);

if ($codebase->methods->methodExists(
$to_string_method_id,
$context->calling_method_id,
$codebase->collect_locations
? new CodeLocation($statements_analyzer->getSource(), $left)
: null,
!$context->collect_initializations
&& !$context->collect_mutations
? $statements_analyzer
: null,
$statements_analyzer->getFilePath()
)) {
try {
$storage = $codebase->methods->getStorage(
new \Psalm\Internal\MethodIdentifier(
$atomic_type->value,
'__tostring'
)
);
$storage = $codebase->methods->getStorage($to_string_method_id);
} catch (\UnexpectedValueException $e) {
continue;
}

if (!$storage->mutation_free) {
if ($context->mutation_free && !$storage->mutation_free) {
if (IssueBuffer::accepts(
new ImpureMethodCall(
'Cannot call a possibly-mutating method '
Expand Down Expand Up @@ -1923,21 +1934,32 @@ public static function analyzeConcatOp(
}
}

if ($context->mutation_free) {
foreach ($right_type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
foreach ($right_type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
$to_string_method_id = new \Psalm\Internal\MethodIdentifier(
$atomic_type->value,
'__tostring'
);

if ($codebase->methods->methodExists(
$to_string_method_id,
$context->calling_method_id,
$codebase->collect_locations
? new CodeLocation($statements_analyzer->getSource(), $right)
: null,
!$context->collect_initializations
&& !$context->collect_mutations
? $statements_analyzer
: null,
$statements_analyzer->getFilePath()
)) {
try {
$storage = $codebase->methods->getStorage(
new \Psalm\Internal\MethodIdentifier(
$atomic_type->value,
'__tostring'
)
);
$storage = $codebase->methods->getStorage($to_string_method_id);
} catch (\UnexpectedValueException $e) {
continue;
}

if (!$storage->mutation_free) {
if ($context->mutation_free && !$storage->mutation_free) {
if (IssueBuffer::accepts(
new ImpureMethodCall(
'Cannot call a possibly-mutating method '
Expand Down
Expand Up @@ -355,7 +355,15 @@ public static function analyze(
if (!$interface_has_method
&& $codebase->methods->methodExists(
new MethodIdentifier($fq_class_name, '__call'),
$context->calling_method_id
$context->calling_method_id,
$codebase->collect_locations
? new CodeLocation($source, $stmt->name)
: null,
!$context->collect_initializations
&& !$context->collect_mutations
? $statements_analyzer
: null,
$statements_analyzer->getFilePath()
)
) {
$new_call_context = MissingMethodCallHandler::handleMagicMethod(
Expand Down
Expand Up @@ -529,8 +529,7 @@ public static function analyzeInstance(
}
}

if ($codebase->methods->methodExists($get_method_id)
&& (!$naive_property_exists
if ((!$naive_property_exists
|| ($stmt_var_id !== '$this'
&& $fq_class_name !== $context->self
&& ClassLikeAnalyzer::checkPropertyVisibility(
Expand All @@ -542,6 +541,18 @@ public static function analyzeInstance(
false
) !== true)
)
&& $codebase->methods->methodExists(
$get_method_id,
$context->calling_method_id,
$codebase->collect_locations
? new CodeLocation($statements_analyzer->getSource(), $stmt)
: null,
!$context->collect_initializations
&& !$context->collect_mutations
? $statements_analyzer
: null,
$statements_analyzer->getFilePath()
)
) {
$has_magic_getter = true;

Expand Down
50 changes: 26 additions & 24 deletions src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php
Expand Up @@ -612,7 +612,7 @@ public static function analyze(
}

if ($statements_analyzer->node_data->getType($stmt->expr)) {
$stmt_type = self::castStringAttempt($statements_analyzer, $stmt->expr, true);
$stmt_type = self::castStringAttempt($statements_analyzer, $context, $stmt->expr, true);
} else {
$stmt_type = Type::getString();
}
Expand Down Expand Up @@ -1677,7 +1677,7 @@ protected static function analyzeEncapsulatedString(
}

if ($statements_analyzer->node_data->getType($part)) {
self::castStringAttempt($statements_analyzer, $part);
self::castStringAttempt($statements_analyzer, $context, $part);
}
}

Expand All @@ -1688,6 +1688,7 @@ protected static function analyzeEncapsulatedString(

private static function castStringAttempt(
StatementsAnalyzer $statements_analyzer,
Context $context,
PhpParser\Node\Expr $stmt,
bool $explicit_cast = false
) : Type\Union {
Expand Down Expand Up @@ -1730,32 +1731,33 @@ private static function castStringAttempt(
}

foreach ($intersection_types as $intersection_type) {
if ($intersection_type instanceof TNamedObject
&& $codebase->methods->methodExists(
new \Psalm\Internal\MethodIdentifier(
$intersection_type->value,
'__tostring'
)
)
) {
$return_type = $codebase->methods->getMethodReturnType(
new \Psalm\Internal\MethodIdentifier(
$intersection_type->value,
'__tostring'
),
$self_class
if ($intersection_type instanceof TNamedObject) {
$intersection_method_id = new \Psalm\Internal\MethodIdentifier(
$intersection_type->value,
'__tostring'
);

if ($return_type) {
$castable_types = array_merge(
$castable_types,
array_values($return_type->getAtomicTypes())
if ($codebase->methods->methodExists(
$intersection_method_id,
$context->calling_method_id,
new CodeLocation($statements_analyzer->getSource(), $stmt)
)) {
$return_type = $codebase->methods->getMethodReturnType(
$intersection_method_id,
$self_class
);
} else {
$castable_types[] = new TString();
}

continue 2;
if ($return_type) {
$castable_types = array_merge(
$castable_types,
array_values($return_type->getAtomicTypes())
);
} else {
$castable_types[] = new TString();
}

continue 2;
}
}

if ($intersection_type instanceof Type\Atomic\TObjectWithProperties
Expand Down
13 changes: 11 additions & 2 deletions src/Psalm/Internal/Codebase/ClassLikes.php
Expand Up @@ -1866,7 +1866,17 @@ private function checkMethodReferences(ClassLikeStorage $classlike_storage, Meth
);

if (!$method_referenced
&& (substr($method_name, 0, 2) !== '__' || $method_name === '__construct')
&& $method_name !== '__destruct'
&& $method_name !== '__clone'
&& $method_name !== '__invoke'
&& $method_name !== '__unset'
&& $method_name !== '__sleep'
&& $method_name !== '__wakeup'
&& $method_name !== '__serialize'
&& $method_name !== '__unserialize'
&& $method_name !== '__set_state'
&& $method_name !== '__debuginfo'
&& $method_name !== '__tostring' // can be called in array_unique
&& $method_storage->location
) {
$method_location = $method_storage->location;
Expand Down Expand Up @@ -2188,7 +2198,6 @@ private function checkPropertyReferences(ClassLikeStorage $classlike_storage)
}

if ((!$property_referenced || $property_constructor_referenced)
&& (substr($property_name, 0, 2) !== '__' || $property_name === '__construct')
&& $property_storage->location
) {
$property_id = $classlike_storage->name . '::$' . $property_name;
Expand Down
7 changes: 0 additions & 7 deletions src/Psalm/Storage/FunctionLikeParameter.php
Expand Up @@ -112,13 +112,6 @@ public function __construct(
$this->default_type = $default_type;
}

public function __toString()
{
return ($this->type ?: 'mixed')
. ($this->is_variadic ? '...' : '')
. ($this->is_optional ? '=' : '');
}

public function getId() : string
{
return ($this->type ? $this->type->getId() : 'mixed')
Expand Down

0 comments on commit 2af0a17

Please sign in to comment.