Skip to content

begin handling of property hooks #11359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 6.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/Psalm/Internal/Analyzer/ClassAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
use function str_replace;
use function strtolower;
use function substr;
use function ucfirst;

/**
* @internal
Expand Down Expand Up @@ -525,6 +526,17 @@ public function analyze(
}
}
}

foreach ($stmt->hooks as $hook) {
$classMethod = $this->transformHookToClassMethod($stmt, $hook);
$this->analyzeClassMethod(
$classMethod,
$storage,
$this,
$class_context,
$global_context,
);
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
$member_stmts[] = $stmt;

Expand Down Expand Up @@ -2533,4 +2545,50 @@ private function checkEnum(): void
}
}
}

private function transformHookToClassMethod(
PhpParser\Node\Stmt\Property $stmt,
PhpParser\Node\PropertyHook $hook,
): PhpParser\Node\Stmt\ClassMethod {
$hooked_method_name = $hook->name->name . ucfirst($hook->getAttribute('propertyName'));

$fake_method_params = array_map(
static function (FunctionLikeParameter $param): PhpParser\Node\Param {
$fake_param = (new PhpParser\Builder\Param($param->name));
if ($param->signature_type) {
$fake_param->setType((string)$param->signature_type);
}

$node = $fake_param->getNode();

$attributes = $param->location
? [
'startFilePos' => $param->location->raw_file_start,
'endFilePos' => $param->location->raw_file_end,
'startLine' => $param->location->raw_line_number,
]
: [];

$node->setAttributes($attributes);

return $node;
},
$this->storage->methods[strtolower($hooked_method_name)]->params,
);

$fake_constructor_attributes = [
'startLine' => $hook->getLine(),
'startFilePos' => $hook->getAttribute('startFilePos'),
'endFilePos' => $hook->getAttribute('endFilePos'),
];
return new VirtualClassMethod(
new VirtualIdentifier($hooked_method_name),
[
'flags' => PhpParser\Modifiers::PUBLIC,//this may be private?
'params' => $fake_method_params,
'stmts' => $hook->body,
],
$fake_constructor_attributes,
);
}
}
147 changes: 146 additions & 1 deletion src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
use function str_contains;
use function str_starts_with;
use function strtolower;
use function ucfirst;

/**
* @internal
Expand Down Expand Up @@ -534,6 +535,31 @@ public function start(
&& $storage instanceof FunctionStorage
) {
$this->file_storage->functions[$function_id] = $storage;
} elseif ($stmt instanceof PhpParser\Node\PropertyHook
&& $classlike_storage
&& $storage instanceof MethodStorage
&& $method_name_lc
&& !$fake_method
&& $method_id
) {
$classlike_storage->methods[$method_name_lc] = $storage;

$classlike_storage->declaring_method_ids[$method_name_lc]
= $classlike_storage->appearing_method_ids[$method_name_lc]
= $method_id;

$is_private = false;//can this be private? $stmt->isPrivate();
if (!$is_private
|| $method_name_lc === '__construct'
|| $method_name_lc === '__clone'
|| $classlike_storage->is_trait
) {
$classlike_storage->inheritable_method_ids[$method_name_lc] = $method_id;
}

if (!isset($classlike_storage->overridden_method_ids[$method_name_lc])) {
$classlike_storage->overridden_method_ids[$method_name_lc] = [];
}
}

if ($classlike_storage && $method_name_lc === '__construct') {
Expand Down Expand Up @@ -1132,8 +1158,127 @@ private function createStorageForFunctionLike(
}
}
}
} elseif ($stmt instanceof PhpParser\Node\PropertyHook) {
if (!$this->classlike_storage) {
throw new LogicException('$this->classlike_storage should not be null');
}

$fq_classlike_name = $this->classlike_storage->name;
$property_name = $stmt->getAttribute('propertyName');
$property_name_uc_first = ucfirst($property_name);

$method_name_cased = $stmt->name->name . $property_name_uc_first;
$method_name_lc = strtolower($stmt->name->name . $property_name);

$function_id = $fq_classlike_name . '::' . $method_name_lc;
$cased_function_id = $fq_classlike_name . '::' . $method_name_cased;

$classlike_storage = $this->classlike_storage;

$storage = null;

if (isset($classlike_storage->methods[$method_name_lc])) {
if (!$this->codebase->register_stub_files) {
$duplicate_method_storage = $classlike_storage->methods[$method_name_lc];

IssueBuffer::maybeAdd(
new DuplicateMethod(
'Method ' . $function_id . ' has already been defined'
. ($duplicate_method_storage->location
? ' in ' . $duplicate_method_storage->location->file_path
: ''),
new CodeLocation($this->file_scanner, $stmt, null, true),
),
);

$this->file_storage->has_visitor_issues = true;

$duplicate_method_storage->has_visitor_issues = true;

return false;
}

// skip methods based on @since docblock tag
$doc_comment = $stmt->getDocComment();

if ($doc_comment) {
$docblock_info = null;
try {
$code_location = new CodeLocation($this->file_scanner, $stmt, null, true);
$docblock_info = FunctionLikeDocblockParser::parse(
$doc_comment,
$code_location,
$cased_function_id,
);
} catch (IncorrectDocblockException|DocblockParseException) {
}
if ($docblock_info) {
if ($docblock_info->since_php_major_version && !$this->aliases->namespace) {
$analysis_major_php_version = $this->codebase->getMajorAnalysisPhpVersion();
$analysis_minor_php_version = $this->codebase->getMinorAnalysisPhpVersion();
if ($docblock_info->since_php_major_version > $analysis_major_php_version) {
return false;
}

if ($docblock_info->since_php_major_version === $analysis_major_php_version
&& $docblock_info->since_php_minor_version > $analysis_minor_php_version
) {
return false;
}
}
}
}

$is_functionlike_override = true;
$storage = $this->storage = $classlike_storage->methods[$method_name_lc];
}

if (!$storage) {
$storage = $this->storage = new MethodStorage();
}

$storage->stubbed = $this->codebase->register_stub_files;
$storage->defining_fqcln = $fq_classlike_name;

$method_id = new MethodIdentifier(
$fq_classlike_name,
$method_name_lc,
);

$storage->is_static = false;
$storage->abstract = false;

$is_private = false;//can this be private? $stmt->isPrivate();
$is_protected = false;//can this be protected? $stmt->isProtected();
if ($is_private && $stmt->isFinal() && $method_name_lc !== '__construct') {
IssueBuffer::maybeAdd(
new PrivateFinalMethod(
'Private methods cannot be final',
new CodeLocation($this->file_scanner, $stmt, null, true),
(string) $method_id,
),
);
if ($this->codebase->analysis_php_version_id >= 8_00_00) {
// ignore `final` on the method as that's what PHP does
$storage->final = $classlike_storage->final;
} else {
$storage->final = true;
}
} else {
$storage->final = $classlike_storage->final || $stmt->isFinal();
}

$storage->final_from_docblock = $classlike_storage->final_from_docblock;

if ($is_private) {
$storage->visibility = ClassLikeAnalyzer::VISIBILITY_PRIVATE;
} elseif ($is_protected) {
$storage->visibility = ClassLikeAnalyzer::VISIBILITY_PROTECTED;
} else {
$storage->visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC;
}
} else {
throw new UnexpectedValueException('Unrecognized functionlike');
throw new UnexpectedValueException('Unrecognized functionlike '.$stmt::class);
}

return [
Expand Down