diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3844bb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Composer +/composer.lock +/vendor/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c86d38 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# EasyCodingStandard configurations for Contao + +[![](https://img.shields.io/packagist/v/contao/easy-coding-standard.svg?style=flat-square)](https://packagist.org/packages/contao/easy-coding-standard) +[![](https://img.shields.io/packagist/dt/contao/easy-coding-standard.svg?style=flat-square)](https://packagist.org/packages/contao/easy-coding-standard) + +This package includes the EasyCodingStandard configurations for [Contao][1]. + +## Installation + +You can install the package with Composer: + +``` +composer require contao/easy-coding-standard +``` + +## Usage + +``` +vendor/bin/ecs check *-bundle/src *-bundle/tests --config vendor/bin/contao/easy-coding-standard/config/default.yaml +``` + +## License + +Contao is licensed under the terms of the LGPLv3. + +## Getting support + +Visit the [support page][2] to learn about the available support options. + +[1]: https://contao.org +[2]: https://contao.org/en/support.html diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..59c6c91 --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "contao/easy-coding-standard", + "type": "library", + "description": "EasyCodingStandard configurations for Contao", + "license": "LGPL-3.0-or-later", + "authors": [ + { + "name": "Leo Feyer", + "homepage": "https://github.com/leofeyer" + } + ], + "require": { + "php": "^7.1", + "symplify/coding-standard": "^7.1", + "symplify/easy-coding-standard": "^7.1" + }, + "autoload": { + "psr-4": { + "Contao\\EasyCodingStandard\\": "src/" + } + } +} diff --git a/config/default.yaml b/config/default.yaml new file mode 100644 index 0000000..1aa3f61 --- /dev/null +++ b/config/default.yaml @@ -0,0 +1,12 @@ +imports: + - { resource: set/contao.yaml } + +parameters: + exclude_files: + - '*/Resources/*' + + skip: + Symplify\CodingStandard\Sniffs\Naming\AbstractClassNameSniff: + - '*TestCase.php' + + cache_directory: '%sys_get_temp_dir%/ecs_default_cache' diff --git a/config/legacy.yaml b/config/legacy.yaml new file mode 100644 index 0000000..2e2f75a --- /dev/null +++ b/config/legacy.yaml @@ -0,0 +1,77 @@ +imports: + - { resource: set/contao.yaml } + +parameters: + exclude_files: + - '*/languages/*' + - '*/templates/*' + - '*/themes/*' + + skip: + PhpCsFixer\Fixer\ArrayNotation\TrailingCommaInMultilineArrayFixer: ~ + PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer: ~ + PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer: ~ + PhpCsFixer\Fixer\ControlStructure\YodaStyleFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\NoSpacesAfterFunctionNameFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\VoidReturnFixer: ~ + PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer: ~ + PhpCsFixer\Fixer\Operator\IncrementStyleFixer: ~ + PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer: ~ # FIXME: remove + PhpCsFixer\Fixer\Phpdoc\PhpdocOrderFixer: ~ + PhpCsFixer\Fixer\Phpdoc\PhpdocScalarFixer: ~ + PhpCsFixer\Fixer\Phpdoc\PhpdocSeparationFixer: ~ + PhpCsFixer\Fixer\Phpdoc\PhpdocSummaryFixer: ~ + PhpCsFixer\Fixer\Phpdoc\PhpdocToCommentFixer: ~ + PhpCsFixer\Fixer\ReturnNotation\ReturnAssignmentFixer: ~ + PhpCsFixer\Fixer\Semicolon\MultilineWhitespaceBeforeSemicolonsFixer: ~ + PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer: ~ + PhpCsFixer\Fixer\Strict\StrictComparisonFixer: ~ + PhpCsFixer\Fixer\Strict\StrictParamFixer: ~ + PhpCsFixer\Fixer\StringNotation\SingleQuoteFixer: ~ + SlevomatCodingStandard\Sniffs\PHP\UselessParenthesesSniff: ~ + SlevomatCodingStandard\Sniffs\Variables\UselessVariableSniff: ~ + Symplify\CodingStandard\Sniffs\Naming\AbstractClassNameSniff: ~ + Symplify\CodingStandard\Sniffs\Naming\InterfaceNameSniff: ~ + Symplify\CodingStandard\Sniffs\Naming\TraitNameSniff: ~ + + indentation: tab + cache_directory: '%sys_get_temp_dir%/ecs_legacy_cache' + +services: + PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer: + syntax: long + + PhpCsFixer\Fixer\Basic\BracesFixer: + allow_single_line_closure: true + position_after_anonymous_constructs: next + position_after_control_structures: next + + PhpCsFixer\Fixer\ListNotation\ListSyntaxFixer: + syntax: long + + PhpCsFixer\Fixer\Operator\ConcatSpaceFixer: + spacing: one + + # Remove the "case" statement (see contao.yaml) + PhpCsFixer\Fixer\Whitespace\BlankLineBeforeStatementFixer: + statements: + - declare + - default + - do + - for + - foreach + - if + - return + - switch + - throw + - try + - while + + # Remove the "throws" token (see symfony.yaml) + PhpCsFixer\Fixer\Whitespace\NoExtraBlankLinesFixer: + tokens: + - curly_brace_block + - extra + - parenthesis_brace_block + - square_brace_block + - use diff --git a/config/set/contao.yaml b/config/set/contao.yaml new file mode 100644 index 0000000..5fea9de --- /dev/null +++ b/config/set/contao.yaml @@ -0,0 +1,137 @@ +imports: + - { resource: '%vendor_dir%/symplify/easy-coding-standard/config/set/php70.yaml' } + - { resource: '%vendor_dir%/symplify/easy-coding-standard/config/set/php71.yaml' } + - { resource: '%vendor_dir%/symplify/easy-coding-standard/config/set/common.yaml' } + - { resource: '%vendor_dir%/symplify/easy-coding-standard/config/set/clean-code.yaml' } + - { resource: '%vendor_dir%/symplify/easy-coding-standard/config/set/dead-code.yaml' } + - { resource: '%vendor_dir%/symplify/easy-coding-standard/config/set/symfony.yaml' } + - { resource: '%vendor_dir%/symplify/easy-coding-standard/config/set/symfony-risky.yaml' } + +parameters: + skip: + # We allow assignments in conditions (see common.yaml) + PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis\AssignmentInConditionSniff: ~ + + # We do not want explicit variables (see common.yaml) + PhpCsFixer\Fixer\LanguageConstruct\ExplicitIndirectVariableFixer: ~ + PhpCsFixer\Fixer\StringNotation\ExplicitStringVariableFixer: ~ + + # We do not want a space after the ! operator (see common.yaml) + PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer: ~ + + # We allow assertEquals() to compare objects in configuration tests (see phpunit.yaml) + PhpCsFixer\Fixer\PhpUnit\PhpUnitStrictFixer: + - '*/*BundleTest.php' + - '*/*ExtensionTest.php' + + # The method chaining identation fixer does not work for the Symfony configuration tree + PhpCsFixer\Fixer\Whitespace\MethodChainingIndentationFixer: + - '*/Configuration.php' + - '*/src/Resources/contao/*' # FIXME: remove? + + # We do not require null default values to be nullable (see php71.yaml) + SlevomatCodingStandard\Sniffs\TypeHints\NullableTypeForNullDefaultValueSniff: ~ + + # Do not report unused variables in foreach loops etc. (see clean-code.yaml) + SlevomatCodingStandard\Sniffs\Variables\UnusedVariableSniff: ~ # FIXME: remove? + +services: + PhpCsFixer\Fixer\Comment\HeaderCommentFixer: + header: "This file is part of Contao.\n\n(c) Leo Feyer\n\n@license LGPL-3.0-or-later" + + PhpCsFixer\Fixer\ConstantNotation\NativeConstantInvocationFixer: + scope: namespaced + fix_built_in: false + include: + - DIRECTORY_SEPARATOR + - PHP_SAPI + - PHP_VERSION_ID + + PhpCsFixer\Fixer\FunctionNotation\FopenFlagsFixer: + b_mode: false + + PhpCsFixer\Fixer\FunctionNotation\NativeFunctionInvocationFixer: + scope: namespaced + strict: true + include: + - '@@compiler_optimized' + + # We do not enforce blank lines before "break", "continue" and "yield" + PhpCsFixer\Fixer\Whitespace\BlankLineBeforeStatementFixer: + statements: + - case + - declare + - default + - do + - for + - foreach + - if + - return + - switch + - throw + - try + - while + + PhpCsFixer\Fixer\PhpUnit\PhpUnitTestCaseStaticMethodCallsFixer: + call_type: this + + # Add fixers from PHP-CS-Fixer + PhpCsFixer\Fixer\Alias\PowToExponentiationFixer: ~ + PhpCsFixer\Fixer\Alias\SetTypeToCastFixer: ~ + PhpCsFixer\Fixer\Casing\LowercaseStaticReferenceFixer: ~ + PhpCsFixer\Fixer\Casing\MagicMethodCasingFixer: ~ + PhpCsFixer\Fixer\Casing\NativeFunctionTypeDeclarationCasingFixer: ~ + PhpCsFixer\Fixer\CastNotation\NoUnsetCastFixer: ~ + PhpCsFixer\Fixer\ClassNotation\NoNullPropertyInitializationFixer: ~ + PhpCsFixer\Fixer\Comment\MultilineCommentOpeningClosingFixer: ~ + PhpCsFixer\Fixer\ControlStructure\IncludeFixer: ~ + PhpCsFixer\Fixer\ControlStructure\NoAlternativeSyntaxFixer: ~ + PhpCsFixer\Fixer\ControlStructure\NoSuperfluousElseifFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\CombineNestedDirnameFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\FopenFlagOrderFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\ImplodeCallFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\NoUnreachableDefaultArgumentValueFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\SingleLineThrowFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\StaticLambdaFixer: ~ + PhpCsFixer\Fixer\Import\FullyQualifiedStrictTypesFixer: ~ + PhpCsFixer\Fixer\Import\SingleImportPerStatementFixer: ~ + PhpCsFixer\Fixer\LanguageConstruct\CombineConsecutiveIssetsFixer: ~ + PhpCsFixer\Fixer\LanguageConstruct\CombineConsecutiveUnsetsFixer: ~ + PhpCsFixer\Fixer\LanguageConstruct\ErrorSuppressionFixer: ~ + PhpCsFixer\Fixer\LanguageConstruct\NoUnsetOnPropertyFixer: ~ + PhpCsFixer\Fixer\Operator\LogicalOperatorsFixer: ~ + PhpCsFixer\Fixer\Phpdoc\AlignMultilineCommentFixer: ~ + PhpCsFixer\Fixer\Phpdoc\PhpdocOrderFixer: ~ + PhpCsFixer\Fixer\Phpdoc\PhpdocVarAnnotationCorrectOrderFixer: ~ + PhpCsFixer\Fixer\PhpTag\LinebreakAfterOpeningTagFixer: ~ + PhpCsFixer\Fixer\PhpTag\NoShortEchoTagFixer: ~ + PhpCsFixer\Fixer\PhpUnit\PhpUnitDedicateAssertInternalTypeFixer: ~ + PhpCsFixer\Fixer\PhpUnit\PhpUnitExpectationFixer: ~ + PhpCsFixer\Fixer\PhpUnit\PhpUnitMockFixer: ~ + PhpCsFixer\Fixer\PhpUnit\PhpUnitMockShortWillReturnFixer: ~ + PhpCsFixer\Fixer\PhpUnit\PhpUnitNamespacedFixer: ~ + PhpCsFixer\Fixer\PhpUnit\PhpUnitNoExpectationAnnotationFixer: ~ + PhpCsFixer\Fixer\PhpUnit\PhpUnitOrderedCoversFixer: ~ + PhpCsFixer\Fixer\ReturnNotation\NoUselessReturnFixer: ~ + PhpCsFixer\Fixer\Semicolon\MultilineWhitespaceBeforeSemicolonsFixer: ~ + PhpCsFixer\Fixer\StringNotation\EscapeImplicitBackslashesFixer: ~ + PhpCsFixer\Fixer\StringNotation\HeredocToNowdocFixer: ~ + PhpCsFixer\Fixer\StringNotation\NoBinaryStringFixer: ~ + PhpCsFixer\Fixer\StringNotation\SimpleToComplexStringVariableFixer: ~ + PhpCsFixer\Fixer\StringNotation\StringLineEndingFixer: ~ + + # Add fixers and sniffs from https://github.com/Symplify/CodingStandard + Symplify\CodingStandard\Fixer\Commenting\ParamReturnAndVarTagMalformsFixer: ~ + Symplify\CodingStandard\Fixer\Commenting\RemoveEmptyDocBlockFixer: ~ + Symplify\CodingStandard\Fixer\Commenting\RemoveSuperfluousDocBlockWhitespaceFixer: ~ + Symplify\CodingStandard\Fixer\ControlStructure\RequireFollowedByAbsolutePathFixer: ~ + Symplify\CodingStandard\Sniffs\CleanCode\ForbiddenParentClassSniff: ~ + Symplify\CodingStandard\Sniffs\Commenting\AnnotationTypeExistsSniff: ~ + Symplify\CodingStandard\Sniffs\Debug\DebugFunctionCallSniff: ~ + Symplify\CodingStandard\Sniffs\Naming\AbstractClassNameSniff: ~ + Symplify\CodingStandard\Sniffs\Naming\InterfaceNameSniff: ~ + Symplify\CodingStandard\Sniffs\Naming\TraitNameSniff: ~ + + # Add custom fixers + Contao\EasyCodingStandard\Fixer\SingleLineConfigureCommandFixer: ~ + Contao\EasyCodingStandard\Sniffs\SetDefinitionCommandSniff: ~ diff --git a/config/template.yaml b/config/template.yaml new file mode 100644 index 0000000..c710b00 --- /dev/null +++ b/config/template.yaml @@ -0,0 +1,21 @@ +imports: + - { resource: set/contao.yaml } + +parameters: + file_extensions: + - html5 + + skip: + PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer: ~ + PhpCsFixer\Fixer\Comment\HeaderCommentFixer: ~ + PhpCsFixer\Fixer\ControlStructure\NoAlternativeSyntaxFixer: ~ + PhpCsFixer\Fixer\FunctionNotation\VoidReturnFixer: ~ + PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer: ~ + PhpCsFixer\Fixer\PhpTag\LinebreakAfterOpeningTagFixer: ~ + PhpCsFixer\Fixer\PhpTag\NoShortEchoTagFixer: ~ + PhpCsFixer\Fixer\Semicolon\SemicolonAfterInstructionFixer: ~ + PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer: ~ + PhpCsFixer\Fixer\Strict\StrictComparisonFixer: ~ + PhpCsFixer\Fixer\Strict\StrictParamFixer: ~ + + cache_directory: '%sys_get_temp_dir%/ecs_template_cache' diff --git a/src/Fixer/SingleLineConfigureCommandFixer.php b/src/Fixer/SingleLineConfigureCommandFixer.php new file mode 100644 index 0000000..a5ee1a1 --- /dev/null +++ b/src/Fixer/SingleLineConfigureCommandFixer.php @@ -0,0 +1,90 @@ +addOption( + "bundles", + null, + InputOption::VALUE_NONE, + "List all bundles or the bundle configuration of the given plugin" + ); + } +} +' + ), + ] + ); + } + + public function isCandidate(Tokens $tokens): bool + { + return $tokens->isAllTokenKindsFound([T_CLASS, T_FUNCTION, T_OBJECT_OPERATOR]); + } + + protected function applyFix(\SplFileInfo $file, Tokens $tokens): void + { + for ($index = 1, $count = \count($tokens); $index < $count; ++$index) { + switch (true) { + case $tokens[$index]->isGivenKind(T_CLASS): + $nextMeaningful = $tokens->getNextMeaningfulToken($index); + + // Return if the class is not a command + if ('Command' !== substr($tokens[$nextMeaningful]->getContent(), -7)) { + return; + } + break; + + case $tokens[$index]->isGivenKind(T_FUNCTION): + $nextMeaningful = $tokens->getNextMeaningfulToken($index); + + // Skip the method if it is not the configure() method + if ('configure' !== $tokens[$nextMeaningful]->getContent()) { + $nextMeaningful = $tokens->getNextMeaningfulToken($index); + + if ($tokens[$nextMeaningful]->isGivenKind('(')) { + $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextMeaningful); + } + } + break; + + case $tokens[$index]->isGivenKind(T_OBJECT_OPERATOR): + $nextMeaningful = $tokens->getNextMeaningfulToken($index); + + if (!\in_array($tokens[$nextMeaningful]->getContent(), ['addArgument', 'addOption'], true)) { + continue 2; + } + + $blockStart = $nextMeaningful + 1; + $blockEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $blockStart); + + for ($i = $blockStart; $i < $blockEnd; ++$i) { + if ($tokens[$i]->isWhitespace()) { + $tokens->clearAt($i); + } + } + } + } + } +} diff --git a/src/Sniffs/SetDefinitionCommandSniff.php b/src/Sniffs/SetDefinitionCommandSniff.php new file mode 100644 index 0000000..6385256 --- /dev/null +++ b/src/Sniffs/SetDefinitionCommandSniff.php @@ -0,0 +1,41 @@ +getTokens(); + + switch (true) { + case T_CLASS === $tokens[$index]['code']: + if ('Command' !== substr($tokens[$index + 2]['content'], -7)) { + return \count($tokens) + 1; + } + break; + + case T_FUNCTION === $tokens[$index]['code']: + $this->isConfigure = 'configure' === $tokens[$index + 2]['content']; + break; + + case T_OBJECT_OPERATOR === $tokens[$index]['code']: + if ($this->isConfigure && 'setDefinition' === $tokens[$index + 1]['content']) { + $file->addError('Do not use the setDefinition() method to configure commands. Use addArgument() and addOption() instead.', $index, self::class); + } + break; + } + + return \count($tokens) + 1; + } +}