Skip to content
This repository has been archived by the owner on Mar 6, 2022. It is now read-only.

Update property generation for typed properties #14

Closed
Closed
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
42 changes: 25 additions & 17 deletions lib/Adapter/TolerantParser/Updater/ClassLikeUpdater.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
namespace Phpactor\CodeBuilder\Adapter\TolerantParser\Updater;

use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\Node\ClassConstDeclaration;
use Microsoft\PhpParser\Node\Expression\AssignmentExpression;
use Microsoft\PhpParser\Node\Expression\Variable;
use Microsoft\PhpParser\Node\MethodDeclaration;
use Microsoft\PhpParser\Node\PropertyDeclaration;
use Microsoft\PhpParser\Node\TraitUseClause;
use Phpactor\CodeBuilder\Adapter\TolerantParser\Edits;
use Phpactor\CodeBuilder\Domain\Prototype\ClassLikePrototype;
use Phpactor\CodeBuilder\Domain\Prototype\Type;
use Phpactor\CodeBuilder\Domain\Renderer;

abstract class ClassLikeUpdater
Expand Down Expand Up @@ -54,41 +55,48 @@ protected function updateProperties(Edits $edits, ClassLikePrototype $classProto
return;
}

$lastProperty = $classMembers->openBrace;
$previousMember = $classMembers->openBrace;
Copy link
Contributor

@dantleech dantleech Dec 8, 2019

Choose a reason for hiding this comment

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

can we add a test(s) for the these fixes?

Copy link
Contributor

Choose a reason for hiding this comment

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

or is this just presentation? if it's not a functional change then the test is not so important.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The name it's just presentation.
But the fact that the blank line before new properties was moved to the template it's functional.
Didn't I update the tests already ?

$memberDeclarations = $this->memberDeclarations($classMembers);

$nextMember = null;
$nextMember = reset($memberDeclarations) ?: null;
$existingPropertyNames = [];

foreach ($memberDeclarations as $memberNode) {
if (null === $nextMember) {
$nextMember = $memberNode;
// Property goes after traits and constants
if ($memberNode instanceof TraitUseClause ||
$memberNode instanceof ClassConstDeclaration
) {
$previousMember = $memberNode;
}

if ($memberNode instanceof PropertyDeclaration) {
foreach ($memberNode->propertyElements->getElements() as $property) {
$existingPropertyNames[] = $this->resolvePropertyName($property);
}
$lastProperty = $memberNode;
$previousMember = $memberNode;
$nextMember = next($memberDeclarations) ?: $nextMember;
prev($memberDeclarations);
}
}

// If the previous member is neither the open brace of class nor a property
// Then we must add a blank line before the new properties
if ($previousMember !== $classMembers->openBrace
&& !($previousMember instanceof PropertyDeclaration)
) {
$edits->after($previousMember, PHP_EOL);
}

foreach ($classPrototype->properties()->notIn($existingPropertyNames) as $property) {
// if property type exists then the last property has a docblock - add a line break
if ($lastProperty instanceof PropertyDeclaration && $property->type() != Type::none()) {
$edits->after($lastProperty, PHP_EOL);
}
$renderedProperty = $this->renderer->render($property);

$edits->after(
$lastProperty,
PHP_EOL . $edits->indent($this->renderer->render($property), 1)
);
$edits->after($previousMember, PHP_EOL . $edits->indent($renderedProperty, 1));
}

if ($classPrototype->properties()->isLast($property) && $nextMember instanceof MethodDeclaration) {
$edits->after($lastProperty, PHP_EOL);
}
if ($previousMember === $classMembers->openBrace &&
$nextMember instanceof MethodDeclaration
) {
$edits->after($previousMember, PHP_EOL);
}
}
}
13 changes: 11 additions & 2 deletions lib/Adapter/WorseReflection/WorseBuilderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ private function buildProperty(ClassLikeBuilder $classBuilder, ReflectionPropert
$type = $property->inferredTypes()->best();
if ($type->isDefined()) {
$this->resolveClassMemberType($classBuilder, $property->class()->name(), $type);
$propertyBuilder->type((string) $type->short());
$imports = $property->scope()->nameImports();
$typeName = $this->resolveTypeNameFromNameImports($type, $imports);
$propertyBuilder->type($typeName);
}
}

Expand All @@ -99,7 +101,9 @@ private function buildMethod(ClassLikeBuilder $classBuilder, ReflectionMethod $m
if ($method->returnType()->isDefined()) {
$type = $method->returnType();
$this->resolveClassMemberType($classBuilder, $method->class()->name(), $type);
$methodBuilder->returnType($type->short());
$imports = $method->scope()->nameImports();
$typeName = $this->resolveTypeNameFromNameImports($type, $imports);
$methodBuilder->returnType($typeName);
}

if ($method->isStatic()) {
Expand Down Expand Up @@ -150,6 +154,11 @@ private function resolveTypeNameFromNameImports(Type $type, NameImports $imports
$typeName = $alias;
}
}

if ($type->isNullable()) {
return "?$typeName";
}

return $typeName;
}
}
4 changes: 4 additions & 0 deletions lib/Util/TextFormat.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public function indent(string $string, int $level = 0)
{
$lines = explode(PHP_EOL, $string);
$lines = array_map(function ($line) use ($level) {
Copy link
Contributor

@dantleech dantleech Dec 8, 2019

Choose a reason for hiding this comment

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

test is missing for this class, but it would be good to add

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

if (empty($line)) {
return $line;
}

return str_repeat($this->indentation, $level) . $line;
}, $lines);

Expand Down
3 changes: 2 additions & 1 deletion templates/Property.php.twig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% if prototype.type.notNone %}

/**
* @var {{ prototype.type }}
* @var {{ prototype.type|trim('?', 'left') }}{{ prototype.type.nullable ? '|null' }}
*/
{% endif %}
{{ prototype.visibility }} ${{ prototype.name}}{% if prototype.defaultValue.notNone %} = {{ prototype.defaultValue.export }}{% endif %};
4 changes: 4 additions & 0 deletions tests/Adapter/GeneratorTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ class Kitten implements Feline, Infant
Type::fromString('PlaneCollection')
),
<<<'EOT'

/**
* @var PlaneCollection
*/
Expand Down Expand Up @@ -487,6 +488,7 @@ public function sleep();

trait Oryctolagus
{

/**
* @var bool
*/
Expand All @@ -499,6 +501,7 @@ public function burrow(Depth $depth = 'deep')

class Rabbits extends Leporidae implements Animal
{

/**
* @var int
*/
Expand Down Expand Up @@ -584,6 +587,7 @@ class Rabbits implements Animal
const LEGS = 4;
const SKIN = 'soft';


Copy link
Contributor

Choose a reason for hiding this comment

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

there should be one blank line between member "types"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's the same issue than above.
I forgot to add tests for the 7.4 version, once done you will see that the problem exists only with the phpdoc.

/**
* @var int
*/
Expand Down
61 changes: 60 additions & 1 deletion tests/Adapter/UpdaterTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Phpactor\CodeBuilder\Domain\Code;
use Phpactor\CodeBuilder\Domain\Builder\SourceCodeBuilder;
use Phpactor\CodeBuilder\Domain\Prototype\SourceCode;
use Phpactor\WorseReflection\Core\Type;

abstract class UpdaterTestCase extends TestCase
{
Expand Down Expand Up @@ -883,7 +884,7 @@ class Aardvark
EOT
];

yield 'It adds a documented properties' => [
yield 'It adds a typed property' => [
<<<'EOT'
class Aardvark
{
Expand All @@ -905,6 +906,33 @@ class Aardvark
*/
public $propertyOne;
}
EOT
];

yield 'It adds a nullable typed property' => [
<<<'EOT'
class Aardvark
{
public $eyes
}
EOT
, SourceCodeBuilder::create()
->class('Aardvark')
->property('propertyOne')->type(
Type::fromString('Hello')->asNullable()
)->end()
->end()
->build(),
<<<'EOT'
class Aardvark
{
public $eyes

/**
* @var Hello|null
*/
public $propertyOne;
}
EOT
];

Expand All @@ -925,6 +953,7 @@ public function crawl()
<<<'EOT'
class Aardvark
{

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm, actually this change is controversial for me and is not the SF coding standard or PSR-12, would rather have no space here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would love to too :)
The thing is that I trade a "bug" for another one.
Before the changes if you had

<?php

class Foo
{
    use SomeTrait;

    public function __construct($a)
    {
    }
}

And you try to complete the constructor, the property would have been added above the trait declaration, which is not PSR-12 compliant neither.

The problem here is that we want to add a blank line only if there is a phpdoc block for the new property.
But the phpdoc part is totally up to the template, so the updater can't know when to add one or not.
And the templates can't know what is the preview member to decide if the blank line should be avoid or not.

Copy link
Contributor

@dantleech dantleech Dec 8, 2019

Choose a reason for hiding this comment

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

hmm, only thing I can suggest is making it as consistent as possible, if we trade one bug for another then let's not add this change.

The problem in general with this library is that it explicitly couples to the presenatation - one solution might be to write a simple CS/whitespace fixer using the Tolerant Parser AST, which can operate on a range of lines (hmm 🤔 )

We could then care less about whitespace concerns when building the source code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's consistent in the way that it's only in two scenario and only if using phpdoc.
Plus it's 100% compliant and bug free for PHP 7.4.
So I would said that the trade is favorable, otherwise we go back with the previous problems in all versions.

And most important it only appends (if I remember correctly), when you have no properties yet.
So it's in a pretty specific and rare case.

The use of the parser would make it clean of course, if we can manage to check the number of black lines between a declaration member and a function member.
I'm not sure how though, will have to check.

Copy link
Contributor

Choose a reason for hiding this comment

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

hmm, ok. if the pros outweigh the cons, the let's go with it.

The user of the parser would ...

I'll try and have a look at this

* @var Hello
*/
Expand Down Expand Up @@ -1092,6 +1121,7 @@ public function crawl()
<<<'EOT'
trait Aardvark
{

/**
* @var Hello
*/
Expand Down Expand Up @@ -1267,6 +1297,35 @@ public function methodOne(Barf $sniff)
{
}
}
EOT
];

yield 'It adds nullable typed parameters' => [
<<<'EOT'
class Aardvark
{
public function methodOne()
{
}
}
EOT
, SourceCodeBuilder::create()
->class('Aardvark')
->method('methodOne')
->parameter('sniff')->type(
Type::fromString('Barf')->asNullable()
)
->end()
->end()
->end()
->build(),
<<<'EOT'
class Aardvark
{
public function methodOne(?Barf $sniff)
{
}
}
EOT
];

Expand Down
29 changes: 29 additions & 0 deletions tests/Adapter/WorseReflection/WorseBuilderFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ public function testClassWithPropertyImportedType()
$this->assertEquals('Bar\Foobar', (string) $source->useStatements()->first());
}

public function testClassWithPropertyImportedNullableType()
{
$source = $this->build('<?php namespace Test; use Bar\Foobar; class Foobar { private ?Foobar $foo = "foobar"; }');
$this->assertEquals(
'?Foobar',
(string) $source->classes()->first()->properties()->first()->type()
);
$this->assertEquals('Bar\Foobar', (string) $source->useStatements()->first());
}

public function testSimpleTrait()
{
$source = $this->build('<?php trait Foobar {}');
Expand Down Expand Up @@ -119,6 +129,19 @@ public function testMethodWithReturnType()
$this->assertEquals('string', $source->classes()->first()->methods()->first()->returnType());
}

public function testMethodWithNullableReturnType()
{
$source = $this->build('<?php class Foobar { public function method(): ?string {} }');
$this->assertEquals('?string', $source->classes()->first()->methods()->first()->returnType());
}

public function testMethodWithImportedNullableReturnType()
{
$source = $this->build('<?php namespace Test; use Bar\Baz; class Foobar { public function method(): ?Baz {} }');
$this->assertEquals('?Baz', $source->classes()->first()->methods()->first()->returnType());
$this->assertEquals('Bar\Baz', (string) $source->useStatements()->first());
}

public function testMethodProtected()
{
$source = $this->build('<?php class Foobar { protected function method() {} }');
Expand All @@ -143,6 +166,12 @@ public function testMethodWithTypedParameter()
$this->assertEquals('string', (string) $source->classes()->first()->methods()->first()->parameters()->first()->type());
}

public function testMethodWithNullableTypedParameter()
{
$source = $this->build('<?php class Foobar { public function method(?string $param) {} }');
$this->assertEquals('?string', (string) $source->classes()->first()->methods()->first()->parameters()->first()->type());
}

public function testMethodWithAliasedParameter()
{
$source = $this->build('<?php use Foobar as Barfoo; class Foobar { public function method(Barfoo $param) {} }');
Expand Down
43 changes: 43 additions & 0 deletions tests/Unit/Util/TextFormatTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Phpactor\CodeBuilder\Tests\Unit\Util;

use PHPUnit\Framework\TestCase;
use Phpactor\CodeBuilder\Util\TextFormat;

class TextFormatTest extends TestCase
{
public function testThatItDoesNotIndentBlankLines()
{
$input = <<<EOT
Non blank line

Non blank line
EOT;
$expectedOutput = <<<EOT
Non blank line

Non blank line
EOT;

$formater = new TextFormat(' ');
$this->assertSame($expectedOutput, $formater->indent($input, 1));
}

public function testThatItIndentOnMoreThanOneLevel()
{
$input = <<<EOT
Non blank line

Non blank line
EOT;
$expectedOutput = <<<EOT
Non blank line

Non blank line
EOT;

$formater = new TextFormat(' ');
$this->assertSame($expectedOutput, $formater->indent($input, 2));
}
}