Passed
Push — main ( ddd1a4...b98cb8 )
by Michiel
08:47
created

PgsqlPDOQuerySplitter   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 264
Duplicated Lines 0 %

Test Coverage

Coverage 86.67%

Importance

Changes 0
Metric Value
wmc 56
eloc 136
dl 0
loc 264
rs 5.5199
c 0
b 0
f 0
ccs 104
cts 120
cp 0.8667

4 Methods

Rating   Name   Duplication   Size   Complexity  
A getc() 0 12 4
F nextQuery() 0 137 42
A ungetc() 0 3 1
B checkDollarQuote() 0 29 9

How to fix   Complexity   

Complex Class

Complex classes like PgsqlPDOQuerySplitter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PgsqlPDOQuerySplitter, and based on these observations, apply Extract Interface, too.

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

151
                for ($i = 0, $tagLength = strlen(/** @scrutinizer ignore-type */ $tag); $i < $tagLength; $i++) {
Loading history...
152
                    $this->ungetc();
153
                }
154
155
                return false;
156
            }
157
        }
158
    }
159
160
    /**
161
     * @return null|string
162
     */
163 1
    public function nextQuery()
164
    {
165 1
        $sql = '';
166 1
        $delimiter = $this->parent->getDelimiter();
167 1
        $openParens = 0;
168
169 1
        while (false !== ($ch = $this->getc())) {
170 1
            switch ($this->state) {
171 1
                case self::STATE_NORMAL:
172
                    switch ($ch) {
173 1
                        case '-':
174 1
                            if ('-' == $this->getc()) {
175 1
                                $this->state = self::STATE_COMMENT_LINEEND;
176
                            } else {
177
                                $this->ungetc();
178
                            }
179 1
                            break;
180 1
                        case '"':
181 1
                            $this->state = self::STATE_DOUBLE_QUOTED;
182 1
                            break;
183 1
                        case "'":
184 1
                            $this->state = self::STATE_SINGLE_QUOTED;
185 1
                            break;
186 1
                        case '/':
187 1
                            if ('*' === $this->getc()) {
188 1
                                $this->state = self::STATE_COMMENT_MULTILINE;
189 1
                                $this->commentDepth = 1;
190
                            } else {
191 1
                                $this->ungetc();
192
                            }
193 1
                            break;
194 1
                        case '$':
195 1
                            if (false !== ($tag = $this->checkDollarQuote())) {
196 1
                                $this->state = self::STATE_DOLLAR_QUOTED;
197 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...
198 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

198
                                $sql .= '$' . /** @scrutinizer ignore-type */ $tag . '$';
Loading history...
199 1
                                continue 3;
200
                            }
201
                            break;
202 1
                        case '(':
203 1
                            $openParens++;
204 1
                            break;
205 1
                        case ')':
206 1
                            $openParens--;
207 1
                            break;
208
                        // technically we can use e.g. psql's \g command as delimiter
209 1
                        case $delimiter[0]:
210
                            // special case to allow "create rule" statements
211
                            // http://www.postgresql.org/docs/current/interactive/sql-createrule.html
212 1
                            if (';' === $delimiter && 0 < $openParens) {
213 1
                                break;
214
                            }
215 1
                            $hasQuery = true;
216 1
                            for ($i = 1, $delimiterLength = strlen($delimiter); $i < $delimiterLength; $i++) {
217
                                if ($delimiter[$i] != $this->getc()) {
218
                                    $hasQuery = false;
219
                                }
220
                            }
221 1
                            if ($hasQuery) {
222 1
                                return $sql;
223
                            }
224
225
                            for ($j = 1; $j < $i; $j++) {
226
                                $this->ungetc();
227
                            }
228
                    }
229 1
                    break;
230
231 1
                case self::STATE_COMMENT_LINEEND:
232 1
                    if ("\n" === $ch) {
233 1
                        $this->state = self::STATE_NORMAL;
234
                    }
235 1
                    break;
236
237 1
                case self::STATE_COMMENT_MULTILINE:
238 1
                    switch ($ch) {
239 1
                        case '/':
240 1
                            if ('*' != $this->getc()) {
241
                                $this->ungetc();
242
                            } else {
243 1
                                $this->commentDepth++;
244
                            }
245 1
                            break;
246
247 1
                        case '*':
248 1
                            if ('/' != $this->getc()) {
249
                                $this->ungetc();
250
                            } else {
251 1
                                $this->commentDepth--;
252 1
                                if (0 == $this->commentDepth) {
253 1
                                    $this->state = self::STATE_NORMAL;
254 1
                                    continue 3;
255
                                }
256
                            }
257
                    }
258
259
                // no break
260 1
                case self::STATE_SINGLE_QUOTED:
261 1
                case self::STATE_DOUBLE_QUOTED:
262 1
                    if ($this->escape) {
263
                        $this->escape = false;
264
                        break;
265
                    }
266 1
                    $quote = $this->state == self::STATE_SINGLE_QUOTED ? "'" : '"';
267
                    switch ($ch) {
268 1
                        case '\\':
269
                            $this->escape = true;
270
                            break;
271 1
                        case $quote:
272 1
                            if ($quote == $this->getc()) {
273 1
                                $sql .= $quote;
274
                            } else {
275 1
                                $this->ungetc();
276 1
                                $this->state = self::STATE_NORMAL;
277
                            }
278
                    }
279
280
                // no break
281 1
                case self::STATE_DOLLAR_QUOTED:
282 1
                    if ('$' == $ch && false !== ($tag = $this->checkDollarQuote())) {
283 1
                        if ($tag == $this->quotingTag) {
284 1
                            $this->state = self::STATE_NORMAL;
285
                        }
286 1
                        $sql .= '$' . $tag . '$';
287 1
                        continue 2;
288
                    }
289
            }
290
291 1
            if ($this->state != self::STATE_COMMENT_LINEEND && $this->state != self::STATE_COMMENT_MULTILINE) {
292 1
                $sql .= $ch;
293
            }
294
        }
295 1
        if ('' !== $sql) {
296 1
            return $sql;
297
        }
298
299 1
        return null;
300
    }
301
}
302