-
Notifications
You must be signed in to change notification settings - Fork 361
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
Support basic usage of class_alias #586
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really sure how complete the other maintainers would want support for class_alias to be before adding it to a release.
Things that could be improved were mentioned in my review comments
src/Phan/CodeBase.php
Outdated
FullyQualifiedClassName $alias | ||
) { | ||
$class = $this->getClassByFQSEN( $original ); | ||
$this->fqsen_class_map[$alias] = $class; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems ok overall as a first start, might want to have this behind a config (false by default and in .phan/config.php) since it might not be finished for a while.
There can be duplicates of classes.
Also, there might also be a class by the same name as an alias, and
- something like PhanDuplicateClass/PhanDuplicateClassAlias should be emit (may or may not check if the strings are equal)
- See the other uses of withAlternateId() in Phan when adding class definitions
Also, people can call class_alias above (or in a different file parsed before) a class declaration.
nit: Also, add doc comment with @return void
, like the others in the file
nit: also, remove space between (
and first param, remove space between last param and )
in function invocations
Long term, there should be a step after the parseFiles phase but before the analyzeClasses/analyzeMethods phase, when all of the classes have been parsed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And there can technically be a class_alias of class_alias of a class_alias ...., which also makes a complete implementation harder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If people start to alias their aliases like that they are on their own. Having support for a single level of aliasing should be sufficient for most cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A single level of aliasing seems fine.
Also, need to check that the original class exists in Phan\CodeBase when adding an alias, emit an PhanUndeclaredClass if it doesn't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've got most of this worked out, the one problem i'm having is code such as:
<?php
class A {}
class_alias('A', 'B');
class B {}
The issue generated ends up looking like
%s:4 PhanRedefineClass Class \B defined at %s:4 was previously defined as Class \A at %s:2
Basically, the aliased class reports itself as the original class (like it does in php), but then when it comes time to report the error the original definition is reported as the original class definition, rather than the class_alias() call. It does work appropriately the other way around, when the issue is emitted from the class_alias call rather than from the class definition.
Will update the patch with my latest work, but it won't be ready to merge yet while i ponder this issue.
src/Phan/Parse/ParseVisitor.php
Outdated
&& isset($args->children[1]) | ||
&& is_string($args->children[1]) | ||
) { | ||
$originalFqsen = FullyQualifiedClassName::fromFullyQualifiedString( $args->children[0] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May want to check if $originalFqsen is valid, emit a new issue type here? https://secure.php.net/manual/en/language.variables.basics.php
As a regular expression, it would be expressed thus:
'[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I double checked and php actually doesn't care, you can class_alias("Foo", "\0"); and then class_alias("\0", "Bar"); and Bar will be a valid alias (as is $nb = "\0"; $x = new $nb();). While dirty, it's allowed so not sure we need an issue?
tests/files/src/0258_class_alias.php
Outdated
namespace { | ||
class A {} | ||
class_alias('A', 'B'); | ||
$x = new B(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a test of an undeclared class in anonymous namespace as well?
Optionally, add a test of this working for traits
Optionally, add a test of something extending an alias class
Add a test of using something from the original (e.g. calling a method of) a class alias (Defined in class A)
As I like the idea of emitting a
Phan does a funny thing now which is to defer "hydration" of classes until the class is requested. We used to scan all classes before the analysis phase, but we got a nice speedup by deferring it until after Phan has forked off the sub-processes for analysis. If we choose to do that hydration serially so that we have full access to the classes before analysis, we should just be aware of the hit on speed we'll be taking.
If we get bug reports from folks saying that something broke when they aliases |
23dddc5
to
705416a
Compare
705416a
to
ef0a9c1
Compare
Took me awhile to have the time to get back to this, but I've updated the patch for various reviews:
Problems:
|
You need to isolate the names used in a unit test. Phan runs unit tests on a group of files, not on files individually (The state isn't cleared after each test file)
Instead of A, B, C, I'd suggest using the names A271, B271, C271 for the new test classes (Because 271 is the number for this unit test) (and global functions). (Or something specific to what's being tested, AAliasSource, BAliased, etc.) |
tests/files/src/0271_class_alias.php
Outdated
testB(new Z()); | ||
} | ||
|
||
namespace Foo { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefer Foo271 here, A271 above, this may conflict with other tests for namespace foo
tests/files/src/0271_class_alias.php
Outdated
class_alias('A', 'B'); | ||
class Z extends B {} | ||
|
||
function testA(A $x) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
testA271
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was exactly it, thanks! Tests passing locally now.
@@ -992,6 +992,16 @@ public function asExpandedTypes( | |||
} | |||
} | |||
|
|||
// Add in aliases | |||
$fqsenAliases = $code_base->getClassAliasesByFQSEN($class_fqsen); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asExpandedType seems like the wrong place, it converts Subclass to Subclass|BaseClass.
It'd make more sense to me if aliases were replaced with the original type(s), instead of adding aliases
Type::make() might work (But that returns only one type)
EDIT: asExpandedType might end up being the best way to handle multiple possible original classes for a given class, not completely sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately make doesn't have CodeBase, and is called from plenty of places that don't pass around CodeBase. It would be pretty invasive to get the right information into make for it to resolve the alias back into the original class name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, Type::make doesn't work. It needs to be at a higher level. This is out of scope of this PR.
It should be possible to do this in a way similar to the way phan handles imports use SomeNamespace\Foo as Bar
(Phan sees Bar, but expands it to SomeNamespace\Foo
everywhere - instanceof
checks, parameters, phpdoc parameters, etc.)
All of the methods such as UnionType::fromNode(context, code_base, children)
(Called in AnalysisVisitor), UnionType::fromStringInContext(type, context, true)
, etc. , all get a Context, which contains a link to the codebase.
- See the implementation of Type::fromStringInContext, from the
// Check to see if the type name is mapped via a using clause.
part of the code.
(This would be checked afteruse
statements are checked, and possibly in combination with ause
statement)
src/Phan/CodeBase.php
Outdated
$fqsenAliases = []; | ||
} | ||
$fqsenAliases[] = $alias; | ||
$this->fqsen_alias_map[$original] = $fqsenAliases; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This list may end up with duplicates if multiple files define the same alias. A map to a set of fqsens may make more sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set makes sense here, I've switched it over.
ab7ec83
to
6cd61f4
Compare
This supports the most basic form of class_alias, which probably covers most use cases. It adds aliases for class_alias used in the global scope with constant string arguments or the class referenced via the ::class constant. Fixes phan#521
6cd61f4
to
a035257
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems pretty close to done.
Code style is consistent with what we already have, and the code is well commented.
This is reading files from disk much more often than necessary. See my comment on src/Phan/Analysis.php
The step to extract the occurences of class_alias seems like it should be merged into the Parse phase completely. As this already does, the extracted occurences should be resolved after the Parse phase is completely over and classes are created.
RedefineClass may make more sense if split into RedefineClassAlias/RedefineAliasedClass, so that users are able to better fine tune which issues to suppress in large codebases.
) { | ||
foreach ($file_path_list as $i => $file_path) { | ||
try { | ||
AliasAnalyzer::parseFile($code_base, $file_path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way this is written, this will read every single file from disk, and call \ast\parse_code
twice.
This is inefficient, and will slow phan down (Especially the parsing from disk part).
- most files in a typical process have 0 class_alias statements, but this step would parse them anyway.
- a small fraction of lines in a file with
class_alias
are actuallyclass_alias
On another note, I think that class_alias
should take effect for all parsed files, not just the subset which is analyzed. Class declarations, function declarations, etc. are all parsed from parsed files, and creating classes from an alias is somewhat similar.
A preferred solution would be:
- During the parse phase, when
class_alias
is encountered, add information with the context, source class name, and target class name to this.
E.g. create\Phan\Language\Element\ClassAlias
with the propertiessourceClassName
,targetClassName
, and maybeContext
and maybe\ast\Node $node
Add those to a list
(Not 100% sure if the context is necessary, butMyClass::class
would be a different string innamespace Foo
. Actually, the file and line might be needed to warn about bad aliases (e.g. missing source class). $node probably isn't needed either.) - During the analyze phase, loop over that list of ClassAlias, and perform the AliasAnalyzer steps
(The step to create aliases seems short enough to fit in one or two methods of theAnalysis
class.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in PR 907
|
||
// Recurse into each child node | ||
$child_context = $context; | ||
foreach ($node->children ?? [] as $child_node) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be moved into the parse phase
You'll need to track these somewhere. I suggest $code_base->recordClassAlias(sourceClass, targetClass, etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in PR 907
$expression = $node->children['expr']; | ||
|
||
if ($expression->kind !== \ast\AST_NAME | ||
|| $expression->children['name'] !== 'class_alias' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: strcasecmp. The other code is a bad example, but Phan should really be using strcasecmp(name, 'fnname') === 0
in a lot of places.
(If it's invoked as CLASS_ALIAS
or some other weird case-insensitive equivalent).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in PR 907
} else if ($this->code_base->hasClassWithFQSEN($alias_fqsen)) { | ||
$clazz = $this->code_base->getClassByFQSEN($alias_fqsen); | ||
$this->emitIssue( | ||
Issue::RedefineClass, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would prefer a new issue type RedefineClassAlias
with the same category/severity. I don't really care as much about what happens if class and alias conflict (for this PR), mostly mention this for the case of alias and alias conflict.
- These are all done at the level of the global scope, and phan doesn't have a way to suppress an individual statement outside of a function/method.
- Users may want a way to check for duplicate classes without checking for duplicate aliases, or vice versa
(E.g. Same alias may exist in different files).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in PR 907
Issue::RedefineClass, | ||
$node->lineno ?? 0, | ||
$args->children[1], | ||
$this->context->getFile(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The suggestion I made would involve taking this context and file/line from the AliasInfo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in PR 907
class Dupe {} | ||
trait OriginalTrait {} | ||
|
||
class_alias('\Foo278\NamespacedOriginal', 'AliasedFromNamespace278'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a test with NamespacedOriginal::class
passed to the source of class_alias
php > namespace C{echo A::class;}
C\A
Additionally, after #563 (Phan Daemon Mode) is merged, this PR will have to be modified. Class aliases would have to be removed if they were removed from a file which was removed (Or if the file was edited and a line was removed)
Also, after rebasing, this PR should mention class_alias support in the NEWS file in the top level directory in the notes for the latest 0.9.x-dev code. |
This PR is based off of phan#586 - Account for daemon mode (Not fully tested with the config setting on yet) - Parse class_alias original and alias FQSENs in one phase, and create the entries in another phase (Keeps the number of times each file is parsed the same as before) - Follow variable naming conventions - Tests continue to pass.
This PR had merge conflicts and pending review comments for a while. #907 (which is based on this PR) fixed the merge conflicts, and most of the remaining review comments. That PR was merged to the master branch. In order to analyze class_alias statements in the global scope, use the master branch (0.9.3-dev) with the following addition to .phan/config.php // If true, Phan will read `class_alias` calls in the global scope,
// then (1) create aliases from the *parsed* files if no class definition was found,
// and (2) emit issues in the global scope if the source or target class is invalid.
// (If there are multiple possible valid original classes for an aliased class name,
// the one which will be created is unspecified.)
// NOTE: THIS IS EXPERIMENTAL, and the implementation may change.
'enable_class_alias_support' => true, |
This supports the most basic form of class_alias, which probably
covers most use cases. It adds aliases for class_alias used in the
global scope with constant string arguments.
Fixes #521