Summary
When running duster lint --dirty on a project that has a custom PHP_CodeSniffer config (.phpcs.xml, .phpcs.xml.dist, phpcs.xml, or phpcs.xml.dist) at the project root, the dirty file list is correctly resolved by App\Project::paths() and stored in DusterConfig, but it is never passed to PHPCS. PHPCS instead falls back to the <file> directives in the ruleset and scans the entire project.
The other tools (TLint, PHP CS Fixer, Pint) handle --dirty correctly — only PHPCS is affected.
Versions
- Duster:
3.4.2
- PHP:
8.4
- Laravel:
13.5.0
Reproduction
-
In a project with a .phpcs.xml.dist like:
<?xml version="1.0"?>
<ruleset>
<rule ref="Tighten">
<exclude name="Generic.Files.LineLength"/>
</rule>
<file>src/</file>
<!-- ... -->
</ruleset>
-
Modify a single PHP file, e.g. src/Some/File.php.
-
Run:
vendor/bin/duster lint --dirty
Expected
PHPCS should scan only the dirty file (src/Some/File.php).
Actual
PHPCS scans every PHP file matched by <file>src/</file> in the ruleset (in our case 1,341 files):
=> Linting using TLint
>> success: No issues found.
=> Linting using PHP_CodeSniffer
............................................................ 60 / 1341 (4%)
............................................................ 120 / 1341 (9%)
...
..................... 1341 / 1341 (100%)
TLint, PHP CS Fixer and Pint correctly process only the dirty file.
Root cause
In app/Support/PhpCodeSniffer.php (3.4.2):
public function lint(): int
{
$this->heading('Linting using PHP_CodeSniffer');
if (! $this->hasCustomConfig()) {
if (empty($paths = $this->getPaths())) {
return 0;
}
}
return $this->process('runPHPCS', $paths ?? []);
}
When hasCustomConfig() returns true, the inner block where $paths would be assigned from getPaths() is skipped, so $paths is undefined and $paths ?? [] evaluates to []. PHPCS is then invoked with no path arguments and falls back to the ruleset's <file> directives.
I confirmed this by extracting the phar and adding debug output:
DEBUG: hasCustomConfig() => true
DEBUG: dusterConfig paths => array (
0 => '/path/to/project/src/Some/File.php',
)
DEBUG: Final params to PHPCS => array ()
The dirty paths are computed correctly — they just never reach the PHPCS runner when a custom config exists.
The same issue affects fix() (same hasCustomConfig() short-circuit) and presumably --diff as well.
Likely origin
This appears to be a regression introduced alongside the fix for #198 / #200 / #201, where the "skip path forwarding when a custom config exists" branch was added so that ruleset-driven setups would work with Blade-only paths. That change unintentionally swallows the path list selected by --dirty and --diff.
Suggested fix
Pass the resolved paths through to PHPCS even when a custom config is present, e.g.:
public function lint(): int
{
$this->heading('Linting using PHP_CodeSniffer');
$paths = $this->getPaths();
if (! $this->hasCustomConfig() && empty($paths)) {
return 0;
}
return $this->process('runPHPCS', $paths);
}
This preserves the "no-op when there are no paths and no custom config" behaviour while still letting --dirty / --diff narrow the run when a custom config is present. (PHPCS's documented behaviour is that paths on the command line override <file> directives in the ruleset, so this should DTRT for everyone.)
Workaround
Drop PHPCS from the --dirty invocation:
vendor/bin/duster lint --dirty --using=tlint,pint,php-cs-fixer
Summary
When running
duster lint --dirtyon a project that has a custom PHP_CodeSniffer config (.phpcs.xml,.phpcs.xml.dist,phpcs.xml, orphpcs.xml.dist) at the project root, the dirty file list is correctly resolved byApp\Project::paths()and stored inDusterConfig, but it is never passed to PHPCS. PHPCS instead falls back to the<file>directives in the ruleset and scans the entire project.The other tools (TLint, PHP CS Fixer, Pint) handle
--dirtycorrectly — only PHPCS is affected.Versions
3.4.28.413.5.0Reproduction
In a project with a
.phpcs.xml.distlike:Modify a single PHP file, e.g.
src/Some/File.php.Run:
Expected
PHPCS should scan only the dirty file (
src/Some/File.php).Actual
PHPCS scans every PHP file matched by
<file>src/</file>in the ruleset (in our case 1,341 files):TLint, PHP CS Fixer and Pint correctly process only the dirty file.
Root cause
In
app/Support/PhpCodeSniffer.php(3.4.2):When
hasCustomConfig()returnstrue, the inner block where$pathswould be assigned fromgetPaths()is skipped, so$pathsis undefined and$paths ?? []evaluates to[]. PHPCS is then invoked with no path arguments and falls back to the ruleset's<file>directives.I confirmed this by extracting the phar and adding debug output:
The dirty paths are computed correctly — they just never reach the PHPCS runner when a custom config exists.
The same issue affects
fix()(samehasCustomConfig()short-circuit) and presumably--diffas well.Likely origin
This appears to be a regression introduced alongside the fix for #198 / #200 / #201, where the "skip path forwarding when a custom config exists" branch was added so that ruleset-driven setups would work with Blade-only paths. That change unintentionally swallows the path list selected by
--dirtyand--diff.Suggested fix
Pass the resolved paths through to PHPCS even when a custom config is present, e.g.:
This preserves the "no-op when there are no paths and no custom config" behaviour while still letting
--dirty/--diffnarrow the run when a custom config is present. (PHPCS's documented behaviour is that paths on the command line override<file>directives in the ruleset, so this should DTRT for everyone.)Workaround
Drop PHPCS from the
--dirtyinvocation: