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
6 changes: 5 additions & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ parameters:
paths:
- tests/PHPStan/data/

# Test fixtures intentionally contain Lab ID capitalization violations
- message: '#should use "Lab ID" instead of#'
paths:
- tests/PHPStan/data/

# PHPStan internal API usage is acceptable in tests
- message: '#is not covered by backward compatibility promise#'
paths:
- tests/PHPStan/

94 changes: 94 additions & 0 deletions src/PHPStan/Rules/CanonicalCapitalizationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php declare(strict_types=1);

namespace MLL\Utils\PHPStan\Rules;

use Illuminate\Support\Str;
use PhpParser\Node;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* Only checks string literals because canonical forms like "Lab ID" contain spaces,
* which are invalid in PHP identifiers (variables, methods, classes).
*
* @implements Rule<String_>
*/
abstract class CanonicalCapitalizationRule implements Rule
{
abstract protected function getCanonicalForm(): string;
Comment on lines +18 to +20
Copy link
Member

Choose a reason for hiding this comment

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

Ich finde es nicht korrekt hier von Capitalization zu sprechen. Würde auch die Überschrift der Guideline abändern, wie wäre es mit Canonical ...:

  • forms
  • spelling
  • ?


/** @return array<int, string> */
abstract protected function getWrongVariants(): array;

abstract protected function getErrorIdentifier(): string;

public function getNodeType(): string
{
return String_::class;
Copy link
Member

Choose a reason for hiding this comment

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

Fangen wir damit auch alle möglichen Varianten in PHP Strings zu definieren?

}

/** @param String_ $node */
public function processNode(Node $node, Scope $scope): array
{
if ($this->hasLanguageAnnotation($node)) {
return [];
}

$value = $node->value;

if ($this->cannotContainSpaces($value)) {
return [];
}

$wrongVariant = $this->findWrongVariant($value);

if ($wrongVariant === null) {
return [];
}

$expectedValue = $this->fixCapitalization($value, $wrongVariant);

return [
RuleErrorBuilder::message(<<<TXT
String "{$value}" should use "{$this->getCanonicalForm()}" instead of "{$wrongVariant}", change it to "{$expectedValue}".
TXT)
->identifier($this->getErrorIdentifier())
->build(),
];
}

protected function cannotContainSpaces(string $value): bool
{
return ! str_contains($value, ' ');
Comment on lines +62 to +64
Copy link
Member

Choose a reason for hiding this comment

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

Diese Heuristik kann schon wild inakkurat sein. Wenn jemand fälschlicherweise Limes2 schreibt wo auch Limes 2 stehen könnte, würde es das nicht fangen.

}

/** @see https://www.jetbrains.com/help/phpstorm/using-language-injections.html */
protected function hasLanguageAnnotation(String_ $node): bool
{
foreach ($node->getComments() as $comment) {
if (Str::contains($comment->getText(), '@lang')) {
Copy link
Member

Choose a reason for hiding this comment

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

Eventuell können wir das hier einschränken auf gewisse Languages. In @lang Markdown wären alle Formen ok.

return true;
}
}

return false;
}

public function findWrongVariant(string $value): ?string
{
foreach ($this->getWrongVariants() as $wrongVariant) {
if (Str::contains($value, $wrongVariant)) {
return $wrongVariant;
}
}

return null;
}

public function fixCapitalization(string $value, string $wrongVariant): string
{
return str_replace($wrongVariant, $this->getCanonicalForm(), $value);
Comment on lines +90 to +92
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public function fixCapitalization(string $value, string $wrongVariant): string
{
return str_replace($wrongVariant, $this->getCanonicalForm(), $value);
public function fixCapitalization(string $subject, string $wrongVariant): string
{
return str_replace($wrongVariant, $this->getCanonicalForm(), $subject);

}
}
1 change: 0 additions & 1 deletion src/PHPStan/Rules/IdToIDRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ abstract protected function getErrorIdentifier(): string;

abstract protected function extractName(Node $node): ?string;

/** Override for custom formatting (e.g., adding $ prefix for variables). */
protected function formatNameForMessage(string $name): string
{
return $name;
Expand Down
32 changes: 32 additions & 0 deletions src/PHPStan/Rules/LabIDCapitalizationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);

namespace MLL\Utils\PHPStan\Rules;

final class LabIDCapitalizationRule extends CanonicalCapitalizationRule
{
protected function getCanonicalForm(): string
{
return 'Lab ID';
}

/** @return array<int, string> */
protected function getWrongVariants(): array
{
return [
'LabID',
'labID',
'LABID',
'Labid',
'labid',
'Lab-ID',
'lab-Id',
'Lab Id',
'lab id',
];
}

protected function getErrorIdentifier(): string
{
return 'mll.labIDCapitalization';
}
}
99 changes: 0 additions & 99 deletions tests/PHPStan/CapitalizationOfIDRuleIntegrationTest.php

This file was deleted.

54 changes: 54 additions & 0 deletions tests/PHPStan/IdToIDRuleIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types=1);

namespace MLL\Utils\Tests\PHPStan;

use PHPUnit\Framework\Attributes\DataProvider;

final class IdToIDRuleIntegrationTest extends PHPStanIntegrationTestCase
{
private const ERROR_PATTERN = 'should use "ID" instead of "Id"';

/** @return iterable<array{0: string, 1: array<int, array<int, string>>}> */
public static function dataIntegrationTests(): iterable
{
self::getContainer();

yield 'wrong capitalization' => [__DIR__ . '/data/wrong-capitalization.php', [
5 => [
'Name of Stmt_Class "LabIdProcessor" should use "ID" instead of "Id", rename it to "LabIDProcessor".',
],
7 => [
'Name of Stmt_ClassMethod "getLabId" should use "ID" instead of "Id", rename it to "getLabID".',
],
12 => [
'Name of Stmt_ClassMethod "processLabId" should use "ID" instead of "Id", rename it to "processLabID".',
'Name of Param "$labId" should use "ID" instead of "Id", rename it to "$labID".',
],
14 => [
'Name of Expr_Variable "$sampleId" should use "ID" instead of "Id", rename it to "$sampleID".',
'Name of Expr_Variable "$labId" should use "ID" instead of "Id", rename it to "$labID".',
],
]];

yield 'correct capitalization' => [__DIR__ . '/data/correct-capitalization.php', []];
}

/**
* @param array<int, array<int, string>> $expectedErrors
*
* @dataProvider dataIntegrationTests
*/
#[DataProvider('dataIntegrationTests')]
public function testIntegration(string $file, array $expectedErrors): void
{
$errors = $this->analyseFile($file);
$filteredErrors = $this->filterErrors($errors, self::ERROR_PATTERN);

if ($expectedErrors === []) {
self::assertEmpty($filteredErrors, 'Should not report errors for correct capitalization');
} else {
self::assertNotEmpty($filteredErrors, 'Should detect wrong capitalization');
$this->assertExpectedErrors($expectedErrors, $filteredErrors);
}
}
}
65 changes: 65 additions & 0 deletions tests/PHPStan/LabIDCapitalizationRuleIntegrationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php declare(strict_types=1);

namespace MLL\Utils\Tests\PHPStan;

use PHPUnit\Framework\Attributes\DataProvider;

/**
* @requires PHP >= 8.3
*/
final class LabIDCapitalizationRuleIntegrationTest extends PHPStanIntegrationTestCase
{
private const ERROR_PATTERN = 'should use "Lab ID"';

/** @return iterable<string, array{0: string, 1: array<int, array<int, string>>}> */
public static function dataIntegrationTests(): iterable
{
self::getContainer();

yield 'detects wrong capitalization' => [
__DIR__ . '/data/lab-id-capitalization.php',
[
9 => ['String "The LabID is wrong" should use "Lab ID" instead of "LabID", change it to "The Lab ID is wrong".'],
Copy link
Member

Choose a reason for hiding this comment

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

Hier den ganzen String in der Nachricht zu zeigen kann sehr unübersichtlich werden, wenn es sich um lange Multiline-Werte handelt.

30 => ['String "
{
patient {
labID
}
}
" should use "Lab ID" instead of "labID", change it to "
{
patient {
Lab ID
}
}
".'],
Comment on lines +23 to +35
Copy link
Member

Choose a reason for hiding this comment

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

Negativbeispiel, in dem Fall sollte via @lang GraphQL der Wert ausgenommen werden.

66 => ['String "
SELECT exam_no AS labID
FROM examinations
" should use "Lab ID" instead of "labID", change it to "
SELECT exam_no AS Lab ID
FROM examinations
".'],
Comment on lines +36 to +42
Copy link
Member

Choose a reason for hiding this comment

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

Auch Negativbeispiel, @lang SQL sollte auch hier zur Exklusion führen.

],
];
}

/**
* @param array<int, array<int, string>> $expectedErrors
*
* @dataProvider dataIntegrationTests
*/
#[DataProvider('dataIntegrationTests')]
public function testIntegration(string $file, array $expectedErrors): void
{
$errors = $this->analyseFile($file);
$filteredErrors = $this->filterErrors($errors, self::ERROR_PATTERN);

if ($expectedErrors === []) {
self::assertEmpty($filteredErrors, 'Should not report errors for correct capitalization');
} else {
self::assertNotEmpty($filteredErrors, 'Should detect wrong Lab ID capitalization');
$this->assertExpectedErrors($expectedErrors, $filteredErrors);
}
}
}
Loading