Passed
Push — master ( 3edb00...c81ad5 )
by Björn
02:28
created

FluentSetterSniff::getFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BestIt\Sniffs\Functions;
6
7
use BestIt\CodeSniffer\File as FileDecorator;
8
use BestIt\CodeSniffer\Helper\PropertyHelper;
9
use BestIt\Sniffs\SuppressingTrait;
10
use PHP_CodeSniffer\Files\File;
11
use PHP_CodeSniffer\Standards\Squiz\Sniffs\Scope\MethodScopeSniff;
12
use SlevomatCodingStandard\Helpers\TokenHelper;
13
use function in_array;
14
use function substr;
15
16
/**
17
 * Checks if a fluent setter is used per default.
18
 *
19
 * @package BestIt\Sniffs\Functions
20
 *
21
 * @author Nick Lubisch <[email protected]>
22
 * @author Björn Lange <[email protected]>
23
 */
24
class FluentSetterSniff extends MethodScopeSniff
25
{
26
    use SuppressingTrait;
27
28
    /**
29
     * Every setter function MUST return $this if nothing else is returned.
30
     */
31
    public const CODE_MUST_RETURN_THIS = 'MustReturnThis';
32
33
    /**
34
     * Your method MUST contain a return.
35
     */
36
    public const CODE_NO_RETURN_FOUND = 'NoReturnFound';
37
38
    /**
39
     * Error message when the method does not return $this.
40
     */
41
    private const ERROR_MUST_RETURN_THIS = 'The method "%s" must return $this';
42
43
    /**
44
     * Error message when no return statement is found.
45
     */
46
    private const ERROR_NO_RETURN_FOUND = 'Method "%s" has no return statement';
47
48
    /**
49
     * Specifies how an identation looks like.
50
     *
51
     * @var string
52
     */
53
    public $identation = '    ';
54
55
    /**
56
     * The used file decorated for the interface.
57
     *
58
     * @var FileDecorator
59
     */
60
    private $file;
61
62
    /**
63
     * The position of this node.
64
     *
65
     * @var int
66
     */
67
    private $stackPos;
68
69
    /**
70
     * Registers an error if an empty return (return null; or return;) is given.
71 4
     *
72
     * @param File $file The sniffed file.
73 4
     * @param int $functionPos The position of the function.
74 4
     * @param int $returnPos The position of the return call.
75
     * @param string $methodIdent The ident for the method to given in an error.
76
     *
77
     * @return void
78
     */
79
    private function checkAndRegisterEmptyReturnErrors(
80
        File $file,
81
        int $functionPos,
82
        int $returnPos,
83
        string $methodIdent
84
    ): void {
85 4
        $nextToken = $file->getTokens()[TokenHelper::findNextEffective($file, $returnPos + 1)];
86
87
        if (!$nextToken || (in_array($nextToken['content'], ['null', ';']))) {
88
            $fixMustReturnThis = $file->addFixableError(
89
                self::ERROR_MUST_RETURN_THIS,
90 4
                $functionPos,
91 4
                static::CODE_MUST_RETURN_THIS,
92
                $methodIdent
0 ignored issues
show
Documentation introduced by
$methodIdent is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
93 4
            );
94 1
95
            if ($fixMustReturnThis) {
96
                $this->fixMustReturnThis($file, $returnPos);
97 4
            }
98 4
        }
99
    }
100 4
101 4
    /**
102 4
     * Checks if there are fluent setter errors and registers errors if needed.
103
     *
104 4
     * @param File $phpcsFile The file for this sniff.
105
     * @param int $functionPos The position of the used token.
106 4
     * @param int $classPos The position of the class.
107 1
     *
108 1
     * @return void
109 1
     */
110 1
    private function checkForFluentSetterErrors(File $phpcsFile, int $functionPos, int $classPos): void
111 1
    {
112
        $tokens = $phpcsFile->getTokens();
113
        $errorData = $phpcsFile->getDeclarationName($classPos) . '::' . $phpcsFile->getDeclarationName($functionPos);
114 1
115 1
        $functionToken = $tokens[$functionPos];
116
        $openBracePtr = $functionToken['scope_opener'];
117
        $closeBracePtr = $functionToken['scope_closer'];
118 1
119
        $returnPtr = $phpcsFile->findNext(T_RETURN, $openBracePtr, $closeBracePtr);
120
121 4
        if ($returnPtr === false) {
122 4
            $fixNoReturnFound = $phpcsFile->addFixableError(
123 4
                self::ERROR_NO_RETURN_FOUND,
124 4
                $functionPos,
125
                static::CODE_NO_RETURN_FOUND,
126
                $errorData
0 ignored issues
show
Documentation introduced by
$errorData is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
127 4
            );
128 1
129 1
            if ($fixNoReturnFound) {
130 1
                $this->fixNoReturnFound($phpcsFile, $closeBracePtr);
131 1
            }
132 1
133
            return;
134 1
        }
135
136
        $this->checkAndRegisterEmptyReturnErrors($phpcsFile, $functionPos, $returnPtr, $errorData);
137 3
    }
138
139 3
    /**
140 1
     * Returns the used file decorated for the interface.
141 1
     *
142 1
     * @return FileDecorator
143 1
     */
144 1
    public function getFile(): FileDecorator
145
    {
146
        return $this->file;
147 1
    }
148 1
149
    /**
150
     * Returns the position of this node.
151 1
     * @return int
152
     */
153 3
    public function getStackPos(): int
154
    {
155
        return $this->stackPos;
156
    }
157
158
    /**
159
     * Processes the tokens that this test is listening for.
160
     *
161
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
162
     *
163
     * @param File $file The file where this token was found.
164
     * @param int $functionPos The position in the stack where this token was found.
165
     * @param int $classPos The position in the tokens array that opened the scope that this test is listening for.
166 4
     *
167
     * @return void
168 4
     */
169
    protected function processTokenWithinScope(
170 4
        File $file,
171
        $functionPos,
172
        $classPos
173
    ): void {
174
        $this->file = new FileDecorator($file);
175
        $this->stackPos = $functionPos;
176
177
        $isSuppressed = $this->isSniffSuppressed(static::CODE_NO_RETURN_FOUND);
178
179
        if (!$isSuppressed && $this->checkIfSetterFunction($classPos, $file, $functionPos)) {
180
            $this->checkForFluentSetterErrors($file, $functionPos, $classPos);
181 1
        }
182
    }
183 1
184 1
    /**
185
     * Checks if the given method name relates to a setter function of a property.
186 1
     *
187
     * @param int $classPosition The position of the class token.
188 1
     * @param File $file The file of the sniff.
189 1
     * @param int $methodPosition The position of the method token.
190 1
     *
191 1
     * @return bool Indicator if the given method is a setter function
192 1
     */
193 1
    private function checkIfSetterFunction(int $classPosition, File $file, int $methodPosition): bool
194
    {
195
        $isSetter = false;
196
        $methodName = $file->getDeclarationName($methodPosition);
197
198
        if (substr($methodName, 0, 3) === 'set') {
199
            // We define in our styleguide, that there is only one class per file!
200
            $properties = (new PropertyHelper(new FileDecorator($file)))->getProperties(
201
                $file->getTokens()[$classPosition]
202
            );
203 1
204
            // We require camelCase for methods and properties,
205 1
            // so there should be an "lcfirst-Method" without set-prefix.
206
            $isSetter = in_array(lcfirst(substr($methodName, 3)), $properties, true);
207 1
        }
208 1
209
        return $isSetter;
210
    }
211 1
212 1
    /**
213 1
     * Fixes if no return statement is found.
214 1
     *
215
     * @param File $phpcsFile The php cs file
216 1
     * @param int $closingBracePtr Pointer to the closing curly brace of the function
217 1
     *
218
     * @return void
219
     */
220
    private function fixNoReturnFound(File $phpcsFile, int $closingBracePtr): void
221
    {
222
        $tokens = $phpcsFile->getTokens();
223
        $closingBraceToken = $tokens[$closingBracePtr];
224
225
        $expectedReturnSpaces = str_repeat($this->identation, $closingBraceToken['level'] + 1);
226
227
        $phpcsFile->fixer->beginChangeset();
228
        $phpcsFile->fixer->addNewlineBefore($closingBracePtr - 1);
229
        $phpcsFile->fixer->addContentBefore($closingBracePtr - 1, $expectedReturnSpaces . 'return $this;');
230
        $phpcsFile->fixer->addNewlineBefore($closingBracePtr - 1);
231
        $phpcsFile->fixer->endChangeset();
232
    }
233
234
    /**
235
     * Fixes the return value of a function to $this.
236
     *
237
     * @param File $phpcsFile The php cs file
238
     * @param int $returnPtr Pointer to the return token
239
     *
240
     * @return void
241
     */
242
    private function fixMustReturnThis(File $phpcsFile, int $returnPtr): void
243
    {
244
        $returnSemicolonPtr = $phpcsFile->findEndOfStatement($returnPtr);
245
246
        for ($i = $returnPtr + 1; $i < $returnSemicolonPtr; $i++) {
247
            $phpcsFile->fixer->replaceToken($i, '');
248
        }
249
250
        $phpcsFile->fixer->beginChangeset();
251
        $phpcsFile->fixer->addContentBefore(
252
            $returnSemicolonPtr,
253
            ' $this'
254
        );
255
        $phpcsFile->fixer->endChangeset();
256
    }
257
}
258