Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 160 |
|
0.00% |
0 / 20 |
CRAP | |
0.00% |
0 / 1 |
PEGParserBase | |
0.00% |
0 / 160 |
|
0.00% |
0 / 20 |
2652 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
traceCall | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
12 | |||
text | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
location | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
expected | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
error | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
charAt | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
charsAt | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
consumeChar | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
newRef | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
computePosDetails | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
computeLocation | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
fail | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
expandExpectations | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
buildMessage | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
buildException | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
buildParseException | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
initialize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initInternal | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
parse | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
0 |
1 | <?php |
2 | |
3 | namespace Wikimedia\WikiPEG; |
4 | |
5 | abstract 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 | } |