Skip to content
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

#62 Add Psalm support. #66

Merged
merged 14 commits into from
Apr 1, 2022
Merged

#62 Add Psalm support. #66

merged 14 commits into from
Apr 1, 2022

Conversation

hkirsman
Copy link
Collaborator

@hkirsman hkirsman commented Sep 24, 2021

You can try this out on Drupal 9 project:

composer require wunderio/code-quality:dev-feature/62-add-psalm-task --dev

or in Lando prefix with lando. Psalm requires PHP's mbstring and in my experience I was missing it locally but Lando had it.

lando composer require wunderio/code-quality:dev-feature/62-add-psalm-task --dev

Lock files can be tricky and every project is different. On my 9.2.6 installation I did this:

lando composer require wunderio/code-quality:dev-feature/62-add-psalm-task composer/xdebug-handler:1.4.6 composer/composer:2.0.2  --dev
  1. Check that first line in grumphp.yml is grumphp: and not parameters:

image

  1. In Grumphp add psalm task and extension lines ("psalm: ~" under "tasks:" and "- Wunderio\GrumPHP\Task\Psalm\PsalmExtensionLoader" under "extensions:")

image

  1. Copy the psalm.xml config to project root

    cp vendor/wunderio/code-quality/config/psalm.xml ./psalm.xml

  2. You might see issues with tests not finding dependencies. ERROR: MissingDependency - web/modules/custom/redirect_old_tua/tests/src/HomePageTest.php:13:28 - Drupal\Tests\BrowserTestBase depends on class or interface phpunit\framework\testcase that does not exist (see https://psalm.dev/157)
    class HomePageTest extends BrowserTestBase {

For that install testing Drupals testing support:

composer require drupal/core-dev:X.X.X --dev
  1. As I don't have php locally I'm also using Landos php with this grumphp config:

    grumphp:
    git_hook_variables:
    EXEC_GRUMPHP_COMMAND: 'lando php'

and re-init grumphp

lando grumphp git:init

Where X.X.X is your current core version.

@hkirsman hkirsman changed the title #62 Add Psalm task. #62 Add Psalm support. Sep 25, 2021
@hkirsman
Copy link
Collaborator Author

hkirsman commented Sep 25, 2021

Seems as if we also need https://github.com/fenetikm/autoload-drupal because otherwise it complains missing classes although they are there (and dev-master because it supports Composer 2). I've added this with last commit.

image

In composer.json under extras add

    "autoload-drupal": {
        "modules": [
            "web/modules/custom/",
            "web/modules/contrib/",
            "web/core/modules/"
        ],
        "classmap": [
            "web/core/tests/Drupal/Tests"
        ]
    }

Run

lando composer dump-autoload 

And finally try to commit code or run

lando grumphp run

@hkirsman
Copy link
Collaborator Author

hkirsman commented Sep 25, 2021

Could also be we need to test https://github.com/mortenson/psalm-plugin-drupal

@hkirsman
Copy link
Collaborator Author

https://github.com/fenetikm/autoload-drupal seems to be quite good but doesn't seem to load .module files so I'm seeing undefined constant error with my code.

This one says it solves the .module and .theme loading https://github.com/mortenson/psalm-plugin-drupal

@hkirsman
Copy link
Collaborator Author

For temporary fix it seems I'd need to create file something like autoload-extras-for-psalm.php

<?php

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
$projectRoot = realpath($baseDir . '/..');

// @todo: find all .module and .theme files and require here.
require_once $projectRoot . '/web/modules/contrib/adobe_captivate/adobe_captivate.module';

In psalm.xml add autoloader parameter:

<psalm
    errorLevel="6"
    resolveFromConfigFile="true"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
    autoloader="vendor/wunderio/code-quality/autoload-extras-for-psalm.php"
>

@hkirsman
Copy link
Collaborator Author

Hm, can't get this to work.

One more thing. We already have mglaman/phpstan-drupal installed and this seems promising: https://github.com/mglaman/phpstan-drupal/blob/main/drupal-autoloader.php

@hkirsman
Copy link
Collaborator Author

I've taken the https://github.com/mglaman/phpstan-drupal/blob/main/drupal-autoloader.php and refactored it a bit. To me it seems to work nicely.

@hkirsman
Copy link
Collaborator Author

Should be ok now for testing. See initial comment.

@hkirsman
Copy link
Collaborator Author

I'm getting

ERROR: UndefinedInterfaceMethod - web/modules/custom/tekla_apireference/src/Form/APIReferenceDocumentRevisionRevertTranslationForm.php:101:54 - Method Drupal\Core\Entity\EntityInterface::getTranslation does not exist (see https://psalm.dev/181)
    $latest_revision_translation = $latest_revision->getTranslation($this->langcode);

This is the code:

    $latest_revision = $this->apiReferenceDocumentStorage->load($revision->id());
    $latest_revision_translation = $latest_revision->getTranslation($this->langcode);

I don't see it's actually a valid error. PhpStorm find the interface and if I try to add it then PhpStorm also yells that I can't add it again.

So I'll disable this error:

    <issueHandlers>
      <UndefinedInterfaceMethod errorLevel="suppress" />
    </issueHandlers>

@hkirsman
Copy link
Collaborator Author

Another one:

ERROR: ImplicitToStringCast - web/modules/custom/tekla_apireference/src/Form/APIReferenceDocumentImportForm.php:137:58 - Argument 2 of Drupal\Core\Form\FormStateInterface::setErrorByName expects string, Drupal\Core\StringTranslation\TranslatableMarkup provided with a __toString method (see https://psalm.dev/060)
      $form_state->setErrorByName('source_package_file', t('Import could not continue because directory %directory could not be created for extraction.', [
        '%directory' => $extract_path,
      ]));

But we're not going to do something like t('Foobar')->____toString(). That would output string but with the placeholders. I'll create ignore for that specific error.

All in all I'm adding these ignores to config:

    <issueHandlers>
      <UndefinedInterfaceMethod errorLevel="suppress" />
      <ImplicitToStringCast errorLevel="suppress" />
    </issueHandlers>

Copy link

@k4lv15 k4lv15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, @hkirsman! I've been testing this feature on one of the customer's projects for some time. Seems to be working nicely in overall 🙂 👍 But since commit 5a9c35c I'm facing a few issues, please see my other two comments below.

Also, one thing that is not working is PSALM's Docblock suppression (/** @psalm-suppress <SomeError> */) as PHPCS task throws Drupal.Commenting.InlineComment.DocBlock error whenever such docblock is used. In my project I've added a rule to phpcs.xml file to ignore such errors completely (see my snippet below), but a better option would prolly be to ignore only @psalm-suppress docblock comments. Not sure how, but I guess something similar has been done with @var docblocks.

<!-- Mark inline DocBlock comments only as a warning as otherwise using @psalm-suppress annotation comments leads to PHPCS errors-->
<rule ref="Drupal.Commenting.InlineComment.DocBlock">
    <severity>4</severity>
</rule>

UPDATE:
PSALM's Docblock suppression is now allowed and works since latest commit (361795e) 🎉

defaults: null
allowed_types: ['string', 'null' ]
threads:
defaults: 6
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running ./vendor/bin/grumphp run --tasks=psalm gives "The pcntl & posix extensions must be loaded in order for Psalm to be able to use multiple processes." Happens both locally (using MacOs BigSur) and on CircleCI (using eu.gcr.io/silta-images/php:7.4-fpm-v0.1 for PHP container). If I override the default value of threads parameter back to 1, then it works again.
UPDATE:
Locally, if I use custom lando tooling that utilises lando's appserver for running for GrumPHP commands, everything works just fine:

  grumphp:
    description: Runs GrumPHP commands
    cmd:
      - appserver: ./vendor/bin/grumphp

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works nicely now both on Lando and CircleCI :)

$arguments->addOptionalArgument('--report=%s', $config['report']);
$arguments->addOptionalArgument('--no-cache', $config['no_cache']);
$arguments->addOptionalArgument('--threads=%d', $config['threads']);
$arguments->add('--find-unused-code');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When testing on one of client's projects, UnusedVariable errors can't be ignored if using /** @psalm-suppress UnusedVariable */ annotation-comment before the variable., as --find-unused-code seems to have higher priority. It can be changed by ignoring particular errors in psalm.xml file by adding

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

but prolly all new UnusedVariable errors will be ignored from now on, which is not great, I guess.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not remove that variable or is false positive?

Copy link

@k4lv15 k4lv15 Mar 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a false positive to me as the variable is used multiple times after defined. Also PHPStorm doesn't mark it as unused which happened in all other cases I fixed. What's different is that it's defined as a reference to another variable (see the example code below). Maybe PSALM doesn't like that for some reason.

$subform1 =& $form['field_content_row']['widget'][$key]['subform'];
    if (!empty($subform1['field_block_reference']['widget'])) {
        foreach (Element::children($subform1['field_block_reference']['widget']) as $key1) {
        ...

UPDATE
Managed to ignore the error by adding @psalm-suppress UnusedVariable for the function which had that variable, at least better option then ignoring such errors completely 🙂 So seems that only inline usages of @psalm-suppress doesn't work.

Copy link

@k4lv15 k4lv15 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, all of the mentioned issues have been fixed ✅
NOTE: Set-up was tested on a real customer project and worked nicely ✅
Green light from me 🟢 And great work! 💪 💪 💪

@hkirsman hkirsman merged commit 85a8935 into master Apr 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants