├── src
└── WorksomeSniff
│ ├── Sniffs
│ ├── Comments
│ │ └── DisallowTodoCommentsSniff.php
│ ├── Laravel
│ │ ├── DisallowEnvUsageSniff.php
│ │ ├── ConfigFilenameKebabCaseSniff.php
│ │ ├── EventListenerSuffixSniff.php
│ │ ├── DisallowBladeOutsideOfResourcesDirectorySniff.php
│ │ └── DisallowHasFactorySniff.php
│ ├── PhpDoc
│ │ ├── DisallowParamNoTypeOrCommentSniff.php
│ │ └── PropertyDollarSignSniff.php
│ ├── Classes
│ │ └── ExceptionSuffixSniff.php
│ └── Functions
│ │ └── DisallowCompactUsageSniff.php
│ ├── Support
│ └── PropertyDoc.php
│ └── ruleset.xml
├── .github
└── workflows
│ └── main.yml
├── LICENSE
├── composer.json
└── README.md
/src/WorksomeSniff/Sniffs/Comments/DisallowTodoCommentsSniff.php:
--------------------------------------------------------------------------------
1 | getTokensAsString($stackPtr, 1), 'TODO') === false) {
23 | return;
24 | }
25 |
26 | $phpcsFile->addError(
27 | "Comments with TODO are disallowed",
28 | $stackPtr,
29 | self::class
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 |
6 | jobs:
7 | setup:
8 | timeout-minutes: 30
9 | runs-on: ubuntu-latest
10 | name: Pest on PHP ${{ matrix.php }} with composer ${{ matrix.dependencies }}
11 | strategy:
12 | matrix:
13 | php: ["8.0", "8.1"]
14 | dependencies: ["lowest", "highest"]
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v2
19 |
20 | - name: Setup PHP
21 | uses: shivammathur/setup-php@v2
22 | with:
23 | coverage: none
24 | php-version: ${{ matrix.php }}
25 | env:
26 | COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 |
28 | - name: Install Composer dependencies
29 | uses: ramsey/composer-install@v2
30 | with:
31 | dependency-versions: ${{ matrix.dependencies }}
32 |
33 | - name: Test php code
34 | run: vendor/bin/pest
35 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/Laravel/DisallowEnvUsageSniff.php:
--------------------------------------------------------------------------------
1 | getTokensAsString($stackPtr, 1);
20 | $path = $phpcsFile->getFilename();
21 |
22 | // Check if method is env method.
23 | if (strtolower($string) !== 'env') {
24 | return;
25 | }
26 |
27 | // Allow `env` usage in config files.
28 | if (str_contains($path, 'config/')) {
29 | return;
30 | }
31 |
32 | $phpcsFile->addError(
33 | "Usage of env in non-config file is disallowed.",
34 | $stackPtr,
35 | self::class
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/Laravel/ConfigFilenameKebabCaseSniff.php:
--------------------------------------------------------------------------------
1 | getFilename();
21 |
22 | // Filter away non config files.
23 | if (!str_contains($path, 'config/')) {
24 | return;
25 | }
26 |
27 | $filenameWithExtension = basename($path);
28 | $filename = Str::before($filenameWithExtension, '.');
29 |
30 | if (Str::kebab($filename) === $filename) {
31 | return;
32 | }
33 |
34 | $phpcsFile->addError(
35 | "Config files should be named with kebab-case.",
36 | 0,
37 | self::class,
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Worksome
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/Laravel/EventListenerSuffixSniff.php:
--------------------------------------------------------------------------------
1 | getFilename();
23 |
24 | // Filter away non listener classes
25 | if (!str_contains($path, 'app/Listeners/')) {
26 | return;
27 | }
28 |
29 | $classNamePointer = $stackPtr + 2;
30 | $className = $phpcsFile->getTokensAsString($classNamePointer, 1);
31 |
32 | // Check if class is named with Listener suffix.
33 | if (Str::endsWith($className, $this->suffix)) {
34 | return;
35 | }
36 |
37 | $phpcsFile->addError(
38 | "Listeners should have `Listener` suffix.",
39 | $classNamePointer,
40 | self::class,
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/PhpDoc/DisallowParamNoTypeOrCommentSniff.php:
--------------------------------------------------------------------------------
1 | getTokensAsString($stackPtr, 1), '@param')) {
22 | return;
23 | }
24 |
25 | $value = $phpcsFile->getTokensAsString($stackPtr+2, 1);
26 |
27 | // Check if param tag with no type or comment
28 | $regex = '/^\$\w+\s*$/m';
29 |
30 | if (!\Safe\preg_match($regex, $value, $matches)) {
31 | return;
32 | }
33 |
34 | $phpcsFile->addFixableError(
35 | "@param tags with no type or comment are disallowed",
36 | $stackPtr,
37 | self::class
38 | );
39 |
40 | foreach (range(-4, 2) as $pointer) {
41 | $phpcsFile->fixer->replaceToken(
42 | $stackPtr+$pointer,
43 | ""
44 | );
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "worksome/code-sniffer",
3 | "description": "Worksome's preferences and custom sniffers for phpcs",
4 | "type": "phpcodesniffer-standard",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Oliver Nybroe",
9 | "email": "oliver@worksome.com"
10 | }
11 | ],
12 | "autoload": {
13 | "psr-4": {
14 | "Worksome\\WorksomeSniff\\": "src/WorksomeSniff"
15 | }
16 | },
17 | "autoload-dev": {
18 | "psr-4": {
19 | "Worksome\\WorksomeSniff\\Tests\\": "tests"
20 | }
21 | },
22 | "require": {
23 | "PHP": "^8.0",
24 | "squizlabs/php_codesniffer": "^3.6.1",
25 | "slevomat/coding-standard": "^8.5",
26 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
27 | "thecodingmachine/safe": "^2.0",
28 | "illuminate/support": "^8.78 || ^9.0",
29 | "illuminate/collections": "^8.78 || ^9.0",
30 | "illuminate/contracts": "^8.78 || ^9.0"
31 | },
32 | "require-dev": {
33 | "pestphp/pest": "^1.21",
34 | "symfony/var-dumper": "^5.3"
35 | },
36 | "minimum-stability": "dev",
37 | "config": {
38 | "allow-plugins": {
39 | "dealerdirect/phpcodesniffer-composer-installer": true,
40 | "pestphp/pest-plugin": true
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/Laravel/DisallowBladeOutsideOfResourcesDirectorySniff.php:
--------------------------------------------------------------------------------
1 | getFilename(), '.blade.php')) {
24 | return;
25 | }
26 |
27 | if (str_contains($phpcsFile->getFilename(), $this->resourcesDirectory())) {
28 | return;
29 | }
30 |
31 | if ($this->errorAlreadyRegisteredForFile) {
32 | return;
33 | }
34 |
35 | $phpcsFile->addError(
36 | "Blade files must be placed in the resources directory.",
37 | $stackPtr,
38 | self::class
39 | );
40 |
41 | $this->errorAlreadyRegisteredForFile = true;
42 | }
43 |
44 | private function resourcesDirectory(): string
45 | {
46 | return $this->resourcesDirectory ?? getcwd() . DIRECTORY_SEPARATOR . 'resources';
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/Classes/ExceptionSuffixSniff.php:
--------------------------------------------------------------------------------
1 | getTokensAsString($stackPtr + 2, $baseClassNameLength + 1),'\\')) {
25 | $baseClassNameLength += 2;
26 | }
27 | $baseClassName = $phpcsFile->getTokensAsString($stackPtr +2, max($baseClassNameLength, 1));
28 |
29 | if (!Str::endsWith($baseClassName, $this->suffix) || !Str::contains($baseClassName, 'Exception')) {
30 | return;
31 | }
32 |
33 | $classNamePointer = $stackPtr - 2;
34 |
35 | $className = $phpcsFile->getTokensAsString($classNamePointer, 1);
36 |
37 | // Check if class is named with Exceptions suffix.
38 | if (Str::endsWith($className, $this->suffix)) {
39 | return;
40 | }
41 |
42 | $phpcsFile->addError(
43 | "Exceptions should have `Exception` suffix.",
44 | $classNamePointer,
45 | self::class,
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/Laravel/DisallowHasFactorySniff.php:
--------------------------------------------------------------------------------
1 | getTokensAsString($traitNamePointer, 10), ';');
26 |
27 | $this->trackRelevantPartialNamespace($className);
28 |
29 | if ($this->isHasFactoryNamespace($className)) {
30 | $this->addError($phpcsFile, $traitNamePointer);
31 | }
32 | }
33 |
34 | private function isHasFactoryNamespace(string $givenNamespace): bool
35 | {
36 | if ($this->partial !== null && strlen($this->partial) > 0) {
37 | $givenNamespace = $this->partial . Str::after($givenNamespace, Str::afterLast($this->partial, "\\"));
38 | }
39 |
40 | if ($givenNamespace !== self::HAS_FACTORY_FQCN) {
41 | return false;
42 | }
43 |
44 | return true;
45 | }
46 |
47 | private function trackRelevantPartialNamespace(string $givenNamespace): void
48 | {
49 | Str::of(self::HAS_FACTORY_FQCN)
50 | ->explode("\\")
51 | ->skip(1)
52 | ->reduce(function (string $carry, string $item) use ($givenNamespace) {
53 | $updatedNamespacePartial = "{$carry}\\{$item}";
54 |
55 | if (Str::contains($givenNamespace, $updatedNamespacePartial)) {
56 | $this->partial = $updatedNamespacePartial;
57 | }
58 |
59 | return $updatedNamespacePartial;
60 | }, 'Illuminate');
61 | }
62 |
63 | private function addError(File $phpcsFile, int $line): void
64 | {
65 | $phpcsFile->addError(
66 | "Models should not use the `HasFactory` trait.",
67 | $line,
68 | self::class,
69 | );
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/PhpDoc/PropertyDollarSignSniff.php:
--------------------------------------------------------------------------------
1 | getLineOfDocblock($phpcsFile, $stackPtr));
23 | } catch (InvalidArgumentException) {
24 | return;
25 | }
26 |
27 | if ($propertyDoc->variableHasDollarSymbol()) {
28 | return;
29 | }
30 |
31 | $phpcsFile->addFixableError(
32 | "All @property variables should start with a dollar symbol.",
33 | $stackPtr,
34 | self::class
35 | );
36 |
37 | if ($phpcsFile->fixer === null) {
38 | return;
39 | }
40 |
41 | $this->replaceLineOfDocblock($phpcsFile, $stackPtr, " {$propertyDoc->joined()}" . PHP_EOL);
42 | }
43 |
44 | private function getLineOfDocblock(File $phpcsFile, int $startPtr): string
45 | {
46 | $currentPointer = $startPtr;
47 | $content = '';
48 |
49 | while (! str_ends_with($content, PHP_EOL)) {
50 | if ($phpcsFile->numTokens === $currentPointer) {
51 | continue;
52 | }
53 |
54 | $content .= $phpcsFile->getTokensAsString($currentPointer, 1);
55 | $currentPointer += 1;
56 | }
57 |
58 | return trim($content);
59 | }
60 |
61 | private function replaceLineOfDocblock(File $phpcsFile, int $startPtr, string $newContent): void
62 | {
63 | $eolPointer = $startPtr;
64 | $content = '';
65 |
66 | do {
67 | if ($phpcsFile->numTokens === $eolPointer) {
68 | continue;
69 | }
70 |
71 | $eolPointer += 1;
72 | $content = $phpcsFile->getTokensAsString($eolPointer, 1);
73 | } while (! str_ends_with($content, PHP_EOL));
74 |
75 | foreach (array_reverse(range($startPtr, $eolPointer)) as $ptrToDelete) {
76 | $phpcsFile->fixer->replaceToken($ptrToDelete, '');
77 | }
78 |
79 | $phpcsFile->fixer->replaceToken($startPtr - 1, $newContent);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Sniffs/Functions/DisallowCompactUsageSniff.php:
--------------------------------------------------------------------------------
1 | getTokensAsString($stackPtr, 1);
20 |
21 | // Check if method is env method.
22 | if (strtolower($methodName) !== 'compact') {
23 | return;
24 | }
25 |
26 | $phpcsFile->addFixableError(
27 | "Usage of compact function is disallowed.",
28 | $stackPtr,
29 | self::class
30 | );
31 |
32 | [$variables, $lastTokenPointer] = $this->findVariablesInCompactCall($phpcsFile, $stackPtr);
33 |
34 | $phpCode = $this->generatePhpArray($variables);
35 |
36 | // Remove all the old code after the `compact` string.
37 | foreach (range(1, $lastTokenPointer-1) as $currentPointer) {
38 | $phpcsFile->fixer->replaceToken(
39 | $stackPtr+$currentPointer,
40 | ""
41 | );
42 | }
43 |
44 | // Replace the `compact` string with our new array.
45 | $phpcsFile->fixer->replaceToken(
46 | $stackPtr,
47 | $phpCode
48 | );
49 | }
50 |
51 | private function generatePhpArray(array $variables): string
52 | {
53 | $phpCode = '[';
54 | foreach ($variables as $variable) {
55 | $phpCode .= "'$variable' => \$$variable, ";
56 | }
57 |
58 | // Remove the trailing `, ` from the array.
59 | $phpCode = substr($phpCode, 0, -2);
60 | // Close the array
61 | $phpCode .= "]";
62 |
63 | return $phpCode;
64 | }
65 |
66 | private function findVariablesInCompactCall(File $phpcsFile, int $stackPtr): array
67 | {
68 | $variables = [];
69 | $pointer = 1;
70 |
71 | do {
72 | $token = $phpcsFile->getTokensAsString($stackPtr + $pointer, 1);
73 | $pointer++;
74 |
75 | if (!\Safe\preg_match("/['\"](.*?)['\"]/", $token, $matches)) {
76 | continue;
77 | }
78 |
79 | $variableName = $matches[1];
80 | $variables[] = $variableName;
81 | } while ($token !== ')');
82 |
83 | return [$variables, $pointer, $matches];
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | :warning: Deprecated in favor of [coding-style](https://github.com/worksome/coding-style) package :warning:
2 |
3 | # Worksome's custom sniffs
4 | This package contains all of Worksome's custom sniffs and configuration.
5 |
6 | ```
7 | composer require worksome/code-sniffer --dev
8 | ```
9 |
10 | ## Usage
11 | Create a `phpcs.xml` in the root of your project with content like the following
12 | ```xml
13 |
14 |
15 | YourCompany Coding Standard
16 |
17 | ./app
18 | ./tests
19 | ./config
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ```
29 |
30 | Run phpcs for seeing if you have any errors
31 | ```
32 | vendor/bin/phpcs
33 | ```
34 | Or run phpcbf for automatically fixing the errors when possible
35 | ```
36 | vendor/bin/phpcbf
37 | ```
38 |
39 | We suggest adding the commands as scripts in your `composer.json` for easier execution.
40 | ```json
41 | ...
42 | "scripts": {
43 | "phpcs": "vendor/bin/phpcs",
44 | "phpcbf": "vendor/bin/phpcbf"
45 | }
46 | ...
47 | ```
48 |
49 | This way you can now execute them via composer
50 | ```
51 | $ composer phpcs
52 | $ composer phpcbf
53 | ```
54 |
55 |
56 | # Custom sniffs
57 | List all the custom sniffs created by Worksome.
58 |
59 | ## Laravel
60 | All custom sniffs specific to Laravel.
61 |
62 | ### Config filename kebab case
63 | Checks if all config files are written in kebab case.
64 |
65 | ### Disallow env usage
66 | Makes sure that you don't use `env` helper in your code, except for config files.
67 |
68 | ### Event listener suffix
69 | Enforces event listeners to end with a specific suffix, this suffix is defaulted to `Listener`.
70 |
71 | | parameters | defaults |
72 | | --- | --- |
73 | | suffix | Listener |
74 |
75 | ### Disallow blade outside of the `resources` directory
76 | Makes sure no `.blade.php` files exist outside of Laravel's `resources` directory.
77 |
78 | | parameters | defaults |
79 | | --- | --- |
80 | | resourcesDirectory | {YOUR_PROJECT}/resources |
81 |
82 | ## PhpDoc
83 | All custom sniffs which are not specific to Laravel.
84 |
85 | ### Property dollar sign
86 | Makes sure that you always have a dollar sign in your properties defined in phpdoc.
87 | ```php
88 | /**
89 | * @property string $name
90 | */
91 | class User {}
92 | ```
93 |
94 | ### Param tags with no type or comment
95 | This removes all `@param` tags which has no specified a type or comment
96 | ```php
97 | /**
98 | * @param string $name
99 | * @param $type some random type
100 | * @param $other // <-- will be removed
101 | */
102 | public function someMethod($name, $type, $other): void
103 | {}
104 | ```
105 |
106 |
107 | This is mainly because phpstan requires this before it sees the property as valid.
108 |
--------------------------------------------------------------------------------
/src/WorksomeSniff/Support/PropertyDoc.php:
--------------------------------------------------------------------------------
1 | after($types)->trim();
35 |
36 | if ($remainderAfterType->isNotEmpty()) {
37 | $variable = $remainderAfterType->before(' ')->__toString();
38 | $description = $remainderAfterType->after(' ')->__toString();
39 | } else {
40 | $types = null;
41 | $variable = $typeMatches[0];
42 | $description = null;
43 | }
44 |
45 | if (str_starts_with($types ?? '', '$')) {
46 | $description = $variable;
47 | $variable = $types;
48 | $types = null;
49 | }
50 |
51 | return new self(
52 | $scope,
53 | $types,
54 | $variable,
55 | $description !== $variable ? $description : null,
56 | );
57 | }
58 |
59 | public function variableHasDollarSymbol(): bool
60 | {
61 | return str_starts_with($this->variableName, '$');
62 | }
63 |
64 | public function joined(): string
65 | {
66 | return join(' ', array_filter([
67 | $this->scope,
68 | $this->types,
69 | Str::start($this->variableName, '$'),
70 | $this->description,
71 | ]));
72 | }
73 |
74 | private static function regexForTypes(): string
75 | {
76 | return <<]+|(?2))*+ # Recursively ignore any matching sets of '<' and '>', '{' and '}' or '[' and ']' found in nested types. Eg: `Collection>`.
83 | [\]}>] # Until we match the closing '>' or '}'.
84 | )? # End capture group #2. Not all types are generics, so capture group 2 is optional.
85 | ) # End capture group #1.
86 | ( # Capture group #3.
87 | (?:\s?\|\s?)(?1) # If we find an '|' symbol we expect another type, so we'll recursively check capture group #1 again.
88 | )* # End capture group #3. There can be 0 or more of this group.
89 | /x
90 | REGEXP;
91 | }
92 | }
--------------------------------------------------------------------------------
/src/WorksomeSniff/ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Worksome Coding Standard
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | error
52 |
53 |
54 |
55 |
56 | error
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------