Skip to content

Commit 1cc8739

Browse files
committed
feat: add output formatter
Signed-off-by: Emilien Escalle <[email protected]>
1 parent 6f1b011 commit 1cc8739

File tree

11 files changed

+495
-50
lines changed

11 files changed

+495
-50
lines changed

docs/usage.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Result:
2727
Usage:
2828
------
2929
30-
php-css-lint [--options='{ }'] input_to_lint
30+
php-css-lint [--options='{ }'] [--formatter=plain|json] input_to_lint
3131
3232
Arguments:
3333
----------
@@ -40,6 +40,13 @@ Arguments:
4040
* "nonStandards": { "property" => bool }: will merge with the current property
4141
Example: --options='{ "constructors": {"o" : false}, "allowedIndentationChars": ["\t"] }'
4242
43+
--formatter
44+
The formatter(s) to be used
45+
If not specified, the first available formatter will be used.
46+
Multiple formatters can be specified as a comma-separated list.
47+
Available formatters: plain, json
48+
Example: --formatter=plain
49+
4350
input_to_lint
4451
The CSS file path (absolute or relative)
4552
a glob pattern of file(s) to be linted

src/CssLint/Cli.php

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
namespace CssLint;
66

7-
use Generator;
87
use RuntimeException;
98
use Throwable;
9+
use CssLint\Formatter\FormatterInterface;
10+
use CssLint\Formatter\FormatterFactory;
11+
use Generator;
1012

1113
/**
1214
* @phpstan-import-type Errors from \CssLint\Linter
@@ -21,6 +23,10 @@ class Cli
2123

2224
private const RETURN_CODE_SUCCESS = 0;
2325

26+
private ?FormatterFactory $formatterFactory = null;
27+
28+
private FormatterInterface $formatterManager;
29+
2430
/**
2531
* Entrypoint of the cli, will execute the linter according to the given arguments
2632
* @param string[] $arguments arguments to be parsed (@see $_SERVER['argv'])
@@ -29,6 +35,15 @@ class Cli
2935
public function run(array $arguments): int
3036
{
3137
$cliArgs = $this->parseArguments($arguments);
38+
39+
try {
40+
$this->formatterManager = $this->getFormatterFactory()->create($cliArgs->formatter);
41+
} catch (RuntimeException $error) {
42+
// report invalid formatter names via default (plain) formatter
43+
$this->getFormatterFactory()->create(null)->printFatalError(null, $error);
44+
return self::RETURN_CODE_ERROR;
45+
}
46+
3247
if ($cliArgs->input === null || $cliArgs->input === '' || $cliArgs->input === '0') {
3348
$this->printUsage();
3449
return self::RETURN_CODE_SUCCESS;
@@ -41,7 +56,7 @@ public function run(array $arguments): int
4156

4257
return $this->lintInput($cssLinter, $cliArgs->input);
4358
} catch (Throwable $throwable) {
44-
$this->printError($throwable->getMessage());
59+
$this->formatterManager->printFatalError(null, $throwable);
4560
return self::RETURN_CODE_ERROR;
4661
}
4762
}
@@ -51,10 +66,13 @@ public function run(array $arguments): int
5166
*/
5267
private function printUsage(): void
5368
{
69+
$availableFormatters = $this->getFormatterFactory()->getAvailableFormatters();
70+
$defaultFormatter = $availableFormatters[0];
71+
5472
$this->printLine('Usage:' . PHP_EOL .
5573
'------' . PHP_EOL .
5674
PHP_EOL .
57-
' ' . self::SCRIPT_NAME . " [--options='{ }'] input_to_lint" . PHP_EOL .
75+
' ' . self::SCRIPT_NAME . " [--options='{ }'] [--formatter=plain|json] input_to_lint" . PHP_EOL .
5876
PHP_EOL .
5977
'Arguments:' . PHP_EOL .
6078
'----------' . PHP_EOL .
@@ -68,6 +86,13 @@ private function printUsage(): void
6886
' Example: --options=\'{ "constructors": {"o" : false}, "allowedIndentationChars": ["\t"] }\'' .
6987
PHP_EOL .
7088
PHP_EOL .
89+
' --formatter' . PHP_EOL .
90+
' The formatter(s) to be used' . PHP_EOL .
91+
' If not specified, the first available formatter will be used.' . PHP_EOL .
92+
' Multiple formatters can be specified as a comma-separated list.' . PHP_EOL .
93+
' Available formatters: ' . implode(', ', $availableFormatters) . PHP_EOL .
94+
' Example: --formatter=' . $defaultFormatter . PHP_EOL .
95+
PHP_EOL .
7196
' input_to_lint' . PHP_EOL .
7297
' The CSS file path (absolute or relative)' . PHP_EOL .
7398
' a glob pattern of file(s) to be linted' . PHP_EOL .
@@ -100,6 +125,15 @@ private function parseArguments(array $arguments): CliArgs
100125
return new CliArgs($arguments);
101126
}
102127

128+
private function getFormatterFactory(): FormatterFactory
129+
{
130+
if ($this->formatterFactory === null) {
131+
$this->formatterFactory = new FormatterFactory();
132+
}
133+
134+
return $this->formatterFactory;
135+
}
136+
103137
/**
104138
* Retrieve the properties from the given options
105139
* @param string $options the options to be parsed
@@ -207,7 +241,7 @@ private function lintGlob(string $glob): int
207241
$cssLinter = new Linter();
208242
$files = glob($glob);
209243
if ($files === [] || $files === false) {
210-
$this->printError('No files found for glob "' . $glob . '"');
244+
$this->formatterManager->printFatalError($glob, 'No files found for given glob pattern');
211245
return self::RETURN_CODE_ERROR;
212246
}
213247

@@ -227,19 +261,17 @@ private function lintGlob(string $glob): int
227261
*/
228262
private function lintFile(Linter $cssLinter, string $filePath): int
229263
{
230-
$source = "CSS file \"" . $filePath . "\"";
231-
$this->printLine('# Lint ' . $source . '...');
232-
264+
$source = "CSS file \"{$filePath}\"";
265+
$this->formatterManager->startLinting($source);
233266
if (!is_readable($filePath)) {
234-
$this->printError('File "' . $filePath . '" is not readable');
267+
$this->formatterManager->printFatalError($source, 'File is not readable');
235268
return self::RETURN_CODE_ERROR;
236269
}
237270

238271
$errors = $cssLinter->lintFile($filePath);
239272
return $this->printLinterErrors($source, $errors);
240273
}
241274

242-
243275
/**
244276
* Performs lint on a given string
245277
* @param Linter $cssLinter the instance of the linter
@@ -249,43 +281,29 @@ private function lintFile(Linter $cssLinter, string $filePath): int
249281
private function lintString(Linter $cssLinter, string $stringValue): int
250282
{
251283
$source = 'CSS string';
252-
$this->printLine('# Lint ' . $source . '...');
284+
$this->formatterManager->startLinting($source);
253285
$errors = $cssLinter->lintString($stringValue);
254286
return $this->printLinterErrors($source, $errors);
255287
}
256288

257-
/**
258-
* Display an error message
259-
* @param string $error the message to be displayed
260-
*/
261-
private function printError(string $error): void
262-
{
263-
$this->printLine("\033[31m/!\ Error: " . $error . "\033[0m" . PHP_EOL);
264-
}
265-
266289
/**
267290
* Display the errors returned by the linter
268291
* @param Generator<LintError> $errors the generated errors to be displayed
269292
* @return int the return code related to the execution of the linter
270293
*/
271294
private function printLinterErrors(string $source, Generator $errors): int
272295
{
273-
$hasErrors = false;
296+
$isValid = true;
274297
foreach ($errors as $error) {
275-
if ($hasErrors === false) {
276-
$this->printLine("\033[31m => " . $source . " is not valid:\033[0m" . PHP_EOL);
277-
$hasErrors = true;
298+
if ($isValid === true) {
299+
$isValid = false;
278300
}
279-
$this->printLine("\033[31m - " . $error . "\033[0m");
301+
$this->formatterManager->printLintError($source, $error);
280302
}
281303

282-
if ($hasErrors) {
283-
$this->printLine("");
284-
return self::RETURN_CODE_ERROR;
285-
}
304+
$this->formatterManager->endLinting($source, $isValid);
286305

287-
$this->printLine("\033[32m => " . $source . " is valid\033[0m" . PHP_EOL);
288-
return self::RETURN_CODE_SUCCESS;
306+
return $isValid ? self::RETURN_CODE_SUCCESS : self::RETURN_CODE_ERROR;
289307
}
290308

291309
/**

src/CssLint/CliArgs.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ class CliArgs
1515

1616
public ?string $options = null;
1717

18+
/**
19+
* Output formatter type
20+
*/
21+
public ?string $formatter = null;
22+
1823
/**
1924
* Constructor
2025
* @param Arguments $arguments arguments to be parsed (@see $_SERVER['argv'])
@@ -37,6 +42,9 @@ public function __construct(array $arguments)
3742
if (!empty($parsedArguments['options'])) {
3843
$this->options = $parsedArguments['options'];
3944
}
45+
if (!empty($parsedArguments['formatter'])) {
46+
$this->formatter = $parsedArguments['formatter'];
47+
}
4048
}
4149
}
4250

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CssLint\Formatter;
6+
7+
use RuntimeException;
8+
9+
/**
10+
* Factory to create FormatterManager based on requested names.
11+
*/
12+
class FormatterFactory
13+
{
14+
/** @var array<non-empty-string, FormatterInterface> */
15+
private array $available;
16+
17+
public function __construct()
18+
{
19+
$availableFormatters = [new PlainFormatter()];
20+
foreach ($availableFormatters as $formatter) {
21+
$this->available[$formatter->getName()] = $formatter;
22+
}
23+
}
24+
25+
/**
26+
* Create a FormatterManager based on a comma-separated list of formatter names.
27+
* @param string|null $formatterArg e.g. 'plain,json'
28+
* @return FormatterManager
29+
* @throws RuntimeException on invalid formatter names
30+
*/
31+
public function create(?string $formatterArg): FormatterManager
32+
{
33+
$names = array_filter(array_map('trim', explode(',', (string) $formatterArg)));
34+
$instances = [];
35+
$invalid = [];
36+
37+
$available = $this->getAvailableFormatters();
38+
39+
foreach ($names as $name) {
40+
if (in_array($name, $available, true)) {
41+
$instances[] = $this->available[$name];
42+
} else {
43+
$invalid[] = $name;
44+
}
45+
}
46+
47+
if (!empty($invalid)) {
48+
throw new RuntimeException('Invalid formatter(s): ' . implode(', ', $invalid));
49+
}
50+
51+
if (empty($instances)) {
52+
// Return the first available formatter if none specified
53+
// If no formatters are available, throw an exception
54+
if (empty($this->available)) {
55+
throw new RuntimeException('No formatters available');
56+
}
57+
58+
$instances[] = $this->available[array_key_first($this->available)];
59+
}
60+
61+
return new FormatterManager($instances);
62+
}
63+
64+
/**
65+
* Get the names of all available formatters.
66+
* @return non-empty-string[] List of formatter names
67+
*/
68+
public function getAvailableFormatters(): array
69+
{
70+
return array_keys($this->available);
71+
}
72+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CssLint\Formatter;
6+
7+
use CssLint\LintError;
8+
use Throwable;
9+
10+
interface FormatterInterface
11+
{
12+
/**
13+
* Returns the name used to select this formatter (e.g., 'plain', 'json').
14+
*
15+
* @return non-empty-string
16+
*/
17+
public function getName(): string;
18+
19+
/**
20+
* Start linting a source file.
21+
*
22+
* @param string $source The source being linted (e.g., "CSS file \"...\"").
23+
*/
24+
public function startLinting(string $source): void;
25+
26+
/**
27+
* Output a fatal error message.
28+
*
29+
* @param string|null $source The source being linted (e.g., "CSS file \"...\"").
30+
* @param Throwable|string $error The exception or error that occurred, which may include a message and stack trace.
31+
*/
32+
public function printFatalError(?string $source, mixed $error): void;
33+
34+
/**
35+
* Output a parsing or runtime error message.
36+
*
37+
* @param string $source The source being linted (e.g., "CSS file \"...\"").
38+
* @param LintError $error The error to be printed, which may include details like line number, column, and message.
39+
*/
40+
public function printLintError(string $source, LintError $error): void;
41+
42+
43+
/**
44+
* End linting a source file.
45+
*
46+
* @param string $source The source being linted (e.g., "CSS file \"...\"").
47+
* @param bool $isValid Whether the source is valid CSS.
48+
*/
49+
public function endLinting(string $source, bool $isValid): void;
50+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace CssLint\Formatter;
6+
7+
use RuntimeException;
8+
9+
/**
10+
* Manages one or more formatters as a single formatter.
11+
*/
12+
class FormatterManager implements FormatterInterface
13+
{
14+
/**
15+
* Constructor for FormatterManager.
16+
* @param FormatterInterface[] $formatters List of formatter instances to manage.
17+
*/
18+
public function __construct(private readonly array $formatters) {}
19+
20+
public function startLinting(string $source): void
21+
{
22+
foreach ($this->formatters as $formatter) {
23+
$formatter->startLinting($source);
24+
}
25+
}
26+
27+
public function printFatalError(?string $source, mixed $error): void
28+
{
29+
foreach ($this->formatters as $formatter) {
30+
$formatter->printFatalError($source, $error);
31+
}
32+
}
33+
34+
public function printLintError(string $source, mixed $error): void
35+
{
36+
foreach ($this->formatters as $formatter) {
37+
$formatter->printLintError($source, $error);
38+
}
39+
}
40+
41+
public function endLinting(string $source, bool $isValid): void
42+
{
43+
foreach ($this->formatters as $formatter) {
44+
$formatter->endLinting($source, $isValid);
45+
}
46+
}
47+
48+
public function getName(): string
49+
{
50+
throw new RuntimeException('FormatterManager does not have a single name. Use the names of individual formatters instead.');
51+
}
52+
}

0 commit comments

Comments
 (0)