Skip to content

Commit fc1ac4e

Browse files
authored
feat: Do not swallow exceptions when scoping (#869)
Closes #847. As reported in #847, the current strategy is not ideal: if a failure occurs, the file is preserved. You can only see that there was a failure by paying attention to the output or having a more verbose output, unless you pass the `--stop-on-failure` option. I think historically it has been that way because in the early days of PHP-Scoper, a failure was non infrequent and it was very frustrating to not be able to examine the result despite the failure. The tool is now more robust and the only exception that I can see regularly coming still is invalid PHP code (e.g. when it is a template). As such, this PR proposes to: - Leave the file unchanged if a PHP-Parser parsing error occurred (it is still marked as failed and appears in the logs as before). - Fail on any other failure (i.e. proceed as if `--stop-on-failure` was always provided). - Introduce a new counterpart option `--continue-on-failure`. The option `--stop-on-failure` has now been deprecated and will be removed a future version as it is now useless.
1 parent 2133987 commit fc1ac4e

File tree

10 files changed

+89
-47
lines changed

10 files changed

+89
-47
lines changed
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
{"just here": "for the detection"}
1+
{
2+
"packages": []
3+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
{"just here": "for the detection"}
1+
{
2+
"packages": []
3+
}

phpstan.neon.dist

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ parameters:
2323
path: 'src/PhpParser/NodeVisitor/Resolver/OriginalNameResolver.php'
2424
- message: '#NamespaceManipulator::getOriginalName\(\) should return#'
2525
path: 'src/PhpParser/NodeVisitor/NamespaceStmt/NamespaceManipulator.php'
26-
- message: '#Dead catch#'
27-
path: 'tests/Scoper/PhpScoperTest.php'
2826
- message: '#Dead catch#'
2927
path: 'tests/Scoper/PhpScoperSpecTest.php'
3028
- message: '#DummyScoperFactory extends @final#'

src/Console/Command/AddPrefixCommand.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ final class AddPrefixCommand implements Command, CommandAware
5151
private const OUTPUT_DIR_OPT = 'output-dir';
5252
private const FORCE_OPT = 'force';
5353
private const STOP_ON_FAILURE_OPT = 'stop-on-failure';
54+
private const CONTINUE_ON_FAILURE_OPT = 'continue-on-failure';
5455
private const CONFIG_FILE_OPT = 'config';
5556
private const NO_CONFIG_OPT = 'no-config';
5657

@@ -107,6 +108,12 @@ public function getConfiguration(): CommandConfiguration
107108
InputOption::VALUE_NONE,
108109
'Stops on failure.',
109110
),
111+
new InputOption(
112+
self::CONTINUE_ON_FAILURE_OPT,
113+
null,
114+
InputOption::VALUE_NONE,
115+
'Continue on non PHP-Parser parsing failures.',
116+
),
110117
new InputOption(
111118
self::CONFIG_FILE_OPT,
112119
'c',
@@ -150,12 +157,40 @@ public function execute(IO $io): int
150157
$config,
151158
$paths,
152159
$outputDir,
153-
$io->getOption(self::STOP_ON_FAILURE_OPT)->asBoolean(),
160+
self::getStopOnFailure($io),
154161
);
155162

156163
return ExitCode::SUCCESS;
157164
}
158165

166+
private static function getStopOnFailure(IO $io): bool
167+
{
168+
$stopOnFailure = $io->getOption(self::STOP_ON_FAILURE_OPT)->asBoolean();
169+
$continueOnFailure = $io->getOption(self::CONTINUE_ON_FAILURE_OPT)->asBoolean();
170+
171+
if ($stopOnFailure) {
172+
$io->info(
173+
sprintf(
174+
'Using the option "%s" is now deprecated. Any non PHP-Parser parsing error will now halt the process. To continue upon non-PHP-Parser parsing errors, use the option "%s".',
175+
self::STOP_ON_FAILURE_OPT,
176+
self::CONTINUE_ON_FAILURE_OPT,
177+
),
178+
);
179+
}
180+
181+
if (true === $stopOnFailure && true === $continueOnFailure) {
182+
$io->info(
183+
sprintf(
184+
'Only one of the two options "%s" and "%s" can be used at the same time.',
185+
self::STOP_ON_FAILURE_OPT,
186+
self::CONTINUE_ON_FAILURE_OPT,
187+
),
188+
);
189+
}
190+
191+
return $stopOnFailure;
192+
}
193+
159194
/**
160195
* @return non-empty-string
161196
*/

src/Console/ConsoleScoper.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
use function preg_match as native_preg_match;
3434
use function Safe\file_get_contents;
3535
use function Safe\fileperms;
36-
use function sprintf;
3736
use function str_replace;
3837
use function strlen;
3938
use function usort;
@@ -225,18 +224,23 @@ private function scopeFile(
225224
bool $stopOnFailure,
226225
ScoperLogger $logger
227226
): void {
227+
$successfullyScoped = false;
228+
228229
try {
229230
$scoppedContent = $scoper->scope(
230231
$inputFilePath,
231232
$inputContents,
232233
);
234+
235+
$successfullyScoped = true;
236+
} catch (ParsingException $parsingException) {
237+
$logger->outputWarnOfFailure($inputFilePath, $parsingException);
238+
239+
// Fallback on unchanged content
240+
$scoppedContent = file_get_contents($inputFilePath);
233241
} catch (Throwable $throwable) {
234-
$exception = new ParsingException(
235-
sprintf(
236-
'Could not parse the file "%s".',
237-
$inputFilePath,
238-
),
239-
0,
242+
$exception = ParsingException::forFile(
243+
$inputFilePath,
240244
$throwable,
241245
);
242246

@@ -256,7 +260,7 @@ private function scopeFile(
256260
$outputFilePath,
257261
);
258262

259-
if (!isset($exception)) {
263+
if ($successfullyScoped) {
260264
$logger->outputSuccess($inputFilePath);
261265
}
262266
}

src/Scoper/PhpScoper.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
use Humbug\PhpScoper\PhpParser\Printer\Printer;
1818
use Humbug\PhpScoper\PhpParser\TraverserFactory;
19+
use Humbug\PhpScoper\Throwable\Exception\ParsingException;
1920
use PhpParser\Error as PhpParserError;
2021
use PhpParser\Lexer;
2122
use PhpParser\Parser;
@@ -42,16 +43,18 @@ public function __construct(
4243

4344
/**
4445
* Scopes PHP files.
45-
*
46-
* @throws PhpParserError
4746
*/
4847
public function scope(string $filePath, string $contents): string
4948
{
5049
if (!self::isPhpFile($filePath, $contents)) {
5150
return $this->decoratedScoper->scope(...func_get_args());
5251
}
5352

54-
return $this->scopePhp($contents);
53+
try {
54+
return $this->scopePhp($contents);
55+
} catch (PhpParserError $parsingException) {
56+
throw ParsingException::forFile($filePath, $parsingException);
57+
}
5558
}
5659

5760
public function scopePhp(string $php): string

src/Throwable/Exception/ParsingException.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414

1515
namespace Humbug\PhpScoper\Throwable\Exception;
1616

17+
use Throwable;
18+
1719
final class ParsingException extends RuntimeException
1820
{
21+
public static function forFile(string $filePath, Throwable $previous): self
22+
{
23+
return new self(
24+
sprintf(
25+
'Could not parse the file "%s".',
26+
$filePath,
27+
),
28+
previous: $previous,
29+
);
30+
}
1931
}

tests/Console/Command/AddPrefixCommandIntegrationTest.php

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public function test_scope_in_verbose_mode(): void
195195
196196
PhpScoper version TestVersion 28/01/2020
197197
198-
* [NO] /path/to/composer/installed.json
198+
* [OK] /path/to/composer/installed.json
199199
* [OK] /path/to/executable-file.php
200200
* [OK] /path/to/file.php
201201
* [NO] /path/to/invalid-file.php
@@ -245,19 +245,7 @@ public function test_scope_in_very_verbose_mode(): void
245245
246246
PhpScoper version TestVersion 28/01/2020
247247
248-
* [NO] /path/to/composer/installed.json
249-
Could not parse the file "/path/to/composer/installed.json".: InvalidArgumentException
250-
Stack trace:
251-
#0
252-
#1
253-
#2
254-
#3
255-
#4
256-
#5
257-
#6
258-
#7
259-
#8
260-
#9
248+
* [OK] /path/to/composer/installed.json
261249
* [OK] /path/to/executable-file.php
262250
* [OK] /path/to/file.php
263251
* [NO] /path/to/invalid-file.php

tests/Console/Command/AddPrefixCommandTest.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131
use Humbug\PhpScoper\Symbol\EnrichedReflectorFactory;
3232
use Humbug\PhpScoper\Symbol\Reflector;
3333
use InvalidArgumentException;
34+
use PhpParser\Error as PhpParserError;
3435
use Prophecy\Argument;
3536
use Prophecy\PhpUnit\ProphecyTrait;
3637
use Prophecy\Prophecy\ObjectProphecy;
37-
use RuntimeException as RootRuntimeException;
3838
use Symfony\Component\Console\Exception\RuntimeException;
3939
use Symfony\Component\Console\Tester\ApplicationTester;
4040
use Symfony\Component\Filesystem\Filesystem;
@@ -104,7 +104,7 @@ public function test_scope_the_given_paths(): void
104104
$this->fileSystemProphecy->remove(Argument::cetera())->shouldNotBeCalled();
105105

106106
$expectedFiles = [
107-
'composer/installed.json' => 'f1',
107+
'composer/installed.json' => '{"packages": []}}',
108108
'executable-file.php' => 'f5',
109109
'file.php' => 'f2',
110110
'invalid-file.php' => 'f3',
@@ -145,7 +145,7 @@ public function test_scope_the_given_paths(): void
145145
->shouldHaveBeenCalledTimes(count($expectedFiles));
146146
}
147147

148-
public function test_let_the_file_unchanged_when_cannot_scope_a_file(): void
148+
public function test_let_the_file_unchanged_when_cannot_scope_a_file_but_is_marked_as_continue_on_failure(): void
149149
{
150150
$input = [
151151
'add-prefix',
@@ -156,6 +156,7 @@ public function test_let_the_file_unchanged_when_cannot_scope_a_file(): void
156156
'--output-dir' => $this->tmp,
157157
'--no-interaction',
158158
'--no-config' => null,
159+
'--continue-on-failure' => null,
159160
];
160161

161162
$this->fileSystemProphecy->isAbsolutePath($root)->willReturn(true);
@@ -197,7 +198,7 @@ public function test_let_the_file_unchanged_when_cannot_scope_a_file(): void
197198
} else {
198199
$this->scoperProphecy
199200
->scope($inputPath, $inputContents)
200-
->willThrow(new RootRuntimeException('Scoping of the file failed'));
201+
->willThrow(new RuntimeException('Scoping of the file failed'));
201202

202203
$this->fileSystemProphecy->dumpFile($outputPath, $inputContents)->shouldBeCalled();
203204
}
@@ -582,7 +583,7 @@ public function test_can_scope_projects_with_invalid_files(): void
582583

583584
$this->scoperProphecy
584585
->scope($inputPath, $fileContents)
585-
->willThrow(new RuntimeException('Could not scope file'));
586+
->willThrow(new PhpParserError('Could not scope file'));
586587

587588
$this->fileSystemProphecy->dumpFile($outputPath, $fileContents)->shouldBeCalled();
588589
}

tests/Scoper/PhpScoperTest.php

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Humbug\PhpScoper\Symbol\EnrichedReflector;
2424
use Humbug\PhpScoper\Symbol\Reflector;
2525
use Humbug\PhpScoper\Symbol\SymbolsRegistry;
26+
use Humbug\PhpScoper\Throwable\Exception\ParsingException;
2627
use LogicException;
2728
use PhpParser\Error as PhpParserError;
2829
use PhpParser\Lexer;
@@ -267,18 +268,14 @@ public function test_cannot_scope_an_invalid_php_file(): void
267268

268269
PHP;
269270

270-
try {
271-
$this->scoper->scope($filePath, $contents);
272-
273-
self::fail('Expected exception to have been thrown.');
274-
} catch (PhpParserError $error) {
275-
self::assertEquals(
276-
'Syntax error, unexpected \';\' on line 3',
277-
$error->getMessage(),
278-
);
279-
self::assertSame(0, $error->getCode());
280-
self::assertNull($error->getPrevious());
281-
}
271+
$this->expectExceptionObject(
272+
new ParsingException(
273+
'Could not parse the file "invalid-file.php".',
274+
previous: new PhpParserError('Syntax error, unexpected \';\' on line 3'),
275+
),
276+
);
277+
278+
$this->scoper->scope($filePath, $contents);
282279
}
283280

284281
public function test_creates_a_new_traverser_for_each_file(): void

0 commit comments

Comments
 (0)