1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 12:55:26 +01:00

Fix #4198 - make analysis of class-string property assignments more robust

This commit is contained in:
Brown 2020-09-20 09:18:27 -04:00
parent cb7065ae07
commit ef3cf67f50
2 changed files with 237 additions and 206 deletions

View File

@ -43,257 +43,277 @@ class StaticPropertyAssignmentAnalyzer
$statements_analyzer
);
$fq_class_name = (string) $statements_analyzer->node_data->getType($stmt->class);
$lhs_type = $statements_analyzer->node_data->getType($stmt->class);
if (!$lhs_type) {
return null;
}
$codebase = $statements_analyzer->getCodebase();
$prop_name = $stmt->name;
if (!$prop_name instanceof PhpParser\Node\Identifier) {
if (ExpressionAnalyzer::analyze($statements_analyzer, $prop_name, $context) === false) {
foreach ($lhs_type->getAtomicTypes() as $lhs_atomic_type) {
if ($lhs_atomic_type instanceof Type\Atomic\TClassString) {
if (!$lhs_atomic_type->as_type) {
continue;
}
$lhs_atomic_type = $lhs_atomic_type->as_type;
}
if (!$lhs_atomic_type instanceof Type\Atomic\TNamedObject) {
continue;
}
$fq_class_name = $lhs_atomic_type->value;
if (!$prop_name instanceof PhpParser\Node\Identifier) {
if (ExpressionAnalyzer::analyze($statements_analyzer, $prop_name, $context) === false) {
return false;
}
if (!$context->ignore_variable_property) {
$codebase->analyzer->addMixedMemberName(
strtolower($fq_class_name) . '::$',
$context->calling_method_id ?: $statements_analyzer->getFileName()
);
}
return null;
}
$property_id = $fq_class_name . '::$' . $prop_name;
if (!$codebase->properties->propertyExists($property_id, false, $statements_analyzer, $context)) {
if (IssueBuffer::accepts(
new UndefinedPropertyAssignment(
'Static property ' . $property_id . ' is not defined',
new CodeLocation($statements_analyzer->getSource(), $stmt),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return null;
}
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
) === false) {
return false;
}
if ($fq_class_name && !$context->ignore_variable_property) {
$codebase->analyzer->addMixedMemberName(
strtolower($fq_class_name) . '::$',
$context->calling_method_id ?: $statements_analyzer->getFileName()
);
}
return null;
}
$property_id = $fq_class_name . '::$' . $prop_name;
if (!$codebase->properties->propertyExists($property_id, false, $statements_analyzer, $context)) {
if (IssueBuffer::accepts(
new UndefinedPropertyAssignment(
'Static property ' . $property_id . ' is not defined',
new CodeLocation($statements_analyzer->getSource(), $stmt),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
return null;
}
if (ClassLikeAnalyzer::checkPropertyVisibility(
$property_id,
$context,
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues()
) === false) {
return false;
}
$declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
$fq_class_name . '::$' . $prop_name->name,
false
);
$declaring_property_id = strtolower((string) $declaring_property_class) . '::$' . $prop_name;
if ($codebase->alter_code && $stmt->class instanceof PhpParser\Node\Name) {
$moved_class = $codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$stmt->class,
$fq_class_name,
$context->calling_method_id
$declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
$fq_class_name . '::$' . $prop_name->name,
false
);
if (!$moved_class) {
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
if ($declaring_property_id === $original_pattern) {
[$old_declaring_fq_class_name] = explode('::$', $declaring_property_id);
[$new_fq_class_name, $new_property_name] = explode('::$', $transformation);
$declaring_property_id = strtolower((string) $declaring_property_class) . '::$' . $prop_name;
$file_manipulations = [];
if ($codebase->alter_code && $stmt->class instanceof PhpParser\Node\Name) {
$moved_class = $codebase->classlikes->handleClassLikeReferenceInMigration(
$codebase,
$statements_analyzer,
$stmt->class,
$fq_class_name,
$context->calling_method_id
);
if (!$moved_class) {
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
if ($declaring_property_id === $original_pattern) {
[$old_declaring_fq_class_name] = explode('::$', $declaring_property_id);
[$new_fq_class_name, $new_property_name] = explode('::$', $transformation);
$file_manipulations = [];
if (strtolower($new_fq_class_name) !== strtolower($old_declaring_fq_class_name)) {
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$statements_analyzer->getNamespace(),
$statements_analyzer->getAliasedClassesFlipped(),
null
)
);
}
if (strtolower($new_fq_class_name) !== strtolower($old_declaring_fq_class_name)) {
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->class->getAttribute('startFilePos'),
(int) $stmt->class->getAttribute('endFilePos') + 1,
Type::getStringFromFQCLN(
$new_fq_class_name,
$statements_analyzer->getNamespace(),
$statements_analyzer->getAliasedClassesFlipped(),
null
)
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
);
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
$file_manipulations[] = new \Psalm\FileManipulation(
(int) $stmt->name->getAttribute('startFilePos'),
(int) $stmt->name->getAttribute('endFilePos') + 1,
'$' . $new_property_name
);
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
}
}
}
$class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
$class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
if ($var_id) {
$context->vars_in_scope[$var_id] = $assignment_value_type;
}
if ($var_id) {
$context->vars_in_scope[$var_id] = $assignment_value_type;
}
$class_property_type = $codebase->properties->getPropertyType(
$property_id,
true,
$statements_analyzer,
$context
);
$class_property_type = $codebase->properties->getPropertyType(
$property_id,
true,
$statements_analyzer,
$context
);
if (!$class_property_type) {
$class_property_type = Type::getMixed();
if (!$class_property_type) {
$class_property_type = Type::getMixed();
$source_analyzer = $statements_analyzer->getSource()->getSource();
$source_analyzer = $statements_analyzer->getSource()->getSource();
$prop_name_name = $prop_name->name;
$prop_name_name = $prop_name->name;
if ($source_analyzer instanceof ClassAnalyzer
&& $fq_class_name === $source_analyzer->getFQCLN()
) {
if (isset($source_analyzer->inferred_property_types[$prop_name_name])) {
$source_analyzer->inferred_property_types[$prop_name_name] = Type::combineUnionTypes(
$assignment_value_type,
$source_analyzer->inferred_property_types[$prop_name_name]
);
if ($source_analyzer instanceof ClassAnalyzer
&& $fq_class_name === $source_analyzer->getFQCLN()
) {
if (isset($source_analyzer->inferred_property_types[$prop_name_name])) {
$source_analyzer->inferred_property_types[$prop_name_name] = Type::combineUnionTypes(
$assignment_value_type,
$source_analyzer->inferred_property_types[$prop_name_name]
);
} else {
$source_analyzer->inferred_property_types[$prop_name_name] = $assignment_value_type;
}
}
} else {
$class_property_type = clone $class_property_type;
}
if ($assignment_value_type->hasMixed()) {
return null;
}
if ($class_property_type->hasMixed()) {
return null;
}
$class_property_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,
$class_property_type,
$fq_class_name,
$fq_class_name,
$class_storage->parent_class
);
$union_comparison_results = new \Psalm\Internal\Type\Comparator\TypeComparisonResult();
$type_match_found = UnionTypeComparator::isContainedBy(
$codebase,
$assignment_value_type,
$class_property_type,
true,
true,
$union_comparison_results
);
if ($union_comparison_results->type_coerced) {
if ($union_comparison_results->type_coerced_from_mixed) {
if (IssueBuffer::accepts(
new MixedPropertyTypeCoercion(
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
. ' parent type `' . $assignment_value_type->getId() . '` provided',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
}
} else {
$source_analyzer->inferred_property_types[$prop_name_name] = $assignment_value_type;
if (IssueBuffer::accepts(
new PropertyTypeCoercion(
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
. ' parent type \'' . $assignment_value_type->getId() . '\' provided',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
}
}
}
} else {
$class_property_type = clone $class_property_type;
}
if ($assignment_value_type->hasMixed()) {
return null;
}
if ($class_property_type->hasMixed()) {
return null;
}
$class_property_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,
$class_property_type,
$fq_class_name,
$fq_class_name,
$class_storage->parent_class
);
$union_comparison_results = new \Psalm\Internal\Type\Comparator\TypeComparisonResult();
$type_match_found = UnionTypeComparator::isContainedBy(
$codebase,
$assignment_value_type,
$class_property_type,
true,
true,
$union_comparison_results
);
if ($union_comparison_results->type_coerced) {
if ($union_comparison_results->type_coerced_from_mixed) {
if ($union_comparison_results->to_string_cast) {
if (IssueBuffer::accepts(
new MixedPropertyTypeCoercion(
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
. ' parent type `' . $assignment_value_type->getId() . '` provided',
new ImplicitToStringCast(
$var_id . ' expects \'' . $class_property_type . '\', '
. '\'' . $assignment_value_type . '\' provided with a __toString method',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
),
$property_id
)
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
}
} else {
if (IssueBuffer::accepts(
new PropertyTypeCoercion(
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
. ' parent type \'' . $assignment_value_type->getId() . '\' provided',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
// keep soldiering on
// fall through
}
}
}
if ($union_comparison_results->to_string_cast) {
if (IssueBuffer::accepts(
new ImplicitToStringCast(
$var_id . ' expects \'' . $class_property_type . '\', '
. '\'' . $assignment_value_type . '\' provided with a __toString method',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt,
$context->include_location
)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
if (!$type_match_found && !$union_comparison_results->type_coerced) {
if (UnionTypeComparator::canBeContainedBy($codebase, $assignment_value_type, $class_property_type)) {
if (IssueBuffer::accepts(
new PossiblyInvalidPropertyAssignmentValue(
$var_id . ' with declared type \''
. $class_property_type->getId() . '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
if (!$type_match_found && !$union_comparison_results->type_coerced) {
if (UnionTypeComparator::canBeContainedBy($codebase, $assignment_value_type, $class_property_type)) {
if (IssueBuffer::accepts(
new PossiblyInvalidPropertyAssignmentValue(
$var_id . ' with declared type \''
. $class_property_type->getId() . '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
),
$property_id
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
new InvalidPropertyAssignmentValue(
$var_id . ' with declared type \'' . $class_property_type->getId()
. '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
new InvalidPropertyAssignmentValue(
$var_id . ' with declared type \'' . $class_property_type->getId()
. '\' cannot be assigned type \''
. $assignment_value_type->getId() . '\'',
new CodeLocation(
$statements_analyzer->getSource(),
$assignment_value ?: $stmt
),
$property_id
),
$property_id
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
}
}
}
if ($var_id) {
$context->vars_in_scope[$var_id] = $assignment_value_type;
if ($var_id) {
$context->vars_in_scope[$var_id] = $assignment_value_type;
}
}
return null;

View File

@ -2063,6 +2063,17 @@ class PropertyTypeTest extends TestCase
if ($a->i === 3) {}
if ($a->i === "foo") {}'
],
'setClassStringOfStatic' => [
'<?php
class A {
public static array $stack = [];
public static function foo() : void {
$class = get_called_class();
$class::$stack[] = 1;
}
}'
],
];
}