Skip to content

Commit b8f3b63

Browse files
committed
Implement exec task interface
Add docs
1 parent 2aa6aa2 commit b8f3b63

File tree

3 files changed

+277
-12
lines changed

3 files changed

+277
-12
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
],
2020
"require": {
2121
"php": "^7.3 || ^8.0",
22-
"phpcq/plugin-api": "^1.0@dev"
22+
"ext-pcre": "*",
23+
"phpcq/plugin-api": "dev-feature/exec-interface"
2324
},
2425
"require-dev": {
2526
"phpcq/runner-bootstrap": "^1.0@dev",

composer.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/phpcs.php

Lines changed: 265 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@
44

55
use Phpcq\PluginApi\Version10\Configuration\PluginConfigurationBuilderInterface;
66
use Phpcq\PluginApi\Version10\Configuration\PluginConfigurationInterface;
7+
use Phpcq\PluginApi\Version10\Definition\Builder\ConsoleApplicationBuilderInterface;
8+
use Phpcq\PluginApi\Version10\Definition\ExecTaskDefinitionBuilderInterface;
79
use Phpcq\PluginApi\Version10\DiagnosticsPluginInterface;
10+
use Phpcq\PluginApi\Version10\Exception\RuntimeException;
811
use Phpcq\PluginApi\Version10\EnvironmentInterface;
12+
use Phpcq\PluginApi\Version10\ExecPluginInterface;
13+
use Phpcq\PluginApi\Version10\Output\OutputInterface;
14+
use Phpcq\PluginApi\Version10\Task\OutputWritingTaskInterface;
15+
use Phpcq\PluginApi\Version10\Task\TaskInterface;
916
use Phpcq\PluginApi\Version10\Util\CheckstyleReportAppender;
1017

11-
return new class implements DiagnosticsPluginInterface {
18+
return new class implements DiagnosticsPluginInterface, ExecPluginInterface {
1219
public function getName(): string
1320
{
1421
return 'phpcs';
@@ -132,4 +139,261 @@ function ($path) use ($projectPath): string {
132139

133140
return array_merge($arguments, $config->getStringList('directories'));
134141
}
142+
143+
public function describeExecTask(
144+
ExecTaskDefinitionBuilderInterface $definitionBuilder,
145+
EnvironmentInterface $environment
146+
): void {
147+
148+
$this->describeApplication(
149+
'phpcs',
150+
'PHP CodeSniffer by Squiz (http://www.squiz.net)',
151+
$definitionBuilder,
152+
$environment
153+
);
154+
$this->describeApplication(
155+
'phpcbf',
156+
'PHP Code Beautifier and Fixer',
157+
$definitionBuilder,
158+
$environment,
159+
'fix'
160+
);
161+
}
162+
163+
public function createExecTask(
164+
?string $application,
165+
array $arguments,
166+
EnvironmentInterface $environment
167+
): TaskInterface {
168+
switch ($application) {
169+
case null:
170+
return $environment->getTaskFactory()->buildRunPhar('phpcs', $arguments)->build();
171+
172+
case 'fix':
173+
return $environment->getTaskFactory()->buildRunPhar('phpcbf', $arguments)->build();
174+
175+
default:
176+
throw new RuntimeException('Unknown application "' . $application . '"');
177+
}
178+
}
179+
180+
private function describeApplication(
181+
string $tool,
182+
string $description,
183+
ExecTaskDefinitionBuilderInterface $definitionBuilder,
184+
EnvironmentInterface $environment,
185+
?string $applicationName = null
186+
): void {
187+
$application = $definitionBuilder->describeApplication($description, $applicationName);
188+
189+
$task = $environment->getTaskFactory()->buildRunPhar($tool, ['--help'])->build();
190+
if ($task instanceof OutputWritingTaskInterface) {
191+
$parser = $this->createHelpParser();
192+
$task->runForOutput($parser);
193+
/** @psalm-suppress UndefinedInterfaceMethod */
194+
$parser->parse($application);
195+
}
196+
}
197+
198+
private function createHelpParser(): OutputInterface
199+
{
200+
return new class implements OutputInterface
201+
{
202+
/** @var string */
203+
private $help = '';
204+
205+
/** @var array<string,string> */
206+
private $descriptions = [];
207+
208+
/** @var array<string,array{description:string, short?: bool, paramName?: string|null}> */
209+
private $options = [];
210+
211+
/** @var array<string,string> */
212+
private $arguments = [];
213+
214+
public function write(
215+
string $message,
216+
int $verbosity = self::VERBOSITY_NORMAL,
217+
int $channel = self::CHANNEL_STDOUT
218+
): void {
219+
if ($channel === self::CHANNEL_STDOUT) {
220+
$this->help .= $message;
221+
}
222+
}
223+
224+
public function writeln(
225+
string $message,
226+
int $verbosity = self::VERBOSITY_NORMAL,
227+
int $channel = self::CHANNEL_STDOUT
228+
): void {
229+
if ($channel === self::CHANNEL_STDOUT) {
230+
$this->help .= $message . "\n";
231+
}
232+
}
233+
234+
public function parse(ConsoleApplicationBuilderInterface $application): void
235+
{
236+
$this->doParse();
237+
$this->describe($application);
238+
}
239+
240+
private function doParse(): void
241+
{
242+
preg_match('#Usage: [a-z]+ (.+)\n\n(.+)\n\n(.+)\n\n(.+)#s', $this->help, $blocks);
243+
244+
// Parse descriptions first so other block can use them
245+
if (isset($blocks[4])) {
246+
$this->parseDescriptions($blocks[4]);
247+
}
248+
249+
// Parse usage
250+
if (isset($blocks[1])) {
251+
$this->parseUsage($blocks[1]);
252+
}
253+
254+
// Parse short option descriptions
255+
if (isset($blocks[2])) {
256+
$this->parseShortOptionDescriptions($blocks[2]);
257+
}
258+
259+
// Parse option descriptions
260+
if (isset($blocks[3])) {
261+
$this->parseOptionDescriptions($blocks[3]);
262+
}
263+
}
264+
265+
private function describe(ConsoleApplicationBuilderInterface $application): void
266+
{
267+
foreach ($this->arguments as $argument => $description) {
268+
$argument = $application->describeArgument($argument, $description);
269+
270+
if (stripos($description, 'one or more') !== false) {
271+
$argument->isArray();
272+
}
273+
}
274+
275+
foreach ($this->options as $option => $config) {
276+
$definition = $application->describeOption($option, $config['description']);
277+
278+
if ($config['short'] ?? false) {
279+
$definition->withShortcutOnly();
280+
}
281+
282+
if ($config['paramName'] ?? null) {
283+
$definition->withRequiredValue($config['paramName']);
284+
}
285+
286+
// Fixme: Is there a way to detect it properly?
287+
if ($config['keyValue'] ?? false) {
288+
$definition->withOptionValueSeparator(' ');
289+
$definition->withKeyValueMap(true);
290+
}
291+
}
292+
}
293+
294+
private function parseDescriptions(string $descriptions): void
295+
{
296+
$lines = explode("\n", $descriptions);
297+
foreach ($lines as $line) {
298+
preg_match('#^\s+<([^>]+)>\s+(.*)$#', $line, $matches);
299+
if (isset($matches[1])) {
300+
$this->descriptions[$matches[1]] = $matches[2];
301+
}
302+
}
303+
}
304+
305+
private function parseUsage(string $help): void
306+
{
307+
preg_match_all('#\[-(-?)([a-z-]+)(=<([^\]]+)>)?\]#', $help, $matches);
308+
foreach ($matches[2] as $index => $option) {
309+
if ($matches[1][$index] === '-') {
310+
$this->options[$option] = [
311+
'description' => $this->descriptions[$matches[4][$index] ?? $option] ?? '',
312+
'paramName' => $matches[4][$index] ?: null,
313+
'short' => false,
314+
];
315+
} else {
316+
foreach (str_split($option, 1) as $shortOption) {
317+
$this->options[$shortOption] = [
318+
'description' => $this->descriptions[$matches[4][$index] ?? $option] ?? '',
319+
'short' => true,
320+
];
321+
}
322+
}
323+
}
324+
325+
preg_match_all('#\[-(-?)([a-z-]+)\s([^=]+)\[=(.*)\]\]#', $help, $matches);
326+
foreach ($matches[2] as $index => $option) {
327+
$this->options[$option] = [
328+
'description' => $this->descriptions[$matches[4][$index]] ?? '',
329+
'keyValue' => true,
330+
'short' => $matches[1][$index] !== '-',
331+
];
332+
}
333+
334+
preg_match_all('#\s<([^>]+)>#', $help, $matches);
335+
foreach ($matches[1] as $match) {
336+
$this->arguments[$match] = $this->descriptions[$match] ?? '';
337+
}
338+
}
339+
340+
private function parseShortOptionDescriptions(string $help): void
341+
{
342+
$lines = explode("\n", $help);
343+
$currentName = null;
344+
$currentDescription = '';
345+
346+
foreach ($lines as $line) {
347+
preg_match('#^\s+(-[a-z-]*)?\s+(.*)$#', $line, $matches);
348+
349+
if ($matches[1] === '') {
350+
$currentDescription .= ' ' . trim($matches[2]);
351+
continue;
352+
}
353+
354+
if (null !== $currentName) {
355+
assert(is_string($currentName));
356+
$this->options[$currentName]['short'] = true;
357+
$this->options[$currentName]['description'] = $currentDescription;
358+
}
359+
360+
$currentName = substr($matches[1], 1);
361+
$currentDescription = $matches[2];
362+
}
363+
364+
if ($currentName !== null) {
365+
assert(is_string($currentName));
366+
$this->options[$currentName]['short'] = true;
367+
$this->options[$currentName]['description'] = $currentDescription;
368+
}
369+
}
370+
371+
private function parseOptionDescriptions(string $help): void
372+
{
373+
$lines = explode("\n", $help);
374+
$currentName = null;
375+
$currentDescription = '';
376+
377+
foreach ($lines as $line) {
378+
preg_match('#^\s+(--[a-z-]*)?\s+(.*)$#', $line, $matches);
379+
380+
if ($matches[1] === '' || $matches[1] === null) {
381+
$currentDescription .= ' ' . trim($matches[2]);
382+
continue;
383+
}
384+
385+
if (null !== $currentName) {
386+
$this->options[$currentName]['description'] = $currentDescription;
387+
}
388+
389+
$currentName = substr($matches[1], 2);
390+
$currentDescription = $matches[2];
391+
}
392+
393+
if (null !== $currentName) {
394+
$this->options[$currentName]['description'] = $currentDescription;
395+
}
396+
}
397+
};
398+
}
135399
};

0 commit comments

Comments
 (0)