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'
+