diff --git a/config.xsd b/config.xsd index 026ca1e51f4..c0a9fd37db2 100644 --- a/config.xsd +++ b/config.xsd @@ -46,6 +46,8 @@ + + diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index deb6f5c1691..3b28f7231cd 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -257,6 +257,23 @@ When `true`, Psalm will attempt to find all unused code (including unused variab ``` When `true`, Psalm will emit issues when using literal keys on unshaped arrays (useful to enforce usage of shaped arrays). Defaults to `false`. +#### allFunctionsGlobal +```xml + +``` +When `true`, Psalm will treat all functions declared in scanned files as available to all files, even if they aren't loaded by the Composer autoloader (useful for legacy codebases which make heavy use of `require`/`include`, instead of Composer's autoloader). Defaults to `false`. + +#### allConstantsGlobal +```xml + +``` + +When `true`, Psalm will treat all constants declared in scanned files as available to all files, even if they aren't loaded by the Composer autoloader (useful for legacy codebases which make heavy use of `require`/`include`, instead of Composer's autoloader). Defaults to `false`. + #### findUnusedPsalmSuppress ```xml - + tags['variablesfrom'][0]]]> @@ -61,6 +61,9 @@ + + + @@ -1263,7 +1266,6 @@ cased_name]]> - cased_name]]> diff --git a/psalm.xml.dist b/psalm.xml.dist index c1085debc91..63679cfbc51 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -11,6 +11,8 @@ xsi:schemaLocation="https://getpsalm.org/schema/config config.xsd" limitMethodComplexity="true" errorBaseline="psalm-baseline.xml" + allFunctionsGlobal="true" + allConstantsGlobal="true" findUnusedPsalmSuppress="true" findUnusedBaselineEntry="true" findUnusedIssueHandlerSuppression="true" diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index fc1d7ea41ee..a132593f1de 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -150,6 +150,10 @@ final class Codebase */ public bool $register_stub_files = false; + public bool $all_functions_global = false; + + public bool $all_constants_global = false; + public bool $find_unused_variables = false; public Scanner $scanner; @@ -585,6 +589,14 @@ public function getStubbedConstantType(string $const_id): ?Union return self::$stubbed_constants[$const_id] ?? null; } + /** + * @param array $stubs + */ + public function addGlobalConstantTypes(array $stubs): void + { + self::$stubbed_constants += $stubs; + } + /** * @return array */ diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 4f96989536b..081446f9042 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -398,6 +398,10 @@ final class Config public bool $literal_array_key_check = false; + public bool $all_functions_global = false; + + public bool $all_constants_global = false; + public int $max_graph_size = 200; public int $max_avg_path_length = 70; @@ -1111,6 +1115,16 @@ private static function fromXmlAndPaths( $config->literal_array_key_check = $attribute_text === 'true' || $attribute_text === '1'; } + if (isset($config_xml['allFunctionsGlobal'])) { + $attribute_text = (string) $config_xml['allFunctionsGlobal']; + $config->all_functions_global = $attribute_text === 'true' || $attribute_text === '1'; + } + + if (isset($config_xml['allConstantsGlobal'])) { + $attribute_text = (string) $config_xml['allConstantsGlobal']; + $config->all_constants_global = $attribute_text === 'true' || $attribute_text === '1'; + } + if (isset($config_xml['findUnusedVariablesAndParams'])) { $attribute_text = (string) $config_xml['findUnusedVariablesAndParams']; $config->find_unused_variables = $attribute_text === 'true' || $attribute_text === '1'; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 6746e21027d..95bd4925522 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -612,7 +612,8 @@ public static function checkFunctionExists( } else { IssueBuffer::maybeAdd( new UndefinedFunction( - 'Function ' . $cased_function_id . ' does not exist', + 'Function ' . $cased_function_id . ' does not exist' + .', consider enabling the allFunctionsGlobal config option if scanning legacy codebases', $code_location, $function_id, ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php index 30d77a93763..036aa557ca5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php @@ -106,7 +106,8 @@ public static function analyze( } elseif ($context->check_consts) { IssueBuffer::maybeAdd( new UndefinedConstant( - 'Const ' . $const_name . ' is not defined', + 'Const ' . $const_name . ' is not defined'. + ', consider enabling the allConstantsGlobal config option if scanning legacy codebases', new CodeLocation($statements_analyzer->getSource(), $stmt), ), $statements_analyzer->getSuppressedIssues(), @@ -151,10 +152,12 @@ public static function getGlobalConstType( || array_key_exists($const_name, $predefined_constants) ) { switch ($const_name) { - case 'PHP_VERSION': case 'DIRECTORY_SEPARATOR': case 'PATH_SEPARATOR': case 'PHP_EOL': + return Type::getSingleLetter(); + + case 'PHP_VERSION': return Type::getNonEmptyString(); case 'PEAR_EXTENSION_DIR': diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 9e1c937eef6..394625940c1 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -1284,6 +1284,8 @@ private static function configureProjectAnalyzer( if ($config->literal_array_key_check) { $project_analyzer->getCodebase()->literal_array_key_check = true; } + $project_analyzer->getCodebase()->all_constants_global = $config->all_constants_global; + $project_analyzer->getCodebase()->all_functions_global = $config->all_functions_global; if ($config->run_taint_analysis || $run_taint_analysis) { $project_analyzer->trackTaintedInputs(); diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index 07f5025cc9c..d5b53853801 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -153,13 +153,21 @@ public function addGlobalFunction(string $function_id, FunctionStorage $storage) self::$stubbed_functions[strtolower($function_id)] = $storage; } + /** + * @param array $stubs + */ + public function addGlobalFunctions(array $stubs): void + { + self::$stubbed_functions += $stubs; + } + public function hasStubbedFunction(string $function_id): bool { return isset(self::$stubbed_functions[strtolower($function_id)]); } /** - * @return array + * @return array */ public function getAllStubbedFunctions(): array { diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php index 36ff641f06e..dfbeac19182 100644 --- a/src/Psalm/Internal/Codebase/Scanner.php +++ b/src/Psalm/Internal/Codebase/Scanner.php @@ -20,7 +20,9 @@ use Psalm\Progress\Progress; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FileStorage; +use Psalm\Storage\FunctionStorage; use Psalm\Type; +use Psalm\Type\Union; use ReflectionClass; use Throwable; use UnexpectedValueException; @@ -78,7 +80,9 @@ * classlike_storage:array, * file_storage:array, * new_file_content_hashes: array, - * taint_data: ?TaintFlowGraph + * taint_data: ?TaintFlowGraph, + * global_constants: array, + * global_functions: array * } */ @@ -352,6 +356,9 @@ private function scanFilePaths(int $pool_size): bool $pool_data['new_file_content_hashes'], ); } + + $this->codebase->addGlobalConstantTypes($pool_data['global_constants']); + $this->codebase->functions->addGlobalFunctions($pool_data['global_functions']); } } else { foreach ($files_to_scan as $file_path => $_) { @@ -364,27 +371,6 @@ private function scanFilePaths(int $pool_size): bool $this->codebase->statements_provider->parser_cache_provider->saveFileContentHashes(); } - foreach ($files_to_scan as $scanned_file) { - if ($this->config->hasStubFile($scanned_file)) { - $file_storage = $this->file_storage_provider->get($scanned_file); - - foreach ($file_storage->functions as $function_storage) { - if ($function_storage->cased_name - && !$this->codebase->functions->hasStubbedFunction($function_storage->cased_name) - ) { - $this->codebase->functions->addGlobalFunction( - $function_storage->cased_name, - $function_storage, - ); - } - } - - foreach ($file_storage->constants as $name => $type) { - $this->codebase->addGlobalConstantType($name, $type); - } - } - } - $this->file_reference_provider->addClassLikeFiles($this->classlike_files); return true; @@ -519,7 +505,9 @@ private function scanFile( $this->queueClassLikeForScanning($fq_classlike_name, false, false); } - if ($this->codebase->register_autoload_files) { + if ($this->codebase->register_autoload_files + || $this->codebase->all_functions_global + ) { foreach ($file_storage->functions as $function_storage) { if ($function_storage->cased_name && !$this->codebase->functions->hasStubbedFunction($function_storage->cased_name) @@ -530,7 +518,10 @@ private function scanFile( ); } } - + } + if ($this->codebase->register_autoload_files + || $this->codebase->all_constants_global + ) { foreach ($file_storage->constants as $name => $type) { $this->codebase->addGlobalConstantType($name, $type); } diff --git a/src/Psalm/Internal/Fork/ShutdownScannerTask.php b/src/Psalm/Internal/Fork/ShutdownScannerTask.php index 3962f6acad0..96f49240021 100644 --- a/src/Psalm/Internal/Fork/ShutdownScannerTask.php +++ b/src/Psalm/Internal/Fork/ShutdownScannerTask.php @@ -47,6 +47,8 @@ public function run(Channel $channel, Cancellation $cancellation): mixed ? $statements_provider->parser_cache_provider->getNewFileContentHashes() : [], 'taint_data' => $codebase->taint_flow_graph, + 'global_constants' => $codebase->getAllStubbedConstants(), + 'global_functions' => $codebase->functions->getAllStubbedFunctions(), ]; } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 9ccbd8e6c61..4a8e872c72b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -188,8 +188,10 @@ private static function registerClassMapFunctionCall( $file_storage->declaring_constants[$const_name] = $file_storage->file_path; } - if (($codebase->register_stub_files || $codebase->register_autoload_files) - && (!defined($const_name) || $const_type->isMixed()) + if (($codebase->register_stub_files + || $codebase->register_autoload_files + || $codebase->all_constants_global + ) && (!defined($const_name) || !$const_type->isMixed()) ) { $codebase->addGlobalConstantType($const_name, $const_type); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 08e97ccda1a..8ff69952023 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -489,7 +489,8 @@ public function start( && $function_id && $storage instanceof FunctionStorage ) { - if ($this->codebase->register_stub_files + if ($this->codebase->all_functions_global + || $this->codebase->register_stub_files || ($this->codebase->register_autoload_files && !$this->codebase->functions->hasStubbedFunction($function_id)) ) { @@ -914,7 +915,10 @@ private function createStorageForFunctionLike( $storage = $this->storage = new FunctionStorage(); - if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) { + if ($this->codebase->register_stub_files + || $this->codebase->register_autoload_files + || $this->codebase->all_functions_global + ) { if (isset($this->file_storage->functions[$function_id]) && ($this->codebase->register_stub_files || !$this->codebase->functions->hasStubbedFunction($function_id)) diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 3ef230a4a55..e253720961f 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -38,6 +38,7 @@ use UnexpectedValueException; use function array_pop; +use function defined; use function end; use function explode; use function in_array; @@ -288,7 +289,11 @@ public function enterNode(PhpParser\Node $node): ?int $fq_const_name = Type::getFQCLNFromString($const->name->name, $this->aliases); - if ($this->codebase->register_stub_files || $this->codebase->register_autoload_files) { + if (($this->codebase->register_stub_files + || $this->codebase->register_autoload_files + || $this->codebase->all_constants_global + ) && (!defined($fq_const_name) || !$const_type->isMixed()) + ) { $this->codebase->addGlobalConstantType($fq_const_name, $const_type); } diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index b30aa993e9d..5c4292f573e 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -762,7 +762,7 @@ public function testJsonReport(): void 'line_from' => 8, 'line_to' => 8, 'type' => 'UndefinedConstant', - 'message' => 'Const CHANGE_ME is not defined', + 'message' => 'Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases', 'file_name' => 'somefile.php', 'file_path' => 'somefile.php', 'snippet' => 'echo CHANGE_ME;', @@ -885,7 +885,7 @@ public function testSonarqubeReport(): void 'engineId' => 'Psalm', 'ruleId' => 'UndefinedConstant', 'primaryLocation' => [ - 'message' => 'Const CHANGE_ME is not defined', + 'message' => 'Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases', 'filePath' => 'somefile.php', 'textRange' => [ 'startLine' => 8, @@ -935,7 +935,7 @@ public function testEmacsReport(): void <<<'EOF' somefile.php:3:10:error - UndefinedVariable: Cannot find referenced variable $as_you_____type (see https://psalm.dev/024) somefile.php:3:10:error - MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138) - somefile.php:8:6:error - UndefinedConstant: Const CHANGE_ME is not defined (see https://psalm.dev/020) + somefile.php:8:6:error - UndefinedConstant: Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases (see https://psalm.dev/020) somefile.php:17:6:warning - PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126) EOF, @@ -953,7 +953,7 @@ public function testPylintReport(): void <<<'EOF' somefile.php:3: [E0001] UndefinedVariable: Cannot find referenced variable $as_you_____type (column 10) somefile.php:3: [E0001] MixedReturnStatement: Could not infer a return type (column 10) - somefile.php:8: [E0001] UndefinedConstant: Const CHANGE_ME is not defined (column 6) + somefile.php:8: [E0001] UndefinedConstant: Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases (column 6) somefile.php:17: [W0001] PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (column 6) EOF, @@ -976,7 +976,7 @@ public function testConsoleReport(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) return $as_you_____type; - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) + ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases (see https://psalm.dev/020) echo CHANGE_ME; INFO: PossiblyUndefinedGlobalVariable - somefile.php:17:6 - Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126) @@ -1004,7 +1004,7 @@ public function testConsoleReportNoInfo(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) return $as_you_____type; - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) + ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases (see https://psalm.dev/020) echo CHANGE_ME; @@ -1029,7 +1029,7 @@ public function testConsoleReportNoSnippet(): void ERROR: MixedReturnStatement - somefile.php:3:10 - Could not infer a return type (see https://psalm.dev/138) - ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined (see https://psalm.dev/020) + ERROR: UndefinedConstant - somefile.php:8:6 - Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases (see https://psalm.dev/020) INFO: PossiblyUndefinedGlobalVariable - somefile.php:17:6 - Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126) @@ -1087,14 +1087,15 @@ public function testCompactReport(): void <<<'EOF' FILE: somefile.php - +----------+------+---------------------------------+--------------------------------------------------------------+ - | SEVERITY | LINE | ISSUE | DESCRIPTION | - +----------+------+---------------------------------+--------------------------------------------------------------+ - | ERROR | 3 | UndefinedVariable | Cannot find referenced variable $as_you_____type | - | ERROR | 3 | MixedReturnStatement | Could not infer a return type | - | ERROR | 8 | UndefinedConstant | Const CHANGE_ME is not defined | - | INFO | 17 | PossiblyUndefinedGlobalVariable | Possibly undefined global variable $a, first seen on line 11 | - +----------+------+---------------------------------+--------------------------------------------------------------+ + +----------+------+---------------------------------+------------------------------------------------------------------------+ + | SEVERITY | LINE | ISSUE | DESCRIPTION | + +----------+------+---------------------------------+------------------------------------------------------------------------+ + | ERROR | 3 | UndefinedVariable | Cannot find referenced variable $as_you_____type | + | ERROR | 3 | MixedReturnStatement | Could not infer a return type | + | ERROR | 8 | UndefinedConstant | Const CHANGE_ME is not defined, consider enabling the allConstantsGlob | + | | | | al config option if scanning legacy codebases | + | INFO | 17 | PossiblyUndefinedGlobalVariable | Possibly undefined global variable $a, first seen on line 11 | + +----------+------+---------------------------------+------------------------------------------------------------------------+ EOF, $this->toUnixLineEndings(IssueBuffer::getOutput(IssueBuffer::getIssuesData(), $compact_report_options)), @@ -1118,7 +1119,7 @@ public function testCheckstyleReport(): void - + @@ -1170,7 +1171,7 @@ public function testJunitReport(): void - message: Const CHANGE_ME is not defined + message: Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases type: UndefinedConstant snippet: echo CHANGE_ME; selected_text: CHANGE_ME @@ -1221,7 +1222,7 @@ public function testGithubActionsOutput(): void $expected_output = <<<'EOF' ::error file=somefile.php,line=3,col=10,title=UndefinedVariable::somefile.php:3:10: UndefinedVariable: Cannot find referenced variable $as_you_____type (see https://psalm.dev/024) ::error file=somefile.php,line=3,col=10,title=MixedReturnStatement::somefile.php:3:10: MixedReturnStatement: Could not infer a return type (see https://psalm.dev/138) - ::error file=somefile.php,line=8,col=6,title=UndefinedConstant::somefile.php:8:6: UndefinedConstant: Const CHANGE_ME is not defined (see https://psalm.dev/020) + ::error file=somefile.php,line=8,col=6,title=UndefinedConstant::somefile.php:8:6: UndefinedConstant: Const CHANGE_ME is not defined, consider enabling the allConstantsGlobal config option if scanning legacy codebases (see https://psalm.dev/020) ::warning file=somefile.php,line=17,col=6,title=PossiblyUndefinedGlobalVariable::somefile.php:17:6: PossiblyUndefinedGlobalVariable: Possibly undefined global variable $a, first seen on line 11 (see https://psalm.dev/126) EOF; diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 1c030577b3d..5b694a638db 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -1176,7 +1176,6 @@ function someFunction(string $param, array $param2, ?int $param3 = null): string } } - /** @psalm-suppress InvalidArgument Psalm couldn't detect the function exists */ $reflectionFunc = new ReflectionFunction('Psalm\Tests\someFunction'); $reflectionParams = $reflectionFunc->getParameters(); diff --git a/tests/fixtures/stubs/conditional_constant_define_inferred.phpstub b/tests/fixtures/stubs/conditional_constant_define_inferred.phpstub index af6d3d6abeb..409f93f7a8b 100644 --- a/tests/fixtures/stubs/conditional_constant_define_inferred.phpstub +++ b/tests/fixtures/stubs/conditional_constant_define_inferred.phpstub @@ -1,6 +1,9 @@