44
55namespace Efabrica \PHPStanLatte \Analyser ;
66
7- use Efabrica \ PHPStanLatte \ LatteContext \ CollectedData \ CollectedRelatedFiles ;
7+ use Composer \ InstalledVersions ;
88use Efabrica \PHPStanLatte \LatteContext \Collector \AbstractLatteContextCollector ;
9+ use Efabrica \PHPStanLatte \Temp \TempDirResolver ;
10+ use Exception ;
11+ use InvalidArgumentException ;
12+ use Nette \Utils \FileSystem ;
13+ use Nette \Utils \Json ;
914use PhpParser \Node ;
1015use PhpParser \Node \Stmt \TraitUse ;
1116use PHPStan \Analyser \NodeScopeResolver ;
1419use PHPStan \Analyser \ScopeFactory ;
1520use PHPStan \File \FileHelper ;
1621use PHPStan \Parser \Parser ;
22+ use PHPStan \PhpDoc \TypeStringResolver ;
1723use PHPStan \Reflection \ReflectionProvider ;
1824use PHPStan \Rules \RuleErrorBuilder ;
25+ use RuntimeException ;
1926use Throwable ;
2027
2128final class LatteContextAnalyser
@@ -26,12 +33,16 @@ final class LatteContextAnalyser
2633
2734 private Parser $ parser ;
2835
36+ private TypeStringResolver $ typeStringResolver ;
37+
2938 private ReflectionProvider $ reflectionProvider ;
3039
3140 private FileHelper $ fileHelper ;
3241
3342 private LatteContextCollectorRegistry $ collectorRegistry ;
3443
44+ private string $ tmpDir ;
45+
3546 /**
3647 * @param AbstractLatteContextCollector[] $collectors
3748 */
@@ -41,15 +52,23 @@ public function __construct(
4152 ReflectionProvider $ reflectionProvider ,
4253 FileHelper $ fileHelper ,
4354 Parser $ parser ,
44- array $ collectors
55+ TypeStringResolver $ typeStringResolver ,
56+ TempDirResolver $ tempDirResolver ,
57+ array $ collectors ,
58+ bool $ debugMode = false
4559 ) {
4660 $ this ->scopeFactory = $ scopeFactory ;
4761 $ this ->nodeScopeResolver = clone $ nodeScopeResolver ;
4862 $ this ->reflectionProvider = $ reflectionProvider ;
4963 $ this ->fileHelper = $ fileHelper ;
5064 // $this->nodeScopeResolver->setAnalysedFiles(null); TODO when changes in PHPStan are merged
5165 $ this ->parser = $ parser ;
66+ $ this ->typeStringResolver = $ typeStringResolver ;
5267 $ this ->collectorRegistry = new LatteContextCollectorRegistry ($ collectors );
68+ $ this ->tmpDir = $ tempDirResolver ->resolveCollectorDir ();
69+ if (file_exists ($ this ->tmpDir ) && $ debugMode ) {
70+ FileSystem::delete ($ this ->tmpDir );
71+ }
5372 }
5473
5574 /**
@@ -59,29 +78,34 @@ public function analyseFiles(array $files): LatteContextData
5978 {
6079 $ errors = [];
6180 $ collectedData = [];
81+ $ processedFiles = [];
82+ $ counter = 0 ;
6283
6384 $ this ->nodeScopeResolver ->setAnalysedFiles ($ files ); // TODO when changes in PHPStan are merged
6485
65- $ collectedRelatedFiles = [];
6686 do {
87+ if ($ counter ++ > 100 ) {
88+ throw new RuntimeException ('Infinite loop detected in LatteContextAnalyser. ' );
89+ }
90+ $ relatedFiles = [];
6791 foreach ($ files as $ file ) {
68- $ fileResult = $ this ->analyseFile ($ file );
69- if ($ fileResult ->getErrors () !== []) {
70- $ errors = array_merge ($ errors , $ fileResult ->getErrors ());
92+ $ fileResult = $ this ->loadLatteContextDataFromCache ($ file );
93+ if (!$ fileResult ) {
94+ $ fileResult = $ this ->analyseFile ($ file );
95+ if ($ fileResult ->getErrors () === []) {
96+ $ this ->saveLatteContextDataToCache ($ file , $ fileResult );
97+ } else {
98+ $ errors = array_merge ($ errors , $ fileResult ->getErrors ());
99+ }
100+ } else {
71101 }
72102 if ($ fileResult ->getAllCollectedData () !== []) {
73103 $ collectedData = array_merge ($ collectedData , $ fileResult ->getAllCollectedData ());
104+ $ processedFiles = array_unique (array_merge ($ processedFiles , $ fileResult ->getProcessedFiles ()));
105+ $ relatedFiles = array_unique (array_merge ($ relatedFiles , $ fileResult ->getRelatedFiles ()));
74106 }
75- $ collectedRelatedFiles = array_merge ($ collectedRelatedFiles , $ fileResult ->getCollectedData (CollectedRelatedFiles::class));
76107 }
77-
78- $ processedFiles = [];
79- $ relatedFiles = [];
80- foreach ($ collectedRelatedFiles as $ collectedRelatedFile ) {
81- $ processedFiles [] = $ collectedRelatedFile ->getProcessedFile ();
82- $ relatedFiles [] = $ collectedRelatedFile ->getRelatedFiles ();
83- }
84- $ files = array_diff (array_unique (array_merge (...$ relatedFiles )), array_unique ($ processedFiles ));
108+ $ files = array_diff ($ relatedFiles , $ processedFiles );
85109 } while (count ($ files ) > 0 );
86110
87111 return new LatteContextData ($ collectedData , $ errors );
@@ -169,4 +193,115 @@ public function withCollectors(array $collectors): self
169193 $ clone ->collectorRegistry = new LatteContextCollectorRegistry ($ collectors );
170194 return $ clone ;
171195 }
196+
197+ private function cacheFilename (string $ file ): string
198+ {
199+ $ cacheKey = md5 (
200+ $ file .
201+ PHP_VERSION_ID .
202+ (class_exists (InstalledVersions::class) ? json_encode (InstalledVersions::getAllRawData ()) : '' )
203+ );
204+ return $ this ->tmpDir . basename ($ file ) . '. ' . $ cacheKey . '.json ' ;
205+ }
206+
207+ private function saveLatteContextDataToCache (string $ file , LatteContextData $ fileResult ): void
208+ {
209+ if (!is_dir ($ this ->tmpDir )) {
210+ Filesystem::createDir ($ this ->tmpDir , 0777 );
211+ }
212+
213+ $ cacheFile = $ this ->cacheFilename ($ file );
214+
215+ try {
216+ $ data = $ fileResult ->jsonSerialize ();
217+ } catch (InvalidArgumentException $ e ) {
218+ // Cannot serialize data, skip caching
219+ if (is_file ($ cacheFile )) {
220+ FileSystem::delete ($ cacheFile );
221+ }
222+ return ;
223+ }
224+
225+ $ cacheData = [
226+ 'file ' => $ file ,
227+ 'fileHash ' => sha1 (Filesystem::read ($ file )),
228+ 'data ' => $ data ,
229+ ];
230+ foreach ($ fileResult ->getRelatedFiles () as $ relatedFile ) {
231+ $ cacheData ['dependencies ' ][] = [
232+ 'file ' => $ relatedFile ,
233+ 'fileHash ' => sha1 (Filesystem::read ($ relatedFile )),
234+ ];
235+ }
236+ Filesystem::write (
237+ $ cacheFile ,
238+ Json::encode ($ cacheData , JSON_PRETTY_PRINT )
239+ );
240+ }
241+
242+ private function loadLatteContextDataFromCache (string $ file ): ?LatteContextData
243+ {
244+ $ cacheFile = $ this ->cacheFilename ($ file );
245+ if (!is_file ($ cacheFile )) {
246+ return null ;
247+ }
248+
249+ try {
250+ $ cacheData = Json::decode (Filesystem::read ($ cacheFile ), JSON_OBJECT_AS_ARRAY );
251+ } catch (Exception $ e ) {
252+ FileSystem::delete ($ cacheFile );
253+ return null ;
254+ }
255+
256+ if (!is_array ($ cacheData ) || !isset ($ cacheData ['file ' ], $ cacheData ['fileHash ' ], $ cacheData ['data ' ])) {
257+ FileSystem::delete ($ cacheFile );
258+ return null ;
259+ }
260+
261+ $ file = $ cacheData ['file ' ];
262+ $ fileHash = $ cacheData ['fileHash ' ];
263+
264+ if (!is_string ($ file ) || !is_string ($ fileHash )) {
265+ FileSystem::delete ($ cacheFile );
266+ return null ;
267+ }
268+
269+ // Check if the file has changed since the cache was created
270+ if (sha1 (Filesystem::read ($ file )) !== $ fileHash ) {
271+ return null ;
272+ }
273+
274+ if (isset ($ cacheData ['dependencies ' ]) && is_array ($ cacheData ['dependencies ' ])) {
275+ foreach ($ cacheData ['dependencies ' ] as $ dependency ) {
276+ if (!is_array ($ dependency ) || !isset ($ dependency ['file ' ], $ dependency ['fileHash ' ])) {
277+ return null ;
278+ }
279+ $ dependencyFile = $ dependency ['file ' ];
280+ $ dependencyFileHash = $ dependency ['fileHash ' ];
281+ if (!is_string ($ dependencyFile ) || !is_string ($ dependencyFileHash )) {
282+ return null ;
283+ }
284+ if (!is_file ($ dependencyFile )) {
285+ return null ;
286+ }
287+ // Check if the dependency file has changed since the cache was created
288+ if (sha1 (Filesystem::read ($ dependencyFile )) !== $ dependencyFileHash ) {
289+ return null ;
290+ }
291+ }
292+ }
293+
294+ $ data = $ cacheData ['data ' ];
295+ if (!is_array ($ data )) {
296+ FileSystem::delete ($ cacheFile );
297+ return null ;
298+ }
299+
300+ try {
301+ return LatteContextData::fromJson ($ data , $ this ->typeStringResolver );
302+ } catch (Exception $ e ) {
303+ FileSystem::delete ($ cacheFile );
304+ return null ;
305+ }
306+ }
172307}
0 commit comments