Skip to content

--dirty is silently ignored by PHPCS when project has a custom phpcs config (regression in 3.4.x) #206

@mhdarffn

Description

@mhdarffn

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

  1. 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>
  2. Modify a single PHP file, e.g. src/Some/File.php.

  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions