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

Autoloading of sniff fails when multiple classes declared in same file #3145

Closed
aik099 opened this issue Oct 23, 2020 · 14 comments
Closed

Autoloading of sniff fails when multiple classes declared in same file #3145

aik099 opened this issue Oct 23, 2020 · 14 comments
Milestone

Comments

@aik099
Copy link
Contributor

aik099 commented Oct 23, 2020

Describe the bug
When a sniff PHP file contains multiple PHP classes in it (e.g. sniff class itself and a helper class), then \PHP_CodeSniffer\Autoload::loadFile method called in \PHP_CodeSniffer\Ruleset::registerSniffs method returns the last declared class in that file, which might not be a sniff class at all. As a result that class is treated like a sniff class and nothing good happens.

In my case, that class was with mandatory constructor arguments and PHP_CodeSniffer tried to initiate it like a sniff (without constructor arguments) and I've got a Fatal Error.

Code sample

class TypeCommentStructure
{
    public function __construct(File $phpcsFile, $stackPtr)
    {
    }
}

To reproduce
Steps to reproduce the behavior:

  1. add an above-added class at the bottom of any sniff class containing file
  2. run the ruleset, that contains a sniff that was just added
  3. See error message displayed
PHP Fatal error:  Uncaught ArgumentCountError: Too few arguments to function CodingStandard\Sniffs\Commenting\TypeCommentStructure::__construct(), 0 passed in /mnt/UserData/home/user/.config/composer/vendor/squizlabs/php_codesniffer/src/Ruleset.php on line 1209 and exactly 2 expected in /mnt/UserData/home/user/project/vendor/aik099/coding-standard/CodingStandard/Sniffs/Commenting/TypeCommentSniff.php:608
      Stack trace:
      #0 /mnt/UserData/home/user/.config/composer/vendor/squizlabs/php_codesniffer/src/Ruleset.php(1209): CodingStandard\Sniffs\Commenting\TypeCommentStructure->__construct()
      #1 /mnt/UserData/home/user/.config/composer/vendor/squizlabs/php_codesniffer/src/Ruleset.php(218): PHP_CodeSniffer\Ruleset->populateTokenListeners()
      #2 /mnt/UserData/home/user/.config/composer/vendor/squizlabs/php_codesniffer/src/Runner.php(332): PHP_CodeSniffer\Ruleset->__construct(Object(PHP_CodeSniffer\Config))
      #3 /mnt/UserData/home/user/.config/composer/vendor/squizlabs/php_codesniffer/src/Runne... (182 more bytes) ...
(Run with `--trace` for a full exception trace.)

Expected behavior
A clear and concise description of what you expected to happen.

Versions (please complete the following information):

  • OS: Linux
  • PHP: 7.2
  • PHPCS: 3.5.8
  • Standard: any

Additional context
No issue at PHP_CodeSniffer 3.5.6 and below. The issue started happening after PHP_CodeSniffer 3.5.7 release.

@jrfnl
Copy link
Contributor

jrfnl commented Oct 23, 2020

Sounds related to #3130.

@aik099
Copy link
Contributor Author

aik099 commented Oct 25, 2020

@jrfnl , I've tried reverting all changes from 2020 year, including this one, and still had the same issue (at least on PHP 7.2).

@jrfnl
Copy link
Contributor

jrfnl commented Oct 26, 2020

@aik099 Could you give me some insight into the directory structure your standard uses ? and where this file lives within that structure ?

@aik099
Copy link
Contributor Author

aik099 commented Oct 28, 2020

@jrfnl , even better, here it's on the GitHub: https://github.com/aik099/CodingStandard . The sniff file in question is https://github.com/aik099/CodingStandard/blob/master/CodingStandard/Sniffs/Commenting/TypeCommentSniff.php, where I have several classes defined.

If you attempt to run tests (included in that repo) for that specific sniff (or all test suite) you'll see that test for it fail, because 2nd class (instead of a sniff class) is registered as a sniff and of course isn't able to find any issues in code to allow the test to pass.

@jrfnl
Copy link
Contributor

jrfnl commented Oct 28, 2020

Ah yes, so basically it comes down to the autoloader having an assumption that each file only contains one class. I'm surprised to hear you say that it worked in PHPCS 3.5.6 though, as the change I referred to above is the only change of any significance made to the autoloader since PHPCS 3.0.2.

I'd say that it should probably be made more clear that "one class per file" is a requirement in the tutorial.

As for your use-case, what about moving the second class to its own file in a CodingStandard/Helpers directory ? The PHPCS autoloader will handle loading classes anywhere within the CodingStandard directory without problems, but expects classes in the Sniffs directory to be, well, sniffs.

@aik099
Copy link
Contributor Author

aik099 commented Oct 28, 2020

Sure, I can extract a class into a separate file. You can see yourself that it works on PHP_CodeSniffer 3.5.6 if you:

  1. change composer.json file in a coding standard to use 3.5.6 version specifically (instead of latest available)
  2. run a composer update command
  3. run tests

Still sounds like a BC break to me, because it worked and stopped.

@theilig
Copy link

theilig commented Oct 28, 2020

I'm seeing the same issue. Having multiple classes in a file worked in 3.5.6, broke in 3.5.8

Working on splitting them out

@gsherwood
Copy link
Member

I think I might know what this is. The array_reverse call was removed as part of the PHP 7.4 fix because the order of loaded classes can no longer be relied upon. So while the issue with multiple classes in a single file was likely to be encountered by developers sometime in the future, the removal of this call has forced it to occur now.

If anyone is able to test this, could you change this line in autoload.php:

$newClasses = array_diff($classesAfterLoad['classes'], $classesBeforeLoad['classes']);

To be:

$newClasses = array_reverse(array_diff($classesAfterLoad['classes'], $classesBeforeLoad['classes']));

It works for me locally, but would be good to test with a failing standard as well.

@aik099
Copy link
Contributor Author

aik099 commented Oct 29, 2020

@gsherwood , the fix proposed in #3145 (comment) worked for PHP < 7.4, but with that fix on PHP 7.4 it became broken.

@gsherwood
Copy link
Member

@aik099 Thanks for testing. I think this is one of those PHP changes where there isn't a lot of ways around it - moving to a single class per file appears to be the only thing that's going to work 100% of the time.

If anyone else has any ideas, please let me know.

@aik099
Copy link
Contributor Author

aik099 commented Nov 3, 2020

It's possible to detect PHP version and it's below PHP 7.4 add array_reverse call.

@gsherwood
Copy link
Member

@aik099 Sounds like a good idea. Any chance you could try this code:

$newClasses = array_diff($classesAfterLoad['classes'], $classesBeforeLoad['classes']);
if (PHP_VERSION_ID < 70400) {
    $newClasses = array_reverse($newClasses);
}

@aik099
Copy link
Contributor Author

aik099 commented Nov 10, 2020

Yes, this solution works correctly.

Maybe you can try adding a test case for this and Travis CI would happily ensure, that it keeps working as expected.

soniCaH added a commit to soniCaH/PHP_CodeSniffer that referenced this issue Dec 7, 2020
@gsherwood gsherwood added this to the 3.6.0 milestone Dec 11, 2020
gsherwood added a commit that referenced this issue Jan 13, 2021
gsherwood added a commit that referenced this issue Jan 13, 2021
@gsherwood
Copy link
Member

Have committed this change. It will be in 3.6.0. Thanks for testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants