Psalm: Autoloading false-positives

Created on 18 Dec 2018  路  58Comments  路  Source: vimeo/psalm

I'm just beginning use of psalm on my project and am running into auto-loading issues (MissingDependency, UndefinedFunction, UndefinedClass). The relevant classes/functions are auto-loaded with composer.

I installed psalm via composer, initialized, and ran.

Here is my xml config, which is a straight generated one, except for a modified projectFiles section. Can you help?

<?xml version="1.0"?>
<psalm
    totallyTyped="false"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config file://C:\wamp\www\main2\vendors\vimeo\psalm\config.xsd"
>
    <projectFiles>
        <directory name="app" />
        <directory name="ci" />
        <directory name="config" />
        <directory name="database" />
        <directory name="linter" />
        <directory name="tests" />
        <ignoreFiles>
            <file name="app/Console/cake.php" />
            <directory name="app/Plugin/Alertify" />
            <directory name="app/Plugin/DebugKit" />
            <directory name="app/Plugin/Migrations" />
            <directory name="app/Plugin/WhoopsCakephp" />
            <directory name="app/Vendor/_additions/Sears" />
            <directory name="app/Vendor/_additions/Zendesk" />
            <directory name="app/Vendor/_additions/fzaninotto" />
            <directory name="app/Vendor/_additions/league" />
            <file name="app/Vendor/_additions/sphinxapi.php" />
            <directory name="app/Vendor/_overrides" />
            <directory name="app/webroot" />
            <directory name="tests/_support/_generated" />
            <file name="tests/acceptance/AcceptanceTester.php" />
            <file name="tests/functional/FunctionalTester.php" />
            <file name="tests/unit/UnitTester.php" />
            <directory name="vendors" />
        </ignoreFiles>
    </projectFiles>

    <issueHandlers>
        <LessSpecificReturnType errorLevel="info" />

        <!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->

        <DeprecatedMethod errorLevel="info" />
        <DeprecatedProperty errorLevel="info" />
        <DeprecatedClass errorLevel="info" />
        <DeprecatedConstant errorLevel="info" />
        <DeprecatedInterface errorLevel="info" />
        <DeprecatedTrait errorLevel="info" />

        <InternalMethod errorLevel="info" />
        <InternalProperty errorLevel="info" />
        <InternalClass errorLevel="info" />

        <MissingClosureReturnType errorLevel="info" />
        <MissingReturnType errorLevel="info" />
        <MissingPropertyType errorLevel="info" />
        <InvalidDocblock errorLevel="info" />
        <MisplacedRequiredParam errorLevel="info" />

        <PropertyNotSetInConstructor errorLevel="info" />
        <MissingConstructor errorLevel="info" />
        <MissingClosureParamType errorLevel="info" />
        <MissingParamType errorLevel="info" />

        <RedundantCondition errorLevel="info" />

        <DocblockTypeContradiction errorLevel="info" />
        <RedundantConditionGivenDocblockType errorLevel="info" />

        <UnresolvableInclude errorLevel="info" />

        <RawObjectIteration errorLevel="info" />

        <InvalidStringClass errorLevel="info" />
    </issueHandlers>
</psalm>
bug

Most helpful comment

All 58 comments

Hey! How are those autoloaded classes/functions defined? Is it just regular PSR-4 stuff? And are they third-party classes or things you鈥檝e written?

Hey @muglug Some are class aliases included via a package's composer.json, and some are loaded via a custom "loader" function. For the latter, is there a way to register certain classes/files/dirs as auto-loaded?

For the latter, is there a way to register certain classes/files/dirs as auto-loaded?

Yup, you can reference an autoloader/bootstrapper the way you would in tests.

So add autoload="path/to/file.php" in you Psalm config. There's short explanation here: https://getpsalm.org/docs/configuration/#running-psalm

That helped, thanks!. Now it's processing, but ran into this:

PHP Fatal error:  Uncaught InvalidArgumentException: Could not get class storage for object in C:\wamp\www\main2\vendors\vimeo\psalm\src\Psalm\Internal\Provider\ClassLikeStorageProvider.php:43
Stack trace:
#0 C:\wamp\www\main2\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\Statements\Expression\Call\StaticCallAnalyzer.php(82): Psalm\Internal\Provider\ClassLikeStorageProvider->get('object')
#1 C:\wamp\www\main2\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer.php(115): Psalm\Internal\Analyzer\Statements\Expression\Call\StaticCallAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\StaticCall), Object(Psalm\Context))
#2 C:\wamp\www\main2\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\StatementsAnalyzer.php(575): Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\StaticCall), Object(Psalm\Context), false, Object(Psalm\Context))
#3 C:\wamp\www\main2\vendors\vimeo\psal in C:\wamp\www\main2\vendors\vimeo\psalm\src\Psalm\Internal\Provider\ClassLikeStorageProvider.php on line 43

Would you mind running with --debug-by-line? That'll give a good indication of where the problem's coming from.

It's 131k lines of output, but I cannot figure it out. How do you recommend I get it to you?

Oh it鈥檚 not for me. The last line referenced before the exception will tell you what code is causing Psalm to crash.

OK, so this is what it's showing:

Notice: Undefined index: $Model->hasMany in ...\vendors\vimeo\psalm\src\Psalm\Type\Reconciler.php on line 2113

Fatal error: Uncaught Error: Call to a member function getTypes() on null in ...\vendors\vimeo\psalm\src\Psalm\Type\Reconciler.php:2113
Stack trace:
#0 ...\vendors\vimeo\psalm\src\Psalm\Type\Reconciler.php(145): Psalm\Type\Reconciler::getValueForKey(Object(Psalm\Codebase), '$Model->hasMany...', Array, Array, Object(Psalm\CodeLocation))
#1 ...\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\Statements\Block\IfAnalyzer.php(326): Psalm\Type\Reconciler::reconcileKeyedTypes(Array, Array, Array, Array, Object(Psalm\Internal\Analyzer\StatementsAnalyzer), false, Object(Psalm\CodeLocation))
#2 ...\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\StatementsAnalyzer.php(239): Psalm\Internal\Analyzer\Statements\Block\IfAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Stmt\If_), Object(Psalm\Context))
#3 ...\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\FunctionLikeAnalyzer.php(509): Psalm\Internal\Analyzer\StatementsAnaly in ...\vendors\vimeo\psalm\src\Psalm\Type\Reconciler.php on line 2113

This particular property is absolutely set and used in a zillion places.

Oh right - I mean the last line that references your files - like, what code was it trying to analyse when it crashed?

Normal, 100% working code that is called several times per page load

I fixed an embarrassingly obvious bug here: https://github.com/vimeo/psalm/commit/5842ac15173f2f6a33d654180769d5336279ef28

Would you mind trying again with latest master?

I've gone ahead added that fix to the 3.0.6 release, so you'd just need to run composer update vimeo/psalm

Nice! Will check ...

OK, so now it's running out of memory, which all things considered is a good thing, which means it got past that issue. Checking psalm --help didn't show any option for increasing the memory.

PHP Fatal error:  Allowed memory size of 4294967296 bytes exhausted (tried to allocate 1179648 bytes) in ...\vendors\vimeo\psalm\src\Psalm\Internal\Diff\FileDiffer.php on line 49

It this usual?

It this usual?

Not at all - run --clear-cache and retry.

I've noticed that OOM error once before when truncating a many-thousand-line file, and it's easy for me to fix.

I added this check before performing file diffing which should prevent that particular OOM error (though errors could be coming from somewhere else).

OK... making progress. But now it's back to this again, albeit in a different place and with a legit error (found using --debug-by-line:

PHP Fatal error:  Uncaught InvalidArgumentException: Could not get class storage for Model in ...\vendors\vimeo\psalm\src\Psalm\Internal\Provider\ClassLikeStorageProvider.php:43

But shouldn't psalm be able to report that error rather than crashing? Or does my system (which passes level 3 in phpstan) have _that_ many errors?

Referring to my earlier comment, there is a class Tightenco\Collect\Support\Collection, aliased in the vendor package as Illuminate\Support\Collection, see https://github.com/tightenco/collect/blob/master/src/Collect/Support/alias.php#L7

Psalm is showing a MissingDependency error:

ERROR: MissingDependency - my\File.php:95:17 - Foo\ColumnCollection depends on class or interface illuminate\support\collection that does not exist

Can it not find it due to capitalization? My code has correct capitalization, but somehow the error message seems to point to some issue there.

Thanks for your continued help here. Much appreciated!

But shouldn't psalm be able to report that error rather than crashing?

Absolutely - what's the code that it's crashing on?

Can it not find it due to capitalization?

No, it's because of the class aliasing. Psalm currently only properly supports class aliasing if it can reason about the class_alias calls themselves. By putting it in a loop, Psalm loses the ability to infer properly.

I'll fix this on the other side so when Psalm does reflection it remembers what class it was reflecting in the first place and stores that the same way it stores explicit class_alias calls.

I've just fixed the class_alias issue above.

Thanks! So now it finds the class, but it cannot get class storage for it.

OK, I added a fix here in e744e71 that fixed analysis of an open-source package that uses tightenco/collect: https://github.com/bmitch/churn-php

You are awesome! Thanks for your prolonged help.

Unfortunately, the problem of Uncaught InvalidArgumentException: Could not get class storage for Illuminate\Support\Arr in ...\vendors\vimeo\psalm\src\Psalm\Internal\Provider\ClassLikeStorageProvider.php:43 is still there 鈽癸笍

I would understand if you've have enough of me 馃槃 and could not continue here

Turns out that both commits are still getting stuck, but the first one (02003d9) actually gets further (9804 lines of output using --debug-by-line), while e744e71 stops at about 4300 lines.

Both on Illuminate\Support\Arr

How are you using that class at that particular location? And what happens when you just run Psalm on the last file it had checked?

I'm able to reproduce with https://github.com/laravie/parser, stay tuned

OK, a bunch of things should have been fixed in the last two commits.

K, I'll try

And if you still see errors, just dump the Psalm stack trace here and I'll be able to figure out what's not using the proper aliased names pretty quickly.

Got past those 馃憦

Now getting a completely different error:

Fatal error: Uncaught InvalidArgumentException: This is a bad place in ...\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer.php:836
Stack trace:
#0 ...\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer.php(149): Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer::getArrayAccessTypeGivenOffset(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\ArrayDimFetch), Object(Psalm\Type\Union), Object(Psalm\Type\Union), false, '$settings', NULL, true)
#1 ...\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer.php(361): Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer::analyze(Object(Psalm\Internal\Analyzer\StatementsAnalyzer), Object(PhpParser\Node\Expr\ArrayDimFetch), Object(Psalm\Context))
#2 ...\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer.php(1334): Psalm\In in ...\vendors\vimeo\psalm\src\Psalm\Internal\Analyzer\Statements\Expression\Fetch\ArrayFetchAnalyzer.php on line 836

The code (simplified) is this:

/**
 * Renders HTML for a single button
 *
 * @param string       $text         The display text
 * @param array|string $settings     Various settings
 * @param boolean      $isInDropdown Is this button contained in a dropdown
 *
 * @return string|null The requisite HTML
 */
private function _renderButton(string $text, $settings, bool $isInDropdown = false): ?string
{
    // string = url
    if (is_string($settings)) {
        $settings = ['url' => $settings];
    }

    ...

    // icon
    if (isset($settings['icon'])) { // <-- blowing up here
        ...
    }

    return '...';
}

Should I open a new issue for this?

Should I open a new issue for this?

You can keep this issue open - already enough twists and turns here.

Could you try to reproduce in https://getpsalm.org ?

What you posted itself looks fine: https://getpsalm.org/r/b90ff12f39

It won't reproduce there.

Here's the entire method (plus more scaffold) https://getpsalm.org/r/fddcdcd202

Awesome, I can reproduce with that code in https://github.com/laravie/parser

Do you have a twitter handle? You deserve a shout-out for sticking it through.

Bug can be reproduced here - the _one_ thing you were missing was the pass-by-ref on the first arg of App::pull: https://getpsalm.org/r/0c13262cb9

Good catch. I guess pass-by-refs are a bad place to be 馃槅

Fixed in 10a36def968a4e642b1afec74e3f0be602c79f58

Is all good?

Much better!

I am noticing interesting behavior. When run with --debug-by-line it runs to completion, but when run straight $ psalm it quits with Uncaught InvalidArgumentException: Could not get class storage for Model

Uhh that's weird. Try with --no-cache - I bet that's the deciding factor.

psalm --no-cache still crashed

Same if I run --clear-cache first, followed by a normal run

But --debug-by-line works? All it does is some echoing...

Yep. It produces about 97k lines

And with --debug?

With --debug works, only 7800 lines this time

where does Model come from?

And it quits with Uncaught InvalidArgumentException: Could not get class storage for Model what's the rest of the trace?

OK, now it's working. Seems intermittent

Intermittent even with --no-cache? Psalm isn't doing anything magical (but it does use whatever autoloaders have been configured beyond the regular Composer one, and _they_ might be doing something weird)

All seems well. Huge thank you for all your time and efforts!

Will you release these 6 or so commits in a release, or should I stay on the commit hash for a while?

I ran it a few times without any debug, and it runs to completion 馃憦

I'll test against a few places, then release in an hour or so

Thanks again for your help!

Was this page helpful?
0 / 5 - 0 ratings