Skip to content

Commit

Permalink
introduces a new component Configuration/ConfigResolver to solve issue
Browse files Browse the repository at this point in the history
  • Loading branch information
llaville committed Dec 21, 2022
1 parent 3b485f1 commit f3178c0
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 80 deletions.
3 changes: 2 additions & 1 deletion .phplint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ extensions:
exclude:
- vendor
warning: true
#memory_limit: -1
#memory-limit: -1
no-cache: false
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"ext-json": "*",
"symfony/console": "^5.3.11 || ^6.0",
"symfony/finder": "^5.3.7 || ^6.0",
"symfony/options-resolver": "^5.4 || ^6.0",
"symfony/process": "^5.3.12 || ^6.0",
"symfony/yaml": "^5.3.11 || ^6.0",
"n98/junit-xml": "1.1.0"
Expand Down
103 changes: 24 additions & 79 deletions src/Command/LintCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
namespace Overtrue\PHPLint\Command;

use DateTime;
use Overtrue\PHPLint\Configuration\ConfigResolver;
use Overtrue\PHPLint\Cache;
use Overtrue\PHPLint\Linter;
use PHP_Parallel_Lint\PhpConsoleColor\ConsoleColor;
use PHP_Parallel_Lint\PhpConsoleColor\InvalidStyleException;
use PHP_Parallel_Lint\PhpConsoleHighlighter\Highlighter;
use N98\JUnitXml\Document;
use Overtrue\PHPLint\Cache;
use Overtrue\PHPLint\Linter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputArgument;
Expand All @@ -17,23 +18,9 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;

class LintCommand extends Command
{
private const DEFAULT_EXTENSIONS = ['php'];
private const DEFAULT_JOBS = 5;
private const DEFAULT_PATH = '.';

protected array $defaults = [
'jobs' => self::DEFAULT_JOBS,
'path' => self::DEFAULT_PATH,
'exclude' => [],
'extensions' => self::DEFAULT_EXTENSIONS,
'warning' => false
];

protected InputInterface $input;
protected OutputInterface $output;

Expand All @@ -45,7 +32,8 @@ protected function configure(): void
->addArgument(
'path',
InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
'Path to file or directory to lint.'
'Path to file or directory to lint',
[ConfigResolver::DEFAULT_PATH]
)
->addOption(
'exclude',
Expand All @@ -58,21 +46,21 @@ protected function configure(): void
null,
InputOption::VALUE_REQUIRED,
'Check only files with selected extensions',
self::DEFAULT_EXTENSIONS
ConfigResolver::DEFAULT_EXTENSIONS
)
->addOption(
'jobs',
'j',
InputOption::VALUE_REQUIRED,
'Number of paralleled jobs to run',
self::DEFAULT_JOBS
ConfigResolver::DEFAULT_JOBS
)
->addOption(
'configuration',
'c',
InputOption::VALUE_REQUIRED,
'Read configuration from config file',
$this->getConfigFile([self::DEFAULT_PATH])
ConfigResolver::DEFAULT_CONFIG_FILE
)
->addOption(
'no-configuration',
Expand All @@ -89,8 +77,9 @@ protected function configure(): void
->addOption(
'cache',
null,
InputOption::VALUE_REQUIRED,
'Path to the cache file.'
InputOption::VALUE_OPTIONAL,
'Path to the cache file.',
ConfigResolver::DEFAULT_CACHE_FILE
)
->addOption(
'no-progress',
Expand Down Expand Up @@ -137,7 +126,7 @@ public function initialize(InputInterface $input, OutputInterface $output): void
}

/**
* @throws InvalidStyleException
* @throws InvalidStyleException|\Throwable
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
Expand All @@ -156,7 +145,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

if ($verbosity >= OutputInterface::VERBOSITY_DEBUG) {
$output->writeln('Options : <comment>' . json_encode($options) . "</comment>\n");
$output->writeln('Options :');
foreach ($options as $name => $value) {
$output->writeln(\sprintf("<comment>%13s</comment> > <info>%s</info>", $name, \json_encode($value, \JSON_UNESCAPED_SLASHES)));
}
}
$output->writeln('');

Expand All @@ -168,13 +160,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

$usingCache = 'No';
if (!$input->getOption('no-cache') && Cache::isCached()) {
if (!$options['no-cache'] && Cache::isCached()) {
$usingCache = 'Yes';
$linter->setCache(Cache::get());
}

if (!empty($options['memory_limit'])) {
$linter->setMemoryLimit($options['memory_limit']);
if (!empty($options['memory-limit'])) {
$linter->setMemoryLimit($options['memory-limit']);
}

$fileCount = count($linter->getFiles());
Expand Down Expand Up @@ -365,61 +357,14 @@ public function getHighlightedCodeSnippet(string $filePath, int $lineNumber, int

protected function mergeOptions(): array
{
$options = $this->input->getOptions();
$options['path'] = $this->input->getArgument('path');
$options['cache'] = $this->input->getOption('cache');
if ($options['warning'] === false) {
unset($options['warning']);
}

$config = [];

if (!$this->input->getOption('no-configuration')) {
$inputPath = $this->input->getArgument('path');
$filename = $this->getConfigFile($inputPath);
$configResolver = new ConfigResolver($this->input);
$options = $configResolver->resolve();
$failures = $configResolver->getNestedExceptions();

if (empty($options['configuration']) && $filename) {
$options['configuration'] = $filename;
}

if (!empty($options['configuration'])) {
$config = $this->loadConfiguration($options['configuration']);
}
if (!empty($failures)) {
throw $failures[0];
}

$options = array_merge($this->defaults, array_filter($config), array_filter($options));

is_array($options['extensions']) || $options['extensions'] = explode(',', $options['extensions']);

return $options;
}

protected function getConfigFile(array $inputPath): false|string
{
if (1 == count($inputPath) && $first = reset($inputPath)) {
$dir = is_dir($first) ? $first : dirname($first);
} else {
$dir = \getcwd() . DIRECTORY_SEPARATOR;
}

$filename = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.phplint.yml';

return realpath($filename);
}

protected function loadConfiguration(string $path): array
{
try {
$configuration = Yaml::parse(file_get_contents($path));
if (!is_array($configuration)) {
throw new ParseException('Invalid content.', 1);
}

return $configuration;
} catch (ParseException $e) {
$this->output->writeln(sprintf('<error>Unable to parse the YAML string: %s</error>', $e->getMessage()));

return [];
}
}
}
167 changes: 167 additions & 0 deletions src/Configuration/ConfigResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<?php

namespace Overtrue\PHPLint\Configuration;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;

use Throwable;
use function array_keys;
use function count;
use function dirname;
use function getcwd;
use function ini_get;
use function is_array;
use function is_dir;
use function is_string;
use function realpath;
use function reset;
use function rtrim;
use function sprintf;
use const DIRECTORY_SEPARATOR;

/**
* @author Laurent Laville
*/
final class ConfigResolver
{
private const OPTION_JOBS = 'jobs';
private const OPTION_PATH = 'path';
private const OPTION_EXCLUDE = 'exclude';
private const OPTION_EXTENSIONS = 'extensions';
private const OPTION_WARNING = 'warning';
private const OPTION_CACHE_FILE = 'cache';
private const OPTION_NO_CACHE = 'no-cache';
private const OPTION_CONFIG_FILE = 'configuration';
private const OPTION_MEMORY_LIMIT = 'memory-limit';

public const DEFAULT_JOBS = 5;
public const DEFAULT_PATH = '.';
public const DEFAULT_EXTENSIONS = ['php'];
public const DEFAULT_CACHE_FILE = '.phplint-cache';
public const DEFAULT_CONFIG_FILE = '.phplint.yml';

private array $options = [
self::OPTION_JOBS => self::DEFAULT_JOBS,
self::OPTION_PATH => self::DEFAULT_PATH,
self::OPTION_EXCLUDE => [],
self::OPTION_EXTENSIONS => self::DEFAULT_EXTENSIONS,
self::OPTION_WARNING => false,
self::OPTION_CACHE_FILE => self::DEFAULT_CACHE_FILE,
self::OPTION_NO_CACHE => false,
self::OPTION_CONFIG_FILE => self::DEFAULT_CONFIG_FILE,
self::OPTION_MEMORY_LIMIT => false,
];

/**
* @var Throwable[]
*/
private array $exceptions = [];

public function __construct(InputInterface $input)
{
foreach (array_keys($this->options) as $option) {
if (self::OPTION_PATH == $option) {
$this->options[$option] = $input->getArgument('path');
} elseif (self::OPTION_MEMORY_LIMIT == $option) {
$this->options[$option] = ini_get('memory_limit');
} else {
$this->options[$option] = $input->getOption($option);
}
}

if ($input->getOption('no-configuration')) {
$this->options[self::OPTION_CONFIG_FILE] = '';
}
}

public function resolve(): array
{
if (!empty($this->options[self::OPTION_CONFIG_FILE])) {
$conf = $this->loadConfiguration($this->options[self::OPTION_CONFIG_FILE]);
$config = $this->getOptions()->resolve($conf);
} else {
$config = $this->options;
}
return $config;
}

/**
* @return Throwable[]
*/
public function getNestedExceptions(): array
{
return $this->exceptions;
}

/**
* @param string[] $inputPath
*/
private function getConfigFile(array $inputPath): false|string
{
if (1 == count($inputPath) && $first = reset($inputPath)) {
$dir = is_dir($first) ? $first : dirname($first);
} else {
$dir = getcwd() . DIRECTORY_SEPARATOR;
}

$filename = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . self::DEFAULT_CONFIG_FILE;

return realpath($filename);
}

private function loadConfiguration(string $path): array
{
try {
$configuration = Yaml::parseFile($path);
if (is_array($configuration)) {
return $configuration;
}
$this->exceptions[] = new ParseException(
sprintf('Invalid content type in "%s". Expected yaml format.', $path),
1
);
} catch (ParseException $e) {
$this->exceptions[] = $e;
}
return [];
}

private function getOptions(): OptionsResolver
{
$resolver = new OptionsResolver();

$resolver->setDefaults($this->options);

$resolver->setRequired(self::OPTION_PATH);

$resolver->setAllowedTypes(self::OPTION_JOBS, 'int');
$resolver->setAllowedTypes(self::OPTION_PATH, ['string', 'string[]']);
$resolver->setAllowedTypes(self::OPTION_EXCLUDE, ['string[]']);
$resolver->setAllowedTypes(self::OPTION_EXTENSIONS, ['string[]']);
$resolver->setAllowedTypes(self::OPTION_WARNING, 'bool');
$resolver->setAllowedTypes(self::OPTION_CACHE_FILE, ['null', 'string']);
$resolver->setAllowedTypes(self::OPTION_NO_CACHE, 'bool');
$resolver->setAllowedTypes(self::OPTION_CONFIG_FILE, 'string');
$resolver->setAllowedTypes(self::OPTION_MEMORY_LIMIT, ['int', 'string']);

$resolver->setNormalizer(self::OPTION_PATH, function(Options $options, $value) {
if (is_string($value)) {
$value = [$value];
}
return $value;
});
$resolver->setNormalizer(self::OPTION_CONFIG_FILE, function (Options $options, $value) {
$configFile = $this->getConfigFile($this->options[self::OPTION_PATH]);
if ($configFile === false) {
return '';
}
return $configFile;
});

return $resolver;
}
}

0 comments on commit f3178c0

Please sign in to comment.