Skip to content

Commit 8d1b18f

Browse files
committed
#282 Added public properties throwOnWarning and displayErrorLine (switch if the error message should show the byte/line where the error happened) to the StrictPoLoader
1 parent 8700676 commit 8d1b18f

2 files changed

Lines changed: 63 additions & 44 deletions

File tree

src/Loader/StrictPoLoader.php

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
*/
1313
final class StrictPoLoader extends Loader
1414
{
15+
/** @var bool */
16+
public $throwOnWarning = false;
17+
/** @var bool */
18+
public $displayErrorLine = false;
19+
1520
/** @var Translations */
1621
private $translations;
1722
/** @var Translation */
@@ -26,58 +31,49 @@ final class StrictPoLoader extends Loader
2631
private $pluralCount;
2732
/** @var bool */
2833
private $inPreviousPart;
29-
/** @var bool */
30-
private $throwOnWarning;
3134
/** @var string[] */
3235
private $warnings = [];
3336
/** @var bool */
3437
private $isDisabled;
38+
/** @var bool */
39+
private $displayLineColumn;
3540

3641
/**
3742
* Generates a Translations object from a .po based string
3843
*/
3944
public function loadString(string $data, Translations $translations = null): Translations
4045
{
41-
return $this->loadStringExtended(...func_get_args());
42-
}
43-
44-
/**
45-
* Generates a Translations object from a .po based string with extra options
46-
*/
47-
public function loadStringExtended(
48-
string $data,
49-
Translations $translations = null,
50-
bool $throwOnWarning = false
51-
): Translations {
5246
$this->data = $data;
5347
$this->position = 0;
5448
$this->translations = parent::loadString($this->data, $translations);
5549
$this->header = $this->translations->find(null, '');
5650
$this->pluralCount = $this->translations->getHeaders()->getPluralForm()[0] ?? null;
57-
$this->throwOnWarning = $throwOnWarning;
5851
$this->warnings = [];
5952
for ($length = strlen($this->data); $this->newEntry(); $this->saveEntry()) {
6053
for ($hasComment = false; $this->readComment(); $hasComment = true);
6154
$this->readWhitespace();
6255
// End of data
6356
if ($this->position >= $length) {
6457
if ($hasComment) {
65-
$this->addWarning("Comment ignored at the end of the string at byte {$this->position}");
58+
$this->addWarning("Comment ignored at the end of the string{$this->getErrorPosition()}");
6659
}
6760
break;
6861
}
6962
$this->readContext();
7063
$this->readOriginal();
64+
if ($this->translations->has($this->translation)) {
65+
throw new Exception("Duplicated entry{$this->getErrorPosition()}");
66+
}
7167
if (!$this->readPlural()) {
7268
$this->readTranslation();
7369
continue;
7470
}
7571
for ($count = 0; $this->readPluralTranslation(!$count); ++$count);
7672
$count !== ($this->pluralCount ?? $count) && $this->addWarning("The translation has {$count} plural "
77-
. "forms, while the header expects {$this->pluralCount} at byte {$this->position}");
73+
. "forms, while the header expects {$this->pluralCount}{$this->getErrorPosition()}");
7874
}
7975
if (!$this->header) {
80-
$this->addWarning("The loaded string has no header translation at byte {$this->position}");
76+
$this->addWarning("The loaded string has no header translation{$this->getErrorPosition()}");
8177
}
8278

8379
return $this->translations;
@@ -112,9 +108,6 @@ private function saveEntry(): void
112108

113109
return;
114110
}
115-
if ($this->translations->has($this->translation)) {
116-
throw new Exception("Duplicated entry at byte {$this->position}");
117-
}
118111
$this->translations->add($this->translation);
119112
}
120113

@@ -154,7 +147,7 @@ private function readChar(string $char): bool
154147
private function readCharset(string $charset, int $min, int $max, string $name): string
155148
{
156149
if (($length = strspn($this->data, $charset, $this->position, $max)) < $min) {
157-
throw new Exception("Expected at least {$min} occurrence of {$name} characters at byte {$this->position}");
150+
throw new Exception("Expected at least {$min} occurrence of {$name} characters{$this->getErrorPosition()}");
158151
}
159152

160153
return substr($this->data, ($this->position += $length) - $length, $length);
@@ -184,7 +177,7 @@ private function readQuotedString(?string $context = null): string
184177
$this->position = $checkpoint;
185178
break;
186179
}
187-
throw new Exception("Expected an opening quote at byte {$this->position}");
180+
throw new Exception("Expected an opening quote{$this->getErrorPosition()}");
188181
}
189182
$isNewPart = false;
190183
// Collects chars until an edge case is found
@@ -204,14 +197,14 @@ private function readQuotedString(?string $context = null): string
204197
// Unexpected newline
205198
case "\r":
206199
case "\n":
207-
throw new Exception("Newline character must be escaped at byte {$this->position}");
200+
throw new Exception("Newline character must be escaped{$this->getErrorPosition()}");
208201
// Unexpected end of file
209202
case null:
210-
throw new Exception("Expected a closing quote at byte {$this->position}");
203+
throw new Exception("Expected a closing quote{$this->getErrorPosition()}");
211204
}
212205
}
213206
if ($context && strlen($data) && strpbrk($data[0] . $data[strlen($data) - 1], "\r\n") && !$this->isHeader()) {
214-
$this->addWarning("$context cannot start nor end with a newline at byte {$this->position}");
207+
$this->addWarning("$context cannot start nor end with a newline{$this->getErrorPosition()}");
215208
}
216209

217210
return $data;
@@ -230,7 +223,7 @@ private function readEscape(): string
230223
case strpbrk($char, $octalDigits = '01234567'):
231224
// GNU gettext fails with an octal above the signed char range
232225
if (($decimal = octdec($char . $this->readCharset($octalDigits, 0, 2, 'octal'))) > 127) {
233-
throw new Exception("Octal value out of range [0, 0177] at byte {$this->position}");
226+
throw new Exception("Octal value out of range [0, 0177]{$this->getErrorPosition()}");
234227
}
235228

236229
return chr($decimal);
@@ -247,7 +240,7 @@ private function readEscape(): string
247240

248241
return mb_convert_encoding(hex2bin($value), 'UTF-8', 'UTF-' . ($digits * 4));
249242
}
250-
throw new Exception("Invalid escaped character at byte {$this->position}");
243+
throw new Exception("Invalid escaped character{$this->getErrorPosition()}");
251244
}
252245

253246
/**
@@ -270,15 +263,15 @@ private function readComment(): bool
270263
break;
271264
case '~':
272265
if ($this->translation->getPreviousOriginal() !== null) {
273-
throw new Exception("Inconsistent use of #~ at byte {$this->position}");
266+
throw new Exception("Inconsistent use of #~{$this->getErrorPosition()}");
274267
}
275268
$this->translation->disable();
276269
$this->isDisabled = true;
277270
break;
278271
case '|':
279272
if ($this->translation->getPreviousOriginal() !== null) {
280273
throw new Exception('Cannot redeclare the previous comment #|, '
281-
. "ensure the definitions are in the right order at byte {$this->position}");
274+
. "ensure the definitions are in the right order{$this->getErrorPosition()}");
282275
}
283276
$this->inPreviousPart = true;
284277
$this->translation->setPreviousContext($this->readIdentifier('msgctxt'));
@@ -320,7 +313,7 @@ private function readIdentifier(string $identifier, bool $throwIfNotFound = fals
320313
return $this->readQuotedString($identifier);
321314
}
322315
if ($throwIfNotFound) {
323-
throw new Exception("Expected $identifier at byte {$this->position}");
316+
throw new Exception("Expected $identifier{$this->getErrorPosition()}");
324317
}
325318
$this->position = $checkpoint;
326319

@@ -359,7 +352,7 @@ private function readTranslation(): void
359352
{
360353
$this->readWhitespace();
361354
if (!$this->readString('msgstr')) {
362-
throw new Exception("Expected msgstr at byte {$this->position}");
355+
throw new Exception("Expected msgstr{$this->getErrorPosition()}");
363356
}
364357
$this->translation->translate($this->readQuotedString('msgstr'));
365358
}
@@ -372,27 +365,27 @@ private function readPluralTranslation(bool $throwIfNotFound = false): bool
372365
$this->readWhitespace();
373366
if (!$this->readString('msgstr')) {
374367
if ($throwIfNotFound) {
375-
throw new Exception("Expected indexed msgstr at byte {$this->position}");
368+
throw new Exception("Expected indexed msgstr{$this->getErrorPosition()}");
376369
}
377370

378371
return false;
379372
}
380373
$this->readWhitespace();
381374
if (!$this->readChar('[')) {
382-
throw new Exception("Expected character \"[\" at byte {$this->position}");
375+
throw new Exception("Expected character \"[\"{$this->getErrorPosition()}");
383376
}
384377
$this->readWhitespace();
385378
$index = (int) $this->readCharset('0123456789', 1, PHP_INT_MAX, 'numeric');
386379
$this->readWhitespace();
387380
if (!$this->readChar(']')) {
388-
throw new Exception("Expected character \"]\" at byte {$this->position}");
381+
throw new Exception("Expected character \"]\"{$this->getErrorPosition()}");
389382
}
390383
$translations = $this->translation->getPluralTranslations();
391384
if (($translation = $this->translation->getTranslation()) !== null) {
392385
array_unshift($translations, $translation);
393386
}
394387
if (count($translations) !== (int) $index) {
395-
throw new Exception("The msgstr has an invalid index at byte {$this->position}");
388+
throw new Exception("The msgstr has an invalid index{$this->getErrorPosition()}");
396389
}
397390
$data = $this->readQuotedString('msgstr');
398391
$translations[] = $data;
@@ -423,7 +416,7 @@ private function processHeader(): void
423416
$this->pluralCount = $headers->getPluralForm()[0] ?? null;
424417
foreach (['Language', 'Plural-Forms', 'Content-Type'] as $header) {
425418
if (($headers->get($header) ?? '') === '') {
426-
$this->addWarning("$header header not declared or empty at byte {$this->position}");
419+
$this->addWarning("$header header not declared or empty{$this->getErrorPosition()}");
427420
}
428421
}
429422
}
@@ -441,14 +434,14 @@ private function readHeaders(string $data): array
441434
if (preg_match('/^[\w-]+:/', $line)) {
442435
[$name, $value] = explode(':', $line, 2);
443436
if (isset($headers[$name])) {
444-
$this->addWarning("Header already defined at byte {$this->position}");
437+
$this->addWarning("Header already defined{$this->getErrorPosition()}");
445438
}
446439
$headers[$name] = trim($value);
447440
continue;
448441
}
449442
// Data without a definition
450443
if ($name === null) {
451-
$this->addWarning("Malformed header name at byte {$this->position}");
444+
$this->addWarning("Malformed header name{$this->getErrorPosition()}");
452445
continue;
453446
}
454447
$headers[$name] .= $line;
@@ -475,4 +468,18 @@ private function isHeader(): bool
475468
{
476469
return $this->translation->getOriginal() === '' && $this->translation->getContext() === null;
477470
}
471+
472+
/**
473+
* Retrieves the position where an error was detected
474+
*/
475+
private function getErrorPosition(): string
476+
{
477+
if ($this->displayErrorLine) {
478+
$pieces = preg_split("/\\r\\n|\\n\\r|\\n|\\r/", substr($this->data, 0, $this->position));
479+
$line = count($pieces);
480+
$column = strlen(end($pieces));
481+
return " at line {$line} column {$column}";
482+
}
483+
return " at byte {$this->position}";
484+
}
478485
}

tests/StrictPoLoaderTest.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,20 +274,32 @@ public function badFormattedPoProvider(): array
274274
# Dangling comment',
275275
true,
276276
],
277+
'Dangling comment in the end of the data using error report with line/column' => [
278+
'/Comment ignored at the end.*line 4 column 34/',
279+
'msgid "original"
280+
msgstr "translation"
281+
282+
# Dangling comment',
283+
true,
284+
true,
285+
],
277286
];
278287
}
279288

280289
/**
281290
* @dataProvider badFormattedPoProvider
282291
*/
283-
public function testBadFormattedPo(string $exceptionPattern, string $po, bool $throwOnWarning = false): void
292+
public function testBadFormattedPo(
293+
string $exceptionPattern,
294+
string $po,
295+
bool $throwOnWarning = false,
296+
bool $displayErrorLine = false
297+
): void
284298
{
285299
$this->expectExceptionMessageMatches($exceptionPattern);
286300
$loader = $this->createPoLoader();
287-
if ($throwOnWarning) {
288-
$loader->loadStringExtended($po, null, $throwOnWarning);
289-
} else {
290-
$loader->loadString($po);
291-
}
301+
$loader->throwOnWarning = $throwOnWarning;
302+
$loader->displayErrorLine = $displayErrorLine;
303+
$loader->loadString($po);
292304
}
293305
}

0 commit comments

Comments
 (0)