file_provider = new FakeFileProvider(); $config = new TestConfig(); $providers = new Providers( $this->file_provider, new \Psalm\Tests\Internal\Provider\ParserInstanceCacheProvider(), null, null, new Provider\FakeFileReferenceCacheProvider(), new \Psalm\Tests\Internal\Provider\ProjectCacheProvider() ); $this->project_analyzer = new ProjectAnalyzer( $config, $providers ); $this->project_analyzer->setPhpVersion('7.3'); } /** * @dataProvider providerTestValidUpdates * * @param array $start_files * @param array $end_files * @param array $error_levels * */ public function testValidInclude( array $start_files, array $end_files, array $initial_analyzed_methods, array $unaffected_analyzed_methods, array $error_levels = [] ): void { $test_name = $this->getTestName(); if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } $this->project_analyzer->getCodebase()->diff_methods = true; $codebase = $this->project_analyzer->getCodebase(); $config = $codebase->config; $config->throw_exception = false; foreach ($error_levels as $error_type => $error_level) { $config->setCustomErrorLevel($error_type, $error_level); } foreach ($start_files as $file_path => $contents) { $this->file_provider->registerFile($file_path, $contents); $codebase->addFilesToAnalyze([$file_path => $file_path]); } $codebase->scanFiles(); $this->assertSame([], $codebase->analyzer->getAnalyzedMethods()); $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); $this->assertSame( $initial_analyzed_methods, $codebase->analyzer->getAnalyzedMethods(), 'initial analyzed methods are not the same' ); foreach ($end_files as $file_path => $contents) { $this->file_provider->registerFile($file_path, $contents); } $codebase->reloadFiles($this->project_analyzer, array_keys($end_files)); $codebase->analyzer->loadCachedResults($this->project_analyzer); $this->assertSame( $unaffected_analyzed_methods, $codebase->analyzer->getAnalyzedMethods(), 'unaffected analyzed methods are not the same' ); } /** * @return array,end_files:array,initial_analyzed_methods:array>,unaffected_analyzed_methods:array>,4?:array}> */ public function providerTestValidUpdates(): array { return [ 'basicRequire' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } public function noReturnType() {} }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } public function noReturnType() {} }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, 'foo\a::barbar' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, 'foo\b::noreturntype' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::barbar' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, 'foo\b::noreturntype' => 1, ], ], [ 'MissingReturnType' => \Psalm\Config::REPORT_INFO, ], ], 'invalidateAfterPropertyChange' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo; } public function bar() : void { $a = new A(); } }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo; } public function bar() : void { $a = new A(); } }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], ], 'invalidateAfterStaticPropertyChange' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], ], 'invalidateAfterStaticFlipPropertyChange' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], ], 'invalidateAfterConstantChange' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], ], 'dontInvalidateTraitMethods' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } public function noReturnType() {} }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } public function noReturnType() {} }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, 'foo\b::noreturntype' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, 'foo\b::noreturntype' => 1, ], ], [ 'MissingReturnType' => \Psalm\Config::REPORT_INFO, ], ], 'invalidateTraitMethodsWhenTraitRemoved' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ], ], 'invalidateTraitMethodsWhenTraitReplaced' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ], ], 'invalidateTraitMethodsWhenMethodChanged' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'fooFoo(); } public function bar() : void { echo (new A)->barBar(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, 'foo\a::bat&foo\t::bat' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => 1, 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::bat&foo\t::bat' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ], ], 'invalidateTraitMethodsWhenMethodSuperimposed' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'barBar(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'barBar(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::barbar&foo\t::barbar' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ], ], 'dontInvalidateConstructor' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } private function setFoo() : void { $this->reallySetFoo(); } private function reallySetFoo() : void { $this->foo = "bar"; } }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } private function setFoo() : void { $this->reallySetFoo(); } private function reallySetFoo() : void { $this->foo = "bar"; } }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::setfoo' => 1, 'foo\a::reallysetfoo' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::setfoo' => 1, 'foo\a::reallysetfoo' => 1, ], ], ], 'invalidateConstructorWhenDependentMethodChanges' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } private function setFoo() : void { $this->reallySetFoo(); } private function reallySetFoo() : void { $this->foo = "bar"; } }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } private function setFoo() : void { $this->reallySetFoo(); } private function reallySetFoo() : void { //$this->foo = "bar"; } }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::setfoo' => 1, 'foo\a::reallysetfoo' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::setfoo' => 1, ], ], ], 'invalidateConstructorWhenDependentMethodInSubclassChanges' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } abstract protected function setFoo() : void; }', getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => 'reallySetFoo(); } private function reallySetFoo() : void { $this->foo = "bar"; } }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } abstract protected function setFoo() : void; }', getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => 'reallySetFoo(); } private function reallySetFoo() : void { //$this->foo = "bar"; } }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 1, 'foo\a::setfoo' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ 'foo\achild::setfoo' => 1, 'foo\achild::reallysetfoo' => 1, 'foo\achild::__construct' => 2, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 1, 'foo\a::setfoo' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ 'foo\achild::setfoo' => 1, ], ], ], 'invalidateConstructorWhenDependentMethodInSubclassChanges2' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } protected function setFoo() : void { $this->foo = "bar"; } }', getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } protected function setFoo() : void { $this->foo = "baz"; } }', getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::setfoo' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ 'foo\achild::__construct' => 2, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [], ], ], 'invalidateConstructorWhenDependentTraitMethodChanges' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => 'foo = "bar"; } }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'setFoo(); } }', getcwd() . DIRECTORY_SEPARATOR . 'T.php' => 'foo = "bar"; } }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [ 'foo\a::setfoo&foo\t::setfoo' => 1, ], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'T.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], ], ], 'rescanPropertyAssertingMethod' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'foo === null) {} } }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'foo === null) {} } }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, ], ], [ 'PropertyNotSetInConstructor' => \Psalm\Config::REPORT_INFO, 'DocblockTypeContradiction' => \Psalm\Config::REPORT_INFO, 'RedundantConditionGivenDocblockType' => \Psalm\Config::REPORT_INFO, ], ], 'noChangeAfterSyntaxError' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'foo === null) {} } }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'foo === null) {} } }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1 ], ], ], 'nothingBeforeSyntaxError' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'foo === null) {} } }', ], 'end_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'foo === null) {} } }', ], 'initial_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, 'foo\a::bar' => 1 ], ], ], 'modifyPropertyOfChildClass' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'arr[$a]; $this->b = $b; } }', getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => 'arr[$a]; $this->b = $b; } }', getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, ], getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [ 'foo\achild::__construct' => 2, ], ], 'unaffected_analyzed_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::__construct' => 2, ], getcwd() . DIRECTORY_SEPARATOR . 'AChild.php' => [] ], ], ]; } public function testFileMapsUpdated(): void { $codebase = $this->project_analyzer->getCodebase(); $config = $codebase->config; $config->throw_exception = false; $this->file_provider->registerFile('somefile.php', ' addFilesToAnalyze(['somefile.php' => 'somefile.php']); $codebase->scanFiles(); $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); $maps = $codebase->analyzer->getMapsForFile('somefile.php'); $this->assertNotEmpty($maps[0]); $this->file_provider->setOpenContents('somefile.php', ''); $codebase->reloadFiles($this->project_analyzer, ['somefile.php']); $codebase->analyzer->analyzeFiles($this->project_analyzer, 1, false); $updated_maps = $codebase->analyzer->getMapsForFile('somefile.php'); $this->assertSame([], $updated_maps[0]); $this->assertSame([], $updated_maps[1]); $this->assertSame([], $updated_maps[2]); } }