Completed
Push — master ( b231ee...7cd792 )
by Björn
03:22
created

FluentSetterSniff::checkForFluentSetterErrors()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 18
cts 18
cp 1
rs 9.472
c 0
b 0
f 0
cc 3
nc 3
nop 3
crap 3
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 PHP_CodeSniffer\Files\File;
10
use PHP_CodeSniffer\Standards\Squiz\Sniffs\Scope\MethodScopeSniff;
11
use SlevomatCodingStandard\Helpers\SuppressHelper;
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
    /**
27
     * Code when the method does not return $this.
28
     *
29
     * @var string
30
     */
31
    public const CODE_MUST_RETURN_THIS = 'MustReturnThis';
32
33
    /**
34
     * Code when no return statement is found.
35
     *
36
     * @var string
37
     */
38
    public const CODE_NO_RETURN_FOUND = 'NoReturnFound';
39
40
    /**
41
     * Error message when the method does not return $this.
42
     *
43
     * @var string
44
     */
45
    private const ERROR_MUST_RETURN_THIS = 'The method "%s" must return $this';
46
47
    /**
48
     * Error message when no return statement is found.
49
     *
50
     * @var string
51
     */
52
    private const ERROR_NO_RETURN_FOUND = 'Method "%s" has no return statement';
53
54
    /**
55
     * Specifies how an identation looks like.
56
     *
57
     * @var string
58
     */
59
    public $identation = '    ';
60
61
    /**
62
     * Registers an error if an empty return (return null; or return;) is given.
63
     *
64
     * @param File $file The sniffed file.
65
     * @param int $functionPos The position of the function.
66
     * @param int $returnPos The position of the return call.
67
     * @param string $methodIdent The ident for the method to given in an error.
68
     *
69
     * @return void
70
     */
71 4
    private function checkAndRegisterEmptyReturnErrors(
72
        File $file,
73 4
        int $functionPos,
74 4
        int $returnPos,
75
        string $methodIdent
76
    ): void {
77
        $nextToken = $file->getTokens()[TokenHelper::findNextEffective($file, $returnPos + 1)];
78
79
        if (!$nextToken || (in_array($nextToken['content'], ['null', ';']))) {
80
            $fixMustReturnThis = $file->addFixableError(
81
                self::ERROR_MUST_RETURN_THIS,
82
                $functionPos,
83
                self::CODE_MUST_RETURN_THIS,
84
                $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...
85 4
            );
86
87
            if ($fixMustReturnThis) {
88
                $this->fixMustReturnThis($file, $returnPos);
89
            }
90 4
        }
91 4
    }
92
93 4
    /**
94 1
     * Checks if there are fluent setter errors and registers errors if needed.
95
     *
96
     * @param File $phpcsFile The file for this sniff.
97 4
     * @param int $functionPos The position of the used token.
98 4
     * @param int $classPos The position of the class.
99
     *
100 4
     * @return void
101 4
     */
102 4
    private function checkForFluentSetterErrors(File $phpcsFile, int $functionPos, int $classPos): void
103
    {
104 4
        $tokens = $phpcsFile->getTokens();
105
        $errorData = $phpcsFile->getDeclarationName($classPos) . '::' . $phpcsFile->getDeclarationName($functionPos);
106 4
107 1
        $functionToken = $tokens[$functionPos];
108 1
        $openBracePtr = $functionToken['scope_opener'];
109 1
        $closeBracePtr = $functionToken['scope_closer'];
110 1
111 1
        $returnPtr = $phpcsFile->findNext(T_RETURN, $openBracePtr, $closeBracePtr);
112
113
        if ($returnPtr === false) {
114 1
            $fixNoReturnFound = $phpcsFile->addFixableError(
115 1
                self::ERROR_NO_RETURN_FOUND,
116
                $functionPos,
117
                self::CODE_NO_RETURN_FOUND,
118 1
                $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...
119
            );
120
121 4
            if ($fixNoReturnFound) {
122 4
                $this->fixNoReturnFound($phpcsFile, $closeBracePtr);
123 4
            }
124 4
125
            return;
126
        }
127 4
128 1
        $this->checkAndRegisterEmptyReturnErrors($phpcsFile, $functionPos, $returnPtr, $errorData);
129 1
    }
130 1
131 1
    /**
132 1
     * Get the sniff name.
133
     *
134 1
     * @param string $sniffName If there is an optional sniff name.
135
     *
136
     * @return string Returns the special sniff name in the code sniffer context.
137 3
     */
138
    private function getSniffName(string $sniffName = ''): string
139 3
    {
140 1
        $sniffFQCN = preg_replace(
141 1
            '/Sniff$/',
142 1
            '',
143 1
            str_replace(['\\', '.Sniffs'], ['.', ''], static::class)
144 1
        );
145
146
        if ($sniffName) {
147 1
            $sniffFQCN .= '.' . $sniffName;
148 1
        }
149
150
        return $sniffFQCN;
151 1
    }
152
153 3
    /**
154
     * Processes the tokens that this test is listening for.
155
     *
156
     * @param File $file The file where this token was found.
157
     * @param int $functionPos The position in the stack where this token was found.
158
     * @param int $classPos The position in the tokens array that opened the scope that this test is listening for.
159
     *
160
     * @return void
161
     */
162
    protected function processTokenWithinScope(
163
        File $file,
164
        $functionPos,
165
        $classPos
166 4
    ): void {
167
        $isSuppressed = SuppressHelper::isSniffSuppressed(
168 4
            $file,
169
            $functionPos,
170 4
            $this->getSniffName(static::CODE_NO_RETURN_FOUND)
171
        );
172
173
        if (!$isSuppressed && $this->checkIfSetterFunction($classPos, $file, $functionPos)) {
174
            $this->checkForFluentSetterErrors($file, $functionPos, $classPos);
175
        }
176
    }
177
178
    /**
179
     * Checks if the given method name relates to a setter function of a property.
180
     *
181 1
     * @param int $classPosition The position of the class token.
182
     * @param File $file The file of the sniff.
183 1
     * @param int $methodPosition The position of the method token.
184 1
     *
185
     * @return bool Indicator if the given method is a setter function
186 1
     */
187
    private function checkIfSetterFunction(int $classPosition, File $file, int $methodPosition): bool
188 1
    {
189 1
        $isSetter = false;
190 1
        $methodName = $file->getDeclarationName($methodPosition);
191 1
192 1
        if (substr($methodName, 0, 3) === 'set') {
193 1
            // We define in our styleguide, that there is only one class per file!
194
            $properties = (new PropertyHelper(new FileDecorator($file)))->getProperties(
195
                $file->getTokens()[$classPosition]
196
            );
197
198
            // We require camelCase for methods and properties,
199
            // so there should be an "lcfirst-Method" without set-prefix.
200
            $isSetter = in_array(lcfirst(substr($methodName, 3)), $properties, true);
201
        }
202
203 1
        return $isSetter;
204
    }
205 1
206
    /**
207 1
     * Fixes if no return statement is found.
208 1
     *
209
     * @param File $phpcsFile The php cs file
210
     * @param int $closingBracePtr Pointer to the closing curly brace of the function
211 1
     *
212 1
     * @return void
213 1
     */
214 1
    private function fixNoReturnFound(File $phpcsFile, int $closingBracePtr)
215
    {
216 1
        $tokens = $phpcsFile->getTokens();
217 1
        $closingBraceToken = $tokens[$closingBracePtr];
218
219
        $expectedReturnSpaces = str_repeat($this->identation, $closingBraceToken['level'] + 1);
220
221
        $phpcsFile->fixer->beginChangeset();
222
        $phpcsFile->fixer->addNewlineBefore($closingBracePtr - 1);
223
        $phpcsFile->fixer->addContentBefore($closingBracePtr - 1, $expectedReturnSpaces . 'return $this;');
224
        $phpcsFile->fixer->addNewlineBefore($closingBracePtr - 1);
225
        $phpcsFile->fixer->endChangeset();
226
    }
227
228
    /**
229
     * Fixes the return value of a function to $this.
230
     *
231
     * @param File $phpcsFile The php cs file
232
     * @param int $returnPtr Pointer to the return token
233
     *
234
     * @return void
235
     */
236
    private function fixMustReturnThis(File $phpcsFile, $returnPtr)
237
    {
238
        $returnSemicolonPtr = $phpcsFile->findEndOfStatement($returnPtr);
239
240
        for ($i = $returnPtr + 1; $i < $returnSemicolonPtr; $i++) {
241
            $phpcsFile->fixer->replaceToken($i, '');
242
        }
243
244
        $phpcsFile->fixer->beginChangeset();
245
        $phpcsFile->fixer->addContentBefore(
246
            $returnSemicolonPtr,
247
            ' $this'
248
        );
249
        $phpcsFile->fixer->endChangeset();
250
    }
251
}
252