Skip to content

Commit

Permalink
Add autodiscovery of --init folders based on composer.json
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed May 9, 2019
1 parent fe22e83 commit 335b041
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 51 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ Make sure to check out the [Contributing to Open Source on GitHub](https://guide
You can create an issue [here](https://github.com/vimeo/psalm/issues/new), but before you do, follow these guidelines: You can create an issue [here](https://github.com/vimeo/psalm/issues/new), but before you do, follow these guidelines:


* Make sure that you are using the latest version (`dev-master`). * Make sure that you are using the latest version (`dev-master`).
* It’s by no means a requirement, but if it's a bug, and you provide demonstration code that can be pasted into https://getpsalm.org, it will likely get fixed faster. * It’s by no means a requirement, but if it's a bug, and you provide demonstration code that can be pasted into https://psalm.dev, it will likely get fixed faster.


## Pull Requests ## Pull Requests


Before you send a pull request, make sure you follow these guidelines: Before you send a pull request, make sure you follow these guidelines:


* Make sure to run `composer tests` and `./psalm --find-dead-code` to ensure that Travis builds will pass * Make sure to run `composer tests` and `./psalm --find-dead-code` to ensure that Travis builds will pass
* Don’t forget to add tests! * Don’t forget to add tests!
2 changes: 1 addition & 1 deletion phpcs.xml
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


<!-- These are just examples and stub classes/files, so it doesn't really matter if they're PSR-2 compliant. --> <!-- These are just examples and stub classes/files, so it doesn't really matter if they're PSR-2 compliant. -->
<exclude-pattern>src/Psalm/Internal/Stubs/</exclude-pattern> <exclude-pattern>src/Psalm/Internal/Stubs/</exclude-pattern>
<exclude-pattern>tests/fixtures/stubs/</exclude-pattern> <exclude-pattern>tests/fixtures/</exclude-pattern>
<rule ref="Generic.Files.LineLength"> <rule ref="Generic.Files.LineLength">
<exclude-pattern>tests</exclude-pattern> <exclude-pattern>tests</exclude-pattern>
</rule> </rule>
Expand Down
125 changes: 125 additions & 0 deletions src/Psalm/Config/Creator.php
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php
namespace Psalm\Config;

use Psalm\Exception\ConfigCreationException;
use Psalm\Internal\Provider;

class Creator
{
public static function getContents(
string $current_dir,
string $suggested_dir = null,
int $level = 3
) : string {
$replacements = [];

if ($suggested_dir) {
if (is_dir($current_dir . DIRECTORY_SEPARATOR . $suggested_dir)) {
$replacements[] = '<directory name="' . $suggested_dir . '" />';
} else {
$bad_dir_path = $current_dir . DIRECTORY_SEPARATOR . $suggested_dir;

throw new ConfigCreationException(
'The given path "' . $bad_dir_path . '" does not appear to be a directory'
);
}
} elseif (is_dir($current_dir . DIRECTORY_SEPARATOR . 'src')) {
$replacements[] = '<directory name="src" />';
} else {
$composer_json_location = $current_dir . DIRECTORY_SEPARATOR . 'composer.json';

if (!file_exists($composer_json_location)) {
throw new ConfigCreationException(
'Problem during config autodiscovery - could not find composer.json during initialization.'
);
}

/** @psalm-suppress MixedAssignment */
if (!$composer_json = json_decode(file_get_contents($composer_json_location), true)) {
throw new ConfigCreationException('Invalid composer.json at ' . $composer_json_location);
}

if (!is_array($composer_json)) {
throw new ConfigCreationException('Invalid composer.json at ' . $composer_json_location);
}

$replacements = self::getPsr4Paths($current_dir, $composer_json);

if (!$replacements) {
throw new ConfigCreationException(
'Could not located any PSR-4-compatible paths in ' . $composer_json_location
);
}
}

$template_file_name = dirname(__DIR__, 3) . '/assets/config_levels/' . $level . '.xml';

if (!file_exists($template_file_name)) {
throw new ConfigCreationException('Could not open config template ' . $template_file_name);
}

$template = (string)file_get_contents($template_file_name);

$template = str_replace(
'<directory name="src" />',
implode("\n ", $replacements),
$template
);

return $template;
}

/**
* @return string[]
* @psalm-suppress MixedAssignment
* @psalm-suppress MixedOperand
*/
private static function getPsr4Paths(string $current_dir, array $composer_json) : array
{
if (!isset($composer_json['autoload']['psr-4'])) {
return [];
}

$nodes = [];

/** @var string|string[] $path */
foreach ($composer_json['autoload']['psr-4'] as $paths) {
if (!is_array($paths)) {
$paths = [$paths];
}

foreach ($paths as $path) {
if ($path === '') {
/** @var string[] */
$php_files = array_merge(
glob($current_dir . DIRECTORY_SEPARATOR . '*.php'),
glob($current_dir . DIRECTORY_SEPARATOR . '**/*.php'),
glob($current_dir . DIRECTORY_SEPARATOR . '**/**/*.php')
);

foreach ($php_files as $php_file) {
$parts = explode(DIRECTORY_SEPARATOR, $php_file);

if ($parts[0] === 'vendor') {
continue;
}

if (count($parts) === 1) {
$nodes[] = '<file name="' . $php_file . '" />';
} else {
$nodes[] = '<file name="' . $parts[0] . '" />';
}
}
} else {
$nodes[] = '<directory name="' . $path . '" />';
}
}
}

$nodes = array_unique($nodes);

sort($nodes);

return $nodes;
}
}
55 changes: 32 additions & 23 deletions src/Psalm/Config/FileFilter.php
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace Psalm\Config; namespace Psalm\Config;


use SimpleXMLElement; use SimpleXMLElement;
use Psalm\Exception\ConfigException;


class FileFilter class FileFilter
{ {
Expand Down Expand Up @@ -116,9 +117,10 @@ public static function loadFromXMLElement(
continue; continue;
} }


echo 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR . throw new ConfigException(
(string)$directory['name'] . PHP_EOL; 'Could not resolve config path to ' . $base_dir
exit(1); . DIRECTORY_SEPARATOR . (string)$directory['name']
);
} }


foreach ($globs as $glob_index => $directory_path) { foreach ($globs as $glob_index => $directory_path) {
Expand All @@ -127,9 +129,10 @@ public static function loadFromXMLElement(
continue; continue;
} }


echo 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR . throw new ConfigException(
(string)$directory['name'] . ':' . $glob_index . PHP_EOL; 'Could not resolve config path to ' . $base_dir
exit(1); . DIRECTORY_SEPARATOR . (string)$directory['name'] . ':' . $glob_index
);
} }


if (!$directory_path) { if (!$directory_path) {
Expand All @@ -156,15 +159,17 @@ public static function loadFromXMLElement(
continue; continue;
} }


echo 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR . throw new ConfigException(
(string)$directory['name'] . PHP_EOL; 'Could not resolve config path to ' . $base_dir
exit(1); . DIRECTORY_SEPARATOR . (string)$directory['name']
);
} }


if (!is_dir($directory_path)) { if (!is_dir($directory_path)) {
echo $base_dir . DIRECTORY_SEPARATOR . (string)$directory['name'] throw new ConfigException(
. ' is not a directory ' . PHP_EOL; $base_dir . DIRECTORY_SEPARATOR . (string)$directory['name']
exit(1); . ' is not a directory'
);
} }


/** @var \RecursiveDirectoryIterator */ /** @var \RecursiveDirectoryIterator */
Expand Down Expand Up @@ -226,16 +231,18 @@ public static function loadFromXMLElement(
); );


if (empty($globs)) { if (empty($globs)) {
echo 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR . throw new ConfigException(
(string)$file['name'] . PHP_EOL; 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR .
exit(1); (string)$file['name']
);
} }


foreach ($globs as $glob_index => $file_path) { foreach ($globs as $glob_index => $file_path) {
if (!$file_path) { if (!$file_path) {
echo 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR . throw new ConfigException(
(string)$file['name'] . ':' . $glob_index . PHP_EOL; 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR .
exit(1); (string)$file['name'] . ':' . $glob_index
);
} }
$filter->addFile($file_path); $filter->addFile($file_path);
} }
Expand All @@ -245,9 +252,10 @@ public static function loadFromXMLElement(
$file_path = realpath($prospective_file_path); $file_path = realpath($prospective_file_path);


if (!$file_path) { if (!$file_path) {
echo 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR . throw new ConfigException(
(string)$file['name'] . PHP_EOL; 'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR .
exit(1); (string)$file['name']
);
} }


$filter->addFile($file_path); $filter->addFile($file_path);
Expand All @@ -274,8 +282,9 @@ public static function loadFromXMLElement(
$method_id = (string)$referenced_method['name']; $method_id = (string)$referenced_method['name'];


if (!preg_match('/^[^:]+::[^:]+$/', $method_id) && !static::isRegularExpression($method_id)) { if (!preg_match('/^[^:]+::[^:]+$/', $method_id) && !static::isRegularExpression($method_id)) {
echo 'Invalid referencedMethod ' . $method_id . PHP_EOL; throw new ConfigException(
exit(1); 'Invalid referencedMethod ' . $method_id
);
} }


$filter->method_ids[] = strtolower($method_id); $filter->method_ids[] = strtolower($method_id);
Expand Down
6 changes: 6 additions & 0 deletions src/Psalm/Exception/ConfigCreationException.php
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php
namespace Psalm\Exception;

class ConfigCreationException extends \Exception
{
}
32 changes: 7 additions & 25 deletions src/psalm.php
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ function ($arg) {
)); ));


$level = 3; $level = 3;
$source_dir = 'src'; $source_dir = null;


if (count($args)) { if (count($args)) {
if (count($args) > 2) { if (count($args) > 2) {
Expand All @@ -363,31 +363,13 @@ function ($arg) {
$source_dir = $args[0]; $source_dir = $args[0];
} }


if (!is_dir($source_dir)) { try {
$bad_dir_path = getcwd() . DIRECTORY_SEPARATOR . $source_dir; $template_contents = Psalm\Config\Creator::getContents($current_dir, $source_dir, $level);

} catch (Psalm\Exception\ConfigCreationException $e) {
if (!isset($args[0])) { die($e->getMessage() . PHP_EOL);
die('Please specify a directory - the default, "src", was not found in this project.' . PHP_EOL);
}

die('The given path "' . $bad_dir_path . '" does not appear to be a directory' . PHP_EOL);
}

$template_file_name = dirname(__DIR__) . '/assets/config_levels/' . $level . '.xml';

if (!file_exists($template_file_name)) {
die('Could not open config template ' . $template_file_name . PHP_EOL);
} }


$template = (string)file_get_contents($template_file_name); if (!file_put_contents($current_dir . 'psalm.xml', $template_contents)) {

$template = str_replace(
'<directory name="src" />',
'<directory name="' . $source_dir . '" />',
$template
);

if (!file_put_contents($current_dir . 'psalm.xml', $template)) {
die('Could not write to psalm.xml' . PHP_EOL); die('Could not write to psalm.xml' . PHP_EOL);
} }


Expand Down Expand Up @@ -450,7 +432,7 @@ function ($arg) {
$config = Config::getConfigForPath($current_dir, $current_dir, $output_format); $config = Config::getConfigForPath($current_dir, $current_dir, $output_format);
} }
} catch (Psalm\Exception\ConfigException $e) { } catch (Psalm\Exception\ConfigException $e) {
echo $e->getMessage(); echo $e->getMessage() . PHP_EOL;
exit(1); exit(1);
} }


Expand Down
56 changes: 56 additions & 0 deletions tests/Config/CreatorTest.php
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
namespace Psalm\Tests\Config;

use Psalm\Config\Creator;

class CreatorTest extends \Psalm\Tests\TestCase
{
/**
* @return void
*/
public static function setUpBeforeClass()
{
}

/**
* @return void
*/
public function setUp()
{
}

/**
* @return void
*/
public function testDiscoverLibDirectory()
{
$lib_contents = Creator::getContents(
dirname(__DIR__, 1)
. DIRECTORY_SEPARATOR . 'fixtures'
. DIRECTORY_SEPARATOR . 'config_discovery'
. DIRECTORY_SEPARATOR . 'files_in_lib',
null,
1
);

$this->assertSame('<?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"
>
<projectFiles>
<directory name="lib" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
</issueHandlers>
</psalm>
', $lib_contents);
}
}
8 changes: 8 additions & 0 deletions tests/fixtures/config_discovery/files_in_lib/composer.json
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "dummy/lib",
"autoload": {
"psr-4": {
"Foo\\Bar": "lib"
}
}
}
5 changes: 5 additions & 0 deletions tests/fixtures/config_discovery/files_in_lib/lib/Baz.php
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Foo\Bar;

class Baz {}
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

// some file
Loading

0 comments on commit 335b041

Please sign in to comment.