Passed
Pull Request — master (#40)
by
unknown
06:43
created

FluentSetterSniff::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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