Skip to content

Commit 7b125d5

Browse files
committed
Initial commit
0 parents  commit 7b125d5

23 files changed

+1570
-0
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/.github export-ignore
2+
/tests export-ignore
3+
/phpunit.xml.dist export-ignore

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# composer
2+
composer.lock
3+
/vendor
4+
5+
# phpunit
6+
/.phpunit.cache
7+
/.phpunit.result.cache
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Classes;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
8+
final class ForbidDirectClassInheritanceSniff implements Sniff
9+
{
10+
public const FORBIDDEN_CLASS_INHERITED = 'ForbiddenInheritance';
11+
12+
/**
13+
* A list of forbidden classes not allowed to inherit directly.
14+
* Usually used to force developers to use our custom wrappers instead of framework or library functionality.
15+
* @var array<class-string, class-string|null>
16+
*/
17+
public $forbiddenClasses = [];
18+
19+
/** @return list<int> */
20+
public function register(): array
21+
{
22+
return [
23+
\T_CLASS,
24+
];
25+
}
26+
27+
/**
28+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
29+
* @param int $classPointer
30+
*/
31+
public function process(File $phpcsFile, $classPointer): void
32+
{
33+
$parentFQCN = $phpcsFile->findExtendedClassName($classPointer);
34+
35+
if ($parentFQCN === false || ! array_key_exists($parentFQCN, $this->forbiddenClasses)) {
36+
return;
37+
}
38+
39+
$replaceBy = $this->forbiddenClasses[$parentFQCN];
40+
$error = sprintf(
41+
'Class “%s” is forbidden for direct inheritance, “%s” should be used instead.',
42+
$parentFQCN,
43+
$replaceBy
44+
);
45+
$phpcsFile->addError($error, $classPointer, self::FORBIDDEN_CLASS_INHERITED);
46+
}
47+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Classes;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use SlevomatCodingStandard\Helpers\ClassHelper;
8+
9+
final class ForbidMethodDeclarationSniff implements Sniff
10+
{
11+
public const FORBIDDEN_METHOD_DECLARATION = 'ForbiddenMethodDeclaration';
12+
13+
/**
14+
* A list of methods which should not be declared
15+
* in specific.
16+
* @var array<string, string>
17+
*/
18+
public $forbiddenMethods = [];
19+
20+
/** @return list<int> */
21+
public function register(): array
22+
{
23+
return [
24+
\T_CLASS,
25+
];
26+
}
27+
28+
/**
29+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
30+
* @param int $classPointer
31+
*/
32+
public function process(File $phpcsFile, $classPointer): void
33+
{
34+
/** @var class-string $fqcn */
35+
$fqcn = ClassHelper::getFullyQualifiedName($phpcsFile, $classPointer);
36+
foreach ($this->forbiddenMethods as $typeAndMethod => $replacement) {
37+
[$type, $method] = explode('::', $typeAndMethod);
38+
if (! is_subclass_of($fqcn, $type)) {
39+
continue;
40+
}
41+
42+
if (! method_exists($fqcn, $method)) {
43+
continue;
44+
}
45+
46+
$phpcsFile->addError(sprintf('Method “%s” is forbidden, use “%s” instead.', $typeAndMethod, $replacement), $classPointer, self::FORBIDDEN_METHOD_DECLARATION);
47+
}
48+
}
49+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Files;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
8+
/** Checks that all file names are BEM-cased. */
9+
final class BemCasedFilenameSniff implements Sniff
10+
{
11+
private const MODIFIER_DELIMITER = '--';
12+
private const ELEMENT_DELIMITER = '__';
13+
private const BEM_FILE_NAME_PATTERN = '/^(?:(?:(?:__)?[a-z][a-zA-Z0-9]*)(?:(?:--)[\w][a-zA-Z0-9]*)?)+?\.blade\.php$/';
14+
private const BLADE_COMPONENT_FILE_NAME_PATTERN = '/^([a-z]+(\-[a-z]+)+)\.blade\.php$/';
15+
16+
/** @var array<string, bool> */
17+
private array $checkedFiles = [];
18+
19+
/** @inheritDoc */
20+
public function register(): array
21+
{
22+
return [\T_INLINE_HTML, \T_OPEN_TAG];
23+
}
24+
25+
/** @inheritDoc */
26+
public function process(File $phpcsFile, $stackPtr): int
27+
{
28+
$filename = $phpcsFile->getFilename();
29+
30+
$hash = md5($filename);
31+
if (isset($this->checkedFiles[$hash]) || $filename === 'STDIN') {
32+
return 0;
33+
}
34+
35+
$this->checkedFiles[$hash] = true;
36+
37+
$filename = basename($filename);
38+
39+
if ($this->hasInvalidNumberOfElements($filename)) {
40+
$phpcsFile->addError(
41+
'Filename “%s” has too many element delimiters',
42+
$stackPtr,
43+
'TooManyElementDelimiters',
44+
[$filename]
45+
);
46+
$phpcsFile->recordMetric($stackPtr, 'BEM element delimiters', 'no');
47+
} else {
48+
$phpcsFile->recordMetric($stackPtr, 'BEM element delimiters', 'yes');
49+
}
50+
51+
if (substr_count($filename, self::MODIFIER_DELIMITER) > 1) {
52+
$phpcsFile->addError(
53+
'Filename “%s” has to many element modifiers',
54+
$stackPtr,
55+
'TooManyElementModifiers',
56+
[$filename]
57+
);
58+
$phpcsFile->recordMetric($stackPtr, 'BEM element modifiers', 'no');
59+
} else {
60+
$phpcsFile->recordMetric($stackPtr, 'BEM element modifiers', 'yes');
61+
}
62+
63+
if ($this->hasCorrectFileName($filename)) {
64+
$phpcsFile->recordMetric($stackPtr, 'BEM case filename', 'yes');
65+
} else {
66+
$error = 'Filename “%s” does not match the expected BEM filename convention';
67+
$phpcsFile->addError($error, $stackPtr, 'InvalidCharacters', [$filename]);
68+
$phpcsFile->recordMetric($stackPtr, 'BEM case filename', 'no');
69+
}
70+
71+
// Ignore the rest of the file.
72+
return $phpcsFile->numTokens + 1;
73+
}
74+
75+
private function hasInvalidNumberOfElements(string $filename): bool
76+
{
77+
return count(explode(self::ELEMENT_DELIMITER, ltrim($filename, self::ELEMENT_DELIMITER))) > 3;
78+
}
79+
80+
private function hasCorrectFileName(string $filename): bool
81+
{
82+
return $this->isBemFilename($filename) || $this->isComponentFileName($filename) || $this->isErrorPage($filename);
83+
}
84+
85+
private function isBemFilename(string $filename): bool
86+
{
87+
return preg_match(self::BEM_FILE_NAME_PATTERN, $filename) === 1;
88+
}
89+
90+
private function isComponentFileName(string $filename): bool
91+
{
92+
return preg_match(self::BLADE_COMPONENT_FILE_NAME_PATTERN, $filename) === 1;
93+
}
94+
95+
private function isErrorPage(string $filename): bool
96+
{
97+
return preg_match('/^([45])\d{2}\.blade\.php$/', $filename) === 1;
98+
}
99+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Functions;
4+
5+
use PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\ForbiddenFunctionsSniff as BaseForbiddenFunctionsSniff;
6+
7+
final class ForbiddenFunctionsSniff extends BaseForbiddenFunctionsSniff
8+
{
9+
/** @inheritDoc */
10+
public $forbiddenFunctions = [];
11+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace IxDFCodingStandard\Sniffs\Laravel;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\AbstractScopeSniff;
7+
use SlevomatCodingStandard\Helpers\ClassHelper;
8+
9+
final class DisallowGuardedAttributeSniff extends AbstractScopeSniff
10+
{
11+
public const CODE_EMPTY_GUARDED = 'EmptyGuarded';
12+
public const CODE_NON_EMPTY_GUARDED = 'NonEmptyGuarded';
13+
14+
/**
15+
* A list of tokenizers this sniff supports.
16+
* @var list<string>
17+
*/
18+
public array $supportedTokenizers = ['PHP'];
19+
20+
/** Constructs the test with the tokens it wishes to listen for. */
21+
public function __construct()
22+
{
23+
parent::__construct([\T_CLASS], [\T_VARIABLE], false);
24+
}
25+
26+
/** @inheritDoc */
27+
protected function processTokenWithinScope(File $phpcsFile, $varPointer, $currScope)
28+
{
29+
$varToken = $phpcsFile->getTokens()[$varPointer];
30+
31+
if ($varToken['content'] !== '$guarded') {
32+
return;
33+
}
34+
35+
$classTokenPointer = $phpcsFile->findPrevious([\T_CLASS], $varPointer);
36+
37+
$classFQCN = ClassHelper::getFullyQualifiedName($phpcsFile, $classTokenPointer);
38+
39+
$probablyModelInstance = new $classFQCN(); // @todo find a more performant option
40+
if (! $probablyModelInstance instanceof \Illuminate\Database\Eloquent\Model) {
41+
return;
42+
}
43+
44+
$modelInstance = $probablyModelInstance;
45+
46+
if ($modelInstance->getGuarded() === []) {
47+
$error = 'Usage of unguarded Model attributes is forbidden for security reasons.';
48+
$phpcsFile->addError($error, $varPointer, self::CODE_EMPTY_GUARDED);
49+
return;
50+
}
51+
52+
if ($modelInstance->getGuarded() !== ['*']) {
53+
$error = 'Usage of unguarded Model attributes is forbidden for security reasons.';
54+
$phpcsFile->addError($error, $varPointer, self::CODE_NON_EMPTY_GUARDED);
55+
return;
56+
}
57+
}
58+
59+
/** @inheritDoc */
60+
protected function processTokenOutsideScope(File $phpcsFile, $stackPtr)
61+
{
62+
// nothing to do here
63+
}
64+
}

0 commit comments

Comments
 (0)