Skip to content

Commit

Permalink
Modular config files
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
weirdan committed Jan 16, 2020
1 parent d434f7f commit 7c6598e
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 7 deletions.
23 changes: 23 additions & 0 deletions docs/running_psalm/configuration.md
Expand Up @@ -11,6 +11,29 @@ Psalm uses an XML config file (by default, `psalm.xml`). A barebones example loo
</psalm>
```

Configuration file may be split into several files using [XInclude](https://www.w3.org/TR/xinclude/) tags (c.f. previous example):
#### psalm.xml
```xml
<?xml version="1.0"?>
<psalm
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
xmlns:xi="http://www.w3.org/2001/XInclude"
>
<xi:include href="files.xml"/>
</psalm>
```
#### files.xml
```xml
<?xml version="1.0" encoding="UTF-8"?>
<projectFiles xmlns="https://getpsalm.org/schema/config">
<file name="Bar.php" />
<file name="Bat.php" />
</projectFiles>
```


## Optional `<psalm />` attributes

### Coding style
Expand Down
35 changes: 28 additions & 7 deletions src/Psalm/Config.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -605,25 +611,39 @@ 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';

if (!file_exists($schema_path)) {
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');

Expand All @@ -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
Expand Down Expand Up @@ -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',
Expand Down
14 changes: 14 additions & 0 deletions tests/Config/ConfigTest.php
Expand Up @@ -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();
Expand Down
23 changes: 23 additions & 0 deletions tests/fixtures/ModularConfig/Bar.php
@@ -0,0 +1,23 @@
<?php
namespace Vimeo\Test\DummyProject;

class Bar
{
use SomeTrait;

/** @var string */
public $x;

public function __construct()
{
$this->x = 'hello';
}
}

/**
* @return void
*/
function someFunction()
{
echo 'here';
}
13 changes: 13 additions & 0 deletions tests/fixtures/ModularConfig/Bat.php
@@ -0,0 +1,13 @@
<?php
namespace Vimeo\Test\DummyProject;

class Bat
{
public function __construct()
{
$a = new Bar();

someFunction();
someOtherFunction();
}
}
14 changes: 14 additions & 0 deletions tests/fixtures/ModularConfig/SomeTrait.php
@@ -0,0 +1,14 @@
<?php
namespace Vimeo\Test\DummyProject;

trait SomeTrait
{
}

/**
* @return void
*/
function someOtherFunction()
{
echo 'here';
}
5 changes: 5 additions & 0 deletions tests/fixtures/ModularConfig/files.xml
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectFiles xmlns="https://getpsalm.org/schema/config">
<file name="Bar.php" />
<file name="Bat.php" />
</projectFiles>
10 changes: 10 additions & 0 deletions tests/fixtures/ModularConfig/psalm.xml
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<psalm
totallyTyped="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
xmlns:xi="http://www.w3.org/2001/XInclude"
>
<xi:include href="files.xml"/>
</psalm>

0 comments on commit 7c6598e

Please sign in to comment.