PgsqlPDOQuerySplitter::nextQuery()   F
last analyzed

Complexity

Conditions 42
Paths 1335

Size

Total Lines 160
Code Lines 98

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 73
CRAP Score 46.9659

Importance

Changes 0
Metric Value
eloc 98
c 0
b 0
f 0
dl 0
loc 160
ccs 73
cts 85
cp 0.8588
rs 0
cc 42
nc 1335
nop 0
crap 46.9659

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing\Task\System\Pdo;
22
23
/**
24
 * Splits PostgreSQL's dialect of SQL into separate queries.
25
 *
26
 * Unlike DefaultPDOQuerySplitter this uses a lexer instead of regular
27
 * expressions. This allows handling complex constructs like C-style comments
28
 * (including nested ones) and dollar-quoted strings.
29
 *
30
 * @author  Alexey Borzov <[email protected]>
31
 *
32
 * @see    http://www.phing.info/trac/ticket/499
33
 * @see    http://www.postgresql.org/docs/current/interactive/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING
34
 */
35
class PgsqlPDOQuerySplitter extends PDOQuerySplitter
36
{
37
    /*#@+
38
     * Lexer states
39
     */
40
    public const STATE_NORMAL = 0;
41
    public const STATE_SINGLE_QUOTED = 1;
42
    public const STATE_DOUBLE_QUOTED = 2;
43
    public const STATE_DOLLAR_QUOTED = 3;
44
    public const STATE_COMMENT_LINEEND = 4;
45
    public const STATE_COMMENT_MULTILINE = 5;
46
    public const STATE_BACKSLASH = 6;
47
    // #@-
48
49
    /**
50
     * Nesting depth of current multiline comment.
51
     *
52
     * @var int
53
     */
54
    protected $commentDepth = 0;
55
56
    /**
57
     * Current dollar-quoting "tag".
58
     *
59
     * @var string
60
     */
61
    protected $quotingTag = '';
62
63
    /**
64
     * Current lexer state, one of STATE_* constants.
65
     *
66
     * @var int
67
     */
68
    protected $state = self::STATE_NORMAL;
69
70
    /**
71
     * Whether a backslash was just encountered in quoted string.
72
     *
73
     * @var bool
74
     */
75
    protected $escape = false;
76
77
    /**
78
     * Current source line being examined.
79
     *
80
     * @var string
81
     */
82
    protected $line = '';
83
84
    /**
85
     * Position in current source line.
86
     *
87
     * @var int
88
     */
89
    protected $inputIndex;
90
91
    /**
92
     * Gets next symbol from the input, false if at end.
93
     *
94
     * @return bool|string
95
     */
96 1
    public function getc()
97
    {
98 1
        if (!strlen($this->line) || $this->inputIndex >= strlen($this->line)) {
99 1
            if (null === ($line = $this->sqlReader->readLine())) {
100 1
                return false;
101
            }
102 1
            $project = $this->parent->getOwningTarget()->getProject();
103 1
            $this->line = $project->replaceProperties($line) . "\n";
104 1
            $this->inputIndex = 0;
105
        }
106
107 1
        return $this->line[$this->inputIndex++];
108
    }
109
110
    /**
111
     * Bactracks one symbol on the input.
112
     *
113
     * NB: we don't need ungetc() at the start of the line, so this case is
114
     * not handled.
115
     */
116 1
    public function ungetc(): void
117
    {
118 1
        --$this->inputIndex;
119
    }
120
121
    /**
122
     * @return null|string
123
     */
124 1
    public function nextQuery(): ?string
125
    {
126 1
        $sql = '';
127 1
        $delimiter = $this->parent->getDelimiter();
128 1
        $openParens = 0;
129
130 1
        while (false !== ($ch = $this->getc())) {
131 1
            switch ($this->state) {
132
                case self::STATE_NORMAL:
133
                    switch ($ch) {
134 1
                        case '-':
135 1
                            if ('-' === $this->getc()) {
136 1
                                $this->state = self::STATE_COMMENT_LINEEND;
137
                            } else {
138
                                $this->ungetc();
139
                            }
140
141 1
                            break;
142
143 1
                        case '"':
144 1
                            $this->state = self::STATE_DOUBLE_QUOTED;
145
146 1
                            break;
147
148 1
                        case "'":
149 1
                            $this->state = self::STATE_SINGLE_QUOTED;
150
151 1
                            break;
152
153 1
                        case '/':
154 1
                            if ('*' === $this->getc()) {
155 1
                                $this->state = self::STATE_COMMENT_MULTILINE;
156 1
                                $this->commentDepth = 1;
157
                            } else {
158 1
                                $this->ungetc();
159
                            }
160
161 1
                            break;
162
163 1
                        case '$':
164 1
                            if (false !== ($tag = $this->checkDollarQuote())) {
165 1
                                $this->state = self::STATE_DOLLAR_QUOTED;
166 1
                                $this->quotingTag = $tag;
0 ignored issues
show
Documentation Bug introduced by
It seems like $tag can also be of type true. However, the property $quotingTag is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
167 1
                                $sql .= '$' . $tag . '$';
0 ignored issues
show
Bug introduced by
Are you sure $tag of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

167
                                $sql .= '$' . /** @scrutinizer ignore-type */ $tag . '$';
Loading history...
168
169 1
                                continue 3;
170
                            }
171
172
                            break;
173
174 1
                        case '(':
175 1
                            $openParens++;
176
177 1
                            break;
178
179 1
                        case ')':
180 1
                            $openParens--;
181
182 1
                            break;
183
                        // technically we can use e.g. psql's \g command as delimiter
184 1
                        case $delimiter[0]:
185
                            // special case to allow "create rule" statements
186
                            // http://www.postgresql.org/docs/current/interactive/sql-createrule.html
187 1
                            if (';' === $delimiter && 0 < $openParens) {
188 1
                                break;
189
                            }
190 1
                            $hasQuery = true;
191 1
                            for ($i = 1, $delimiterLength = strlen($delimiter); $i < $delimiterLength; ++$i) {
192
                                if ($delimiter[$i] != $this->getc()) {
193
                                    $hasQuery = false;
194
                                }
195
                            }
196 1
                            if ($hasQuery) {
197 1
                                return $sql;
198
                            }
199
200
                            for ($j = 1; $j < $i; ++$j) {
201
                                $this->ungetc();
202
                            }
203
                    }
204
205 1
                    break;
206
207
                case self::STATE_COMMENT_LINEEND:
208 1
                    if ("\n" === $ch) {
209 1
                        $this->state = self::STATE_NORMAL;
210
                    }
211
212 1
                    break;
213
214
                case self::STATE_COMMENT_MULTILINE:
215
                    switch ($ch) {
216 1
                        case '/':
217 1
                            if ('*' !== $this->getc()) {
218
                                $this->ungetc();
219
                            } else {
220 1
                                ++$this->commentDepth;
221
                            }
222
223 1
                            break;
224
225 1
                        case '*':
226 1
                            if ('/' !== $this->getc()) {
227
                                $this->ungetc();
228
                            } else {
229 1
                                --$this->commentDepth;
230 1
                                if (0 == $this->commentDepth) {
231 1
                                    $this->state = self::STATE_NORMAL;
232
233 1
                                    continue 3;
234
                                }
235
                            }
236
                    }
237
238
                // no break
239
                case self::STATE_SINGLE_QUOTED:
240
                case self::STATE_DOUBLE_QUOTED:
241 1
                    if ($this->escape) {
242
                        $this->escape = false;
243
244
                        break;
245
                    }
246 1
                    $quote = self::STATE_SINGLE_QUOTED == $this->state ? "'" : '"';
247
248
                    switch ($ch) {
249 1
                        case '\\':
250
                            $this->escape = true;
251
252
                            break;
253
254 1
                        case $quote:
255 1
                            if ($quote == $this->getc()) {
256 1
                                $sql .= $quote;
257
                            } else {
258 1
                                $this->ungetc();
259 1
                                $this->state = self::STATE_NORMAL;
260
                            }
261
                    }
262
263
                // no break
264
                case self::STATE_DOLLAR_QUOTED:
265 1
                    if ('$' === $ch && false !== ($tag = $this->checkDollarQuote())) {
266 1
                        if ($tag == $this->quotingTag) {
267 1
                            $this->state = self::STATE_NORMAL;
268
                        }
269 1
                        $sql .= '$' . $tag . '$';
270
271 1
                        continue 2;
272
                    }
273
            }
274
275 1
            if (self::STATE_COMMENT_LINEEND != $this->state && self::STATE_COMMENT_MULTILINE != $this->state) {
276 1
                $sql .= $ch;
277
            }
278
        }
279 1
        if ('' !== $sql) {
280 1
            return $sql;
281
        }
282
283 1
        return null;
284
    }
285
286
    /**
287
     * Checks whether symbols after $ are a valid dollar-quoting tag.
288
     *
289
     * @return bool|string Dollar-quoting "tag" if it is present, false otherwise
290
     */
291 1
    protected function checkDollarQuote()
292
    {
293 1
        $ch = $this->getc();
294 1
        if ('$' === $ch) {
295
            // empty tag
296 1
            return '';
297
        }
298
299 1
        if (!ctype_alpha($ch) && '_' !== $ch) {
300
            // not a delimiter
301 1
            $this->ungetc();
302
303 1
            return false;
304
        }
305
306 1
        $tag = $ch;
307 1
        while (false !== ($ch = $this->getc())) {
308 1
            if ('$' === $ch) {
309 1
                return $tag;
310
            }
311
312 1
            if (ctype_alnum($ch) || '_' === $ch) {
313 1
                $tag .= $ch;
314
            } else {
315
                for ($i = 0, $tagLength = strlen($tag); $i < $tagLength; ++$i) {
0 ignored issues
show
Bug introduced by
It seems like $tag can also be of type boolean; however, parameter $string of strlen() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

315
                for ($i = 0, $tagLength = strlen(/** @scrutinizer ignore-type */ $tag); $i < $tagLength; ++$i) {
Loading history...
316
                    $this->ungetc();
317
                }
318
319
                return false;
320
            }
321
        }
322
323
        return $tag;
324
    }
325
}
326