├── .gitignore
├── tests
├── Sniffs
│ └── Imports
│ │ ├── Fixtures
│ │ ├── FileKeywordFixture.php
│ │ ├── MultipleFilesFixtures
│ │ │ ├── MultipleFilesFixtures2.php
│ │ │ └── MultipleFilesFixtures1.php
│ │ ├── ClassUsedAsNamespaceFixture.php
│ │ ├── ConstantsFixure.php
│ │ ├── RequireImportsMethodNameFixture.php
│ │ ├── UsedTraitFixture.php
│ │ ├── ClassFixtures.php
│ │ ├── GlobalNamespaceFixture.php
│ │ ├── RequireImportsAllowedPatternFixture.php
│ │ ├── InterfaceFixture.php
│ │ ├── TraitFixture.php
│ │ ├── NestedFunctionsFixture.php
│ │ ├── WordPressFixture.php
│ │ └── RequireImportsFixture.php
│ │ └── RequireImportsSniffTest.php
├── bootstrap.php
└── SniffTestHelper.php
├── ImportDetection
├── ruleset.xml
├── FileSymbolRecord.php
├── Symbol.php
├── SniffHelpers.php
├── Sniffs
│ └── Imports
│ │ └── RequireImportsSniff.php
└── WordPressSymbols.php
├── phpunit.xml
├── .circleci
└── config.yml
├── composer-php7.json
├── composer.json
├── phpcs.xml
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/Fixtures/FileKeywordFixture.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | A set of phpcs sniffs to look for unused or unimported symbols.
4 |
5 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/Fixtures/MultipleFilesFixtures/MultipleFilesFixtures2.php:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | tests
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/Fixtures/MultipleFilesFixtures/MultipleFilesFixtures1.php:
--------------------------------------------------------------------------------
1 | energize();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/Fixtures/ClassFixtures.php:
--------------------------------------------------------------------------------
1 | vars[$name] = $var;
19 | }
20 |
21 | public function getHtml($template)
22 | {
23 | foreach($this->vars as $name => $value) {
24 | $template = str_replace('{' . $name . '}', $value, $template);
25 | }
26 |
27 | return $template;
28 | }
29 | }
30 |
31 | class BadClass implements BadTemplate {
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/Fixtures/TraitFixture.php:
--------------------------------------------------------------------------------
1 | traitVar;
11 | }
12 | }
13 |
14 | // Use the trait
15 | // This will work
16 | class UseTrait
17 | {
18 | use GoodTrait;
19 | private $vars = array();
20 |
21 | public function setVariable($name, $var)
22 | {
23 | $this->vars[$name] = $var;
24 | }
25 |
26 | public function getHtml($template)
27 | {
28 | foreach($this->vars as $name => $value) {
29 | $template = str_replace('{' . $name . '}', $value, $template);
30 | }
31 |
32 | return $template;
33 | }
34 | }
35 |
36 | class BadClass {
37 | use BadTrait;
38 | }
39 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build_php7.0:
4 | docker:
5 | - image: circleci/php:7.0-apache-stretch-browsers
6 | steps:
7 | - checkout
8 | - run: COMPOSER=composer-php7.json composer install
9 | - run: COMPOSER=composer-php7.json composer test
10 | build_php8.0:
11 | docker:
12 | - image: circleci/php:8.0
13 | steps:
14 | - checkout
15 | - run: COMPOSER=composer.json composer install
16 | - run: COMPOSER=composer.json composer test
17 | build_php7.4:
18 | docker:
19 | - image: circleci/php:7.4.6
20 | steps:
21 | - checkout
22 | - run: COMPOSER=composer.json composer install
23 | - run: COMPOSER=composer.json composer test
24 | workflows:
25 | version: 2
26 | build_php_versions:
27 | jobs:
28 | - build_php7.0
29 | - build_php7.4
30 | - build_php8.0
31 |
--------------------------------------------------------------------------------
/ImportDetection/FileSymbolRecord.php:
--------------------------------------------------------------------------------
1 | importedFunctions = array_merge($this->importedFunctions, $names);
15 | }
16 |
17 | public function addImportedClasses($names) {
18 | $this->importedClasses = array_merge($this->importedClasses, $names);
19 | }
20 |
21 | public function addImportedConsts($names) {
22 | $this->importedConsts = array_merge($this->importedConsts, $names);
23 | }
24 |
25 | public function addImportedSymbolRecords($names) {
26 | $this->importedSymbolRecords = array_merge($this->importedSymbolRecords, $names);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/composer-php7.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sirbrillig/phpcs-import-detection",
3 | "description": "A set of phpcs sniffs to look for unused or unimported symbols.",
4 | "type": "phpcodesniffer-standard",
5 | "keywords" : [ "phpcs", "static analysis" ],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Payton Swick",
10 | "email": "payton@foolord.com"
11 | }
12 | ],
13 | "autoload": {
14 | "psr-4": {
15 | "ImportDetection\\": "ImportDetection/"
16 | }
17 | },
18 | "minimum-stability": "dev",
19 | "prefer-stable": true,
20 | "scripts": {
21 | "test": "./vendor/bin/phpunit"
22 | },
23 | "require" : {
24 | "php": "^7.0",
25 | "squizlabs/php_codesniffer": "^3.5.8"
26 | },
27 | "require-dev": {
28 | "sirbrillig/phpcs-variable-analysis": "^2.0.1",
29 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1",
30 | "phpunit/phpunit": "^6.4"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sirbrillig/phpcs-import-detection",
3 | "description": "A set of phpcs sniffs to look for unused or unimported symbols.",
4 | "type": "phpcodesniffer-standard",
5 | "keywords" : [ "phpcs", "static analysis" ],
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Payton Swick",
10 | "email": "payton@foolord.com"
11 | }
12 | ],
13 | "autoload": {
14 | "psr-4": {
15 | "ImportDetection\\": "ImportDetection/"
16 | }
17 | },
18 | "minimum-stability": "dev",
19 | "prefer-stable": true,
20 | "scripts": {
21 | "test": "./vendor/bin/phpunit"
22 | },
23 | "require" : {
24 | "php": "^7.0 || ^8.0",
25 | "squizlabs/php_codesniffer": "^3.5.8"
26 | },
27 | "require-dev": {
28 | "sirbrillig/phpcs-variable-analysis": "^2.0.1",
29 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1",
30 | "phpunit/phpunit": "^9.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/Fixtures/NestedFunctionsFixture.php:
--------------------------------------------------------------------------------
1 | nestedFuncA( 'hello' );
22 |
23 | if (true) {
24 | nestedFuncB( 'boo' );
25 | }
26 |
27 | if (true) {
28 | function nestedFuncC() {
29 | echo 'we are deep now';
30 | }
31 |
32 | nestedFuncC();
33 | }
34 | }
35 |
36 | public function doANestedThing() {
37 | function nestedFuncA() {
38 | echo 'nope';
39 | }
40 |
41 | \registerThing(new class {
42 | public function subClassFunc() {
43 | function nestedFuncC( $arg ) {
44 | echo $arg . ' world';
45 | }
46 |
47 | nestedFuncC( 'hello' );
48 | nestedFuncA( 'blarg' ); // warning: undefined
49 | }
50 | });
51 |
52 | doANestedThing('thisfunc'); // warning: undefined
53 | }
54 | }
55 |
56 | nestedFuncA( 'hi' ); // warning: undefined
57 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | From https://gist.github.com/Ovsyanka/e2ab2ff76e7c0d7e75a1e4213a03ff95
5 | PSR2 with changes:
6 | * tabs instead of spaces (https://gist.github.com/gsherwood/9d22f634c57f990a7c64)
7 | * bracers on end of line instead new line
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | error
43 |
44 |
45 | 0
46 |
47 |
48 |
--------------------------------------------------------------------------------
/ImportDetection/Symbol.php:
--------------------------------------------------------------------------------
1 | tokens = $tokens;
21 | $this->isUsed = false;
22 | $this->alias = $alias;
23 | }
24 |
25 | public static function getTokenWithPosition(array $token, int $stackPtr): array {
26 | $token['tokenPosition'] = $stackPtr;
27 | return $token;
28 | }
29 |
30 | public function getTokens(): array {
31 | return $this->tokens;
32 | }
33 |
34 | public function getName(): string {
35 | return $this->joinSymbolParts($this->tokens);
36 | }
37 |
38 | public function getAlias(): string {
39 | if ($this->alias) {
40 | return $this->alias->getName();
41 | }
42 |
43 | return $this->tokens[count($this->tokens) - 1]['content'];
44 | }
45 |
46 | public function isAbsoluteNamespace(): bool {
47 | $type = $this->tokens[0]['type'] ?? '';
48 | return $type === 'T_NS_SEPARATOR';
49 | }
50 |
51 | public function isNamespaced(): bool {
52 | return count($this->tokens) > 1;
53 | }
54 |
55 | /**
56 | * @return string|null
57 | */
58 | public function getTopLevelNamespace() {
59 | if (! $this->isNamespaced()) {
60 | return null;
61 | }
62 | return $this->tokens[0]['content'] ?? null;
63 | }
64 |
65 | public function getSymbolPosition(): int {
66 | return $this->tokens[0]['tokenPosition'] ?? 1;
67 | }
68 |
69 | public function getSymbolConditions(): array {
70 | return $this->tokens[0]['conditions'] ?? [];
71 | }
72 |
73 | public function markUsed() {
74 | $this->isUsed = true;
75 | }
76 |
77 | public function isUsed(): bool {
78 | return $this->isUsed;
79 | }
80 |
81 | private function joinSymbolParts(array $tokens): string {
82 | $symbolStrings = array_map(function (array $token): string {
83 | return $token['content'] ?? '';
84 | }, $tokens);
85 | return implode('', $symbolStrings);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/Fixtures/WordPressFixture.php:
--------------------------------------------------------------------------------
1 | test_classes();
52 | if (is_wp_error($output)) { // WordPressSymbol
53 | return 4;
54 | }
55 | $count = HOUR_IN_SECONDS + 10; // WordPressSymbol
56 | return $count;
57 | }
58 |
59 | public function test_classes() {
60 | if (WP_USE_THEMES) { // WordPressSymbol
61 | return new WP_Error(); // unimported
62 | }
63 | return new WP_Post(); // unimported
64 | }
65 |
66 | public function test_additional_constants() {
67 | global $wpdb;
68 | return $wpdb->get_results(
69 | "SELECT data.*
70 | FROM {$wpdb->data} bar
71 | LEFT JOIN {$wpdb->data_meta} foo
72 | ON foo.id = bar.id
73 | LIMIT 1",
74 | OBJECT_K // WordPressSymbol
75 | );
76 | }
77 | }
78 |
79 | function foo() {}
80 | add_action('init', foo);
81 | foo();
82 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD Licence Agreement
2 | -----------------------------------------------------------------------
3 |
4 | This software is available to you under the BSD license,
5 | available in the LICENSE file accompanying this software.
6 | You may obtain a copy of the License at
7 |
8 | http://www.opensource.org/licenses/bsd-license.php
9 |
10 | -----------------------------------------------------------------------
11 |
12 | Copyright (c) 2011, Sam Graham
13 | All rights reserved.
14 |
15 | Redistribution and use in source and binary forms, with or without
16 | modification, are permitted provided that the following conditions are
17 | met:
18 |
19 | * Redistributions of source code must retain the above copyright
20 | notice, this list of conditions and the following disclaimer.
21 | * Redistributions in binary form must reproduce the above copyright
22 | notice, this list of conditions and the following disclaimer in the
23 | documentation and/or other materials provided with the
24 | distribution.
25 |
26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
30 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
34 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 |
38 | -----------------------------------------------------------------------
39 |
40 | Portions of this sofware derived from work Copyright (c) 2010, Monotek d.o.o,
41 | released under a BSD License available at:
42 |
43 | http://www.opensource.org/licenses/bsd-license.php
44 |
45 | -----------------------------------------------------------------------
46 |
47 | Portions of this software derived from work Copyright (c), 2006 Squiz
48 | Pty Ltd (ABN 77 084 670 600), available under the following license:
49 |
50 | BSD Licence Agreement
51 | -----------------------------------------------------------------------
52 |
53 | This software is available to you under the BSD license,
54 | available in the LICENSE file accompanying this software.
55 | You may obtain a copy of the License at
56 |
57 | http://matrix.squiz.net/developer/tools/php_cs/licence
58 |
59 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
60 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
61 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
62 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
63 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
64 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
65 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
66 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
67 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
68 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
69 |
70 | Copyright (c), 2006 Squiz Pty Ltd (ABN 77 084 670 600).
71 | All rights reserved.
72 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/Fixtures/RequireImportsFixture.php:
--------------------------------------------------------------------------------
1 | polluter = new PollutionProducer();
61 | // next line has an unimported class
62 | $data = new stdClass(PHP_VERSION);
63 | $store = new WeatherStore($data);
64 | try {
65 | $store->trackWeather($store->key);
66 | } catch (\MyException $err) {
67 | Alerts\makeAnAlert($err);
68 | // next line has an unimported function
69 | OldAlerts\makeAlert($err);
70 | // next line has an unimported class
71 | $oldAlert = new OldAlerts\OldAlert();
72 | // next line has an unimported class
73 | $oldAlert = OldAlerts\OldAlert::makeRed($oldAlert);
74 | $oldAlert->warn();
75 | $alert = new Alerts\MyAlert();
76 | $alert = Alerts\MyAlert::markAlertImportant($alert);
77 | $alert->notifyUser();
78 | // next line has an unimported class
79 | return new Exception('buggy');
80 | }
81 | $name = $store->current_name_as_string;
82 | $data = new \stdClass(PHP_VERSION);
83 | return new MovementType('driving in ' . $name);
84 | }
85 |
86 | // next line has an unimported class
87 | public function convertToRobot(Car $car): Robot {
88 | // next line has an unimported class
89 | return new Robot($car);
90 | }
91 | }
92 |
93 | function startMonitor() {
94 | // next line has an unimported function
95 | OldAlerts\makeAlert();
96 | new Car();
97 | echo str_replace('Foo', 'Bar', 'Foobar...');
98 | $rows = whitelisted_function();
99 | $data = allowed_funcs_function_one();
100 | array_map(function (array $row) use ($data) {
101 | $data && monitor_begin($row);
102 | }, $rows);
103 | }
104 |
105 | define(WHATEVER, 'some words');
106 |
107 | function engageMagic(object $foobar) {
108 | echo $foobar;
109 | }
110 |
111 | function engageMoreMagic(mixed $foobar) {
112 | echo $foobar;
113 | }
114 |
--------------------------------------------------------------------------------
/tests/SniffTestHelper.php:
--------------------------------------------------------------------------------
1 | registerSniffs($sniffFiles, [], []);
27 | $ruleset->populateTokenListeners();
28 | if (! file_exists($fixtureFile)) {
29 | throw new \Exception('Fixture file does not exist! ' . $fixtureFile);
30 | }
31 | return new LocalFile($fixtureFile, $ruleset, $config);
32 | }
33 |
34 | public function prepareLocalFilesForSniffs($sniffFiles, $fixtureFiles): FileList {
35 | $config = new Config();
36 | if (! is_array($fixtureFiles)) {
37 | $fixtureFiles = [$fixtureFiles];
38 | }
39 | $config->files = $fixtureFiles;
40 | $ruleset = new Ruleset($config);
41 | if (! is_array($sniffFiles)) {
42 | $sniffFiles = [$sniffFiles];
43 | }
44 | $ruleset->registerSniffs($sniffFiles, [], []);
45 | $ruleset->populateTokenListeners();
46 | return new FileList($config, $ruleset);
47 | }
48 |
49 | public function processFiles($sniffFiles) {
50 | foreach ($sniffFiles as $phpcsFile) {
51 | if (! file_exists($phpcsFile->path)) {
52 | throw new \Exception('Fixture file does not exist! ' . $phpcsFile->path);
53 | }
54 | $phpcsFile->process();
55 | }
56 | }
57 |
58 | public function getWarningMessageRecords(array $messages) {
59 | $messageRecords = [];
60 | foreach ($messages as $rowNumber => $messageRow) {
61 | foreach ($messageRow as $columnNumber => $messageArrays) {
62 | foreach ($messageArrays as $messageArray) {
63 | $messageRecord = new MessageRecord();
64 | $messageRecord->rowNumber = $rowNumber;
65 | $messageRecord->columnNumber = $columnNumber;
66 | $messageRecord->message = $messageArray['message'];
67 | $messageRecord->source = $messageArray['source'];
68 | $messageRecords[] = $messageRecord;
69 | }
70 | }
71 | }
72 | return $messageRecords;
73 | }
74 |
75 | public function getLineNumbersFromMessages(array $messages): array {
76 | $lines = array_keys($messages);
77 | sort($lines);
78 | return $lines;
79 | }
80 |
81 | public function getNoticesFromFiles($phpcsFiles, string $noticeType): array {
82 | $noticesByFile = [];
83 | foreach ($phpcsFiles as $phpcsFile) {
84 | switch ($noticeType) {
85 | case 'warning':
86 | $noticesByFile[$phpcsFile->path] = $this->getLineNumbersFromMessages($phpcsFile->getWarnings());
87 | break;
88 | case 'error':
89 | $noticesByFile[$phpcsFile->path] = $this->getLineNumbersFromMessages($phpcsFile->getErrors());
90 | break;
91 | default:
92 | throw new \Exception("Invalid notice type '{$noticeType}'");
93 | }
94 | }
95 | return $noticesByFile;
96 | }
97 |
98 | public function getWarningLineNumbersFromFile(LocalFile $phpcsFile): array {
99 | return $this->getLineNumbersFromMessages($phpcsFile->getWarnings());
100 | }
101 |
102 | public function getErrorLineNumbersFromFile(LocalFile $phpcsFile): array {
103 | return $this->getLineNumbersFromMessages($phpcsFile->getErrors());
104 | }
105 |
106 | public function getFixedFileContents(LocalFile $phpcsFile) {
107 | $phpcsFile->fixer->startFile($phpcsFile);
108 | $phpcsFile->fixer->fixFile();
109 | return $phpcsFile->fixer->getContents();
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ImportDetection
2 |
3 | A set of [phpcs](https://github.com/squizlabs/PHP_CodeSniffer) sniffs to look for unused or unimported symbols.
4 |
5 | This adds a sniff which shows warnings if a symbol (function, constant, class) is used and is not defined directly, imported explicitly, nor has its namespace imported.
6 |
7 | > [!WARNING]
8 | > PHP 8 changed how `use` statements are tokenized, leading to [this bug](https://github.com/sirbrillig/phpcs-import-detection/issues/52) which basically breaks this sniff. This sniff also has fairly poor performance. I don't have time with my current work to continue to refactor this sniff at the moment and I wouldn't recommend it until at least that issue is fixed. If anyone wants to work on improvements, feel free to open a PR!
9 |
10 | When code is moved around, it can be problematic if classes which are used in a relative or global context get moved to a different namespace. In those cases it's better if the classes use their fully-qualified namespace, or if they are imported explicitly using `use` (in which case they can be detected by a linter like this one). These warnings should help when refactoring code to avoid bugs.
11 |
12 | It also detects imports which are _not_ being used.
13 |
14 | For example:
15 |
16 | ```php
17 | namespace Vehicles;
18 | use Registry;
19 | use function Vehicles\startCar;
20 | use Chocolate; // this will be a warning because `Chocolate` is never used
21 | class Car {
22 | public function drive() {
23 | startCar(); // this is fine because `startCar` is imported
24 | Registry\registerCar($this); // this is fine because `Registry` is imported
25 | \DrivingTracker\registerDrive($this); // this is fine because it's fully-qualified
26 | goFaster(); // this will be a warning because `goFaster` was not imported
27 | }
28 | }
29 | ```
30 |
31 | **Note:** This sniff is a lightweight syntax checker providing a scan of the current file and it doesn't know what other files might have defined. Therefore it will warn you about implicitly imported symbols even if they're in the same namespace. It's safe to import something from the same namespace and can even improve readability, but if you'd prefer to scan multiple files, I suggest using static analysis tools like [psalm](https://psalm.dev/) or [phpstan](https://github.com/phpstan/phpstan).
32 |
33 | ## Installation
34 |
35 | To use these rules in a project which is set up using [composer](https://href.li/?https://getcomposer.org/), we recommend using the [phpcodesniffer-composer-installer library](https://href.li/?https://github.com/DealerDirect/phpcodesniffer-composer-installer) which will automatically use all installed standards in the current project with the composer type `phpcodesniffer-standard` when you run phpcs.
36 |
37 | ```
38 | composer require --dev sirbrillig/phpcs-import-detection dealerdirect/phpcodesniffer-composer-installer
39 | ```
40 |
41 | ## Configuration
42 |
43 | When installing sniff standards in a project, you edit a `phpcs.xml` file with the `rule` tag inside the `ruleset` tag. The `ref` attribute of that tag should specify a standard, category, sniff, or error code to enable. It’s also possible to use these tags to disable or modify certain rules. The [official annotated file](https://href.li/?https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml) explains how to do this.
44 |
45 | ```xml
46 |
47 |
48 | My library.
49 |
50 |
51 | ```
52 |
53 | ## Sniff Codes
54 |
55 | There are two sniff codes that are reported by this sniff. Both are warnings.
56 |
57 | - `ImportDetection.Imports.RequireImports.Symbol`: A symbol has been used but not imported
58 | - `ImportDetection.Imports.RequireImports.Import`: A symbol has been imported and not used
59 |
60 | In any given file, you can use phpcs comments to disable these sniffs. For example, if you have a global class called `MyGlobalClass` which you don't want to import, you could use it like this:
61 |
62 | ```php
63 | doSomething();
67 | ```
68 |
69 | For a whole file, you can ignore a sniff like this:
70 |
71 | ```php
72 | doSomething();
77 | ```
78 |
79 | For a whole project, you can use the `phpcs.xml` file to disable these sniffs or modify their priority. For example, to disable checks for unused imports, you could use a configuration like this:
80 |
81 | ```xml
82 |
83 |
84 | My library.
85 |
86 |
87 | 0
88 |
89 |
90 | ```
91 |
92 | ## Ignoring Symbol Patterns
93 |
94 | Oftentimes there might be global symbols that you want to use without importing or using a fully-qualified path.
95 |
96 | (Remember that function call resolution first searches the current namespace, then the global namespace, but constant and class resolution only searches the current namespace! You still have to import things like `Exception` or use the fully-qualified `\Exception`.)
97 |
98 | You can ignore certain patterns by using the `ignoreUnimportedSymbols` config option. It is a regular expression. Here is an example for some common WordPress symbols:
99 |
100 | ```xml
101 |
102 |
103 | My library.
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | ```
112 |
113 | Despite the name, you can also use the `ignoreUnimportedSymbols` pattern to ignore specific unused imports.
114 |
115 | ## Ignoring Global Symbols in Global Namespace
116 |
117 | If a file is in the global namespace, then sometimes it may be unnecessary to import functions that are also global. If you'd like to ignore global symbol use in the global namespace, you can enable the `ignoreGlobalsWhenInGlobalScope` option, like this:
118 |
119 | ```xml
120 |
121 |
122 | My library.
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | ```
131 |
132 | ## Ignoring WordPress Patterns
133 |
134 | A common use-case is to ignore all the globally available WordPress symbols. Rather than trying to come up with a pattern to ignore them all yourself, you can set the config option `ignoreWordPressSymbols` which will ignore as many of them as it knows about. For example:
135 |
136 | ```xml
137 |
138 |
139 | My library.
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | ```
148 |
149 | ## Usage
150 |
151 | Most editors have a phpcs plugin available, but you can also run phpcs manually. To run phpcs on a file in your project, just use the command-line as follows (the `-s` causes the sniff code to be shown, which is very important for learning about an error).
152 |
153 | ```
154 | vendor/bin/phpcs -s src/MyProject/MyClass.php
155 | ```
156 |
157 | ## See Also
158 |
159 | - [VariableAnalysis](https://github.com/sirbrillig/phpcs-variable-analysis): Find undefined and unused variables.
160 |
--------------------------------------------------------------------------------
/ImportDetection/SniffHelpers.php:
--------------------------------------------------------------------------------
1 | getTokens();
12 | $prevPtr = $phpcsFile->findPrevious([T_OBJECT_OPERATOR], $stackPtr - 1, $stackPtr - 2);
13 | return ($prevPtr && isset($tokens[$prevPtr]));
14 | }
15 |
16 | public function isStaticReference(File $phpcsFile, $stackPtr) {
17 | $tokens = $phpcsFile->getTokens();
18 | $prevPtr = $phpcsFile->findPrevious([T_DOUBLE_COLON], $stackPtr - 1, $stackPtr - 2);
19 | return ($prevPtr && isset($tokens[$prevPtr]));
20 | }
21 |
22 | // Borrowed this idea from
23 | // https://pear.php.net/reference/PHP_CodeSniffer-3.1.1/apidoc/PHP_CodeSniffer/LowercasePHPFunctionsSniff.html
24 | public function isBuiltInFunction(File $phpcsFile, $stackPtr) {
25 | $allFunctions = get_defined_functions();
26 | $builtInFunctions = array_flip($allFunctions['internal']);
27 | $tokens = $phpcsFile->getTokens();
28 | $functionName = $tokens[$stackPtr]['content'];
29 | return isset($builtInFunctions[strtolower($functionName)]);
30 | }
31 |
32 | public function isPredefinedTypehint(File $phpcsFile, $stackPtr) {
33 | $allTypehints = [
34 | 'bool',
35 | 'string',
36 | 'mixed',
37 | 'object',
38 | 'int',
39 | 'float',
40 | 'void',
41 | 'self',
42 | 'array',
43 | 'callable',
44 | 'iterable',
45 | ];
46 | $tokens = $phpcsFile->getTokens();
47 | $tokenContent = $tokens[$stackPtr]['content'];
48 | return in_array($tokenContent, $allTypehints, true);
49 | }
50 |
51 | public function isPredefinedConstant(File $phpcsFile, $stackPtr) {
52 | $allConstants = get_defined_constants();
53 | $tokens = $phpcsFile->getTokens();
54 | $constantName = $tokens[$stackPtr]['content'];
55 | return isset($allConstants[$constantName]);
56 | }
57 |
58 | public function isPredefinedClass(File $phpcsFile, $stackPtr) {
59 | $allClasses = get_declared_classes();
60 | $tokens = $phpcsFile->getTokens();
61 | $className = $tokens[$stackPtr]['content'];
62 | return in_array($className, $allClasses);
63 | }
64 |
65 | public function getImportType(File $phpcsFile, $stackPtr): string {
66 | $tokens = $phpcsFile->getTokens();
67 | if (! empty($tokens[$stackPtr]['conditions'])) {
68 | return 'trait-application';
69 | }
70 | $nextStringPtr = $phpcsFile->findNext([T_STRING], $stackPtr + 1);
71 | if (! $nextStringPtr) {
72 | return 'unknown';
73 | }
74 | $isClosureImport = $phpcsFile->findNext([T_OPEN_PARENTHESIS], $stackPtr + 1, $nextStringPtr);
75 | if ($isClosureImport) {
76 | return 'closure';
77 | }
78 | $nextString = $tokens[$nextStringPtr];
79 | if ($nextString['content'] === 'function') {
80 | return 'function';
81 | }
82 | if ($nextString['content'] === 'const') {
83 | return 'const';
84 | }
85 | return 'class';
86 | }
87 |
88 | private function getImportedSymbolsFromGroupStatement(File $phpcsFile, int $stackPtr): array {
89 | $tokens = $phpcsFile->getTokens();
90 | $endBracketPtr = $phpcsFile->findNext([T_CLOSE_USE_GROUP], $stackPtr + 1);
91 | $startBracketPtr = $phpcsFile->findNext([T_OPEN_USE_GROUP], $stackPtr + 1);
92 | if (! $endBracketPtr || ! $startBracketPtr) {
93 | throw new \Exception('Invalid group import statement starting at token ' . $stackPtr . ': ' . $tokens[$stackPtr]['content']);
94 | }
95 |
96 | // Get the namespace for the import first, so we can attach it to each Symbol
97 | $endOfImportNamespace = $phpcsFile->findPrevious([T_NS_SEPARATOR], $startBracketPtr - 1);
98 | if (! $endOfImportNamespace) {
99 | throw new \Exception('Unable to parse namespace for group import statement starting at token ' . $stackPtr . ': ' . $tokens[$stackPtr]['content']);
100 | }
101 | $importPrefixSymbol = $this->getFullSymbol($phpcsFile, $endOfImportNamespace);
102 |
103 | $collectedSymbols = [];
104 | $lastStringPtr = $startBracketPtr;
105 |
106 | while ($lastStringPtr < $endBracketPtr) {
107 | $commaPtr = $phpcsFile->findNext([T_COMMA], $lastStringPtr + 1, $endBracketPtr) ?: $endBracketPtr;
108 | $aliasPtr = $phpcsFile->findNext([T_AS], $lastStringPtr + 1, $commaPtr);
109 | $aliasSymbol = null;
110 |
111 | if ($aliasPtr) {
112 | $aliasStringPtr = $phpcsFile->findNext([T_STRING], $aliasPtr + 1, $commaPtr);
113 | $aliasSymbol = new Symbol([Symbol::getTokenWithPosition($tokens[$aliasStringPtr], $aliasStringPtr)]);
114 | }
115 |
116 | $endOfStringsPtr = $aliasPtr ?: $commaPtr;
117 | $importSuffixSymbols = [];
118 |
119 | do {
120 | $lastStringPtr = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], $lastStringPtr + 1, $endOfStringsPtr);
121 |
122 | if ($lastStringPtr) {
123 | $importSuffixSymbols[] = Symbol::getTokenWithPosition($tokens[$lastStringPtr], $lastStringPtr);
124 | }
125 | } while ($lastStringPtr);
126 |
127 | $lastStringPtr = $commaPtr;
128 |
129 | if (! empty($importSuffixSymbols)) {
130 | $importCompleteSymbol = array_merge($importPrefixSymbol->getTokens(), $importSuffixSymbols);
131 | $collectedSymbols[] = new Symbol($importCompleteSymbol, $aliasSymbol);
132 | }
133 | }
134 |
135 | return $collectedSymbols;
136 | }
137 |
138 | public function getImportNames(File $phpcsFile, $stackPtr): array {
139 | $symbols = $this->getImportedSymbolsFromImportStatement($phpcsFile, $stackPtr);
140 | return array_map(function ($symbol) {
141 | return $symbol->getAlias();
142 | }, $symbols);
143 | }
144 |
145 | public function getImportedSymbolsFromImportStatement(File $phpcsFile, $stackPtr): array {
146 | $tokens = $phpcsFile->getTokens();
147 |
148 | $endOfStatementPtr = $phpcsFile->findNext([T_SEMICOLON], $stackPtr + 1);
149 | if (! $endOfStatementPtr) {
150 | return [];
151 | }
152 |
153 | // Process grouped imports differently
154 | $nextBracketPtr = $phpcsFile->findNext([T_OPEN_USE_GROUP], $stackPtr + 1, $endOfStatementPtr);
155 | if ($nextBracketPtr) {
156 | return $this->getImportedSymbolsFromGroupStatement($phpcsFile, $stackPtr);
157 | }
158 |
159 | // Get the last string before the last semicolon, comma, or closing curly bracket
160 | $endOfImportPtr = $phpcsFile->findPrevious(
161 | [T_COMMA, T_CLOSE_USE_GROUP],
162 | $stackPtr + 1,
163 | $endOfStatementPtr
164 | );
165 | if (! $endOfImportPtr) {
166 | $endOfImportPtr = $endOfStatementPtr;
167 | }
168 | $lastStringPtr = $phpcsFile->findPrevious([T_STRING], $endOfImportPtr - 1, $stackPtr);
169 | if (! $lastStringPtr || ! isset($tokens[$lastStringPtr])) {
170 | return [];
171 | }
172 | return [$this->getFullSymbol($phpcsFile, $lastStringPtr)];
173 | }
174 |
175 | public function getPreviousStatementPtr(File $phpcsFile, int $stackPtr): int {
176 | return $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET], $stackPtr - 1) ?: 1;
177 | }
178 |
179 | public function isWithinDeclareCall(File $phpcsFile, $stackPtr): bool {
180 | $previousStatementPtr = $this->getPreviousStatementPtr($phpcsFile, $stackPtr);
181 | return !! $phpcsFile->findPrevious([T_DECLARE], $stackPtr - 1, $previousStatementPtr);
182 | }
183 |
184 | public function isWithinDefineCall(File $phpcsFile, $stackPtr): bool {
185 | $previousStatementPtr = $this->getPreviousStatementPtr($phpcsFile, $stackPtr);
186 | return !! $phpcsFile->findPrevious([T_STRING], $stackPtr - 1, $previousStatementPtr, false, 'define');
187 | }
188 |
189 | public function isWithinNamespaceStatement(File $phpcsFile, $stackPtr): bool {
190 | $previousStatementPtr = $this->getPreviousStatementPtr($phpcsFile, $stackPtr);
191 | return !! $phpcsFile->findPrevious([T_NAMESPACE], $stackPtr - 1, $previousStatementPtr);
192 | }
193 |
194 | public function isWithinImportStatement(File $phpcsFile, $stackPtr): bool {
195 | $tokens = $phpcsFile->getTokens();
196 | if (! empty($tokens[$stackPtr]['conditions'])) {
197 | return false;
198 | }
199 | $isClosureImport = $phpcsFile->findNext([T_OPEN_PARENTHESIS], $stackPtr + 1, $stackPtr + 5);
200 | if ($isClosureImport) {
201 | return false;
202 | }
203 | $previousStatementPtr = $this->getPreviousStatementPtr($phpcsFile, $stackPtr);
204 | return !! $phpcsFile->findPrevious([T_USE], $stackPtr - 1, $previousStatementPtr);
205 | }
206 |
207 | /**
208 | * @return array|null
209 | */
210 | public function getPreviousNonWhitespaceToken(File $phpcsFile, int $stackPtr) {
211 | $tokens = $phpcsFile->getTokens();
212 | $prevNonWhitespacePtr = $phpcsFile->findPrevious(T_WHITESPACE, $stackPtr - 1, $stackPtr - 3, true, null, false);
213 | if (! $prevNonWhitespacePtr || ! isset($tokens[$prevNonWhitespacePtr])) {
214 | return null;
215 | }
216 | return $tokens[$prevNonWhitespacePtr];
217 | }
218 |
219 | public function getConstantName(File $phpcsFile, $stackPtr) {
220 | $tokens = $phpcsFile->getTokens();
221 | $nextStringPtr = $phpcsFile->findNext([T_STRING], $stackPtr + 1, $stackPtr + 3);
222 | if (! $nextStringPtr || ! isset($tokens[$nextStringPtr])) {
223 | return null;
224 | }
225 | return $tokens[$nextStringPtr]['content'];
226 | }
227 |
228 | public function isSymbolADefinition(File $phpcsFile, Symbol $symbol): bool {
229 | // if the previous non-whitespace token is const, function, class, or trait, it is a definition
230 | // Note: this does not handle use statements, for that use isWithinImportStatement
231 | $stackPtr = $symbol->getSymbolPosition();
232 | $prevToken = $this->getPreviousNonWhitespaceToken($phpcsFile, $stackPtr) ?? [];
233 | return $this->isTokenADefinition($prevToken) || $this->isWithinDefineCall($phpcsFile, $stackPtr) || $this->isWithinDeclareCall($phpcsFile, $stackPtr);
234 | }
235 |
236 | public function isTokenADefinition(array $token): bool {
237 | // Note: this does not handle use or define
238 | $type = $token['type'] ?? '';
239 | $definitionTypes = ['T_CLASS', 'T_FUNCTION', 'T_CONST', 'T_INTERFACE', 'T_TRAIT'];
240 | return in_array($type, $definitionTypes, true);
241 | }
242 |
243 | public function getFullSymbol($phpcsFile, $stackPtr): Symbol {
244 | $originalPtr = $stackPtr;
245 | $tokens = $phpcsFile->getTokens();
246 | // go backwards and forward and collect all the tokens until we encounter
247 | // anything other than a backslash or a string
248 | $currentToken = Symbol::getTokenWithPosition($tokens[$stackPtr], $stackPtr);
249 | $fullSymbolParts = [];
250 | while ($this->isTokenASymbolPart($currentToken)) {
251 | $fullSymbolParts[] = $currentToken;
252 | $stackPtr--;
253 | $currentToken = Symbol::getTokenWithPosition($tokens[$stackPtr] ?? [], $stackPtr);
254 | }
255 | $fullSymbolParts = array_reverse($fullSymbolParts);
256 | $stackPtr = $originalPtr + 1;
257 | $currentToken = Symbol::getTokenWithPosition($tokens[$stackPtr] ?? [], $stackPtr);
258 | while ($this->isTokenASymbolPart($currentToken)) {
259 | $fullSymbolParts[] = $currentToken;
260 | $stackPtr++;
261 | $currentToken = Symbol::getTokenWithPosition($tokens[$stackPtr] ?? [], $stackPtr);
262 | }
263 | return new Symbol($fullSymbolParts);
264 | }
265 |
266 | public function isTokenASymbolPart(array $token): bool {
267 | $type = $token['type'] ?? '';
268 | $symbolParts = ['T_NS_SEPARATOR', 'T_STRING', 'T_RETURN_TYPE'];
269 | return in_array($type, $symbolParts, true);
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/tests/Sniffs/Imports/RequireImportsSniffTest.php:
--------------------------------------------------------------------------------
1 | prepareLocalFileForSniffs($sniffFile, $fixtureFile);
14 | $phpcsFile->ruleset->setSniffProperty(
15 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
16 | 'ignoreUnimportedSymbols',
17 | '/^(something_to_ignore|whitelisted_function|allowed_funcs_\w+)$/'
18 | );
19 | $phpcsFile->process();
20 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
21 | $expectedLines = [
22 | 10,
23 | 15,
24 | 19,
25 | 27,
26 | 30,
27 | 34,
28 | 37,
29 | 39,
30 | 47,
31 | 52,
32 | 57,
33 | 62,
34 | 69,
35 | 71,
36 | 73,
37 | 79,
38 | 87,
39 | 89,
40 | 95,
41 | ];
42 | $this->assertEquals($expectedLines, $lines);
43 | $this->assertSame(count($expectedLines), $phpcsFile->getWarningCount());
44 | }
45 |
46 | public function testRequireImportsSniffFindsUnimportedFunctionsWithNoConfig() {
47 | $fixtureFile = __DIR__ . '/Fixtures/RequireImportsAllowedPatternFixture.php';
48 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
49 | $helper = new SniffTestHelper();
50 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
51 | $phpcsFile->ruleset->setSniffProperty(
52 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
53 | 'ignoreUnimportedSymbols',
54 | ''
55 | );
56 | $phpcsFile->process();
57 | $messages = $helper->getWarningMessageRecords($phpcsFile->getWarnings());
58 | $messages = array_values(array_filter($messages, function ($message) {
59 | return $message->source === 'ImportDetection.Imports.RequireImports.Symbol';
60 | }));
61 | $lines = array_map(function ($message) {
62 | return $message->rowNumber;
63 | }, $messages);
64 | $expectedLines = [
65 | 15,
66 | 16,
67 | ];
68 | $this->assertEquals($expectedLines, $lines);
69 | }
70 |
71 | public function testRequireImportsSniffFindsUnusedImportsWithNoConfig() {
72 | $fixtureFile = __DIR__ . '/Fixtures/RequireImportsAllowedPatternFixture.php';
73 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
74 | $helper = new SniffTestHelper();
75 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
76 | $phpcsFile->ruleset->setSniffProperty(
77 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
78 | 'ignoreUnimportedSymbols',
79 | ''
80 | );
81 | $phpcsFile->process();
82 | $messages = $helper->getWarningMessageRecords($phpcsFile->getWarnings());
83 | $messages = array_values(array_filter($messages, function ($message) {
84 | return $message->source === 'ImportDetection.Imports.RequireImports.Import';
85 | }));
86 | $lines = array_map(function ($message) {
87 | return $message->rowNumber;
88 | }, $messages);
89 | $expectedLines = [
90 | 8,
91 | 9,
92 | ];
93 | $this->assertEquals($expectedLines, $lines);
94 | }
95 |
96 | public function testRequireImportsSniffIgnoresWhitelistedUnimportedSymbols() {
97 | $fixtureFile = __DIR__ . '/Fixtures/RequireImportsAllowedPatternFixture.php';
98 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
99 | $helper = new SniffTestHelper();
100 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
101 | $phpcsFile->ruleset->setSniffProperty(
102 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
103 | 'ignoreUnimportedSymbols',
104 | '/^(something_to_ignore|whitelisted_function|allowed_funcs_\w+|another_[a-z_]+)$/'
105 | );
106 | $phpcsFile->process();
107 | $messages = $helper->getWarningMessageRecords($phpcsFile->getWarnings());
108 | $messages = array_values(array_filter($messages, function ($message) {
109 | return $message->source === 'ImportDetection.Imports.RequireImports.Symbol';
110 | }));
111 | $lines = array_map(function ($message) {
112 | return $message->rowNumber;
113 | }, $messages);
114 | $expectedLines = [
115 | 16,
116 | ];
117 | $this->assertEquals($expectedLines, $lines);
118 | }
119 |
120 | public function testRequireImportsSniffIgnoresWhitelistedUnusedImports() {
121 | $fixtureFile = __DIR__ . '/Fixtures/RequireImportsAllowedPatternFixture.php';
122 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
123 | $helper = new SniffTestHelper();
124 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
125 | $phpcsFile->ruleset->setSniffProperty(
126 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
127 | 'ignoreUnimportedSymbols',
128 | '/\\\unused_function|\\\four_function/'
129 | );
130 | $phpcsFile->process();
131 | $messages = $helper->getWarningMessageRecords($phpcsFile->getWarnings());
132 | $messages = array_values(array_filter($messages, function ($message) {
133 | return $message->source === 'ImportDetection.Imports.RequireImports.Import';
134 | }));
135 | $lines = array_map(function ($message) {
136 | return $message->rowNumber;
137 | }, $messages);
138 | $expectedLines = [];
139 | $this->assertEquals($expectedLines, $lines);
140 | }
141 |
142 | public function testRequireImportsSniffFindsWordPressPatternsIfNotSet() {
143 | $fixtureFile = __DIR__ . '/Fixtures/WordPressFixture.php';
144 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
145 | $helper = new SniffTestHelper();
146 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
147 | $phpcsFile->ruleset->setSniffProperty(
148 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
149 | 'ignoreWordPressSymbols',
150 | 'false'
151 | );
152 | $phpcsFile->process();
153 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
154 | $expectedLines = [
155 | 23,
156 | 32,
157 | 33,
158 | 34,
159 | 35,
160 | 36,
161 | 37,
162 | 38,
163 | 42,
164 | 45,
165 | 48,
166 | 52,
167 | 55,
168 | 60,
169 | 61,
170 | 63,
171 | 74,
172 | 80,
173 | ];
174 | $this->assertEquals($expectedLines, $lines);
175 | }
176 |
177 | public function testRequireImportsSniffIgnoresWordPressPatternsIfSet() {
178 | $fixtureFile = __DIR__ . '/Fixtures/WordPressFixture.php';
179 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
180 | $helper = new SniffTestHelper();
181 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
182 | $phpcsFile->ruleset->setSniffProperty(
183 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
184 | 'ignoreWordPressSymbols',
185 | 'true'
186 | );
187 | $phpcsFile->process();
188 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
189 | $expectedLines = [ 38, 61, 63, 80 ];
190 | $this->assertEquals($expectedLines, $lines);
191 | }
192 |
193 | public function testRequireImportsSniffDoesNotCountMethodNames() {
194 | $fixtureFile = __DIR__ . '/Fixtures/RequireImportsMethodNameFixture.php';
195 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
196 | $helper = new SniffTestHelper();
197 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
198 | $phpcsFile->process();
199 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
200 | $expectedLines = [ 11 ];
201 | $this->assertEquals($expectedLines, $lines);
202 | }
203 |
204 | public function testRequireImportsSniffCountsTraitUseAsUsage() {
205 | $fixtureFile = __DIR__ . '/Fixtures/UsedTraitFixture.php';
206 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
207 | $helper = new SniffTestHelper();
208 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
209 | $phpcsFile->process();
210 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
211 | $expectedLines = [10];
212 | $this->assertEquals($expectedLines, $lines);
213 | }
214 |
215 | public function testRequireImportsSniffWorksWithInterfaces() {
216 | $fixtureFile = __DIR__ . '/Fixtures/InterfaceFixture.php';
217 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
218 | $helper = new SniffTestHelper();
219 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
220 | $phpcsFile->process();
221 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
222 | $expectedLines = [31];
223 | $this->assertEquals($expectedLines, $lines);
224 | }
225 |
226 | public function testRequireImportsSniffWorksWithTraits() {
227 | $fixtureFile = __DIR__ . '/Fixtures/TraitFixture.php';
228 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
229 | $helper = new SniffTestHelper();
230 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
231 | $phpcsFile->process();
232 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
233 | $expectedLines = [37];
234 | $this->assertEquals($expectedLines, $lines);
235 | }
236 |
237 | public function testRequireImportsSniffFindsGlobalSymbolsIfNoConfig() {
238 | $fixtureFile = __DIR__ . '/Fixtures/GlobalNamespaceFixture.php';
239 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
240 | $helper = new SniffTestHelper();
241 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
242 | $phpcsFile->process();
243 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
244 | $expectedLines = [
245 | 6,
246 | 7,
247 | 13,
248 | 14,
249 | 19,
250 | ];
251 | $this->assertEquals($expectedLines, $lines);
252 | }
253 |
254 | public function testRequireImportsSniffIgnoresGlobalSymbolsIfConfigured() {
255 | $fixtureFile = __DIR__ . '/Fixtures/GlobalNamespaceFixture.php';
256 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
257 | $helper = new SniffTestHelper();
258 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
259 | $phpcsFile->ruleset->setSniffProperty(
260 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
261 | 'ignoreGlobalsWhenInGlobalScope',
262 | 'true'
263 | );
264 | $phpcsFile->process();
265 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
266 | $expectedLines = [
267 | 6,
268 | 7,
269 | 19,
270 | ];
271 | $this->assertEquals($expectedLines, $lines);
272 | }
273 |
274 | public function testRequireImportsSniffFindsGlobalSymbolsInNamespaceIfConfigured() {
275 | $fixtureFile = __DIR__ . '/Fixtures/RequireImportsAllowedPatternFixture.php';
276 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
277 | $helper = new SniffTestHelper();
278 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
279 | $phpcsFile->ruleset->setSniffProperty(
280 | 'ImportDetection\Sniffs\Imports\RequireImportsSniff',
281 | 'ignoreGlobalsWhenInGlobalScope',
282 | 'true'
283 | );
284 | $phpcsFile->process();
285 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
286 | $expectedLines = [
287 | 8,
288 | 9,
289 | 15,
290 | 16,
291 | ];
292 | $this->assertEquals($expectedLines, $lines);
293 | }
294 |
295 | public function testRequireImportsDoesNotBleedToMultipleFiles() {
296 | $fixtureFile = __DIR__ . '/Fixtures/MultipleFilesFixtures';
297 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
298 | $helper = new SniffTestHelper();
299 | $phpcsFiles = $helper->prepareLocalFilesForSniffs($sniffFile, $fixtureFile);
300 | // Unclear why this works, but if I run this twice the first fixture file
301 | // gets all its warnings cleared (and the other fixture file has the same
302 | // warning twice).
303 | $helper->processFiles($phpcsFiles);
304 | $helper->processFiles($phpcsFiles);
305 | $linesByFile = $helper->getNoticesFromFiles($phpcsFiles, 'warning');
306 | $expectedLines = [
307 | // The runner runs 'MultipleFilesFixtures2' first.
308 | __DIR__ . '/Fixtures/MultipleFilesFixtures/MultipleFilesFixtures2.php' => [],
309 | __DIR__ . '/Fixtures/MultipleFilesFixtures/MultipleFilesFixtures1.php' => [5],
310 | ];
311 | $this->assertEquals($expectedLines, $linesByFile);
312 | }
313 |
314 | public function testRequireImportsFindsUnimportedNamespaceIdenticalToClass() {
315 | $fixtureFile = __DIR__ . '/Fixtures/ClassUsedAsNamespaceFixture.php';
316 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
317 | $helper = new SniffTestHelper();
318 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
319 | $phpcsFile->process();
320 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
321 | $expectedLines = [
322 | 7,
323 | ];
324 | $this->assertEquals($expectedLines, $lines);
325 | }
326 |
327 | public function testRequireImportsNoticesUnusedClasses() {
328 | $fixtureFile = __DIR__ . '/Fixtures/ClassFixtures.php';
329 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
330 | $helper = new SniffTestHelper();
331 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
332 | $phpcsFile->process();
333 |
334 | $warnings = $phpcsFile->getWarnings();
335 | $messages = $helper->getWarningMessageRecords($warnings);
336 | $messages = array_values(array_filter($messages, function ($message) {
337 | return $message->source === 'ImportDetection.Imports.RequireImports.Import';
338 | }));
339 | $lines = array_map(function ($message) {
340 | return $message->rowNumber;
341 | }, $messages);
342 | $expectedLines = [
343 | 2,
344 | 7,
345 | 7,
346 | ];
347 | $this->assertEquals($expectedLines, $lines);
348 | $this->assertCount(3, $messages);
349 | $this->assertEquals('Found unused symbol \'NamespaceName\C\D\'.', $messages[0]->message);
350 | $this->assertEquals('Found unused symbol \'NamespaceName\B\'.', $messages[1]->message);
351 | $this->assertEquals('Found unused symbol \'NamespaceName\I\J\'.', $messages[2]->message);
352 | }
353 |
354 | public function testRequireImportsNoticesUnusedConstants() {
355 | $fixtureFile = __DIR__ . '/Fixtures/ConstantsFixure.php';
356 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
357 | $helper = new SniffTestHelper();
358 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
359 | $phpcsFile->process();
360 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
361 | $expectedLines = [
362 | 3,
363 | 12,
364 | ];
365 | $this->assertEquals($expectedLines, $lines);
366 | }
367 |
368 | public function testRequireImportsSniffTreatsFileImportAsUsedWhenUsed() {
369 | $fixtureFile = __DIR__ . '/Fixtures/FileKeywordFixture.php';
370 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
371 | $helper = new SniffTestHelper();
372 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
373 | $phpcsFile->process();
374 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
375 | $expectedLines = [];
376 | $this->assertEquals($expectedLines, $lines);
377 | }
378 |
379 | public function testRequireImportsNoticesNestedFunctions() {
380 | $fixtureFile = __DIR__ . '/Fixtures/NestedFunctionsFixture.php';
381 | $sniffFile = __DIR__ . '/../../../ImportDetection/Sniffs/Imports/RequireImportsSniff.php';
382 | $helper = new SniffTestHelper();
383 | $phpcsFile = $helper->prepareLocalFileForSniffs($sniffFile, $fixtureFile);
384 | $phpcsFile->process();
385 | $lines = $helper->getWarningLineNumbersFromFile($phpcsFile);
386 | $expectedLines = [
387 | 9,
388 | 10,
389 | 20,
390 | 48,
391 | 52,
392 | 56,
393 | ];
394 | $this->assertEquals($expectedLines, $lines);
395 | }
396 | }
397 |
--------------------------------------------------------------------------------
/ImportDetection/Sniffs/Imports/RequireImportsSniff.php:
--------------------------------------------------------------------------------
1 | getTokens();
26 | $token = $tokens[$stackPtr];
27 | // Keep one set of symbol records per file
28 | $this->symbolRecordsByFile[$phpcsFile->path] = $this->symbolRecordsByFile[$phpcsFile->path] ?? new FileSymbolRecord;
29 | if ($token['type'] === 'T_NAMESPACE') {
30 | return $this->processNamespace($phpcsFile, $stackPtr);
31 | }
32 | if ($token['type'] === 'T_WHITESPACE') {
33 | $this->debug('found whitespace');
34 | return $this->processEndOfFile($phpcsFile, $stackPtr);
35 | }
36 | if ($token['type'] === 'T_USE') {
37 | $this->debug('found import');
38 | if (in_array($helper->getImportType($phpcsFile, $stackPtr), ['function', 'const', 'class'])) {
39 | $this->recordImportedSymbols($phpcsFile, $stackPtr);
40 | }
41 | return $this->processUse($phpcsFile, $stackPtr);
42 | }
43 | $symbol = $helper->getFullSymbol($phpcsFile, $stackPtr);
44 | // If the symbol has been seen before (if this is a duplicate), ignore it
45 | if (in_array($symbol, $this->symbolRecordsByFile[$phpcsFile->path]->seenSymbols)) {
46 | $this->debug('found duplicate symbol: ' . $symbol->getName());
47 | return;
48 | }
49 | $this->symbolRecordsByFile[$phpcsFile->path]->seenSymbols[] = $symbol;
50 | // If the symbol is in the ignore list, ignore it
51 | if ($this->isSymbolIgnored($symbol)) {
52 | $this->debug('found ignored symbol: ' . $symbol->getName());
53 | $this->markSymbolUsed($phpcsFile, $symbol);
54 | return;
55 | }
56 | // If the symbol is a fully-qualified namespace, ignore it
57 | if ($symbol->isAbsoluteNamespace()) {
58 | $this->debug('found absolute namespaced symbol: ' . $symbol->getName());
59 | return;
60 | }
61 | // If this symbol is a definition, ignore it
62 | if ($helper->isSymbolADefinition($phpcsFile, $symbol)) {
63 | $this->debug('found definition symbol: ' . $symbol->getName());
64 | return;
65 | }
66 | // If this symbol is a static reference or an object reference, ignore it
67 | if ($helper->isStaticReference($phpcsFile, $stackPtr) || $helper->isObjectReference($phpcsFile, $stackPtr)) {
68 | $this->debug('found static symbol: ' . $symbol->getName());
69 | return;
70 | }
71 | // If this symbol is a namespace definition, ignore it
72 | if ($helper->isWithinNamespaceStatement($phpcsFile, $symbol->getSymbolPosition())) {
73 | $this->debug('found namespace definition symbol: ' . $symbol->getName());
74 | return;
75 | }
76 | // If this symbol is an import, ignore it
77 | if ($helper->isWithinImportStatement($phpcsFile, $symbol->getSymbolPosition())) {
78 | $this->debug('found symbol inside an import: ' . $symbol->getName());
79 | return;
80 | }
81 | // If the symbol's namespace is imported or defined, ignore it
82 | // If the symbol has no namespace and is itself is imported or defined, ignore it
83 | if ($this->isSymbolDefined($phpcsFile, $symbol)) {
84 | $this->debug('found defined symbol: ' . $symbol->getName());
85 | $this->markSymbolUsed($phpcsFile, $symbol);
86 | return;
87 | }
88 | // If the symbol is predefined, ignore it
89 | if ($helper->isPredefinedConstant($phpcsFile, $stackPtr) || $helper->isBuiltInFunction($phpcsFile, $stackPtr)) {
90 | $this->debug('found predefined symbol: ' . $symbol->getName());
91 | return;
92 | }
93 | // If this symbol is a predefined typehint, ignore it
94 | if ($helper->isPredefinedTypehint($phpcsFile, $stackPtr)) {
95 | $this->debug('found typehint symbol: ' . $symbol->getName());
96 | return;
97 | }
98 | // If the symbol is global, we are in the global namespace, and
99 | // configured to ignore global symbols in the global namespace,
100 | // ignore it
101 | if ($this->ignoreGlobalsWhenInGlobalScope && ! $symbol->isNamespaced() && ! $this->symbolRecordsByFile[$phpcsFile->path]->activeNamespace) {
102 | $this->debug('found global symbol in global namespace: ' . $symbol->getName());
103 | return;
104 | }
105 | $this->debug('found unimported symbol: ' . $symbol->getName());
106 | $error = "Found unimported symbol '{$symbol->getName()}'.";
107 | $phpcsFile->addWarning($error, $stackPtr, 'Symbol');
108 | }
109 |
110 | private function debug(string $message) {
111 | if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
112 | return;
113 | }
114 | if (PHP_CODESNIFFER_VERBOSITY > 3) {
115 | echo PHP_EOL . "RequireImportsSniff: DEBUG: $message" . PHP_EOL;
116 | }
117 | }
118 |
119 | private function isSymbolIgnored(Symbol $symbol): bool {
120 | $ignorePattern = $this->getIgnoredSymbolPattern();
121 | $doesSymbolMatchIgnorePattern = $this->doesSymbolMatchPattern($symbol, $ignorePattern);
122 | if ($doesSymbolMatchIgnorePattern) {
123 | return true;
124 | }
125 |
126 | $wordPressPatterns = $this->getIgnoredWordPressSymbolPatterns();
127 | $matchingWordPressPatterns = array_values(array_filter($wordPressPatterns, function (string $pattern) use ($symbol): bool {
128 | return $this->doesSymbolMatchPattern($symbol, "/{$pattern}/");
129 | }));
130 | return count($matchingWordPressPatterns) > 0;
131 | }
132 |
133 | private function doesSymbolMatchPattern(Symbol $symbol, string $pattern): bool {
134 | $symbolName = $symbol->getName();
135 | if (empty($pattern)) {
136 | return false;
137 | }
138 | try {
139 | return (1 === preg_match($pattern, $symbolName));
140 | } catch (\Exception $err) {
141 | throw new \Exception("Invalid ignore pattern found: '{$pattern}'");
142 | }
143 | }
144 |
145 | private function getIgnoredWordPressSymbolPatterns() {
146 | return empty($this->ignoreWordPressSymbols) ? [] : WordPressSymbols::getWordPressSymbolPatterns();
147 | }
148 |
149 | private function getIgnoredSymbolPattern() {
150 | return $this->ignoreUnimportedSymbols ?? '';
151 | }
152 |
153 | private function isSymbolDefined(File $phpcsFile, Symbol $symbol): bool {
154 | $namespace = $symbol->getTopLevelNamespace();
155 | // If the symbol's namespace is imported, ignore it
156 | if ($namespace) {
157 | return $this->isNamespaceImported($phpcsFile, $namespace);
158 | }
159 | // If the symbol has no namespace and is itself is imported or defined, ignore it
160 | return $this->isNamespaceImportedOrDefined($phpcsFile, $symbol);
161 | }
162 |
163 | private function isNamespaceImported(File $phpcsFile, string $namespace): bool {
164 | return (
165 | $this->isClassImported($phpcsFile, $namespace)
166 | || $this->isFunctionImported($phpcsFile, $namespace)
167 | || $this->isConstImported($phpcsFile, $namespace)
168 | );
169 | }
170 |
171 | private function isSymbolAFunctionCall(File $phpcsFile, Symbol $symbol): bool {
172 | $tokens = $phpcsFile->getTokens();
173 | $stackPtr = $symbol->getSymbolPosition();
174 | if (isset($tokens[$stackPtr + 1]) && $tokens[$stackPtr + 1]['type'] === 'T_OPEN_PARENTHESIS') {
175 | return true;
176 | }
177 | return false;
178 | }
179 |
180 | private function isNamespaceImportedOrDefined(File $phpcsFile, Symbol $symbol): bool {
181 | $namespace = $symbol->getName();
182 | $conditions = $symbol->getSymbolConditions();
183 | return (
184 | $this->isClassImported($phpcsFile, $namespace)
185 | || $this->isClassDefined($phpcsFile, $namespace)
186 | || $this->isFunctionImported($phpcsFile, $namespace)
187 | || $this->isFunctionDefined($phpcsFile, $symbol, $namespace, $conditions)
188 | || $this->isConstImported($phpcsFile, $namespace)
189 | || $this->isConstDefined($phpcsFile, $namespace)
190 | );
191 | }
192 |
193 | private function processUse(File $phpcsFile, $stackPtr) {
194 | $helper = new SniffHelpers();
195 | $importType = $helper->getImportType($phpcsFile, $stackPtr);
196 | switch ($importType) {
197 | case 'function':
198 | return $this->saveFunctionImport($phpcsFile, $stackPtr);
199 | case 'const':
200 | return $this->saveConstImport($phpcsFile, $stackPtr);
201 | case 'class':
202 | return $this->saveClassImport($phpcsFile, $stackPtr);
203 | }
204 | }
205 |
206 | private function recordImportedSymbols(File $phpcsFile, int $stackPtr) {
207 | $helper = new SniffHelpers();
208 | $symbols = $helper->getImportedSymbolsFromImportStatement($phpcsFile, $stackPtr);
209 | $this->debug('recording imported symbols: ' . implode(', ', array_map(function (Symbol $symbol): string {
210 | return $symbol->getName();
211 | }, $symbols)));
212 | $symbols = array_map(function ($symbol) {
213 | if ($this->isSymbolIgnored($symbol)) {
214 | $this->debug('found ignored imported symbol: ' . $symbol->getName());
215 | $symbol->markUsed();
216 | }
217 | return $symbol;
218 | }, $symbols);
219 | $this->symbolRecordsByFile[$phpcsFile->path]->addImportedSymbolRecords($symbols);
220 | }
221 |
222 | private function saveFunctionImport(File $phpcsFile, $stackPtr) {
223 | $helper = new SniffHelpers();
224 | $importNames = $helper->getImportNames($phpcsFile, $stackPtr);
225 | $this->symbolRecordsByFile[$phpcsFile->path]->addImportedFunctions($importNames);
226 | }
227 |
228 | private function saveConstImport(File $phpcsFile, $stackPtr) {
229 | $helper = new SniffHelpers();
230 | $importNames = $helper->getImportNames($phpcsFile, $stackPtr);
231 | $this->symbolRecordsByFile[$phpcsFile->path]->addImportedConsts($importNames);
232 | }
233 |
234 | private function saveClassImport(File $phpcsFile, $stackPtr) {
235 | $helper = new SniffHelpers();
236 | $importNames = $helper->getImportNames($phpcsFile, $stackPtr);
237 | $this->symbolRecordsByFile[$phpcsFile->path]->addImportedClasses($importNames);
238 | }
239 |
240 | private function isFunctionImported(File $phpcsFile, string $functionName): bool {
241 | return in_array($functionName, $this->symbolRecordsByFile[$phpcsFile->path]->importedFunctions);
242 | }
243 |
244 | private function isConstImported(File $phpcsFile, string $constName): bool {
245 | return in_array($constName, $this->symbolRecordsByFile[$phpcsFile->path]->importedConsts);
246 | }
247 |
248 | private function isClassImported(File $phpcsFile, string $name): bool {
249 | return in_array($name, $this->symbolRecordsByFile[$phpcsFile->path]->importedClasses);
250 | }
251 |
252 | private function isClassDefined(File $phpcsFile, string $className): bool {
253 | $classPtr = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], 0);
254 | while ($classPtr) {
255 | $thisClassName = $phpcsFile->getDeclarationName($classPtr);
256 | if ($className === $thisClassName) {
257 | return true;
258 | }
259 | $classPtr = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], $classPtr + 1);
260 | }
261 | return false;
262 | }
263 |
264 | private function isFunctionDefined(File $phpcsFile, Symbol $symbol, string $functionName, array $conditions): bool {
265 | $tokens = $phpcsFile->getTokens();
266 |
267 | if (! $this->isSymbolAFunctionCall($phpcsFile, $symbol)) {
268 | return false;
269 | }
270 |
271 | $scopesToEnter = array_filter(array_keys($conditions), function ($conditionPtr) use ($conditions) {
272 | return $conditions[$conditionPtr] === T_FUNCTION;
273 | });
274 | $this->debug("looking for definition for function {$functionName}");
275 | $this->debug("my conditions are " . json_encode($conditions));
276 | $this->debug("scopes to enter " . implode(',', $scopesToEnter));
277 |
278 | // Only look at the inner-most scope and global scope
279 | $scopesToEnter = [end($scopesToEnter), 0];
280 |
281 | foreach ($scopesToEnter as $scopeStart) {
282 | $functionToken = $tokens[$scopeStart];
283 | $scopeEnd = $functionToken['scope_closer'] ?? null;
284 |
285 | // Within each function scope, find all the function definitions and
286 | // compare their names to the name we are looking for.
287 | $functionDefinitionsInScope = $this->findAllFunctionDefinitionsInScope($phpcsFile, $scopeStart, $scopeEnd);
288 |
289 | foreach ($functionDefinitionsInScope as $thisFunctionName) {
290 | $this->debug("is this function the one we want? " . $thisFunctionName);
291 | if ($functionName === $thisFunctionName) {
292 | $this->debug("yes indeed");
293 | return true;
294 | }
295 | }
296 | }
297 | return false;
298 | }
299 |
300 | /**
301 | * Return an array of function names defined in a scope
302 | */
303 | private function findAllFunctionDefinitionsInScope(File $phpcsFile, int $scopeStart, int $scopeEnd = null): array {
304 | $this->debug("looking for functions defined between {$scopeStart} and {$scopeEnd}");
305 | $tokens = $phpcsFile->getTokens();
306 | $functionNames = [];
307 |
308 | $tokensToInvestigate = [T_FUNCTION, T_CLASS, T_TRAIT, T_INTERFACE];
309 |
310 | // Skip the function we are in, but not the global scope
311 | $functionToken = $tokens[$scopeStart];
312 | $scopeOffset = $functionToken['type'] === 'T_FUNCTION' ? 2 : 0;
313 | $functionPtr = $phpcsFile->findNext($tokensToInvestigate, $scopeStart + $scopeOffset, $scopeEnd);
314 |
315 | while ($functionPtr) {
316 | $functionName = $phpcsFile->getDeclarationName($functionPtr);
317 | $functionToken = $tokens[$functionPtr];
318 | $thisFunctionScopeEnd = $functionToken['scope_closer'] ?? 0;
319 |
320 | // Skip things other than IF that have their own scope
321 | if ($functionToken['type'] !== 'T_FUNCTION') {
322 | if (! $thisFunctionScopeEnd) {
323 | $this->debug("function at {$functionPtr} has no end:" . $functionName);
324 | break;
325 | }
326 | $functionPtr = $phpcsFile->findNext($tokensToInvestigate, $thisFunctionScopeEnd, $scopeEnd);
327 | continue;
328 | }
329 |
330 | $this->debug("found function at {$functionPtr}:" . $functionName);
331 | $functionNames[] = $functionName;
332 | $functionPtr = $phpcsFile->findNext($tokensToInvestigate, $thisFunctionScopeEnd, $scopeEnd);
333 | }
334 | return $functionNames;
335 | }
336 |
337 | private function isConstDefined(File $phpcsFile, string $functionName): bool {
338 | $helper = new SniffHelpers();
339 | $functionPtr = $phpcsFile->findNext([T_CONST], 0);
340 | while ($functionPtr) {
341 | $thisFunctionName = $helper->getConstantName($phpcsFile, $functionPtr);
342 | if ($functionName === $thisFunctionName) {
343 | return true;
344 | }
345 | $functionPtr = $phpcsFile->findNext([T_CONST], $functionPtr + 1);
346 | }
347 | return false;
348 | }
349 |
350 | private function markSymbolUsed(File $phpcsFile, Symbol $symbol) {
351 | $record = $this->getRecordedImportedSymbolMatchingSymbol($phpcsFile, $symbol);
352 | if (! $record) {
353 | // Symbol records only exist for imported symbols, so if a used symbol
354 | // has not been imported we don't need to mark anything.
355 | $this->debug("ignoring marking symbol used since it was never imported: {$symbol->getName()}");
356 | return;
357 | }
358 | $record->markUsed();
359 | }
360 |
361 | private function getRecordedImportedSymbolMatchingSymbol(File $phpcsFile, Symbol $symbol) {
362 | foreach ($this->symbolRecordsByFile[$phpcsFile->path]->importedSymbolRecords as $record) {
363 | $namespaceOrAlias = $symbol->getTopLevelNamespace() ?? $symbol->getAlias();
364 | $this->debug("comparing symbol {$namespaceOrAlias} to alias {$record->getAlias()}");
365 | if ($record->getAlias() === $namespaceOrAlias) {
366 | return $record;
367 | }
368 | }
369 | return null;
370 | }
371 |
372 | private function processEndOfFile(File $phpcsFile, int $stackPtr) {
373 | $tokens = $phpcsFile->getTokens();
374 | // If this is not the end of the file, ignore it
375 | if (isset($tokens[$stackPtr + 1])) {
376 | return;
377 | }
378 | // For each import, if the Symbol was not used, mark a warning
379 | foreach ($this->symbolRecordsByFile[$phpcsFile->path]->importedSymbolRecords as $record) {
380 | if (! $record->isUsed()) {
381 | $this->debug("found unused symbol: {$record->getName()}");
382 | $error = "Found unused symbol '{$record->getName()}'.";
383 | $phpcsFile->addWarning($error, $record->getSymbolPosition(), 'Import');
384 | }
385 | }
386 | }
387 |
388 | private function processNamespace(File $phpcsFile, int $stackPtr) {
389 | $helper = new SniffHelpers();
390 | $symbols = $helper->getImportedSymbolsFromImportStatement($phpcsFile, $stackPtr);
391 | if (count($symbols) < 1) {
392 | return;
393 | }
394 | if (count($symbols) > 1) {
395 | throw new \Exception('Found more than one namespace: ' . var_export($symbols, true));
396 | }
397 | $this->debug('we are in the namespace: ' . $symbols[0]->getName());
398 | $this->symbolRecordsByFile[$phpcsFile->path]->activeNamespace = $symbols[0];
399 | }
400 | }
401 |
--------------------------------------------------------------------------------
/ImportDetection/WordPressSymbols.php:
--------------------------------------------------------------------------------
1 |