Skip to content

New Feature: Add .editorconfig Support #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,28 @@ vendor/bin/ecs list-checkers --output-format json

<br>

### Can I Use My [`.editorconfig`](https://editorconfig.org/)?

Mostly! By using `withEditorConfig()`, ECS will automatically discover
the `.editorconfig` file in the project's root directory. It will use any
rules under `[*]` or `[*.php]` (the latter taking priority) and respect the
settings for:

- `indent_style`
- `end_of_line`
- `max_line_length`
- `trim_trailing_whitespace`
- `insert_final_newline`
- [`quote_type`](https://github.com/jednano/codepainter#quote_type-single-double-auto)
- Only `single` and `auto` are respected.
- Warning: this is a proposed field, but not fully standard.

These settings will take precedence over similar rules configured through sets
like PSR12, to avoid conflicting with other tooling using your `.editorconfig`.

Unfortunately, not all settings are currently respected, but PRs are always
welcome!

## How to Migrate from another coding standard tool?

Do you use another tool and want to migrate? It's pretty straightforward - here is "how to":
Expand Down
1 change: 1 addition & 0 deletions ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

return ECSConfig::configure()
->withPaths([__DIR__ . '/bin', __DIR__ . '/config', __DIR__ . '/src', __DIR__ . '/tests'])
->withEditorConfig()
->withRules([LineLengthFixer::class])
->withRootFiles()
->withSkip(['*/Source/*', '*/Fixture/*'])
Expand Down
92 changes: 92 additions & 0 deletions src/Configuration/ECSConfigBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@
namespace Symplify\EasyCodingStandard\Configuration;

use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\EndFileNewlineSniff as GenericEndFileNewlineSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\EndFileNoNewlineSniff;
use PHP_CodeSniffer\Standards\PSR2\Sniffs\Files\EndFileNewlineSniff as Psr2EndFileNewlineSniff;
use PHP_CodeSniffer\Standards\Squiz\Sniffs\Strings\DoubleQuoteUsageSniff;
use PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\SuperfluousWhitespaceSniff;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Fixer\StringNotation\SingleQuoteFixer;
use PhpCsFixer\Fixer\Whitespace\NoTrailingWhitespaceFixer;
use PhpCsFixer\Fixer\Whitespace\SingleBlankLineAtEofFixer;
use Symfony\Component\Finder\Finder;
use Symplify\CodingStandard\Fixer\LineLength\LineLengthFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\Configuration\EditorConfig\EditorConfigFactory;
use Symplify\EasyCodingStandard\Configuration\EditorConfig\EndOfLine;
use Symplify\EasyCodingStandard\Configuration\EditorConfig\IndentStyle;
use Symplify\EasyCodingStandard\Configuration\EditorConfig\QuoteType;
use Symplify\EasyCodingStandard\Exception\Configuration\SuperfluousConfigurationException;
use Symplify\EasyCodingStandard\ValueObject\Option;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
Expand Down Expand Up @@ -73,8 +86,12 @@ final class ECSConfigBuilder

private ?bool $reportingRealPath = null;

private ?bool $useEditorConfig = null;

public function __invoke(ECSConfig $ecsConfig): void
{
$this->applyEditorConfigSettings();

if ($this->sets !== []) {
$ecsConfig->sets($this->sets);
}
Expand Down Expand Up @@ -539,6 +556,13 @@ public function withCache(?string $directory = null, ?string $namespace = null):
return $this;
}

public function withEditorConfig(bool $enabled = true): self
{
$this->useEditorConfig = $enabled;

return $this;
}

/**
* @param Option::INDENTATION_*|null $indentation
*/
Expand Down Expand Up @@ -596,4 +620,72 @@ public function withRealPathReporting(bool $absolutePath = true): self

return $this;
}

private function applyEditorConfigSettings(): void
{
if (! $this->useEditorConfig) {
return;
}

/**
* PHP CS Fixer handles most of this, code sniffer just needs to stay
* out of out way. Luckily, we have a pass to make sure it does!
*
* This does introduce a quirk that if someone manually disables a Fixer
* rule, but does not enable the equivalent Sniffer rule, that
* EditorConfig setting won't be respected. But why would they do that?
*
* @see Symplify\EasyCodingStandard\DependencyInjection\CompilerPass\RemoveMutualCheckersCompilerPass
*/
$editorConfig = (new EditorConfigFactory())->load();

if ($editorConfig->indentStyle instanceof IndentStyle) {
$this->indentation = match ($editorConfig->indentStyle) {
IndentStyle::Space => Option::INDENTATION_SPACES,
IndentStyle::Tab => Option::INDENTATION_TAB,
};
}

if ($editorConfig->endOfLine instanceof EndOfLine) {
$this->lineEnding = match ($editorConfig->endOfLine) {
EndOfLine::Posix => "\n",
EndOfLine::Legacy => "\r",
EndOfLine::Windows => "\r\n",
};
}

if ($editorConfig->maxLineLength) {
$this->rulesWithConfiguration[LineLengthFixer::class] = [
...($this->rulesWithConfiguration[LineLengthFixer::class] ?? []),
'line_length' => $editorConfig->maxLineLength,
];
}

if ($editorConfig->trimTrailingWhitespace === true) {
$this->rules[] = NoTrailingWhitespaceFixer::class;
} elseif ($editorConfig->trimTrailingWhitespace === false) {
$this->skip = [...$this->skip, NoTrailingWhitespaceFixer::class, SuperfluousWhitespaceSniff::class];
}

if ($editorConfig->insertFinalNewline === true) {
$this->rules[] = SingleBlankLineAtEofFixer::class;
} elseif ($editorConfig->insertFinalNewline === false) {
$this->rules[] = EndFileNoNewlineSniff::class;
$this->skip[] = [
SingleBlankLineAtEofFixer::class,
Psr2EndFileNewlineSniff::class,
GenericEndFileNewlineSniff::class,
];
}

if ($editorConfig->quoteType === QuoteType::Auto) {
$this->rules[] = SingleQuoteFixer::class;
} elseif ($editorConfig->quoteType === QuoteType::Single) {
$this->rulesWithConfiguration[SingleQuoteFixer::class] = [
'strings_containing_single_quote_chars' => true,
];
} elseif ($editorConfig->quoteType === QuoteType::Double) {
$this->skip = [...$this->skip, SingleQuoteFixer::class, DoubleQuoteUsageSniff::class];
}
}
}
21 changes: 21 additions & 0 deletions src/Configuration/EditorConfig/EditorConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Configuration\EditorConfig;

/**
* @see https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties
*/
class EditorConfig
{
public function __construct(
public readonly ?IndentStyle $indentStyle,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems due to enum usage

enum IndentStyle: string
{
    case Space = 'space';
    case Tab = 'tab';
}

I think we can just use class and constant.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created new PR to fix it

by use class and constant instead.

public readonly ?EndOfLine $endOfLine,
public readonly ?bool $trimTrailingWhitespace,
public readonly ?bool $insertFinalNewline,
public readonly ?int $maxLineLength,
public readonly ?QuoteType $quoteType
) {
}
}
76 changes: 76 additions & 0 deletions src/Configuration/EditorConfig/EditorConfigFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Configuration\EditorConfig;

use Exception;

class EditorConfigFactory
{
public function load(): EditorConfig
{
// By default, composer executes scripts from within the project root.
$projectRoot = getcwd();
$editorConfigPath = $projectRoot . '/.editorconfig';

if (! file_exists($editorConfigPath)) {
throw new Exception('No .editorconfig found.');
}

$configFileContent = file_get_contents($editorConfigPath);

if ($configFileContent === false) {
throw new Exception('Unable to load .editorconfig.');
}

return $this->parse($configFileContent);
}

public function parse(string $editorConfigFileContents): EditorConfig
{
$fullConfig = parse_ini_string($editorConfigFileContents, true, INI_SCANNER_TYPED);

if ($fullConfig === false) {
throw new Exception('Unable to parse .editorconfig.');
}

$config = [...$fullConfig['*'] ?? [], ...$fullConfig['*.php'] ?? []];

// Just letting "validation" happen with PHP's type hints.
return new EditorConfig(
indentStyle: $this->field($config, 'indent_style', IndentStyle::tryFrom(...)),
endOfLine: $this->field($config, 'end_of_line', EndOfLine::tryFrom(...)),
trimTrailingWhitespace: $this->field($config, 'trim_trailing_whitespace', $this->id(...)),
insertFinalNewline: $this->field($config, 'insert_final_newline', $this->id(...)),
maxLineLength: $this->field($config, 'max_line_length', $this->id(...)),
quoteType: $this->field($config, 'quote_type', QuoteType::tryFrom(...)),
);
}

/**
* @template From
* @template To
* @param mixed[] $config
* @param callable(From): To $transform
* @return To|null
*/
private function field(array $config, string $field, callable $transform): mixed
{
if (! isset($config[$field])) {
return null;
}

return $transform($config[$field]);
}

/**
* @template T
* @param T $value
* @return T
*/
private function id(mixed $value): mixed
{
return $value;
}
}
12 changes: 12 additions & 0 deletions src/Configuration/EditorConfig/EndOfLine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Configuration\EditorConfig;

enum EndOfLine: string
{
case Posix = 'lf';
case Legacy = 'cr';
case Windows = 'crlf';
}
11 changes: 11 additions & 0 deletions src/Configuration/EditorConfig/IndentStyle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Configuration\EditorConfig;

enum IndentStyle: string
{
case Space = 'space';
case Tab = 'tab';
}
15 changes: 15 additions & 0 deletions src/Configuration/EditorConfig/QuoteType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Symplify\EasyCodingStandard\Configuration\EditorConfig;

/**
* @see https://github.com/jednano/codepainter#quote_type-single-double-auto
*/
enum QuoteType: string
{
case Single = 'single';
case Double = 'double';
case Auto = 'auto';
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
namespace Symplify\EasyCodingStandard\DependencyInjection\CompilerPass;

use Illuminate\Container\Container;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\EndFileNewlineSniff as GenericEndFileNewlineSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\EndFileNoNewlineSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\LowerCaseConstantSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\UpperCaseConstantSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\DisallowSpaceIndentSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\DisallowTabIndentSniff;
use PHP_CodeSniffer\Standards\PSR12\Sniffs\Files\FileHeaderSniff;
use PHP_CodeSniffer\Standards\PSR2\Sniffs\Files\EndFileNewlineSniff;
use PhpCsFixer\Fixer\Casing\ConstantCaseFixer;
use PhpCsFixer\Fixer\ControlStructure\YodaStyleFixer;
use PhpCsFixer\Fixer\LanguageConstruct\DeclareEqualNormalizeFixer;
Expand All @@ -32,6 +37,9 @@ final class ConflictingCheckersCompilerPass
['SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff', DeclareEqualNormalizeFixer::class],
['SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff', BlankLineAfterOpeningTagFixer::class],
[FileHeaderSniff::class, NoBlankLinesAfterPhpdocFixer::class],
[EndFileNewlineSniff::class, EndFileNoNewlineSniff::class],
[GenericEndFileNewlineSniff::class, EndFileNoNewlineSniff::class],
[DisallowTabIndentSniff::class, DisallowSpaceIndentSniff::class],
];

public function process(Container $container): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use PHP_CodeSniffer\Standards\Generic\Sniffs\Arrays\DisallowLongArraySyntaxSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Arrays\DisallowShortArraySyntaxSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis\AssignmentInConditionSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\EndFileNewlineSniff as GenericEndFileNewlineSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineEndingsSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\DisallowMultipleStatementsSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\LowerCaseConstantSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\LowerCaseKeywordSniff;
Expand Down Expand Up @@ -52,7 +54,9 @@
use PhpCsFixer\Fixer\Whitespace\IndentationTypeFixer;
use PhpCsFixer\Fixer\Whitespace\LineEndingFixer;
use PhpCsFixer\Fixer\Whitespace\NoExtraBlankLinesFixer;
use PhpCsFixer\Fixer\Whitespace\NoTrailingWhitespaceFixer;
use PhpCsFixer\Fixer\Whitespace\SingleBlankLineAtEofFixer;
use Symplify\CodingStandard\Fixer\LineLength\LineLengthFixer;

final class RemoveMutualCheckersCompilerPass
{
Expand All @@ -62,6 +66,7 @@ final class RemoveMutualCheckersCompilerPass
* @var string[][]
*/
private const DUPLICATED_CHECKER_GROUPS = [
[IndentationTypeFixer::class, ScopeIndentSniff::class],
[IndentationTypeFixer::class, DisallowTabIndentSniff::class],
[IndentationTypeFixer::class, DisallowSpaceIndentSniff::class],
[StrictComparisonFixer::class, 'SlevomatCodingStandard\Sniffs\Operators\DisallowEqualOperatorsSniff'],
Expand Down Expand Up @@ -89,6 +94,7 @@ final class RemoveMutualCheckersCompilerPass
'SlevomatCodingStandard\Sniffs\Commenting\ForbiddenAnnotationsSniff',
],
[NoExtraBlankLinesFixer::class, SuperfluousWhitespaceSniff::class],
[NoTrailingWhitespaceFixer::class, SuperfluousWhitespaceSniff::class],
[IncludeFixer::class, LanguageConstructSpacingSniff::class],
[
AssignmentInConditionSniff::class,
Expand All @@ -104,11 +110,14 @@ final class RemoveMutualCheckersCompilerPass
[ConstantCaseFixer::class, LowerCaseConstantSniff::class],
[LowercaseKeywordsFixer::class, LowerCaseKeywordSniff::class],
[SingleBlankLineAtEofFixer::class, EndFileNewlineSniff::class],
[SingleBlankLineAtEofFixer::class, GenericEndFileNewlineSniff::class],
[EndFileNewlineSniff::class, GenericEndFileNewlineSniff::class],
[BracesFixer::class, ScopeIndentSniff::class],
[BracesFixer::class, ScopeClosingBraceSniff::class],
[ClassDefinitionFixer::class, ClassDeclarationSniff::class],
[NoClosingTagFixer::class, ClosingTagSniff::class],
[SingleClassElementPerStatementFixer::class, PropertyDeclarationSniff::class],
[LineLengthFixer::class, LineLengthSniff::class],
];

public function process(Container $container): void
Expand Down
Loading