Skip to content
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
2 changes: 1 addition & 1 deletion blink/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ with `bin/rule-counts.sh`; CI keeps it current.
| tool | rules | coverage |
|---|---:|---:|
| fixer universe (PHP-CS-Fixer + Symplify + in-set sniffs) | 338 | 100% |
| blink | 268 | 78% |
| blink | 288 | 84% |
<!-- rule-counts:end -->

## Build
Expand Down
46 changes: 46 additions & 0 deletions blink/internal/fixer/rules/phpunit_assert_new_names.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package rules

import (
"github.com/ecsphp/ecs-src/blink/internal/tokens"
)

// PHP-CS-Fixer: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/PhpUnit/PhpUnitAssertNewNamesFixer.php
//
// PhpUnitAssertNewNames renames deprecated PHPUnit assertions to their new
// names (assertFileNotExists -> assertFileDoesNotExist, ...) inside test
// classes. Risky: assumes the methods are PHPUnit's own.
type PhpUnitAssertNewNames struct{}

var phpUnitAssertNewNamesMap = map[string]string{
"assertnotisreadable": "assertIsNotReadable",
"assertnotiswritable": "assertIsNotWritable",
"assertdirectorynotexists": "assertDirectoryDoesNotExist",
"assertfilenotexists": "assertFileDoesNotExist",
"assertdirectorynotisreadable": "assertDirectoryIsNotReadable",
"assertdirectorynotiswritable": "assertDirectoryIsNotWriteable",
"assertfilenotisreadable": "assertFileIsNotReadable",
"assertfilenotiswritable": "assertFileIsNotWriteable",
"assertregexp": "assertMatchesRegularExpression",
"assertnotregexp": "assertDoesNotMatchRegularExpression",
}

func (PhpUnitAssertNewNames) Name() string {
return `PhpCsFixer\Fixer\PhpUnit\PhpUnitAssertNewNamesFixer`
}

func (PhpUnitAssertNewNames) SourceURL() string {
return "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/PhpUnit/PhpUnitAssertNewNamesFixer.php"
}

func (PhpUnitAssertNewNames) Fix(s *tokens.Stream) bool {
changed := false
for _, cls := range phpUnitTestClasses(s) {
for _, call := range puPreviousAssertCalls(s, cls[0], cls[1]) {
if replacement, ok := phpUnitAssertNewNamesMap[call.lowered]; ok {
s.SetValue(call.index, replacement)
changed = true
}
}
}
return changed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package rules

import "testing"

// Cases checked against PHP-CS-Fixer 3.95 (php_unit_assert_new_names).
func TestPhpUnitAssertNewNamesPhpCsFixerCases(t *testing.T) {
t.Parallel()
aliasRunUpstreamCases(t, PhpUnitAssertNewNames{}, []aliasUpstreamCase{
{
"all renames", nil,
"<?php\nfinal class MyTest extends \\PHPUnit\\Framework\\TestCase {\n public function t() {\n $this->assertFileNotExists(\"a\");\n $this->assertNotIsWritable(\"b\");\n $this->assertRegExp(\"/x/\", $s);\n $this->assertNotRegExp(\"/x/\", $s);\n $this->assertDirectoryNotExists(\"d\");\n $this->assertSame(1, 2);\n }\n}\n",
"<?php\nfinal class MyTest extends \\PHPUnit\\Framework\\TestCase {\n public function t() {\n $this->assertFileDoesNotExist(\"a\");\n $this->assertIsNotWritable(\"b\");\n $this->assertMatchesRegularExpression(\"/x/\", $s);\n $this->assertDoesNotMatchRegularExpression(\"/x/\", $s);\n $this->assertDirectoryDoesNotExist(\"d\");\n $this->assertSame(1, 2);\n }\n}\n",
},
{
"self and static receivers", nil,
"<?php\nfinal class MyTest extends \\PHPUnit\\Framework\\TestCase {\n public function t() {\n self::assertFileNotExists(\"a\");\n static::assertRegExp(\"/x/\", $s);\n $obj->assertFileNotExists(\"c\");\n }\n}\n",
"<?php\nfinal class MyTest extends \\PHPUnit\\Framework\\TestCase {\n public function t() {\n self::assertFileDoesNotExist(\"a\");\n static::assertMatchesRegularExpression(\"/x/\", $s);\n $obj->assertFileNotExists(\"c\");\n }\n}\n",
},
{
"not a PHPUnit class is untouched", nil,
"<?php\nclass Foo {\n public function t() {\n $this->assertFileNotExists(\"a\");\n }\n}\n",
"<?php\nclass Foo {\n public function t() {\n $this->assertFileNotExists(\"a\");\n }\n}\n",
},
})
}
122 changes: 122 additions & 0 deletions blink/internal/fixer/rules/phpunit_dedicate_assert_internal_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package rules

import (
"strings"

"github.com/ecsphp/ecs-src/blink/internal/fixer"
"github.com/ecsphp/ecs-src/blink/internal/token"
"github.com/ecsphp/ecs-src/blink/internal/tokens"
)

// PHP-CS-Fixer: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/PhpUnit/PhpUnitDedicateAssertInternalTypeFixer.php
//
// PhpUnitDedicateAssertInternalType replaces assertInternalType("array", $x)
// with the dedicated assertIsArray($x) and assertNotInternalType with the
// negated form, inside test classes. The target option does not change the
// mapping. Risky: assumes PHPUnit's own methods.
type PhpUnitDedicateAssertInternalType struct{}

var phpUnitInternalTypeMap = map[string]string{
"array": "assertIsArray",
"boolean": "assertIsBool",
"bool": "assertIsBool",
"double": "assertIsFloat",
"float": "assertIsFloat",
"integer": "assertIsInt",
"int": "assertIsInt",
"null": "assertNull",
"numeric": "assertIsNumeric",
"object": "assertIsObject",
"real": "assertIsFloat",
"resource": "assertIsResource",
"string": "assertIsString",
"scalar": "assertIsScalar",
"callable": "assertIsCallable",
"iterable": "assertIsIterable",
}

func (PhpUnitDedicateAssertInternalType) Name() string {
return `PhpCsFixer\Fixer\PhpUnit\PhpUnitDedicateAssertInternalTypeFixer`
}

func (PhpUnitDedicateAssertInternalType) SourceURL() string {
return "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/PhpUnit/PhpUnitDedicateAssertInternalTypeFixer.php"
}

// WithConfig accepts the target option, which does not affect behavior.
func (f PhpUnitDedicateAssertInternalType) WithConfig(map[string]any) fixer.Fixer {
return f
}

func (PhpUnitDedicateAssertInternalType) Fix(s *tokens.Stream) bool {
changed := false
for _, cls := range puClassesLastFirst(s) {
if dedicateInternalTypeClass(s, cls[0], cls[1]) {
changed = true
}
}
return changed
}

func dedicateInternalTypeClass(s *tokens.Stream, startIndex, endIndex int) bool {
anon := map[int]int{}
for i := startIndex; i < endIndex; i++ {
if !kwIs(s.At(i), "class") || !taIsAnonymousClass(s, i) {
continue
}
open := nextPunctOfKind(s, i, "{")
if open < 0 {
continue
}
if end := s.MatchForward(open); end > open {
anon[end] = open
}
}

changed := false
for index := endIndex - 1; index > startIndex; index-- {
if open, ok := anon[index]; ok {
index = open
continue
}
t := s.At(index)
if t.Kind != token.Ident {
continue
}
fn := strings.ToLower(t.Value)
if fn != "assertinternaltype" && fn != "assertnotinternaltype" {
continue
}
bracket := sigNext(s, index)
if !isPunctVal(s, bracket, "(") {
continue
}
typeIdx := sigNext(s, bracket)
if typeIdx < 0 || !s.At(typeIdx).IsLiteral() {
continue
}
expectedType := strings.Trim(s.At(typeIdx).Value, "'\"")
newAssertion, ok := phpUnitInternalTypeMap[expectedType]
if !ok {
continue
}
comma := sigNext(s, typeIdx)
if !isPunctVal(s, comma, ",") {
continue
}
if fn == "assertnotinternaltype" {
newAssertion = strings.ReplaceAll(newAssertion, "Is", "IsNot")
newAssertion = strings.ReplaceAll(newAssertion, "Null", "NotNull")
}
next := sigNext(s, comma)
if next < 0 {
continue
}
s.ReplaceRange(index, next-1, []token.Token{
{Kind: token.Ident, Value: newAssertion},
{Kind: token.Punct, Value: "("},
})
changed = true
}
return changed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package rules

import "testing"

// Cases checked against PHP-CS-Fixer 3.95 (php_unit_dedicate_assert_internal_type).
func TestPhpUnitDedicateAssertInternalTypePhpCsFixerCases(t *testing.T) {
t.Parallel()
aliasRunUpstreamCases(t, PhpUnitDedicateAssertInternalType{}, []aliasUpstreamCase{
{
"mapped types, negation, unknown and malformed left", nil,
"<?php\nfinal class MyTest extends \\PHPUnit\\Framework\\TestCase {\n public function testMe() {\n $this->assertInternalType(\"array\", $var);\n $this->assertNotInternalType(\"array\", $var);\n $this->assertInternalType('float', $var);\n $this->assertInternalType(\"unknownType\", $var);\n $obj->assertInternalType(\"array\", $var);\n $this->assertInternalType(\"array\");\n $this->assertInternalType($var, $x);\n }\n}\n",
"<?php\nfinal class MyTest extends \\PHPUnit\\Framework\\TestCase {\n public function testMe() {\n $this->assertIsArray($var);\n $this->assertIsNotArray($var);\n $this->assertIsFloat($var);\n $this->assertInternalType(\"unknownType\", $var);\n $obj->assertIsArray($var);\n $this->assertInternalType(\"array\");\n $this->assertInternalType($var, $x);\n }\n}\n",
},
{
"null maps to assertNull / assertNotNull", nil,
"<?php\nfinal class MyTest extends \\PHPUnit\\Framework\\TestCase {\n public function testMe() {\n $this->assertInternalType(\"null\", $var);\n $this->assertNotInternalType(\"null\", $var);\n }\n}\n",
"<?php\nfinal class MyTest extends \\PHPUnit\\Framework\\TestCase {\n public function testMe() {\n $this->assertNull($var);\n $this->assertNotNull($var);\n }\n}\n",
},
{
"not a PHPUnit class is untouched", nil,
"<?php\nclass Foo {\n public function t() {\n $this->assertInternalType(\"array\", $var);\n }\n}\n",
"<?php\nclass Foo {\n public function t() {\n $this->assertInternalType(\"array\", $var);\n }\n}\n",
},
})
}
74 changes: 74 additions & 0 deletions blink/internal/fixer/rules/phpunit_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package rules

import (
"strings"

"github.com/ecsphp/ecs-src/blink/internal/fixer"
"github.com/ecsphp/ecs-src/blink/internal/token"
"github.com/ecsphp/ecs-src/blink/internal/tokens"
)

// PHP-CS-Fixer: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/PhpUnit/PhpUnitMockFixer.php
//
// PhpUnitMock replaces ->getMock and ->getMockWithoutInvokingTheOriginalConstructor
// with ->createMock / ->createPartialMock inside test classes. The default
// target (newest) also rewrites a two-argument getMock to createPartialMock.
// Risky: assumes PHPUnit's own methods.
type PhpUnitMock struct {
target string
}

func (PhpUnitMock) Name() string {
return `PhpCsFixer\Fixer\PhpUnit\PhpUnitMockFixer`
}

func (PhpUnitMock) SourceURL() string {
return "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/PhpUnit/PhpUnitMockFixer.php"
}

func (f PhpUnitMock) WithConfig(config map[string]any) fixer.Fixer {
if v, ok := config["target"].(string); ok {
f.target = v
}
return f
}

func (f PhpUnitMock) Fix(s *tokens.Stream) bool {
fixCreatePartial := phpUnitTargetFulfills(f.target, "5.5")
changed := false
for _, cls := range phpUnitTestClasses(s) {
start, end := cls[0], cls[1]
for index := start; index < end && index < s.Len(); index++ {
if !puIsObjectOperator(s, index) {
continue
}
index = sigNext(s, index)
if index < 0 {
break
}
if t := s.At(index); t.Kind != token.Ident {
continue
}
name := s.At(index).Value
switch {
case strings.EqualFold(name, "getMockWithoutInvokingTheOriginalConstructor"):
s.SetValue(index, "createMock")
changed = true
case strings.EqualFold(name, "getMock"):
open := sigNext(s, index)
if !isPunctVal(s, open, "(") {
continue
}
n := len(aliasArguments(s, open, s.MatchForward(open)))
if n == 1 {
s.SetValue(index, "createMock")
changed = true
} else if n == 2 && fixCreatePartial {
s.SetValue(index, "createPartialMock")
changed = true
}
}
}
}
return changed
}
25 changes: 25 additions & 0 deletions blink/internal/fixer/rules/phpunit_mock_upstream_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package rules

import "testing"

// Cases checked against PHP-CS-Fixer 3.95 (php_unit_mock).
func TestPhpUnitMockPhpCsFixerCases(t *testing.T) {
t.Parallel()
aliasRunUpstreamCases(t, PhpUnitMock{}, []aliasUpstreamCase{
{
"newest target", nil,
"<?php\nfinal class MyTest extends \\PHPUnit_Framework_TestCase {\n public function testFoo() {\n $a = $this->getMockWithoutInvokingTheOriginalConstructor(\"Foo\");\n $b = $this->getMock(\"Foo\");\n $c = $this->getMock(\"Bar\", [\"x\"]);\n $d = $this->getMock(\"Baz\", [\"x\"], [\"y\"]);\n }\n}\n",
"<?php\nfinal class MyTest extends \\PHPUnit_Framework_TestCase {\n public function testFoo() {\n $a = $this->createMock(\"Foo\");\n $b = $this->createMock(\"Foo\");\n $c = $this->createPartialMock(\"Bar\", [\"x\"]);\n $d = $this->getMock(\"Baz\", [\"x\"], [\"y\"]);\n }\n}\n",
},
{
"target 5.4 keeps two-arg getMock", map[string]any{"target": "5.4"},
"<?php\nfinal class MyTest extends \\PHPUnit_Framework_TestCase {\n public function testFoo() {\n $b = $this->getMock(\"Foo\");\n $c = $this->getMock(\"Bar\", [\"x\"]);\n }\n}\n",
"<?php\nfinal class MyTest extends \\PHPUnit_Framework_TestCase {\n public function testFoo() {\n $b = $this->createMock(\"Foo\");\n $c = $this->getMock(\"Bar\", [\"x\"]);\n }\n}\n",
},
{
"not a PHPUnit class is untouched", nil,
"<?php\nclass Foo {\n public function t() {\n $b = $this->getMock(\"Foo\");\n }\n}\n",
"<?php\nclass Foo {\n public function t() {\n $b = $this->getMock(\"Foo\");\n }\n}\n",
},
})
}
Loading
Loading