├── CHANGELOG.md
├── LICENSE
├── README.md
├── Yoast
├── Docs
│ ├── Commenting
│ │ ├── CodeCoverageIgnoreDeprecatedStandard.xml
│ │ ├── CoversTagStandard.xml
│ │ ├── FileCommentStandard.xml
│ │ └── TestsHaveCoversTagStandard.xml
│ ├── Files
│ │ ├── FileNameStandard.xml
│ │ └── TestDoublesStandard.xml
│ ├── NamingConventions
│ │ ├── NamespaceNameStandard.xml
│ │ ├── ObjectNameDepthStandard.xml
│ │ └── ValidHookNameStandard.xml
│ ├── Tools
│ │ └── BrainMonkeyRaceConditionStandard.xml
│ ├── WhiteSpace
│ │ └── FunctionSpacingStandard.xml
│ └── Yoast
│ │ └── JsonEncodeAlternativeStandard.xml
├── Reports
│ └── Threshold.php
├── Sniffs
│ ├── Commenting
│ │ ├── CodeCoverageIgnoreDeprecatedSniff.php
│ │ ├── CoversTagSniff.php
│ │ ├── FileCommentSniff.php
│ │ └── TestsHaveCoversTagSniff.php
│ ├── Files
│ │ ├── FileNameSniff.php
│ │ └── TestDoublesSniff.php
│ ├── NamingConventions
│ │ ├── NamespaceNameSniff.php
│ │ ├── ObjectNameDepthSniff.php
│ │ └── ValidHookNameSniff.php
│ ├── Tools
│ │ └── BrainMonkeyRaceConditionSniff.php
│ ├── WhiteSpace
│ │ └── FunctionSpacingSniff.php
│ └── Yoast
│ │ └── JsonEncodeAlternativeSniff.php
├── Utils
│ ├── CustomPrefixesTrait.php
│ ├── PSR4PathsTrait.php
│ ├── PathHelper.php
│ └── PathValidationHelper.php
└── ruleset.xml
├── autoload-bootstrap.php
├── composer.json
└── phpunit-bootstrap.php
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Yoast
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Yoast Coding Standards
2 |
3 | [](https://coveralls.io/github/Yoast/yoastcs?branch=develop)
4 |
5 | Yoast Coding Standards (YoastCS) is a project with rulesets for code style and quality tools to be used in Yoast projects.
6 |
7 | ## Installation
8 |
9 | ### Standalone
10 |
11 | Standards are provided as a [Composer](https://getcomposer.org/) package and can be installed with:
12 |
13 | ```bash
14 | composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
15 | composer global require --dev yoast/yoastcs:"^3.0"
16 | ```
17 |
18 | ### As dependency
19 |
20 | To include standards as part of a project require them as development dependencies:
21 |
22 | ```bash
23 | composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
24 | composer require --dev yoast/yoastcs:"^3.0"
25 | ```
26 |
27 | Composer will automatically install dependencies and register YoastCS and other external standards with PHP_CodeSniffer.
28 |
29 | ## Tools provided via YoastCS
30 |
31 | * PHP Parallel Lint
32 | * PHP_CodeSniffer and select standards for PHP_CodeSniffer, including a number of Yoast native sniffs.
33 |
34 |
35 | ## PHP Parallel Lint
36 |
37 | [PHP Parallel Lint](https://github.com/php-parallel-lint/PHP-Parallel-Lint/) is a tool to lint PHP files against parse errors.
38 |
39 | PHP Parallel Lint does not use a configuration file, so [command-line options](https://github.com/php-parallel-lint/PHP-Parallel-Lint/#command-line-options) need to be passed to configure what files to scan.
40 |
41 | It is best practice within the Yoast projects, to add a script to the `composer.json` file which encapsules the command with the appropriate command-line options to ensure that running the tool will yield the same results each time.
42 |
43 | Typically, (a variation on) the following snippet would be added to the `composer.json` file for a project:
44 | ```json
45 | "scripts" : {
46 | "lint": [
47 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --show-deprecated --exclude vendor --exclude .git"
48 | ]
49 | }
50 | ```
51 |
52 |
53 | ## PHP Code Sniffer
54 |
55 | Set of [PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) rules.
56 |
57 | Severity levels:
58 |
59 | - error level issues are considered mandatory to fix in Yoast projects and enforced in continuous integration
60 | - warning level issues are considered recommended to fix
61 |
62 | ### The YoastCS Standard
63 |
64 | The `Yoast` standard for PHP_CodeSniffer is comprised of the following:
65 | * The `WordPress` ruleset from the [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards) implementing the official [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/), with some [select exclusions](https://github.com/Yoast/yoastcs/blob/develop/Yoast/ruleset.xml#L29-L75).
66 | * The [`PHPCompatibilityWP`](https://github.com/PHPCompatibility/PHPCompatibilityWP) ruleset which checks code for PHP cross-version compatibility while preventing false positives for functionality polyfilled within WordPress.
67 | * The [`VariableAnalysis`](https://github.com/sirbrillig/phpcs-variable-analysis/) ruleset.
68 | * Select additional sniffs taken from [`PHP_CodeSniffer`](https://github.com/PHPCSStandards/PHP_CodeSniffer).
69 | * Select additional sniffs taken from [`PHPCSExtra`](https://github.com/PHPCSStandards/PHPCSExtra).
70 | * Select additional sniffs taken from [`SlevomatCodingStandard`](https://github.com/slevomat/coding-standard).
71 | * Select additional sniffs taken from [WordPress VIP Coding Standards](https://github.com/Automattic/VIP-Coding-Standards/).
72 | * A number of custom Yoast specific sniffs.
73 |
74 | Files within version management and dependency related directories, such as the Composer `vendor` directory, are excluded from the scans by default.
75 |
76 | #### Sniffs
77 |
78 | To obtain a list of all sniffs used within YoastCS:
79 | ```bash
80 | "vendor/bin/phpcs" -e --standard=Yoast
81 | ```
82 |
83 | #### Sniff Documentation
84 |
85 | Not all sniffs have documentation available about what they sniff for, but for those which do, this documentation can be viewed from the command-line:
86 | ```bash
87 | "vendor/bin/phpcs" --standard=Yoast --generator=Text
88 | ```
89 |
90 | ### Running the sniffs
91 |
92 | #### Command line
93 |
94 | ```bash
95 | "vendor/bin/phpcs" --extensions=php /path/to/folder/
96 | ```
97 |
98 | For more command-line options, please have a read through the [PHP_CodeSniffer documentation](https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Usage).
99 |
100 | #### Yoast plugin repositories
101 |
102 | All Yoast plugin repositories contain a `[.]phpcs.xml.dist` file which contains the repository specific configuration.
103 |
104 | From the root of these repositories, you can run PHPCS by using:
105 | ```bash
106 | composer check-cs-warnings
107 | ```
108 |
109 | #### PhpStorm
110 |
111 | Refer to [Using PHP Code Sniffer Tool](https://www.jetbrains.com/phpstorm/help/using-php-code-sniffer-tool.html) in the PhpStorm documentation.
112 |
113 | After installation, the `Yoast` standard will be available as a choice in PHP Code Sniffer Validation inspection.
114 |
115 | ### The YoastCS "Threshold" report
116 |
117 | The YoastCS package includes a custom `YoastCS\Yoast\Reports\Threshold` report for PHP_CodeSniffer to compare the current PHPCS run results with predefined "threshold" settings.
118 |
119 | The report will look in the runtime environment for the following two environment variables and will take the values of those as the thresholds to compare the PHPCS run results against:
120 | * `YOASTCS_THRESHOLD_ERRORS`
121 | * `YOASTCS_THRESHOLD_WARNINGS`
122 |
123 | If the environment variables are not set, they will default to 0 for both, i.e. no errors or warnings allowed.
124 |
125 | The report will not print any details about the issues found, it just shows a summary based on the thresholds:
126 | ```
127 | PHP CODE SNIFFER THRESHOLD COMPARISON
128 | ------------------------------------------------------------------------------------------------------------------------
129 | Coding standards ERRORS: 148/130.
130 | Coding standards WARNINGS: 539/539.
131 |
132 | Please fix any errors introduced in your code and run PHPCS again to verify.
133 | Please fix any warnings introduced in your code and run PHPCS again to verify.
134 | ```
135 |
136 | After the report has run, a global `YOASTCS_ABOVE_THRESHOLD` constant (boolean) will be available which can be used in calling scripts.
137 |
138 | To use this report, run PHPCS with the following command-line argument: `--report=YoastCS\Yoast\Reports\Threshold`.
139 | _Note: depending on the OS the command is run on, the backslashes in the report name may need to be escaped (doubled)._
140 |
141 | For those Yoast plugin repositories which use thresholds, the status can be checked locally by running:
142 | ```bash
143 | composer check-cs-thresholds
144 | ```
145 |
146 | ## Changelog
147 |
148 | The changelog for this package can be found in the [CHANGELOG.md](https://github.com/Yoast/yoastcs/blob/develop/CHANGELOG.md) file.
149 |
--------------------------------------------------------------------------------
/Yoast/Docs/Commenting/CodeCoverageIgnoreDeprecatedStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 | @deprecated x.x
16 | * @codeCoverageIgnore
17 | */
18 | function deprecated_function() {}
19 | ]]>
20 |
21 |
22 | @deprecated x.x
25 | */
26 | function deprecated_function() {}
27 | ]]>
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Yoast/Docs/Commenting/CoversTagStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
16 |
17 | Class_Name::method_name
23 | */
24 | function test_something() {}
25 | }
26 | ]]>
27 |
28 |
29 | ()
35 | */
36 | function test_something() {}
37 | }
38 | ]]>
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
59 |
60 |
61 | Name\Space\function_name
69 | */
70 | function test_something() {}
71 | }
72 | ]]>
73 |
74 |
75 |
76 |
79 |
80 |
81 |
82 | @covers ::globalFunction
88 | */
89 | function test_something() {}
90 |
91 | /**
92 | * Testing...
93 | *
94 | * @coversNothing
95 | */
96 | function test_something_else() {}
97 | }
98 | ]]>
99 |
100 |
101 | @coversNothing
107 | * @covers ::globalFunction
108 | */
109 | function test_something() {}
110 | }
111 | ]]>
112 |
113 |
114 |
115 | tags is deprecated since PHPUnit 9.0 and support has been removed in PHPUnit 10.0.
117 | These type of annotations should not be used.
118 | ]]>
119 |
120 |
121 |
122 | ::globalFunction
128 | * @covers \ClassName::methodName
129 | */
130 | function test_something() {}
131 | }
132 | ]]>
133 |
134 |
135 | \ClassName::
141 | * @covers \ClassName::
142 | * @covers \ClassName
143 | */
144 | function test_something() {}
145 | }
146 | ]]>
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/Yoast/Docs/Commenting/FileCommentStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 | namespace Yoast\A\B;
17 |
18 | /**
19 | * Class docblock.
20 | */
21 | class {
22 | ...
23 | ]]>
24 |
25 |
26 | /**
29 | * File comment.
30 | */
31 |
32 | namespace Yoast\A\B;
33 |
34 | /**
35 | * Class docblock.
36 | */
37 | class {
38 | ...
39 | ]]>
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 | /**
53 | * File comment.
54 | */
55 |
56 | /**
57 | * Class docblock.
58 | */
59 | class {
60 | ...
61 | ]]>
62 |
63 |
64 |
67 | /**
68 | * Class docblock.
69 | */
70 | class {
71 | ...
72 | ]]>
73 |
74 |
75 |
76 |
77 |
80 |
81 |
82 |
83 | /**
86 | * File comment.
87 | */
88 | ]]>
89 |
90 |
91 | /*
94 | * File comment.
95 | */
96 | ]]>
97 |
98 |
99 |
100 |
101 |
104 |
105 |
106 |
107 |
109 | /**
110 | * File comment.
111 | */
112 | ]]>
113 |
114 |
115 |
117 |
118 |
119 | /**
120 | * File comment.
121 | */
122 | ]]>
123 |
124 |
125 |
126 |
127 |
130 |
131 |
132 |
133 |
138 |
139 | echo $something;
140 | ]]>
141 |
142 |
143 |
148 | echo $something;
149 | ]]>
150 |
151 |
152 |
153 |
154 |
157 |
158 |
159 |
160 | @package Yoast\Package
166 | */
167 | ]]>
168 |
169 |
170 |
176 |
177 |
178 |
179 |
180 |
183 |
184 |
185 |
186 | @package Yoast\Package
192 | */
193 | ]]>
194 |
195 |
196 | @package
202 | */
203 | ]]>
204 |
205 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/Yoast/Docs/Commenting/TestsHaveCoversTagStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 |
23 |
24 |
25 |
33 |
34 |
35 |
36 |
37 |
50 |
51 |
52 |
60 |
61 |
62 |
63 |
64 |
76 |
77 |
78 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/Yoast/Docs/Files/FileNameStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 | file-name.php
15 | ]]>
16 |
17 |
18 | File_Name.php
20 | ]]>
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 | utils.php -->
33 | Utils {}
35 | ]]>
36 |
37 |
38 | class-wpseo-utils.php -->
40 |
43 |
44 |
45 |
46 |
49 |
50 |
51 |
52 | output-thing-interface.php -->
54 | interface Yoast_Output_Thing {}
56 | ]]>
57 |
58 |
59 | yoast-outline-something.php -->
61 | trait Yoast_Outline_Something {}
63 | ]]>
64 |
65 |
66 |
67 |
76 |
77 |
78 |
79 | Yoast_Output_Thing.php -->
81 | Yoast_Output_Thing {}
83 | ]]>
84 |
85 |
86 | outline-something.php -->
88 | Yoast_Outline_Something {}
90 | ]]>
91 |
92 |
93 |
94 |
97 |
98 |
99 |
100 | functions.php -->
102 |
106 |
107 |
108 |
110 |
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/Yoast/Docs/Files/TestDoublesStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
14 |
17 |
18 |
19 |
20 | Test_Double extends Original_Class {
22 | // Code.
23 | }
24 | ]]>
25 |
26 |
27 | Foo_Class extends Original_Foo_Class {
29 | // Code.
30 | }
31 | ]]>
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Yoast/Docs/NamingConventions/NamespaceNameStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
14 |
15 | Yoast\WP\Plugin\Admin\Forms;
17 | ]]>
18 |
19 |
20 | Admin\Forms;
22 | ]]>
23 |
24 |
25 |
26 |
33 |
34 |
35 |
36 | Admin\Forms;
38 |
39 | namespace Yoast\WP\Plugin\Tests\Admin\Forms;
40 | ]]>
41 |
42 |
43 | Admin\Forms\Type\Sub;
45 |
46 | namespace Yoast\WP\Plugin\Tests\Foo\Bar\Flo\Sub;
47 | ]]>
48 |
49 |
50 |
51 |
66 |
67 |
68 |
69 | admin/forms/file.php -->
71 | Admin\Forms;
73 | ]]>
74 |
75 |
76 | admin/forms/file.php -->
78 | Unrelated;
80 | ]]>
81 |
82 |
83 |
84 |
85 | User_Forms/file.php -->
87 | User_Forms;
89 | ]]>
90 |
91 |
92 | User_forms/file.php -->
94 | user_Forms;
96 | ]]>
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/Yoast/Docs/NamingConventions/ObjectNameDepthStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
14 |
15 |
16 |
17 | Non_Namespaced_Long_Class_Name {}
19 | ]]>
20 |
21 |
22 | Namespaced_Long_Class_Name {}
26 | ]]>
27 |
28 |
29 |
30 |
31 | Short_Class_Name {}
35 | ]]>
36 |
37 |
38 | Namespaced_Too_Long_Class_Name {}
42 | ]]>
43 |
44 |
45 |
46 |
47 | Short_Class_Name_Test
53 | extends TestCase {}
54 | ]]>
55 |
56 |
57 | Namespaced_Too_Long_Class_Name_Test
63 | extends TestCase {}
64 | ]]>
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Yoast/Docs/NamingConventions/ValidHookNameStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
14 |
15 | 'Yoast\WP\Plugin\hook_name', $var );
17 | ]]>
18 |
19 |
20 | 'Yoast\WP\Plugin\Hook_NAME', $var );
22 | ]]>
23 |
24 |
25 |
26 |
27 | 'Yoast\WP\Plugin\hook_name',
30 | $var
31 | );
32 | ]]>
33 |
34 |
35 | 'Yoast\WP\Plugin\some/hook-name',
38 | $var
39 | );
40 | ]]>
41 |
42 |
43 |
44 |
49 |
50 |
51 |
52 | 'Yoast\WP\Plugin\hook_name'
55 | );
56 | ]]>
57 |
58 |
59 | 'Yoast\WP\Plugin\long_hook_name_too_long'
62 | );
63 | ]]>
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Yoast/Docs/Tools/BrainMonkeyRaceConditionStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
14 |
15 | once();
19 | Functions\expect( "apply_filters" )
20 | ->once();
21 | Monkey\Functions\expect( 'do_action' )
22 | ->once();
23 |
24 | // Do something to satisfy expectations.
25 | }
26 |
27 | public function test_expectApplied_Done() {
28 | Monkey\Filters\expectApplied( 'filter' )
29 | ->with( true );
30 |
31 | Actions\expectDone( "yoast\action_name" )
32 | ->with( $param );
33 |
34 | expectDone( 'yoast\action_name' )
35 | ->with( $param );
36 |
37 | // Do something to satisfy expectations.
38 | }
39 | }
40 | ]]>
41 |
42 |
43 | once();
47 |
48 | Filters\ExpectApplied( 'filter_name' )
49 | ->with( true );
50 |
51 | // Do something to satisfy expectations.
52 | }
53 |
54 | public function test_mixing_action_expects() {
55 | Expect( 'do_action' /*comment*/)->once();
56 |
57 | Actions\expectDone( 'yoast\action_name' )
58 | ->with( $param );
59 |
60 | // Do something to satisfy expectations.
61 | }
62 | }
63 | ]]>
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Yoast/Docs/WhiteSpace/FunctionSpacingStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 |
15 |
16 | /**
17 | * Function docblock.
18 | */
19 | function func1() {
20 | }
21 | }
22 | ]]>
23 |
24 |
25 |
27 | /**
28 | * Function docblock.
29 | */
30 | function func1() {
31 | }
32 | }
33 | ]]>
34 |
35 |
36 |
37 |
38 |
41 |
42 |
43 |
44 |
52 |
53 | /**
54 | * Function docblock.
55 | */
56 | function func2() {
57 | }
58 | }
59 | ]]>
60 |
61 |
62 |
70 |
71 |
72 | /**
73 | * Function docblock.
74 | */
75 | function func2() {
76 | }
77 | /**
78 | * Function docblock.
79 | */
80 | function func3() {
81 | }
82 | }
83 | ]]>
84 |
85 |
86 |
87 |
88 |
91 |
92 |
93 |
94 |
102 | }
103 | ]]>
104 |
105 |
106 |
114 |
115 | }
116 | ]]>
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/Yoast/Docs/Yoast/JsonEncodeAlternativeStandard.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 | WPSEO_Utils:format_json_encode( $in );
15 | ]]>
16 |
17 |
18 | wp_json_encode( $in );
20 | ]]>
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Yoast/Reports/Threshold.php:
--------------------------------------------------------------------------------
1 | >> $report Prepared report data.
71 | * @param File $phpcsFile The file being reported on.
72 | * @param bool $showSources Whether to show the source codes.
73 | * @param int $width Maximum allowed line width.
74 | *
75 | * @return bool
76 | */
77 | public function generateFileReport( $report, File $phpcsFile, $showSources = false, $width = 80 ) {
78 | if ( \PHP_CODESNIFFER_VERBOSITY === 0
79 | && $report['errors'] === 0
80 | && $report['warnings'] === 0
81 | ) {
82 | // Nothing to do.
83 | return false;
84 | }
85 |
86 | return true;
87 | }
88 |
89 | /**
90 | * Generates a summary of all errors and warnings compares against preset thresholds.
91 | *
92 | * @param string $cachedData Any partial report data that was returned from
93 | * generateFileReport during the run.
94 | * @param int $totalFiles Total number of files processed during the run.
95 | * @param int $totalErrors Total number of errors found during the run.
96 | * @param int $totalWarnings Total number of warnings found during the run.
97 | * @param int $totalFixable Total number of problems that can be fixed.
98 | * @param bool $showSources Whether to show the source codes.
99 | * @param int $width Maximum allowed line width.
100 | * @param bool $interactive Whether PHPCS is being run in interactive mode.
101 | * @param bool $toScreen Whether the report is being printed to screen.
102 | *
103 | * @return void
104 | */
105 | public function generate(
106 | $cachedData,
107 | $totalFiles,
108 | $totalErrors,
109 | $totalWarnings,
110 | $totalFixable,
111 | $showSources = false,
112 | $width = 80,
113 | $interactive = false,
114 | $toScreen = true
115 | ) {
116 | $error_threshold = (int) \getenv( 'YOASTCS_THRESHOLD_ERRORS' );
117 | $warning_threshold = (int) \getenv( 'YOASTCS_THRESHOLD_WARNINGS' );
118 |
119 | echo \PHP_EOL, self::WHITE, 'PHP CODE SNIFFER THRESHOLD COMPARISON', self::RESET, \PHP_EOL;
120 | echo \str_repeat( '-', $width ), \PHP_EOL;
121 |
122 | $color = self::GREEN;
123 | if ( $totalErrors > $error_threshold ) {
124 | $color = self::RED;
125 | }
126 | echo "{$color}Coding standards ERRORS: $totalErrors/$error_threshold.", self::RESET, \PHP_EOL;
127 |
128 | $color = self::GREEN;
129 | if ( $totalWarnings > $warning_threshold ) {
130 | $color = self::YELLOW;
131 | }
132 | echo "{$color}Coding standards WARNINGS: $totalWarnings/$warning_threshold.", self::RESET, \PHP_EOL;
133 | echo \PHP_EOL;
134 |
135 | $above_threshold = false;
136 | $below_threshold = false;
137 |
138 | if ( $totalErrors > $error_threshold ) {
139 | echo self::RED, 'Please fix any errors introduced in your code and run PHPCS again to verify.', self::RESET, \PHP_EOL;
140 | $above_threshold = true;
141 | }
142 | elseif ( $totalErrors < $error_threshold ) {
143 | echo self::GREEN, 'Found less errors than the threshold, great job!', self::RESET, \PHP_EOL;
144 | echo 'Please update the ERRORS threshold in the composer.json file to ', self::GREEN, $totalErrors, '.', self::RESET, \PHP_EOL;
145 | $below_threshold = true;
146 | }
147 |
148 | if ( $totalWarnings > $warning_threshold ) {
149 | echo self::YELLOW, 'Please fix any warnings introduced in your code and run PHPCS again to verify.', self::RESET, \PHP_EOL;
150 | $above_threshold = true;
151 | }
152 | elseif ( $totalWarnings < $warning_threshold ) {
153 | echo self::GREEN, 'Found less warnings than the threshold, great job!', self::RESET, \PHP_EOL;
154 | echo 'Please update the WARNINGS threshold in the composer.json file to ', self::GREEN, $totalWarnings, '.', self::RESET, \PHP_EOL;
155 | $below_threshold = true;
156 | }
157 |
158 | if ( $above_threshold === false ) {
159 | echo \PHP_EOL;
160 | echo 'Coding standards checks have passed!', \PHP_EOL;
161 | }
162 |
163 | // Make the threshold comparison outcomes available to the calling script.
164 | // The conditional define is only so as to make the method testable.
165 | $exact_match = ( $above_threshold === false && $below_threshold === false );
166 | if ( \defined( 'PHP_CODESNIFFER_IN_TESTS' ) === false ) {
167 | \define( 'YOASTCS_ABOVE_THRESHOLD', $above_threshold );
168 | \define( 'YOASTCS_THRESHOLD_EXACT_MATCH', $exact_match );
169 | }
170 | else {
171 | // Only used in the tests to verify the above constants are being set correctly.
172 | echo \PHP_EOL;
173 | echo 'YOASTCS_ABOVE_THRESHOLD: ', ( $above_threshold === true ) ? 'true' : 'false', \PHP_EOL;
174 | echo 'YOASTCS_THRESHOLD_EXACT_MATCH: ', ( $exact_match === true ) ? 'true' : 'false', \PHP_EOL;
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/Commenting/CodeCoverageIgnoreDeprecatedSniff.php:
--------------------------------------------------------------------------------
1 |
23 | */
24 | public function register() {
25 | $targets = Tokens::$ooScopeTokens;
26 | // Ignore interfaces as they can't contain code. Ignore anon classes as they are normally nested in another construct.
27 | unset( $targets[ \T_ANON_CLASS ], $targets[ \T_INTERFACE ] );
28 |
29 | $targets[ \T_FUNCTION ] = \T_FUNCTION;
30 |
31 | return $targets;
32 | }
33 |
34 | /**
35 | * Processes this test, when one of its tokens is encountered.
36 | *
37 | * @param File $phpcsFile The file being scanned.
38 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
39 | *
40 | * @return void
41 | */
42 | public function process( File $phpcsFile, $stackPtr ) {
43 |
44 | $tokens = $phpcsFile->getTokens();
45 |
46 | if ( $tokens[ $stackPtr ]['code'] === \T_FUNCTION ) {
47 | $ignore = Tokens::$methodPrefixes;
48 | }
49 | else {
50 | $ignore = Collections::classModifierKeywords();
51 | }
52 | $ignore[ \T_WHITESPACE ] = \T_WHITESPACE;
53 |
54 | for ( $commentEnd = ( $stackPtr - 1 ); $commentEnd >= 0; $commentEnd-- ) {
55 | if ( isset( $ignore[ $tokens[ $commentEnd ]['code'] ] ) === true ) {
56 | continue;
57 | }
58 |
59 | if ( isset( $tokens[ $commentEnd ]['attribute_opener'] ) === true ) {
60 | $commentEnd = $tokens[ $commentEnd ]['attribute_opener'];
61 | continue;
62 | }
63 |
64 | break;
65 | }
66 |
67 | if ( $tokens[ $commentEnd ]['code'] !== \T_DOC_COMMENT_CLOSE_TAG ) {
68 | // Function without (proper) docblock. Not our concern.
69 | return;
70 | }
71 |
72 | $commentStart = $tokens[ $commentEnd ]['comment_opener'];
73 |
74 | $deprecated = false;
75 | foreach ( $tokens[ $commentStart ]['comment_tags'] as $tag ) {
76 | if ( $tokens[ $tag ]['content'] === '@deprecated' ) {
77 | $deprecated = true;
78 | break;
79 | }
80 | }
81 |
82 | if ( $deprecated === false ) {
83 | // Not a deprecated function/OO structure.
84 | return;
85 | }
86 |
87 | $codeCoverageIgnore = false;
88 | foreach ( $tokens[ $commentStart ]['comment_tags'] as $tag ) {
89 | if ( $tokens[ $tag ]['content'] === '@codeCoverageIgnore' ) {
90 | $codeCoverageIgnore = true;
91 | break;
92 | }
93 | }
94 |
95 | if ( $codeCoverageIgnore === true ) {
96 | // Docblock contains the @codeCoverageIgnore tag.
97 | return;
98 | }
99 |
100 | $hasTagAsString = $phpcsFile->findNext( \T_DOC_COMMENT_STRING, ( $commentStart + 1 ), $commentEnd, false, 'codeCoverageIgnore' );
101 | if ( $hasTagAsString !== false ) {
102 | $prev = $phpcsFile->findPrevious( \T_DOC_COMMENT_WHITESPACE, ( $hasTagAsString - 1 ), $commentStart, true );
103 | if ( $tokens[ $prev ]['code'] === \T_DOC_COMMENT_STAR ) {
104 | $fix = $phpcsFile->addFixableError(
105 | 'The `codeCoverageIgnore` annotation in the function docblock needs to be prefixed with an `@`.',
106 | $hasTagAsString,
107 | 'NotTag'
108 | );
109 | if ( $fix === true ) {
110 | $phpcsFile->fixer->addContentBefore( $hasTagAsString, '@' );
111 | }
112 |
113 | return;
114 | }
115 | }
116 |
117 | // If we're still here, the tag is missing.
118 | $phpcsFile->addError(
119 | 'The function is marked as deprecated, but the docblock does not contain a `@codeCoverageIgnore` annotation.',
120 | $stackPtr,
121 | 'Missing'
122 | );
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/Commenting/CoversTagSniff.php:
--------------------------------------------------------------------------------
1 | [a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\\\\)*(?P>OOName)(?:|::<[!]?(?:public|protected|private)>|::(?(?!public$|protected$|private$)(?P>OOName)))?|::(?P>functionName)|::<[!]?(?:public|protected|private)>|\\\\?(?:(?P>OOName)\\\\)+(?P>functionName))';
29 |
30 | /**
31 | * Regex to check for deprecated `@covers ClassName` tag format.
32 | *
33 | * @link https://github.com/sebastianbergmann/phpunit/issues/3630 PHPUnit 9.0 deprecation.
34 | * @link https://github.com/sebastianbergmann/phpunit/issues/3631 PHPUnit 10.0 removal.
35 | *
36 | * @var string
37 | */
38 | private const DEPRECATED_FORMAT_EXTENDED = '`^(?:\\\\)?(?:(?[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\\\\)*(?P>OOName)$`';
39 |
40 | /**
41 | * Regex to check for deprecated `@covers *::<[!]public|protected|private>` tag format.
42 | *
43 | * @link https://github.com/sebastianbergmann/phpunit/issues/3630 PHPUnit 9.0 deprecation.
44 | * @link https://github.com/sebastianbergmann/phpunit/issues/3631 PHPUnit 10.0 removal.
45 | *
46 | * @var string
47 | */
48 | private const DEPRECATED_FORMAT_VISIBILITY = '`::<[!]?(?:public|protected|private)>$`';
49 |
50 | /**
51 | * Base error message.
52 | *
53 | * Will be enhanced during the run.
54 | *
55 | * @var string
56 | */
57 | private const ERROR_MSG = 'Invalid @covers annotation found.';
58 |
59 | /**
60 | * Returns an array of tokens this test wants to listen for.
61 | *
62 | * @return array
63 | */
64 | public function register() {
65 | return [
66 | \T_DOC_COMMENT_OPEN_TAG,
67 | ];
68 | }
69 |
70 | /**
71 | * Processes this test, when one of its tokens is encountered.
72 | *
73 | * @param File $phpcsFile The file being scanned.
74 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
75 | *
76 | * @return void
77 | */
78 | public function process( File $phpcsFile, $stackPtr ) {
79 | $tokens = $phpcsFile->getTokens();
80 |
81 | /*
82 | * Find all relevant tags and check for common mistakes in the tag format.
83 | */
84 | $firstCoversTag = false;
85 | $coversTags = [];
86 | $coversNothingTags = [];
87 | foreach ( $tokens[ $stackPtr ]['comment_tags'] as $tag ) {
88 | if ( $tokens[ $tag ]['content'] === '@coversNothing' ) {
89 | $coversNothingTags[] = $tag;
90 | continue;
91 | }
92 |
93 | if ( $tokens[ $tag ]['content'] !== '@covers' ) {
94 | continue;
95 | }
96 |
97 | if ( $firstCoversTag === false ) {
98 | $firstCoversTag = $tag;
99 | }
100 |
101 | // Found a @covers tag.
102 | $next = $phpcsFile->findNext( \T_DOC_COMMENT_WHITESPACE, ( $tag + 1 ), null, true );
103 | if ( $next === false // Shouldn't be possible.
104 | || $tokens[ $next ]['code'] !== \T_DOC_COMMENT_STRING
105 | || $tokens[ $next ]['line'] !== $tokens[ $tag ]['line']
106 | ) {
107 | $phpcsFile->addError(
108 | 'A @covers tag must indicate which class/function/method is being covered by the test',
109 | $tag,
110 | 'Empty'
111 | );
112 |
113 | continue;
114 | }
115 |
116 | $annotation = $tokens[ $next ]['content'];
117 | $coversTags[ "$tag-$next" ] = $annotation;
118 |
119 | // Check for deprecated/removed @covers formats.
120 | if ( \preg_match( self::DEPRECATED_FORMAT_EXTENDED, $annotation ) === 1
121 | || \preg_match( self::DEPRECATED_FORMAT_VISIBILITY, $annotation ) === 1
122 | ) {
123 | $warning = 'Use of the "ClassName<*>" type values for @covers annotations has been deprecated in PHPUnit 9.0';
124 | $warning .= ' and support has been removed in PHPUnit 10.0. Found: %s';
125 | $data = [ $annotation ];
126 | $phpcsFile->addWarning( $warning, $next, 'RemovedFormat', $data );
127 | }
128 |
129 | if ( \preg_match( '`^' . self::VALID_CONTENT_REGEX . '$`', $annotation ) === 1 ) {
130 | continue;
131 | }
132 |
133 | /*
134 | * Account for a number of common "mistakes".
135 | */
136 |
137 | $errorThrown = false;
138 |
139 | // Check for Union/Intersect types.
140 | if ( \strpos( $annotation, '&' ) !== false ) {
141 | if ( $this->fixAnnotationToSplit( $phpcsFile, $next, 'IntersectFound', '&' ) === true ) {
142 | continue;
143 | }
144 |
145 | $errorThrown = true;
146 | }
147 |
148 | if ( \strpos( $annotation, '|' ) !== false ) {
149 | if ( $this->fixAnnotationToSplit( $phpcsFile, $next, 'UnionFound', '|' ) === true ) {
150 | continue;
151 | }
152 |
153 | $errorThrown = true;
154 | }
155 |
156 | // Parentheses/Braces at the end of the annotation.
157 | $expected = \rtrim( $annotation, '(){} ' );
158 | if ( $this->fixSimpleError( $phpcsFile, $next, $expected, 'InvalidBraces' ) === true ) {
159 | $errorThrown = true;
160 | }
161 |
162 | // Incorrect `` annotation.
163 | if ( \preg_match( '`::[{(\[]?(!)?(public|protected|private)[})\]]?`', $annotation, $matches ) === 1 ) {
164 | $replacement = '::<' . $matches[1] . $matches[2] . '>';
165 | $expected = \str_replace( $matches[0], $replacement, $annotation );
166 |
167 | if ( $this->fixSimpleError( $phpcsFile, $next, $expected, 'InvalidFunctionGroup' ) === true ) {
168 | $errorThrown = true;
169 | }
170 | }
171 |
172 | if ( $errorThrown === true ) {
173 | // We've already thrown an error. No need for duplicates.
174 | continue;
175 | }
176 |
177 | // Throw a generic error for all other invalid annotations.
178 | $error = self::ERROR_MSG;
179 | $error .= ' Check the PHPUnit documentation to see which annotations are supported. Found: %s';
180 | $data = [ $annotation ];
181 | $phpcsFile->addError( $error, $next, 'Invalid', $data );
182 | }
183 |
184 | /*
185 | * Check that a docblock doesn't contain both `@covers` tags as well as `@coversNothing` tag(s).
186 | */
187 | $coversNothingCount = \count( $coversNothingTags );
188 | if ( $firstCoversTag !== false && $coversNothingCount > 0 ) {
189 | $error = 'A test can\'t both cover something as well as cover nothing. First @coversNothing tag encountered on line %d; first @covers tag encountered on line %d';
190 | $data = [
191 | $tokens[ $coversNothingTags[0] ]['line'],
192 | $tokens[ $firstCoversTag ]['line'],
193 | ];
194 |
195 | $phpcsFile->addError( $error, $tokens[ $stackPtr ]['comment_closer'], 'Contradictory', $data );
196 | }
197 |
198 | /*
199 | * Check for duplicate `@coversNothing` tags.
200 | */
201 | if ( $coversNothingCount > 1 ) {
202 | $error = 'Only one @coversNothing tag allowed per test';
203 | $code = 'DuplicateCoversNothing';
204 | $removeTags = [];
205 | foreach ( $coversNothingTags as $ptr ) {
206 | $next = ( $ptr + 1 );
207 | if ( $tokens[ $next ]['code'] === \T_DOC_COMMENT_WHITESPACE
208 | && $tokens[ $next ]['content'] === $phpcsFile->eolChar
209 | ) {
210 | // No comment, ok to remove.
211 | $removeTags[] = $ptr;
212 | }
213 | }
214 |
215 | $removalCount = \count( $removeTags );
216 | if ( ( $coversNothingCount - $removalCount ) > 1 ) {
217 | // More than one tag had a comment.
218 | $phpcsFile->addError( $error, $tokens[ $stackPtr ]['comment_closer'], $code );
219 | }
220 | else {
221 | $fix = $phpcsFile->addFixableError( $error, $tokens[ $stackPtr ]['comment_closer'], $code );
222 | if ( $fix === true ) {
223 | $skipFirst = ( $coversNothingCount === $removalCount );
224 |
225 | $phpcsFile->fixer->beginChangeset();
226 |
227 | foreach ( $removeTags as $key => $ptr ) {
228 | if ( $skipFirst === true && $key === 0 ) {
229 | // Let the first one remain if none of the tags has a comment.
230 | continue;
231 | }
232 |
233 | // Remove the whole line.
234 | for ( $i = ( $ptr + 1 ); $i >= 0; $i-- ) {
235 | if ( $tokens[ $i ]['line'] !== $tokens[ $ptr ]['line'] ) {
236 | break;
237 | }
238 |
239 | $phpcsFile->fixer->replaceToken( $i, '' );
240 | }
241 | }
242 |
243 | $phpcsFile->fixer->endChangeset();
244 | }
245 | }
246 | }
247 |
248 | /*
249 | * Check for duplicate `@covers ...` tags.
250 | */
251 | $coversCount = \count( $coversTags );
252 | if ( $coversCount > 1 ) {
253 | $unique = \array_unique( $coversTags );
254 | if ( \count( $unique ) !== $coversCount ) {
255 | $value_count = \array_count_values( $coversTags );
256 | $error = 'Duplicate @covers tag found. First tag with the same annotation encountered on line %d';
257 | $code = 'DuplicateCovers';
258 | foreach ( $value_count as $annotation => $count ) {
259 | if ( $count < 2 ) {
260 | continue;
261 | }
262 |
263 | $first = null;
264 | $data = [];
265 | foreach ( $coversTags as $ptrs => $annot ) {
266 | if ( $annotation !== $annot ) {
267 | continue;
268 | }
269 |
270 | if ( $first === null ) {
271 | $first = \explode( '-', $ptrs );
272 | $data = [ $tokens[ $first[0] ]['line'] ];
273 | continue;
274 | }
275 |
276 | $ptrs = \explode( '-', $ptrs );
277 |
278 | $fix = $phpcsFile->addFixableError( $error, (int) $ptrs[0], $code, $data );
279 | if ( $fix === true ) {
280 | $phpcsFile->fixer->beginChangeset();
281 |
282 | // Remove the whole line.
283 | for ( $i = (int) $ptrs[1]; $i >= 0; $i-- ) {
284 | if ( $tokens[ $i ]['line'] !== $tokens[ $ptrs[1] ]['line'] ) {
285 | if ( $tokens[ $i ]['code'] === \T_DOC_COMMENT_WHITESPACE
286 | && $tokens[ $i ]['content'] === $phpcsFile->eolChar
287 | ) {
288 | $phpcsFile->fixer->replaceToken( $i, '' );
289 | }
290 | break;
291 | }
292 |
293 | $phpcsFile->fixer->replaceToken( $i, '' );
294 | }
295 |
296 | $phpcsFile->fixer->endChangeset();
297 | }
298 | }
299 | }
300 | }
301 | }
302 | }
303 |
304 | /**
305 | * Add a fixable error if a suitable alternative is available.
306 | *
307 | * @param File $phpcsFile The file being scanned.
308 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
309 | * @param string $expected The expected alternative annotation.
310 | * This annotation might not be valid itself.
311 | * @param string $errorCode The error code.
312 | *
313 | * @return bool Whether an error has been thrown or not.
314 | */
315 | private function fixSimpleError( File $phpcsFile, $stackPtr, $expected, $errorCode ) {
316 | $tokens = $phpcsFile->getTokens();
317 | $annotation = $tokens[ $stackPtr ]['content'];
318 |
319 | if ( $expected === $annotation
320 | || \preg_match( '`^' . self::VALID_CONTENT_REGEX . '$`', $expected ) !== 1
321 | ) {
322 | return false;
323 | }
324 |
325 | $error = self::ERROR_MSG . "\nExpected: `%s`\nFound: `%s`";
326 | $data = [
327 | $expected,
328 | $annotation,
329 | ];
330 |
331 | $fix = $phpcsFile->addFixableError( $error, $stackPtr, $errorCode, $data );
332 | if ( $fix === true ) {
333 | $phpcsFile->fixer->replaceToken( $stackPtr, $expected );
334 | }
335 |
336 | return true;
337 | }
338 |
339 | /**
340 | * Add a fixable error for a union/intersect @covers annotation.
341 | *
342 | * @param File $phpcsFile The file being scanned.
343 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
344 | * @param string $errorCode The error code.
345 | * @param string $separator The separator to split the annotation on.
346 | *
347 | * @return bool Whether to skip the rest of the annotation examination or not.
348 | */
349 | private function fixAnnotationToSplit( File $phpcsFile, $stackPtr, $errorCode, $separator ) {
350 | $fix = $phpcsFile->addFixableError(
351 | 'Each @covers annotation should reference only one covered structure',
352 | $stackPtr,
353 | $errorCode
354 | );
355 |
356 | if ( $fix === true ) {
357 | $tokens = $phpcsFile->getTokens();
358 | $annotation = $tokens[ $stackPtr ]['content'];
359 | $annotations = \explode( $separator, $annotation );
360 | // @phpstan-ignore argument.type (explode will never return `false` as it is never given an empty string separator.)
361 | $annotations = \array_map( 'trim', $annotations );
362 | $annotations = \array_filter( $annotations ); // Remove empties.
363 |
364 | $phpcsFile->fixer->beginChangeset();
365 | $phpcsFile->fixer->replaceToken( $stackPtr, '' );
366 |
367 | for ( $i = ( $stackPtr - 1 ); $i >= 0; $i-- ) {
368 | if ( $tokens[ $i ]['line'] !== $tokens[ $stackPtr ]['line'] ) {
369 | break;
370 | }
371 |
372 | $phpcsFile->fixer->replaceToken( $i, '' );
373 | }
374 |
375 | $stub = GetTokensAsString::origContent( $phpcsFile, $i, ( $stackPtr - 1 ) );
376 | $replacement = '';
377 | foreach ( $annotations as $annotation ) {
378 | $replacement .= $stub . $annotation;
379 | }
380 |
381 | $phpcsFile->fixer->replaceToken( $i, $replacement );
382 | $phpcsFile->fixer->endChangeset();
383 |
384 | return true;
385 | }
386 |
387 | return false;
388 | }
389 | }
390 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/Commenting/FileCommentSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
45 |
46 | /*
47 | * Check if the file is namespaced. If not, fall through to the parent sniff.
48 | */
49 | $namespace_token = $stackPtr;
50 | do {
51 | $namespace_token = $phpcsFile->findNext( Tokens::$emptyTokens, ( $namespace_token + 1 ), null, true );
52 | if ( $namespace_token === false ) {
53 | // No non-empty token found, fall through to parent sniff.
54 | return parent::process( $phpcsFile, $stackPtr );
55 | }
56 |
57 | if ( $tokens[ $namespace_token ]['code'] === \T_DECLARE ) {
58 | // Declare statement found. Find the end of it and skip over it.
59 | $end = $phpcsFile->findNext( [ \T_SEMICOLON, \T_OPEN_CURLY_BRACKET ], ( $namespace_token + 1 ), null, false, null, true );
60 |
61 | if ( $end !== false ) {
62 | $namespace_token = $end;
63 | }
64 |
65 | continue;
66 | }
67 |
68 | if ( $tokens[ $namespace_token ]['code'] !== \T_NAMESPACE ) {
69 | // No namespace found, fall through to parent sniff.
70 | return parent::process( $phpcsFile, $stackPtr );
71 | }
72 |
73 | // Stop searching if the next non-empty token wasn't a namespace token.
74 | break;
75 | } while ( true );
76 |
77 | $next_non_empty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $namespace_token + 1 ), null, true );
78 | if ( $next_non_empty === false
79 | || $tokens[ $next_non_empty ]['code'] === \T_SEMICOLON
80 | || $tokens[ $next_non_empty ]['code'] === \T_OPEN_CURLY_BRACKET
81 | || $tokens[ $next_non_empty ]['code'] === \T_NS_SEPARATOR
82 | ) {
83 | // Either live coding, global namespace (i.e. not really namespaced) or namespace operator.
84 | // Fall through to parent sniff.
85 | return parent::process( $phpcsFile, $stackPtr );
86 | }
87 |
88 | /*
89 | * As of here, we know we are in a namespaced file.
90 | */
91 |
92 | $comment_start = $phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), $namespace_token, true );
93 |
94 | if ( $comment_start === false || $tokens[ $comment_start ]['code'] !== \T_DOC_COMMENT_OPEN_TAG ) {
95 | // No file comment found, we're good.
96 | return $phpcsFile->numTokens;
97 | }
98 |
99 | // Respect phpcs:disable comments in the file docblock.
100 | if ( $phpcsFile->config->annotations === true && isset( $tokens[ $comment_start ]['comment_closer'] ) ) {
101 | for ( $i = ( $comment_start + 1 ); $i < $tokens[ $comment_start ]['comment_closer']; $i++ ) {
102 | if ( $tokens[ $i ]['code'] !== \T_PHPCS_DISABLE ) {
103 | continue;
104 | }
105 |
106 | if ( empty( $tokens[ $i ]['sniffCodes'] ) === true
107 | || isset( $tokens[ $i ]['sniffCodes']['Yoast'] ) === true
108 | || isset( $tokens[ $i ]['sniffCodes']['Yoast.Commenting'] ) === true
109 | || isset( $tokens[ $i ]['sniffCodes']['Yoast.Commenting.FileComment'] ) === true
110 | || isset( $tokens[ $i ]['sniffCodes']['Yoast.Commenting.FileComment.Unnecessary'] ) === true
111 | ) {
112 | // Applicable disable annotation found.
113 | return $phpcsFile->numTokens;
114 | }
115 | }
116 | }
117 |
118 | /*
119 | * Okay, so we have a file docblock in a namespaced file, now check if there is a named
120 | * OO structure declaration.
121 | */
122 | $find = Tokens::$ooScopeTokens;
123 | unset( $find[ \T_ANON_CLASS ] );
124 | $hasOODeclaration = $phpcsFile->findNext( $find, $next_non_empty );
125 | if ( $hasOODeclaration === false ) {
126 | /*
127 | * This is a file which doesn't contain (just) an OO declaration.
128 | * If there is a file docblock, allow it and check it like any other file docblock.
129 | */
130 | return parent::process( $phpcsFile, $stackPtr );
131 | }
132 |
133 | $phpcsFile->addWarning(
134 | 'A file containing a (named) namespace declaration does not need a file docblock',
135 | $comment_start,
136 | 'Unnecessary'
137 | );
138 |
139 | return $phpcsFile->numTokens;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/Commenting/TestsHaveCoversTagSniff.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | public function register() {
26 | return [
27 | \T_CLASS,
28 | \T_FUNCTION,
29 | ];
30 | }
31 |
32 | /**
33 | * Processes this test, when one of its tokens is encountered.
34 | *
35 | * @param File $phpcsFile The file being scanned.
36 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
37 | *
38 | * @return int|void Optionally returns a stack pointer. This sniff will not be
39 | * called again on the current file until the returned stack
40 | * pointer is reached.
41 | */
42 | public function process( File $phpcsFile, $stackPtr ) {
43 |
44 | $tokens = $phpcsFile->getTokens();
45 |
46 | if ( $tokens[ $stackPtr ]['code'] === \T_CLASS ) {
47 | return $this->process_class( $phpcsFile, $stackPtr );
48 | }
49 |
50 | // This must be a T_FUNCTION token.
51 | $this->process_function( $phpcsFile, $stackPtr );
52 | }
53 |
54 | /**
55 | * Processes the docblock for a class token.
56 | *
57 | * @param File $phpcsFile The file being scanned.
58 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
59 | *
60 | * @return int|void If covers annotations were found (or this is not a test class),
61 | * will return the stack pointer to the end of the class.
62 | */
63 | private function process_class( File $phpcsFile, $stackPtr ) {
64 | $tokens = $phpcsFile->getTokens();
65 | $name = ObjectDeclarations::getName( $phpcsFile, $stackPtr );
66 |
67 | if ( empty( $name )
68 | || ( \substr( $name, -4 ) !== 'Test'
69 | && \substr( $name, -8 ) !== 'TestCase'
70 | && \substr( $name, 0, 4 ) !== 'Test' )
71 | ) {
72 | // Not a test class.
73 | if ( isset( $tokens[ $stackPtr ]['scope_closer'] ) ) {
74 | // No need to examine the methods in this class.
75 | return $tokens[ $stackPtr ]['scope_closer'];
76 | }
77 |
78 | return;
79 | }
80 |
81 | // @todo: Once PHPCSUtils 1.2.0 (?) is out, replace with call to new findCommentAboveOOStructure() method.
82 | $ignore = Collections::classModifierKeywords();
83 | $ignore[ \T_WHITESPACE ] = \T_WHITESPACE;
84 |
85 | $commentEnd = $stackPtr;
86 | for ( $commentEnd = ( $stackPtr - 1 ); $commentEnd >= 0; $commentEnd-- ) {
87 | if ( isset( $ignore[ $tokens[ $commentEnd ]['code'] ] ) === true ) {
88 | continue;
89 | }
90 |
91 | if ( $tokens[ $commentEnd ]['code'] === \T_ATTRIBUTE_END
92 | && isset( $tokens[ $commentEnd ]['attribute_opener'] ) === true
93 | ) {
94 | $commentEnd = $tokens[ $commentEnd ]['attribute_opener'];
95 | continue;
96 | }
97 |
98 | break;
99 | }
100 |
101 | if ( $tokens[ $commentEnd ]['code'] !== \T_DOC_COMMENT_CLOSE_TAG ) {
102 | // Class without (proper) docblock. Not our concern.
103 | return;
104 | }
105 |
106 | $commentStart = $tokens[ $commentEnd ]['comment_opener'];
107 |
108 | $foundCovers = false;
109 | $foundCoversNothing = false;
110 | foreach ( $tokens[ $commentStart ]['comment_tags'] as $tag ) {
111 | if ( $tokens[ $tag ]['content'] === '@covers' ) {
112 | $foundCovers = true;
113 | break;
114 | }
115 |
116 | if ( $tokens[ $tag ]['content'] === '@coversNothing' ) {
117 | $foundCoversNothing = true;
118 | break;
119 | }
120 | }
121 |
122 | if ( $foundCovers === true || $foundCoversNothing === true ) {
123 | // Class level tags found, valid for all methods. No need to examine the individual methods.
124 | if ( isset( $tokens[ $stackPtr ]['scope_closer'] ) ) {
125 | return $tokens[ $stackPtr ]['scope_closer'];
126 | }
127 | }
128 | }
129 |
130 | /**
131 | * Processes the docblock for a function token.
132 | *
133 | * @param File $phpcsFile The file being scanned.
134 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
135 | *
136 | * @return void
137 | */
138 | private function process_function( File $phpcsFile, $stackPtr ) {
139 | $tokens = $phpcsFile->getTokens();
140 |
141 | if ( Scopes::isOOMethod( $phpcsFile, $stackPtr ) === false ) {
142 | // This is a global function, not a method in a test class.
143 | return;
144 | }
145 |
146 | // @todo: Once PHPCSUtils 1.2.0 (?) is out, replace with call to new findCommentAboveFunction() method.
147 | $ignore = Tokens::$methodPrefixes;
148 | $ignore[ \T_WHITESPACE ] = \T_WHITESPACE;
149 |
150 | $commentEnd = $stackPtr;
151 | for ( $commentEnd = ( $stackPtr - 1 ); $commentEnd >= 0; $commentEnd-- ) {
152 | if ( isset( $ignore[ $tokens[ $commentEnd ]['code'] ] ) === true ) {
153 | continue;
154 | }
155 |
156 | if ( $tokens[ $commentEnd ]['code'] === \T_ATTRIBUTE_END
157 | && isset( $tokens[ $commentEnd ]['attribute_opener'] ) === true
158 | ) {
159 | $commentEnd = $tokens[ $commentEnd ]['attribute_opener'];
160 | continue;
161 | }
162 |
163 | break;
164 | }
165 |
166 | $foundTest = false;
167 | $foundCovers = false;
168 | $foundCoversNothing = false;
169 |
170 | if ( $tokens[ $commentEnd ]['code'] === \T_DOC_COMMENT_CLOSE_TAG ) {
171 | $commentStart = $tokens[ $commentEnd ]['comment_opener'];
172 |
173 | foreach ( $tokens[ $commentStart ]['comment_tags'] as $tag ) {
174 | if ( $tokens[ $tag ]['content'] === '@test' ) {
175 | $foundTest = true;
176 | continue;
177 | }
178 |
179 | if ( $tokens[ $tag ]['content'] === '@covers' ) {
180 | $foundCovers = true;
181 | continue;
182 | }
183 |
184 | if ( $tokens[ $tag ]['content'] === '@coversNothing' ) {
185 | $foundCoversNothing = true;
186 | continue;
187 | }
188 | }
189 | }
190 |
191 | $name = FunctionDeclarations::getName( $phpcsFile, $stackPtr );
192 | if ( empty( $name ) ) {
193 | // Parse error. Ignore this method as it will never be run as a test.
194 | return;
195 | }
196 |
197 | if ( \stripos( $name, 'test' ) !== 0 && $foundTest === false ) {
198 | // Not a test method.
199 | return;
200 | }
201 |
202 | $method_props = FunctionDeclarations::getProperties( $phpcsFile, $stackPtr );
203 | if ( $method_props['is_abstract'] === true ) {
204 | // Abstract test method, not implemented.
205 | return;
206 | }
207 |
208 | if ( $foundCovers === true || $foundCoversNothing === true ) {
209 | // Docblock contains one or more @covers tags.
210 | return;
211 | }
212 |
213 | $msg = 'Each test function should have at least one @covers tag annotating which class/method/function is being tested.';
214 | $data = [ $name ];
215 |
216 | if ( $tokens[ $commentEnd ]['code'] === \T_DOC_COMMENT_CLOSE_TAG ) {
217 | $msg .= ' Tag missing for function %s().';
218 | $code = 'Missing';
219 | }
220 | else {
221 | $msg .= ' Test function %s() does not have a docblock with a @covers tag.';
222 | $code = 'NoDocblock';
223 | }
224 |
225 | $phpcsFile->addError( $msg, $stackPtr, $code, $data );
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/Files/FileNameSniff.php:
--------------------------------------------------------------------------------
1 |
41 | */
42 | private const NAMED_OO_TOKENS = [
43 | \T_CLASS,
44 | \T_ENUM,
45 | \T_INTERFACE,
46 | \T_TRAIT,
47 | ];
48 |
49 | /**
50 | * List of prefixes for object structures.
51 | *
52 | * These prefixes do not need to be reflected in the file name.
53 | *
54 | * Note:
55 | * - Prefixes are matched in a case-insensitive manner.
56 | * - When several overlapping prefixes match, the longest matching prefix
57 | * will be removed.
58 | *
59 | * @var array
60 | */
61 | public $oo_prefixes = [];
62 |
63 | /**
64 | * List of files to exclude from the strict file name check.
65 | *
66 | * This is used primarily to prevent the sniff from recommending that the
67 | * name of the plugin "main" file should be changed as changing that would
68 | * deactivate the plugin on upgrade and is therefore not a good idea.
69 | *
70 | * File names should be provided including the path to the file relative
71 | * to the "basepath" known to PHPCS.
72 | * File names should not be prefixed with a directory separator.
73 | * The list should be provided as a PHPCS array list.
74 | *
75 | * For this functionality to work with relative file paths - i.e. file paths
76 | * from the root of the repository - , the PHPCS `--basepath` config variable
77 | * needs to be set. If it is not, a warning will be issued.
78 | *
79 | * @var array
80 | */
81 | public $excluded_files_strict_check = [];
82 |
83 | /**
84 | * Cache of previously set OO prefixes.
85 | *
86 | * Prevents having to do the same prefix validation over and over again.
87 | *
88 | * @var array
89 | */
90 | private $previous_oo_prefixes = [];
91 |
92 | /**
93 | * Validated & cleaned up OO set prefixes.
94 | *
95 | * @var array
96 | */
97 | private $clean_oo_prefixes = [];
98 |
99 | /**
100 | * Cache of previously set list of excluded files.
101 | *
102 | * Prevents having to do the same file validation over and over again.
103 | *
104 | * @var array
105 | */
106 | private $previous_excluded_files = [];
107 |
108 | /**
109 | * Validated & cleaned up list of absolute paths to the excluded files.
110 | *
111 | * @var array Both the key and the value will be the same absolute path.
112 | */
113 | private $validated_excluded_files = [];
114 |
115 | /**
116 | * Track if the "missing basepath" warning has been thrown.
117 | *
118 | * This prevents this warning potentially being thrown for every single file in a PHPCS run.
119 | *
120 | * @var bool
121 | */
122 | private $basepath_warning_thrown = false;
123 |
124 | /**
125 | * Returns an array of tokens this test wants to listen for.
126 | *
127 | * @return array
128 | */
129 | public function register() {
130 | return Collections::phpOpenTags();
131 | }
132 |
133 | /**
134 | * Processes this test, when one of its tokens is encountered.
135 | *
136 | * @param File $phpcsFile The file being scanned.
137 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
138 | *
139 | * @return int StackPtr to the end of the file, this sniff needs to only
140 | * check each file once.
141 | */
142 | public function process( File $phpcsFile, $stackPtr ) {
143 | // Stripping potential quotes to ensure `stdin_path` passed by IDEs does not include quotes.
144 | $file = TextStrings::stripQuotes( $phpcsFile->getFileName() );
145 |
146 | if ( $file === 'STDIN' ) {
147 | return $phpcsFile->numTokens; // @codeCoverageIgnore
148 | }
149 |
150 | // Respect phpcs:disable comments as long as they are not accompanied by an enable.
151 | $tokens = $phpcsFile->getTokens();
152 | $i = -1;
153 | while ( $i = $phpcsFile->findNext( \T_PHPCS_DISABLE, ( $i + 1 ) ) ) {
154 | if ( empty( $tokens[ $i ]['sniffCodes'] )
155 | || isset( $tokens[ $i ]['sniffCodes']['Yoast'] )
156 | || isset( $tokens[ $i ]['sniffCodes']['Yoast.Files'] )
157 | || isset( $tokens[ $i ]['sniffCodes']['Yoast.Files.FileName'] )
158 | ) {
159 | do {
160 | $i = $phpcsFile->findNext( \T_PHPCS_ENABLE, ( $i + 1 ) );
161 | } while ( $i !== false
162 | && ! empty( $tokens[ $i ]['sniffCodes'] )
163 | && ! isset( $tokens[ $i ]['sniffCodes']['Yoast'] )
164 | && ! isset( $tokens[ $i ]['sniffCodes']['Yoast.Files'] )
165 | && ! isset( $tokens[ $i ]['sniffCodes']['Yoast.Files.FileName'] )
166 | );
167 |
168 | if ( $i === false ) {
169 | // The entire (rest of the) file is disabled.
170 | return $phpcsFile->numTokens;
171 | }
172 | }
173 | }
174 |
175 | $path_info = \pathinfo( $file );
176 |
177 | // Basename = filename + extension.
178 | $basename = '';
179 | if ( ! empty( $path_info['basename'] ) ) {
180 | $basename = $path_info['basename'];
181 | }
182 |
183 | $file_name = '';
184 | if ( ! empty( $path_info['filename'] ) ) {
185 | $file_name = $path_info['filename'];
186 | }
187 |
188 | $extension = '';
189 | if ( ! empty( $path_info['extension'] ) ) {
190 | $extension = $path_info['extension'];
191 | }
192 |
193 | $error = 'Filenames should be all lowercase with hyphens as word separators.';
194 | $error_code = 'NotHyphenatedLowercase';
195 | $expected = \strtolower( \preg_replace( '`[[:punct:]]`', '-', $file_name ) );
196 |
197 | if ( ! isset( $phpcsFile->config->basepath ) ) {
198 | $this->add_missing_basepath_warning( $phpcsFile );
199 | }
200 |
201 | $oo_structure = $phpcsFile->findNext( self::NAMED_OO_TOKENS, $stackPtr );
202 | if ( $oo_structure !== false ) {
203 |
204 | $oo_name = ObjectDeclarations::getName( $phpcsFile, $oo_structure );
205 |
206 | if ( \is_string( $oo_name ) && $oo_name !== '' ) {
207 |
208 | if ( $this->is_in_psr4_path( $phpcsFile, $file ) ) {
209 | $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.';
210 | $error_code = 'InvalidPSR4FileName';
211 | $expected = $oo_name;
212 | }
213 | elseif ( $this->is_file_excluded( $phpcsFile, $file ) === false ) {
214 | $this->validate_oo_prefixes();
215 | if ( ! empty( $this->clean_oo_prefixes ) ) {
216 | foreach ( $this->clean_oo_prefixes as $prefix ) {
217 | if ( $oo_name !== $prefix && \stripos( $oo_name, $prefix ) === 0 ) {
218 | $oo_name = \substr( $oo_name, \strlen( $prefix ) );
219 | $oo_name = \ltrim( $oo_name, '_-' );
220 | break;
221 | }
222 | }
223 | }
224 |
225 | $expected = \strtolower( \str_replace( '_', '-', $oo_name ) );
226 |
227 | switch ( $tokens[ $oo_structure ]['code'] ) {
228 | case \T_CLASS:
229 | $error = 'Class file names should be based on the class name without the plugin prefix.';
230 | $error_code = 'InvalidClassFileName';
231 | break;
232 |
233 | // Interfaces, traits, enums.
234 | default:
235 | $oo_type = \strtolower( $tokens[ $oo_structure ]['content'] );
236 | $oo_type_ucfirst = \ucfirst( $oo_type );
237 |
238 | $error = \sprintf(
239 | '%1$s file names should be based on the %2$s name without the plugin prefix and should have "-%2$s" as a suffix.',
240 | $oo_type_ucfirst,
241 | $oo_type
242 | );
243 | $error_code = \sprintf( 'Invalid%sFileName', $oo_type_ucfirst );
244 |
245 | // Don't duplicate "interface/trait/enum" in the filename.
246 | $expected_suffix = '-' . $oo_type;
247 | if ( \substr( $expected, -\strlen( $expected_suffix ) ) !== $expected_suffix ) {
248 | $expected .= $expected_suffix;
249 | }
250 | break;
251 | }
252 | }
253 | }
254 | }
255 | elseif ( $this->is_file_excluded( $phpcsFile, $file ) === false ) {
256 | $has_function = $phpcsFile->findNext( \T_FUNCTION, $stackPtr );
257 | if ( $has_function !== false && $file_name !== 'functions' ) {
258 | $error = 'Files containing function declarations should have "-functions" as a suffix.';
259 | $error_code = 'InvalidFunctionsFileName';
260 |
261 | if ( \substr( $expected, -10 ) !== '-functions' ) {
262 | $expected .= '-functions';
263 | }
264 | }
265 | }
266 |
267 | // Throw the error.
268 | if ( $expected !== '' && $file_name !== $expected ) {
269 | $phpcsFile->addError(
270 | $error . ' Expected "%s", but found "%s".',
271 | 0,
272 | $error_code,
273 | [
274 | $expected . '.' . $extension,
275 | $basename,
276 | ]
277 | );
278 | }
279 |
280 | // Only run this sniff once per file, no need to run it again.
281 | return $phpcsFile->numTokens;
282 | }
283 |
284 | /**
285 | * Check if the file is on the exclude list.
286 | *
287 | * @param File $phpcsFile The file being scanned.
288 | * @param string $path_to_file The full path to the file currently being examined.
289 | *
290 | * @return bool
291 | */
292 | private function is_file_excluded( File $phpcsFile, $path_to_file ) {
293 | $this->validate_excluded_files( $phpcsFile );
294 | if ( empty( $this->validated_excluded_files ) ) {
295 | return false;
296 | }
297 |
298 | $path_to_file = PathHelper::normalize_absolute_path( $path_to_file );
299 |
300 | return isset( $this->validated_excluded_files[ $path_to_file ] );
301 | }
302 |
303 | /**
304 | * Clean a custom array property received from a ruleset.
305 | *
306 | * Deals with incorrectly passed custom array properties.
307 | * - Remove whitespace surrounding values.
308 | * - Remove empty array entries.
309 | *
310 | * @param array $property The current property value.
311 | *
312 | * @return array
313 | */
314 | private function clean_custom_array_property( $property ) {
315 | return \array_filter( \array_map( 'trim', $property ) );
316 | }
317 |
318 | /**
319 | * Validate and sort the OO prefixes passed from a custom ruleset.
320 | *
321 | * This will only need to be done once in a normal PHPCS run, though for
322 | * tests the function may be called multiple times.
323 | *
324 | * @return void
325 | */
326 | private function validate_oo_prefixes() {
327 | if ( $this->previous_oo_prefixes === $this->oo_prefixes ) {
328 | return;
329 | }
330 |
331 | // Set the cache *before* validation so as to not break the above compare.
332 | $this->previous_oo_prefixes = $this->oo_prefixes;
333 |
334 | $this->clean_oo_prefixes = $this->clean_custom_array_property( $this->oo_prefixes );
335 |
336 | if ( ! empty( $this->clean_oo_prefixes ) ) {
337 | // Use reverse natural sorting to get the longest of overlapping prefixes first.
338 | \rsort( $this->clean_oo_prefixes, ( \SORT_NATURAL | \SORT_FLAG_CASE ) );
339 | }
340 | }
341 |
342 | /**
343 | * Validate the list of excluded files passed from a custom ruleset.
344 | *
345 | * This will only need to be done once in a normal PHPCS run, though for
346 | * tests the function may be called multiple times.
347 | *
348 | * @param File $phpcsFile The file being scanned.
349 | *
350 | * @return void
351 | */
352 | private function validate_excluded_files( $phpcsFile ) {
353 | // The basepath check needs to be done first as otherwise the previous/current comparison would be broken.
354 | if ( ! isset( $phpcsFile->config->basepath ) ) {
355 | // Only relevant for the tests: make sure previously set validated paths are cleared out.
356 | $this->validated_excluded_files = [];
357 |
358 | // No use continuing as we can't turn relative paths into absolute paths.
359 | return;
360 | }
361 |
362 | if ( $this->previous_excluded_files === $this->excluded_files_strict_check ) {
363 | return;
364 | }
365 |
366 | // Set the cache *before* validation so as to not break the above compare.
367 | $this->previous_excluded_files = $this->excluded_files_strict_check;
368 |
369 | $absolute_paths = PathValidationHelper::relative_to_absolute( $phpcsFile, $this->excluded_files_strict_check );
370 | $absolute_paths = \array_unique( $absolute_paths );
371 | $absolute_paths = \array_values( $absolute_paths );
372 |
373 | $this->validated_excluded_files = \array_combine( $absolute_paths, $absolute_paths );
374 | }
375 |
376 | /**
377 | * Throw a warning if the basepath is missing (and this warning hasn't been thrown before).
378 | *
379 | * @param File $phpcsFile The file being scanned.
380 | *
381 | * @return void
382 | */
383 | private function add_missing_basepath_warning( File $phpcsFile ) {
384 | if ( $this->basepath_warning_thrown === true ) {
385 | return;
386 | }
387 |
388 | $phpcsFile->addWarning(
389 | 'For the excluded files and the psr4 paths properties to work with relative file paths, the --basepath needs to be set.',
390 | 0,
391 | 'MissingBasePath'
392 | );
393 |
394 | $this->basepath_warning_thrown = true;
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/Files/TestDoublesSniff.php:
--------------------------------------------------------------------------------
1 |
37 | */
38 | public $doubles_path = [];
39 |
40 | /**
41 | * Validated absolute target paths for test fixture directories or an empty array
42 | * if the intended target directory/directories don't exist.
43 | *
44 | * @var array
45 | */
46 | private $target_paths;
47 |
48 | /**
49 | * Returns an array of tokens this test wants to listen for.
50 | *
51 | * @return array
52 | */
53 | public function register() {
54 | $targets = Tokens::$ooScopeTokens;
55 | unset( $targets[ \T_ANON_CLASS ] );
56 |
57 | return $targets;
58 | }
59 |
60 | /**
61 | * Processes this test, when one of its tokens is encountered.
62 | *
63 | * @param File $phpcsFile The file being scanned.
64 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
65 | *
66 | * @return int|void Void or $stackPtr to the end of the file if no basepath was set
67 | * or no valid doubles_path(s) were found.
68 | */
69 | public function process( File $phpcsFile, $stackPtr ) {
70 | // Stripping potential quotes to ensure `stdin_path` passed by IDEs does not include quotes.
71 | $file = TextStrings::stripQuotes( $phpcsFile->getFileName() );
72 |
73 | if ( $file === 'STDIN' ) {
74 | return $phpcsFile->numTokens; // @codeCoverageIgnore
75 | }
76 |
77 | if ( ! isset( $phpcsFile->config->basepath ) ) {
78 | $phpcsFile->addWarning(
79 | 'For the TestDoubles sniff to be able to function, the --basepath needs to be set.',
80 | 0,
81 | 'MissingBasePath'
82 | );
83 |
84 | return $phpcsFile->numTokens;
85 | }
86 |
87 | if ( empty( $this->doubles_path ) ) {
88 | // Just in case someone would overrule the property with an empty value.
89 | $phpcsFile->addWarning(
90 | 'Required property "doubles_path" missing. Please edit your custom ruleset to add the property.',
91 | 0,
92 | 'NoDoublesPathProperty'
93 | );
94 |
95 | return $phpcsFile->numTokens;
96 | }
97 |
98 | if ( ! isset( $this->target_paths ) || \defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) {
99 | $this->target_paths = PathValidationHelper::relative_to_absolute( $phpcsFile, $this->doubles_path );
100 | $this->target_paths = \array_filter( $this->target_paths, 'file_exists' );
101 | $this->target_paths = \array_filter( $this->target_paths, 'is_dir' );
102 | }
103 |
104 | $object_name = ObjectDeclarations::getName( $phpcsFile, $stackPtr );
105 | if ( empty( $object_name ) ) {
106 | return;
107 | }
108 |
109 | $name_contains_double_or_mock = false;
110 | if ( \stripos( $object_name, 'mock' ) !== false || \stripos( $object_name, 'double' ) !== false ) {
111 | $name_contains_double_or_mock = true;
112 | }
113 |
114 | $tokens = $phpcsFile->getTokens();
115 | if ( empty( $this->target_paths ) === true ) {
116 | if ( $name_contains_double_or_mock === false ) {
117 | return;
118 | }
119 |
120 | // Mock/Double class found, but no valid target paths found.
121 | $data = [
122 | $tokens[ $stackPtr ]['content'],
123 | $phpcsFile->config->basepath,
124 | ];
125 |
126 | if ( \count( $this->doubles_path ) === 1 ) {
127 | $data[] = 'directory';
128 | $data[] = \implode( '', $this->doubles_path );
129 | }
130 | else {
131 | $all_paths = \implode( '", "', $this->doubles_path );
132 | $all_paths = \substr_replace( $all_paths, ' and', \strrpos( $all_paths, ',' ), 1 );
133 |
134 | $data[] = 'directories';
135 | $data[] = $all_paths;
136 | }
137 |
138 | $phpcsFile->addError(
139 | 'Double/Mock test helper %1$s detected, but no test fixtures sub-%3$s found in "%2$s". Expected: "%4$s". Please create the sub-%3$s.',
140 | $stackPtr,
141 | 'NoDoublesDirectory',
142 | $data
143 | );
144 |
145 | return;
146 | }
147 |
148 | $path_to_file = PathHelper::normalize_absolute_path( $file );
149 | $is_double_dir = false;
150 |
151 | foreach ( $this->target_paths as $target_path ) {
152 | if ( PathHelper::path_starts_with( $path_to_file, $target_path ) === true ) {
153 | $is_double_dir = true;
154 | break;
155 | }
156 | }
157 |
158 | $data = [
159 | $tokens[ $stackPtr ]['content'],
160 | $object_name,
161 | ];
162 |
163 | if ( $name_contains_double_or_mock === true && $is_double_dir === false ) {
164 | $phpcsFile->addError(
165 | 'Double/Mock test helpers should be placed in a dedicated test fixtures sub-directory. Found %s: %s',
166 | $stackPtr,
167 | 'WrongDirectory',
168 | $data
169 | );
170 | return;
171 | }
172 |
173 | if ( $name_contains_double_or_mock === false && $is_double_dir === true ) {
174 | $phpcsFile->addError(
175 | 'Double/Mock test helpers should contain "Double" or "Mock" in the class name. Found %s: %s',
176 | $stackPtr,
177 | 'InvalidClassName',
178 | $data
179 | );
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/NamingConventions/NamespaceNameSniff.php:
--------------------------------------------------------------------------------
1 | Key is the subdirectory name, value the length of that name.
42 | */
43 | private const DOUBLE_DIRS = [
44 | '\Doubles\\' => 9,
45 | '\Mocks\\' => 7,
46 | '\Fixtures\\' => 10,
47 | ];
48 |
49 | /**
50 | * Project root(s).
51 | *
52 | * When the _real_ project root as set in `$basepath` is not the
53 | * starting point for the translation between directories and levels,
54 | * one or more sub-directories of the project root can be indicated
55 | * as starting points for the translation.
56 | *
57 | * @var array
58 | */
59 | public $src_directory = [];
60 |
61 | /**
62 | * Maximum number of levels.
63 | *
64 | * The maximum number of sub-levels a namespace name should consist of, each
65 | * separated by a namespace separator.
66 | *
67 | * If the name consists of more sub-levels, an ERROR will be thrown.
68 | *
69 | * @var int
70 | */
71 | public $max_levels = 3;
72 |
73 | /**
74 | * Recommended maximum number of levels.
75 | *
76 | * The recommended maximum number of sub-levels a namespace name should consist of, each
77 | * separated by a namespace separator.
78 | *
79 | * If the name consists of more sub-levels, a WARNING will be thrown.
80 | *
81 | * @var int
82 | */
83 | public $recommended_max_levels = 2;
84 |
85 | /**
86 | * Project roots after validation.
87 | *
88 | * Validated src_directories will look like "$basepath/src/", i.e.:
89 | * - absolute paths;
90 | * - with linux slashes;
91 | * - and a trailing slash.
92 | *
93 | * @var array
94 | */
95 | private $validated_src_directory;
96 |
97 | /**
98 | * Cache of previously set project roots.
99 | *
100 | * Prevents having to do the same validation over and over again.
101 | *
102 | * @var array
103 | */
104 | private $previous_src_directory;
105 |
106 | /**
107 | * Returns an array of tokens this test wants to listen for.
108 | *
109 | * @return array
110 | */
111 | public function register() {
112 | return [ \T_NAMESPACE ];
113 | }
114 |
115 | /**
116 | * Filter out all prefixes which don't have namespace separators.
117 | *
118 | * @param array $prefixes The unvalidated prefixes.
119 | *
120 | * @return array
121 | */
122 | protected function filter_prefixes( $prefixes ) {
123 | return $this->filter_allow_only_namespace_prefixes( $prefixes );
124 | }
125 |
126 | /**
127 | * Processes this test, when one of its tokens is encountered.
128 | *
129 | * @param File $phpcsFile The file being scanned.
130 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
131 | *
132 | * @return void
133 | */
134 | public function process( File $phpcsFile, $stackPtr ) {
135 |
136 | $namespace_name = Namespaces::getDeclaredName( $phpcsFile, $stackPtr );
137 | if ( empty( $namespace_name ) ) {
138 | // Either not a namespace declaration or global namespace.
139 | return;
140 | }
141 |
142 | // Stripping potential quotes to ensure `stdin_path` passed by IDEs does not include quotes.
143 | $file = TextStrings::stripQuotes( $phpcsFile->getFileName() );
144 | if ( $file === 'STDIN' ) {
145 | $file = ''; // @codeCoverageIgnore
146 | }
147 | else {
148 | $file = PathHelper::normalize_absolute_path( $file );
149 | }
150 |
151 | $valid_prefixes = [];
152 | $psr4_info = false;
153 | if ( $file !== '' ) {
154 | $psr4_info = $this->get_psr4_info( $phpcsFile, $file );
155 | }
156 |
157 | if ( \is_array( $psr4_info ) ) {
158 | // If a PSR4 path matched, there will only ever be one valid prefix for the matched path.
159 | $valid_prefixes = [ $psr4_info['prefix'] . '\\' ];
160 | }
161 | else {
162 | // Safeguard that the PSR-4 prefixes are always included.
163 | // Makes sure level depth check still happens even if there is no basepath or path doesn't match PSR-4 path.
164 | if ( empty( $this->prefixes ) && ! empty( $this->psr4_paths ) ) {
165 | $this->prefixes = \array_keys( $this->psr4_paths );
166 | }
167 |
168 | $this->validate_prefixes();
169 | $valid_prefixes = $this->validated_prefixes;
170 | }
171 |
172 | // Strip off the (longest) plugin prefix.
173 | $namespace_name_no_prefix = $namespace_name;
174 | $found_prefix = '';
175 | if ( ! empty( $valid_prefixes ) ) {
176 | $name = $namespace_name . '\\'; // Validated prefixes always have a \ at the end.
177 | foreach ( $valid_prefixes as $prefix ) {
178 | if ( \strpos( $name, $prefix ) === 0 ) {
179 | $namespace_name_no_prefix = \rtrim( \substr( $name, \strlen( $prefix ) ), '\\' );
180 | $found_prefix = \rtrim( $prefix, '\\' );
181 | break;
182 | }
183 | }
184 | unset( $prefix, $name );
185 | }
186 |
187 | // Check if a prefix is used.
188 | if ( ! empty( $valid_prefixes ) && $found_prefix === '' ) {
189 | $prefixes = $valid_prefixes;
190 |
191 | if ( $psr4_info !== false ) {
192 | $error = 'PSR-4 namespace name for this path is required to start with the "%1$s" prefix.';
193 | $errorcode = 'MissingPSR4Prefix';
194 | }
195 | else {
196 | $error = 'A namespace name is required to start with one of the following prefixes: "%s"';
197 | $errorcode = 'MissingPrefix';
198 |
199 | $prefixes = \array_merge( $prefixes, \array_keys( $this->psr4_paths ) );
200 | $prefixes = \array_unique( $prefixes );
201 |
202 | if ( \count( $prefixes ) === 1 ) {
203 | $error = 'A namespace name is required to start with the "%s" prefix.';
204 | }
205 | else {
206 | \natcasesort( $prefixes );
207 | }
208 | }
209 |
210 | $data = [ \implode( '", "', $prefixes ) ];
211 |
212 | $phpcsFile->addError( $error, $stackPtr, $errorcode, $data );
213 | }
214 |
215 | /*
216 | * Check the namespace level depth.
217 | */
218 | if ( $namespace_name_no_prefix !== '' ) {
219 | $namespace_for_level_check = $namespace_name_no_prefix;
220 |
221 | // Allow for a variation of `Tests\` and `Tests\*\Doubles\` after the prefix.
222 | $starts_with_tests = ( \strpos( $namespace_for_level_check, 'Tests\\' ) === 0 );
223 | $prefix_ends_with_tests = ( \substr( $found_prefix, -6 ) === '\Tests' );
224 | if ( $starts_with_tests === true || $prefix_ends_with_tests === true ) {
225 | $stripped = false;
226 | foreach ( self::DOUBLE_DIRS as $dir => $length ) {
227 | if ( \strpos( $namespace_for_level_check, $dir ) !== false ) {
228 | $namespace_for_level_check = \substr( $namespace_for_level_check, ( \strpos( $namespace_for_level_check, $dir ) + $length ) );
229 | $stripped = true;
230 | break;
231 | }
232 | }
233 |
234 | if ( $stripped === false ) {
235 | if ( $starts_with_tests === true ) {
236 | // No double dir found, now check/strip typical test dirs.
237 | if ( \strpos( $namespace_for_level_check, 'Tests\WP\\' ) === 0 ) {
238 | $namespace_for_level_check = \substr( $namespace_for_level_check, 9 );
239 | }
240 | elseif ( \strpos( $namespace_for_level_check, 'Tests\Unit\\' ) === 0 ) {
241 | $namespace_for_level_check = \substr( $namespace_for_level_check, 11 );
242 | }
243 | else {
244 | // Okay, so this only has the `Tests` prefix, just strip it.
245 | $namespace_for_level_check = \substr( $namespace_for_level_check, 6 );
246 | }
247 | }
248 | elseif ( $prefix_ends_with_tests === true ) {
249 | // Prefix which already includes `Tests`.
250 | if ( \strpos( $namespace_for_level_check, 'WP\\' ) === 0 ) {
251 | $namespace_for_level_check = \substr( $namespace_for_level_check, 3 );
252 | }
253 | elseif ( \strpos( $namespace_for_level_check, 'Unit\\' ) === 0 ) {
254 | $namespace_for_level_check = \substr( $namespace_for_level_check, 5 );
255 | }
256 | }
257 | }
258 | }
259 |
260 | $parts = \explode( '\\', $namespace_for_level_check );
261 | $part_count = \count( $parts );
262 |
263 | $phpcsFile->recordMetric( $stackPtr, 'Nr of levels in namespace name', $part_count );
264 |
265 | if ( $part_count > $this->max_levels ) {
266 | $error = 'A namespace name is not allowed to be more than %d levels deep (excluding the prefix). Level depth found: %d in %s';
267 | $data = [
268 | $this->max_levels,
269 | $part_count,
270 | $namespace_name,
271 | ];
272 |
273 | $phpcsFile->addError( $error, $stackPtr, 'MaxExceeded', $data );
274 | }
275 | elseif ( $part_count > $this->recommended_max_levels ) {
276 | $error = 'A namespace name should be no more than %d levels deep (excluding the prefix). Level depth found: %d in %s';
277 | $data = [
278 | $this->recommended_max_levels,
279 | $part_count,
280 | $namespace_name,
281 | ];
282 |
283 | $phpcsFile->addWarning( $error, $stackPtr, 'TooLong', $data );
284 | }
285 | }
286 |
287 | /*
288 | * Prepare to check the path to level translation.
289 | */
290 |
291 | if ( ! isset( $phpcsFile->config->basepath ) ) {
292 | // If no basepath is set, we don't know the project root, so bow out.
293 | return;
294 | }
295 |
296 | if ( $file === '' ) {
297 | // STDIN.
298 | return; // @codeCoverageIgnore
299 | }
300 |
301 | $relative_directory = '';
302 | if ( \is_array( $psr4_info ) ) {
303 | $relative_directory = $psr4_info['relative'];
304 | }
305 | else {
306 | $directory = PathHelper::normalize_absolute_path( \dirname( $file ) );
307 |
308 | $this->validate_src_directory( $phpcsFile );
309 |
310 | if ( empty( $this->validated_src_directory ) === false ) {
311 | foreach ( $this->validated_src_directory as $absolute_src_path ) {
312 | if ( PathHelper::path_starts_with( $directory, $absolute_src_path ) === false ) {
313 | continue;
314 | }
315 |
316 | $relative_directory = PathHelper::strip_basepath( $directory, $absolute_src_path );
317 | break;
318 | }
319 | }
320 | }
321 |
322 | if ( $relative_directory === '.' ) {
323 | $relative_directory = '';
324 | }
325 |
326 | // Now any potential src directory has been stripped, remove surrounding slashes.
327 | $relative_directory = \trim( $relative_directory, '/' );
328 |
329 | // Directory to namespace translation.
330 | $expected = '[Plugin\Prefix]';
331 | if ( $found_prefix !== '' ) {
332 | $expected = $found_prefix;
333 | }
334 | // Namespace name doesn't have the correct prefix, but we do know what the prefix should be.
335 | elseif ( \is_array( $psr4_info ) ) {
336 | $expected = $psr4_info['prefix'];
337 | }
338 | elseif ( \count( $valid_prefixes ) === 1 ) {
339 | $expected = \rtrim( $valid_prefixes[0], '\\' );
340 | }
341 |
342 | $clean = [];
343 | $name_for_compare = '';
344 |
345 | if ( $relative_directory !== '' ) {
346 | $levels = \explode( '/', $relative_directory );
347 | $levels = \array_filter( $levels ); // Remove empties, just in case.
348 |
349 | foreach ( $levels as $level ) {
350 | $cleaned_level = $level;
351 | if ( $psr4_info === false ) {
352 | $cleaned_level = \preg_replace( '`[[:punct:]]`', '_', $cleaned_level );
353 | $words = \explode( '_', $cleaned_level );
354 | $words = \array_map( 'ucfirst', $words );
355 | $cleaned_level = \implode( '_', $words );
356 | }
357 |
358 | if ( NamingConventions::isValidIdentifierName( $cleaned_level ) === false ) {
359 | $phpcsFile->addError(
360 | 'Translating the directory name to a namespace name would not yield a valid namespace name. Rename the "%s" directory.',
361 | 0,
362 | 'DirectoryInvalid',
363 | [ $level ]
364 | );
365 |
366 | // Continuing would be useless as the name would be invalid anyway.
367 | return;
368 | }
369 |
370 | $clean[] = $cleaned_level;
371 | }
372 |
373 | $name_for_compare = \implode( '\\', $clean );
374 | }
375 |
376 | // Check whether the namespace name complies with the rules.
377 | if ( $psr4_info !== false ) {
378 | // Check for PSR-4 compliant namespace.
379 | if ( \strcmp( $name_for_compare, $namespace_name_no_prefix ) === 0 ) {
380 | return;
381 | }
382 |
383 | $error = 'Directory marked as a PSR-4 path. The namespace name should match the path exactly in a case-sensitive manner. Expected namespace name: "%s"; found: "%s"';
384 | $code = 'NotPSR4Compliant';
385 | }
386 | else {
387 | // Check for "old-style" namespace.
388 | if ( \strcasecmp( $name_for_compare, $namespace_name_no_prefix ) === 0 ) {
389 | return;
390 | }
391 |
392 | $error = 'The namespace (sub)level(s) should reflect the directory path to the file. Expected: "%s"; Found: "%s"';
393 | $code = 'Invalid';
394 | }
395 |
396 | if ( $name_for_compare !== '' ) {
397 | $expected .= '\\' . $name_for_compare;
398 | }
399 |
400 | $data = [
401 | $expected,
402 | $namespace_name,
403 | ];
404 |
405 | $phpcsFile->addError( $error, $stackPtr, $code, $data );
406 | }
407 |
408 | /**
409 | * Validate a $src_directory property when set in a custom ruleset.
410 | *
411 | * @param File $phpcsFile The file being scanned.
412 | *
413 | * @return void
414 | */
415 | private function validate_src_directory( File $phpcsFile ) {
416 | if ( $this->previous_src_directory === $this->src_directory ) {
417 | return;
418 | }
419 |
420 | // Set the cache *before* validation so as to not break the above compare.
421 | $this->previous_src_directory = $this->src_directory;
422 |
423 | // Clear out previously validated src directories.
424 | $this->validated_src_directory = [];
425 |
426 | // Note: the check whether a basepath is available is done in the main `process()` routine.
427 | $base_path = PathHelper::normalize_absolute_path( $phpcsFile->config->basepath );
428 |
429 | // Add any src directories.
430 | $absolute_paths = PathValidationHelper::relative_to_absolute( $phpcsFile, $this->src_directory );
431 |
432 | // The base path is always a valid src directory.
433 | if ( isset( $absolute_paths['.'] ) === false ) {
434 | $absolute_paths['.'] = $base_path;
435 | }
436 |
437 | $this->validated_src_directory = \array_unique( $absolute_paths );
438 |
439 | // Use reverse natural sorting to get the longest directory first.
440 | \rsort( $this->validated_src_directory, ( \SORT_NATURAL | \SORT_FLAG_CASE ) );
441 | }
442 | }
443 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/NamingConventions/ObjectNameDepthSniff.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | private const TEST_SUFFIXES = [
31 | 'test' => true,
32 | 'testcase' => true,
33 | 'mock' => false,
34 | 'double' => false,
35 | ];
36 |
37 | /**
38 | * Maximum number of words.
39 | *
40 | * The maximum number of words an object name should consist of, each
41 | * separated by an underscore.
42 | *
43 | * If the name consists of more words, an ERROR will be thrown.
44 | *
45 | * @var int
46 | */
47 | public $max_words = 3;
48 |
49 | /**
50 | * Recommended maximum number of words.
51 | *
52 | * The recommended maximum number of words an object name should consist of, each
53 | * separated by an underscore.
54 | *
55 | * If the name consists of more words, a WARNING will be thrown.
56 | *
57 | * @var int
58 | */
59 | public $recommended_max_words = 3;
60 |
61 | /**
62 | * Returns an array of tokens this test wants to listen for.
63 | *
64 | * @return array
65 | */
66 | public function register() {
67 | $targets = Tokens::$ooScopeTokens;
68 | unset( $targets[ \T_ANON_CLASS ] );
69 |
70 | return $targets;
71 | }
72 |
73 | /**
74 | * Processes this test, when one of its tokens is encountered.
75 | *
76 | * @param File $phpcsFile The file being scanned.
77 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
78 | *
79 | * @return void
80 | */
81 | public function process( File $phpcsFile, $stackPtr ) {
82 |
83 | // Check whether we are in a namespace or not.
84 | if ( Namespaces::determineNamespace( $phpcsFile, $stackPtr ) === '' ) {
85 | return;
86 | }
87 |
88 | $object_name = ObjectDeclarations::getName( $phpcsFile, $stackPtr );
89 | if ( empty( $object_name ) ) {
90 | return;
91 | }
92 |
93 | $snakecase_object_name = \ltrim( $object_name, '_' );
94 |
95 | // Handle names which are potentially in CamelCaps.
96 | if ( \strpos( $snakecase_object_name, '_' ) === false ) {
97 | $snakecase_object_name = SnakeCaseHelper::get_suggestion( $snakecase_object_name );
98 |
99 | // Always treat "TestCase" as one word.
100 | if ( \substr( $snakecase_object_name, -9 ) === 'test_case' ) {
101 | $snakecase_object_name = \substr( $snakecase_object_name, 0, -9 ) . 'testcase';
102 | }
103 | }
104 |
105 | $parts = \explode( '_', $snakecase_object_name );
106 | $part_count = \count( $parts );
107 |
108 | /*
109 | * Allow the OO name to be one part longer for confirmed test/mock/double OO structures.
110 | */
111 | $tokens = $phpcsFile->getTokens();
112 |
113 | $last = \strtolower( \array_pop( $parts ) );
114 | if ( isset( self::TEST_SUFFIXES[ $last ] ) ) {
115 | if ( $tokens[ $stackPtr ]['code'] === \T_ENUM ) {
116 | // Enums cannot extend, but a mock/double without direct link to the parent could be needed.
117 | --$part_count;
118 | }
119 | else {
120 | $extends = ObjectDeclarations::findExtendedClassName( $phpcsFile, $stackPtr );
121 | if ( \is_string( $extends ) ) {
122 | --$part_count;
123 | }
124 | }
125 | }
126 |
127 | if ( $part_count <= $this->recommended_max_words && $part_count <= $this->max_words ) {
128 | $phpcsFile->recordMetric( $stackPtr, 'Nr of words in object name', $part_count );
129 | return;
130 | }
131 |
132 | // Check if the OO structure is deprecated.
133 | $ignore = Collections::classModifierKeywords();
134 | $ignore[ \T_WHITESPACE ] = \T_WHITESPACE;
135 |
136 | $comment_end = $stackPtr;
137 | for ( $comment_end = ( $stackPtr - 1 ); $comment_end >= 0; $comment_end-- ) {
138 | if ( isset( $ignore[ $tokens[ $comment_end ]['code'] ] ) === true ) {
139 | continue;
140 | }
141 |
142 | if ( $tokens[ $comment_end ]['code'] === \T_ATTRIBUTE_END
143 | && isset( $tokens[ $comment_end ]['attribute_opener'] ) === true
144 | ) {
145 | $comment_end = $tokens[ $comment_end ]['attribute_opener'];
146 | continue;
147 | }
148 |
149 | break;
150 | }
151 |
152 | if ( $tokens[ $comment_end ]['code'] === \T_DOC_COMMENT_CLOSE_TAG ) {
153 | // Only check if the OO structure has a docblock.
154 | $comment_start = $tokens[ $comment_end ]['comment_opener'];
155 | foreach ( $tokens[ $comment_start ]['comment_tags'] as $tag ) {
156 | if ( $tokens[ $tag ]['content'] === '@deprecated' ) {
157 | // Deprecated OO structure, ignore.
158 | return;
159 | }
160 | }
161 | }
162 |
163 | $phpcsFile->recordMetric( $stackPtr, 'Nr of words in object name', $part_count );
164 |
165 | // Not a deprecated OO structure, this OO structure should comply with the rules.
166 | $object_type = 'a ' . $tokens[ $stackPtr ]['content'];
167 | if ( $tokens[ $stackPtr ]['code'] === \T_INTERFACE || $tokens[ $stackPtr ]['code'] === \T_ENUM ) {
168 | $object_type = 'an ' . $tokens[ $stackPtr ]['content'];
169 | }
170 |
171 | if ( $part_count > $this->max_words ) {
172 | $error = 'The name of %s is not allowed to consist of more than %d words. Words found: %d in %s';
173 | $data = [
174 | $object_type,
175 | $this->max_words,
176 | $part_count,
177 | $object_name,
178 | ];
179 |
180 | $phpcsFile->addError( $error, $stackPtr, 'MaxExceeded', $data );
181 | }
182 | elseif ( $part_count > $this->recommended_max_words ) {
183 | $error = 'The name of %s should not consist of more than %d words. Words found: %d in %s';
184 | $data = [
185 | $object_type,
186 | $this->recommended_max_words,
187 | $part_count,
188 | $object_name,
189 | ];
190 |
191 | $phpcsFile->addWarning( $error, $stackPtr, 'TooLong', $data );
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/NamingConventions/ValidHookNameSniff.php:
--------------------------------------------------------------------------------
1 | > $parameters Array with information about the parameters.
98 | *
99 | * @return void
100 | */
101 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
102 | $hook_name_param = WPHookHelper::get_hook_name_param( $matched_content, $parameters );
103 | if ( $hook_name_param === false ) {
104 | // If we can't find the hook name parameter, there's nothing to do, so bow out.
105 | return;
106 | }
107 |
108 | /*
109 | * The custom prefix should be in the first text passed to `transform()` for each
110 | * matched function call.
111 | *
112 | * Reset the properties which help manage this each time a new function call
113 | * is encountered.
114 | */
115 | $this->found_prefix = '';
116 | $this->first_string = '';
117 | $this->prefix_quote_style = '';
118 | $this->validate_prefixes();
119 |
120 | /*
121 | * If any prefixes were passed, check if this is a hook belonging to the plugin being checked.
122 | */
123 | if ( empty( $this->validated_prefixes ) === false ) {
124 | $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $hook_name_param['start'], ( $hook_name_param['end'] + 1 ), true );
125 | $found_prefix = '';
126 |
127 | if ( isset( Tokens::$stringTokens[ $this->tokens[ $first_non_empty ]['code'] ] ) ) {
128 | $this->prefix_quote_style = $this->tokens[ $first_non_empty ]['content'][0];
129 | $content = \trim( TextStrings::stripQuotes( $this->tokens[ $first_non_empty ]['content'] ) );
130 |
131 | foreach ( $this->validated_prefixes as $prefix ) {
132 | if ( \strpos( $prefix, '\\' ) === false
133 | && \strpos( $content, $prefix ) === 0
134 | ) {
135 | $found_prefix = $prefix;
136 | break;
137 | }
138 |
139 | $prefix = \str_replace( '\\\\', '[\\\\]+', \preg_quote( $prefix, '`' ) );
140 | if ( \preg_match( '`^' . $prefix . '`', $content, $matches ) === 1 ) {
141 | $found_prefix = $matches[0];
142 | break;
143 | }
144 | }
145 | }
146 |
147 | if ( $found_prefix === '' ) {
148 | /*
149 | * Not a hook name with a prefix indicating it belongs to the specific plugin
150 | * being checked. Ignore as it's probably a WP Core or external plugin hook name
151 | * which we cannot change.
152 | */
153 | return;
154 | }
155 |
156 | $this->found_prefix = $found_prefix;
157 | }
158 |
159 | // Do the WPCS native hook name check.
160 | parent::process_parameters( $stackPtr, $group_name, $matched_content, $parameters );
161 |
162 | if ( $this->found_prefix === '' ) {
163 | return;
164 | }
165 |
166 | // Do the YoastCS specific hook name length and prefix check.
167 | $this->verify_yoast_hook_name( $stackPtr, $hook_name_param );
168 | }
169 |
170 | /**
171 | * Transform an arbitrary string to lowercase and replace punctuation and spaces with underscores.
172 | *
173 | * This overloads the parent to prevent errors being triggered on the Yoast specific
174 | * plugin prefix for hook names and remembers whether a prefix was found to allow
175 | * checking whether it was the correct one.
176 | *
177 | * @param string $text_string The target string.
178 | * @param string $regex The punctuation regular expression to use.
179 | * @param string $transform_type Whether to do a partial or complete transform.
180 | * Valid values are: 'full', 'case', 'punctuation'.
181 | *
182 | * @return string
183 | */
184 | protected function transform( $text_string, $regex, $transform_type = 'full' ) {
185 |
186 | if ( empty( $this->validated_prefixes ) ) {
187 | return parent::transform( $text_string, $regex, $transform_type );
188 | }
189 |
190 | if ( $this->first_string === '' ) {
191 | $this->first_string = $text_string;
192 | }
193 |
194 | // Not the first text string.
195 | if ( $text_string !== $this->first_string ) {
196 | return parent::transform( $text_string, $regex, $transform_type );
197 | }
198 |
199 | // Repeated call for the first text string.
200 | if ( $this->found_prefix !== '' ) {
201 | $text_string = \substr( $text_string, \strlen( $this->found_prefix ) );
202 | }
203 |
204 | return $this->found_prefix . parent::transform( $text_string, $regex, $transform_type );
205 | }
206 |
207 | /**
208 | * Additional YoastCS specific hook name checks.
209 | *
210 | * @param int $stackPtr The position of the current token in the stack.
211 | * @param array $hook_name_param Array with information about the hook name parameter.
212 | *
213 | * @return void
214 | */
215 | private function verify_yoast_hook_name( $stackPtr, $hook_name_param ) {
216 | // @phpstan-ignore binaryOp.invalid, argument.type (The passed value will only ever be an integer, PHPStan just doesn't know the shape of the array.)
217 | $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $hook_name_param['start'], ( $hook_name_param['end'] + 1 ), true );
218 | if ( $first_non_empty === false ) {
219 | // Shouldn't be possible as we've checked this before.
220 | return; // @codeCoverageIgnore
221 | }
222 |
223 | /*
224 | * Check that the namespace-like prefix is used for hooks.
225 | */
226 | if ( \strpos( $this->found_prefix, '\\' ) === false ) {
227 | /*
228 | * Find which namespace-based prefix should have been used.
229 | * Loop till the end as the shortest prefix will be last.
230 | */
231 | $namespace_prefix = '';
232 | foreach ( $this->validated_prefixes as $prefix ) {
233 | if ( \strpos( $prefix, '\\' ) !== false ) {
234 | $namespace_prefix = $prefix;
235 | }
236 | }
237 |
238 | $this->phpcsFile->addWarning(
239 | 'Wrong prefix type used. Hook names should use the "%s" namespace-like prefix. Found prefix: %s',
240 | $first_non_empty,
241 | 'WrongPrefix',
242 | [
243 | $namespace_prefix,
244 | $this->found_prefix,
245 | ]
246 | );
247 |
248 | $this->phpcsFile->recordMetric( $stackPtr, 'Hook name prefix type', 'old_style' );
249 | }
250 | else {
251 | $this->phpcsFile->recordMetric( $stackPtr, 'Hook name prefix type', 'new\style' );
252 |
253 | /*
254 | * Check whether the namespace separator slashes are correctly escaped.
255 | */
256 | if ( $this->prefix_quote_style === '"' ) {
257 | \preg_match_all( '`[\\\\]+`', $this->found_prefix, $matches );
258 | if ( empty( $matches[0] ) === false ) {
259 | $counter = 0;
260 | foreach ( $matches[0] as $match ) {
261 | if ( $match === '\\' ) {
262 | ++$counter;
263 | }
264 | }
265 |
266 | if ( $counter > 0 ) {
267 | $this->phpcsFile->addWarning(
268 | 'When using a double quoted string for the hook name, it is strongly recommended to escape the backslashes in the hook name (prefix). Found %s unescaped backslashes.',
269 | $first_non_empty,
270 | 'EscapeSlashes',
271 | [ $counter ]
272 | );
273 | }
274 | }
275 | }
276 | }
277 |
278 | /*
279 | * Check if the hook name is a single quoted string.
280 | */
281 | $allow = [ \T_CONSTANT_ENCAPSED_STRING ];
282 | $allow += Tokens::$emptyTokens;
283 |
284 | // @phpstan-ignore binaryOp.invalid, argument.type (The passed value will only ever be an integer, PHPStan just doesn't know the shape of the array.)
285 | $has_non_string = $this->phpcsFile->findNext( $allow, $hook_name_param['start'], ( $hook_name_param['end'] + 1 ), true );
286 | if ( $has_non_string !== false ) {
287 | /*
288 | * Double quoted string or a hook name concatenated together, checking the word count for the
289 | * hook name can not be done in a reliable manner.
290 | *
291 | * Throwing a warning to allow for examining these if desired.
292 | * Severity 3 makes sure that this warning will normally be invisible and will only
293 | * be thrown when PHPCS is explicitly requested to check with a lower severity.
294 | */
295 | $this->phpcsFile->addWarning(
296 | 'Hook name could not reliably be examined for maximum word count (max is %d words). Please verify this hook name manually. Found: %s',
297 | $first_non_empty,
298 | 'NonString',
299 | [
300 | $this->max_words,
301 | $hook_name_param['raw'],
302 | ],
303 | 3
304 | );
305 |
306 | $this->phpcsFile->recordMetric( $stackPtr, 'Nr of words in hook name', 'undetermined' );
307 |
308 | return;
309 | }
310 |
311 | /*
312 | * Check the hook name depth.
313 | */
314 | $hook_ptr = $first_non_empty; // If no other tokens were found, the first non empty will be the hook name.
315 | $hook_name = TextStrings::stripQuotes( $this->tokens[ $hook_ptr ]['content'] );
316 | $hook_name = \substr( $hook_name, \strlen( $this->found_prefix ) );
317 |
318 | $parts = \explode( '_', $hook_name );
319 | $part_count = \count( $parts );
320 |
321 | $this->phpcsFile->recordMetric( $stackPtr, 'Nr of words in hook name', $part_count );
322 |
323 | if ( $part_count > $this->max_words ) {
324 | $error = 'A hook name is not allowed to consist of more than %d words after the plugin prefix. Words found: %d in %s';
325 | $data = [
326 | $this->max_words,
327 | $part_count,
328 | $this->tokens[ $hook_ptr ]['content'],
329 | ];
330 |
331 | $this->phpcsFile->addError( $error, $hook_ptr, 'MaxExceeded', $data );
332 | }
333 | elseif ( $part_count > $this->recommended_max_words ) {
334 | $error = 'A hook name should not consist of more than %d words after the plugin prefix. Words found: %d in %s';
335 | $data = [
336 | $this->recommended_max_words,
337 | $part_count,
338 | $this->tokens[ $hook_ptr ]['content'],
339 | ];
340 |
341 | $this->phpcsFile->addWarning( $error, $hook_ptr, 'TooLong', $data );
342 | }
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/Tools/BrainMonkeyRaceConditionSniff.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | public function register() {
31 | return [ \T_STRING ];
32 | }
33 |
34 | /**
35 | * Processes this test, when one of its tokens is encountered.
36 | *
37 | * @param File $phpcsFile The file being scanned.
38 | * @param int $stackPtr The position of the current token in the stack passed in $tokens.
39 | *
40 | * @return void
41 | */
42 | public function process( File $phpcsFile, $stackPtr ) {
43 | $tokens = $phpcsFile->getTokens();
44 |
45 | if ( \strtolower( $tokens[ $stackPtr ]['content'] ) !== 'expect' ) {
46 | return;
47 | }
48 |
49 | $nextNonEmpty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true );
50 | if ( $nextNonEmpty === false || $tokens[ $nextNonEmpty ]['code'] !== \T_OPEN_PARENTHESIS ) {
51 | // Definitely not a function call.
52 | return;
53 | }
54 |
55 | $prevNonEmpty = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
56 | if ( $prevNonEmpty === false
57 | || isset( Collections::objectOperators()[ $tokens[ $prevNonEmpty ]['code'] ] )
58 | || $tokens[ $prevNonEmpty ]['code'] === \T_FUNCTION
59 | ) {
60 | // Method call or function declaration, not a function call.
61 | return;
62 | }
63 |
64 | $functionToken = Conditions::getLastCondition( $phpcsFile, $stackPtr, [ \T_FUNCTION ] );
65 | if ( $functionToken === false ) {
66 | return;
67 | }
68 |
69 | if ( Scopes::isOOMethod( $phpcsFile, $functionToken ) === false ) {
70 | return;
71 | }
72 |
73 | // Check that this is an expect() for one of the hook functions.
74 | $param = PassedParameters::getParameter( $phpcsFile, $stackPtr, 1, 'function_name' );
75 | if ( empty( $param ) ) {
76 | return;
77 | }
78 |
79 | $expected = Tokens::$emptyTokens;
80 | $expected[] = \T_CONSTANT_ENCAPSED_STRING;
81 |
82 | // @phpstan-ignore binaryOp.invalid, argument.type (The passed value will only ever be an integer, PHPStan just doesn't know the shape of the array.)
83 | $hasUnexpected = $phpcsFile->findNext( $expected, $param['start'], ( $param['end'] + 1 ), true );
84 | if ( $hasUnexpected !== false ) {
85 | return;
86 | }
87 |
88 | // @phpstan-ignore binaryOp.invalid, argument.type (The passed value will only ever be an integer, PHPStan just doesn't know the shape of the array.)
89 | $text = $phpcsFile->findNext( Tokens::$emptyTokens, $param['start'], ( $param['end'] + 1 ), true );
90 | $textContent = TextStrings::stripQuotes( $tokens[ $text ]['content'] );
91 | if ( $textContent !== 'apply_filters' && $textContent !== 'do_action' ) {
92 | return;
93 | }
94 |
95 | // Now walk the contents of the function declaration to see if we can find the other function call.
96 | if ( isset( $tokens[ $functionToken ]['scope_opener'], $tokens[ $functionToken ]['scope_closer'] ) === false ) {
97 | // We don't know the start or end of the function. Edge case which can't happen under normal circumstances.
98 | return; // @codeCoverageIgnore
99 | }
100 |
101 | $targetContent = 'expectdone';
102 | if ( $textContent === 'apply_filters' ) {
103 | $targetContent = 'expectapplied';
104 | }
105 |
106 | $start = $tokens[ $functionToken ]['scope_opener'];
107 | $end = $tokens[ $functionToken ]['scope_closer'];
108 |
109 | for ( $i = $start; $i < $end; $i++ ) {
110 | if ( $tokens[ $i ]['code'] !== \T_STRING ) {
111 | continue;
112 | }
113 |
114 | if ( \strtolower( $tokens[ $i ]['content'] ) !== $targetContent ) {
115 | continue;
116 | }
117 |
118 | // Make sure it is a function call.
119 | $next = $phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
120 | if ( $next === false || $tokens[ $next ]['code'] !== \T_OPEN_PARENTHESIS ) {
121 | continue;
122 | }
123 |
124 | // Okay, we have found the race condition. Throw error.
125 | $message = 'The %s() test method contains both a call to Monkey\Functions\expect( %s ), as well as a call to %s(). This causes a race condition which will cause the tests to fail. Only use one of these in a test.';
126 | $data = [
127 | FunctionDeclarations::getName( $phpcsFile, $functionToken ),
128 | $tokens[ $text ]['content'],
129 | ( $targetContent === 'expectdone' ) ? 'Monkey\Actions\expectDone' : 'Monkey\Filters\expectApplied',
130 | ];
131 |
132 | $phpcsFile->addError( $message, $functionToken, 'Found', $data );
133 | break;
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Yoast/Sniffs/WhiteSpace/FunctionSpacingSniff.php:
--------------------------------------------------------------------------------
1 | > Function names as the keys and the name of the first declared parameter
33 | * as the value.
34 | * There can be multiple parameter names if the parameter
35 | * was renamed over time.
36 | */
37 | private const PARAM_INFO = [
38 | 'json_encode' => 'value',
39 |
40 | /*
41 | * The current parameter name is `$data`, but this is expected to be changed to `$value` in WP 6.5.
42 | * See: https://core.trac.wordpress.org/ticket/59630
43 | */
44 | 'wp_json_encode' => [ 'data', 'value' ],
45 | ];
46 |
47 | /**
48 | * Groups of functions to restrict.
49 | *
50 | * @return array>>
51 | */
52 | public function getGroups() {
53 | return [
54 | 'json_encode' => [
55 | 'functions' => [
56 | 'json_encode',
57 | 'wp_json_encode',
58 | ],
59 | ],
60 | ];
61 | }
62 |
63 | /**
64 | * Process a matched token.
65 | *
66 | * @param int $stackPtr The position of the current token in the stack.
67 | * @param string $group_name The name of the group which was matched.
68 | * @param string $matched_content The token content (function name) which was matched
69 | * in lowercase.
70 | *
71 | * @return void
72 | */
73 | public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
74 | $error = 'Detected a call to %s(). Use %s() instead.';
75 | $error_code = 'Found';
76 | $data = [
77 | $matched_content,
78 | self::REPLACEMENT,
79 | ];
80 |
81 | $params = PassedParameters::getParameters( $this->phpcsFile, $stackPtr );
82 |
83 | /*
84 | * If no parameters were passed, we can safely replace the function call, even though
85 | * the function call itself, as-is, is not correct/working (but that's not the concern of
86 | * this sniff).
87 | */
88 | if ( empty( $params ) ) {
89 | /*
90 | * Make sure this is not a PHP 8.1+ first class callable. If it is, throw the error, but don't autofix.
91 | */
92 | $ignore = Tokens::$emptyTokens;
93 | $ignore[ \T_OPEN_PARENTHESIS ] = \T_OPEN_PARENTHESIS;
94 |
95 | $first_in_call = $this->phpcsFile->findNext( $ignore, ( $stackPtr + 1 ), null, true );
96 | if ( $first_in_call !== false && $this->tokens[ $first_in_call ]['code'] === \T_ELLIPSIS ) {
97 | $error_code .= 'InFirstClassCallable';
98 | $this->phpcsFile->addError( $error, $stackPtr, $error_code, $data );
99 | return;
100 | }
101 |
102 | $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code, $data );
103 | if ( $fix === true ) {
104 | $this->fix_it( $stackPtr );
105 | }
106 |
107 | return;
108 | }
109 |
110 | /*
111 | * If there are function parameters, we need to verify that only the first ($value) parameter
112 | * was passed, taking PHP 8.0+ function calls with named parameters into account.
113 | *
114 | * We also need to check for parameter unpacking being used as in that case, the
115 | * parameter count will be unreliable.
116 | */
117 | $value_param = PassedParameters::getParameterFromStack( $params, 1, self::PARAM_INFO[ $matched_content ] );
118 | if ( \is_array( $value_param ) && \count( $params ) === 1 ) {
119 | // @phpstan-ignore binaryOp.invalid, argument.type (The passed value will only ever be an integer, PHPStan just doesn't know the shape of the array.)
120 | $first_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, $value_param['start'], ( $value_param['end'] + 1 ), true );
121 | if ( $first_token === false || $this->tokens[ $first_token ]['code'] !== \T_ELLIPSIS ) {
122 | /*
123 | * Okay, so this is a function call with only the first/$value parameter passed.
124 | * This can be safely replaced.
125 | */
126 | $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code, $data );
127 | if ( $fix === true ) {
128 | $this->fix_it( $stackPtr, $value_param );
129 | }
130 |
131 | return;
132 | }
133 | }
134 |
135 | /*
136 | * In all other cases, we cannot auto-fix, only flag.
137 | */
138 | $error_code .= 'WithAdditionalParams';
139 |
140 | $this->phpcsFile->addError( $error, $stackPtr, $error_code, $data );
141 | }
142 |
143 | /**
144 | * Auto-fix the function call to use the replacement function.
145 | *
146 | * @since 3.0.0
147 | *
148 | * @param int $stackPtr The position of the current token in the stack.
149 | * @param array|false $value_param Optional. Parameter information for the first/$value
150 | * parameter if available, or false if not.
151 | *
152 | * @return void
153 | */
154 | private function fix_it( $stackPtr, $value_param = false ) {
155 | $this->phpcsFile->fixer->beginChangeset();
156 |
157 | // Remove potential leading namespace separator for fully qualified function call.
158 | $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true );
159 | if ( $prev !== false && $this->tokens[ $prev ]['code'] === \T_NS_SEPARATOR ) {
160 | $this->phpcsFile->fixer->replaceToken( $prev, '' );
161 | }
162 |
163 | // Replace the function call with a, potentially fully qualified, call to the replacement.
164 | $namespaced = Namespaces::determineNamespace( $this->phpcsFile, $stackPtr );
165 | if ( empty( $namespaced ) ) {
166 | $this->phpcsFile->fixer->replaceToken( $stackPtr, self::REPLACEMENT );
167 | }
168 | else {
169 | $this->phpcsFile->fixer->replaceToken( $stackPtr, '\\' . self::REPLACEMENT );
170 | }
171 |
172 | if ( \is_array( $value_param ) && isset( $value_param['name_token'] ) ) {
173 | // Update the parameter name when the function call uses named parameters.
174 | // `$data` is the parameter name used in the WPSEO_Utils::format_json_encode() function.
175 |
176 | // @phpstan-ignore argument.type (The passed value will only ever be an integer, PHPStan just doesn't know the shape of the array.)
177 | $this->phpcsFile->fixer->replaceToken( $value_param['name_token'], 'data' );
178 | }
179 |
180 | $this->phpcsFile->fixer->endChangeset();
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/Yoast/Utils/CustomPrefixesTrait.php:
--------------------------------------------------------------------------------
1 |
27 | */
28 | public $prefixes = [];
29 |
30 | /**
31 | * Target prefixes after validation.
32 | *
33 | * @var array
34 | */
35 | protected $validated_prefixes = [];
36 |
37 | /**
38 | * Cache of previously set prefixes.
39 | *
40 | * Prevents having to do the same prefix validation over and over again.
41 | *
42 | * @var array
43 | */
44 | protected $previous_prefixes = [];
45 |
46 | /**
47 | * Prepare the prefixes array for use by a sniff.
48 | *
49 | * Checks and makes sure that:
50 | * - "Namespace"-like prefixes do not start with a `\` and end with a `\`.
51 | * - Non-namespace-like prefixes do not start with a `_` and end with a `_`.
52 | *
53 | * @return void
54 | */
55 | final protected function validate_prefixes() {
56 | if ( $this->previous_prefixes === $this->prefixes ) {
57 | return;
58 | }
59 |
60 | // Set the cache *before* validation so as to not break the above compare.
61 | $this->previous_prefixes = $this->prefixes;
62 |
63 | $prefixes = (array) $this->prefixes;
64 | $prefixes = \array_filter( \array_map( 'trim', $prefixes ) );
65 |
66 | if ( empty( $prefixes ) ) {
67 | $this->validated_prefixes = [];
68 | return;
69 | }
70 |
71 | // Allow sniffs to add extra rules.
72 | $prefixes = $this->filter_prefixes( $prefixes );
73 |
74 | $validated = [];
75 | foreach ( $prefixes as $prefix ) {
76 | if ( \strpos( $prefix, '\\' ) !== false ) {
77 | $prefix = \trim( $prefix, '\\' );
78 | $validated[] = $prefix . '\\';
79 | }
80 | else {
81 | // Old-style prefix.
82 | $prefix = \trim( $prefix, '_' );
83 | $validated[] = $prefix . '_';
84 | }
85 | }
86 |
87 | // Use reverse natural sorting to get the longest prefix first.
88 | \rsort( $validated, ( \SORT_NATURAL | \SORT_FLAG_CASE ) );
89 |
90 | // Set the validated prefixes cache.
91 | $this->validated_prefixes = $validated;
92 | }
93 |
94 | /**
95 | * Overloadable method to do custom prefix filtering prior to validation.
96 | *
97 | * @param array $prefixes The unvalidated prefixes.
98 | *
99 | * @return array
100 | */
101 | protected function filter_prefixes( $prefixes ) {
102 | return $prefixes;
103 | }
104 |
105 | /**
106 | * Filter out all prefixes which don't contain a namespace separator.
107 | *
108 | * @param array $prefixes The unvalidated prefixes.
109 | *
110 | * @return array
111 | */
112 | final protected function filter_allow_only_namespace_prefixes( $prefixes ) {
113 | $filtered = [];
114 | foreach ( $prefixes as $prefix ) {
115 | if ( \strpos( $prefix, '\\' ) === false ) {
116 | continue;
117 | }
118 |
119 | $filtered[] = $prefix;
120 | }
121 |
122 | return $filtered;
123 | }
124 |
125 | /**
126 | * Filter out all prefixes which only contain lowercase characters.
127 | *
128 | * @param array $prefixes The unvalidated prefixes.
129 | *
130 | * @return array
131 | */
132 | final protected function filter_exclude_lowercase_prefixes( $prefixes ) {
133 | $filtered = [];
134 | foreach ( $prefixes as $prefix ) {
135 | if ( \strtolower( $prefix ) === $prefix ) {
136 | continue;
137 | }
138 |
139 | $filtered[] = $prefix;
140 | }
141 |
142 | return $filtered;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/Yoast/Utils/PSR4PathsTrait.php:
--------------------------------------------------------------------------------
1 |
22 | *
23 | *
24 | *
25 | *
26 | *
27 | *
28 | *
29 | *
30 | *
31 | * ```
32 | *
33 | * Note: paths are handled case-sensitively!
34 | *
35 | * @var array Key should be the prefix, value a comma-separated list of relative paths.
36 | */
37 | public $psr4_paths = [];
38 |
39 | /**
40 | * Cache of previously set list of psr4 paths.
41 | *
42 | * Prevents having to do the same path validation over and over again.
43 | *
44 | * @var array
45 | */
46 | private $previous_psr4_paths = [];
47 |
48 | /**
49 | * Validated & cleaned up list of absolute paths to the directories expecting PSR-4 file names
50 | * with their associated prefixes.
51 | *
52 | * @var array Key is the absolute path, value the applicable prefix without trailing slash.
53 | */
54 | private $validated_psr4_paths = [];
55 |
56 | /**
57 | * Check if the file is in one of the PSR4 directories.
58 | *
59 | * @param File $phpcsFile The file being scanned.
60 | * @param string $path_to_file Optional The absolute path to the file currently being examined.
61 | * If not provided, the file name will be retrieved from the File object.
62 | *
63 | * @return bool
64 | */
65 | final protected function is_in_psr4_path( File $phpcsFile, $path_to_file = '' ) {
66 | return \is_array( $this->get_psr4_info( $phpcsFile, $path_to_file ) );
67 | }
68 |
69 | /**
70 | * Retrieve all applicable information for a PSR-4 path.
71 | *
72 | * @param File $phpcsFile The file being scanned.
73 | * @param string $path_to_file Optional The absolute path to the file currently being examined.
74 | * If not provided, the file name will be retrieved from the File object.
75 | *
76 | * @return array|false Array with information about the PSR-4 path. Otherwise FALSE.
77 | */
78 | final protected function get_psr4_info( File $phpcsFile, $path_to_file = '' ) {
79 | if ( $path_to_file === '' ) {
80 | $path_to_file = TextStrings::stripQuotes( $phpcsFile->getFileName() );
81 | if ( $path_to_file === 'STDIN' ) {
82 | return false;
83 | }
84 | }
85 |
86 | $this->validate_psr4_paths( $phpcsFile );
87 | if ( empty( $this->validated_psr4_paths ) ) {
88 | return false;
89 | }
90 |
91 | $path_to_file = PathHelper::normalize_absolute_path( $path_to_file );
92 |
93 | foreach ( $this->validated_psr4_paths as $psr4_path => $prefix ) {
94 | $remainder = PathHelper::strip_basepath( $path_to_file, $psr4_path );
95 | if ( $remainder === $path_to_file ) {
96 | // Nothing was stripped, so this wasn't a match.
97 | continue;
98 | }
99 |
100 | return [
101 | 'prefix' => $prefix,
102 | 'basepath' => $psr4_path,
103 | 'relative' => \dirname( $remainder ),
104 | ];
105 | }
106 |
107 | return false;
108 | }
109 |
110 | /**
111 | * Validate the list of PSR-4 paths passed from a custom ruleset.
112 | *
113 | * This will only need to be done once in a normal PHPCS run, though for
114 | * tests the function may be called multiple times.
115 | *
116 | * @param File $phpcsFile The file being scanned.
117 | *
118 | * @return void
119 | *
120 | * @throws RuntimeException When the `psr4_paths` array is missing keys.
121 | * @throws RuntimeException When the `psr4_paths` array contains duplicate paths in multiple entries.
122 | */
123 | private function validate_psr4_paths( File $phpcsFile ) {
124 | // The basepath check needs to be done first as otherwise the previous/current comparison would be broken.
125 | if ( ! isset( $phpcsFile->config->basepath ) ) {
126 | // Only relevant for the tests: make sure previously set validated paths are cleared out.
127 | $this->validated_psr4_paths = [];
128 |
129 | // No use continuing as we can't turn relative paths into absolute paths.
130 | return;
131 | }
132 |
133 | if ( $this->previous_psr4_paths === $this->psr4_paths ) {
134 | return;
135 | }
136 |
137 | // Set the cache *before* validation so as to not break the above compare.
138 | $this->previous_psr4_paths = $this->psr4_paths;
139 |
140 | $validated_paths = [];
141 |
142 | foreach ( $this->psr4_paths as $prefix => $paths ) {
143 | // @phpstan-ignore function.alreadyNarrowedType, identical.alwaysFalse (Defensive coding as the property value is user provided via the ruleset.)
144 | if ( \is_string( $prefix ) === false || $prefix === '' ) {
145 | throw new RuntimeException(
146 | 'Invalid value passed for `psr4_paths`. Path "' . $paths . '" is not associated with a namespace prefix'
147 | );
148 | }
149 |
150 | $prefix = \rtrim( $prefix, '\\' );
151 |
152 | $paths = \rtrim( \ltrim( $paths, '[' ), ']' ); // Trim off potential [] copied over from Composer.json.
153 | $paths = \explode( ',', $paths );
154 | $paths = \array_map( 'trim', $paths );
155 | $paths = \array_map( [ TextStrings::class, 'stripQuotes' ], $paths ); // Trim off potential quotes around the paths copied over.
156 | $paths = \array_map( 'trim', $paths );
157 | $paths = PathValidationHelper::relative_to_absolute( $phpcsFile, $paths );
158 | $paths = \array_unique( $paths ); // Filter out multiple of the same paths for the same prefix.
159 |
160 | foreach ( $paths as $path ) {
161 | if ( isset( $validated_paths[ $path ] ) ) {
162 | throw new RuntimeException(
163 | 'Invalid value passed for `psr4_paths`. Multiple prefixes include the same path. Problem path: ' . $path
164 | );
165 | }
166 |
167 | $validated_paths[ $path ] = $prefix;
168 | }
169 | }
170 |
171 | // Set the validated value.
172 | $this->validated_psr4_paths = $validated_paths;
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/Yoast/Utils/PathHelper.php:
--------------------------------------------------------------------------------
1 | $relative_paths Array of relative paths which should become absolute paths.
28 | * Paths are expected to be relative to the "basepath" setting.
29 | *
30 | * @return array Array of absolute paths or an empty array if the conversion could not be executed.
31 | * The array will contain the original relative paths as the keys and the absolute paths
32 | * as the values.
33 | * Note: multiple relative paths may result in the same absolute path.
34 | * The values are not guaranteed to be unique!
35 | */
36 | public static function relative_to_absolute( File $phpcsFile, array $relative_paths ) {
37 | $absolute = [];
38 |
39 | if ( ! isset( $phpcsFile->config->basepath ) ) {
40 | // No use continuing as we can't turn relative paths into absolute paths.
41 | return $absolute;
42 | }
43 |
44 | $base_path = PathHelper::normalize_absolute_path( $phpcsFile->config->basepath );
45 |
46 | foreach ( $relative_paths as $path ) {
47 | $result_path = \trim( $path );
48 | $result_path = PathHelper::normalize_relative_path( $result_path );
49 |
50 | if ( $result_path === '' ) {
51 | continue;
52 | }
53 |
54 | if ( \strpos( $result_path, '..' ) !== false ) {
55 | // Ignore paths containing path walking.
56 | continue;
57 | }
58 |
59 | if ( $result_path === './' ) {
60 | $absolute[ $path ] = $base_path;
61 | continue;
62 | }
63 |
64 | if ( \strpos( $result_path, './' ) === 0 ) {
65 | $result_path = \substr( $result_path, 2 );
66 | }
67 |
68 | $absolute[ $path ] = $base_path . $result_path;
69 | }
70 |
71 | return $absolute;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Yoast/ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Yoast Coding Standards
5 |
6 |
12 |
13 |
14 | */.git/*
15 | */.wordpress-svn/*
16 |
17 |
18 | */node_modules/*
19 | */vendor(_prefixed)?/*
20 |
21 |
22 | */wp-content/plugins/*
23 |
24 |
25 |
34 |
35 | ./../autoload-bootstrap.php
36 |
37 |
38 |
45 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | error
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | error
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
153 |
154 | 5
155 |
156 |
157 | 5
158 |
159 |
160 | 5
161 |
162 |
163 |
164 |
169 |
170 |
171 | *\.php$
172 |
173 |
174 |
175 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
332 |
333 |
334 | 5
335 |
336 |
337 | 5
338 |
339 |
340 |
341 |
342 | 5
343 |
344 |
345 | 5
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 | */tests/*\.php$
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 | */index\.php$
415 |
416 |
417 | */index\.php$
418 |
419 |
420 |
421 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 | 5
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
474 |
476 |
477 | */tests/*\.php$
478 |
479 |
480 |
482 |
483 | */tests/*\.php$
484 |
485 |
486 |
487 |
488 | */tests(/*)?/Doubles/*\.php$
489 |
490 |
491 |
492 |
493 | */tests/*\.php$
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 | */tests/*\.php$
502 |
503 | */tests(/*)?/Doubles/*\.php$
504 |
505 |
506 |
507 |
--------------------------------------------------------------------------------
/autoload-bootstrap.php:
--------------------------------------------------------------------------------
1 | =7.2",
28 | "ext-tokenizer": "*",
29 | "automattic/vipwpcs": "^3.0.1",
30 | "php-parallel-lint/php-console-highlighter": "^1.0.0",
31 | "php-parallel-lint/php-parallel-lint": "^1.4.0",
32 | "phpcompatibility/phpcompatibility-wp": "^2.1.6",
33 | "phpcsstandards/phpcsextra": "^1.2.1",
34 | "phpcsstandards/phpcsutils": "^1.0.12",
35 | "sirbrillig/phpcs-variable-analysis": "^2.12.0",
36 | "slevomat/coding-standard": "^8.15.0",
37 | "squizlabs/php_codesniffer": "^3.12.0",
38 | "wp-coding-standards/wpcs": "^3.1.0"
39 | },
40 | "require-dev": {
41 | "phpcompatibility/php-compatibility": "^9.3.5",
42 | "phpcsstandards/phpcsdevtools": "^1.2.2",
43 | "phpunit/phpunit": "^8.0 || ^9.0",
44 | "roave/security-advisories": "dev-master"
45 | },
46 | "minimum-stability": "dev",
47 | "prefer-stable": true,
48 | "config": {
49 | "allow-plugins": {
50 | "dealerdirect/phpcodesniffer-composer-installer": true
51 | },
52 | "lock": false
53 | },
54 | "scripts": {
55 | "lint": [
56 | "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --show-deprecated --exclude vendor --exclude .git"
57 | ],
58 | "check-cs": [
59 | "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs"
60 | ],
61 | "fix-cs": [
62 | "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf"
63 | ],
64 | "test": [
65 | "@php ./vendor/phpunit/phpunit/phpunit --filter Yoast ./vendor/squizlabs/php_codesniffer/tests/AllTests.php --no-coverage"
66 | ],
67 | "coverage": [
68 | "@php ./vendor/phpunit/phpunit/phpunit --filter Yoast ./vendor/squizlabs/php_codesniffer/tests/AllTests.php"
69 | ],
70 | "check-complete": [
71 | "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./Yoast"
72 | ]
73 | },
74 | "scripts-descriptions": {
75 | "lint": "Check the PHP files for parse errors.",
76 | "check-cs": "Check the PHP files for code style violations and best practices.",
77 | "fix-cs": "Auto-fix code style violations in the PHP files.",
78 | "test": "Run the unit tests without code coverage.",
79 | "coverage": "Run the unit tests with code coverage.",
80 | "check-complete": "Check if all the sniffs have tests and XML documentation."
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/phpunit-bootstrap.php:
--------------------------------------------------------------------------------
1 | true,
64 | ];
65 |
66 | $all_standards = Standards::getInstalledStandards();
67 | $all_standards[] = 'Generic';
68 |
69 | $standards_to_ignore = [];
70 | foreach ( $all_standards as $standard ) {
71 | if ( isset( $yoast_standards[ $standard ] ) === true ) {
72 | continue;
73 | }
74 |
75 | $standards_to_ignore[] = $standard;
76 | }
77 |
78 | $standards_to_ignore_string = implode( ',', $standards_to_ignore );
79 |
80 | // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv -- This is not production, but test code.
81 | putenv( "PHPCS_IGNORE_TESTS={$standards_to_ignore_string}" );
82 |
83 | // Clean up.
84 | unset( $phpcs_dir, $composer_phpcs_path, $all_standards, $standards_to_ignore, $standard, $standards_to_ignore_string );
85 |
--------------------------------------------------------------------------------