Skip to content

Commit 2747116

Browse files
authored
Refactor to move scope management to ScopeManager class (sirbrillig#265)
* Fix some minor type errors and improve code comments * Remove unnecessary scopeEndIndexCache * Add more function comments * If no scope closer is found, use the end of the file * Add ScopeManager * Improve index of ScopeManager using scopeStart * Replace scope list with ScopeManager * Move getScopesClosedBy to ScopeManager * Linting fixes * Support php 5.5 by not passing expression to empty) * Remove unused imports
1 parent e1b89d1 commit 2747116

File tree

2 files changed

+130
-120
lines changed

2 files changed

+130
-120
lines changed

VariableAnalysis/Lib/ScopeManager.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace VariableAnalysis\Lib;
4+
5+
use VariableAnalysis\Lib\ScopeInfo;
6+
use VariableAnalysis\Lib\Helpers;
7+
use PHP_CodeSniffer\Files\File;
8+
9+
class ScopeManager
10+
{
11+
/**
12+
* An associative array of a list of token index pairs which start and end
13+
* scopes and will be used to check for unused variables.
14+
*
15+
* The outer array of scopes is keyed by a string containing the filename.
16+
* The inner array of scopes in keyed by the scope start token index.
17+
*
18+
* @var array<string, array<int, ScopeInfo>>
19+
*/
20+
private $scopes = [];
21+
22+
/**
23+
* Add a scope's start and end index to our record for the file.
24+
*
25+
* @param File $phpcsFile
26+
* @param int $scopeStartIndex
27+
*
28+
* @return ScopeInfo
29+
*/
30+
public function recordScopeStartAndEnd(File $phpcsFile, $scopeStartIndex)
31+
{
32+
$scopeEndIndex = Helpers::getScopeCloseForScopeOpen($phpcsFile, $scopeStartIndex);
33+
$filename = $phpcsFile->getFilename();
34+
if (! isset($this->scopes[$filename])) {
35+
$this->scopes[$filename] = [];
36+
}
37+
Helpers::debug('recording scope for file', $filename, 'start/end', $scopeStartIndex, $scopeEndIndex);
38+
$scope = new ScopeInfo($scopeStartIndex, $scopeEndIndex);
39+
$this->scopes[$filename][$scopeStartIndex] = $scope;
40+
return $scope;
41+
}
42+
43+
/**
44+
* Return the scopes for a file.
45+
*
46+
* @param string $filename
47+
*
48+
* @return ScopeInfo[]
49+
*/
50+
public function getScopesForFilename($filename)
51+
{
52+
if (empty($this->scopes[$filename])) {
53+
return [];
54+
}
55+
return array_values($this->scopes[$filename]);
56+
}
57+
58+
/**
59+
* Return the scope for a scope start index.
60+
*
61+
* @param string $filename
62+
* @param int $scopeStartIndex
63+
*
64+
* @return ScopeInfo|null
65+
*/
66+
public function getScopeForScopeStart($filename, $scopeStartIndex)
67+
{
68+
if (empty($this->scopes[$filename][$scopeStartIndex])) {
69+
return null;
70+
}
71+
return $this->scopes[$filename][$scopeStartIndex];
72+
}
73+
74+
/**
75+
* Find scopes closed by a scope close index.
76+
*
77+
* @param string $filename
78+
* @param int $scopeEndIndex
79+
*
80+
* @return ScopeInfo[]
81+
*/
82+
public function getScopesForScopeEnd($filename, $scopeEndIndex)
83+
{
84+
$scopePairsForFile = $this->getScopesForFilename($filename);
85+
$scopeIndicesThisCloses = array_reduce(
86+
$scopePairsForFile,
87+
/**
88+
* @param ScopeInfo[] $found
89+
* @param ScopeInfo $scope
90+
*
91+
* @return ScopeInfo[]
92+
*/
93+
function ($found, $scope) use ($scopeEndIndex) {
94+
if (! is_int($scope->scopeEndIndex)) {
95+
Helpers::debug('No scope closer found for scope start', $scope->scopeStartIndex);
96+
return $found;
97+
}
98+
99+
if ($scopeEndIndex === $scope->scopeEndIndex) {
100+
$found[] = $scope;
101+
}
102+
return $found;
103+
},
104+
[]
105+
);
106+
return $scopeIndicesThisCloses;
107+
}
108+
}

VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php

Lines changed: 22 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use VariableAnalysis\Lib\VariableInfo;
88
use VariableAnalysis\Lib\Constants;
99
use VariableAnalysis\Lib\Helpers;
10+
use VariableAnalysis\Lib\ScopeManager;
1011
use PHP_CodeSniffer\Sniffs\Sniff;
1112
use PHP_CodeSniffer\Files\File;
1213
use PHP_CodeSniffer\Util\Tokens;
@@ -21,36 +22,9 @@ class VariableAnalysisSniff implements Sniff
2122
protected $currentFile = null;
2223

2324
/**
24-
* An associative array of scopes for variables encountered so far and the
25-
* variables within them.
26-
*
27-
* Each scope is keyed by a string of the form `filename:scopeStartIndex`
28-
* (see `getScopeKey`).
29-
*
30-
* @var array<string, ScopeInfo>
31-
*/
32-
private $scopes = [];
33-
34-
/**
35-
* An associative array of a list of token index pairs which start and end
36-
* scopes and will be used to check for unused variables.
37-
*
38-
* Each array of scopes is keyed by a string containing the filename (see
39-
* `getFilename`).
40-
*
41-
* Unlike the `ScopeInfo` objects stored in `$this->scopes`, these objects do
42-
* not track variables themselves, only the position of the scope boundaries.
43-
*
44-
* @var array<string, ScopeInfo[]>
25+
* @var ScopeManager
4526
*/
46-
private $scopeStartEndPairs = [];
47-
48-
/**
49-
* A cache of scope end indices in the current file to improve performance.
50-
*
51-
* @var int[]
52-
*/
53-
private $scopeEndIndexCache = [];
27+
private $scopeManager;
5428

5529
/**
5630
* A list of custom functions which pass in variables to be initialized by
@@ -165,6 +139,11 @@ class VariableAnalysisSniff implements Sniff
165139
*/
166140
public $allowUnusedVariablesBeforeRequire = false;
167141

142+
public function __construct()
143+
{
144+
$this->scopeManager = new ScopeManager();
145+
}
146+
168147
/**
169148
* Decide which tokens to scan.
170149
*
@@ -237,13 +216,12 @@ public function process(File $phpcsFile, $stackPtr)
237216
// easily accessed in other places which aren't passed the object.
238217
if ($this->currentFile !== $phpcsFile) {
239218
$this->currentFile = $phpcsFile;
240-
// Reset the scope end cache when the File changes since it is per-file.
241-
$this->scopeEndIndexCache = [];
242219
}
243220

244221
// Add the global scope for the current file to our scope indexes.
245-
if (empty($this->scopeStartEndPairs[$this->getFilename()])) {
246-
$this->recordScopeStartAndEnd($phpcsFile, 0);
222+
$scopesForFilename = $this->scopeManager->getScopesForFilename($phpcsFile->getFilename());
223+
if (empty($scopesForFilename)) {
224+
$this->scopeManager->recordScopeStartAndEnd($phpcsFile, 0);
247225
}
248226

249227
// Report variables defined but not used in the current scope as unused
@@ -282,69 +260,11 @@ public function process(File $phpcsFile, $stackPtr)
282260
Helpers::isArrowFunction($phpcsFile, $stackPtr)
283261
) {
284262
Helpers::debug('found scope condition', $token);
285-
$this->recordScopeStartAndEnd($phpcsFile, $stackPtr);
263+
$this->scopeManager->recordScopeStartAndEnd($phpcsFile, $stackPtr);
286264
return;
287265
}
288266
}
289267

290-
/**
291-
* Add a scope's start and end index to our record for the file.
292-
*
293-
* @param File $phpcsFile
294-
* @param int $scopeStartIndex
295-
*
296-
* @return void
297-
*/
298-
private function recordScopeStartAndEnd($phpcsFile, $scopeStartIndex)
299-
{
300-
$scopeEndIndex = Helpers::getScopeCloseForScopeOpen($phpcsFile, $scopeStartIndex);
301-
$filename = $this->getFilename();
302-
if (! isset($this->scopeStartEndPairs[$filename])) {
303-
$this->scopeStartEndPairs[$filename] = [];
304-
}
305-
Helpers::debug('recording scope for file', $filename, 'start/end', $scopeStartIndex, $scopeEndIndex);
306-
$this->scopeStartEndPairs[$filename][] = new ScopeInfo($scopeStartIndex, $scopeEndIndex);
307-
$this->scopeEndIndexCache[] = $scopeEndIndex;
308-
}
309-
310-
/**
311-
* Find scopes closed by a token.
312-
*
313-
* @param File $phpcsFile
314-
* @param int $stackPtr
315-
*
316-
* @return ScopeInfo[]
317-
*/
318-
private function getScopesClosedBy($phpcsFile, $stackPtr)
319-
{
320-
if (! in_array($stackPtr, $this->scopeEndIndexCache, true)) {
321-
return [];
322-
}
323-
$scopePairsForFile = isset($this->scopeStartEndPairs[$this->getFilename()]) ? $this->scopeStartEndPairs[$this->getFilename()] : [];
324-
$scopeIndicesThisCloses = array_reduce(
325-
$scopePairsForFile,
326-
/**
327-
* @param ScopeInfo[] $found
328-
* @param ScopeInfo $scope
329-
*
330-
* @return ScopeInfo[]
331-
*/
332-
function ($found, $scope) use ($stackPtr) {
333-
if (! is_int($scope->scopeEndIndex)) {
334-
Helpers::debug('No scope closer found for scope start', $scope->scopeStartIndex);
335-
return $found;
336-
}
337-
338-
if ($stackPtr === $scope->scopeEndIndex) {
339-
$found[] = $scope;
340-
}
341-
return $found;
342-
},
343-
[]
344-
);
345-
return $scopeIndicesThisCloses;
346-
}
347-
348268
/**
349269
* Find scopes closed by a token and process their variables.
350270
*
@@ -357,7 +277,7 @@ function ($found, $scope) use ($stackPtr) {
357277
*/
358278
private function searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr)
359279
{
360-
$scopeIndicesThisCloses = $this->getScopesClosedBy($phpcsFile, $stackPtr);
280+
$scopeIndicesThisCloses = $this->scopeManager->getScopesForScopeEnd($phpcsFile->getFilename(), $stackPtr);
361281

362282
foreach ($scopeIndicesThisCloses as $scopeIndexThisCloses) {
363283
Helpers::debug('found closing scope at index', $stackPtr, 'for scopes starting at:', $scopeIndexThisCloses);
@@ -388,16 +308,6 @@ protected function isGetDefinedVars(File $phpcsFile, $stackPtr)
388308
return true;
389309
}
390310

391-
/**
392-
* @param int $currScope
393-
*
394-
* @return string
395-
*/
396-
protected function getScopeKey($currScope)
397-
{
398-
return $this->getFilename() . ':' . $currScope;
399-
}
400-
401311
/**
402312
* @return string
403313
*/
@@ -406,29 +316,21 @@ protected function getFilename()
406316
return $this->currentFile ? $this->currentFile->getFilename() : 'unknown file';
407317
}
408318

409-
/**
410-
* @param int $currScope
411-
*
412-
* @return ScopeInfo|null
413-
*/
414-
protected function getScopeInfo($currScope)
415-
{
416-
$scopeKey = $this->getScopeKey($currScope);
417-
return isset($this->scopes[$scopeKey]) ? $this->scopes[$scopeKey] : null;
418-
}
419-
420319
/**
421320
* @param int $currScope
422321
*
423322
* @return ScopeInfo
424323
*/
425324
protected function getOrCreateScopeInfo($currScope)
426325
{
427-
$scopeKey = $this->getScopeKey($currScope);
428-
if (!isset($this->scopes[$scopeKey])) {
429-
$this->scopes[$scopeKey] = new ScopeInfo($currScope);
326+
$scope = $this->scopeManager->getScopeForScopeStart($this->getFilename(), $currScope);
327+
if (! $scope) {
328+
if (! $this->currentFile) {
329+
throw new \Exception('Cannot create scope info; current file is not set.');
330+
}
331+
$scope = $this->scopeManager->recordScopeStartAndEnd($this->currentFile, $currScope);
430332
}
431-
return $this->scopes[$scopeKey];
333+
return $scope;
432334
}
433335

434336
/**
@@ -439,7 +341,7 @@ protected function getOrCreateScopeInfo($currScope)
439341
*/
440342
protected function getVariableInfo($varName, $currScope)
441343
{
442-
$scopeInfo = $this->getScopeInfo($currScope);
344+
$scopeInfo = $this->scopeManager->getScopeForScopeStart($this->getFilename(), $currScope);
443345
return ($scopeInfo && isset($scopeInfo->variables[$varName])) ? $scopeInfo->variables[$varName] : null;
444346
}
445347

@@ -1873,7 +1775,7 @@ protected function processCompact(File $phpcsFile, $stackPtr)
18731775
*/
18741776
protected function processScopeClose(File $phpcsFile, $stackPtr)
18751777
{
1876-
$scopeInfo = $this->getScopeInfo($stackPtr);
1778+
$scopeInfo = $this->scopeManager->getScopeForScopeStart($phpcsFile->getFilename(), $stackPtr);
18771779
if (is_null($scopeInfo)) {
18781780
return;
18791781
}

0 commit comments

Comments
 (0)