Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create namespace for whitelisted classes #138

Merged

Conversation

moorscode
Copy link
Contributor

@moorscode moorscode commented Jan 5, 2018

Technical choices:

This is the simplest solution to the problem we could think of.

Global namespace

Decided to not put classes in a global namespace that are not whitelisted in a file, which contains a whitelisted class.

We have tried this, but it resulted in having to put every class in a namespace { } wrap, which fails a lot of tests.

As PSR-0 declares:
This means each class is in a file by itself, and is in a namespace of at least one level: a top-level vendor name.

Updated the functionality to make sure any code that is not wrapped in a namespace will receive the global namespace. This prevents invalid PHP from being generated.

Paired on it together with @atimmer

Fixes #135

@moorscode
Copy link
Contributor Author

While testing this code in a project, we ran across the problem that defines are placed outside the namespace.

This is causing fatal errors.

Placing the define inside the namespace resolves the error.

@moorscode
Copy link
Contributor Author

moorscode commented Jan 5, 2018

Having to resolve this, I think we need to wrap -anything- in a namespace group as this is the method of prefixing used in this tool.

This would result in:

// Global namespace define
namespace {
    define('SOME_DEFINE', 'value');
}

// Prefixed class
namespace Humbug {
    class b {}
}

// Global namespace class
namespace {
    class c {}
}

Copy link
Member

@theofidry theofidry left a comment

Choose a reason for hiding this comment

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

Big 👍

I added comments; a few are regarding some missing tests and others are mostly nitpicks.

Huge thanks for this one 😄

],

[
'spec' => <<<'SPEC'
Copy link
Member

Choose a reason for hiding this comment

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

good use of the format:

[
    'spec' => '',
    'payload' => '',
]

but I think it's a case simple enough where we can use the simplified format:

'spec content' => 'payload content'

As you can find here

],

[
'spec' => <<<'SPEC'
Copy link
Member

Choose a reason for hiding this comment

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

I would add two more cases:

  • One with multiple different namespaces like you can find here (it doesn't need to be the same it's just for the idea)
  • One with two classes belonging to the global namespace: one which is a whitelisted one, so AppKernel which as a result will be prefixed, and another which is not like Closure which should not be prefixed

use PhpParser\NodeVisitorAbstract;

/**
* Creates a namespace for whitelisted classes.
Copy link
Member

Choose a reason for hiding this comment

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

[...] belonging to the global namespace. It is an important part as the whitelisting feature is different depending of if the class belongs or not to the global namespace cf. global-namespace-whitelisting and whitelist

final class NamespaceStmtCreator extends NodeVisitorAbstract
{
private $prefix;

Copy link
Member

Choose a reason for hiding this comment

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

let's remove the blank lines between those properties

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added those because the code style checker was marking them as errors. Will remove the lines.

Copy link
Member

@theofidry theofidry Jan 6, 2018

Choose a reason for hiding this comment

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

Ah right. I need to figure out which parameter in the CS fixer it is to disable that, it really annoys me

* @param string $prefix
* @param NamespaceStmtCollection $namespaceStatements
* @param callable $globalWhitelister
*/
Copy link
Member

Choose a reason for hiding this comment

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

the doc here is not useful as completely redundant with the signature, I know I may not have been consistent everywhere but I would remove it still :)

{
return ($node instanceof Class_)
? $this->addNamespaceStmt($node)
: $node;
Copy link
Member

Choose a reason for hiding this comment

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

return ($node instanceof Class_)
    ? $this->addNamespaceStmt($node)
    : $node
;

}

/**
* @param Class_ $node
Copy link
Member

Choose a reason for hiding this comment

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

same here, useless phpdoc

@@ -0,0 +1,85 @@
<?php
Copy link
Member

Choose a reason for hiding this comment

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

There is also a few other cases that you can find in specs/class that needs to be accounted for. For example it should also apply to:

  • abstract classes
  • interfaces
  • traits: they cannot be whitelisted; may require a test that it doesn't work there still
  • anonymous: they cannot be whitelisted; may require a test that it doesn't work there still

@moorscode
Copy link
Contributor Author

Additional functionality:

I've used the beforeTraverse to determine if this "scope/file" has any whitelisted elements. If so, anything not white-listed needs to have a namespace.

If no namespace exists, the global namespace will be wrapped around it.

This resolves the problems described before.

@moorscode
Copy link
Contributor Author

(Sorry I'm not so good at squashing commits and pushing after everything is done ;))

@theofidry
Copy link
Member

(Sorry I'm not so good at squashing commits and pushing after everything is done ;))

Don't worry about it I'll do a "Squash & Merge", only the CI may resent you a bit if you push them one by one but I think Travis & Scrutinizer got better at it

}

/**
* @param Node[] $nodes List of nodes to find whitelisted nodes from.
Copy link
Member

Choose a reason for hiding this comment

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

I think the signature is descriptive enough, no need for the List [...] comment. I would keep the phpdoc as it does add the type array, but I tend to avoid comments when the code is self-explanatory as the problem with comments/docs is that they easily get out of date with the code they are documenting. So keeping it to minimal is better IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you prefer to have an empty docblock or just none at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the projects we do at Yoast, we have very strict documentation rules.
We don't find ourselves having a lot of outdated documentation entries in general.

The fact that we are in a WordPress environment, where backwards compatibility is a major thing might have something to do with it, but I don't think that is the only factor in it.

Writing the documentation mostly helps me re-evaluate what I wrote functionally and if the naming of the variables and logic are actually the best solutions.

My two cents on the subject.

I'm happy to oblige to the standards and approach you set for your project.

Copy link
Member

Choose a reason for hiding this comment

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

I think @param Node[] $nodes is enough here

If it's too "redundant" with the code, I really prefer to avoid the comments. For public interfaces, non trivial things or for example the doc to explain the purpose of the class I tend to be in favour comments :)

}

/**
* @return bool
Copy link
Member

Choose a reason for hiding this comment

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

no need for the phpdoc here

return null !== $node->name && ($this->globalWhitelister)($node->name);
});

return !empty($nodes);
Copy link
Member

Choose a reason for hiding this comment

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

as the returned type is an array I think [] !== $nodes or with a count is more expressive here

public function beforeTraverse(array $nodes)
{
$classes = array_filter($nodes, [$this, 'isWhitelistableNode']);
if (empty($classes)) {
Copy link
Member

Choose a reason for hiding this comment

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

as the returned type is an array I think [] !== $nodes or with a count is more expressive here

@moorscode
Copy link
Contributor Author

moorscode commented Jan 6, 2018

While writing specs, I came across another problem when a global namespace is already defined around a class.
It does not seem to be in the scope of this feature, so I decided not to add it in this PR:

'Traits in different namespace.' => <<<'PHP'
<?php

namespace Foo {
    trait SomeTrait{}
}

namespace {
    class AppKernel{}
}

----
<?php

namespace Humbug\Foo{
    trait SomeTrait
    {
    }
}
namespace Humbug {
    class AppKernel
    {
    }
}

PHP

As the namespace prefixer should pick this up and add the namespace for the whitelisted class in the global namespace.

@theofidry
Copy link
Member

You mean right now we would have:

<?php

namespace Foo {
    trait SomeTrait{}
}

namespace {
    class AppKernel{}
}

----
<?php

namespace Humbug\Foo{
    trait SomeTrait
    {
    }
}
namespace Humbug {
    class AppKernel
    {
    }
}

instead of having canonical namespaces, i.e. only one Humbug namespace declaration? If that's the case IMO there is nothing to do here it works fine https://3v4l.org/DCjXW

@moorscode
Copy link
Contributor Author

Hope this output helps to describe it a bit better.
The term whitelist gets a bit confusing as it whitelists from receiving the prefix..

I would expect the AppKernel (whitelisted node name) to be prefixed.
But as it already has a namespace, which is global, it is ignored in the Namespace Statement Prefixer: https://github.com/humbug/php-scoper/blob/master/src/NodeVisitor/NamespaceStmtPrefixer.php#L79

================================================================================
INPUT
================================================================================
<?php

namespace Foo {
    trait SomeTrait{}
}

namespace {
    class AppKernel{}
}

namespace {
    class Bla{}
}


================================================================================
EXPECTED
================================================================================
<?php

namespace Humbug\Foo {
    trait SomeTrait
    {
    }
}
namespace Humbug {
    class AppKernel
    {
    }
}
namespace {
    class Bla
    {
    }
}


================================================================================
ACTUAL
================================================================================
<?php

namespace Humbug\Foo {
    trait SomeTrait
    {
    }
}
namespace {
    class AppKernel
    {
    }
}
namespace {
    class Bla
    {
    }
}

@theofidry
Copy link
Member

theofidry commented Jan 6, 2018

We're clear on the expected input/expected output.

I wonder if the work you're doing shouldn't be in the NamespaceStmtPrefixer as otherwise, this one is missing the necessary context to know if the namespace should be prefixed or not

@moorscode
Copy link
Contributor Author

We're clear on the expected input/expected output; But I wonder if the work you're doing shouldn't be in the NamespaceStmtPrefixer as otherwise, this one is missing the necessary context to know if the namespace should be prefixed or not

That's a good point, I initially started a new visitor because prefixing and creating seemed like two concepts.
As the global namespace doesn't have to be defined but is always present, prefixing it and making it explicit fits in the prefix concept when looking at it that way.

@moorscode
Copy link
Contributor Author

Updated the code and added the desired StyleCI disables to remove the checks on property separation and const separation.

@moorscode
Copy link
Contributor Author

moorscode commented Jan 6, 2018

Also put the StyleCI changes in a separate PR: #139

Copy link
Member

@theofidry theofidry left a comment

Choose a reason for hiding this comment

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

Awesome! Could you rebase it (for the CS failure)?


/**
* @param string $prefix
* @param NamespaceStmtCollection $namespaceStatements
* @param callable $globalWhitelister
Copy link
Member

Choose a reason for hiding this comment

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

let's just remove the phpdoc here

'Defines should be wrapped in namespace alongside whitelisted class.' => <<<'PHP'
<?php

define( "MY_DEFINE", "value" );
Copy link
Member

Choose a reason for hiding this comment

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

small extra space here :)

As the global namespace is implicitly always present, we are making it explicit when needed by adding the statement to the stack.

Added extra test for complex situation with non-whitespaced class in combination with whitespaced class.
Simplify by combining two if-statements
@moorscode moorscode force-pushed the 135-create-namespace-for-whitelisted-class branch from 381286a to e4e0849 Compare January 8, 2018 08:16
@theofidry theofidry merged commit 674e24d into humbug:master Jan 8, 2018
@theofidry
Copy link
Member

Many thanks @moorscode, that was a critical bug :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants