Skip to content
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

Enforce PSR-12 and organise imports on Winter CMS core files #1188

Draft
wants to merge 32 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e7ebbb2
Enforce PSR-12 on Winter CMS core files
bennothommo Aug 23, 2024
66bd3f3
Additional code style fixes to improve future diffs and IDE support
bennothommo Aug 23, 2024
dbf6019
Use Composer-derived PHPCS, udpate GitHub Actions
bennothommo Aug 23, 2024
ce14395
Update GitHub Actions
bennothommo Aug 23, 2024
87171ca
Reset modules before checking code quality
bennothommo Aug 23, 2024
e23c0b2
Exclude some rules for partials and views
bennothommo Aug 23, 2024
b2ffbe9
Add rule for array indentation
bennothommo Aug 23, 2024
7b72f82
Fix reference to File facade
bennothommo Aug 23, 2024
2643609
Merge branch 'develop' into fix/psr-12
bennothommo Sep 2, 2024
52a824f
Split off Winter CMS code style ruleset to its own XML
bennothommo Sep 2, 2024
a6b814b
Remove partial from exclusion list
bennothommo Sep 2, 2024
236c459
Add "winter:sniff" command
bennothommo Sep 2, 2024
47fbf2a
Merge branch 'develop' into fix/psr-12
bennothommo Dec 7, 2024
9d1b92f
Fix remaining code smells
bennothommo Dec 7, 2024
b1a5a79
Change alias command
bennothommo Dec 7, 2024
1da2568
Add support for `--fix` option to `sniff` command
LukeTowers Dec 7, 2024
51af67d
Alias `winter:test` to `test`
LukeTowers Dec 7, 2024
0ae77a8
Improve / relocate PHPCS stubs
LukeTowers Dec 11, 2024
1dd04fc
Update composer scripts
LukeTowers Dec 11, 2024
f2e7289
Add class constant visibility
LukeTowers Dec 11, 2024
041dbb1
Fix remaining warnings
LukeTowers Dec 11, 2024
61b127f
Add progress flag to phpcs
LukeTowers Dec 11, 2024
33a1518
Add initial version of NoGlobalAliasesSniffTest
LukeTowers Dec 16, 2024
afedfb9
Add support for --parallel option to winter:sniff
LukeTowers Dec 16, 2024
69f19e3
Fix remaining global imports
LukeTowers Dec 16, 2024
bace8e7
Ignore the project's phpcs.xml file when exporting it as a new project
LukeTowers Dec 16, 2024
16d7cc2
Fix overzealous find/replace
LukeTowers Dec 16, 2024
09b945e
Remove debug code
LukeTowers Dec 16, 2024
02c8469
Use Winter's Url facade instead of Laravel's
LukeTowers Dec 16, 2024
6fe5671
Typo fix
LukeTowers Dec 17, 2024
fb92ce3
Allow use statements to reference classes in the same namespace
LukeTowers Dec 17, 2024
27207c2
Fix default scaffolding stub files to conform to coding standard
LukeTowers Dec 18, 2024
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
1 change: 1 addition & 0 deletions modules/system/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ protected function registerConsole()
$this->registerConsoleCommand('winter.version', \System\Console\WinterVersion::class);
$this->registerConsoleCommand('winter.manifest', \System\Console\WinterManifest::class);
$this->registerConsoleCommand('winter.test', \System\Console\WinterTest::class);
$this->registerConsoleCommand('winter.sniff', \System\Console\WinterSniff::class);

$this->registerConsoleCommand('plugin.install', \System\Console\PluginInstall::class);
$this->registerConsoleCommand('plugin.remove', \System\Console\PluginRemove::class);
Expand Down
279 changes: 279 additions & 0 deletions modules/system/console/WinterSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
<?php

namespace System\Console;

use Illuminate\Support\Facades\File;
use Symfony\Component\Process\Exception\ProcessSignaledException;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
use System\Classes\PluginManager;

/**
* Console command to check code style and formatting issues.
*
* If a plugin is provided, this command will search for a `phpcs.xml file inside the plugin's directory. If not found,
* the user will be prompted to create one if they wish.
*/
class WinterSniff extends BaseScaffoldCommand
{
/**
* @var string|null The default command name for lazy loading.
*/
protected static $defaultName = 'winter:sniff';

/**
* @var string The console command name.
*/
protected $name = 'winter:sniff';

/**
* The console command description.
*/
protected $description = 'Validates source code to ensure a consistent coding style.';

/**
* @var string The console command signature as ignoreValidationErrors causes options not to be registered.
*/
protected $signature = 'winter:sniff
{paths?* : The path to the files and/or directories to check, if you wish to limit the scope of files checked.}
{?--c|config= : Path to a custom PHPCS configuration XML file}
{?--p|plugin= : Checks the coding style of a plugin}
{?--e|no-warnings : Ignore warnings and only show errors}
{?--s|summary : Display a summary of the results}
';

protected $stubs = [
'scaffold/phpcs/phpcs.xml.stub' => 'phpcs.xml',
];

/**
* Create a new command instance.
*/
public function __construct()
{
parent::__construct();

// Register aliases for backwards compatibility with October
$this->setAliases(['winter:phpcs']);
bennothommo marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Execute the console command.
*
* @throws \Winter\Storm\Exception\ApplicationException
* @return int|void
*/
public function handle()
{
$configFile = $this->getConfigFile();

if (!File::exists($configFile)) {
$this->error(sprintf('No configuration file found. Exiting.', $configFile));
return 1;
}

$phpCs = $this->findPhpCsExecutable();

if (is_null($phpCs)) {
$this->error('Unable to find the `phpcs` executable. Please ensure that you have installed the developer
dependencies through Composer.');
return 1;
}

$phpCsArgs = $this->buildPhpCsArgs($phpCs, $configFile);

ini_set('memory_limit', '512M');
$process = new Process($phpCsArgs, base_path());

// Set an unlimited timeout
$process->setTimeout(0);

// Attempt to set tty mode, catch and warn with the exception message if unsupported
try {
$process->setTty(true);
} catch (\Throwable $e) {
$this->warn($e->getMessage());
}

$exitCode = 1;

try {
$exitCode = $process->run(function ($type, $line) {
$this->output->write($line);
});
} catch (ProcessSignaledException $e) {
if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}

return 1;
}

if ($exitCode === 0) {
return $this->components->info('No coding style issues found.');
}
}

/**
* Gets the config file path.
*
* If the config file is not found, the user will be prompted to create one if they wish.
*/
protected function getConfigFile(): ?string
{
$path = $this->expectedConfigPath();

if (!File::exists($path)) {
$this->warn(sprintf('Configuration file %s not found.', $path));

if ($this->confirm('Would you like to create a new configuration file?')) {
$this->createConfigFile();
}
}

return $path;
}

/**
* Determines the expected path for the config file.
*
* For plugins, this will be a "phpcs.xml" file inside the plugin's directory. For the core, this will be a
* "phpcs.xml" file in the root directory of the project.
*/
protected function expectedConfigPath(): ?string
{
if ($this->option('config')) {
return $this->option('config');
}

if ($this->option('plugin')) {
$pluginPath = PluginManager::instance()->getPluginPath($this->getPluginIdentifier());

if (is_null($pluginPath)) {
$this->error(sprintf('Plugin %s not found.', $this->getPluginIdentifier()));
}

return $pluginPath . '/phpcs.xml';
}

return base_path('phpcs.xml');
}

/**
* Creates a new configuration file for PHPCS.
*/
protected function createConfigFile(): void
{
if ($this->option('plugin')) {
$this->vars = [
'plugin' => $this->getPluginIdentifier(),
'plugin_code' => str_replace('.', '_', $this->getPluginIdentifier()),
];
$this->makeStub('scaffold/phpcs/phpcs.xml.stub');
return;
}

File::copy(
__DIR__ . '/scaffold/phpcs/phpcs.core.xml.stub',
$this->option('config') ?? base_path('phpcs.xml')
);
}

protected function getDestinationForStub(string $stubName): string
{
if ($this->option('config')) {
return $this->option('config');
}

return parent::getDestinationForStub($stubName);
}

public function getPluginIdentifier($identifier = null): string
{
$pluginManager = PluginManager::instance();
$pluginName = $identifier ?? $this->option('plugin');
$pluginName = $pluginManager->normalizeIdentifier($pluginName);

if (!$pluginManager->hasPlugin($pluginName)) {
$this->error(sprintf('Plugin %s not found.', $this->getPluginIdentifier()));
}

return $pluginName;
}

/**
* Finds the `phpcs` executable in the system.
*
* This should be in the `vendor/bin` directory of the project, if the project has developer dependencies installed
* through Composer.
*/
protected function findPhpCsExecutable(): ?string
{
return (new ExecutableFinder())
->find(
'phpcs',
base_path('vendor/bin/phpcs'),
[
base_path('vendor'),
]
);
}

/**
* Gets the base path where files and directories will be checked.
*
* If the `--plugin` option is provided, this will return the path to the plugin. Otherwise, it will return the base
* path of the project.
*
* @return string
*/
protected function getBasePath(): string
{
if ($this->option('plugin')) {
$pluginPath = PluginManager::instance()->getPluginPath($this->getPluginIdentifier());

if (is_null($pluginPath)) {
$this->error(sprintf('Plugin %s not found.', $this->getPluginIdentifier()));
}

return $pluginPath;
}

return base_path();
}

/**
* Builds the arguments to pass to the `phpcs` executable.
*
* @return string[]
*/
protected function buildPhpCsArgs(string $phpCs, string $configFile): array
{
$args = [
$phpCs,
'-d',
'memory_limit=512M',
'--basepath=' . $this->getBasePath(),
'--standard=' . $configFile,
];

if ($this->option('no-warnings')) {
$args[] = '--warning-severity=0';
}

if ($this->option('summary')) {
$args[] = '--report=summary';
}

$args = array_merge(
$args,
$this->argument('paths') ?: (
($this->option('plugin'))
? [PluginManager::instance()->getPluginPath($this->getPluginIdentifier())]
: []
)
);

return $args;
}
}
24 changes: 24 additions & 0 deletions modules/system/console/scaffold/phpcs/phpcs.core.xml.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<ruleset name="WinterCMSCore">
<description>The coding standard for Winter CMS core.</description>

<!-- Import Winter CMS code style ruleset -->
<rule ref="./phpcs.base.xml" />

<arg name="extensions" value="php" />
<arg name="colors" />

<file>bootstrap/</file>
<file>config/</file>
<file>modules/</file>
<file>plugins/winter/demo/</file>

<!-- Ignore vendor files -->
<exclude-pattern>*/vendor/*</exclude-pattern>
<!-- Ignore manifest test fixtures -->
<exclude-pattern>modules/system/tests/fixtures/manifest/*</exclude-pattern>
<!-- Ignore this view file as fixing the issues in here will break the template -->
<exclude-pattern>modules/system/views/exception.php</exclude-pattern>
<!-- Ignore this test case completely as it's testing a parse error -->
<exclude-pattern>tests/fixtures/plugins/testvendor/goto/Plugin.php</exclude-pattern>
</ruleset>
10 changes: 10 additions & 0 deletions modules/system/console/scaffold/phpcs/phpcs.xml.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<ruleset name="{{ plugin_code }}">
<description>Coding style for {{ plugin }}.</description>

<!-- Import Winter CMS code style ruleset -->
<rule ref="./phpcs.base.xml" />

<arg name="extensions" value="php" />
<arg name="colors" />
</ruleset>
Loading