From 7c6598edd287d4cafe65998f9f89e523ed3e8b9b Mon Sep 17 00:00:00 2001 From: Bruce Weirdan Date: Thu, 16 Jan 2020 05:40:12 +0200 Subject: [PATCH] Modular config files This change introduces an option to have the configuration split across several files using standard XInclude tags. This may be useful for more complex configs, or to include auto-generated parts into a manually written config file. --- docs/running_psalm/configuration.md | 23 ++++++++++++++ src/Psalm/Config.php | 35 +++++++++++++++++----- tests/Config/ConfigTest.php | 14 +++++++++ tests/fixtures/ModularConfig/Bar.php | 23 ++++++++++++++ tests/fixtures/ModularConfig/Bat.php | 13 ++++++++ tests/fixtures/ModularConfig/SomeTrait.php | 14 +++++++++ tests/fixtures/ModularConfig/files.xml | 5 ++++ tests/fixtures/ModularConfig/psalm.xml | 10 +++++++ 8 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/ModularConfig/Bar.php create mode 100644 tests/fixtures/ModularConfig/Bat.php create mode 100644 tests/fixtures/ModularConfig/SomeTrait.php create mode 100644 tests/fixtures/ModularConfig/files.xml create mode 100644 tests/fixtures/ModularConfig/psalm.xml 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 @@ + + + +