Skip to content

Commit cda8aa7

Browse files
authored
Merge pull request #6 from bigbite/feature/filename-sniff
Add sniff for file name conventions
2 parents 23b687e + 3c73b24 commit cda8aa7

File tree

69 files changed

+2949
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2949
-6
lines changed

.editorconfig

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# editorconfig.org
2+
root = true
3+
4+
[*]
5+
indent_style = space
6+
end_of_line = lf
7+
indent_size = 2
8+
charset = utf-8
9+
trim_trailing_whitespace = true
10+
insert_final_newline = true
11+
12+
[*.{json,php}]
13+
indent_size = 4
14+
indent_style = tab
15+
16+
[*.md]
17+
trim_trailing_whitespace = false

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
composer.phar
2+
.vscode/
23
/vendor/

.phpcs.xml.dist

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="Big Bite CS" namespace="BigBite">
3+
<description>The Coding Standard for the Big Bite Coding Standards.</description>
4+
5+
<file>.</file>
6+
7+
<config name="encoding" value="utf-8" />
8+
<config name="testVersion" value="7.0-" />
9+
10+
<arg name="basepath" value="." />
11+
<arg name="extensions" value="php" />
12+
<arg name="tab-width" value="4" />
13+
<arg value="qs" />
14+
15+
<!-- Exclude Composer vendor directory. -->
16+
<exclude-pattern>*/vendor/*</exclude-pattern>
17+
18+
<rule ref="BigBite">
19+
<exclude name="BigBite.Files.FileName" />
20+
<exclude name="WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition" />
21+
<exclude name="WordPress.NamingConventions.ValidVariableName" />
22+
</rule>
23+
24+
<!-- Enforce PSR1 compatible namespaces. -->
25+
<rule ref="PSR1.Classes.ClassDeclaration" />
26+
27+
<!-- prevent empty lines between method bodies and closing braces -->
28+
<rule ref="PSR2.Methods.FunctionClosingBrace" />
29+
30+
<!-- Check code for cross-version PHP compatibility. -->
31+
<rule ref="PHPCompatibility">
32+
<!-- Exclude PHP constants back-filled by PHPCS. -->
33+
<exclude name="PHPCompatibility.Constants.NewConstants.t_finallyFound" />
34+
<exclude name="PHPCompatibility.Constants.NewConstants.t_yieldFound" />
35+
<exclude name="PHPCompatibility.Constants.NewConstants.t_ellipsisFound" />
36+
<exclude name="PHPCompatibility.Constants.NewConstants.t_powFound" />
37+
<exclude name="PHPCompatibility.Constants.NewConstants.t_pow_equalFound" />
38+
<exclude name="PHPCompatibility.Constants.NewConstants.t_spaceshipFound" />
39+
<exclude name="PHPCompatibility.Constants.NewConstants.t_coalesceFound" />
40+
<exclude name="PHPCompatibility.Constants.NewConstants.t_coalesce_equalFound" />
41+
<exclude name="PHPCompatibility.Constants.NewConstants.t_yield_fromFound" />
42+
</rule>
43+
</ruleset>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<documentation title="File Names">
2+
<standard>
3+
<![CDATA[
4+
Files containing classes should be prefixed with "class-" or "abstract-class", and be named
5+
as the class name in lower-kebab-case. For example:
6+
the file name for abstract class My_Class_Name {} would be "abstract-class-my-class-name.php".
7+
]]>
8+
</standard>
9+
</documentation>
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
/**
4+
* BigBite Coding Standards.
5+
*
6+
* @package BigBite\phpcs-config
7+
* @link https://github.com/bigbite/phpcs-config
8+
* @license https://opensource.org/licenses/MIT MIT
9+
*/
10+
11+
declare( strict_types = 1 );
12+
13+
namespace BigBiteCS\BigBite\Sniffs\Files;
14+
15+
use WordPressCS\WordPress\Sniffs\Files\FileNameSniff as WPFileNameSniff;
16+
17+
/**
18+
* Ensures filenames are kebab-case, and are named appropriately
19+
*/
20+
class FileNameSniff extends WPFileNameSniff {
21+
22+
/**
23+
* Processes this test when one of its tokens is encountered.
24+
*
25+
* @param int $stackPtr The position of the current token in the stack.
26+
*
27+
* @return int|void Integer stack pointer to skip forward or void to continue
28+
* normal file processing.
29+
*/
30+
public function process_token( $stackPtr ) {
31+
// strip quotes to ensure `stdin_path` passed by IDEs does not include quotes.
32+
$file = preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $this->phpcsFile->getFileName() );
33+
34+
if ( 'STDIN' === $file ) {
35+
return;
36+
}
37+
38+
if ( $this->is_disabled_by_comments() ) {
39+
return;
40+
}
41+
42+
$fileName = basename( $file );
43+
44+
list( $ext, $file ) = explode( '.', strrev( $fileName ), 2 );
45+
46+
$expected = $this->kebab( strrev( $file ) ) . '.' . strrev( $ext );
47+
48+
/*
49+
* Generic check for lowercase hyphenated file names.
50+
*/
51+
if ( $fileName !== $expected && ( false === $this->is_theme || 1 !== preg_match( self::THEME_EXCEPTIONS_REGEX, $fileName ) ) ) {
52+
$this->phpcsFile->addError(
53+
'Filenames should be all lowercase with hyphens as word separators. Expected %s, but found %s.',
54+
0,
55+
'NotHyphenatedLowercase',
56+
[ $expected, $fileName ]
57+
);
58+
}
59+
60+
unset( $expected );
61+
62+
/*
63+
* Check files containing a class for the "class-" prefix and that the rest of
64+
* the file name reflects the class name. Accounts for abstract classes.
65+
*/
66+
if ( true === $this->strict_class_file_names ) {
67+
$has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr );
68+
69+
if ( false !== $has_class && false === $this->is_test_class( $has_class ) ) {
70+
$is_abstract = $this->phpcsFile->findPrevious( \T_ABSTRACT, $has_class );
71+
$class_name = $this->phpcsFile->getDeclarationName( $has_class );
72+
$expected = 'class-' . $this->kebab( $class_name );
73+
$err_message = 'Class file names should be based on the class name with "class-" prepended. Expected %s, but found %s.';
74+
75+
if ( $is_abstract ) {
76+
$expected = 'abstract-' . $expected;
77+
$err_message = 'Abstract class file names should be based on the class name with "abstract-class-" prepended. Expected %s, but found %s.';
78+
}
79+
80+
if ( substr( $fileName, 0, -4 ) !== $expected ) {
81+
$this->phpcsFile->addError( $err_message, 0, 'InvalidClassFileName', [ $expected . '.php', $fileName ] );
82+
}
83+
84+
unset( $expected );
85+
}
86+
}
87+
88+
// Only run this sniff once per file, no need to run it again.
89+
return ( $this->phpcsFile->numTokens + 1 );
90+
}
91+
92+
/**
93+
* Respect phpcs:disable comments as long as they are not accompanied by an enable (PHPCS 3.2+).
94+
*
95+
* @return bool
96+
*/
97+
protected function is_disabled_by_comments() {
98+
if ( ! \defined( '\T_PHPCS_DISABLE' ) || ! \defined( '\T_PHPCS_ENABLE' ) ) {
99+
return false;
100+
}
101+
102+
$i = -1;
103+
while ( $i = $this->phpcsFile->findNext( \T_PHPCS_DISABLE, ( $i + 1 ) ) ) {
104+
if ( empty( $this->tokens[ $i ]['sniffCodes'] )
105+
|| isset( $this->tokens[ $i ]['sniffCodes']['BigBite'] )
106+
|| isset( $this->tokens[ $i ]['sniffCodes']['BigBite.Files'] )
107+
|| isset( $this->tokens[ $i ]['sniffCodes']['BigBite.Files.FileName'] )
108+
) {
109+
do {
110+
$i = $this->phpcsFile->findNext( \T_PHPCS_ENABLE, ( $i + 1 ) );
111+
} while ( false !== $i
112+
&& ! empty( $this->tokens[ $i ]['sniffCodes'] )
113+
&& ! isset( $this->tokens[ $i ]['sniffCodes']['BigBite'] )
114+
&& ! isset( $this->tokens[ $i ]['sniffCodes']['BigBite.Files'] )
115+
&& ! isset( $this->tokens[ $i ]['sniffCodes']['BigBite.Files.FileName'] ) );
116+
117+
if ( false === $i ) {
118+
// The entire (rest of the) file is disabled.
119+
return true;
120+
}
121+
}
122+
}
123+
124+
return false;
125+
}
126+
127+
/**
128+
* Convert a string to kebab-case
129+
*
130+
* @param string $string the string to texturise
131+
*
132+
* @return string
133+
*/
134+
protected function kebab( $string = '' ) {
135+
$kebab = preg_replace( '/(?>(?!^[A-Z]))([a-z])([A-Z])/', '$1-$2', $string );
136+
$kebab = strtolower( $kebab );
137+
$kebab = str_replace( '_', '-', $kebab );
138+
139+
// allow wordpress to be one word
140+
if ( false !== strpos( $string, 'WordPress' ) ) {
141+
$kebab = str_replace( 'word-press', 'wordpress', $kebab );
142+
}
143+
144+
return $kebab;
145+
}
146+
147+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace BigBiteCS\BigBite\Tests;
4+
5+
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest as PhpCsAbstractSniffUnitTest;
6+
7+
abstract class AbstractSniffUnitTest extends PhpCsAbstractSniffUnitTest {
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?php
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
/**
3+
* Unit test class for BigBite Coding Standard.
4+
*
5+
* @package BigBiteCS\BigBite
6+
* @link https://github.com/bigbite/phpcs-config
7+
* @license https://opensource.org/licenses/MIT MIT
8+
*/
9+
10+
namespace BigBiteCS\BigBite\Tests\Files;
11+
12+
use BigBiteCS\BigBite\Tests\AbstractSniffUnitTest;
13+
14+
/**
15+
* Unit test class for the FileName sniff.
16+
*
17+
* @package BigBiteCS\BigBite
18+
*/
19+
class FileNameUnitTest extends AbstractSniffUnitTest {
20+
21+
/**
22+
* Error files with the expected nr of errors.
23+
*
24+
* @var array
25+
*/
26+
private $expected_results = array(
27+
28+
/*
29+
* In /FileNameUnitTests.
30+
*/
31+
32+
// File names generic.
33+
'some_file.inc' => 1,
34+
'SomeFile.inc' => 1,
35+
'some-File.inc' => 1,
36+
'SomeView.inc' => 1,
37+
38+
// Class file names.
39+
'my-class.inc' => 1,
40+
'my-abstract-class.inc' => 1,
41+
'class-different-class.inc' => 1,
42+
'abstract-class-different-class.inc' => 1,
43+
'ClassMyClass.inc' => 2,
44+
'AbstractClassMyClass.inc' => 2,
45+
46+
// Theme specific exceptions in a non-theme context.
47+
'single-my_post_type.inc' => 1,
48+
'taxonomy-post_format-post-format-audio.inc' => 1,
49+
50+
/*
51+
* In /FileNameUnitTests/NonStrictClassNames.
52+
*/
53+
54+
// Non-strict class names still have to comply with lowercase hyphenated.
55+
'ClassNonStrictClass.inc' => 1,
56+
'AbstractClassNonStrictClass.inc' => 1,
57+
58+
/*
59+
* In /FileNameUnitTests/PHPCSAnnotations.
60+
*/
61+
62+
// Non-strict class names still have to comply with lowercase hyphenated.
63+
'blanket-disable.inc' => 0,
64+
'non-relevant-disable.inc' => 1,
65+
'partial-file-disable.inc' => 1,
66+
'rule-disable.inc' => 0,
67+
'wordpress-disable.inc' => 0,
68+
69+
/*
70+
* In /FileNameUnitTests/TestFiles.
71+
*/
72+
'test-sample-phpunit.inc' => 0,
73+
'test-sample-phpunit6.inc' => 0,
74+
'test-sample-wpunit.inc' => 0,
75+
'test-sample-custom-unit.inc' => 0,
76+
'test-sample-namespaced-declaration-1.inc' => 0,
77+
'test-sample-namespaced-declaration-2.inc' => 1, // Namespaced vs non-namespaced.
78+
'test-sample-namespaced-declaration-3.inc' => 1, // Wrong namespace.
79+
'test-sample-namespaced-declaration-4.inc' => 1, // Non-namespaced vs namespaced.
80+
'test-sample-global-namespace-extends-1.inc' => 0, // Prefixed user input.
81+
'test-sample-global-namespace-extends-2.inc' => 1, // Non-namespaced vs namespaced.
82+
'test-sample-extends-with-use.inc' => 0,
83+
'test-sample-namespaced-extends-1.inc' => 0,
84+
'test-sample-namespaced-extends-2.inc' => 1, // Wrong namespace.
85+
'test-sample-namespaced-extends-3.inc' => 1, // Namespaced vs non-namespaced.
86+
'test-sample-namespaced-extends-4.inc' => 1, // Non-namespaced vs namespaced.
87+
'test-sample-namespaced-extends-5.inc' => 0,
88+
89+
/*
90+
* In /FileNameUnitTests/ThemeExceptions.
91+
*/
92+
93+
// Files in a theme context.
94+
'front_page.inc' => 1,
95+
'FrontPage.inc' => 1,
96+
'author-nice_name.inc' => 1,
97+
98+
/*
99+
* In /FileNameUnitTests/wp-includes.
100+
*/
101+
102+
// Files containing template tags.
103+
'general.inc' => 1,
104+
105+
/*
106+
* In /.
107+
*/
108+
109+
// Fall-back file in case glob() fails.
110+
'FileNameUnitTest.inc' => 1,
111+
);
112+
113+
/**
114+
* Get a list of all test files to check.
115+
*
116+
* @param string $testFileBase The base path that the unit tests files will have.
117+
*
118+
* @return string[]
119+
*/
120+
protected function getTestFiles( $testFileBase ) {
121+
$sep = \DIRECTORY_SEPARATOR;
122+
$test_files = glob( dirname( $testFileBase ) . $sep . 'FileNameUnitTests{' . $sep . ',' . $sep . '*' . $sep . '}*.inc', \GLOB_BRACE );
123+
124+
if ( ! empty( $test_files ) ) {
125+
return $test_files;
126+
}
127+
128+
return array( $testFileBase . '.inc' );
129+
}
130+
131+
/**
132+
* Returns the lines where errors should occur.
133+
*
134+
* @param string $testFile The name of the file being tested.
135+
* @return array <int line number> => <int number of errors>
136+
*/
137+
public function getErrorList( $testFile = '' ) {
138+
if ( isset( $this->expected_results[ $testFile ] ) ) {
139+
return array(
140+
1 => $this->expected_results[ $testFile ],
141+
);
142+
}
143+
144+
return array();
145+
}
146+
147+
/**
148+
* Returns the lines where warnings should occur.
149+
*
150+
* @return array <int line number> => <int number of warnings>
151+
*/
152+
public function getWarningList() {
153+
return array();
154+
}
155+
156+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
abstract class My_Class {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
class My_Class {}

0 commit comments

Comments
 (0)