Completed
Push — master ( 14c836...906b18 )
by Björn
06:09 queued 03:52
created

FluentSetterSniff   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 235
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 0
loc 235
rs 10
c 0
b 0
f 0
ccs 68
cts 68
cp 1
wmc 17
lcom 1
cbo 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BestIt\Sniffs\Functions;
6
7
use BestIt\CodeSniffer\Helper\PropertyHelper;
8
use BestIt\CodeSniffer\Helper\TokenHelper;
9
use BestIt\Sniffs\SuppressingTrait;
10
use PHP_CodeSniffer\Files\File;
11
use PHP_CodeSniffer\Standards\Squiz\Sniffs\Scope\MethodScopeSniff;
12
use function in_array;
13
use function substr;
14
15
/**
16
 * Checks if a fluent setter is used per default.
17
 *
18
 * @package BestIt\Sniffs\Functions
19
 *
20
 * @author Nick Lubisch <[email protected]>
21
 * @author Björn Lange <[email protected]>
22
 */
23
class FluentSetterSniff extends MethodScopeSniff
24
{
25
    use SuppressingTrait;
26
27
    /**
28
     * Every setter function MUST return $this if nothing else is returned.
29
     */
30
    public const CODE_MUST_RETURN_THIS = 'MustReturnThis';
31
32
    /**
33
     * Your method MUST contain a return.
34
     */
35
    public const CODE_NO_RETURN_FOUND = 'NoReturnFound';
36
37
    /**
38
     * Error message when the method does not return $this.
39
     */
40
    private const ERROR_MUST_RETURN_THIS = 'The method "%s" must return $this';
41
42
    /**
43
     * Error message when no return statement is found.
44
     */
45
    private const ERROR_NO_RETURN_FOUND = 'Method "%s" has no return statement';
46
47
    /**
48
     * Specifies how an identation looks like.
49
     *
50
     * @var string
51
     */
52
    public string $identation = '    ';
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_STRING, expecting T_FUNCTION or T_CONST
Loading history...
53
54
    /**
55
     * The used file decorated for the interface.
56
     *
57
     * @var File
58
     */
59
    private File $file;
60
61
    /**
62
     * The position of this node.
63
     *
64
     * @var int
65
     */
66
    private int $stackPos;
67
68
    /**
69
     * Registers an error if an empty return (return null; or return;) is given.
70
     *
71 4
     * @param File $file The sniffed file.
72
     * @param int $functionPos The position of the function.
73 4
     * @param int $returnPos The position of the return call.
74 4
     * @param string $methodIdent The ident for the method to given in an error.
75
     *
76
     * @return void
77
     */
78
    private function checkAndRegisterEmptyReturnErrors(
79
        File $file,
80
        int $functionPos,
81
        int $returnPos,
82
        string $methodIdent
83
    ): void {
84
        $nextToken = $file->getTokens()[TokenHelper::findNextEffective($file, $returnPos + 1)];
85 4
86
        if (!$nextToken || (in_array($nextToken['content'], ['null', ';']))) {
87
            $fixMustReturnThis = $file->addFixableError(
88
                self::ERROR_MUST_RETURN_THIS,
89
                $functionPos,
90 4
                static::CODE_MUST_RETURN_THIS,
91 4
                $methodIdent
92
            );
93 4
94 1
            if ($fixMustReturnThis) {
95
                $this->fixMustReturnThis($file, $returnPos);
96
            }
97 4
        }
98 4
    }
99
100 4
    /**
101 4
     * Checks if there are fluent setter errors and registers errors if needed.
102 4
     *
103
     * @param File $phpcsFile The file for this sniff.
104 4
     * @param int $functionPos The position of the used token.
105
     * @param int $classPos The position of the class.
106 4
     *
107 1
     * @return void
108 1
     */
109 1
    private function checkForFluentSetterErrors(File $phpcsFile, int $functionPos, int $classPos): void
110 1
    {
111 1
        $tokens = $phpcsFile->getTokens();
112
        $errorData = $phpcsFile->getDeclarationName($classPos) . '::' . $phpcsFile->getDeclarationName($functionPos);
113
114 1
        $functionToken = $tokens[$functionPos];
115 1
        $openBracePtr = $functionToken['scope_opener'];
116
        $closeBracePtr = $functionToken['scope_closer'];
117
118 1
        $returnPtr = $phpcsFile->findNext(T_RETURN, $openBracePtr, $closeBracePtr);
119
120
        if ($returnPtr === false) {
121 4
            $fixNoReturnFound = $phpcsFile->addFixableError(
122 4
                self::ERROR_NO_RETURN_FOUND,
123 4
                $functionPos,
124 4
                static::CODE_NO_RETURN_FOUND,
125
                $errorData
126
            );
127 4
128 1
            if ($fixNoReturnFound) {
129 1
                $this->fixNoReturnFound($phpcsFile, $closeBracePtr);
130 1
            }
131 1
132 1
            return;
133
        }
134 1
135
        $this->checkAndRegisterEmptyReturnErrors($phpcsFile, $functionPos, $returnPtr, $errorData);
136
    }
137 3
138
    /**
139 3
     * Returns the used file decorated for the interface.
140 1
     *
141 1
     * @return File
142 1
     */
143 1
    public function getFile(): File
144 1
    {
145
        return $this->file;
146
    }
147 1
148 1
    /**
149
     * Returns the position of this node.
150
     *
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 = $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($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