Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 160
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
PEGParserBase
0.00% covered (danger)
0.00%
0 / 160
0.00% covered (danger)
0.00%
0 / 20
2652
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 traceCall
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 text
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 location
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expected
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 error
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 charAt
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 charsAt
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 consumeChar
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 newRef
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 computePosDetails
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
90
 computeLocation
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 fail
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 expandExpectations
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 buildMessage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 buildException
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 buildParseException
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 initialize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initInternal
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 parse
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0
1<?php
2
3namespace Wikimedia\WikiPEG;
4
5abstract class PEGParserBase {
6    protected static $FAILED;
7    protected static $UNDEFINED;
8    protected $currPos;
9    protected $savedPos;
10    protected $input;
11    protected $inputLength;
12    protected $options;
13    protected $cache;
14
15    /** @var array<int,array{line:int,column:int,seenCR:bool}> */
16    protected $posDetailsCache;
17    protected $maxFailPos;
18    protected $maxFailExpected;
19
20    /** @var array Associative arrays of expectation info */
21    protected $expectations;
22
23    /** @var Expectation[] */
24    private $expectationCache;
25
26    /** @var Tracer */
27    protected $tracer;
28
29    public function __construct() {
30        if ( !self::$FAILED ) {
31            self::$FAILED = new \stdClass;
32        }
33        if ( !self::$UNDEFINED ) {
34            self::$UNDEFINED = new \stdClass;
35        }
36    }
37
38    protected function traceCall( $parseFunc, $name, $argNames, $args ) {
39        $argMap = [];
40        foreach ( $args as $i => $argValue ) {
41            $argMap[$argNames[$i]] = $argValue;
42        }
43        $startPos = $this->currPos;
44        $this->tracer->trace( [
45            'type' => 'rule.enter',
46            'rule' => $name,
47            'location' => $this->computeLocation( $startPos, $startPos ),
48            'args' => $argMap
49        ] );
50        $result = call_user_func_array( $parseFunc, $args );
51        if ( $result !== self::$FAILED ) {
52            $this->tracer->trace( [
53                'type' => 'rule.match',
54                'rule' => $name,
55                'location' => $this->computeLocation( $startPos, $this->currPos ),
56            ] );
57        } else {
58            $this->tracer->trace( [
59                'type' => 'rule.fail',
60                'rule' => $name,
61                'result' => $result,
62                'location' => $this->computeLocation( $startPos, $startPos )
63            ] );
64        }
65        return $result;
66    }
67
68    protected function text() {
69        return substr( $this->input, $this->savedPos, $this->currPos - $this->savedPos );
70    }
71
72    protected function location() {
73        return $this->computeLocation( $this->savedPos, $this->currPos );
74    }
75
76    /**
77     * @param string $description
78     * @return never
79     */
80    protected function expected( $description ) {
81        throw $this->buildException(
82            null,
83            [ [ 'type' => "other", 'description' => $description ] ],
84            $this->text(),
85            $this->computeLocation( $this->savedPos, $this->currPos )
86        );
87    }
88
89    /**
90     * @param string $message
91     * @return never
92     */
93    protected function error( $message ) {
94        throw $this->buildException(
95            $message,
96            null,
97            $this->text(),
98            $this->computeLocation( $this->savedPos, $this->currPos )
99        );
100    }
101
102    public static function charAt( $s, $byteOffset ) {
103        if ( !isset( $s[$byteOffset] ) ) {
104            return '';
105        }
106        $char = $s[$byteOffset];
107        $byte1 = ord( $char );
108        if ( ( $byte1 & 0xc0 ) === 0xc0 ) {
109            $char .= $s[$byteOffset + 1];
110        }
111        if ( ( $byte1 & 0xe0 ) === 0xe0 ) {
112            $char .= $s[$byteOffset + 2];
113        }
114        if ( ( $byte1 & 0xf0 ) === 0xf0 ) {
115            $char .= $s[$byteOffset + 3];
116        }
117        return $char;
118    }
119
120    public static function charsAt( $s, $byteOffset, $numChars ) {
121        $ret = '';
122        for ( $i = 0; $i < $numChars; $i++ ) {
123            $ret .= self::consumeChar( $s, $byteOffset );
124        }
125        return $ret;
126    }
127
128    public static function consumeChar( $s, &$byteOffset ) {
129        if ( !isset( $s[$byteOffset] ) ) {
130            return '';
131        }
132        $char = $s[$byteOffset++];
133        $byte1 = ord( $char );
134        if ( ( $byte1 & 0xc0 ) === 0xc0 ) {
135            $char .= $s[$byteOffset++];
136        }
137        if ( ( $byte1 & 0xe0 ) === 0xe0 ) {
138            $char .= $s[$byteOffset++];
139        }
140        if ( ( $byte1 & 0xf0 ) === 0xf0 ) {
141            $char .= $s[$byteOffset++];
142        }
143        return $char;
144    }
145
146    public static function &newRef( $value ) {
147        return $value;
148    }
149
150    /**
151     * @param int $pos
152     * @return array{line:int,column:int,seenCR:bool}
153     */
154    protected function computePosDetails( $pos ) {
155        if ( isset( $this->posDetailsCache[$pos] ) ) {
156            return $this->posDetailsCache[$pos];
157        }
158        $p = $pos - 1;
159        while ( !isset( $this->posDetailsCache[$p] ) ) {
160            $p--;
161        }
162
163        $details = $this->posDetailsCache[$p];
164
165        while ( $p < $pos ) {
166            $ch = self::charAt( $this->input, $p );
167            if ( $ch === "\n" ) {
168                if ( !$details['seenCR'] ) {
169                    $details['line']++;
170                }
171                $details['column'] = 1;
172                $details['seenCR'] = false;
173            } elseif ( $ch === "\r" || $ch === "\u2028" || $ch === "\u2029" ) {
174                $details['line']++;
175                $details['column'] = 1;
176                $details['seenCR'] = true;
177            } else {
178                $details['column']++;
179                $details['seenCR'] = false;
180            }
181
182            $p++;
183        }
184
185        $this->posDetailsCache[$pos] = $details;
186        return $details;
187    }
188
189    protected function computeLocation( $startPos, $endPos ) {
190        if ( $endPos > $this->inputLength ) {
191            $endPos--;
192        }
193        $startPosDetails = $this->computePosDetails( $startPos );
194        $endPosDetails = $this->computePosDetails( $endPos );
195
196        return new LocationRange(
197            $startPos,
198            $startPosDetails['line'],
199            $startPosDetails['column'],
200            $endPos,
201            $endPosDetails['line'],
202            $endPosDetails['column']
203        );
204    }
205
206    protected function fail( $expected ) {
207        if ( $this->currPos < $this->maxFailPos ) {
208            return;
209        }
210
211        if ( $this->currPos > $this->maxFailPos ) {
212            $this->maxFailPos = $this->currPos;
213            $this->maxFailExpected = [];
214        }
215
216        $this->maxFailExpected[] = $expected;
217    }
218
219    /**
220     * @param array<int|array{type:string,value?:?string,description:string}> $expected
221     * @return Expectation[]
222     */
223    private function expandExpectations( $expected ) {
224        $expanded = [];
225        foreach ( $expected as $index ) {
226            if ( is_int( $index ) ) {
227                if ( !isset( $this->expectationCache[$index] ) ) {
228                    $this->expectationCache[$index] = new Expectation( $this->expectations[$index] );
229                }
230                $expanded[] = $this->expectationCache[$index];
231            } else {
232                $expanded[] = new Expectation( $index );
233            }
234        }
235        return $expanded;
236    }
237
238    private function buildMessage( $expected, $found ) {
239        $expectedDescs = [];
240
241        foreach ( $expected as $info ) {
242            $expectedDescs[] = $info->description;
243        }
244        $lastDesc = array_pop( $expectedDescs );
245        if ( $expectedDescs ) {
246            $expectedDesc = implode( ', ', $expectedDescs ) . ' or ' . $lastDesc;
247        } else {
248            $expectedDesc = $lastDesc;
249        }
250        $foundDesc = $found ? json_encode( $found ) : "end of input";
251
252        return "Expected " . $expectedDesc . " but " . $foundDesc . " found.";
253    }
254
255    protected function buildException( $message, $expected, $found, $location ) {
256        if ( $expected !== null ) {
257            sort( $expected );
258            $expected = array_unique( $expected );
259            $expandedExpected = $this->expandExpectations( $expected );
260            usort( $expandedExpected, static function ( $a, $b ) {
261                return Expectation::compare( $a, $b );
262            } );
263        } else {
264            $expandedExpected = [];
265        }
266
267        return new SyntaxError(
268            $message ?? $this->buildMessage( $expandedExpected, $found ),
269            $expandedExpected,
270            $found,
271            $location
272        );
273    }
274
275    protected function buildParseException() {
276        $char = self::charAt( $this->input, $this->maxFailPos );
277        return $this->buildException(
278            null,
279            $this->maxFailExpected,
280            $char === '' ? null : $char,
281            $this->computeLocation( $this->maxFailPos, $this->maxFailPos + 1 )
282        );
283    }
284
285    protected function initialize() {
286    }
287
288    protected function initInternal( $input, $options ) {
289        $this->currPos = 0;
290        $this->savedPos = 0;
291        $this->input = $input;
292        $this->inputLength = strlen( $input );
293        $this->options = $options;
294        $this->cache = [];
295        $this->posDetailsCache = [ [ 'line' => 1, 'column' => 1, 'seenCR' => false ] ];
296        $this->maxFailPos = 0;
297        $this->maxFailExpected = [];
298        $this->tracer = $options['tracer'] ?? new DefaultTracer;
299
300        $this->initialize();
301    }
302
303    abstract public function parse( $input, $options = [] );
304}