file_provider = new \Psalm\Tests\Provider\FakeFileProvider(); $config = new TestConfig(); $providers = new Providers( $this->file_provider, new \Psalm\Tests\Provider\ParserInstanceCacheProvider(), null, null, new Provider\FakeFileReferenceCacheProvider() ); $this->project_checker = new ProjectChecker( $config, $providers, false, true, ProjectChecker::TYPE_CONSOLE, 1, false ); $this->project_checker->infer_types_from_usage = true; } /** * @dataProvider providerTestValidUpdates * * @param array $start_files * @param array $end_files * @param array $error_levels * * @return void */ public function testValidInclude( array $start_files, array $end_files, array $initial_correct_methods, array $unaffected_correct_methods, array $error_levels = [] ) { $test_name = $this->getTestName(); if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } $this->project_checker->cache_results = true; $codebase = $this->project_checker->getCodebase(); $config = $codebase->config; 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->getCorrectMethods()); $codebase->analyzer->analyzeFiles($this->project_checker, 1, false); $this->assertSame( $initial_correct_methods, $codebase->analyzer->getCorrectMethods() ); foreach ($end_files as $file_path => $contents) { $this->file_provider->registerFile($file_path, $contents); } $codebase->reloadFiles($this->project_checker, array_keys($end_files)); foreach ($end_files as $file_path => $_) { $codebase->addFilesToAnalyze([$file_path => $file_path]); } $codebase->scanFiles(); $codebase->analyzer->loadCachedResults($this->project_checker); $this->assertSame( $unaffected_correct_methods, $codebase->analyzer->getCorrectMethods() ); } /** * @dataProvider providerTestInvalidUpdates * * @param array> $file_stages * @param array $error_levels * * @return void */ public function testErrorAfterUpdate( array $file_stages, string $error_message, array $error_levels = [] ) { $this->project_checker->cache_results = true; $codebase = $this->project_checker->getCodebase(); $config = $codebase->config; foreach ($error_levels as $error_type => $error_level) { $config->setCustomErrorLevel($error_type, $error_level); } $end_files = array_pop($file_stages); foreach ($file_stages as $files) { foreach ($files as $file_path => $contents) { $this->file_provider->registerFile($file_path, $contents); } $codebase->reloadFiles($this->project_checker, array_keys($files)); foreach ($files as $file_path => $contents) { $this->file_provider->registerFile($file_path, $contents); $codebase->addFilesToAnalyze([$file_path => $file_path]); } $codebase->scanFiles(); $codebase->analyzer->analyzeFiles($this->project_checker, 1, false); } foreach ($end_files as $file_path => $contents) { $this->file_provider->registerFile($file_path, $contents); } $codebase->reloadFiles($this->project_checker, array_keys($end_files)); foreach ($end_files as $file_path => $_) { $codebase->addFilesToAnalyze([$file_path => $file_path]); } $codebase->scanFiles(); $this->expectException('\Psalm\Exception\CodeException'); $this->expectExceptionMessageRegexp('/\b' . preg_quote($error_message, '/') . '\b/'); $codebase->analyzer->analyzeFiles($this->project_checker, 1, false); } /** * @return array */ public function providerTestValidUpdates() { 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_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::foofoo' => true, 'foo\a::barbar' => true, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, 'foo\b::noreturntype' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::barbar' => true ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => true, 'foo\b::noreturntype' => true, ], ], [ '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_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => true, ], ] ], 'invalidateAfterStaticPropertyChange' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => true, ], ] ], 'invalidateAfterStaticFlipPropertyChange' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => true, ], ] ], 'invalidateAfterConstantChange' => [ 'start_files' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => true, ], ] ], '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 . 'A.php' => [ 'foo\a::barbar&foo\t::barbar' => true, 'foo\a::foofoo' => true, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, 'foo\b::noreturntype' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::barbar&foo\t::barbar' => true, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => true, 'foo\b::noreturntype' => true, ], ], [ '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 . 'A.php' => [ 'foo\a::barbar&foo\t::barbar' => true, 'foo\a::foofoo' => true, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::barbar&foo\t::barbar' => true, // this doesn't exist, so we don't care ], 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 . 'A.php' => [ 'foo\a::barbar&foo\t::barbar' => true, 'foo\a::foofoo' => true, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, ], ], 'unaffected_correct_methods' => [ 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 . 'A.php' => [ 'foo\a::barbar&foo\t::barbar' => true, 'foo\a::bat&foo\t::bat' => true, 'foo\a::foofoo' => true, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::foo' => true, 'foo\b::bar' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [ 'foo\a::bat&foo\t::bat' => true, ], 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 . 'A.php' => [ 'foo\a::barbar&foo\t::barbar' => true, ], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [ 'foo\b::bar' => true, ], ], 'unaffected_correct_methods' => [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => [], getcwd() . DIRECTORY_SEPARATOR . 'B.php' => [], ] ], ]; } /** * @return array */ public function providerTestInvalidUpdates() { return [ 'invalidateParentCaller' => [ 'file_stages' => [ [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' 'foo(); } }', ], [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' 'foo(); } }', ], ], 'error_message' => 'UndefinedMethod', ], 'invalidateAfterPropertyTypeChange' => [ 'file_stages' => [ [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo; } }', ], [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' 'foo; } }', ], ], 'error_message' => 'InvalidReturnStatement' ], 'invalidateAfterConstantChange' => [ 'file_stages' => [ [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' 'InvalidReturnStatement' ], 'invalidateAfterSkippedAnalysis' => [ 'file_stages' => [ [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' 'getB()->getString(); } }', ], [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' 'getB()->getString(); } public function bat() : void {} }', ], [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' 'getB()->getString(); } }', ], ], 'error_message' => 'NullableReturnStatement' ], 'invalidateMissingConstructorAfterPropertyChange' => [ 'file_stages' => [ [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' 'MissingConstructor' ], 'invalidateEmptyConstructorAfterPropertyChange' => [ 'file_stages' => [ [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' 'PropertyNotSetInConstructor' ], 'invalidateMissingConstructorAfterParentPropertyChange' => [ 'file_stages' => [ [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' 'MissingConstructor' ], 'invalidateNotSetInConstructorAfterParentPropertyChange' => [ 'file_stages' => [ [ getcwd() . DIRECTORY_SEPARATOR . 'A.php' => ' ' ' ' 'MissingConstructor' ], ]; } }