Skip to content

Commit

Permalink
Merge pull request #6 from bigbite/feature/filename-sniff
Browse files Browse the repository at this point in the history
Add sniff for file name conventions
  • Loading branch information
jaymcp authored Apr 5, 2022
2 parents 23b687e + 3c73b24 commit cda8aa7
Show file tree
Hide file tree
Showing 69 changed files with 2,949 additions and 6 deletions.
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# editorconfig.org
root = true

[*]
indent_style = space
end_of_line = lf
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{json,php}]
indent_size = 4
indent_style = tab

[*.md]
trim_trailing_whitespace = false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
composer.phar
.vscode/
/vendor/
43 changes: 43 additions & 0 deletions .phpcs.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0"?>
<ruleset name="Big Bite CS" namespace="BigBite">
<description>The Coding Standard for the Big Bite Coding Standards.</description>

<file>.</file>

<config name="encoding" value="utf-8" />
<config name="testVersion" value="7.0-" />

<arg name="basepath" value="." />
<arg name="extensions" value="php" />
<arg name="tab-width" value="4" />
<arg value="qs" />

<!-- Exclude Composer vendor directory. -->
<exclude-pattern>*/vendor/*</exclude-pattern>

<rule ref="BigBite">
<exclude name="BigBite.Files.FileName" />
<exclude name="WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition" />
<exclude name="WordPress.NamingConventions.ValidVariableName" />
</rule>

<!-- Enforce PSR1 compatible namespaces. -->
<rule ref="PSR1.Classes.ClassDeclaration" />

<!-- prevent empty lines between method bodies and closing braces -->
<rule ref="PSR2.Methods.FunctionClosingBrace" />

<!-- Check code for cross-version PHP compatibility. -->
<rule ref="PHPCompatibility">
<!-- Exclude PHP constants back-filled by PHPCS. -->
<exclude name="PHPCompatibility.Constants.NewConstants.t_finallyFound" />
<exclude name="PHPCompatibility.Constants.NewConstants.t_yieldFound" />
<exclude name="PHPCompatibility.Constants.NewConstants.t_ellipsisFound" />
<exclude name="PHPCompatibility.Constants.NewConstants.t_powFound" />
<exclude name="PHPCompatibility.Constants.NewConstants.t_pow_equalFound" />
<exclude name="PHPCompatibility.Constants.NewConstants.t_spaceshipFound" />
<exclude name="PHPCompatibility.Constants.NewConstants.t_coalesceFound" />
<exclude name="PHPCompatibility.Constants.NewConstants.t_coalesce_equalFound" />
<exclude name="PHPCompatibility.Constants.NewConstants.t_yield_fromFound" />
</rule>
</ruleset>
9 changes: 9 additions & 0 deletions BigBite/Docs/Files/FileNameStandard.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<documentation title="File Names">
<standard>
<![CDATA[
Files containing classes should be prefixed with "class-" or "abstract-class", and be named
as the class name in lower-kebab-case. For example:
the file name for abstract class My_Class_Name {} would be "abstract-class-my-class-name.php".
]]>
</standard>
</documentation>
147 changes: 147 additions & 0 deletions BigBite/Sniffs/Files/FileNameSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

/**
* BigBite Coding Standards.
*
* @package BigBite\phpcs-config
* @link https://github.com/bigbite/phpcs-config
* @license https://opensource.org/licenses/MIT MIT
*/

declare( strict_types = 1 );

namespace BigBiteCS\BigBite\Sniffs\Files;

use WordPressCS\WordPress\Sniffs\Files\FileNameSniff as WPFileNameSniff;

/**
* Ensures filenames are kebab-case, and are named appropriately
*/
class FileNameSniff extends WPFileNameSniff {

/**
* Processes this test when one of its tokens is encountered.
*
* @param int $stackPtr The position of the current token in the stack.
*
* @return int|void Integer stack pointer to skip forward or void to continue
* normal file processing.
*/
public function process_token( $stackPtr ) {
// strip quotes to ensure `stdin_path` passed by IDEs does not include quotes.
$file = preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $this->phpcsFile->getFileName() );

if ( 'STDIN' === $file ) {
return;
}

if ( $this->is_disabled_by_comments() ) {
return;
}

$fileName = basename( $file );

list( $ext, $file ) = explode( '.', strrev( $fileName ), 2 );

$expected = $this->kebab( strrev( $file ) ) . '.' . strrev( $ext );

/*
* Generic check for lowercase hyphenated file names.
*/
if ( $fileName !== $expected && ( false === $this->is_theme || 1 !== preg_match( self::THEME_EXCEPTIONS_REGEX, $fileName ) ) ) {
$this->phpcsFile->addError(
'Filenames should be all lowercase with hyphens as word separators. Expected %s, but found %s.',
0,
'NotHyphenatedLowercase',
[ $expected, $fileName ]
);
}

unset( $expected );

/*
* Check files containing a class for the "class-" prefix and that the rest of
* the file name reflects the class name. Accounts for abstract classes.
*/
if ( true === $this->strict_class_file_names ) {
$has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr );

if ( false !== $has_class && false === $this->is_test_class( $has_class ) ) {
$is_abstract = $this->phpcsFile->findPrevious( \T_ABSTRACT, $has_class );
$class_name = $this->phpcsFile->getDeclarationName( $has_class );
$expected = 'class-' . $this->kebab( $class_name );
$err_message = 'Class file names should be based on the class name with "class-" prepended. Expected %s, but found %s.';

if ( $is_abstract ) {
$expected = 'abstract-' . $expected;
$err_message = 'Abstract class file names should be based on the class name with "abstract-class-" prepended. Expected %s, but found %s.';
}

if ( substr( $fileName, 0, -4 ) !== $expected ) {
$this->phpcsFile->addError( $err_message, 0, 'InvalidClassFileName', [ $expected . '.php', $fileName ] );
}

unset( $expected );
}
}

// Only run this sniff once per file, no need to run it again.
return ( $this->phpcsFile->numTokens + 1 );
}

/**
* Respect phpcs:disable comments as long as they are not accompanied by an enable (PHPCS 3.2+).
*
* @return bool
*/
protected function is_disabled_by_comments() {
if ( ! \defined( '\T_PHPCS_DISABLE' ) || ! \defined( '\T_PHPCS_ENABLE' ) ) {
return false;
}

$i = -1;
while ( $i = $this->phpcsFile->findNext( \T_PHPCS_DISABLE, ( $i + 1 ) ) ) {
if ( empty( $this->tokens[ $i ]['sniffCodes'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['BigBite'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['BigBite.Files'] )
|| isset( $this->tokens[ $i ]['sniffCodes']['BigBite.Files.FileName'] )
) {
do {
$i = $this->phpcsFile->findNext( \T_PHPCS_ENABLE, ( $i + 1 ) );
} while ( false !== $i
&& ! empty( $this->tokens[ $i ]['sniffCodes'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['BigBite'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['BigBite.Files'] )
&& ! isset( $this->tokens[ $i ]['sniffCodes']['BigBite.Files.FileName'] ) );

if ( false === $i ) {
// The entire (rest of the) file is disabled.
return true;
}
}
}

return false;
}

/**
* Convert a string to kebab-case
*
* @param string $string the string to texturise
*
* @return string
*/
protected function kebab( $string = '' ) {
$kebab = preg_replace( '/(?>(?!^[A-Z]))([a-z])([A-Z])/', '$1-$2', $string );
$kebab = strtolower( $kebab );
$kebab = str_replace( '_', '-', $kebab );

// allow wordpress to be one word
if ( false !== strpos( $string, 'WordPress' ) ) {
$kebab = str_replace( 'word-press', 'wordpress', $kebab );
}

return $kebab;
}

}
8 changes: 8 additions & 0 deletions BigBite/Tests/AbstractSniffUnitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace BigBiteCS\BigBite\Tests;

use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest as PhpCsAbstractSniffUnitTest;

abstract class AbstractSniffUnitTest extends PhpCsAbstractSniffUnitTest {
}
1 change: 1 addition & 0 deletions BigBite/Tests/Files/FileNameUnitTest.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php
156 changes: 156 additions & 0 deletions BigBite/Tests/Files/FileNameUnitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php
/**
* Unit test class for BigBite Coding Standard.
*
* @package BigBiteCS\BigBite
* @link https://github.com/bigbite/phpcs-config
* @license https://opensource.org/licenses/MIT MIT
*/

namespace BigBiteCS\BigBite\Tests\Files;

use BigBiteCS\BigBite\Tests\AbstractSniffUnitTest;

/**
* Unit test class for the FileName sniff.
*
* @package BigBiteCS\BigBite
*/
class FileNameUnitTest extends AbstractSniffUnitTest {

/**
* Error files with the expected nr of errors.
*
* @var array
*/
private $expected_results = array(

/*
* In /FileNameUnitTests.
*/

// File names generic.
'some_file.inc' => 1,
'SomeFile.inc' => 1,
'some-File.inc' => 1,
'SomeView.inc' => 1,

// Class file names.
'my-class.inc' => 1,
'my-abstract-class.inc' => 1,
'class-different-class.inc' => 1,
'abstract-class-different-class.inc' => 1,
'ClassMyClass.inc' => 2,
'AbstractClassMyClass.inc' => 2,

// Theme specific exceptions in a non-theme context.
'single-my_post_type.inc' => 1,
'taxonomy-post_format-post-format-audio.inc' => 1,

/*
* In /FileNameUnitTests/NonStrictClassNames.
*/

// Non-strict class names still have to comply with lowercase hyphenated.
'ClassNonStrictClass.inc' => 1,
'AbstractClassNonStrictClass.inc' => 1,

/*
* In /FileNameUnitTests/PHPCSAnnotations.
*/

// Non-strict class names still have to comply with lowercase hyphenated.
'blanket-disable.inc' => 0,
'non-relevant-disable.inc' => 1,
'partial-file-disable.inc' => 1,
'rule-disable.inc' => 0,
'wordpress-disable.inc' => 0,

/*
* In /FileNameUnitTests/TestFiles.
*/
'test-sample-phpunit.inc' => 0,
'test-sample-phpunit6.inc' => 0,
'test-sample-wpunit.inc' => 0,
'test-sample-custom-unit.inc' => 0,
'test-sample-namespaced-declaration-1.inc' => 0,
'test-sample-namespaced-declaration-2.inc' => 1, // Namespaced vs non-namespaced.
'test-sample-namespaced-declaration-3.inc' => 1, // Wrong namespace.
'test-sample-namespaced-declaration-4.inc' => 1, // Non-namespaced vs namespaced.
'test-sample-global-namespace-extends-1.inc' => 0, // Prefixed user input.
'test-sample-global-namespace-extends-2.inc' => 1, // Non-namespaced vs namespaced.
'test-sample-extends-with-use.inc' => 0,
'test-sample-namespaced-extends-1.inc' => 0,
'test-sample-namespaced-extends-2.inc' => 1, // Wrong namespace.
'test-sample-namespaced-extends-3.inc' => 1, // Namespaced vs non-namespaced.
'test-sample-namespaced-extends-4.inc' => 1, // Non-namespaced vs namespaced.
'test-sample-namespaced-extends-5.inc' => 0,

/*
* In /FileNameUnitTests/ThemeExceptions.
*/

// Files in a theme context.
'front_page.inc' => 1,
'FrontPage.inc' => 1,
'author-nice_name.inc' => 1,

/*
* In /FileNameUnitTests/wp-includes.
*/

// Files containing template tags.
'general.inc' => 1,

/*
* In /.
*/

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

/**
* Get a list of all test files to check.
*
* @param string $testFileBase The base path that the unit tests files will have.
*
* @return string[]
*/
protected function getTestFiles( $testFileBase ) {
$sep = \DIRECTORY_SEPARATOR;
$test_files = glob( dirname( $testFileBase ) . $sep . 'FileNameUnitTests{' . $sep . ',' . $sep . '*' . $sep . '}*.inc', \GLOB_BRACE );

if ( ! empty( $test_files ) ) {
return $test_files;
}

return array( $testFileBase . '.inc' );
}

/**
* Returns the lines where errors should occur.
*
* @param string $testFile The name of the file being tested.
* @return array <int line number> => <int number of errors>
*/
public function getErrorList( $testFile = '' ) {
if ( isset( $this->expected_results[ $testFile ] ) ) {
return array(
1 => $this->expected_results[ $testFile ],
);
}

return array();
}

/**
* Returns the lines where warnings should occur.
*
* @return array <int line number> => <int number of warnings>
*/
public function getWarningList() {
return array();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

abstract class My_Class {}
3 changes: 3 additions & 0 deletions BigBite/Tests/Files/FileNameUnitTests/ClassMyClass.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

class My_Class {}
Loading

0 comments on commit cda8aa7

Please sign in to comment.