From f31ebf382e74012206dcae4b2cc08578760dc06d Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 22 Feb 2026 04:40:02 +0800 Subject: [PATCH 1/2] chore: test that `@deprecated` items have version numbers --- tests/system/AutoReview/FrameworkCodeTest.php | 91 ++++++++++++++++++- utils/phpstan-baseline/loader.neon | 2 +- .../missingType.iterableValue.neon | 17 +--- 3 files changed, 91 insertions(+), 19 deletions(-) diff --git a/tests/system/AutoReview/FrameworkCodeTest.php b/tests/system/AutoReview/FrameworkCodeTest.php index 137839b024e5..0c8e60b9d61e 100644 --- a/tests/system/AutoReview/FrameworkCodeTest.php +++ b/tests/system/AutoReview/FrameworkCodeTest.php @@ -30,10 +30,22 @@ final class FrameworkCodeTest extends TestCase { /** - * Cache of discovered test class names. + * Cache of source filenames. + * + * @var list + */ + private static array $sourceFiles = []; + + /** + * Cache of test class names. + * + * @var list */ private static array $testClasses = []; + /** + * @var list + */ private static array $recognizedGroupAttributeNames = [ 'AutoReview', 'CacheLive', @@ -42,6 +54,42 @@ final class FrameworkCodeTest extends TestCase 'SeparateProcess', ]; + public function testDeprecationsAreProperlyVersioned(): void + { + $deprecationsWithoutVersion = []; + + foreach ($this->getSourceFiles() as $file) { + $lines = file($file, FILE_IGNORE_NEW_LINES); + + if ($lines === false) { + continue; + } + + foreach ($lines as $number => $line) { + if (! str_contains($line, '@deprecated')) { + continue; + } + + if (preg_match('/((?:\/\*)?\*|\/\/)\s+@deprecated\s+(?P.+?)(?:\s*\*\s*)?$/', $line, $matches) === 1) { + $deprecationText = trim($matches['text']); + + if (preg_match('/^v?\d+\.\d+/', $deprecationText) !== 1) { + $deprecationsWithoutVersion[] = sprintf('%s:%d', $file, ++$number); + } + } + } + } + + $this->assertCount( + 0, + $deprecationsWithoutVersion, + sprintf( + "The following lines contain @deprecated annotations without a version number:\n%s", + implode("\n", array_map(static fn (string $location): string => " * {$location}", $deprecationsWithoutVersion)), + ), + ); + } + /** * @param class-string $class */ @@ -87,6 +135,9 @@ public static function provideEachTestClassHasCorrectGroupAttributeName(): itera } } + /** + * @return list + */ private static function getTestClasses(): array { if (self::$testClasses !== []) { @@ -94,7 +145,6 @@ private static function getTestClasses(): array } helper('filesystem'); - $directory = set_realpath(dirname(__DIR__), true); $iterator = new RecursiveIteratorIterator( @@ -145,4 +195,41 @@ static function (SplFileInfo $file) use ($directory): string { return $testClasses; } + + /** + * @return list + */ + private function getSourceFiles(): array + { + if (self::$sourceFiles !== []) { + return self::$sourceFiles; + } + + helper('filesystem'); + $phpFiles = []; + $basePath = dirname(__DIR__, 3); + + foreach (['system', 'app', 'tests'] as $dir) { + $directory = set_realpath($basePath . DIRECTORY_SEPARATOR . $dir, true); + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $directory, + FilesystemIterator::SKIP_DOTS, + ), + RecursiveIteratorIterator::CHILD_FIRST, + ); + + /** @var SplFileInfo $file */ + foreach ($iterator as $file) { + if ($file->isFile() && str_ends_with($file->getPathname(), '.php')) { + $phpFiles[] = $file->getRealPath(); + } + } + } + + self::$sourceFiles = $phpFiles; + + return $phpFiles; + } } diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index 9aafab3d4e1f..6458419e3c75 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2115 errors +# total 2112 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index a584450fc456..bdd1b14babff 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -1,4 +1,4 @@ -# total 1258 errors +# total 1255 errors parameters: ignoreErrors: @@ -4767,26 +4767,11 @@ parameters: count: 1 path: ../../tests/system/AutoReview/ComposerJsonTest.php - - - message: '#^Method CodeIgniter\\AutoReview\\FrameworkCodeTest\:\:getTestClasses\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/FrameworkCodeTest.php - - message: '#^Method CodeIgniter\\AutoReview\\FrameworkCodeTest\:\:provideEachTestClassHasCorrectGroupAttributeName\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 path: ../../tests/system/AutoReview/FrameworkCodeTest.php - - - message: '#^Property CodeIgniter\\AutoReview\\FrameworkCodeTest\:\:\$recognizedGroupAttributeNames type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/FrameworkCodeTest.php - - - - message: '#^Property CodeIgniter\\AutoReview\\FrameworkCodeTest\:\:\$testClasses type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../tests/system/AutoReview/FrameworkCodeTest.php - - message: '#^Method CodeIgniter\\CLI\\CLITest\:\:provideTable\(\) return type has no value type specified in iterable type iterable\.$#' count: 1 From be83b6df553e27d484a549d42092317682f42eab Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sun, 22 Feb 2026 05:17:46 +0800 Subject: [PATCH 2/2] Add guidance on deprecations --- contributing/internals.md | 25 +++++++++++++++++++++++++ system/Language/en/Images.php | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/contributing/internals.md b/contributing/internals.md index 124d19209ac8..35a8dd4d444a 100644 --- a/contributing/internals.md +++ b/contributing/internals.md @@ -44,6 +44,31 @@ afraid to use it when it's needed and can help things. - Start simple, refactor as necessary to achieve clean separation of code, but don't overdo it. +## Deprecations + +Deprecations happen when code no longer fits its purpose or is superseded by better solutions. +In such cases, it's important that deprecations are documented clearly to guide developers during +their upgrade process. + +- Add `@deprecated` tags in the doc block of deprecated functions, methods, classes, etc. Include + the version number where the deprecation was introduced, a description of why it's deprecated, + and recommended replacement(s), if any. If there are multiple alternative approaches, list all of them. + +- Do not add `@deprecated` tags to a function/method if you only mean to deprecate its parameter(s). + This will cause the whole function/method to be marked as deprecated by IDEs. Instead, trigger + a deprecation inside the function/method body if those parameters are passed. Ensure to include + the version number since when the deprecation occurred. Optionally, but encouraged, add a `@todo` + comment so that it can be found later on. + +- User-facing deprecation warnings should also be triggered via the framework's deprecation handling + mechanism (e.g., `@trigger_error()` or error log entries) to alert end users. + +- Do not add new tests for deprecated code paths. Instead, use tools and static analysis to ensure + that no code within the framework or official packages is using the deprecated functionality. + +- Document all deprecations in the changelog file for that release, under a "Deprecations" + section, so users are informed when upgrading. + ## Testing Any new packages submitted to the framework must be accompanied by unit diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php index 2af9e0a2d1d3..f5dfc9435ea8 100644 --- a/system/Language/en/Images.php +++ b/system/Language/en/Images.php @@ -33,6 +33,6 @@ 'invalidDirection' => 'Flip direction can be only "vertical" or "horizontal". Given: "{0}"', 'exifNotSupported' => 'Reading EXIF data is not supported by this PHP installation.', - // @deprecated + // @deprecated 4.7.0 'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences. "{0}"', ];