diff --git a/.github/workflows/compatibility-check.yml b/.github/workflows/compatibility-check.yml index 5bffe42..6b2272c 100644 --- a/.github/workflows/compatibility-check.yml +++ b/.github/workflows/compatibility-check.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [Ubuntu, Windows, macOS] - php: [7.4, 8.0, 8.1] + php: [8.0, 8.1] include: - os: Ubuntu diff --git a/.gitignore b/.gitignore index ff72e2d..5e0f179 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /composer.lock /vendor +.phpunit.result.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..07ef968 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,50 @@ +in(array_filter( + [ + './app', + './config', + './database', + './public', + './resources', + './routes', + './tests', + ], + fn($dir) => is_dir($dir) + )) + ->notName('*.blade.php'); + +return (new Config()) + ->setFinder($finder) + ->setUsingCache(false) + ->registerCustomFixers([new CustomOrderedClassElementsFixer()]) + ->setRules([ + 'Tighten/custom_ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'property_public_static', + 'property_protected_static', + 'property_private_static', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public', + 'property_protected', + 'property_private', + 'construct', + 'invoke', + 'method_public_static', + 'method_protected_static', + 'method_private_static', + 'method_public', + 'method_protected', + 'method_private', + 'magic', + ], + ], + ]); diff --git a/README.md b/README.md index 7985436..b262983 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,18 @@ ![Project Banner](https://raw.githubusercontent.com/tighten/duster/main/banner.png) # Duster -Automatically apply Tighten's default code style for Laravel apps: +Automatically apply Tighten's default code style for Laravel apps. -- PHPCS, with PSR-12 + some special preferences -- Tighten's Tlint -- Maybe JS and CSS? +To achieve this, Duster installs and automatically configures the following tools: -To achieve this, this package installs PHPCS (and PHPCBF with it) and Tlint, and automatically configures them. Tlint uses the default `Tighten` preset. PHPCS uses the [`Tighten` preset](https://github.com/tighten/tighten-coding-standard) which is `PSR-12` and a few Tighten-specific rules. +- TLint: Opinionated code linter for Laravel and PHP + - using the default `Tighten` preset +- PHP_CodeSniffer: catch issues that can't be fixed automatically + - using the `Tighten` preset which is mostly PSR1 with some Tighten-specific rules +- PHP CS Fixer: custom rules not supported by Laravel Pint + - `CustomOrderedClassElementsFixer` Tighten-specific order of class elements +- Pint: Laravel's code style rules (with a few Tighten specific customizations) + - using the default `Laravel` preset with some Tighten-specific rules ## Installation @@ -15,66 +20,83 @@ You can install the package via composer: ```bash composer require tightenco/duster --dev -./vendor/bin/duster init ``` -When installing you may see the following message: +Optionally you can publish a GitHub Actions linting config: ->dealerdirect/phpcodesniffer-composer-installer contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins ->Do you trust "dealerdirect/phpcodesniffer-composer-installer" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] - -You will need to accept the [phpcodesniffer-composer-installer](https://github.com/PHPCSStandards/composer-installer) prompt to have the PHPCS rulesets and so the GitHub actions will work. - -This adds an `allowed-plugins` entry to your `composer.json` file: - -```json - ... - "config": { - ... - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - } - }, +```bash +duster github-actions ``` - -You must run `./vendor/bin/duster init` after installing, or you won't have a local copy of the PHPCS config file, and Duster won't work. - -The `init` command will also optionally add a GitHub action to run Duster's linters. - ## Usage To lint everything at once: ```bash -./vendor/bin/duster lint +duster ``` To fix everything at once: ```bash -./vendor/bin/duster fix +duster fix ``` -To run individual lints: +To view all available commands: ```bash -./vendor/bin/duster tlint -./vendor/bin/duster phpcs +duster help ``` -To run individual fixes: +## Customizing -```bash -./vendor/bin/duster tlint fix -./vendor/bin/duster phpcs fix +### TLint + +Create a `tlint.json` file in your project root. Learn more in the [TLint documentation](https://github.com/tighten/tlint#configuration). + +### PHP_CodeSniffer + +Create a `.phpcs.xml.dist` file in your project root with the following: + +```xml + + + app + config + database + public + resources + routes + tests + + + ``` -### Customizing the lints +Now you can add customizations below the `` line or even disable the Tighten rule and use your own ruleset. Learn more in this [introductory article](https://ncona.com/2012/12/creating-your-own-phpcs-standard/). + +### PHP CS Fixer + +Create a `.php-cs-fixer.dist.php` file in your project root with the contents from [Duster's `.php-cs-fixer.dist.php`](.php-cs-fixer.dist.php). Learn more in the [PHP CS Fixer documentation](https://cs.symfony.com/doc/config.html). -To override the configuration for PHPCS, you can edit the `.phpcs.xml.dist` file and add customizations below the `` line or even disable the Tighten rule and use your own ruleset. Learn more in this [introductory article](https://ncona.com/2012/12/creating-your-own-phpcs-standard/). +### Pint + +Create a `pint.json` file in your project root with the following: + +```json +{ + "preset": "laravel", + "rules": { + "concat_space": { + "spacing": "one" + }, + "class_attributes_separation": { + } + } +} +``` -To override the configuration for Tlint, create a `tlint.json` file in your project root. Learn more in the [Tlint documentation](https://github.com/tighten/tlint#configuration). +Now you can add or remove customizations. Learn more in the [Pint documentation](https://laravel.com/docs/pint#configuring-pint). ## Contributing diff --git a/bin/actions/fix b/bin/actions/fix deleted file mode 100755 index 3912eb0..0000000 --- a/bin/actions/fix +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -. ${BIN_DIR}/actions/fix-phpcs -. ${BIN_DIR}/actions/fix-tlint diff --git a/bin/actions/fix-phpcs b/bin/actions/fix-phpcs deleted file mode 100755 index 51948c2..0000000 --- a/bin/actions/fix-phpcs +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [[ -f "./.phpcs.xml.dist" ]]; then - vendor/bin/phpcbf -else - printf "\nYou must run ./vendor/bin/duster init before using Duster's PHPCS fix.\n" -fi diff --git a/bin/actions/fix-tlint b/bin/actions/fix-tlint deleted file mode 100755 index 896031d..0000000 --- a/bin/actions/fix-tlint +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -vendor/bin/tlint format --no-interaction -v diff --git a/bin/actions/help b/bin/actions/help deleted file mode 100755 index 247e0e9..0000000 --- a/bin/actions/help +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -printf "\nUsage:\n\n" -cat << HelpText -duster lint with all -duster fix fix with all -duster phpcs lint with phpcs -duster phpcs fix fix with phpcbf (phpcs fixer) -duster tlint lint with tlint -duster tlint fix fix with tlint -duster init initialize -HelpText diff --git a/bin/actions/init b/bin/actions/init deleted file mode 100755 index be6fdb7..0000000 --- a/bin/actions/init +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash - -printf "\nPublishing PHPCS config...\n\n" - -phpcs_filename=".phpcs.xml.dist" - -if [ -f "$phpcs_filename" ]; then - printf "$phpcs_filename already exists.\n" -else - cp ${BIN_DIR}/../stubs/.phpcs.xml.dist $phpcs_filename - printf "Created $phpcs_filename.\n" -fi - -printf "\nChecking GitHub Action...\n\n" - -gh_action_filename=".github/workflows/lint.yml" - -if [ -f "$gh_action_filename" ]; then - printf "$gh_action_filename already exists.\n" -else - echo -n "Would you like a GitHub action added to this repo for running lints? (Y/n) " - read addGitHubAction - - if [ "$addGitHubAction" != "${addGitHubAction#[Yy]}" ]; then - printf "\nAdding GitHub Actions workflow...\n" - - mkdir -p .github/workflows - cp ${BIN_DIR}/../stubs/github-actions/lint.yml $gh_action_filename - - read -p "Enter the name of your primary branch [main]: " primary_branch - primary_branch=${primary_branch:-main} - sed -i '' "s/YOUR_BRANCH_NAME/${primary_branch}/g" $gh_action_filename - - read -p "Enter your PHP version [8.1]: " php_version - php_version=${php_version:-8.1} - sed -i '' "s/YOUR_PHP_VERSION/${php_version}/g" $gh_action_filename - - printf "\nCreated $gh_action_filename.\n" - else - printf "\nSkipping GitHub Action.\n" - fi -fi diff --git a/bin/actions/lint b/bin/actions/lint deleted file mode 100755 index 381b459..0000000 --- a/bin/actions/lint +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -. ${BIN_DIR}/actions/lint-tlint -. ${BIN_DIR}/actions/lint-phpcs diff --git a/bin/actions/lint-phpcs b/bin/actions/lint-phpcs deleted file mode 100755 index 1ef316b..0000000 --- a/bin/actions/lint-phpcs +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -e - -if [[ -f "./.phpcs.xml.dist" ]]; then - vendor/bin/phpcs -else - printf "\nYou must run ./vendor/bin/duster init before using Duster's PHPCS lint.\n" -fi diff --git a/bin/actions/lint-tlint b/bin/actions/lint-tlint deleted file mode 100755 index 7440e8c..0000000 --- a/bin/actions/lint-tlint +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -vendor/bin/tlint lint --no-interaction -v diff --git a/bin/duster b/bin/duster index b228814..124a279 100755 --- a/bin/duster +++ b/bin/duster @@ -1,41 +1,305 @@ #!/usr/bin/env bash -# ./bin/duster: lint with all -# ./bin/duster fix: fix with all -# ./bin/duster phpcs: lint with phpcs -# ./bin/duster phpcs fix: fix with phpcbf (phpcs fixer) -# ./bin/duster tlint: lint with tlint -# ./bin/duster tlint fix: fix with tlint -# ./bin/duster init: initialize - DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" SYM_DIR="$( dirname "$( readlink ${BASH_SOURCE[0]} )" )" BIN_DIR="${DIR}/${SYM_DIR}" -if [[ $# == 0 ]]; then - filename="lint" -elif [[ $# == 1 ]]; then - if [[ $1 == "fix" ]]; then - filename="fix" - elif [[ $1 == 'init' ]]; then - filename="init" - elif [[ $1 == 'help' ]]; then - filename="help" - elif [[ $1 == 'lint' ]]; then - filename="lint" +GREEN='\033[32m' +YELLOW='\033[33m' +RED='\033[31m' +NOCOLOR='\033[0m' + +PASS_TLINT=true +PASS_PHPCS=true +PASS_PHPCSFIXER=true +PASS_PINT=true + +# Clean Duster arguments to pass to the underlying linters +function _args() { + for arg in "$@"; do + [[ ! $arg == 'github-actions' ]] \ + && [[ ! $arg == 'pint' ]] \ + && [[ ! $arg == 'phpcsfixer' ]] \ + && [[ ! $arg == 'phpcs' ]] \ + && [[ ! $arg == 'tlint' ]] \ + && [[ ! $arg == 'fix' ]] \ + && [[ ! $arg == 'lint' ]] \ + && args+=("$arg") + done + + echo "${args[@]}" +} + +function _heading() { + terminal_width="$(tput cols)" + padding="$(printf '%0.1s' ={1..500})" + printf '%*.*s \e[32m%s\e[0m %*.*s' 0 "$(((terminal_width-2-${#1})/2))" "$padding" "$1" 0 "$(((terminal_width-1-${#1})/2))" "$padding" +} + +############################################################ +# Help # +############################################################ + +function _help() +{ + echo -e "Duster Commands:" + echo -e "${GREEN}duster${NOCOLOR} lint using ${YELLOW}all${NOCOLOR}" + echo -e "${GREEN}duster lint${NOCOLOR} lint using ${YELLOW}all${NOCOLOR}" + echo -e "${GREEN}duster fix${NOCOLOR} fix using ${YELLOW}all${NOCOLOR}" + echo -e "${GREEN}duster tlint${NOCOLOR} lint using ${YELLOW}TLint${NOCOLOR}" + echo -e "${GREEN}duster tlint fix${NOCOLOR} fix using ${YELLOW}TLint${NOCOLOR}" + echo -e "${GREEN}duster phpcs${NOCOLOR} lint using ${YELLOW}PHP_CodeSniffer${NOCOLOR}" + echo -e "${GREEN}duster phpcs fix${NOCOLOR} fix using ${YELLOW}PHP_CodeSniffer${NOCOLOR}" + echo -e "${GREEN}duster phpcsfixer${NOCOLOR} lint using ${YELLOW}PHP CS Fixer${NOCOLOR}" + echo -e "${GREEN}duster phpcsfixer fix${NOCOLOR} fix using ${YELLOW}PHP CS Fixer${NOCOLOR}" + echo -e "${GREEN}duster pint${NOCOLOR} lint using ${YELLOW}Pint${NOCOLOR}" + echo -e "${GREEN}duster pint fix${NOCOLOR} fix using ${YELLOW}Pint${NOCOLOR}" + echo -e "${GREEN}duster github-actions${NOCOLOR} publish GitHub Actions" + echo -e "${GREEN}duster help${NOCOLOR} display this help" +} + +############################################################ +# GitHub Actions # +############################################################ + +function _github_actions() +{ + printf "\nChecking GitHub Actions...\n\n" + + gh_actions_filename=".github/workflows/lint.yml" + + if [ -f "$gh_actions_filename" ]; then + printf "$gh_actions_filename already exists.\n" else - filename="lint-$1" + printf "\nAdding GitHub Actions workflow...\n" + + mkdir -p .github/workflows + cp ${BIN_DIR}/../stubs/github-actions/lint.yml $gh_actions_filename + + read -p "Enter the name of your primary branch [main]: " primary_branch + primary_branch=${primary_branch:-main} + sed -i '' "s/YOUR_BRANCH_NAME/${primary_branch}/g" $gh_actions_filename + + read -p "Enter your PHP version [8.1]: " php_version + php_version=${php_version:-8.1} + sed -i '' "s/YOUR_PHP_VERSION/${php_version}/g" $gh_actions_filename + + printf "\nCreated $gh_actions_filename.\n" fi -else - filename="fix-$1" -fi +} + +############################################################ +# Pint # +############################################################ + +function _pint_fix() +{ + if [[ -f "./.pint.json" ]]; then + vendor/bin/pint $@ + else + vendor/bin/pint --config vendor/tightenco/duster/pint.json $@ + fi + + if [ $? -ne 0 ]; then + PASS_PINT=false + fi +} + +function _pint_lint() +{ + if [[ -f "./.pint.json" ]]; then + vendor/bin/pint --test $@ + else + vendor/bin/pint --config vendor/tightenco/duster/pint.json --test $@ + fi + + if [ $? -ne 0 ]; then + PASS_PINT=false + fi +} + +############################################################ +# PHP CS Fixer # +############################################################ + +function _phpcsfixer_fix() +{ + if [[ -f "./.php-cs-fixer.dist.php" ]] || [[ -f "./.php-cs-fixer.php" ]]; then + vendor/bin/php-cs-fixer fix $@ + else + vendor/bin/php-cs-fixer fix --config=vendor/tightenco/duster/.php-cs-fixer.dist.php $@ + fi + + if [ $? -ne 0 ]; then + PASS_PHPCSFIXER=false + fi +} -filename="$BIN_DIR/actions/$filename" +function _phpcsfixer_lint() +{ + if [[ -f "./.php-cs-fixer.dist.php" ]] || [[ -f "./.php-cs-fixer.php" ]]; then + vendor/bin/php-cs-fixer fix --diff --dry-run $@ + else + vendor/bin/php-cs-fixer fix --config=vendor/tightenco/duster/.php-cs-fixer.dist.php --diff --dry-run $@ + fi + + if [ $? -ne 0 ]; then + PASS_PHPCSFIXER=false + fi +} + +############################################################ +# PHP PHP_CodeSniffer # +############################################################ + +function _phpcs_fix() +{ + if [[ -f "./.phpcs.xml" ]] || [[ -f "./phpcs.xml" ]] || [[ -f "./.phpcs.xml.dist" ]] || [[ -f "./phpcs.xml.dist" ]]; then + vendor/bin/phpcbf --config-set installed_paths ../../tightenco/duster/standards/Tighten > /dev/null + vendor/bin/phpcbf $@ + if [ $? -ne 0 ]; then + PASS_PHPCS=false + fi + + vendor/bin/phpcs -n $@ > /dev/null + else + args="app config database public resources routes tests" && [[ -n "$@" ]] && args=$@ + vendor/bin/phpcbf --standard=vendor/tightenco/duster/standards/Tighten/ $args + if [ $? -ne 0 ]; then + PASS_PHPCS=false + fi + + vendor/bin/phpcs -n --standard=vendor/tightenco/duster/standards/Tighten/ $args > /dev/null + fi + + if [ $? -ne 0 ]; then + echo -e "${RED}WARNING${NOCOLOR} PHP Code_Sniffer found errors that cannot be fixed automatically. Run 'duster phpcs' to view them.\n" + fi +} + +function _phpcs_lint() +{ + if [[ -f "./.phpcs.xml" ]] || [[ -f "./phpcs.xml" ]] || [[ -f "./.phpcs.xml.dist" ]] || [[ -f "./phpcs.xml.dist" ]]; then + vendor/bin/phpcs --config-set installed_paths ../../tightenco/duster/standards/Tighten > /dev/null + vendor/bin/phpcs $@ + else + args="app config database public resources routes tests" && [[ -n "$@" ]] && args=$@ + vendor/bin/phpcs --standard=vendor/tightenco/duster/standards/Tighten/ $args + fi + + if [ $? -ne 0 ]; then + PASS_PHPCS=false + fi +} -if [ ! -f "$filename" ]; then - printf "\nSorry, that is an invalid selection.\n[$filename does not exist]\n" +############################################################ +# TLint # +############################################################ - exit +function _tlint_fix() +{ + if [ -z "$@" ]; then + vendor/bin/tlint --ansi format + if [ $? -ne 0 ]; then + PASS_TLINT=false + fi + fi + + for path in $@ + do + vendor/bin/tlint --ansi format "$path" + if [ $? -ne 0 ]; then + PASS_TLINT=false + fi + done +} + +function _tlint_lint() +{ + if [ -z "$@" ]; then + vendor/bin/tlint --ansi lint + if [ $? -ne 0 ]; then + PASS_TLINT=false + fi + fi + + for path in $@ + do + vendor/bin/tlint --ansi lint "$path" + if [ $? -ne 0 ]; then + PASS_TLINT=false + fi + done +} + +############################################################ +# All Tools # +############################################################ + +function _lint() +{ + _heading 'TLint' + _tlint_lint $@ + _heading 'PHP CodeSniffer' + _phpcs_lint $@ + _heading 'PHP CS Fixer' + _phpcsfixer_lint $@ + _heading 'Pint' + _pint_lint $@ + + if [ $PASS_TLINT == false ] || [ $PASS_PHPCS == false ] || [ $PASS_PHPCSFIXER == false ] || [ $PASS_PINT == false ]; then + exit 1 + else + exit 0 + fi +} + +function _fix() +{ + _heading 'TLint' + _tlint_fix $@ + _heading 'PHP CodeSniffer' + _phpcs_fix $@ + _heading 'PHP CS Fixer' + _phpcsfixer_fix $@ + _heading 'Pint' + _pint_fix $@ + + if [ $PASS_TLINT == false ] || [ $PASS_PHPCS == false ] || [ $PASS_PHPCSFIXER == false ] || [ $PASS_PINT == false ]; then + exit 1 + else + exit 0 + fi +} + +############################################################ +# Main # +############################################################ + +if [[ "$*" == *"help"* ]]; then + _help $(_args $@) +elif [[ "$*" == *"github-actions"* ]]; then + _github_actions $(_args $@) +elif [[ "$*" == *"pint"* ]] && [[ "$*" == *"fix"* ]]; then + _pint_fix $(_args $@) +elif [[ "$*" == *"pint"* ]]; then + _pint_lint $(_args $@) +elif [[ "$*" == *"phpcsfixer"* ]] && [[ "$*" == *"fix"* ]]; then + _phpcsfixer_fix $(_args $@) +elif [[ "$*" == *"phpcsfixer"* ]]; then + _phpcsfixer_lint $(_args $@) +elif [[ "$*" == *"phpcs"* ]] && [[ "$*" == *"fix"* ]]; then + _phpcs_fix $(_args $@) +elif [[ "$*" == *"phpcs"* ]]; then + _phpcs_lint $(_args $@) +elif [[ "$*" == *"tlint"* ]] && [[ "$*" == *"fix"* ]]; then + _tlint_fix $(_args $@) +elif [[ "$*" == *"tlint"* ]]; then + _tlint_lint $(_args $@) +elif [[ "$*" == *"fix"* ]]; then + _fix $(_args $@) +else + _lint $(_args $@) fi -. $filename +exit 1 diff --git a/composer.json b/composer.json index 86eea47..11c9db3 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,6 @@ "tightenco", "duster", "php", - "phpcs", "code style", "laravel" ], @@ -20,18 +19,26 @@ } ], "require": { - "php": "^7.4|^8.0|^8.1", + "php": "^8.0|^8.1", "phpunit/phpunit": "^9.0", - "squizlabs/php_codesniffer": "^3.5", - "tightenco/tighten-coding-standard": "^1.0", + "friendsofphp/php-cs-fixer": "^3.11", + "laravel/pint": "^1.2", + "squizlabs/php_codesniffer": "^3.7", "tightenco/tlint": "^6.0" }, - "config": { - "sort-packages": true, - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "autoload": { + "psr-4": { + "Tighten\\Duster\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tighten\\Duster\\Tests\\": "tests" } }, + "config": { + "sort-packages": true + }, "minimum-stability": "dev", "prefer-stable": true, "bin": [ diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..dc60af7 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + src/ + + + + + tests + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..6dc9acd --- /dev/null +++ b/pint.json @@ -0,0 +1,17 @@ +{ + "preset": "laravel", + "rules": { + "concat_space": { + "spacing": "one" + }, + "class_attributes_separation": { + "elements": { + "method": "one" + } + }, + "php_unit_test_annotation": { + "style": "annotation" + }, + "blank_line_between_import_groups": true + } +} diff --git a/src/.gitkeep b/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Fixer/ClassNotation/CustomOrderedClassElementsFixer.php b/src/Fixer/ClassNotation/CustomOrderedClassElementsFixer.php new file mode 100644 index 0000000..9d0db43 --- /dev/null +++ b/src/Fixer/ClassNotation/CustomOrderedClassElementsFixer.php @@ -0,0 +1,544 @@ + + * Dariusz RumiƄski + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + * + * Modified OrderedClassElementsFixer to include invoke + * @see https://github.com/FriendsOfPHP/PHP-CS-Fixer/pull/6244 + */ + +namespace Tighten\Duster\Fixer\ClassNotation; + +use PhpCsFixer\AbstractFixer; +use PhpCsFixer\Fixer\ConfigurableFixerInterface; +use PhpCsFixer\FixerConfiguration\AllowedValueSubset; +use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; +use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; +use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; +use PhpCsFixer\FixerDefinition\CodeSample; +use PhpCsFixer\FixerDefinition\FixerDefinition; +use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; +use PhpCsFixer\Tokenizer\CT; +use PhpCsFixer\Tokenizer\Token; +use PhpCsFixer\Tokenizer\Tokens; + +/** + * @author Gregor Harlan + */ +final class CustomOrderedClassElementsFixer extends AbstractFixer implements ConfigurableFixerInterface +{ + /** + * @var array Array containing all class element base types (keys) and their parent types (values) + */ + private static $typeHierarchy = [ + 'use_trait' => null, + 'public' => null, + 'protected' => null, + 'private' => null, + 'constant' => null, + 'constant_public' => ['constant', 'public'], + 'constant_protected' => ['constant', 'protected'], + 'constant_private' => ['constant', 'private'], + 'property' => null, + 'property_static' => ['property'], + 'property_public' => ['property', 'public'], + 'property_protected' => ['property', 'protected'], + 'property_private' => ['property', 'private'], + 'property_public_readonly' => ['property_readonly', 'property_public'], + 'property_protected_readonly' => ['property_readonly', 'property_protected'], + 'property_private_readonly' => ['property_readonly', 'property_private'], + 'property_public_static' => ['property_static', 'property_public'], + 'property_protected_static' => ['property_static', 'property_protected'], + 'property_private_static' => ['property_static', 'property_private'], + 'method' => null, + 'method_abstract' => ['method'], + 'method_static' => ['method'], + 'method_public' => ['method', 'public'], + 'method_protected' => ['method', 'protected'], + 'method_private' => ['method', 'private'], + 'method_public_abstract' => ['method_abstract', 'method_public'], + 'method_protected_abstract' => ['method_abstract', 'method_protected'], + 'method_private_abstract' => ['method_abstract', 'method_private'], + 'method_public_abstract_static' => ['method_abstract', 'method_static', 'method_public'], + 'method_protected_abstract_static' => ['method_abstract', 'method_static', 'method_protected'], + 'method_private_abstract_static' => ['method_abstract', 'method_static', 'method_private'], + 'method_public_static' => ['method_static', 'method_public'], + 'method_protected_static' => ['method_static', 'method_protected'], + 'method_private_static' => ['method_static', 'method_private'], + ]; + + /** + * @var array Array containing special method types + */ + private static $specialTypes = [ + 'construct' => null, + 'destruct' => null, + 'invoke' => null, + 'magic' => null, + 'phpunit' => null, + ]; + + /** @internal */ + public const SORT_ALPHA = 'alpha'; + + /** @internal */ + public const SORT_NONE = 'none'; + + private const SUPPORTED_SORT_ALGORITHMS = [ + self::SORT_NONE, + self::SORT_ALPHA, + ]; + + /** + * @var array Resolved configuration array (type => position) + */ + private $typePosition; + + public function getName(): string + { + return 'Tighten/custom_ordered_class_elements'; + } + + /** + * {@inheritdoc} + */ + public function configure(array $configuration): void + { + parent::configure($configuration); + + $this->typePosition = []; + $pos = 0; + + foreach ($this->configuration['order'] as $type) { + $this->typePosition[$type] = $pos++; + } + + foreach (self::$typeHierarchy as $type => $parents) { + if (isset($this->typePosition[$type])) { + continue; + } + + if (! $parents) { + $this->typePosition[$type] = null; + + continue; + } + + foreach ($parents as $parent) { + if (isset($this->typePosition[$parent])) { + $this->typePosition[$type] = $this->typePosition[$parent]; + + continue 2; + } + } + + $this->typePosition[$type] = null; + } + + $lastPosition = \count($this->configuration['order']); + + foreach ($this->typePosition as &$pos) { + if (null === $pos) { + $pos = $lastPosition; + } + + $pos *= 10; // last digit is used by phpunit method ordering + } + } + + /** + * {@inheritdoc} + */ + public function isCandidate(Tokens $tokens): bool + { + return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds()); + } + + /** + * {@inheritdoc} + */ + public function getDefinition(): FixerDefinitionInterface + { + return new FixerDefinition( + 'Orders the elements of classes/interfaces/traits.', + [ + new CodeSample( + ' ['method_private', 'method_public']] + ), + new CodeSample( + ' ['method_public'], 'sort_algorithm' => self::SORT_ALPHA] + ), + ] + ); + } + + /** + * {@inheritdoc} + * + * Must run before ClassAttributesSeparationFixer, NoBlankLinesAfterClassOpeningFixer, SpaceAfterSemicolonFixer. + * Must run after NoPhp4ConstructorFixer, ProtectedToPrivateFixer. + */ + public function getPriority(): int + { + return 65; + } + + /** + * {@inheritdoc} + */ + protected function applyFix(\SplFileInfo $file, Tokens $tokens): void + { + for ($i = 1, $count = $tokens->count(); $i < $count; $i++) { + if (! $tokens[$i]->isClassy()) { + continue; + } + + $i = $tokens->getNextTokenOfKind($i, ['{']); + $elements = $this->getElements($tokens, $i); + + if (0 === \count($elements)) { + continue; + } + + $sorted = $this->sortElements($elements); + $endIndex = $elements[\count($elements) - 1]['end']; + + if ($sorted !== $elements) { + $this->sortTokens($tokens, $i, $endIndex, $sorted); + } + + $i = $endIndex; + } + } + + /** + * {@inheritdoc} + */ + protected function createConfigurationDefinition(): FixerConfigurationResolverInterface + { + return new FixerConfigurationResolver([ + (new FixerOptionBuilder('order', 'List of strings defining order of elements.')) + ->setAllowedTypes(['array']) + ->setAllowedValues([new AllowedValueSubset(array_keys(array_merge(self::$typeHierarchy, self::$specialTypes)))]) + ->setDefault([ + 'use_trait', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public', + 'property_protected', + 'property_private', + 'construct', + 'destruct', + 'magic', + 'phpunit', + 'method_public', + 'method_protected', + 'method_private', + ]) + ->getOption(), + (new FixerOptionBuilder('sort_algorithm', 'How multiple occurrences of same type statements should be sorted')) + ->setAllowedValues(self::SUPPORTED_SORT_ALGORITHMS) + ->setDefault(self::SORT_NONE) + ->getOption(), + ]); + } + + /** + * @return array[] + */ + private function getElements(Tokens $tokens, int $startIndex): array + { + static $elementTokenKinds = [CT::T_USE_TRAIT, T_CONST, T_VARIABLE, T_FUNCTION]; + + $startIndex++; + $elements = []; + + while (true) { + $element = [ + 'start' => $startIndex, + 'visibility' => 'public', + 'abstract' => false, + 'static' => false, + 'readonly' => false, + ]; + + for ($i = $startIndex; ; $i++) { + $token = $tokens[$i]; + + // class end + if ($token->equals('}')) { + return $elements; + } + + if ($token->isGivenKind(T_ABSTRACT)) { + $element['abstract'] = true; + + continue; + } + + if ($token->isGivenKind(T_STATIC)) { + $element['static'] = true; + + continue; + } + + if (\defined('T_READONLY') && $token->isGivenKind(T_READONLY)) { // @TODO: drop condition when PHP 8.1+ is required + $element['readonly'] = true; + } + + if ($token->isGivenKind([T_PROTECTED, T_PRIVATE])) { + $element['visibility'] = strtolower($token->getContent()); + + continue; + } + + if (! $token->isGivenKind($elementTokenKinds)) { + continue; + } + + $type = $this->detectElementType($tokens, $i); + + if (\is_array($type)) { + $element['type'] = $type[0]; + $element['name'] = $type[1]; + } else { + $element['type'] = $type; + } + + if ('property' === $element['type']) { + $element['name'] = $tokens[$i]->getContent(); + } elseif (\in_array($element['type'], ['use_trait', 'constant', 'method', 'magic', 'construct', 'destruct', 'invoke'], true)) { + $element['name'] = $tokens[$tokens->getNextMeaningfulToken($i)]->getContent(); + } + + $element['end'] = $this->findElementEnd($tokens, $i); + + break; + } + + $elements[] = $element; + $startIndex = $element['end'] + 1; + } + } + + /** + * @return array|string type or array of type and name + */ + private function detectElementType(Tokens $tokens, int $index) + { + $token = $tokens[$index]; + + if ($token->isGivenKind(CT::T_USE_TRAIT)) { + return 'use_trait'; + } + + if ($token->isGivenKind(T_CONST)) { + return 'constant'; + } + + if ($token->isGivenKind(T_VARIABLE)) { + return 'property'; + } + + $nameToken = $tokens[$tokens->getNextMeaningfulToken($index)]; + + if ($nameToken->equals([T_STRING, '__construct'], false)) { + return 'construct'; + } + + if ($nameToken->equals([T_STRING, '__destruct'], false)) { + return 'destruct'; + } + + if ($nameToken->equals([T_STRING, '__invoke'], false)) { + return 'invoke'; + } + + if ( + $nameToken->equalsAny([ + [T_STRING, 'setUpBeforeClass'], + [T_STRING, 'doSetUpBeforeClass'], + [T_STRING, 'tearDownAfterClass'], + [T_STRING, 'doTearDownAfterClass'], + [T_STRING, 'setUp'], + [T_STRING, 'doSetUp'], + [T_STRING, 'assertPreConditions'], + [T_STRING, 'assertPostConditions'], + [T_STRING, 'tearDown'], + [T_STRING, 'doTearDown'], + ], false) + ) { + return ['phpunit', strtolower($nameToken->getContent())]; + } + + return str_starts_with($nameToken->getContent(), '__') ? 'magic' : 'method'; + } + + private function findElementEnd(Tokens $tokens, int $index): int + { + $index = $tokens->getNextTokenOfKind($index, ['{', ';']); + + if ($tokens[$index]->equals('{')) { + $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index); + } + + for (++$index; $tokens[$index]->isWhitespace(" \t") || $tokens[$index]->isComment(); $index++); + + $index--; + + return $tokens[$index]->isWhitespace() ? $index - 1 : $index; + } + + /** + * @param array[] $elements + * @return array[] + */ + private function sortElements(array $elements): array + { + static $phpunitPositions = [ + 'setupbeforeclass' => 1, + 'dosetupbeforeclass' => 2, + 'teardownafterclass' => 3, + 'doteardownafterclass' => 4, + 'setup' => 5, + 'dosetup' => 6, + 'assertpreconditions' => 7, + 'assertpostconditions' => 8, + 'teardown' => 9, + 'doteardown' => 10, + ]; + + foreach ($elements as &$element) { + $type = $element['type']; + + if (\array_key_exists($type, self::$specialTypes)) { + if (isset($this->typePosition[$type])) { + $element['position'] = $this->typePosition[$type]; + + if ('phpunit' === $type) { + $element['position'] += $phpunitPositions[$element['name']]; + } + + continue; + } + + $type = 'method'; + } + + if (\in_array($type, ['constant', 'property', 'method'], true)) { + $type .= '_' . $element['visibility']; + + if ($element['abstract']) { + $type .= '_abstract'; + } + + if ($element['static']) { + $type .= '_static'; + } + + if ($element['readonly']) { + $type .= '_readonly'; + } + } + + $element['position'] = $this->typePosition[$type]; + } + + unset($element); + + usort($elements, function (array $a, array $b): int { + if ($a['position'] === $b['position']) { + return $this->sortGroupElements($a, $b); + } + + return $a['position'] <=> $b['position']; + }); + + return $elements; + } + + private function sortGroupElements(array $a, array $b): int + { + $selectedSortAlgorithm = $this->configuration['sort_algorithm']; + + if (self::SORT_ALPHA === $selectedSortAlgorithm) { + return strcasecmp($a['name'], $b['name']); + } + + return $a['start'] <=> $b['start']; + } + + /** + * @param array[] $elements + */ + private function sortTokens(Tokens $tokens, int $startIndex, int $endIndex, array $elements): void + { + $replaceTokens = []; + + foreach ($elements as $element) { + for ($i = $element['start']; $i <= $element['end']; $i++) { + $replaceTokens[] = clone $tokens[$i]; + } + } + + $tokens->overrideRange($startIndex + 1, $endIndex, $replaceTokens); + } +} diff --git a/standards/Tighten/ruleset.xml b/standards/Tighten/ruleset.xml new file mode 100644 index 0000000..0440c06 --- /dev/null +++ b/standards/Tighten/ruleset.xml @@ -0,0 +1,70 @@ + + + Tighten PHP CS rules for Laravel + + + */cache/* + */*.js + */*.css + */*.xml + */*.blade.php + */autoload.php + */storage/* + */docs/* + */vendor/* + */migrations/* + + + + + + + + + /public/index.php + + + + + + + + */database/* + */tests/* + + + + + + + + + + + + + + */tests/* + + + + + + + + + + + + + + + + + /config/* + + + + + + diff --git a/stubs/.phpcs.xml.dist b/stubs/.phpcs.xml.dist deleted file mode 100644 index 5ffcf8e..0000000 --- a/stubs/.phpcs.xml.dist +++ /dev/null @@ -1,12 +0,0 @@ - - - app - config - database - public - resources - routes - tests - - - diff --git a/stubs/github-actions/lint.yml b/stubs/github-actions/lint.yml index 8d79477..ea2ab33 100644 --- a/stubs/github-actions/lint.yml +++ b/stubs/github-actions/lint.yml @@ -6,8 +6,8 @@ on: pull_request: jobs: - phpcs: - name: PHPCS + duster: + name: Duster Lint runs-on: ubuntu-latest @@ -24,28 +24,5 @@ jobs: - name: Install dependencies run: composer install --no-interaction --no-suggest --ignore-platform-reqs - - name: PHPCS Lint - run: vendor/bin/phpcs - - tlint: - name: TLint - - continue-on-error: false - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: YOUR_PHP_VERSION - extensions: posix, dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick - coverage: none - - - name: Install dependencies - run: composer install --no-interaction --no-suggest --ignore-platform-reqs - - - name: Tlint Lint - run: vendor/bin/tlint + - name: Duster Lint + run: vendor/bin/duster diff --git a/tests/Fixer/ClassNotation/CustomOrderedClassElements/.php-cs-fixer.php b/tests/Fixer/ClassNotation/CustomOrderedClassElements/.php-cs-fixer.php new file mode 100644 index 0000000..4953747 --- /dev/null +++ b/tests/Fixer/ClassNotation/CustomOrderedClassElements/.php-cs-fixer.php @@ -0,0 +1,33 @@ +setUsingCache(false) + ->registerCustomFixers([new CustomOrderedClassElementsFixer()]) + ->setRules([ + 'Tighten/custom_ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'property_public_static', + 'property_protected_static', + 'property_private_static', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public', + 'property_protected', + 'property_private', + 'construct', + 'invoke', + 'method_public_static', + 'method_protected_static', + 'method_private_static', + 'method_public', + 'method_protected', + 'method_private', + 'magic', + ], + ], + ]); diff --git a/tests/Fixer/ClassNotation/CustomOrderedClassElements/CustomOrderedClassElementsFixerTest.php b/tests/Fixer/ClassNotation/CustomOrderedClassElements/CustomOrderedClassElementsFixerTest.php new file mode 100644 index 0000000..93b2e91 --- /dev/null +++ b/tests/Fixer/ClassNotation/CustomOrderedClassElements/CustomOrderedClassElementsFixerTest.php @@ -0,0 +1,70 @@ +assertSame($expected, file_get_contents($file)); + } + + public function provideFixCases(): array + { + return [ + [ + <<<'EOT' +