Skip to content

Commit

Permalink
Merge pull request #52 from mr-feek/eloquent-collection
Browse files Browse the repository at this point in the history
Begin templating Eloquent
  • Loading branch information
mr-feek authored May 23, 2020
2 parents db2f1a2 + b5432cf commit f646b57
Show file tree
Hide file tree
Showing 8 changed files with 799 additions and 1 deletion.
17 changes: 16 additions & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\View\Engines\PhpEngine;
use Illuminate\View\Factory;
use Illuminate\View\FileViewFinder;
use Psalm\LaravelPlugin\ReturnTypeProvider\ModelReturnTypeProvider;
use Psalm\LaravelPlugin\ReturnTypeProvider\UrlReturnTypeProvider;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
Expand Down Expand Up @@ -53,6 +54,8 @@ public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement
$registration->registerHooksFromClass(PropertyProvider\ModelPropertyProvider::class);
require_once 'ReturnTypeProvider/UrlReturnTypeProvider.php';
$registration->registerHooksFromClass(UrlReturnTypeProvider::class);
require_once 'ReturnTypeProvider/ModelReturnTypeProvider.php';
$registration->registerHooksFromClass(ModelReturnTypeProvider::class);

$this->addOurStubs($registration);
}
Expand All @@ -68,8 +71,20 @@ private function ingestFacadeStubs(
$view_factory,
string $cache_dir
) : void {
/** @var \Illuminate\Config\Repository $config */
$config = $app['config'];

// The \Eloquent mixin has less specific return types than our custom plugin can determine, so we unset it here
// to not taint our analysis
if ($ideHelperExtra = $config->get('ide-helper.extra')) {
if (isset($ideHelperExtra['Eloquent'])) {
unset($ideHelperExtra['Eloquent']);
$config->set('ide-helper.extra', $ideHelperExtra);
}
}

$stubs_generator_command = new \Barryvdh\LaravelIdeHelper\Console\GeneratorCommand(
$app['config'],
$config,
$fake_filesystem,
$view_factory
);
Expand Down
95 changes: 95 additions & 0 deletions src/ReturnTypeProvider/ModelReturnTypeProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php declare(strict_types=1);

namespace Psalm\LaravelPlugin\ReturnTypeProvider;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use PhpParser\Node\Expr\MethodCall;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\MethodIdentifier;
use Psalm\Plugin\Hook\MethodReturnTypeProviderInterface;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Union;
use function in_array;

final class ModelReturnTypeProvider implements MethodReturnTypeProviderInterface
{
public static function getClassLikeNames(): array
{
return [Model::class];
}

public static function getMethodReturnType(StatementsSource $source, string $fq_classlike_name, string $method_name_lowercase, array $call_args, Context $context, CodeLocation $code_location, array $template_type_parameters = null, string $called_fq_classlike_name = null, string $called_method_name_lowercase = null)
{
if (!$source instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
return null;
}

// proxy to builder object
if ($method_name_lowercase === '__callstatic') {
if (!$called_fq_classlike_name || !$called_method_name_lowercase) {
return null;
}
$methodId = new MethodIdentifier($called_fq_classlike_name, $called_method_name_lowercase);

$fake_method_call = new MethodCall(
new \PhpParser\Node\Expr\Variable('builder'),
$methodId->method_name,
$call_args
);

$type = self::executeFakeCall($source, $fake_method_call, $context, $called_fq_classlike_name);
return $type;
}

return null;
}

private static function executeFakeCall(
\Psalm\Internal\Analyzer\StatementsAnalyzer $statements_analyzer,
\PhpParser\Node\Expr\MethodCall $fake_method_call,
Context $context,
string $called_fq_classlike_name
) : ?Union {
$old_data_provider = $statements_analyzer->node_data;
$statements_analyzer->node_data = clone $statements_analyzer->node_data;

$context = clone $context;
$context->inside_call = true;

$context->vars_in_scope['$builder'] = new Union([
new Type\Atomic\TGenericObject(Builder::class, [
new Union([
new Type\Atomic\TNamedObject($called_fq_classlike_name),
]),
]),
]);

$suppressed_issues = $statements_analyzer->getSuppressedIssues();

if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']);
}

if (\Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze(
$statements_analyzer,
$fake_method_call,
$context,
false
) === false) {
return null;
}

if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) {
$statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']);
}

$returnType = $statements_analyzer->node_data->getType($fake_method_call);

$statements_analyzer->node_data = $old_data_provider;

return $returnType;
}
}
Loading

0 comments on commit f646b57

Please sign in to comment.