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 @@
+
+
+
+