Skip to content
Open
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
45 changes: 45 additions & 0 deletions .github/workflows/rector.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
name: Rector

on:
pull_request:

permissions:
contents: read

concurrency:
group: rector-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
strict:
runs-on: ubuntu-latest

if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}

steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
submodules: true

- name: Set up php
uses: shivammathur/setup-php@ec406be512d7077f68eed36e63f4d91bc006edc4 #v2.35.4
with:
php-version: '8.2'
extensions: apcu,ctype,curl,dom,fileinfo,ftp,gd,imagick,intl,json,ldap,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip
coverage: none
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Composer install
run: composer i

- name: Rector
run: composer run rector:strict

- name: Show changes
if: always()
run: git diff --exit-code -- . ':!lib/composer'
27 changes: 27 additions & 0 deletions .github/workflows/static-code-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,30 @@ jobs:

- name: Psalm
run: composer run psalm:ncu -- --threads=1 --monochrome --no-progress --output-format=github

static-code-analysis-strict:
runs-on: ubuntu-latest

if: ${{ github.event_name != 'push' && github.repository_owner != 'nextcloud-gmbh' }}

steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
with:
persist-credentials: false
submodules: true

- name: Set up php
uses: shivammathur/setup-php@ec406be512d7077f68eed36e63f4d91bc006edc4 #v2.35.4
with:
php-version: '8.2'
extensions: ctype,curl,dom,fileinfo,gd,imagick,intl,json,mbstring,openssl,pdo_sqlite,posix,sqlite,xml,zip
coverage: none
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Composer install
run: composer i

- name: Psalm
run: composer run psalm:strict -- --threads=1 --monochrome --no-progress --output-format=github
1 change: 1 addition & 0 deletions build/files-checker.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
'package.json',
'psalm-ncu.xml',
'psalm-ocp.xml',
'psalm-strict.xml',
'psalm.xml',
'public.php',
'remote.php',
Expand Down
92 changes: 92 additions & 0 deletions build/rector-shared.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

use Nextcloud\Rector\Set\NextcloudSets;
use PhpParser\Node;
use Rector\CodingStyle\Contract\ClassNameImport\ClassNameImportSkipVoterInterface;
use Rector\Config\RectorConfig;
use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Rector\ValueObject\Application\File;

$nextcloudDir = dirname(__DIR__);

class NextcloudNamespaceSkipVoter implements ClassNameImportSkipVoterInterface {
private array $namespacePrefixes = [
'OC',
'OCA',
'OCP',
];
private array $skippedClassNames = [
'Backend',
'Connection',
'Exception',
'IManager',
'IProvider',
'Manager',
'Plugin',
'Provider',
];
public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType, Node $node) : bool {
if (in_array($fullyQualifiedObjectType->getShortName(), $this->skippedClassNames)) {
// Skip common class names to avoid confusion
return true;
}
foreach ($this->namespacePrefixes as $prefix) {
if (str_starts_with($fullyQualifiedObjectType->getClassName(), $prefix . '\\')) {
// Import Nextcloud namespaces
return false;
}
}
// Skip everything else
return true;
}
}

$config = RectorConfig::configure()
->withSkip([
$nextcloudDir . '/apps/*/3rdparty/*',
$nextcloudDir . '/apps/*/build/stubs/*',
$nextcloudDir . '/apps/*/composer/*',
$nextcloudDir . '/apps/*/config/*',
// The mock classes are excluded, as the tests explicitly test the annotations which should not be migrated to attributes
$nextcloudDir . '/tests/lib/AppFramework/Middleware/Mock/*',
$nextcloudDir . '/tests/lib/AppFramework/Middleware/Security/Mock/*',
])
// uncomment to reach your current PHP version
// ->withPhpSets()
->withImportNames(importShortClasses:false)
->withConfiguredRule(ClassPropertyAssignToConstructorPromotionRector::class, [
'inline_public' => true,
'rename_property' => true,
])
->withSets([
NextcloudSets::NEXTCLOUD_27,
PHPUnitSetList::PHPUNIT_100,
]);

$config->registerService(NextcloudNamespaceSkipVoter::class, tag:ClassNameImportSkipVoterInterface::class);

/* Ignore all files ignored by git */
$ignoredEntries = shell_exec('git status --porcelain --ignored ' . escapeshellarg($nextcloudDir));
$ignoredEntries = explode("\n", $ignoredEntries);
$ignoredEntries = array_filter($ignoredEntries, static fn (string $line) => str_starts_with($line, '!! '));
$ignoredEntries = array_map(static fn (string $line) => substr($line, 3), $ignoredEntries);
$ignoredEntries = array_values($ignoredEntries);

foreach ($ignoredEntries as $ignoredEntry) {
if (str_ends_with($ignoredEntry, '/')) {
$config->withSkip([$ignoredEntry . '*']);
} else {
$config->withSkip([$ignoredEntry . '/*']);
}
}

return $config;
30 changes: 30 additions & 0 deletions build/rector-strict.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

$nextcloudDir = dirname(__DIR__);

return (require __DIR__ . '/rector-shared.php')
->withPaths([
$nextcloudDir . '/build/rector-strict.php',
])
->withPreparedSets(
deadCode: true,
codeQuality: true,
codingStyle: true,
typeDeclarations: true,
typeDeclarationDocblocks: true,
privatization: true,
instanceOf: true,
earlyReturn: true,
rectorPreset: true,
phpunitCodeQuality: true,
doctrineCodeQuality: true,
symfonyCodeQuality: true,
symfonyConfigs: true,
Comment on lines +15 to +27
Copy link
Contributor

Choose a reason for hiding this comment

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

I’m uneasy about using rector for code style like this because it has no ignore mechanism so we’re screwed on any false-positive, no?

Also, this PR adds rector-strict.php but only runs it on itself?

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, this PR adds rector-strict.php but only runs it on itself?

Yes, because rector will complain if you run it without any files, so it's just meant as a placeholder.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah! Can you explain that in a comment? 😆

)->withPhpSets(
php82: true,
);
84 changes: 2 additions & 82 deletions build/rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

use Nextcloud\Rector\Set\NextcloudSets;
use PhpParser\Node;
use Rector\CodingStyle\Contract\ClassNameImport\ClassNameImportSkipVoterInterface;
use Rector\Config\RectorConfig;
use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use Rector\ValueObject\Application\File;

$nextcloudDir = dirname(__DIR__);

class NextcloudNamespaceSkipVoter implements ClassNameImportSkipVoterInterface {
private array $namespacePrefixes = [
'OC',
'OCA',
'OCP',
];
private array $skippedClassNames = [
'Backend',
'Connection',
'Exception',
'IManager',
'IProvider',
'Manager',
'Plugin',
'Provider',
];
public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType, Node $node) : bool {
if (in_array($fullyQualifiedObjectType->getShortName(), $this->skippedClassNames)) {
// Skip common class names to avoid confusion
return true;
}
foreach ($this->namespacePrefixes as $prefix) {
if (str_starts_with($fullyQualifiedObjectType->getClassName(), $prefix . '\\')) {
// Import Nextcloud namespaces
return false;
}
}
// Skip everything else
return true;
}
}

$config = RectorConfig::configure()
return (require 'rector-shared.php')
->withPaths([
$nextcloudDir . '/apps',
$nextcloudDir . '/core',
Expand All @@ -71,43 +30,4 @@ public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedO
// $nextcloudDir . '/lib',
// $nextcloudDir . '/themes',
])
->withSkip([
$nextcloudDir . '/apps/*/3rdparty/*',
$nextcloudDir . '/apps/*/build/stubs/*',
$nextcloudDir . '/apps/*/composer/*',
$nextcloudDir . '/apps/*/config/*',
// The mock classes are excluded, as the tests explicitly test the annotations which should not be migrated to attributes
$nextcloudDir . '/tests/lib/AppFramework/Middleware/Mock/*',
$nextcloudDir . '/tests/lib/AppFramework/Middleware/Security/Mock/*',
])
// uncomment to reach your current PHP version
// ->withPhpSets()
->withImportNames(importShortClasses:false)
->withTypeCoverageLevel(0)
->withConfiguredRule(ClassPropertyAssignToConstructorPromotionRector::class, [
'inline_public' => true,
'rename_property' => true,
])
->withSets([
NextcloudSets::NEXTCLOUD_27,
PHPUnitSetList::PHPUNIT_100,
]);

$config->registerService(NextcloudNamespaceSkipVoter::class, tag:ClassNameImportSkipVoterInterface::class);

/* Ignore all files ignored by git */
$ignoredEntries = shell_exec('git status --porcelain --ignored ' . escapeshellarg($nextcloudDir));
$ignoredEntries = explode("\n", $ignoredEntries);
$ignoredEntries = array_filter($ignoredEntries, static fn (string $line) => str_starts_with($line, '!! '));
$ignoredEntries = array_map(static fn (string $line) => substr($line, 3), $ignoredEntries);
$ignoredEntries = array_values($ignoredEntries);

foreach ($ignoredEntries as $ignoredEntry) {
if (str_ends_with($ignoredEntry, '/')) {
$config->withSkip([$ignoredEntry . '*']);
} else {
$config->withSkip([$ignoredEntry . '/*']);
}
}

return $config;
->withTypeCoverageLevel(0);
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"psalm": "psalm --no-cache --threads=$(nproc)",
"psalm:ocp": "psalm --no-cache --threads=$(nproc) -c psalm-ocp.xml",
"psalm:ncu": "psalm --no-cache --threads=$(nproc) -c psalm-ncu.xml",
"psalm:strict": "psalm --no-cache --threads=$(nproc) -c psalm-strict.xml",
"psalm:security": "psalm --no-cache --threads=$(nproc) --taint-analysis --use-baseline=build/psalm-baseline-security.xml",
"psalm:update-baseline": "psalm --no-cache --threads=$(nproc) --update-baseline",
"serve": [
Expand All @@ -75,6 +76,7 @@
"test:db": "@composer run test -- --group DB --group SLOWDB",
"test:files_external": "phpunit --fail-on-warning --fail-on-risky --display-warnings --display-deprecations --display-phpunit-deprecations --colors=always --configuration tests/phpunit-autotest-external.xml",
"rector": "rector --config=build/rector.php && composer cs:fix",
"rector:strict": "rector --config=build/rector-strict.php && composer cs:fix",
"openapi": "./build/openapi-checker.sh"
},
"extra": {
Expand Down
30 changes: 30 additions & 0 deletions psalm-strict.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<psalm
errorLevel="1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config"
resolveFromConfigFile="false"
findUnusedBaselineEntry="true"
findUnusedCode="false"
findUnusedPsalmSuppress="true"
findUnusedVariablesAndParams="true"
phpVersion="8.2"
>
<projectFiles>
<ignoreFiles>
<directory name="apps/**/composer"/>
<directory name="apps/**/tests"/>
<directory name="lib/composer"/>
<directory name="lib/l10n"/>
<directory name="3rdparty"/>
</ignoreFiles>
</projectFiles>
<extraFiles>
<directory name="3rdparty"/>
</extraFiles>
</psalm>
Loading