Skip to content

Commit

Permalink
Files/FileName: add ability to check PSR-4-style file names
Browse files Browse the repository at this point in the history
As the Yoast plugin test directories will start to follow PSR-4. the `FileName` sniff will need to be able to enforce this.

This commit adds this ability to the sniff.

Notes:
* It adds a new `public` `psr4_paths` ruleset property via the `PSR4PathsTrait` utility.
* If the file being examined is in a path indicated as a PSR-4 path, PSR-4 based file names will be enforced.
* For PSR-4 compliant file names, the file name has to match the OO name. This also means that "oo prefixes" should not be stripped and that the "excluded files" property will be ignored.
* For non-OO files in a PSR-4 path, the _normal_ file name rules apply, i.e. lowercase and hyphenathed and if the file contains functions, the file name should have a `-functions` suffix.

Includes ample tests for this new functionality.
Includes updated XML documentation.

Includes updating the YoastCS native PHPCS ruleset to enable the sniff as the YoastCS repo follows PSR-4 completely (as per the PHPCS file name rules).

:point_right: The changes to the pre-existing code in the sniff will be easiest to review while ignoring whitespace changes.
  • Loading branch information
jrfnl committed Nov 20, 2023
1 parent 0d870dc commit 6c60a4e
Show file tree
Hide file tree
Showing 30 changed files with 326 additions and 21 deletions.
11 changes: 8 additions & 3 deletions .phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,6 @@

<rule ref="Yoast">

<!-- Conflicts with PHPCS autoloading of sniffs. -->
<exclude name="Yoast.Files.FileName"/>

<!-- This sniff is irrelevant for a PHPCS standard which follows the PHPCS directory structure. -->
<exclude name="Yoast.Files.TestDoubles"/>

Expand Down Expand Up @@ -77,6 +74,14 @@
#############################################################################
-->

<rule ref="Yoast.Files.FileName">
<properties>
<property name="psr4_paths" type="array">
<element key="YoastCS\Yoast\\" value="Yoast"/>
</property>
</properties>
</rule>

<rule ref="Yoast.NamingConventions.NamespaceName">
<properties>
<property name="prefixes" type="array">
Expand Down
27 changes: 27 additions & 0 deletions Yoast/Docs/Files/FileNameStandard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,33 @@ class WPSEO_Utils {}
</code_comparison>
<standard>
<![CDATA[
Optionally, it can be enforced that files containing classes, interfaces, traits and enums, should follow the PSR-4 based file name rules.
According to PSR-4, the file name should reflect the name of the OO structure within the file.
In contrast to the "normal" file name rules, plugin specific prefixes should only be stripped from the file name if they are also stripped off the OO structure name.
Additionally, interface/trait/enum files are not expected to have a suffix, other than if the interface/trait/enum structure has that suffix in its name already.
The sniff offers a public property, which can be configured from a custom ruleset, to indicate directories which follow PSR-4.
]]>
</standard>
<code_comparison>
<code title="Valid: Using a PSR-4 compliant file name for a file in a path marked as using PSR-4.">
<![CDATA[
<!-- File name: <em>Yoast_Output_Thing</em>.php -->
<?php
class <em>Yoast_Output_Thing</em> {}
]]>
</code>
<code title="Invalid: Using a Yoast/WP compliant file name for a file in a path marked as using PSR-4.">
<![CDATA[
<!-- File name: <em>outline-something</em>.php -->
<?php
trait <em>Yoast_Outline_Something</em> {}
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
Files which don't contain an object structure, but do contain function declarations should have a "-functions" suffix.
]]>
</standard>
Expand Down
44 changes: 28 additions & 16 deletions Yoast/Sniffs/Files/FileNameSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPCSUtils\Utils\TextStrings;
use YoastCS\Yoast\Utils\PathHelper;
use YoastCS\Yoast\Utils\PathValidationHelper;
use YoastCS\Yoast\Utils\PSR4PathsTrait;

/**
* Ensures files comply with the Yoast file name rules.
Expand All @@ -24,10 +25,15 @@
* have a "-functions" suffix.
*
* @since 0.5
* @since 3.0.0 The sniff will now also be enforced for files only using the PHP short open tag.
* @since 3.0.0 - The sniff will now also be enforced for files only using the PHP short open tag.
* - The sniff now also has the ability to check for PSR-4 compliant file names.
*
* @uses \YoastCS\Yoast\Utils\PSR4PathsTrait::$psr4_paths
*/
final class FileNameSniff implements Sniff {

use PSR4PathsTrait;

/**
* Object tokens to search for in a file.
*
Expand Down Expand Up @@ -192,13 +198,19 @@ public function process( File $phpcsFile, $stackPtr ) {
$this->add_missing_basepath_warning( $phpcsFile );
}

if ( $this->is_file_excluded( $phpcsFile, $file ) === false ) {
$oo_structure = $phpcsFile->findNext( self::NAMED_OO_TOKENS, $stackPtr );
if ( $oo_structure !== false ) {
$oo_structure = $phpcsFile->findNext( self::NAMED_OO_TOKENS, $stackPtr );
if ( $oo_structure !== false ) {

$oo_name = ObjectDeclarations::getName( $phpcsFile, $oo_structure );

$oo_name = ObjectDeclarations::getName( $phpcsFile, $oo_structure );
if ( ! empty( $oo_name ) ) {

if ( ! empty( $oo_name ) ) {
if ( $this->is_in_psr4_path( $phpcsFile, $file ) ) {
$error = 'Directory marked as a PSR-4 path. File names should 100%% match the name of the OO structure contained in the file for PSR-4 compliance.';
$error_code = 'InvalidPSR4FileName';
$expected = $oo_name;
}
elseif ( $this->is_file_excluded( $phpcsFile, $file ) === false ) {
$this->validate_oo_prefixes();
if ( ! empty( $this->clean_oo_prefixes ) ) {
foreach ( $this->clean_oo_prefixes as $prefix ) {
Expand Down Expand Up @@ -239,15 +251,15 @@ public function process( File $phpcsFile, $stackPtr ) {
}
}
}
else {
$has_function = $phpcsFile->findNext( \T_FUNCTION, $stackPtr );
if ( $has_function !== false && $file_name !== 'functions' ) {
$error = 'Files containing function declarations should have "-functions" as a suffix.';
$error_code = 'InvalidFunctionsFileName';

if ( \substr( $expected, -10 ) !== '-functions' ) {
$expected .= '-functions';
}
}
elseif ( $this->is_file_excluded( $phpcsFile, $file ) === false ) {
$has_function = $phpcsFile->findNext( \T_FUNCTION, $stackPtr );
if ( $has_function !== false && $file_name !== 'functions' ) {
$error = 'Files containing function declarations should have "-functions" as a suffix.';
$error_code = 'InvalidFunctionsFileName';

if ( \substr( $expected, -10 ) !== '-functions' ) {
$expected .= '-functions';
}
}
}
Expand Down Expand Up @@ -374,7 +386,7 @@ private function add_missing_basepath_warning( File $phpcsFile ) {
}

$phpcsFile->addWarning(
'For the exclude property to work with relative file path files, the --basepath needs to be set.',
'For the excluded files and the psr4 paths properties to work with relative file paths, the --basepath needs to be set.',
0,
'MissingBasePath'
);
Expand Down
38 changes: 36 additions & 2 deletions Yoast/Tests/Files/FileNameUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,41 @@ final class FileNameUnitTest extends AbstractSniffUnitTest {
'partial-file-disable.inc' => 1,
'Errorcode_Disable.inc' => 1, // The sniff can only be disabled completely, not by error code.

// PSR4 file names.
'Some_Class.inc' => 0,
'Some_Enum.inc' => 0,
'Some_Interface.inc' => 0,
'Some_Trait.inc' => 0,
'Wrong_Class.inc' => 1, // Filename not in line with class name.
'Wrong_Enum.inc' => 1, // Filename not in line with enum name.
'Wrong_Interface.inc' => 1, // Filename not in line with interface name.
'Wrong_Trait.inc' => 1, // Filename not in line with trait name.
'wrong_case.inc' => 1, // Names are case-sensitive.
'Wrong_Case_Too.inc' => 1, // Names are case-sensitive.
'WPSEO_Prefixed.inc' => 0, // Prefixes should not be stripped for PSR4 file names.
'Yoast_Prefixed.inc' => 0, // Prefixes should not be stripped for PSR4 file names.
'Prefix_Stripped.inc' => 1, // Prefixes should not be stripped for PSR4 file names.
'Not_Excluded.inc' => 1, // Exclusions do not apply to files where prefix stripping is not supported.
'no-oo.inc' => 0, // Files not containing OO should follow the normal rules for PSR4 dirs.
'no-oo-functions.inc' => 0, // Files containing only functions should follow the normal rules for PSR4 dirs.
'missing-suffix.inc' => 1, // Files containing only functions should follow the normal rules for PSR4 dirs.
'Multiple_Paths.inc' => 0,
'Dot_Prefixed_Path.inc' => 0,
'illegal-psr4-path.inc' => 0, // PSR4 path ignored, so normal rules apply.
'Illegal_PSR4_Path.inc' => 1, // PSR4 path ignored, so normal rules apply.

/*
* In /.
*/

// Fall-back file in case glob() fails.
'FileNameUnitTest.inc' => 1,

// PSR4 related, PSR4 dir is root dir.
'not-in-psr4-path.inc' => 0,
'not-in-psr4-path-wrong-name.inc' => 1, // Filename not in line with class name, non-PSR4.
'PSR4_Path_Is_Root_Path.inc' => 0,
'PSR4_Path_Root_Wrong_Name.inc' => 1, // Filename not in line with class name, PSR4.
];

/**
Expand All @@ -118,7 +147,7 @@ final class FileNameUnitTest extends AbstractSniffUnitTest {
* @return void
*/
public function setCliValues( $filename, $config ): void {
if ( $filename === 'no-basepath.inc' ) {
if ( $filename === 'no-basepath.inc' || $filename === 'no-basepath-psr4.inc' ) {
return;
}

Expand Down Expand Up @@ -169,7 +198,12 @@ public function getErrorList( string $testFile = '' ): array {
* @return array<int, int> Key is the line number, value is the number of expected warnings.
*/
public function getWarningList( string $testFile = '' ): array {
if ( $testFile === 'no-basepath.inc' ) {
/*
* Note: no warning for the 'no-basepath.inc' file as the warning will only be thrown once.
* Also note that in which file the warning is thrown, relies on the sorting order of the
* test files, which could change.
*/
if ( $testFile === 'no-basepath-psr4.inc' ) {
return [
1 => 1,
];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix\\=>./PSR4

<?php

class Dot_Prefixed_Path {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>PSR4/../PSR4/../

<?php

class Illegal_PSR4_Path {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Multiple_Paths.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] PrefixA\=>Other_Path/,PrefixB=>/PSR4/,PrefixC\=>/Deep/Path/Sub/

<?php

class Multiple_Paths {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
11 changes: 11 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Not_Excluded.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>/PSR4/
phpcs:set Yoast.Files.FileName excluded_files_strict_check[] PSR4/Not_Excluded.inc

<?php

// The excluded files property should only be applied when prefix stripping is supported, which it is not for PSR4 files.
class Yoast_Not_Excluded {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
// phpcs:set Yoast.Files.FileName excluded_files_strict_check[]
11 changes: 11 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Prefix_Stripped.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>/PSR4/
phpcs:set Yoast.Files.FileName oo_prefixes[] wpseo,yoast

<?php

// For PSR4 file names, prefixes should *not* be stripped.
class WPSEO_Prefix_Stripped {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
// phpcs:set Yoast.Files.FileName oo_prefixes[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Some_Class.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>/PSR4/

<?php

class Some_Class {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Some_Enum.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix\=>PSR4

<?php

enum Some_Enum:int implements ArrayAccess {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Some_Interface.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix\\=>/PSR4

<?php

interface Some_Interface {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Some_Trait.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>PSR4/

<?php

TraiT Some_Trait {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
11 changes: 11 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/WPSEO_Prefixed.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix\\=>/PSR4/
phpcs:set Yoast.Files.FileName oo_prefixes[] wpseo,yoast

<?php

// For PSR4 file names, prefixes should *not* be stripped.
class WPSEO_Prefixed {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
// phpcs:set Yoast.Files.FileName oo_prefixes[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Wrong_Case_Too.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>/PSR4/

<?php

class wrong_case_too {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Wrong_Class.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix\\=>/PSR4/

<?php

class Some_Class {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Wrong_Enum.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>PSR4

<?php

enum Some_Enum:int implements ArrayAccess {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Wrong_Interface.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>/PSR4

<?php

interface Some_Interface {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
8 changes: 8 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Wrong_Trait.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>PSR4/

<?php

TraiT Some_Trait {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
11 changes: 11 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/Yoast_Prefixed.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] PrefixA=>/src/,PrefixB=>/PSR4/
phpcs:set Yoast.Files.FileName oo_prefixes[] wpseo,yoast

<?php

// For PSR4 file names, prefixes should *not* be stripped.
class Yoast_Prefixed {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
// phpcs:set Yoast.Files.FileName oo_prefixes[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix\\=>PSR4/../PSR4/../

<?php

class Illegal_PSR4_Path {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
11 changes: 11 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/missing-suffix.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix\\=>/PSR4/

<?php

// Files without an OO structure, should follow the normal "lowercase hyphenated" + "-functions" suffix file name rules.
function Foo() {}

function Bar() {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
11 changes: 11 additions & 0 deletions Yoast/Tests/Files/FileNameUnitTests/PSR4/no-oo-functions.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- Annotation must be on line 2 as this sniff throws issues on line 1 and PHPCS ignores errors on annotation lines. -->
phpcs:set Yoast.Files.FileName psr4_paths[] Prefix=>/PSR4/

<?php

// Files without an OO structure, should follow the normal "lowercase hyphenated" + "-functions" suffix file name rules.
function Foo() {}

function Bar() {}

// phpcs:set Yoast.Files.FileName psr4_paths[]
Loading

0 comments on commit 6c60a4e

Please sign in to comment.