Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sniff to limit the use of new in constructor #153

Merged
merged 12 commits into from
Feb 3, 2022
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,35 @@ final class Foo
}
}
```

### Codor.Classes.NewInstance ###
Classes should not instantiate objects. Should use dependency injection.

:x:
```php
class NewInConstructor
{
private MyClass $myClass;

public function __construct()
{
$this->myClass = new MyClass();
}
}
```

:white_check_mark:
```php
class NewInConstructor
{
private MyClass $myClass;
villfa marked this conversation as resolved.
Show resolved Hide resolved

public function __construct(MyClass $myClass)
{
$this->myClass = $myClass;
}
}
```
### Codor.Classes.PropertyDeclaration ###
Produces an error if your class uses undeclared member variables. Only warns if class extends another class.

Expand Down
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
]
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
villfa marked this conversation as resolved.
Show resolved Hide resolved
"bamarni/composer-bin-plugin": true
},
"sort-packages": true
},
"scripts": {
Expand Down
86 changes: 86 additions & 0 deletions src/Codor/Sniffs/Classes/NewInstanceSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php declare(strict_types = 1);

namespace Codor\Sniffs\Classes;

use PHP_CodeSniffer\Sniffs\Sniff as PHP_CodeSniffer_Sniff;
use PHP_CodeSniffer\Files\File as PHP_CodeSniffer_File;

class NewInstanceSniff implements PHP_CodeSniffer_Sniff
{
/**
* Returns the token types that this sniff is interested in.
* @return array
*/
public function register(): array
{
return [T_FUNCTION];
}

/**
* Processes the tokens that this sniff is interested in.
*
* @param PHP_CodeSniffer_File $phpcsFile The file where the token was found.
* @param integer $stackPtr The position in the stack where
* the token was found.
* @return void
*/
public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$functionName = $tokens[$stackPtr + 2]['content'];

$scopes = $this->getBracketsIndex($tokens, $stackPtr);
if (true === empty($scopes['open']) || true === empty($scopes['close'])) {
return;
}

for ($index=$scopes['open']; $index <= $scopes['close']; $index++) {
if ($tokens[$index]['type'] === 'T_NEW') {
$phpcsFile->addWarning($this->getWarningMessage($functionName), $tokens[$index]['column'], __CLASS__);
}
}
}

private function getBracketsIndex(array $tokens, int $stackPtr) : array
{
$token = $tokens[$stackPtr];
$open = $token['scope_opener'] ?? null;
$close = $token['scope_closer'] ?? null;

if (true === empty($open)) {
return $this->searchBrackets($tokens, $stackPtr);
}

return [
'open' => $open,
'close' => $close
];
}

private function searchBrackets(array $tokens, int $stackPtr) : array
{
$open = $close = null;
for ($i=$stackPtr; $i < count($tokens); $i++) {
villfa marked this conversation as resolved.
Show resolved Hide resolved
if ($tokens[$i]['type'] === 'T_OPEN_CURLY_BRACKET') {
$open = $i;
}
if ($tokens[$i]['type'] === 'T_CLOSE_CURLY_BRACKET') {
$close = $i;
}
}

return [
'open' => $open,
'close' => $close
];
}

/**
* Gets the warning message for this sniff.
* @return string
*/
protected function getWarningMessage(string $functionName): string
{
return sprintf('Function %s use new keyword - consider to use DI.', $functionName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

class MultipleNewInClass
{
private MyClass $myClass;

public function __construct()
{
$this->myClass = new MyClass();
}

public function newInstance() : MyClass
{
return new MyClass();
}
}
12 changes: 12 additions & 0 deletions tests/Sniffs/Classes/Assets/NewInstanceSniff/NewInConstructor.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php


class NewInConstructor
{
private MyClass $myClass;

public function __construct()
{
$this->myClass = new MyClass();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

class Foo {
public function __construct(
private $bar = new stdClass()
) {}

public function getBar() {
return $this->bar;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

function newClass(): Class
{
return new Class();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php

$app = new Application();
$app->run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

function newClass()
90 changes: 90 additions & 0 deletions tests/Sniffs/Classes/NewInstanceSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php declare(strict_types = 1);

namespace Codor\Tests\Sniffs\Classes;

use Codor\Tests\BaseTestCase;

/** @group Classes */
class NewInstanceSniffTest extends BaseTestCase
{

public function setup()
{
parent::setup();

$this->runner->setSniff('Codor.Classes.NewInstance')->setFolder(__DIR__.'/Assets/NewInstanceSniff/');
}

/** @test */
public function a_new_in_constructor_function()
{
$results = $this->runner->sniff('NewInConstructor.inc');

$this->assertSame(0, $results->getErrorCount());
$this->assertSame(1, $results->getWarningCount());

$warningMessages = $results->getAllWarningMessages();
$this->assertCount(1, $warningMessages);
$this->assertSame('Function __construct use new keyword - consider to use DI.', $warningMessages[0]);
}

/** @test */
public function a_new_not_in_function()
{
$results = $this->runner->sniff('NewNotInFunction.inc');

$this->assertSame(0, $results->getErrorCount());
$this->assertSame(0, $results->getWarningCount());

$warningMessages = $results->getAllWarningMessages();
$this->assertCount(0, $warningMessages);
}

/** @test */
public function a_new_in_regular_function()
{
$results = $this->runner->sniff('NewInRegularFunction.inc');

$this->assertSame(0, $results->getErrorCount());
$this->assertSame(1, $results->getWarningCount());

$warningMessages = $results->getAllWarningMessages();
$this->assertCount(1, $warningMessages);
}

/** @test */
public function a_multiple_new_in_class()
{
$results = $this->runner->sniff('MultipleNewInClass.inc');

$this->assertSame(0, $results->getErrorCount());
$this->assertSame(2, $results->getWarningCount());

$warningMessages = $results->getAllWarningMessages();
$this->assertCount(2, $warningMessages);
}

/** @test */
public function a_new_in_contructor_parameter()
{
$results = $this->runner->sniff('NewInConstructorParameter.inc');

$this->assertSame(0, $results->getErrorCount());
$this->assertSame(0, $results->getWarningCount());

$warningMessages = $results->getAllWarningMessages();
$this->assertCount(0, $warningMessages);
}

/** @test */
public function a_ignore_if_no_body_in_function()
{
$results = $this->runner->sniff('NoBodyFunction.inc');

$this->assertSame(0, $results->getErrorCount());
$this->assertSame(0, $results->getWarningCount());

$warningMessages = $results->getAllWarningMessages();
$this->assertCount(0, $warningMessages);
}
}
5 changes: 5 additions & 0 deletions vendor-bin/churn-php/composer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"require-dev": {
"bmitch/churn-php": "^1.3"
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true
villfa marked this conversation as resolved.
Show resolved Hide resolved
}
}
}