-
Notifications
You must be signed in to change notification settings - Fork 11
Add SpecifyArgSeparatorFixer to make sure http_build_query is properly used #18
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| coverage_clover: coverage-clover.xml | ||
| json_path: coveralls-upload.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,15 @@ | ||
| language: php | ||
|
|
||
| php: | ||
| - 7.1 | ||
| - 7.2 | ||
|
|
||
| install: | ||
| - travis_retry composer self-update && composer --version | ||
| - travis_retry composer update --no-interaction | ||
|
|
||
| script: | ||
| - composer all | ||
| - composer all-ci | ||
|
|
||
| after_success: | ||
| - travis_retry php vendor/bin/php-coveralls -v | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,20 +10,53 @@ | |
| "homepage": "https://github.com/lmc-eu" | ||
| } | ||
| ], | ||
| "autoload": { | ||
| "psr-4": { | ||
| "Lmc\\CodingStandard\\": "src/" | ||
| } | ||
| }, | ||
| "autoload-dev": { | ||
| "psr-4": { | ||
| "Lmc\\CodingStandard\\": "tests/" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not convetion
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use the same namespace for Class and its corresponding test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know :D but why not convention?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAIK both of the approaches are widely used, so I don't consider any of them as "general convention". And why it should be in standalone namespace in the first place? I don't see a reason for this. The same namespace means you don't need to import the class under test namespace, also it makes sense to me to have class and its tests in the same "module" (namespace), because they belongs to each another. But its still more about personal preference... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar unconvention as putting src and tests in the same directory, causes in php cs fixer only troubles. 80 % of php-scoper setup took me only this to solve. It's really painful to work with such packages, where maintainer don't see behing the package common use.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I don't understand :). This is the way we use it for years and never heard of single issue. And it is not any "hack", just usual setting of Composer autoloading. But yeah, I can imagine this could cause trouble when developing some low-level stuff. BTW There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, open source code is very different to closed one.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True, but I don't think the difference is in the way autoloading is set up :). But this seems like a topic to pub/meetup, so let's don't forget it next time we meet :-D . I'd like to hear about the stuff you must dealt with :). |
||
| } | ||
| }, | ||
| "require": { | ||
| "symplify/easy-coding-standard": "^4.0.2" | ||
| }, | ||
| "require-dev": { | ||
| "j13k/yaml-lint": "^1.1", | ||
| "mhujer/yaml-sort-checker": "^1.2" | ||
| "mhujer/yaml-sort-checker": "^1.2", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you consider using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why :)? This is the package itself 🙃 |
||
| "phpstan/phpstan-phpunit": "^0.9.4", | ||
| "phpstan/phpstan-shim": "^0.9.2", | ||
| "phpunit/phpunit": "^7.1" | ||
| }, | ||
| "scripts": { | ||
| "all": [ | ||
| "@lint" | ||
| "@lint", | ||
| "@analyze", | ||
| "@test" | ||
| ], | ||
| "all-ci": [ | ||
| "@lint", | ||
| "@analyze", | ||
| "@test-ci" | ||
| ], | ||
| "lint": [ | ||
| "for FILE_NAME in *.yml *.yaml; do vendor/bin/yaml-lint \"$FILE_NAME\"; done", | ||
| "vendor/bin/yaml-sort-checker" | ||
| ], | ||
| "analyze": [ | ||
| "vendor/bin/ecs check src/ tests/ -vvv --ansi", | ||
| "vendor/bin/phpstan.phar analyze ./src ./tests -c phpstan.neon --level 7 --ansi" | ||
| ], | ||
| "test": [ | ||
| "./vendor/bin/phpunit --colors=always" | ||
| ], | ||
| "test-ci": [ | ||
| "./vendor/bin/phpunit --colors=always --coverage-clover coverage-clover.xml" | ||
| ] | ||
| }, | ||
| "config": { | ||
| "sort-packages": true | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| includes: | ||
| - vendor/phpstan/phpstan-phpunit/extension.neon |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <phpunit | ||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" | ||
| bootstrap="./vendor/autoload.php" | ||
| colors="true" | ||
| > | ||
| <testsuites> | ||
| <testsuite name="unit"> | ||
| <directory>./tests/</directory> | ||
| </testsuite> | ||
| </testsuites> | ||
|
|
||
| <filter> | ||
| <whitelist> | ||
| <directory suffix=".php">./src</directory> | ||
| </whitelist> | ||
| </filter> | ||
|
|
||
| <php> | ||
| <!-- E_ALL = 30719 --> | ||
| <ini name="error_reporting" value="30719"/> | ||
| </php> | ||
| </phpunit> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace Lmc\CodingStandard\Fixer; | ||
|
|
||
| use PhpCsFixer\Fixer\DefinedFixerInterface; | ||
| use PhpCsFixer\FixerDefinition\CodeSample; | ||
| use PhpCsFixer\FixerDefinition\FixerDefinition; | ||
| use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; | ||
| use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis; | ||
| use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer; | ||
| use PhpCsFixer\Tokenizer\CT; | ||
| use PhpCsFixer\Tokenizer\Token; | ||
| use PhpCsFixer\Tokenizer\Tokens; | ||
|
|
||
| class SpecifyArgSeparatorFixer implements DefinedFixerInterface | ||
| { | ||
| public function getDefinition(): FixerDefinitionInterface | ||
| { | ||
| return new FixerDefinition( | ||
| 'Function `http_build_query()` should always have specified `$arg_separator` argument', | ||
| [ | ||
| new CodeSample("<?php\n\$queryString = http_build_query(['foo' => 'bar', 'baz' => 'bat']);"), | ||
| ], | ||
| 'Function `http_build_query()` uses `arg_separator.output` ini directive as default argument separator, ' | ||
| . 'however when its default value "&" is changed, query string assembled by the method will be ' | ||
| . 'unexpectedly invalid. This Fixer forces you to not rely on ini settings and rather define ' | ||
| . '`$arg_separator` in third argument of the function.', | ||
| null, | ||
| null, | ||
| 'Risky when other than default "&" argument separator should be used in query strings.' | ||
| ); | ||
| } | ||
|
|
||
| public function isCandidate(Tokens $tokens): bool | ||
| { | ||
| return $tokens->isTokenKindFound(T_STRING); | ||
| } | ||
|
|
||
| public function isRisky(): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| public function fix(\SplFileInfo $file, Tokens $tokens): void | ||
| { | ||
| foreach ($tokens as $index => $token) { | ||
| if ($token->isGivenKind(T_STRING) && $token->getContent() === 'http_build_query') { | ||
| if ($this->isFunctionCall($tokens, $index)) { | ||
| continue; | ||
| } | ||
|
|
||
| $this->fixFunction($tokens, $index); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public function getName(): string | ||
| { | ||
| return self::class; | ||
| } | ||
|
|
||
| public function getPriority(): int | ||
| { | ||
| // Should be run after SingleQuoteFixer (priority 0) and ArraySyntaxFixer (priority 1) | ||
| return -1; | ||
| } | ||
|
|
||
| public function supports(\SplFileInfo $file): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| private function fixFunction(Tokens $tokens, int $functionIndex): void | ||
| { | ||
| $openParenthesisIndex = $tokens->getNextTokenOfKind($functionIndex, ['(']); | ||
| if ($openParenthesisIndex === null) { | ||
| return; | ||
| } | ||
|
|
||
| $closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesisIndex); | ||
|
|
||
| $argumentCount = (new ArgumentsAnalyzer()) | ||
| ->countArguments($tokens, $openParenthesisIndex, $closeParenthesisIndex); | ||
|
|
||
| // When third argument is already present and it is null, override its value. | ||
| if ($argumentCount >= 3) { | ||
| $thirdArgumentType = $this->getThirdArgumentInfo($tokens, $openParenthesisIndex, $closeParenthesisIndex); | ||
| if ($thirdArgumentType === null) { | ||
| return; | ||
| } | ||
|
|
||
| if ($thirdArgumentType->getName() === 'null') { | ||
| $this->setArgumentValueToAmp($tokens, $thirdArgumentType->getStartIndex()); | ||
| } | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $tokensToInsert = []; | ||
|
|
||
| // Add second argument if not defined | ||
| if ($argumentCount === 1) { | ||
| $tokensToInsert[] = new Token(','); | ||
| $tokensToInsert[] = new Token([T_WHITESPACE, ' ']); | ||
| $tokensToInsert[] = new Token([T_STRING, 'null']); | ||
| } | ||
|
|
||
| // Add third argument (arg separator): ", &" | ||
| if ($argumentCount < 3) { | ||
| $tokensToInsert[] = new Token(','); | ||
| $tokensToInsert[] = new Token([T_WHITESPACE, ' ']); | ||
| $tokensToInsert[] = new Token([T_STRING, "'&'"]); | ||
| } | ||
|
|
||
| if (!empty($tokensToInsert)) { | ||
| $beforeCloseParenthesisIndex = $tokens->getPrevNonWhitespace($closeParenthesisIndex); | ||
| $tokens->insertAt($beforeCloseParenthesisIndex + 1, $tokensToInsert); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Detect if this is most probably function call (and not function import or function definition). | ||
| */ | ||
| private function isFunctionCall(Tokens $tokens, int $index): bool | ||
| { | ||
| $previousIndex = $tokens->getPrevMeaningfulToken($index); | ||
|
|
||
| return $previousIndex !== null | ||
| && ($tokens[$previousIndex]->isGivenKind(CT::T_FUNCTION_IMPORT) | ||
| || $tokens[$previousIndex]->isGivenKind(T_FUNCTION)); | ||
| } | ||
|
|
||
| private function getThirdArgumentInfo( | ||
| Tokens $tokens, | ||
| int $openParenthesisIndex, | ||
| int $closeParenthesisIndex | ||
| ): ?TypeAnalysis { | ||
| $argumentsAnalyzer = new ArgumentsAnalyzer(); | ||
|
|
||
| $arguments = $argumentsAnalyzer->getArguments($tokens, $openParenthesisIndex, $closeParenthesisIndex); | ||
| $argumentIndex = array_slice($arguments, 2, 1, true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well I would name it rather There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So is this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The content is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I name these |
||
| $argumentInfo = $argumentsAnalyzer->getArgumentInfo($tokens, key($argumentIndex), reset($argumentIndex)); | ||
|
|
||
| return $argumentInfo->getTypeAnalysis(); | ||
| } | ||
|
|
||
| private function setArgumentValueToAmp(Tokens $tokens, int $argumentStartIndex): void | ||
| { | ||
| $ampToken = new Token([T_STRING, "'&'"]); | ||
| $tokens->offsetSet($argumentStartIndex, $ampToken); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace Lmc\CodingStandard\Fixer\Fixtures; | ||
|
|
||
| class Correct | ||
| { | ||
| public function assembleQueryString(): void | ||
| { | ||
| $queryString1 = http_build_query(['foo' => 'bar'], null, '&'); | ||
|
|
||
| $queryString1WithComment = http_build_query(['foo' => 'bar'], /* Comment, with commas, */ null , '&'); | ||
|
|
||
| $queryString1WithObject = http_build_query((object) ['foo' => 'bar'], null, '&'); | ||
|
|
||
| $queryString2 = http_build_query(['foo' => 'bar', 'baz' => 'bat'], null, '&'); | ||
|
|
||
| $queryString3 = http_build_query(['foo' => 'bar'], null, '&'); | ||
|
|
||
| $queryString4 = http_build_query(['foo' => 'bar'], null, ';'); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| function http_build_query($should, $not, $change): void | ||
| { | ||
| // this should not be affected | ||
| } | ||
|
|
||
| function assembleQueryString(): void | ||
| { | ||
| $queryString1 = http_build_query(['foo' => 'bar'], null, '&'); | ||
|
|
||
| $queryString2 = http_build_query(['foo' => 'bar'], 333, '&'); | ||
|
|
||
| $queryString3 = http_build_query(['foo' => 'bar'], 333, '&'); | ||
|
|
||
| $queryString4 = http_build_query(['foo' => 'bar'], 333, '&', PHP_QUERY_RFC3986); | ||
|
|
||
| $queryString5 = http_build_query(['foo' => 'bar'], null, '&', PHP_QUERY_RFC3986); | ||
|
|
||
| $queryString6 = http_build_query(['foo' => 'bar', 'baz' => 'ban'], null, '&', PHP_QUERY_RFC3986); | ||
|
|
||
| $queryString6 = http_build_query((object) ['foo' => 'bar', 'baz' => 'ban'], null, '&', PHP_QUERY_RFC3986); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| function http_build_query($should, $not, $change): void | ||
| { | ||
| // this should not be affected | ||
| } | ||
|
|
||
| function assembleQueryString(): void | ||
| { | ||
| $queryString1 = http_build_query(['foo' => 'bar']); | ||
|
|
||
| $queryString2 = http_build_query(['foo' => 'bar'], 333); | ||
|
|
||
| $queryString3 = http_build_query(['foo' => 'bar'], 333, null); | ||
|
|
||
| $queryString4 = http_build_query(['foo' => 'bar'], 333, null, PHP_QUERY_RFC3986); | ||
|
|
||
| $queryString5 = http_build_query(['foo' => 'bar'], null, null, PHP_QUERY_RFC3986); | ||
|
|
||
| $queryString6 = http_build_query(['foo' => 'bar', 'baz' => 'ban'], null, null, PHP_QUERY_RFC3986); | ||
|
|
||
| $queryString6 = http_build_query((object) ['foo' => 'bar', 'baz' => 'ban'], null, null, PHP_QUERY_RFC3986); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace Lmc\CodingStandard\Fixer; | ||
|
|
||
| use PhpCsFixer\Tokenizer\Tokens; | ||
| use PHPUnit\Framework\TestCase; | ||
|
|
||
| class SpecifyArgSeparatorFixerTest extends TestCase | ||
| { | ||
| /** | ||
| * @test | ||
| * @dataProvider provideFiles | ||
| */ | ||
| public function shouldFixCode(string $inputFile, string $expectedOutputFile): void | ||
| { | ||
| $fixer = new SpecifyArgSeparatorFixer(); | ||
| $fileInfo = new \SplFileInfo(__DIR__ . '/Fixtures/' . $inputFile); | ||
| $tokens = Tokens::fromCode(file_get_contents($fileInfo->getRealPath())); | ||
|
|
||
| $fixer->fix($fileInfo, $tokens); | ||
|
|
||
| $this->assertStringEqualsFile( | ||
| __DIR__ . '/Fixtures/' . $expectedOutputFile, | ||
| $tokens->generateCode() | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @return array[] | ||
| */ | ||
| public function provideFiles(): array | ||
| { | ||
| return [ | ||
| 'Correct file should not be changed' => ['Correct.txt', 'Correct.txt'], | ||
| 'Wrong file should be fixed' => ['Wrong.txt', 'Fixed.txt'], | ||
| ]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While using dataProvider, it makes sense to use one file per case.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @TomasVotruba True, but there is overhead and is also harder to see what is covered and what not. Maybe rather write inline PHP for each case (as some fixer tests do). You will see which line of code is broken by phpunit's diff, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, how do you run broken code only?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You dont (I guess).. but why would you run just part of tests? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do that too, but how to run specific one line in this single file? |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not to encapsulate those commands into
allscript?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need phpunit with specific settings for CI (to generate clover). But I may do something like "all-ci" or so.