PgsqlPDOQuerySplitter::nextQuery()   F
last analyzed

Complexity

Conditions 42
Paths 1335

Size

Total Lines 160
Code Lines 98

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 98
dl 0
loc 160
rs 0
c 0
b 0
f 0
cc 42
nc 1335
nop 0

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
    public function getc()
97
    {
98
        if (!strlen($this->line) || $this->inputIndex >= strlen($this->line)) {
99
            if (null === ($line = $this->sqlReader->readLine())) {
100
                return false;
101
            }
102
            $project = $this->parent->getOwningTarget()->getProject();
103
            $this->line = $project->replaceProperties($line) . "\n";
104
            $this->inputIndex = 0;
105
        }
106
107
        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
    public function ungetc(): void
117
    {
118
        --$this->inputIndex;
119
    }
120
121
    /**
122
     * @return null|string
123
     */
124
    public function nextQuery(): ?string
125
    {
126
        $sql = '';
127
        $delimiter = $this->parent->getDelimiter();
128
        $openParens = 0;
129
130
        while (false !== ($ch = $this->getc())) {
131
            switch ($this->state) {
132
                case self::STATE_NORMAL:
133
                    switch ($ch) {
134
                        case '-':
135
                            if ('-' === $this->getc()) {
136
                                $this->state = self::STATE_COMMENT_LINEEND;
137
                            } else {
138
                                $this->ungetc();
139
                            }
140
141
                            break;
142
143
                        case '"':
144
                            $this->state = self::STATE_DOUBLE_QUOTED;
145
146
                            break;
147
148
                        case "'":
149
                            $this->state = self::STATE_SINGLE_QUOTED;
150
151
                            break;
152
153
                        case '/':
154
                            if ('*' === $this->getc()) {
155
                                $this->state = self::STATE_COMMENT_MULTILINE;
156
                                $this->commentDepth = 1;
157
                            } else {
158
                                $this->ungetc();
159
                            }
160
161
                            break;
162
163
                        case '$':
164
                            if (false !== ($tag = $this->checkDollarQuote())) {
165
                                $this->state = self::STATE_DOLLAR_QUOTED;
166
                                $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
                                $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
                                continue 3;
170
                            }
171
172
                            break;
173
174
                        case '(':
175
                            $openParens++;
176
177
                            break;
178
179
                        case ')':
180
                            $openParens--;
181
182
                            break;
183
                        // technically we can use e.g. psql's \g command as delimiter
184
                        case $delimiter[0]:
185
                            // special case to allow "create rule" statements
186
                            // http://www.postgresql.org/docs/current/interactive/sql-createrule.html
187
                            if (';' === $delimiter && 0 < $openParens) {
188
                                break;
189
                            }
190
                            $hasQuery = true;
191
                            for ($i = 1, $delimiterLength = strlen($delimiter); $i < $delimiterLength; ++$i) {
192
                                if ($delimiter[$i] != $this->getc()) {
193
                                    $hasQuery = false;
194
                                }
195
                            }
196
                            if ($hasQuery) {
197
                                return $sql;
198
                            }
199
200
                            for ($j = 1; $j < $i; ++$j) {
201
                                $this->ungetc();
202
                            }
203
                    }
204
205
                    break;
206
207
                case self::STATE_COMMENT_LINEEND:
208
                    if ("\n" === $ch) {
209
                        $this->state = self::STATE_NORMAL;
210
                    }
211
212
                    break;
213
214
                case self::STATE_COMMENT_MULTILINE:
215
                    switch ($ch) {
216
                        case '/':
217
                            if ('*' !== $this->getc()) {
218
                                $this->ungetc();
219
                            } else {
220
                                ++$this->commentDepth;
221
                            }
222
223
                            break;
224
225
                        case '*':
226
                            if ('/' !== $this->getc()) {
227
                                $this->ungetc();
228
                            } else {
229
                                --$this->commentDepth;
230
                                if (0 == $this->commentDepth) {
231
                                    $this->state = self::STATE_NORMAL;
232
233
                                    continue 3;
234
                                }
235
                            }
236
                    }
237
238
                // no break
239
                case self::STATE_SINGLE_QUOTED:
240
                case self::STATE_DOUBLE_QUOTED:
241
                    if ($this->escape) {
242
                        $this->escape = false;
243
244
                        break;
245
                    }
246
                    $quote = self::STATE_SINGLE_QUOTED == $this->state ? "'" : '"';
247
248
                    switch ($ch) {
249
                        case '\\':
250
                            $this->escape = true;
251
252
                            break;
253
254
                        case $quote:
255
                            if ($quote == $this->getc()) {
256
                                $sql .= $quote;
257
                            } else {
258
                                $this->ungetc();
259
                                $this->state = self::STATE_NORMAL;
260
                            }
261
                    }
262
263
                // no break
264
                case self::STATE_DOLLAR_QUOTED:
265
                    if ('$' === $ch && false !== ($tag = $this->checkDollarQuote())) {
266
                        if ($tag == $this->quotingTag) {
267
                            $this->state = self::STATE_NORMAL;
268
                        }
269
                        $sql .= '$' . $tag . '$';
270
271
                        continue 2;
272
                    }
273
            }
274
275
            if (self::STATE_COMMENT_LINEEND != $this->state && self::STATE_COMMENT_MULTILINE != $this->state) {
276
                $sql .= $ch;
277
            }
278
        }
279
        if ('' !== $sql) {
280
            return $sql;
281
        }
282
283
        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
    protected function checkDollarQuote()
292
    {
293
        $ch = $this->getc();
294
        if ('$' === $ch) {
295
            // empty tag
296
            return '';
297
        }
298
299
        if (!ctype_alpha($ch) && '_' !== $ch) {
300
            // not a delimiter
301
            $this->ungetc();
302
303
            return false;
304
        }
305
306
        $tag = $ch;
307
        while (false !== ($ch = $this->getc())) {
308
            if ('$' === $ch) {
309
                return $tag;
310
            }
311
312
            if (ctype_alnum($ch) || '_' === $ch) {
313
                $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