diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index 96053ac4d29..c619d28d668 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -11,6 +11,29 @@ Psalm uses an XML config file (by default, `psalm.xml`). A barebones example loo ``` +Configuration file may be split into several files using [XInclude](https://www.w3.org/TR/xinclude/) tags (c.f. previous example): +#### psalm.xml +```xml + + + + +``` +#### files.xml +```xml + + + + + +``` + + ## Optional `` attributes ### Coding style diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index bc4ce3afb72..c610351eafb 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -7,6 +7,8 @@ use function array_unique; use function class_exists; use Composer\Autoload\ClassLoader; +use DOMDocument; + use function count; use const DIRECTORY_SEPARATOR; use function dirname; @@ -65,6 +67,10 @@ use function trigger_error; use function unlink; use function version_compare; +use function getcwd; +use function chdir; +use function simplexml_import_dom; +use const LIBXML_NONET; class Config { @@ -605,16 +611,31 @@ public static function loadFromXML($base_dir, $file_contents, $current_dir = nul $current_dir = $base_dir; } - self::validateXmlConfig($file_contents); + self::validateXmlConfig($base_dir, $file_contents); return self::fromXmlAndPaths($base_dir, $file_contents, $current_dir); } + private static function loadDomDocument(string $base_dir, string $file_contents): DOMDocument + { + $dom_document = new DOMDocument(); + + // there's no obvious way to set xml:base for a document when loading it from string + // so instead we're changing the current directory instead to be able to process XIncludes + $oldpwd = getcwd(); + chdir($base_dir); + + $dom_document->loadXML($file_contents, LIBXML_NONET); + $dom_document->xinclude(LIBXML_NONET); + + chdir($oldpwd); + return $dom_document; + } /** * @throws ConfigException */ - private static function validateXmlConfig(string $file_contents): void + private static function validateXmlConfig(string $base_dir, string $file_contents): void { $schema_path = dirname(dirname(__DIR__)) . '/config.xsd'; @@ -622,8 +643,7 @@ private static function validateXmlConfig(string $file_contents): void throw new ConfigException('Cannot locate config schema'); } - $dom_document = new \DOMDocument(); - $dom_document->loadXML($file_contents); + $dom_document = self::loadDomDocument($base_dir, $file_contents); $psalm_nodes = $dom_document->getElementsByTagName('psalm'); @@ -640,8 +660,7 @@ private static function validateXmlConfig(string $file_contents): void $psalm_node->setAttribute('xmlns', 'https://getpsalm.org/schema/config'); $old_dom_document = $dom_document; - $dom_document = new \DOMDocument(); - $dom_document->loadXML($old_dom_document->saveXML()); + $dom_document = self::loadDomDocument($base_dir, $old_dom_document->saveXML()); } // Enable user error handling @@ -674,7 +693,9 @@ private static function fromXmlAndPaths(string $base_dir, string $file_contents, { $config = new static(); - $config_xml = new SimpleXMLElement($file_contents); + $dom_document = self::loadDomDocument($base_dir, $file_contents); + + $config_xml = simplexml_import_dom($dom_document); $booleanAttributes = [ 'useDocblockTypes' => 'use_docblock_types', diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index bf97520060d..443807b8798 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -1195,6 +1195,20 @@ public function testTemplatedFiles() } } + /** @return void */ + public function testModularConfig() + { + $root = __DIR__ . '/../fixtures/ModularConfig'; + $config = Config::loadFromXMLFile($root . '/psalm.xml', $root); + $this->assertEquals( + [ + realpath($root . '/Bar.php'), + realpath($root . '/Bat.php') + ], + $config->getProjectFiles() + ); + } + public function tearDown(): void { parent::tearDown(); diff --git a/tests/fixtures/ModularConfig/Bar.php b/tests/fixtures/ModularConfig/Bar.php new file mode 100644 index 00000000000..16245ebd252 --- /dev/null +++ b/tests/fixtures/ModularConfig/Bar.php @@ -0,0 +1,23 @@ +x = 'hello'; + } +} + +/** + * @return void + */ +function someFunction() +{ + echo 'here'; +} diff --git a/tests/fixtures/ModularConfig/Bat.php b/tests/fixtures/ModularConfig/Bat.php new file mode 100644 index 00000000000..d88e03c9d37 --- /dev/null +++ b/tests/fixtures/ModularConfig/Bat.php @@ -0,0 +1,13 @@ + + + + + diff --git a/tests/fixtures/ModularConfig/psalm.xml b/tests/fixtures/ModularConfig/psalm.xml new file mode 100644 index 00000000000..ded23a9be02 --- /dev/null +++ b/tests/fixtures/ModularConfig/psalm.xml @@ -0,0 +1,10 @@ + + + +