Add Composer autoloading functions and PHPStan for testing

This commit is contained in:
Alex Cabal 2019-02-26 13:03:45 -06:00
parent e198c4db65
commit f5d7d4e02a
1518 changed files with 169063 additions and 30 deletions

View file

@ -0,0 +1,153 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
class ClassTest extends \PHPUnit\Framework\TestCase
{
protected function createClassBuilder($class) {
return new Class_($class);
}
public function testExtendsImplements() {
$node = $this->createClassBuilder('SomeLogger')
->extend('BaseLogger')
->implement('Namespaced\Logger', new Name('SomeInterface'))
->implement('\Fully\Qualified', 'namespace\NamespaceRelative')
->getNode()
;
$this->assertEquals(
new Stmt\Class_('SomeLogger', [
'extends' => new Name('BaseLogger'),
'implements' => [
new Name('Namespaced\Logger'),
new Name('SomeInterface'),
new Name\FullyQualified('Fully\Qualified'),
new Name\Relative('NamespaceRelative'),
],
]),
$node
);
}
public function testAbstract() {
$node = $this->createClassBuilder('Test')
->makeAbstract()
->getNode()
;
$this->assertEquals(
new Stmt\Class_('Test', [
'flags' => Stmt\Class_::MODIFIER_ABSTRACT
]),
$node
);
}
public function testFinal() {
$node = $this->createClassBuilder('Test')
->makeFinal()
->getNode()
;
$this->assertEquals(
new Stmt\Class_('Test', [
'flags' => Stmt\Class_::MODIFIER_FINAL
]),
$node
);
}
public function testStatementOrder() {
$method = new Stmt\ClassMethod('testMethod');
$property = new Stmt\Property(
Stmt\Class_::MODIFIER_PUBLIC,
[new Stmt\PropertyProperty('testProperty')]
);
$const = new Stmt\ClassConst([
new Node\Const_('TEST_CONST', new Node\Scalar\String_('ABC'))
]);
$use = new Stmt\TraitUse([new Name('SomeTrait')]);
$node = $this->createClassBuilder('Test')
->addStmt($method)
->addStmt($property)
->addStmts([$const, $use])
->getNode()
;
$this->assertEquals(
new Stmt\Class_('Test', [
'stmts' => [$use, $const, $property, $method]
]),
$node
);
}
public function testDocComment() {
$docComment = <<<'DOC'
/**
* Test
*/
DOC;
$class = $this->createClassBuilder('Test')
->setDocComment($docComment)
->getNode();
$this->assertEquals(
new Stmt\Class_('Test', [], [
'comments' => [
new Comment\Doc($docComment)
]
]),
$class
);
$class = $this->createClassBuilder('Test')
->setDocComment(new Comment\Doc($docComment))
->getNode();
$this->assertEquals(
new Stmt\Class_('Test', [], [
'comments' => [
new Comment\Doc($docComment)
]
]),
$class
);
}
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_Echo"');
$this->createClassBuilder('Test')
->addStmt(new Stmt\Echo_([]))
;
}
public function testInvalidDocComment() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
$this->createClassBuilder('Test')
->setDocComment(new Comment('Test'));
}
public function testEmptyName() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name cannot be empty');
$this->createClassBuilder('Test')
->extend('');
}
public function testInvalidName() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name must be a string or an instance of Node\Name');
$this->createClassBuilder('Test')
->extend(['Foo']);
}
}

View file

@ -0,0 +1,114 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Expr\Print_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
class FunctionTest extends \PHPUnit\Framework\TestCase
{
public function createFunctionBuilder($name) {
return new Function_($name);
}
public function testReturnByRef() {
$node = $this->createFunctionBuilder('test')
->makeReturnByRef()
->getNode()
;
$this->assertEquals(
new Stmt\Function_('test', [
'byRef' => true
]),
$node
);
}
public function testParams() {
$param1 = new Node\Param(new Variable('test1'));
$param2 = new Node\Param(new Variable('test2'));
$param3 = new Node\Param(new Variable('test3'));
$node = $this->createFunctionBuilder('test')
->addParam($param1)
->addParams([$param2, $param3])
->getNode()
;
$this->assertEquals(
new Stmt\Function_('test', [
'params' => [$param1, $param2, $param3]
]),
$node
);
}
public function testStmts() {
$stmt1 = new Print_(new String_('test1'));
$stmt2 = new Print_(new String_('test2'));
$stmt3 = new Print_(new String_('test3'));
$node = $this->createFunctionBuilder('test')
->addStmt($stmt1)
->addStmts([$stmt2, $stmt3])
->getNode()
;
$this->assertEquals(
new Stmt\Function_('test', [
'stmts' => [
new Stmt\Expression($stmt1),
new Stmt\Expression($stmt2),
new Stmt\Expression($stmt3),
]
]),
$node
);
}
public function testDocComment() {
$node = $this->createFunctionBuilder('test')
->setDocComment('/** Test */')
->getNode();
$this->assertEquals(new Stmt\Function_('test', [], [
'comments' => [new Comment\Doc('/** Test */')]
]), $node);
}
public function testReturnType() {
$node = $this->createFunctionBuilder('test')
->setReturnType('void')
->getNode();
$this->assertEquals(new Stmt\Function_('test', [
'returnType' => 'void'
], []), $node);
}
public function testInvalidNullableVoidType() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('void type cannot be nullable');
$this->createFunctionBuilder('test')->setReturnType('?void');
}
public function testInvalidParamError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected parameter node, got "Name"');
$this->createFunctionBuilder('test')
->addParam(new Node\Name('foo'))
;
}
public function testAddNonStmt() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected statement or expression node');
$this->createFunctionBuilder('test')
->addStmt(new Node\Name('Test'));
}
}

View file

@ -0,0 +1,102 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Scalar\DNumber;
use PhpParser\Node\Stmt;
class InterfaceTest extends \PHPUnit\Framework\TestCase
{
/** @var Interface_ */
protected $builder;
protected function setUp() {
$this->builder = new Interface_('Contract');
}
private function dump($node) {
$pp = new \PhpParser\PrettyPrinter\Standard;
return $pp->prettyPrint([$node]);
}
public function testEmpty() {
$contract = $this->builder->getNode();
$this->assertInstanceOf(Stmt\Interface_::class, $contract);
$this->assertEquals(new Node\Identifier('Contract'), $contract->name);
}
public function testExtending() {
$contract = $this->builder->extend('Space\Root1', 'Root2')->getNode();
$this->assertEquals(
new Stmt\Interface_('Contract', [
'extends' => [
new Node\Name('Space\Root1'),
new Node\Name('Root2')
],
]), $contract
);
}
public function testAddMethod() {
$method = new Stmt\ClassMethod('doSomething');
$contract = $this->builder->addStmt($method)->getNode();
$this->assertSame([$method], $contract->stmts);
}
public function testAddConst() {
$const = new Stmt\ClassConst([
new Node\Const_('SPEED_OF_LIGHT', new DNumber(299792458.0))
]);
$contract = $this->builder->addStmt($const)->getNode();
$this->assertSame(299792458.0, $contract->stmts[0]->consts[0]->value->value);
}
public function testOrder() {
$const = new Stmt\ClassConst([
new Node\Const_('SPEED_OF_LIGHT', new DNumber(299792458))
]);
$method = new Stmt\ClassMethod('doSomething');
$contract = $this->builder
->addStmt($method)
->addStmt($const)
->getNode()
;
$this->assertInstanceOf(Stmt\ClassConst::class, $contract->stmts[0]);
$this->assertInstanceOf(Stmt\ClassMethod::class, $contract->stmts[1]);
}
public function testDocComment() {
$node = $this->builder
->setDocComment('/** Test */')
->getNode();
$this->assertEquals(new Stmt\Interface_('Contract', [], [
'comments' => [new Comment\Doc('/** Test */')]
]), $node);
}
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_PropertyProperty"');
$this->builder->addStmt(new Stmt\PropertyProperty('invalid'));
}
public function testFullFunctional() {
$const = new Stmt\ClassConst([
new Node\Const_('SPEED_OF_LIGHT', new DNumber(299792458))
]);
$method = new Stmt\ClassMethod('doSomething');
$contract = $this->builder
->addStmt($method)
->addStmt($const)
->getNode()
;
eval($this->dump($contract));
$this->assertTrue(interface_exists('Contract', false));
}
}

View file

@ -0,0 +1,162 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Expr\Print_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
class MethodTest extends \PHPUnit\Framework\TestCase
{
public function createMethodBuilder($name) {
return new Method($name);
}
public function testModifiers() {
$node = $this->createMethodBuilder('test')
->makePublic()
->makeAbstract()
->makeStatic()
->getNode()
;
$this->assertEquals(
new Stmt\ClassMethod('test', [
'flags' => Stmt\Class_::MODIFIER_PUBLIC
| Stmt\Class_::MODIFIER_ABSTRACT
| Stmt\Class_::MODIFIER_STATIC,
'stmts' => null,
]),
$node
);
$node = $this->createMethodBuilder('test')
->makeProtected()
->makeFinal()
->getNode()
;
$this->assertEquals(
new Stmt\ClassMethod('test', [
'flags' => Stmt\Class_::MODIFIER_PROTECTED
| Stmt\Class_::MODIFIER_FINAL
]),
$node
);
$node = $this->createMethodBuilder('test')
->makePrivate()
->getNode()
;
$this->assertEquals(
new Stmt\ClassMethod('test', [
'type' => Stmt\Class_::MODIFIER_PRIVATE
]),
$node
);
}
public function testReturnByRef() {
$node = $this->createMethodBuilder('test')
->makeReturnByRef()
->getNode()
;
$this->assertEquals(
new Stmt\ClassMethod('test', [
'byRef' => true
]),
$node
);
}
public function testParams() {
$param1 = new Node\Param(new Variable('test1'));
$param2 = new Node\Param(new Variable('test2'));
$param3 = new Node\Param(new Variable('test3'));
$node = $this->createMethodBuilder('test')
->addParam($param1)
->addParams([$param2, $param3])
->getNode()
;
$this->assertEquals(
new Stmt\ClassMethod('test', [
'params' => [$param1, $param2, $param3]
]),
$node
);
}
public function testStmts() {
$stmt1 = new Print_(new String_('test1'));
$stmt2 = new Print_(new String_('test2'));
$stmt3 = new Print_(new String_('test3'));
$node = $this->createMethodBuilder('test')
->addStmt($stmt1)
->addStmts([$stmt2, $stmt3])
->getNode()
;
$this->assertEquals(
new Stmt\ClassMethod('test', [
'stmts' => [
new Stmt\Expression($stmt1),
new Stmt\Expression($stmt2),
new Stmt\Expression($stmt3),
]
]),
$node
);
}
public function testDocComment() {
$node = $this->createMethodBuilder('test')
->setDocComment('/** Test */')
->getNode();
$this->assertEquals(new Stmt\ClassMethod('test', [], [
'comments' => [new Comment\Doc('/** Test */')]
]), $node);
}
public function testReturnType() {
$node = $this->createMethodBuilder('test')
->setReturnType('bool')
->getNode();
$this->assertEquals(new Stmt\ClassMethod('test', [
'returnType' => 'bool'
], []), $node);
}
public function testAddStmtToAbstractMethodError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot add statements to an abstract method');
$this->createMethodBuilder('test')
->makeAbstract()
->addStmt(new Print_(new String_('test')))
;
}
public function testMakeMethodWithStmtsAbstractError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot make method with statements abstract');
$this->createMethodBuilder('test')
->addStmt(new Print_(new String_('test')))
->makeAbstract()
;
}
public function testInvalidParamError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected parameter node, got "Name"');
$this->createMethodBuilder('test')
->addParam(new Node\Name('foo'))
;
}
}

View file

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Stmt;
class NamespaceTest extends \PHPUnit\Framework\TestCase
{
protected function createNamespaceBuilder($fqn) {
return new Namespace_($fqn);
}
public function testCreation() {
$stmt1 = new Stmt\Class_('SomeClass');
$stmt2 = new Stmt\Interface_('SomeInterface');
$stmt3 = new Stmt\Function_('someFunction');
$docComment = new Doc('/** Test */');
$expected = new Stmt\Namespace_(
new Node\Name('Name\Space'),
[$stmt1, $stmt2, $stmt3],
['comments' => [$docComment]]
);
$node = $this->createNamespaceBuilder('Name\Space')
->addStmt($stmt1)
->addStmts([$stmt2, $stmt3])
->setDocComment($docComment)
->getNode()
;
$this->assertEquals($expected, $node);
$node = $this->createNamespaceBuilder(new Node\Name(['Name', 'Space']))
->setDocComment($docComment)
->addStmts([$stmt1, $stmt2])
->addStmt($stmt3)
->getNode()
;
$this->assertEquals($expected, $node);
$node = $this->createNamespaceBuilder(null)->getNode();
$this->assertNull($node->name);
$this->assertEmpty($node->stmts);
}
}

View file

@ -0,0 +1,166 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
class ParamTest extends \PHPUnit\Framework\TestCase
{
public function createParamBuilder($name) {
return new Param($name);
}
/**
* @dataProvider provideTestDefaultValues
*/
public function testDefaultValues($value, $expectedValueNode) {
$node = $this->createParamBuilder('test')
->setDefault($value)
->getNode()
;
$this->assertEquals($expectedValueNode, $node->default);
}
public function provideTestDefaultValues() {
return [
[
null,
new Expr\ConstFetch(new Node\Name('null'))
],
[
true,
new Expr\ConstFetch(new Node\Name('true'))
],
[
false,
new Expr\ConstFetch(new Node\Name('false'))
],
[
31415,
new Scalar\LNumber(31415)
],
[
3.1415,
new Scalar\DNumber(3.1415)
],
[
'Hallo World',
new Scalar\String_('Hallo World')
],
[
[1, 2, 3],
new Expr\Array_([
new Expr\ArrayItem(new Scalar\LNumber(1)),
new Expr\ArrayItem(new Scalar\LNumber(2)),
new Expr\ArrayItem(new Scalar\LNumber(3)),
])
],
[
['foo' => 'bar', 'bar' => 'foo'],
new Expr\Array_([
new Expr\ArrayItem(
new Scalar\String_('bar'),
new Scalar\String_('foo')
),
new Expr\ArrayItem(
new Scalar\String_('foo'),
new Scalar\String_('bar')
),
])
],
[
new Scalar\MagicConst\Dir,
new Scalar\MagicConst\Dir
]
];
}
/**
* @dataProvider provideTestTypes
*/
public function testTypes($typeHint, $expectedType) {
$node = $this->createParamBuilder('test')
->setTypeHint($typeHint)
->getNode()
;
$type = $node->type;
/* Manually implement comparison to avoid __toString stupidity */
if ($expectedType instanceof Node\NullableType) {
$this->assertInstanceOf(get_class($expectedType), $type);
$expectedType = $expectedType->type;
$type = $type->type;
}
$this->assertInstanceOf(get_class($expectedType), $type);
$this->assertEquals($expectedType, $type);
}
public function provideTestTypes() {
return [
['array', new Node\Identifier('array')],
['callable', new Node\Identifier('callable')],
['bool', new Node\Identifier('bool')],
['int', new Node\Identifier('int')],
['float', new Node\Identifier('float')],
['string', new Node\Identifier('string')],
['iterable', new Node\Identifier('iterable')],
['object', new Node\Identifier('object')],
['Array', new Node\Identifier('array')],
['CALLABLE', new Node\Identifier('callable')],
['Some\Class', new Node\Name('Some\Class')],
['\Foo', new Node\Name\FullyQualified('Foo')],
['self', new Node\Name('self')],
['?array', new Node\NullableType(new Node\Identifier('array'))],
['?Some\Class', new Node\NullableType(new Node\Name('Some\Class'))],
[new Node\Name('Some\Class'), new Node\Name('Some\Class')],
[
new Node\NullableType(new Node\Identifier('int')),
new Node\NullableType(new Node\Identifier('int'))
],
[
new Node\NullableType(new Node\Name('Some\Class')),
new Node\NullableType(new Node\Name('Some\Class'))
],
];
}
public function testVoidTypeError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Parameter type cannot be void');
$this->createParamBuilder('test')->setType('void');
}
public function testInvalidTypeError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Type must be a string, or an instance of Name, Identifier or NullableType');
$this->createParamBuilder('test')->setType(new \stdClass);
}
public function testByRef() {
$node = $this->createParamBuilder('test')
->makeByRef()
->getNode()
;
$this->assertEquals(
new Node\Param(new Expr\Variable('test'), null, null, true),
$node
);
}
public function testVariadic() {
$node = $this->createParamBuilder('test')
->makeVariadic()
->getNode()
;
$this->assertEquals(
new Node\Param(new Expr\Variable('test'), null, null, false, true),
$node
);
}
}

View file

@ -0,0 +1,147 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
class PropertyTest extends \PHPUnit\Framework\TestCase
{
public function createPropertyBuilder($name) {
return new Property($name);
}
public function testModifiers() {
$node = $this->createPropertyBuilder('test')
->makePrivate()
->makeStatic()
->getNode()
;
$this->assertEquals(
new Stmt\Property(
Stmt\Class_::MODIFIER_PRIVATE
| Stmt\Class_::MODIFIER_STATIC,
[
new Stmt\PropertyProperty('test')
]
),
$node
);
$node = $this->createPropertyBuilder('test')
->makeProtected()
->getNode()
;
$this->assertEquals(
new Stmt\Property(
Stmt\Class_::MODIFIER_PROTECTED,
[
new Stmt\PropertyProperty('test')
]
),
$node
);
$node = $this->createPropertyBuilder('test')
->makePublic()
->getNode()
;
$this->assertEquals(
new Stmt\Property(
Stmt\Class_::MODIFIER_PUBLIC,
[
new Stmt\PropertyProperty('test')
]
),
$node
);
}
public function testDocComment() {
$node = $this->createPropertyBuilder('test')
->setDocComment('/** Test */')
->getNode();
$this->assertEquals(new Stmt\Property(
Stmt\Class_::MODIFIER_PUBLIC,
[
new Stmt\PropertyProperty('test')
],
[
'comments' => [new Comment\Doc('/** Test */')]
]
), $node);
}
/**
* @dataProvider provideTestDefaultValues
*/
public function testDefaultValues($value, $expectedValueNode) {
$node = $this->createPropertyBuilder('test')
->setDefault($value)
->getNode()
;
$this->assertEquals($expectedValueNode, $node->props[0]->default);
}
public function provideTestDefaultValues() {
return [
[
null,
new Expr\ConstFetch(new Name('null'))
],
[
true,
new Expr\ConstFetch(new Name('true'))
],
[
false,
new Expr\ConstFetch(new Name('false'))
],
[
31415,
new Scalar\LNumber(31415)
],
[
3.1415,
new Scalar\DNumber(3.1415)
],
[
'Hallo World',
new Scalar\String_('Hallo World')
],
[
[1, 2, 3],
new Expr\Array_([
new Expr\ArrayItem(new Scalar\LNumber(1)),
new Expr\ArrayItem(new Scalar\LNumber(2)),
new Expr\ArrayItem(new Scalar\LNumber(3)),
])
],
[
['foo' => 'bar', 'bar' => 'foo'],
new Expr\Array_([
new Expr\ArrayItem(
new Scalar\String_('bar'),
new Scalar\String_('foo')
),
new Expr\ArrayItem(
new Scalar\String_('foo'),
new Scalar\String_('bar')
),
])
],
[
new Scalar\MagicConst\Dir,
new Scalar\MagicConst\Dir
]
];
}
}

View file

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Comment;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
class TraitTest extends \PHPUnit\Framework\TestCase
{
protected function createTraitBuilder($class) {
return new Trait_($class);
}
public function testStmtAddition() {
$method1 = new Stmt\ClassMethod('test1');
$method2 = new Stmt\ClassMethod('test2');
$method3 = new Stmt\ClassMethod('test3');
$prop = new Stmt\Property(Stmt\Class_::MODIFIER_PUBLIC, [
new Stmt\PropertyProperty('test')
]);
$use = new Stmt\TraitUse([new Name('OtherTrait')]);
$trait = $this->createTraitBuilder('TestTrait')
->setDocComment('/** Nice trait */')
->addStmt($method1)
->addStmts([$method2, $method3])
->addStmt($prop)
->addStmt($use)
->getNode();
$this->assertEquals(new Stmt\Trait_('TestTrait', [
'stmts' => [$use, $prop, $method1, $method2, $method3]
], [
'comments' => [
new Comment\Doc('/** Nice trait */')
]
]), $trait);
}
public function testInvalidStmtError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Unexpected node of type "Stmt_Echo"');
$this->createTraitBuilder('Test')
->addStmt(new Stmt\Echo_([]))
;
}
}

View file

@ -0,0 +1,106 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
class TraitUseAdaptationTest extends \PHPUnit\Framework\TestCase
{
protected function createTraitUseAdaptationBuilder($trait, $method) {
return new TraitUseAdaptation($trait, $method);
}
public function testAsMake() {
$builder = $this->createTraitUseAdaptationBuilder(null, 'foo');
$this->assertEquals(
new Stmt\TraitUseAdaptation\Alias(null, 'foo', null, 'bar'),
(clone $builder)->as('bar')->getNode()
);
$this->assertEquals(
new Stmt\TraitUseAdaptation\Alias(null, 'foo', Class_::MODIFIER_PUBLIC, null),
(clone $builder)->makePublic()->getNode()
);
$this->assertEquals(
new Stmt\TraitUseAdaptation\Alias(null, 'foo', Class_::MODIFIER_PROTECTED, null),
(clone $builder)->makeProtected()->getNode()
);
$this->assertEquals(
new Stmt\TraitUseAdaptation\Alias(null, 'foo', Class_::MODIFIER_PRIVATE, null),
(clone $builder)->makePrivate()->getNode()
);
}
public function testInsteadof() {
$node = $this->createTraitUseAdaptationBuilder('SomeTrait', 'foo')
->insteadof('AnotherTrait')
->getNode()
;
$this->assertEquals(
new Stmt\TraitUseAdaptation\Precedence(
new Name('SomeTrait'),
'foo',
[new Name('AnotherTrait')]
),
$node
);
}
public function testAsOnNotAlias() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot set alias for not alias adaptation buider');
$this->createTraitUseAdaptationBuilder('Test', 'foo')
->insteadof('AnotherTrait')
->as('bar')
;
}
public function testInsteadofOnNotPrecedence() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot add overwritten traits for not precedence adaptation buider');
$this->createTraitUseAdaptationBuilder('Test', 'foo')
->as('bar')
->insteadof('AnotherTrait')
;
}
public function testInsteadofWithoutTrait() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Precedence adaptation must have trait');
$this->createTraitUseAdaptationBuilder(null, 'foo')
->insteadof('AnotherTrait')
;
}
public function testMakeOnNotAlias() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot set access modifier for not alias adaptation buider');
$this->createTraitUseAdaptationBuilder('Test', 'foo')
->insteadof('AnotherTrait')
->makePublic()
;
}
public function testMultipleMake() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Multiple access type modifiers are not allowed');
$this->createTraitUseAdaptationBuilder(null, 'foo')
->makePrivate()
->makePublic()
;
}
public function testUndefinedType() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Type of adaptation is not defined');
$this->createTraitUseAdaptationBuilder(null, 'foo')
->getNode()
;
}
}

View file

@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
class TraitUseTest extends \PHPUnit\Framework\TestCase
{
protected function createTraitUseBuilder(...$traits) {
return new TraitUse(...$traits);
}
public function testAnd() {
$node = $this->createTraitUseBuilder('SomeTrait')
->and('AnotherTrait')
->getNode()
;
$this->assertEquals(
new Stmt\TraitUse([
new Name('SomeTrait'),
new Name('AnotherTrait')
]),
$node
);
}
public function testWith() {
$node = $this->createTraitUseBuilder('SomeTrait')
->with(new Stmt\TraitUseAdaptation\Alias(null, 'foo', null, 'bar'))
->with((new TraitUseAdaptation(null, 'test'))->as('baz'))
->getNode()
;
$this->assertEquals(
new Stmt\TraitUse([new Name('SomeTrait')], [
new Stmt\TraitUseAdaptation\Alias(null, 'foo', null, 'bar'),
new Stmt\TraitUseAdaptation\Alias(null, 'test', null, 'baz')
]),
$node
);
}
public function testInvalidAdaptationNode() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Adaptation must have type TraitUseAdaptation');
$this->createTraitUseBuilder('Test')
->with(new Stmt\Echo_([]))
;
}
}

View file

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace PhpParser\Builder;
use PhpParser\Builder;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
class UseTest extends \PHPUnit\Framework\TestCase
{
protected function createUseBuilder($name, $type = Stmt\Use_::TYPE_NORMAL) {
return new Builder\Use_($name, $type);
}
public function testCreation() {
$node = $this->createUseBuilder('Foo\Bar')->getNode();
$this->assertEquals(new Stmt\Use_([
new Stmt\UseUse(new Name('Foo\Bar'), null)
]), $node);
$node = $this->createUseBuilder(new Name('Foo\Bar'))->as('XYZ')->getNode();
$this->assertEquals(new Stmt\Use_([
new Stmt\UseUse(new Name('Foo\Bar'), 'XYZ')
]), $node);
$node = $this->createUseBuilder('foo\bar', Stmt\Use_::TYPE_FUNCTION)->as('foo')->getNode();
$this->assertEquals(new Stmt\Use_([
new Stmt\UseUse(new Name('foo\bar'), 'foo')
], Stmt\Use_::TYPE_FUNCTION), $node);
$node = $this->createUseBuilder('foo\BAR', Stmt\Use_::TYPE_CONSTANT)->as('FOO')->getNode();
$this->assertEquals(new Stmt\Use_([
new Stmt\UseUse(new Name('foo\BAR'), 'FOO')
], Stmt\Use_::TYPE_CONSTANT), $node);
}
}

View file

@ -0,0 +1,327 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
class BuilderFactoryTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideTestFactory
*/
public function testFactory($methodName, $className) {
$factory = new BuilderFactory;
$this->assertInstanceOf($className, $factory->$methodName('test'));
}
public function provideTestFactory() {
return [
['namespace', Builder\Namespace_::class],
['class', Builder\Class_::class],
['interface', Builder\Interface_::class],
['trait', Builder\Trait_::class],
['method', Builder\Method::class],
['function', Builder\Function_::class],
['property', Builder\Property::class],
['param', Builder\Param::class],
['use', Builder\Use_::class],
['useFunction', Builder\Use_::class],
['useConst', Builder\Use_::class],
];
}
public function testVal() {
// This method is a wrapper around BuilderHelpers::normalizeValue(),
// which is already tested elsewhere
$factory = new BuilderFactory();
$this->assertEquals(
new String_("foo"),
$factory->val("foo")
);
}
public function testConcat() {
$factory = new BuilderFactory();
$varA = new Expr\Variable('a');
$varB = new Expr\Variable('b');
$varC = new Expr\Variable('c');
$this->assertEquals(
new Concat($varA, $varB),
$factory->concat($varA, $varB)
);
$this->assertEquals(
new Concat(new Concat($varA, $varB), $varC),
$factory->concat($varA, $varB, $varC)
);
$this->assertEquals(
new Concat(new Concat(new String_("a"), $varB), new String_("c")),
$factory->concat("a", $varB, "c")
);
}
public function testConcatOneError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected at least two expressions');
(new BuilderFactory())->concat("a");
}
public function testConcatInvalidExpr() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected string or Expr');
(new BuilderFactory())->concat("a", 42);
}
public function testArgs() {
$factory = new BuilderFactory();
$unpack = new Arg(new Expr\Variable('c'), false, true);
$this->assertEquals(
[
new Arg(new Expr\Variable('a')),
new Arg(new String_('b')),
$unpack
],
$factory->args([new Expr\Variable('a'), 'b', $unpack])
);
}
public function testCalls() {
$factory = new BuilderFactory();
// Simple function call
$this->assertEquals(
new Expr\FuncCall(
new Name('var_dump'),
[new Arg(new String_('str'))]
),
$factory->funcCall('var_dump', ['str'])
);
// Dynamic function call
$this->assertEquals(
new Expr\FuncCall(new Expr\Variable('fn')),
$factory->funcCall(new Expr\Variable('fn'))
);
// Simple method call
$this->assertEquals(
new Expr\MethodCall(
new Expr\Variable('obj'),
new Identifier('method'),
[new Arg(new LNumber(42))]
),
$factory->methodCall(new Expr\Variable('obj'), 'method', [42])
);
// Explicitly pass Identifier node
$this->assertEquals(
new Expr\MethodCall(
new Expr\Variable('obj'),
new Identifier('method')
),
$factory->methodCall(new Expr\Variable('obj'), new Identifier('method'))
);
// Dynamic method call
$this->assertEquals(
new Expr\MethodCall(
new Expr\Variable('obj'),
new Expr\Variable('method')
),
$factory->methodCall(new Expr\Variable('obj'), new Expr\Variable('method'))
);
// Simple static method call
$this->assertEquals(
new Expr\StaticCall(
new Name\FullyQualified('Foo'),
new Identifier('bar'),
[new Arg(new Expr\Variable('baz'))]
),
$factory->staticCall('\Foo', 'bar', [new Expr\Variable('baz')])
);
// Dynamic static method call
$this->assertEquals(
new Expr\StaticCall(
new Expr\Variable('foo'),
new Expr\Variable('bar')
),
$factory->staticCall(new Expr\Variable('foo'), new Expr\Variable('bar'))
);
// Simple new call
$this->assertEquals(
new Expr\New_(new Name\FullyQualified('stdClass')),
$factory->new('\stdClass')
);
// Dynamic new call
$this->assertEquals(
new Expr\New_(
new Expr\Variable('foo'),
[new Arg(new String_('bar'))]
),
$factory->new(new Expr\Variable('foo'), ['bar'])
);
}
public function testConstFetches() {
$factory = new BuilderFactory();
$this->assertEquals(
new Expr\ConstFetch(new Name('FOO')),
$factory->constFetch('FOO')
);
$this->assertEquals(
new Expr\ClassConstFetch(new Name('Foo'), new Identifier('BAR')),
$factory->classConstFetch('Foo', 'BAR')
);
$this->assertEquals(
new Expr\ClassConstFetch(new Expr\Variable('foo'), new Identifier('BAR')),
$factory->classConstFetch(new Expr\Variable('foo'), 'BAR')
);
}
public function testVar() {
$factory = new BuilderFactory();
$this->assertEquals(
new Expr\Variable("foo"),
$factory->var("foo")
);
$this->assertEquals(
new Expr\Variable(new Expr\Variable("foo")),
$factory->var($factory->var("foo"))
);
}
public function testPropertyFetch() {
$f = new BuilderFactory();
$this->assertEquals(
new Expr\PropertyFetch(new Expr\Variable('foo'), 'bar'),
$f->propertyFetch($f->var('foo'), 'bar')
);
$this->assertEquals(
new Expr\PropertyFetch(new Expr\Variable('foo'), 'bar'),
$f->propertyFetch($f->var('foo'), new Identifier('bar'))
);
$this->assertEquals(
new Expr\PropertyFetch(new Expr\Variable('foo'), new Expr\Variable('bar')),
$f->propertyFetch($f->var('foo'), $f->var('bar'))
);
}
public function testInvalidIdentifier() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected string or instance of Node\Identifier');
(new BuilderFactory())->classConstFetch('Foo', new Expr\Variable('foo'));
}
public function testInvalidIdentifierOrExpr() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Expected string or instance of Node\Identifier or Node\Expr');
(new BuilderFactory())->staticCall('Foo', new Name('bar'));
}
public function testInvalidNameOrExpr() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Name must be a string or an instance of Node\Name or Node\Expr');
(new BuilderFactory())->funcCall(new Node\Stmt\Return_());
}
public function testInvalidVar() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Variable name must be string or Expr');
(new BuilderFactory())->var(new Node\Stmt\Return_());
}
public function testIntegration() {
$factory = new BuilderFactory;
$node = $factory->namespace('Name\Space')
->addStmt($factory->use('Foo\Bar\SomeOtherClass'))
->addStmt($factory->use('Foo\Bar')->as('A'))
->addStmt($factory->useFunction('strlen'))
->addStmt($factory->useConst('PHP_VERSION'))
->addStmt($factory
->class('SomeClass')
->extend('SomeOtherClass')
->implement('A\Few', '\Interfaces')
->makeAbstract()
->addStmt($factory->useTrait('FirstTrait'))
->addStmt($factory->useTrait('SecondTrait', 'ThirdTrait')
->and('AnotherTrait')
->with($factory->traitUseAdaptation('foo')->as('bar'))
->with($factory->traitUseAdaptation('AnotherTrait', 'baz')->as('test'))
->with($factory->traitUseAdaptation('AnotherTrait', 'func')->insteadof('SecondTrait')))
->addStmt($factory->method('firstMethod'))
->addStmt($factory->method('someMethod')
->makePublic()
->makeAbstract()
->addParam($factory->param('someParam')->setType('SomeClass'))
->setDocComment('/**
* This method does something.
*
* @param SomeClass And takes a parameter
*/'))
->addStmt($factory->method('anotherMethod')
->makeProtected()
->addParam($factory->param('someParam')->setDefault('test'))
->addStmt(new Expr\Print_(new Expr\Variable('someParam'))))
->addStmt($factory->property('someProperty')->makeProtected())
->addStmt($factory->property('anotherProperty')
->makePrivate()
->setDefault([1, 2, 3])))
->getNode()
;
$expected = <<<'EOC'
<?php
namespace Name\Space;
use Foo\Bar\SomeOtherClass;
use Foo\Bar as A;
use function strlen;
use const PHP_VERSION;
abstract class SomeClass extends SomeOtherClass implements A\Few, \Interfaces
{
use FirstTrait;
use SecondTrait, ThirdTrait, AnotherTrait {
foo as bar;
AnotherTrait::baz as test;
AnotherTrait::func insteadof SecondTrait;
}
protected $someProperty;
private $anotherProperty = array(1, 2, 3);
function firstMethod()
{
}
/**
* This method does something.
*
* @param SomeClass And takes a parameter
*/
public abstract function someMethod(SomeClass $someParam);
protected function anotherMethod($someParam = 'test')
{
print $someParam;
}
}
EOC;
$stmts = [$node];
$prettyPrinter = new PrettyPrinter\Standard();
$generated = $prettyPrinter->prettyPrintFile($stmts);
$this->assertEquals(
str_replace("\r\n", "\n", $expected),
str_replace("\r\n", "\n", $generated)
);
}
}

View file

@ -0,0 +1,119 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
class CodeParsingTest extends CodeTestAbstract
{
/**
* @dataProvider provideTestParse
*/
public function testParse($name, $code, $expected, $modeLine) {
if (null !== $modeLine) {
$modes = array_fill_keys(explode(',', $modeLine), true);
} else {
$modes = [];
}
list($parser5, $parser7) = $this->createParsers($modes);
list($stmts5, $output5) = $this->getParseOutput($parser5, $code, $modes);
list($stmts7, $output7) = $this->getParseOutput($parser7, $code, $modes);
if (isset($modes['php5'])) {
$this->assertSame($expected, $output5, $name);
$this->assertNotSame($expected, $output7, $name);
} elseif (isset($modes['php7'])) {
$this->assertNotSame($expected, $output5, $name);
$this->assertSame($expected, $output7, $name);
} else {
$this->assertSame($expected, $output5, $name);
$this->assertSame($expected, $output7, $name);
}
$this->checkAttributes($stmts5);
$this->checkAttributes($stmts7);
}
public function createParsers(array $modes) {
$lexer = new Lexer\Emulative(['usedAttributes' => [
'startLine', 'endLine',
'startFilePos', 'endFilePos',
'startTokenPos', 'endTokenPos',
'comments'
]]);
return [
new Parser\Php5($lexer),
new Parser\Php7($lexer),
];
}
// Must be public for updateTests.php
public function getParseOutput(Parser $parser, $code, array $modes) {
$dumpPositions = isset($modes['positions']);
$errors = new ErrorHandler\Collecting;
$stmts = $parser->parse($code, $errors);
$output = '';
foreach ($errors->getErrors() as $error) {
$output .= $this->formatErrorMessage($error, $code) . "\n";
}
if (null !== $stmts) {
$dumper = new NodeDumper(['dumpComments' => true, 'dumpPositions' => $dumpPositions]);
$output .= $dumper->dump($stmts, $code);
}
return [$stmts, canonicalize($output)];
}
public function provideTestParse() {
return $this->getTests(__DIR__ . '/../code/parser', 'test');
}
private function formatErrorMessage(Error $e, $code) {
if ($e->hasColumnInfo()) {
return $e->getMessageWithColumnInfo($code);
}
return $e->getMessage();
}
private function checkAttributes($stmts) {
if ($stmts === null) {
return;
}
$traverser = new NodeTraverser();
$traverser->addVisitor(new class extends NodeVisitorAbstract {
public function enterNode(Node $node) {
$startLine = $node->getStartLine();
$endLine = $node->getEndLine();
$startFilePos = $node->getStartFilePos();
$endFilePos = $node->getEndFilePos();
$startTokenPos = $node->getStartTokenPos();
$endTokenPos = $node->getEndTokenPos();
if ($startLine < 0 || $endLine < 0 ||
$startFilePos < 0 || $endFilePos < 0 ||
$startTokenPos < 0 || $endTokenPos < 0
) {
throw new \Exception('Missing location information on ' . $node->getType());
}
if ($endLine < $startLine ||
$endFilePos < $startFilePos ||
$endTokenPos < $startTokenPos
) {
// Nops and error can have inverted order, if they are empty
if (!$node instanceof Stmt\Nop && !$node instanceof Expr\Error) {
throw new \Exception('End < start on ' . $node->getType());
}
}
}
});
$traverser->traverse($stmts);
}
}

View file

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace PhpParser;
abstract class CodeTestAbstract extends \PHPUnit\Framework\TestCase
{
protected function getTests($directory, $fileExtension, $chunksPerTest = 2) {
$parser = new CodeTestParser;
$allTests = [];
foreach (filesInDir($directory, $fileExtension) as $fileName => $fileContents) {
list($name, $tests) = $parser->parseTest($fileContents, $chunksPerTest);
// first part is the name
$name .= ' (' . $fileName . ')';
$shortName = ltrim(str_replace($directory, '', $fileName), '/\\');
// multiple sections possible with always two forming a pair
foreach ($tests as $i => list($mode, $parts)) {
$dataSetName = $shortName . (count($parts) > 1 ? '#' . $i : '');
$allTests[$dataSetName] = array_merge([$name], $parts, [$mode]);
}
}
return $allTests;
}
}

View file

@ -0,0 +1,68 @@
<?php declare(strict_types=1);
namespace PhpParser;
class CodeTestParser
{
public function parseTest($code, $chunksPerTest) {
$code = canonicalize($code);
// evaluate @@{expr}@@ expressions
$code = preg_replace_callback(
'/@@\{(.*?)\}@@/',
function($matches) {
return eval('return ' . $matches[1] . ';');
},
$code
);
// parse sections
$parts = preg_split("/\n-----(?:\n|$)/", $code);
// first part is the name
$name = array_shift($parts);
// multiple sections possible with always two forming a pair
$chunks = array_chunk($parts, $chunksPerTest);
$tests = [];
foreach ($chunks as $i => $chunk) {
$lastPart = array_pop($chunk);
list($lastPart, $mode) = $this->extractMode($lastPart);
$tests[] = [$mode, array_merge($chunk, [$lastPart])];
}
return [$name, $tests];
}
public function reconstructTest($name, array $tests) {
$result = $name;
foreach ($tests as list($mode, $parts)) {
$lastPart = array_pop($parts);
foreach ($parts as $part) {
$result .= "\n-----\n$part";
}
$result .= "\n-----\n";
if (null !== $mode) {
$result .= "!!$mode\n";
}
$result .= $lastPart;
}
return $result;
}
private function extractMode($expected) {
$firstNewLine = strpos($expected, "\n");
if (false === $firstNewLine) {
$firstNewLine = strlen($expected);
}
$firstLine = substr($expected, 0, $firstNewLine);
if (0 !== strpos($firstLine, '!!')) {
return [$expected, null];
}
$expected = (string) substr($expected, $firstNewLine + 1);
return [$expected, substr($firstLine, 2)];
}
}

View file

@ -0,0 +1,74 @@
<?php declare(strict_types=1);
namespace PhpParser;
class CommentTest extends \PHPUnit\Framework\TestCase
{
public function testGetSet() {
$comment = new Comment('/* Some comment */', 1, 10, 2);
$this->assertSame('/* Some comment */', $comment->getText());
$this->assertSame('/* Some comment */', (string) $comment);
$this->assertSame(1, $comment->getLine());
$this->assertSame(10, $comment->getFilePos());
$this->assertSame(2, $comment->getTokenPos());
}
/**
* @dataProvider provideTestReformatting
*/
public function testReformatting($commentText, $reformattedText) {
$comment = new Comment($commentText);
$this->assertSame($reformattedText, $comment->getReformattedText());
}
public function provideTestReformatting() {
return [
['// Some text' . "\n", '// Some text'],
['/* Some text */', '/* Some text */'],
[
'/**
* Some text.
* Some more text.
*/',
'/**
* Some text.
* Some more text.
*/'
],
[
'/*
Some text.
Some more text.
*/',
'/*
Some text.
Some more text.
*/'
],
[
'/* Some text.
More text.
Even more text. */',
'/* Some text.
More text.
Even more text. */'
],
[
'/* Some text.
More text.
Indented text. */',
'/* Some text.
More text.
Indented text. */',
],
// invalid comment -> no reformatting
[
'hallo
world',
'hallo
world',
],
];
}
}

View file

@ -0,0 +1,130 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
class ConstExprEvaluatorTest extends \PHPUnit\Framework\TestCase
{
/** @dataProvider provideTestEvaluate */
public function testEvaluate($exprString, $expected) {
$parser = new Parser\Php7(new Lexer());
$expr = $parser->parse('<?php ' . $exprString . ';')[0]->expr;
$evaluator = new ConstExprEvaluator();
$this->assertSame($expected, $evaluator->evaluateDirectly($expr));
}
public function provideTestEvaluate() {
return [
['1', 1],
['1.0', 1.0],
['"foo"', "foo"],
['[0, 1]', [0, 1]],
['["foo" => "bar"]', ["foo" => "bar"]],
['NULL', null],
['False', false],
['true', true],
['+1', 1],
['-1', -1],
['~0', -1],
['!true', false],
['[0][0]', 0],
['"a"[0]', "a"],
['true ? 1 : (1/0)', 1],
['false ? (1/0) : 1', 1],
['42 ?: (1/0)', 42],
['false ?: 42', 42],
['false ?? 42', false],
['null ?? 42', 42],
['[0][0] ?? 42', 0],
['[][0] ?? 42', 42],
['0b11 & 0b10', 0b10],
['0b11 | 0b10', 0b11],
['0b11 ^ 0b10', 0b01],
['1 << 2', 4],
['4 >> 2', 1],
['"a" . "b"', "ab"],
['4 + 2', 6],
['4 - 2', 2],
['4 * 2', 8],
['4 / 2', 2],
['4 % 2', 0],
['4 ** 2', 16],
['1 == 1.0', true],
['1 != 1.0', false],
['1 < 2.0', true],
['1 <= 2.0', true],
['1 > 2.0', false],
['1 >= 2.0', false],
['1 <=> 2.0', -1],
['1 === 1.0', false],
['1 !== 1.0', true],
['true && true', true],
['true and true', true],
['false && (1/0)', false],
['false and (1/0)', false],
['false || false', false],
['false or false', false],
['true || (1/0)', true],
['true or (1/0)', true],
['true xor false', true],
];
}
public function testEvaluateFails() {
$this->expectException(ConstExprEvaluationException::class);
$this->expectExceptionMessage('Expression of type Expr_Variable cannot be evaluated');
$evaluator = new ConstExprEvaluator();
$evaluator->evaluateDirectly(new Expr\Variable('a'));
}
public function testEvaluateFallback() {
$evaluator = new ConstExprEvaluator(function(Expr $expr) {
if ($expr instanceof Scalar\MagicConst\Line) {
return 42;
}
throw new ConstExprEvaluationException();
});
$expr = new Expr\BinaryOp\Plus(
new Scalar\LNumber(8),
new Scalar\MagicConst\Line()
);
$this->assertSame(50, $evaluator->evaluateDirectly($expr));
}
/**
* @dataProvider provideTestEvaluateSilently
*/
public function testEvaluateSilently($expr, $exception, $msg) {
$evaluator = new ConstExprEvaluator();
try {
$evaluator->evaluateSilently($expr);
} catch (ConstExprEvaluationException $e) {
$this->assertSame(
'An error occurred during constant expression evaluation',
$e->getMessage()
);
$prev = $e->getPrevious();
$this->assertInstanceOf($exception, $prev);
$this->assertSame($msg, $prev->getMessage());
}
}
public function provideTestEvaluateSilently() {
return [
[
new Expr\BinaryOp\Mod(new Scalar\LNumber(42), new Scalar\LNumber(0)),
\Error::class,
'Modulo by zero'
],
[
new Expr\BinaryOp\Div(new Scalar\LNumber(42), new Scalar\LNumber(0)),
\ErrorException::class,
'Division by zero'
],
];
}
}

View file

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace PhpParser\ErrorHandler;
use PhpParser\Error;
class CollectingTest extends \PHPUnit\Framework\TestCase
{
public function testHandleError() {
$errorHandler = new Collecting();
$this->assertFalse($errorHandler->hasErrors());
$this->assertEmpty($errorHandler->getErrors());
$errorHandler->handleError($e1 = new Error('Test 1'));
$errorHandler->handleError($e2 = new Error('Test 2'));
$this->assertTrue($errorHandler->hasErrors());
$this->assertSame([$e1, $e2], $errorHandler->getErrors());
$errorHandler->clearErrors();
$this->assertFalse($errorHandler->hasErrors());
$this->assertEmpty($errorHandler->getErrors());
}
}

View file

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace PhpParser\ErrorHandler;
use PhpParser\Error;
class ThrowingTest extends \PHPUnit\Framework\TestCase
{
public function testHandleError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('Test');
$errorHandler = new Throwing();
$errorHandler->handleError(new Error('Test'));
}
}

View file

@ -0,0 +1,104 @@
<?php declare(strict_types=1);
namespace PhpParser;
class ErrorTest extends \PHPUnit\Framework\TestCase
{
public function testConstruct() {
$attributes = [
'startLine' => 10,
'endLine' => 11,
];
$error = new Error('Some error', $attributes);
$this->assertSame('Some error', $error->getRawMessage());
$this->assertSame($attributes, $error->getAttributes());
$this->assertSame(10, $error->getStartLine());
$this->assertSame(11, $error->getEndLine());
$this->assertSame('Some error on line 10', $error->getMessage());
return $error;
}
/**
* @depends testConstruct
*/
public function testSetMessageAndLine(Error $error) {
$error->setRawMessage('Some other error');
$this->assertSame('Some other error', $error->getRawMessage());
$error->setStartLine(15);
$this->assertSame(15, $error->getStartLine());
$this->assertSame('Some other error on line 15', $error->getMessage());
}
public function testUnknownLine() {
$error = new Error('Some error');
$this->assertSame(-1, $error->getStartLine());
$this->assertSame(-1, $error->getEndLine());
$this->assertSame('Some error on unknown line', $error->getMessage());
}
/** @dataProvider provideTestColumnInfo */
public function testColumnInfo($code, $startPos, $endPos, $startColumn, $endColumn) {
$error = new Error('Some error', [
'startFilePos' => $startPos,
'endFilePos' => $endPos,
]);
$this->assertTrue($error->hasColumnInfo());
$this->assertSame($startColumn, $error->getStartColumn($code));
$this->assertSame($endColumn, $error->getEndColumn($code));
}
public function provideTestColumnInfo() {
return [
// Error at "bar"
["<?php foo bar baz", 10, 12, 11, 13],
["<?php\nfoo bar baz", 10, 12, 5, 7],
["<?php foo\nbar baz", 10, 12, 1, 3],
["<?php foo bar\nbaz", 10, 12, 11, 13],
["<?php\r\nfoo bar baz", 11, 13, 5, 7],
// Error at "baz"
["<?php foo bar baz", 14, 16, 15, 17],
["<?php foo bar\nbaz", 14, 16, 1, 3],
// Error at string literal
["<?php foo 'bar\nbaz' xyz", 10, 18, 11, 4],
["<?php\nfoo 'bar\nbaz' xyz", 10, 18, 5, 4],
["<?php foo\n'\nbarbaz\n'\nxyz", 10, 19, 1, 1],
// Error over full string
["<?php", 0, 4, 1, 5],
["<?\nphp", 0, 5, 1, 3],
];
}
public function testNoColumnInfo() {
$error = new Error('Some error', 3);
$this->assertFalse($error->hasColumnInfo());
try {
$error->getStartColumn('');
$this->fail('Expected RuntimeException');
} catch (\RuntimeException $e) {
$this->assertSame('Error does not have column information', $e->getMessage());
}
try {
$error->getEndColumn('');
$this->fail('Expected RuntimeException');
} catch (\RuntimeException $e) {
$this->assertSame('Error does not have column information', $e->getMessage());
}
}
public function testInvalidPosInfo() {
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Invalid position information');
$error = new Error('Some error', [
'startFilePos' => 10,
'endFilePos' => 11,
]);
$error->getStartColumn('code');
}
}

View file

@ -0,0 +1,65 @@
<?php declare(strict_types=1);
namespace PhpParser\Internal;
class DifferTest extends \PHPUnit\Framework\TestCase
{
private function formatDiffString(array $diff) {
$diffStr = '';
foreach ($diff as $diffElem) {
switch ($diffElem->type) {
case DiffElem::TYPE_KEEP:
$diffStr .= $diffElem->old;
break;
case DiffElem::TYPE_REMOVE:
$diffStr .= '-' . $diffElem->old;
break;
case DiffElem::TYPE_ADD:
$diffStr .= '+' . $diffElem->new;
break;
case DiffElem::TYPE_REPLACE:
$diffStr .= '/' . $diffElem->old . $diffElem->new;
break;
default:
assert(false);
break;
}
}
return $diffStr;
}
/** @dataProvider provideTestDiff */
public function testDiff($oldStr, $newStr, $expectedDiffStr) {
$differ = new Differ(function($a, $b) { return $a === $b; });
$diff = $differ->diff(str_split($oldStr), str_split($newStr));
$this->assertSame($expectedDiffStr, $this->formatDiffString($diff));
}
public function provideTestDiff() {
return [
['abc', 'abc', 'abc'],
['abc', 'abcdef', 'abc+d+e+f'],
['abcdef', 'abc', 'abc-d-e-f'],
['abcdef', 'abcxyzdef', 'abc+x+y+zdef'],
['axyzb', 'ab', 'a-x-y-zb'],
['abcdef', 'abxyef', 'ab-c-d+x+yef'],
['abcdef', 'cdefab', '-a-bcdef+a+b'],
];
}
/** @dataProvider provideTestDiffWithReplacements */
public function testDiffWithReplacements($oldStr, $newStr, $expectedDiffStr) {
$differ = new Differ(function($a, $b) { return $a === $b; });
$diff = $differ->diffWithReplacements(str_split($oldStr), str_split($newStr));
$this->assertSame($expectedDiffStr, $this->formatDiffString($diff));
}
public function provideTestDiffWithReplacements() {
return [
['abcde', 'axyze', 'a/bx/cy/dze'],
['abcde', 'xbcdy', '/axbcd/ey'],
['abcde', 'axye', 'a-b-c-d+x+ye'],
['abcde', 'axyzue', 'a-b-c-d+x+y+z+ue'],
];
}
}

View file

@ -0,0 +1,43 @@
<?php declare(strict_types=1);
namespace PhpParser;
class JsonDecoderTest extends \PHPUnit\Framework\TestCase
{
public function testRoundTrip() {
$code = <<<'PHP'
<?php
// comment
/** doc comment */
function functionName(&$a = 0, $b = 1.0) {
echo 'Foo';
}
PHP;
$parser = new Parser\Php7(new Lexer());
$stmts = $parser->parse($code);
$json = json_encode($stmts);
$jsonDecoder = new JsonDecoder();
$decodedStmts = $jsonDecoder->decode($json);
$this->assertEquals($stmts, $decodedStmts);
}
/** @dataProvider provideTestDecodingError */
public function testDecodingError($json, $expectedMessage) {
$jsonDecoder = new JsonDecoder();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage($expectedMessage);
$jsonDecoder->decode($json);
}
public function provideTestDecodingError() {
return [
['???', 'JSON decoding error: Syntax error'],
['{"nodeType":123}', 'Node type must be a string'],
['{"nodeType":"Name","attributes":123}', 'Attributes must be an array'],
['{"nodeType":"Comment"}', 'Comment must have text'],
['{"nodeType":"xxx"}', 'Unknown node type "xxx"'],
];
}
}

View file

@ -0,0 +1,195 @@
<?php declare(strict_types=1);
namespace PhpParser\Lexer;
use PhpParser\ErrorHandler;
use PhpParser\LexerTest;
use PhpParser\Parser\Tokens;
class EmulativeTest extends LexerTest
{
protected function getLexer(array $options = []) {
return new Emulative($options);
}
/**
* @dataProvider provideTestReplaceKeywords
*/
public function testReplaceKeywords($keyword, $expectedToken) {
$lexer = $this->getLexer();
$lexer->startLexing('<?php ' . $keyword);
$this->assertSame($expectedToken, $lexer->getNextToken());
$this->assertSame(0, $lexer->getNextToken());
}
/**
* @dataProvider provideTestReplaceKeywords
*/
public function testNoReplaceKeywordsAfterObjectOperator($keyword) {
$lexer = $this->getLexer();
$lexer->startLexing('<?php ->' . $keyword);
$this->assertSame(Tokens::T_OBJECT_OPERATOR, $lexer->getNextToken());
$this->assertSame(Tokens::T_STRING, $lexer->getNextToken());
$this->assertSame(0, $lexer->getNextToken());
}
public function provideTestReplaceKeywords() {
return [
// PHP 5.5
['finally', Tokens::T_FINALLY],
['yield', Tokens::T_YIELD],
// PHP 5.4
['callable', Tokens::T_CALLABLE],
['insteadof', Tokens::T_INSTEADOF],
['trait', Tokens::T_TRAIT],
['__TRAIT__', Tokens::T_TRAIT_C],
// PHP 5.3
['__DIR__', Tokens::T_DIR],
['goto', Tokens::T_GOTO],
['namespace', Tokens::T_NAMESPACE],
['__NAMESPACE__', Tokens::T_NS_C],
];
}
/**
* @dataProvider provideTestLexNewFeatures
*/
public function testLexNewFeatures($code, array $expectedTokens) {
$lexer = $this->getLexer();
$lexer->startLexing('<?php ' . $code);
$tokens = [];
while (0 !== $token = $lexer->getNextToken($text)) {
$tokens[] = [$token, $text];
}
$this->assertSame($expectedTokens, $tokens);
}
/**
* @dataProvider provideTestLexNewFeatures
*/
public function testLeaveStuffAloneInStrings($code) {
$stringifiedToken = '"' . addcslashes($code, '"\\') . '"';
$lexer = $this->getLexer();
$lexer->startLexing('<?php ' . $stringifiedToken);
$this->assertSame(Tokens::T_CONSTANT_ENCAPSED_STRING, $lexer->getNextToken($text));
$this->assertSame($stringifiedToken, $text);
$this->assertSame(0, $lexer->getNextToken());
}
/**
* @dataProvider provideTestLexNewFeatures
*/
public function testErrorAfterEmulation($code) {
$errorHandler = new ErrorHandler\Collecting;
$lexer = $this->getLexer([]);
$lexer->startLexing('<?php ' . $code . "\0", $errorHandler);
$errors = $errorHandler->getErrors();
$this->assertCount(1, $errors);
$error = $errors[0];
$this->assertSame('Unexpected null byte', $error->getRawMessage());
$attrs = $error->getAttributes();
$expPos = strlen('<?php ' . $code);
$expLine = 1 + substr_count('<?php ' . $code, "\n");
$this->assertSame($expPos, $attrs['startFilePos']);
$this->assertSame($expPos, $attrs['endFilePos']);
$this->assertSame($expLine, $attrs['startLine']);
$this->assertSame($expLine, $attrs['endLine']);
}
public function provideTestLexNewFeatures() {
return [
// PHP 7.4
['??=', [
[Tokens::T_COALESCE_EQUAL, '??='],
]],
['yield from', [
[Tokens::T_YIELD_FROM, 'yield from'],
]],
["yield\r\nfrom", [
[Tokens::T_YIELD_FROM, "yield\r\nfrom"],
]],
['...', [
[Tokens::T_ELLIPSIS, '...'],
]],
['**', [
[Tokens::T_POW, '**'],
]],
['**=', [
[Tokens::T_POW_EQUAL, '**='],
]],
['??', [
[Tokens::T_COALESCE, '??'],
]],
['<=>', [
[Tokens::T_SPACESHIP, '<=>'],
]],
['0b1010110', [
[Tokens::T_LNUMBER, '0b1010110'],
]],
['0b1011010101001010110101010010101011010101010101101011001110111100', [
[Tokens::T_DNUMBER, '0b1011010101001010110101010010101011010101010101101011001110111100'],
]],
['\\', [
[Tokens::T_NS_SEPARATOR, '\\'],
]],
["<<<'NOWDOC'\nNOWDOC;\n", [
[Tokens::T_START_HEREDOC, "<<<'NOWDOC'\n"],
[Tokens::T_END_HEREDOC, 'NOWDOC'],
[ord(';'), ';'],
]],
["<<<'NOWDOC'\nFoobar\nNOWDOC;\n", [
[Tokens::T_START_HEREDOC, "<<<'NOWDOC'\n"],
[Tokens::T_ENCAPSED_AND_WHITESPACE, "Foobar\n"],
[Tokens::T_END_HEREDOC, 'NOWDOC'],
[ord(';'), ';'],
]],
// Flexible heredoc/nowdoc
["<<<LABEL\nLABEL,", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_END_HEREDOC, "LABEL"],
[ord(','), ','],
]],
["<<<LABEL\n LABEL,", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_END_HEREDOC, " LABEL"],
[ord(','), ','],
]],
["<<<LABEL\n Foo\n LABEL;", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_ENCAPSED_AND_WHITESPACE, " Foo\n"],
[Tokens::T_END_HEREDOC, " LABEL"],
[ord(';'), ';'],
]],
["<<<A\n A,<<<A\n A,", [
[Tokens::T_START_HEREDOC, "<<<A\n"],
[Tokens::T_END_HEREDOC, " A"],
[ord(','), ','],
[Tokens::T_START_HEREDOC, "<<<A\n"],
[Tokens::T_END_HEREDOC, " A"],
[ord(','), ','],
]],
["<<<LABEL\nLABELNOPE\nLABEL\n", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_ENCAPSED_AND_WHITESPACE, "LABELNOPE\n"],
[Tokens::T_END_HEREDOC, "LABEL"],
]],
// Interpretation changed
["<<<LABEL\n LABEL\nLABEL\n", [
[Tokens::T_START_HEREDOC, "<<<LABEL\n"],
[Tokens::T_END_HEREDOC, " LABEL"],
[Tokens::T_STRING, "LABEL"],
]],
];
}
}

View file

@ -0,0 +1,263 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Parser\Tokens;
class LexerTest extends \PHPUnit\Framework\TestCase
{
/* To allow overwriting in parent class */
protected function getLexer(array $options = []) {
return new Lexer($options);
}
/**
* @dataProvider provideTestError
*/
public function testError($code, $messages) {
if (defined('HHVM_VERSION')) {
$this->markTestSkipped('HHVM does not throw warnings from token_get_all()');
}
$errorHandler = new ErrorHandler\Collecting();
$lexer = $this->getLexer(['usedAttributes' => [
'comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'
]]);
$lexer->startLexing($code, $errorHandler);
$errors = $errorHandler->getErrors();
$this->assertCount(count($messages), $errors);
for ($i = 0; $i < count($messages); $i++) {
$this->assertSame($messages[$i], $errors[$i]->getMessageWithColumnInfo($code));
}
}
public function provideTestError() {
return [
["<?php /*", ["Unterminated comment from 1:7 to 1:9"]],
["<?php \1", ["Unexpected character \"\1\" (ASCII 1) from 1:7 to 1:7"]],
["<?php \0", ["Unexpected null byte from 1:7 to 1:7"]],
// Error with potentially emulated token
["<?php ?? \0", ["Unexpected null byte from 1:10 to 1:10"]],
["<?php\n\0\1 foo /* bar", [
"Unexpected null byte from 2:1 to 2:1",
"Unexpected character \"\1\" (ASCII 1) from 2:2 to 2:2",
"Unterminated comment from 2:8 to 2:14"
]],
];
}
/**
* @dataProvider provideTestLex
*/
public function testLex($code, $options, $tokens) {
$lexer = $this->getLexer($options);
$lexer->startLexing($code);
while ($id = $lexer->getNextToken($value, $startAttributes, $endAttributes)) {
$token = array_shift($tokens);
$this->assertSame($token[0], $id);
$this->assertSame($token[1], $value);
$this->assertEquals($token[2], $startAttributes);
$this->assertEquals($token[3], $endAttributes);
}
}
public function provideTestLex() {
return [
// tests conversion of closing PHP tag and drop of whitespace and opening tags
[
'<?php tokens ?>plaintext',
[],
[
[
Tokens::T_STRING, 'tokens',
['startLine' => 1], ['endLine' => 1]
],
[
ord(';'), '?>',
['startLine' => 1], ['endLine' => 1]
],
[
Tokens::T_INLINE_HTML, 'plaintext',
['startLine' => 1, 'hasLeadingNewline' => false],
['endLine' => 1]
],
]
],
// tests line numbers
[
'<?php' . "\n" . '$ token /** doc' . "\n" . 'comment */ $',
[],
[
[
ord('$'), '$',
['startLine' => 2], ['endLine' => 2]
],
[
Tokens::T_STRING, 'token',
['startLine' => 2], ['endLine' => 2]
],
[
ord('$'), '$',
[
'startLine' => 3,
'comments' => [
new Comment\Doc('/** doc' . "\n" . 'comment */', 2, 14, 5),
]
],
['endLine' => 3]
],
]
],
// tests comment extraction
[
'<?php /* comment */ // comment' . "\n" . '/** docComment 1 *//** docComment 2 */ token',
[],
[
[
Tokens::T_STRING, 'token',
[
'startLine' => 2,
'comments' => [
new Comment('/* comment */', 1, 6, 1),
new Comment('// comment' . "\n", 1, 20, 3),
new Comment\Doc('/** docComment 1 */', 2, 31, 4),
new Comment\Doc('/** docComment 2 */', 2, 50, 5),
],
],
['endLine' => 2]
],
]
],
// tests differing start and end line
[
'<?php "foo' . "\n" . 'bar"',
[],
[
[
Tokens::T_CONSTANT_ENCAPSED_STRING, '"foo' . "\n" . 'bar"',
['startLine' => 1], ['endLine' => 2]
],
]
],
// tests exact file offsets
[
'<?php "a";' . "\n" . '// foo' . "\n" . '"b";',
['usedAttributes' => ['startFilePos', 'endFilePos']],
[
[
Tokens::T_CONSTANT_ENCAPSED_STRING, '"a"',
['startFilePos' => 6], ['endFilePos' => 8]
],
[
ord(';'), ';',
['startFilePos' => 9], ['endFilePos' => 9]
],
[
Tokens::T_CONSTANT_ENCAPSED_STRING, '"b"',
['startFilePos' => 18], ['endFilePos' => 20]
],
[
ord(';'), ';',
['startFilePos' => 21], ['endFilePos' => 21]
],
]
],
// tests token offsets
[
'<?php "a";' . "\n" . '// foo' . "\n" . '"b";',
['usedAttributes' => ['startTokenPos', 'endTokenPos']],
[
[
Tokens::T_CONSTANT_ENCAPSED_STRING, '"a"',
['startTokenPos' => 1], ['endTokenPos' => 1]
],
[
ord(';'), ';',
['startTokenPos' => 2], ['endTokenPos' => 2]
],
[
Tokens::T_CONSTANT_ENCAPSED_STRING, '"b"',
['startTokenPos' => 5], ['endTokenPos' => 5]
],
[
ord(';'), ';',
['startTokenPos' => 6], ['endTokenPos' => 6]
],
]
],
// tests all attributes being disabled
[
'<?php /* foo */ $bar;',
['usedAttributes' => []],
[
[
Tokens::T_VARIABLE, '$bar',
[], []
],
[
ord(';'), ';',
[], []
]
]
],
// tests no tokens
[
'',
[],
[]
],
];
}
/**
* @dataProvider provideTestHaltCompiler
*/
public function testHandleHaltCompiler($code, $remaining) {
$lexer = $this->getLexer();
$lexer->startLexing($code);
while (Tokens::T_HALT_COMPILER !== $lexer->getNextToken());
$this->assertSame($remaining, $lexer->handleHaltCompiler());
$this->assertSame(0, $lexer->getNextToken());
}
public function provideTestHaltCompiler() {
return [
['<?php ... __halt_compiler();Remaining Text', 'Remaining Text'],
['<?php ... __halt_compiler ( ) ;Remaining Text', 'Remaining Text'],
['<?php ... __halt_compiler() ?>Remaining Text', 'Remaining Text'],
//array('<?php ... __halt_compiler();' . "\0", "\0"),
//array('<?php ... __halt_compiler /* */ ( ) ;Remaining Text', 'Remaining Text'),
];
}
public function testHandleHaltCompilerError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('__HALT_COMPILER must be followed by "();"');
$lexer = $this->getLexer();
$lexer->startLexing('<?php ... __halt_compiler invalid ();');
while (Tokens::T_HALT_COMPILER !== $lexer->getNextToken());
$lexer->handleHaltCompiler();
}
public function testGetTokens() {
$code = '<?php "a";' . "\n" . '// foo' . "\n" . '"b";';
$expectedTokens = [
[T_OPEN_TAG, '<?php ', 1],
[T_CONSTANT_ENCAPSED_STRING, '"a"', 1],
';',
[T_WHITESPACE, "\n", 1],
[T_COMMENT, '// foo' . "\n", 2],
[T_CONSTANT_ENCAPSED_STRING, '"b"', 3],
';',
];
$lexer = $this->getLexer();
$lexer->startLexing($code);
$this->assertSame($expectedTokens, $lexer->getTokens());
}
}

View file

@ -0,0 +1,65 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Use_;
class NameContextTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideTestGetPossibleNames
*/
public function testGetPossibleNames($type, $name, $expectedPossibleNames) {
$nameContext = new NameContext(new ErrorHandler\Throwing());
$nameContext->startNamespace(new Name('NS'));
$nameContext->addAlias(new Name('Foo'), 'Foo', Use_::TYPE_NORMAL);
$nameContext->addAlias(new Name('Foo\Bar'), 'Alias', Use_::TYPE_NORMAL);
$nameContext->addAlias(new Name('Foo\fn'), 'fn', Use_::TYPE_FUNCTION);
$nameContext->addAlias(new Name('Foo\CN'), 'CN', Use_::TYPE_CONSTANT);
$possibleNames = $nameContext->getPossibleNames($name, $type);
$possibleNames = array_map(function (Name $name) {
return $name->toCodeString();
}, $possibleNames);
$this->assertSame($expectedPossibleNames, $possibleNames);
// Here the last name is always the shortest one
$expectedShortName = $expectedPossibleNames[count($expectedPossibleNames) - 1];
$this->assertSame(
$expectedShortName,
$nameContext->getShortName($name, $type)->toCodeString()
);
}
public function provideTestGetPossibleNames() {
return [
[Use_::TYPE_NORMAL, 'Test', ['\Test']],
[Use_::TYPE_NORMAL, 'Test\Namespaced', ['\Test\Namespaced']],
[Use_::TYPE_NORMAL, 'NS\Test', ['\NS\Test', 'Test']],
[Use_::TYPE_NORMAL, 'ns\Test', ['\ns\Test', 'Test']],
[Use_::TYPE_NORMAL, 'NS\Foo\Bar', ['\NS\Foo\Bar']],
[Use_::TYPE_NORMAL, 'ns\foo\Bar', ['\ns\foo\Bar']],
[Use_::TYPE_NORMAL, 'Foo', ['\Foo', 'Foo']],
[Use_::TYPE_NORMAL, 'Foo\Bar', ['\Foo\Bar', 'Foo\Bar', 'Alias']],
[Use_::TYPE_NORMAL, 'Foo\Bar\Baz', ['\Foo\Bar\Baz', 'Foo\Bar\Baz', 'Alias\Baz']],
[Use_::TYPE_NORMAL, 'Foo\fn\Bar', ['\Foo\fn\Bar', 'Foo\fn\Bar']],
[Use_::TYPE_FUNCTION, 'Foo\fn\bar', ['\Foo\fn\bar', 'Foo\fn\bar']],
[Use_::TYPE_FUNCTION, 'Foo\fn', ['\Foo\fn', 'Foo\fn', 'fn']],
[Use_::TYPE_FUNCTION, 'Foo\FN', ['\Foo\FN', 'Foo\FN', 'fn']],
[Use_::TYPE_CONSTANT, 'Foo\CN\BAR', ['\Foo\CN\BAR', 'Foo\CN\BAR']],
[Use_::TYPE_CONSTANT, 'Foo\CN', ['\Foo\CN', 'Foo\CN', 'CN']],
[Use_::TYPE_CONSTANT, 'foo\CN', ['\foo\CN', 'Foo\CN', 'CN']],
[Use_::TYPE_CONSTANT, 'foo\cn', ['\foo\cn', 'Foo\cn']],
// self/parent/static must not be fully qualified
[Use_::TYPE_NORMAL, 'self', ['self']],
[Use_::TYPE_NORMAL, 'parent', ['parent']],
[Use_::TYPE_NORMAL, 'static', ['static']],
// true/false/null do not need to be fully qualified, even in namespaces
[Use_::TYPE_CONSTANT, 'true', ['\true', 'true']],
[Use_::TYPE_CONSTANT, 'false', ['\false', 'false']],
[Use_::TYPE_CONSTANT, 'null', ['\null', 'null']],
];
}
}

View file

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace PhpParser\Node;
class IdentifierTest extends \PHPUnit\Framework\TestCase
{
public function testToString() {
$identifier = new Identifier('Foo');
$this->assertSame('Foo', (string) $identifier);
$this->assertSame('Foo', $identifier->toString());
$this->assertSame('foo', $identifier->toLowerString());
}
/** @dataProvider provideTestIsSpecialClassName */
public function testIsSpecialClassName($identifier, $expected) {
$identifier = new Identifier($identifier);
$this->assertSame($expected, $identifier->isSpecialClassName());
}
public function provideTestIsSpecialClassName() {
return [
['self', true],
['PARENT', true],
['Static', true],
['other', false],
];
}
}

View file

@ -0,0 +1,157 @@
<?php declare(strict_types=1);
namespace PhpParser\Node;
class NameTest extends \PHPUnit\Framework\TestCase
{
public function testConstruct() {
$name = new Name(['foo', 'bar']);
$this->assertSame(['foo', 'bar'], $name->parts);
$name = new Name('foo\bar');
$this->assertSame(['foo', 'bar'], $name->parts);
$name = new Name($name);
$this->assertSame(['foo', 'bar'], $name->parts);
}
public function testGet() {
$name = new Name('foo');
$this->assertSame('foo', $name->getFirst());
$this->assertSame('foo', $name->getLast());
$name = new Name('foo\bar');
$this->assertSame('foo', $name->getFirst());
$this->assertSame('bar', $name->getLast());
}
public function testToString() {
$name = new Name('Foo\Bar');
$this->assertSame('Foo\Bar', (string) $name);
$this->assertSame('Foo\Bar', $name->toString());
$this->assertSame('foo\bar', $name->toLowerString());
}
public function testSlice() {
$name = new Name('foo\bar\baz');
$this->assertEquals(new Name('foo\bar\baz'), $name->slice(0));
$this->assertEquals(new Name('bar\baz'), $name->slice(1));
$this->assertNull($name->slice(3));
$this->assertEquals(new Name('foo\bar\baz'), $name->slice(-3));
$this->assertEquals(new Name('bar\baz'), $name->slice(-2));
$this->assertEquals(new Name('foo\bar'), $name->slice(0, -1));
$this->assertNull($name->slice(0, -3));
$this->assertEquals(new Name('bar'), $name->slice(1, -1));
$this->assertNull($name->slice(1, -2));
$this->assertEquals(new Name('bar'), $name->slice(-2, 1));
$this->assertEquals(new Name('bar'), $name->slice(-2, -1));
$this->assertNull($name->slice(-2, -2));
}
public function testSliceOffsetTooLarge() {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Offset 4 is out of bounds');
(new Name('foo\bar\baz'))->slice(4);
}
public function testSliceOffsetTooSmall() {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Offset -4 is out of bounds');
(new Name('foo\bar\baz'))->slice(-4);
}
public function testSliceLengthTooLarge() {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Length 4 is out of bounds');
(new Name('foo\bar\baz'))->slice(0, 4);
}
public function testSliceLengthTooSmall() {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Length -4 is out of bounds');
(new Name('foo\bar\baz'))->slice(0, -4);
}
public function testConcat() {
$this->assertEquals(new Name('foo\bar\baz'), Name::concat('foo', 'bar\baz'));
$this->assertEquals(
new Name\FullyQualified('foo\bar'),
Name\FullyQualified::concat(['foo'], new Name('bar'))
);
$attributes = ['foo' => 'bar'];
$this->assertEquals(
new Name\Relative('foo\bar\baz', $attributes),
Name\Relative::concat(new Name\FullyQualified('foo\bar'), 'baz', $attributes)
);
$this->assertEquals(new Name('foo'), Name::concat(null, 'foo'));
$this->assertEquals(new Name('foo'), Name::concat('foo', null));
$this->assertNull(Name::concat(null, null));
}
public function testNameTypes() {
$name = new Name('foo');
$this->assertTrue($name->isUnqualified());
$this->assertFalse($name->isQualified());
$this->assertFalse($name->isFullyQualified());
$this->assertFalse($name->isRelative());
$this->assertSame('foo', $name->toCodeString());
$name = new Name('foo\bar');
$this->assertFalse($name->isUnqualified());
$this->assertTrue($name->isQualified());
$this->assertFalse($name->isFullyQualified());
$this->assertFalse($name->isRelative());
$this->assertSame('foo\bar', $name->toCodeString());
$name = new Name\FullyQualified('foo');
$this->assertFalse($name->isUnqualified());
$this->assertFalse($name->isQualified());
$this->assertTrue($name->isFullyQualified());
$this->assertFalse($name->isRelative());
$this->assertSame('\foo', $name->toCodeString());
$name = new Name\Relative('foo');
$this->assertFalse($name->isUnqualified());
$this->assertFalse($name->isQualified());
$this->assertFalse($name->isFullyQualified());
$this->assertTrue($name->isRelative());
$this->assertSame('namespace\foo', $name->toCodeString());
}
public function testInvalidArg() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Expected string, array of parts or Name instance');
Name::concat('foo', new \stdClass);
}
public function testInvalidEmptyString() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Name cannot be empty');
new Name('');
}
public function testInvalidEmptyArray() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Name cannot be empty');
new Name([]);
}
/** @dataProvider provideTestIsSpecialClassName */
public function testIsSpecialClassName($name, $expected) {
$name = new Name($name);
$this->assertSame($expected, $name->isSpecialClassName());
}
public function provideTestIsSpecialClassName() {
return [
['self', true],
['PARENT', true],
['Static', true],
['self\not', false],
['not\self', false],
];
}
}

View file

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Scalar;
class MagicConstTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideTestGetName
*/
public function testGetName(MagicConst $magicConst, $name) {
$this->assertSame($name, $magicConst->getName());
}
public function provideTestGetName() {
return [
[new MagicConst\Class_, '__CLASS__'],
[new MagicConst\Dir, '__DIR__'],
[new MagicConst\File, '__FILE__'],
[new MagicConst\Function_, '__FUNCTION__'],
[new MagicConst\Line, '__LINE__'],
[new MagicConst\Method, '__METHOD__'],
[new MagicConst\Namespace_, '__NAMESPACE__'],
[new MagicConst\Trait_, '__TRAIT__'],
];
}
}

View file

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Scalar;
class StringTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideTestParseEscapeSequences
*/
public function testParseEscapeSequences($expected, $string, $quote) {
$this->assertSame(
$expected,
String_::parseEscapeSequences($string, $quote)
);
}
/**
* @dataProvider provideTestParse
*/
public function testCreate($expected, $string) {
$this->assertSame(
$expected,
String_::parse($string)
);
}
public function provideTestParseEscapeSequences() {
return [
['"', '\\"', '"'],
['\\"', '\\"', '`'],
['\\"\\`', '\\"\\`', null],
["\\\$\n\r\t\f\v", '\\\\\$\n\r\t\f\v', null],
["\x1B", '\e', null],
[chr(255), '\xFF', null],
[chr(255), '\377', null],
[chr(0), '\400', null],
["\0", '\0', null],
['\xFF', '\\\\xFF', null],
];
}
public function provideTestParse() {
$tests = [
['A', '\'A\''],
['A', 'b\'A\''],
['A', '"A"'],
['A', 'b"A"'],
['\\', '\'\\\\\''],
['\'', '\'\\\'\''],
];
foreach ($this->provideTestParseEscapeSequences() as $i => $test) {
// skip second and third tests, they aren't for double quotes
if ($i !== 1 && $i !== 2) {
$tests[] = [$test[0], '"' . $test[1] . '"'];
}
}
return $tests;
}
}

View file

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Stmt;
class ClassConstTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideModifiers
*/
public function testModifiers($modifier) {
$node = new ClassConst(
[], // invalid
constant('PhpParser\Node\Stmt\Class_::MODIFIER_' . strtoupper($modifier))
);
$this->assertTrue($node->{'is' . $modifier}());
}
public function testNoModifiers() {
$node = new ClassConst([], 0);
$this->assertTrue($node->isPublic());
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
}
public function provideModifiers() {
return [
['public'],
['protected'],
['private'],
];
}
}

View file

@ -0,0 +1,123 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Stmt;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
class ClassMethodTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideModifiers
*/
public function testModifiers($modifier) {
$node = new ClassMethod('foo', [
'type' => constant('PhpParser\Node\Stmt\Class_::MODIFIER_' . strtoupper($modifier))
]);
$this->assertTrue($node->{'is' . $modifier}());
}
public function testNoModifiers() {
$node = new ClassMethod('foo', ['type' => 0]);
$this->assertTrue($node->isPublic());
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertFalse($node->isAbstract());
$this->assertFalse($node->isFinal());
$this->assertFalse($node->isStatic());
$this->assertFalse($node->isMagic());
}
public function provideModifiers() {
return [
['public'],
['protected'],
['private'],
['abstract'],
['final'],
['static'],
];
}
/**
* Checks that implicit public modifier detection for method is working
*
* @dataProvider implicitPublicModifiers
*
* @param string $modifier Node type modifier
*/
public function testImplicitPublic(string $modifier)
{
$node = new ClassMethod('foo', [
'type' => constant('PhpParser\Node\Stmt\Class_::MODIFIER_' . strtoupper($modifier))
]);
$this->assertTrue($node->isPublic(), 'Node should be implicitly public');
}
public function implicitPublicModifiers() {
return [
['abstract'],
['final'],
['static'],
];
}
/**
* @dataProvider provideMagics
*
* @param string $name Node name
*/
public function testMagic(string $name) {
$node = new ClassMethod($name);
$this->assertTrue($node->isMagic(), 'Method should be magic');
}
public function provideMagics() {
return [
['__construct'],
['__DESTRUCT'],
['__caLL'],
['__callstatic'],
['__get'],
['__set'],
['__isset'],
['__unset'],
['__sleep'],
['__wakeup'],
['__tostring'],
['__set_state'],
['__clone'],
['__invoke'],
['__debuginfo'],
];
}
public function testFunctionLike() {
$param = new Param(new Variable('a'));
$type = new Name('Foo');
$return = new Return_(new Variable('a'));
$method = new ClassMethod('test', [
'byRef' => false,
'params' => [$param],
'returnType' => $type,
'stmts' => [$return],
]);
$this->assertFalse($method->returnsByRef());
$this->assertSame([$param], $method->getParams());
$this->assertSame($type, $method->getReturnType());
$this->assertSame([$return], $method->getStmts());
$method = new ClassMethod('test', [
'byRef' => true,
'stmts' => null,
]);
$this->assertTrue($method->returnsByRef());
$this->assertNull($method->getStmts());
}
}

View file

@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Stmt;
class ClassTest extends \PHPUnit\Framework\TestCase
{
public function testIsAbstract() {
$class = new Class_('Foo', ['type' => Class_::MODIFIER_ABSTRACT]);
$this->assertTrue($class->isAbstract());
$class = new Class_('Foo');
$this->assertFalse($class->isAbstract());
}
public function testIsFinal() {
$class = new Class_('Foo', ['type' => Class_::MODIFIER_FINAL]);
$this->assertTrue($class->isFinal());
$class = new Class_('Foo');
$this->assertFalse($class->isFinal());
}
public function testGetMethods() {
$methods = [
new ClassMethod('foo'),
new ClassMethod('bar'),
new ClassMethod('fooBar'),
];
$class = new Class_('Foo', [
'stmts' => [
new TraitUse([]),
$methods[0],
new ClassConst([]),
$methods[1],
new Property(0, []),
$methods[2],
]
]);
$this->assertSame($methods, $class->getMethods());
}
public function testGetMethod() {
$methodConstruct = new ClassMethod('__CONSTRUCT');
$methodTest = new ClassMethod('test');
$class = new Class_('Foo', [
'stmts' => [
new ClassConst([]),
$methodConstruct,
new Property(0, []),
$methodTest,
]
]);
$this->assertSame($methodConstruct, $class->getMethod('__construct'));
$this->assertSame($methodTest, $class->getMethod('test'));
$this->assertNull($class->getMethod('nonExisting'));
}
}

View file

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Stmt;
use PhpParser\Node;
class InterfaceTest extends \PHPUnit\Framework\TestCase
{
public function testGetMethods() {
$methods = [
new ClassMethod('foo'),
new ClassMethod('bar'),
];
$interface = new Class_('Foo', [
'stmts' => [
new Node\Stmt\ClassConst([new Node\Const_('C1', new Node\Scalar\String_('C1'))]),
$methods[0],
new Node\Stmt\ClassConst([new Node\Const_('C2', new Node\Scalar\String_('C2'))]),
$methods[1],
new Node\Stmt\ClassConst([new Node\Const_('C3', new Node\Scalar\String_('C3'))]),
]
]);
$this->assertSame($methods, $interface->getMethods());
}
}

View file

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace PhpParser\Node\Stmt;
class PropertyTest extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideModifiers
*/
public function testModifiers($modifier) {
$node = new Property(
constant('PhpParser\Node\Stmt\Class_::MODIFIER_' . strtoupper($modifier)),
[] // invalid
);
$this->assertTrue($node->{'is' . $modifier}());
}
public function testNoModifiers() {
$node = new Property(0, []);
$this->assertTrue($node->isPublic());
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertFalse($node->isStatic());
}
public function testStaticImplicitlyPublic() {
$node = new Property(Class_::MODIFIER_STATIC, []);
$this->assertTrue($node->isPublic());
$this->assertFalse($node->isProtected());
$this->assertFalse($node->isPrivate());
$this->assertTrue($node->isStatic());
}
public function provideModifiers() {
return [
['public'],
['protected'],
['private'],
['static'],
];
}
}

View file

@ -0,0 +1,325 @@
<?php declare(strict_types=1);
namespace PhpParser;
class DummyNode extends NodeAbstract
{
public $subNode1;
public $subNode2;
public function __construct($subNode1, $subNode2, $attributes) {
parent::__construct($attributes);
$this->subNode1 = $subNode1;
$this->subNode2 = $subNode2;
}
public function getSubNodeNames() : array {
return ['subNode1', 'subNode2'];
}
// This method is only overwritten because the node is located in an unusual namespace
public function getType() : string {
return 'Dummy';
}
}
class NodeAbstractTest extends \PHPUnit\Framework\TestCase
{
public function provideNodes() {
$attributes = [
'startLine' => 10,
'endLine' => 11,
'startTokenPos' => 12,
'endTokenPos' => 13,
'startFilePos' => 14,
'endFilePos' => 15,
'comments' => [
new Comment('// Comment' . "\n"),
new Comment\Doc('/** doc comment */'),
],
];
$node = new DummyNode('value1', 'value2', $attributes);
$node->notSubNode = 'value3';
return [
[$attributes, $node],
];
}
/**
* @dataProvider provideNodes
*/
public function testConstruct(array $attributes, Node $node) {
$this->assertSame('Dummy', $node->getType());
$this->assertSame(['subNode1', 'subNode2'], $node->getSubNodeNames());
$this->assertSame(10, $node->getLine());
$this->assertSame(10, $node->getStartLine());
$this->assertSame(11, $node->getEndLine());
$this->assertSame(12, $node->getStartTokenPos());
$this->assertSame(13, $node->getEndTokenPos());
$this->assertSame(14, $node->getStartFilePos());
$this->assertSame(15, $node->getEndFilePos());
$this->assertSame('/** doc comment */', $node->getDocComment()->getText());
$this->assertSame('value1', $node->subNode1);
$this->assertSame('value2', $node->subNode2);
$this->assertObjectHasAttribute('subNode1', $node);
$this->assertObjectHasAttribute('subNode2', $node);
$this->assertObjectNotHasAttribute('subNode3', $node);
$this->assertSame($attributes, $node->getAttributes());
$this->assertSame($attributes['comments'], $node->getComments());
return $node;
}
/**
* @dataProvider provideNodes
*/
public function testGetDocComment(array $attributes, Node $node) {
$this->assertSame('/** doc comment */', $node->getDocComment()->getText());
$comments = $node->getComments();
array_pop($comments); // remove doc comment
$node->setAttribute('comments', $comments);
$this->assertNull($node->getDocComment());
array_pop($comments); // remove comment
$node->setAttribute('comments', $comments);
$this->assertNull($node->getDocComment());
}
public function testSetDocComment() {
$node = new DummyNode(null, null, []);
// Add doc comment to node without comments
$docComment = new Comment\Doc('/** doc */');
$node->setDocComment($docComment);
$this->assertSame($docComment, $node->getDocComment());
// Replace it
$docComment = new Comment\Doc('/** doc 2 */');
$node->setDocComment($docComment);
$this->assertSame($docComment, $node->getDocComment());
// Add docmment to node with other comments
$c1 = new Comment('/* foo */');
$c2 = new Comment('/* bar */');
$docComment = new Comment\Doc('/** baz */');
$node->setAttribute('comments', [$c1, $c2]);
$node->setDocComment($docComment);
$this->assertSame([$c1, $c2, $docComment], $node->getAttribute('comments'));
}
/**
* @dataProvider provideNodes
*/
public function testChange(array $attributes, Node $node) {
// direct modification
$node->subNode = 'newValue';
$this->assertSame('newValue', $node->subNode);
// indirect modification
$subNode =& $node->subNode;
$subNode = 'newNewValue';
$this->assertSame('newNewValue', $node->subNode);
// removal
unset($node->subNode);
$this->assertObjectNotHasAttribute('subNode', $node);
}
/**
* @dataProvider provideNodes
*/
public function testIteration(array $attributes, Node $node) {
// Iteration is simple object iteration over properties,
// not over subnodes
$i = 0;
foreach ($node as $key => $value) {
if ($i === 0) {
$this->assertSame('subNode1', $key);
$this->assertSame('value1', $value);
} elseif ($i === 1) {
$this->assertSame('subNode2', $key);
$this->assertSame('value2', $value);
} elseif ($i === 2) {
$this->assertSame('notSubNode', $key);
$this->assertSame('value3', $value);
} else {
throw new \Exception;
}
$i++;
}
$this->assertSame(3, $i);
}
public function testAttributes() {
/** @var $node Node */
$node = $this->getMockForAbstractClass(NodeAbstract::class);
$this->assertEmpty($node->getAttributes());
$node->setAttribute('key', 'value');
$this->assertTrue($node->hasAttribute('key'));
$this->assertSame('value', $node->getAttribute('key'));
$this->assertFalse($node->hasAttribute('doesNotExist'));
$this->assertNull($node->getAttribute('doesNotExist'));
$this->assertSame('default', $node->getAttribute('doesNotExist', 'default'));
$node->setAttribute('null', null);
$this->assertTrue($node->hasAttribute('null'));
$this->assertNull($node->getAttribute('null'));
$this->assertNull($node->getAttribute('null', 'default'));
$this->assertSame(
[
'key' => 'value',
'null' => null,
],
$node->getAttributes()
);
$node->setAttributes(
[
'a' => 'b',
'c' => null,
]
);
$this->assertSame(
[
'a' => 'b',
'c' => null,
],
$node->getAttributes()
);
}
public function testJsonSerialization() {
$code = <<<'PHP'
<?php
// comment
/** doc comment */
function functionName(&$a = 0, $b = 1.0) {
echo 'Foo';
}
PHP;
$expected = <<<'JSON'
[
{
"nodeType": "Stmt_Function",
"byRef": false,
"name": {
"nodeType": "Identifier",
"name": "functionName",
"attributes": {
"startLine": 4,
"endLine": 4
}
},
"params": [
{
"nodeType": "Param",
"type": null,
"byRef": true,
"variadic": false,
"var": {
"nodeType": "Expr_Variable",
"name": "a",
"attributes": {
"startLine": 4,
"endLine": 4
}
},
"default": {
"nodeType": "Scalar_LNumber",
"value": 0,
"attributes": {
"startLine": 4,
"endLine": 4,
"kind": 10
}
},
"attributes": {
"startLine": 4,
"endLine": 4
}
},
{
"nodeType": "Param",
"type": null,
"byRef": false,
"variadic": false,
"var": {
"nodeType": "Expr_Variable",
"name": "b",
"attributes": {
"startLine": 4,
"endLine": 4
}
},
"default": {
"nodeType": "Scalar_DNumber",
"value": 1,
"attributes": {
"startLine": 4,
"endLine": 4
}
},
"attributes": {
"startLine": 4,
"endLine": 4
}
}
],
"returnType": null,
"stmts": [
{
"nodeType": "Stmt_Echo",
"exprs": [
{
"nodeType": "Scalar_String",
"value": "Foo",
"attributes": {
"startLine": 5,
"endLine": 5,
"kind": 1
}
}
],
"attributes": {
"startLine": 5,
"endLine": 5
}
}
],
"attributes": {
"startLine": 4,
"comments": [
{
"nodeType": "Comment",
"text": "\/\/ comment\n",
"line": 2,
"filePos": 6,
"tokenPos": 1
},
{
"nodeType": "Comment_Doc",
"text": "\/** doc comment *\/",
"line": 3,
"filePos": 17,
"tokenPos": 2
}
],
"endLine": 6
}
}
]
JSON;
$parser = new Parser\Php7(new Lexer());
$stmts = $parser->parse(canonicalize($code));
$json = json_encode($stmts, JSON_PRETTY_PRINT);
$this->assertEquals(canonicalize($expected), canonicalize($json));
}
}

View file

@ -0,0 +1,105 @@
<?php declare(strict_types=1);
namespace PhpParser;
class NodeDumperTest extends \PHPUnit\Framework\TestCase
{
private function canonicalize($string) {
return str_replace("\r\n", "\n", $string);
}
/**
* @dataProvider provideTestDump
*/
public function testDump($node, $dump) {
$dumper = new NodeDumper;
$this->assertSame($this->canonicalize($dump), $this->canonicalize($dumper->dump($node)));
}
public function provideTestDump() {
return [
[
[],
'array(
)'
],
[
['Foo', 'Bar', 'Key' => 'FooBar'],
'array(
0: Foo
1: Bar
Key: FooBar
)'
],
[
new Node\Name(['Hallo', 'World']),
'Name(
parts: array(
0: Hallo
1: World
)
)'
],
[
new Node\Expr\Array_([
new Node\Expr\ArrayItem(new Node\Scalar\String_('Foo'))
]),
'Expr_Array(
items: array(
0: Expr_ArrayItem(
key: null
value: Scalar_String(
value: Foo
)
byRef: false
)
)
)'
],
];
}
public function testDumpWithPositions() {
$parser = (new ParserFactory)->create(
ParserFactory::ONLY_PHP7,
new Lexer(['usedAttributes' => ['startLine', 'endLine', 'startFilePos', 'endFilePos']])
);
$dumper = new NodeDumper(['dumpPositions' => true]);
$code = "<?php\n\$a = 1;\necho \$a;";
$expected = <<<'OUT'
array(
0: Stmt_Expression[2:1 - 2:7](
expr: Expr_Assign[2:1 - 2:6](
var: Expr_Variable[2:1 - 2:2](
name: a
)
expr: Scalar_LNumber[2:6 - 2:6](
value: 1
)
)
)
1: Stmt_Echo[3:1 - 3:8](
exprs: array(
0: Expr_Variable[3:6 - 3:7](
name: a
)
)
)
)
OUT;
$stmts = $parser->parse($code);
$dump = $dumper->dump($stmts, $code);
$this->assertSame($this->canonicalize($expected), $this->canonicalize($dump));
}
public function testError() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Can only dump nodes and arrays.');
$dumper = new NodeDumper;
$dumper->dump(new \stdClass);
}
}

View file

@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Node\Expr;
class NodeFinderTest extends \PHPUnit\Framework\TestCase
{
private function getStmtsAndVars() {
$assign = new Expr\Assign(new Expr\Variable('a'), new Expr\BinaryOp\Concat(
new Expr\Variable('b'), new Expr\Variable('c')
));
$stmts = [new Node\Stmt\Expression($assign)];
$vars = [$assign->var, $assign->expr->left, $assign->expr->right];
return [$stmts, $vars];
}
public function testFind() {
$finder = new NodeFinder;
list($stmts, $vars) = $this->getStmtsAndVars();
$varFilter = function(Node $node) {
return $node instanceof Expr\Variable;
};
$this->assertSame($vars, $finder->find($stmts, $varFilter));
$this->assertSame($vars, $finder->find($stmts[0], $varFilter));
$noneFilter = function () { return false; };
$this->assertSame([], $finder->find($stmts, $noneFilter));
}
public function testFindInstanceOf() {
$finder = new NodeFinder;
list($stmts, $vars) = $this->getStmtsAndVars();
$this->assertSame($vars, $finder->findInstanceOf($stmts, Expr\Variable::class));
$this->assertSame($vars, $finder->findInstanceOf($stmts[0], Expr\Variable::class));
$this->assertSame([], $finder->findInstanceOf($stmts, Expr\BinaryOp\Mul::class));
}
public function testFindFirst() {
$finder = new NodeFinder;
list($stmts, $vars) = $this->getStmtsAndVars();
$varFilter = function(Node $node) {
return $node instanceof Expr\Variable;
};
$this->assertSame($vars[0], $finder->findFirst($stmts, $varFilter));
$this->assertSame($vars[0], $finder->findFirst($stmts[0], $varFilter));
$noneFilter = function () { return false; };
$this->assertNull($finder->findFirst($stmts, $noneFilter));
}
public function testFindFirstInstanceOf() {
$finder = new NodeFinder;
list($stmts, $vars) = $this->getStmtsAndVars();
$this->assertSame($vars[0], $finder->findFirstInstanceOf($stmts, Expr\Variable::class));
$this->assertSame($vars[0], $finder->findFirstInstanceOf($stmts[0], Expr\Variable::class));
$this->assertNull($finder->findFirstInstanceOf($stmts, Expr\BinaryOp\Mul::class));
}
}

View file

@ -0,0 +1,344 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar\String_;
class NodeTraverserTest extends \PHPUnit\Framework\TestCase
{
public function testNonModifying() {
$str1Node = new String_('Foo');
$str2Node = new String_('Bar');
$echoNode = new Node\Stmt\Echo_([$str1Node, $str2Node]);
$stmts = [$echoNode];
$visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor->expects($this->at(0))->method('beforeTraverse')->with($stmts);
$visitor->expects($this->at(1))->method('enterNode')->with($echoNode);
$visitor->expects($this->at(2))->method('enterNode')->with($str1Node);
$visitor->expects($this->at(3))->method('leaveNode')->with($str1Node);
$visitor->expects($this->at(4))->method('enterNode')->with($str2Node);
$visitor->expects($this->at(5))->method('leaveNode')->with($str2Node);
$visitor->expects($this->at(6))->method('leaveNode')->with($echoNode);
$visitor->expects($this->at(7))->method('afterTraverse')->with($stmts);
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$this->assertEquals($stmts, $traverser->traverse($stmts));
}
public function testModifying() {
$str1Node = new String_('Foo');
$str2Node = new String_('Bar');
$printNode = new Expr\Print_($str1Node);
// first visitor changes the node, second verifies the change
$visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
// replace empty statements with string1 node
$visitor1->expects($this->at(0))->method('beforeTraverse')->with([])
->willReturn([$str1Node]);
$visitor2->expects($this->at(0))->method('beforeTraverse')->with([$str1Node]);
// replace string1 node with print node
$visitor1->expects($this->at(1))->method('enterNode')->with($str1Node)
->willReturn($printNode);
$visitor2->expects($this->at(1))->method('enterNode')->with($printNode);
// replace string1 node with string2 node
$visitor1->expects($this->at(2))->method('enterNode')->with($str1Node)
->willReturn($str2Node);
$visitor2->expects($this->at(2))->method('enterNode')->with($str2Node);
// replace string2 node with string1 node again
$visitor1->expects($this->at(3))->method('leaveNode')->with($str2Node)
->willReturn($str1Node);
$visitor2->expects($this->at(3))->method('leaveNode')->with($str1Node);
// replace print node with string1 node again
$visitor1->expects($this->at(4))->method('leaveNode')->with($printNode)
->willReturn($str1Node);
$visitor2->expects($this->at(4))->method('leaveNode')->with($str1Node);
// replace string1 node with empty statements again
$visitor1->expects($this->at(5))->method('afterTraverse')->with([$str1Node])
->willReturn([]);
$visitor2->expects($this->at(5))->method('afterTraverse')->with([]);
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor1);
$traverser->addVisitor($visitor2);
// as all operations are reversed we end where we start
$this->assertEquals([], $traverser->traverse([]));
}
public function testRemove() {
$str1Node = new String_('Foo');
$str2Node = new String_('Bar');
$visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
// remove the string1 node, leave the string2 node
$visitor->expects($this->at(2))->method('leaveNode')->with($str1Node)
->willReturn(NodeTraverser::REMOVE_NODE);
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$this->assertEquals([$str2Node], $traverser->traverse([$str1Node, $str2Node]));
}
public function testMerge() {
$strStart = new String_('Start');
$strMiddle = new String_('End');
$strEnd = new String_('Middle');
$strR1 = new String_('Replacement 1');
$strR2 = new String_('Replacement 2');
$visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
// replace strMiddle with strR1 and strR2 by merge
$visitor->expects($this->at(4))->method('leaveNode')->with($strMiddle)
->willReturn([$strR1, $strR2]);
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$this->assertEquals(
[$strStart, $strR1, $strR2, $strEnd],
$traverser->traverse([$strStart, $strMiddle, $strEnd])
);
}
public function testInvalidDeepArray() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Invalid node structure: Contains nested arrays');
$strNode = new String_('Foo');
$stmts = [[[$strNode]]];
$traverser = new NodeTraverser;
$this->assertEquals($stmts, $traverser->traverse($stmts));
}
public function testDontTraverseChildren() {
$strNode = new String_('str');
$printNode = new Expr\Print_($strNode);
$varNode = new Expr\Variable('foo');
$mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
$negNode = new Expr\UnaryMinus($mulNode);
$stmts = [$printNode, $negNode];
$visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor1->expects($this->at(1))->method('enterNode')->with($printNode)
->willReturn(NodeTraverser::DONT_TRAVERSE_CHILDREN);
$visitor2->expects($this->at(1))->method('enterNode')->with($printNode);
$visitor1->expects($this->at(2))->method('leaveNode')->with($printNode);
$visitor2->expects($this->at(2))->method('leaveNode')->with($printNode);
$visitor1->expects($this->at(3))->method('enterNode')->with($negNode);
$visitor2->expects($this->at(3))->method('enterNode')->with($negNode);
$visitor1->expects($this->at(4))->method('enterNode')->with($mulNode);
$visitor2->expects($this->at(4))->method('enterNode')->with($mulNode)
->willReturn(NodeTraverser::DONT_TRAVERSE_CHILDREN);
$visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode);
$visitor2->expects($this->at(5))->method('leaveNode')->with($mulNode);
$visitor1->expects($this->at(6))->method('leaveNode')->with($negNode);
$visitor2->expects($this->at(6))->method('leaveNode')->with($negNode);
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor1);
$traverser->addVisitor($visitor2);
$this->assertEquals($stmts, $traverser->traverse($stmts));
}
public function testDontTraverseCurrentAndChildren() {
// print 'str'; -($foo * $foo);
$strNode = new String_('str');
$printNode = new Expr\Print_($strNode);
$varNode = new Expr\Variable('foo');
$mulNode = new Expr\BinaryOp\Mul($varNode, $varNode);
$divNode = new Expr\BinaryOp\Div($varNode, $varNode);
$negNode = new Expr\UnaryMinus($mulNode);
$stmts = [$printNode, $negNode];
$visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor1->expects($this->at(1))->method('enterNode')->with($printNode)
->willReturn(NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN);
$visitor1->expects($this->at(2))->method('leaveNode')->with($printNode);
$visitor1->expects($this->at(3))->method('enterNode')->with($negNode);
$visitor2->expects($this->at(1))->method('enterNode')->with($negNode);
$visitor1->expects($this->at(4))->method('enterNode')->with($mulNode)
->willReturn(NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN);
$visitor1->expects($this->at(5))->method('leaveNode')->with($mulNode)->willReturn($divNode);
$visitor1->expects($this->at(6))->method('leaveNode')->with($negNode);
$visitor2->expects($this->at(2))->method('leaveNode')->with($negNode);
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor1);
$traverser->addVisitor($visitor2);
$resultStmts = $traverser->traverse($stmts);
$this->assertInstanceOf(Expr\BinaryOp\Div::class, $resultStmts[1]->expr);
}
public function testStopTraversal() {
$varNode1 = new Expr\Variable('a');
$varNode2 = new Expr\Variable('b');
$varNode3 = new Expr\Variable('c');
$mulNode = new Expr\BinaryOp\Mul($varNode1, $varNode2);
$printNode = new Expr\Print_($varNode3);
$stmts = [$mulNode, $printNode];
// From enterNode() with array parent
$visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor->expects($this->at(1))->method('enterNode')->with($mulNode)
->willReturn(NodeTraverser::STOP_TRAVERSAL);
$visitor->expects($this->at(2))->method('afterTraverse');
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$this->assertEquals($stmts, $traverser->traverse($stmts));
// From enterNode with Node parent
$visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor->expects($this->at(2))->method('enterNode')->with($varNode1)
->willReturn(NodeTraverser::STOP_TRAVERSAL);
$visitor->expects($this->at(3))->method('afterTraverse');
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$this->assertEquals($stmts, $traverser->traverse($stmts));
// From leaveNode with Node parent
$visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor->expects($this->at(3))->method('leaveNode')->with($varNode1)
->willReturn(NodeTraverser::STOP_TRAVERSAL);
$visitor->expects($this->at(4))->method('afterTraverse');
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$this->assertEquals($stmts, $traverser->traverse($stmts));
// From leaveNode with array parent
$visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor->expects($this->at(6))->method('leaveNode')->with($mulNode)
->willReturn(NodeTraverser::STOP_TRAVERSAL);
$visitor->expects($this->at(7))->method('afterTraverse');
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$this->assertEquals($stmts, $traverser->traverse($stmts));
// Check that pending array modifications are still carried out
$visitor = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor->expects($this->at(6))->method('leaveNode')->with($mulNode)
->willReturn(NodeTraverser::REMOVE_NODE);
$visitor->expects($this->at(7))->method('enterNode')->with($printNode)
->willReturn(NodeTraverser::STOP_TRAVERSAL);
$visitor->expects($this->at(8))->method('afterTraverse');
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor);
$this->assertEquals([$printNode], $traverser->traverse($stmts));
}
public function testRemovingVisitor() {
$visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor3 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$traverser = new NodeTraverser;
$traverser->addVisitor($visitor1);
$traverser->addVisitor($visitor2);
$traverser->addVisitor($visitor3);
$preExpected = [$visitor1, $visitor2, $visitor3];
$this->assertAttributeSame($preExpected, 'visitors', $traverser, 'The appropriate visitors have not been added');
$traverser->removeVisitor($visitor2);
$postExpected = [0 => $visitor1, 2 => $visitor3];
$this->assertAttributeSame($postExpected, 'visitors', $traverser, 'The appropriate visitors are not present after removal');
}
public function testNoCloneNodes() {
$stmts = [new Node\Stmt\Echo_([new String_('Foo'), new String_('Bar')])];
$traverser = new NodeTraverser;
$this->assertSame($stmts, $traverser->traverse($stmts));
}
/**
* @dataProvider provideTestInvalidReturn
*/
public function testInvalidReturn($visitor, $message) {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage($message);
$stmts = [new Node\Stmt\Expression(new Node\Scalar\LNumber(42))];
$traverser = new NodeTraverser();
$traverser->addVisitor($visitor);
$traverser->traverse($stmts);
}
public function provideTestInvalidReturn() {
$visitor1 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor1->expects($this->at(1))->method('enterNode')
->willReturn('foobar');
$visitor2 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor2->expects($this->at(2))->method('enterNode')
->willReturn('foobar');
$visitor3 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor3->expects($this->at(3))->method('leaveNode')
->willReturn('foobar');
$visitor4 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor4->expects($this->at(4))->method('leaveNode')
->willReturn('foobar');
$visitor5 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor5->expects($this->at(3))->method('leaveNode')
->willReturn([new Node\Scalar\DNumber(42.0)]);
$visitor6 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor6->expects($this->at(4))->method('leaveNode')
->willReturn(false);
$visitor7 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor7->expects($this->at(1))->method('enterNode')
->willReturn(new Node\Scalar\LNumber(42));
$visitor8 = $this->getMockBuilder(NodeVisitor::class)->getMock();
$visitor8->expects($this->at(2))->method('enterNode')
->willReturn(new Node\Stmt\Return_());
return [
[$visitor1, 'enterNode() returned invalid value of type string'],
[$visitor2, 'enterNode() returned invalid value of type string'],
[$visitor3, 'leaveNode() returned invalid value of type string'],
[$visitor4, 'leaveNode() returned invalid value of type string'],
[$visitor5, 'leaveNode() may only return an array if the parent structure is an array'],
[$visitor6, 'bool(false) return from leaveNode() no longer supported. Return NodeTraverser::REMOVE_NODE instead'],
[$visitor7, 'Trying to replace statement (Stmt_Expression) with expression (Scalar_LNumber). Are you missing a Stmt_Expression wrapper?'],
[$visitor8, 'Trying to replace expression (Scalar_LNumber) with statement (Stmt_Return)'],
];
}
}

View file

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace PhpParser\NodeVisitor;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\NodeTraverser;
class FindingVisitorTest extends \PHPUnit\Framework\TestCase
{
public function testFindVariables() {
$traverser = new NodeTraverser();
$visitor = new FindingVisitor(function(Node $node) {
return $node instanceof Node\Expr\Variable;
});
$traverser->addVisitor($visitor);
$assign = new Expr\Assign(new Expr\Variable('a'), new Expr\BinaryOp\Concat(
new Expr\Variable('b'), new Expr\Variable('c')
));
$stmts = [new Node\Stmt\Expression($assign)];
$traverser->traverse($stmts);
$this->assertSame([
$assign->var,
$assign->expr->left,
$assign->expr->right,
], $visitor->getFoundNodes());
}
public function testFindAll() {
$traverser = new NodeTraverser();
$visitor = new FindingVisitor(function(Node $node) {
return true; // All nodes
});
$traverser->addVisitor($visitor);
$assign = new Expr\Assign(new Expr\Variable('a'), new Expr\BinaryOp\Concat(
new Expr\Variable('b'), new Expr\Variable('c')
));
$stmts = [new Node\Stmt\Expression($assign)];
$traverser->traverse($stmts);
$this->assertSame([
$stmts[0],
$assign,
$assign->var,
$assign->expr,
$assign->expr->left,
$assign->expr->right,
], $visitor->getFoundNodes());
}
}

View file

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace PhpParser\NodeVisitor;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\NodeTraverser;
class FirstFindingVisitorTest extends \PHPUnit\Framework\TestCase
{
public function testFindFirstVariable() {
$traverser = new NodeTraverser();
$visitor = new FirstFindingVisitor(function(Node $node) {
return $node instanceof Node\Expr\Variable;
});
$traverser->addVisitor($visitor);
$assign = new Expr\Assign(new Expr\Variable('a'), new Expr\Variable('b'));
$stmts = [new Node\Stmt\Expression($assign)];
$traverser->traverse($stmts);
$this->assertSame($assign->var, $visitor->getFoundNode());
}
public function testFindNone() {
$traverser = new NodeTraverser();
$visitor = new FirstFindingVisitor(function(Node $node) {
return $node instanceof Node\Expr\BinaryOp;
});
$traverser->addVisitor($visitor);
$assign = new Expr\Assign(new Expr\Variable('a'), new Expr\Variable('b'));
$stmts = [new Node\Stmt\Expression($assign)];
$traverser->traverse($stmts);
$this->assertNull($visitor->getFoundNode());
}
}

View file

@ -0,0 +1,505 @@
<?php declare(strict_types=1);
namespace PhpParser\NodeVisitor;
use PhpParser;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
class NameResolverTest extends \PHPUnit\Framework\TestCase
{
private function canonicalize($string) {
return str_replace("\r\n", "\n", $string);
}
/**
* @covers \PhpParser\NodeVisitor\NameResolver
*/
public function testResolveNames() {
$code = <<<'EOC'
<?php
namespace Foo {
use Hallo as Hi;
new Bar();
new Hi();
new Hi\Bar();
new \Bar();
new namespace\Bar();
bar();
hi();
Hi\bar();
foo\bar();
\bar();
namespace\bar();
}
namespace {
use Hallo as Hi;
new Bar();
new Hi();
new Hi\Bar();
new \Bar();
new namespace\Bar();
bar();
hi();
Hi\bar();
foo\bar();
\bar();
namespace\bar();
}
namespace Bar {
use function foo\bar as baz;
use const foo\BAR as BAZ;
use foo as bar;
bar();
baz();
bar\foo();
baz\foo();
BAR();
BAZ();
BAR\FOO();
BAZ\FOO();
bar;
baz;
bar\foo;
baz\foo;
BAR;
BAZ;
BAR\FOO;
BAZ\FOO;
}
namespace Baz {
use A\T\{B\C, D\E};
use function X\T\{b\c, d\e};
use const Y\T\{B\C, D\E};
use Z\T\{G, function f, const K};
new C;
new E;
new C\D;
new E\F;
new G;
c();
e();
f();
C;
E;
K;
class ClassWithTypeProperties
{
public float $php = 7.4;
public ?Foo $person;
protected static ?bool $probability;
}
}
EOC;
$expectedCode = <<<'EOC'
namespace Foo {
use Hallo as Hi;
new \Foo\Bar();
new \Hallo();
new \Hallo\Bar();
new \Bar();
new \Foo\Bar();
bar();
hi();
\Hallo\bar();
\Foo\foo\bar();
\bar();
\Foo\bar();
}
namespace {
use Hallo as Hi;
new \Bar();
new \Hallo();
new \Hallo\Bar();
new \Bar();
new \Bar();
\bar();
\hi();
\Hallo\bar();
\foo\bar();
\bar();
\bar();
}
namespace Bar {
use function foo\bar as baz;
use const foo\BAR as BAZ;
use foo as bar;
bar();
\foo\bar();
\foo\foo();
\Bar\baz\foo();
BAR();
\foo\bar();
\foo\FOO();
\Bar\BAZ\FOO();
bar;
baz;
\foo\foo;
\Bar\baz\foo;
BAR;
\foo\BAR;
\foo\FOO;
\Bar\BAZ\FOO;
}
namespace Baz {
use A\T\{B\C, D\E};
use function X\T\{b\c, d\e};
use const Y\T\{B\C, D\E};
use Z\T\{G, function f, const K};
new \A\T\B\C();
new \A\T\D\E();
new \A\T\B\C\D();
new \A\T\D\E\F();
new \Z\T\G();
\X\T\b\c();
\X\T\d\e();
\Z\T\f();
\Y\T\B\C;
\Y\T\D\E;
\Z\T\K;
class ClassWithTypeProperties
{
public float $php = 7.4;
public ?\Baz\Foo $person;
protected static ?bool $probability;
}
}
EOC;
$parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative);
$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $parser->parse($code);
$stmts = $traverser->traverse($stmts);
$this->assertSame(
$this->canonicalize($expectedCode),
$prettyPrinter->prettyPrint($stmts)
);
}
/**
* @covers \PhpParser\NodeVisitor\NameResolver
*/
public function testResolveLocations() {
$code = <<<'EOC'
<?php
namespace NS;
class A extends B implements C, D {
use E, F, G {
f as private g;
E::h as i;
E::j insteadof F, G;
}
}
interface A extends C, D {
public function a(A $a) : A;
}
function fn(A $a) : A {}
function fn2(array $a) : array {}
function(A $a) : A {};
function fn3(?A $a) : ?A {}
function fn4(?array $a) : ?array {}
A::b();
A::$b;
A::B;
new A;
$a instanceof A;
namespace\a();
namespace\A;
try {
$someThing;
} catch (A $a) {
$someThingElse;
}
EOC;
$expectedCode = <<<'EOC'
namespace NS;
class A extends \NS\B implements \NS\C, \NS\D
{
use \NS\E, \NS\F, \NS\G {
f as private g;
\NS\E::h as i;
\NS\E::j insteadof \NS\F, \NS\G;
}
}
interface A extends \NS\C, \NS\D
{
public function a(\NS\A $a) : \NS\A;
}
function fn(\NS\A $a) : \NS\A
{
}
function fn2(array $a) : array
{
}
function (\NS\A $a) : \NS\A {
};
function fn3(?\NS\A $a) : ?\NS\A
{
}
function fn4(?array $a) : ?array
{
}
\NS\A::b();
\NS\A::$b;
\NS\A::B;
new \NS\A();
$a instanceof \NS\A;
\NS\a();
\NS\A;
try {
$someThing;
} catch (\NS\A $a) {
$someThingElse;
}
EOC;
$parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative);
$prettyPrinter = new PhpParser\PrettyPrinter\Standard;
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $parser->parse($code);
$stmts = $traverser->traverse($stmts);
$this->assertSame(
$this->canonicalize($expectedCode),
$prettyPrinter->prettyPrint($stmts)
);
}
public function testNoResolveSpecialName() {
$stmts = [new Node\Expr\New_(new Name('self'))];
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$this->assertEquals($stmts, $traverser->traverse($stmts));
}
public function testAddDeclarationNamespacedName() {
$nsStmts = [
new Stmt\Class_('A'),
new Stmt\Interface_('B'),
new Stmt\Function_('C'),
new Stmt\Const_([
new Node\Const_('D', new Node\Scalar\LNumber(42))
]),
new Stmt\Trait_('E'),
new Expr\New_(new Stmt\Class_(null)),
];
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $traverser->traverse([new Stmt\Namespace_(new Name('NS'), $nsStmts)]);
$this->assertSame('NS\\A', (string) $stmts[0]->stmts[0]->namespacedName);
$this->assertSame('NS\\B', (string) $stmts[0]->stmts[1]->namespacedName);
$this->assertSame('NS\\C', (string) $stmts[0]->stmts[2]->namespacedName);
$this->assertSame('NS\\D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName);
$this->assertSame('NS\\E', (string) $stmts[0]->stmts[4]->namespacedName);
$this->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[5]->class);
$stmts = $traverser->traverse([new Stmt\Namespace_(null, $nsStmts)]);
$this->assertSame('A', (string) $stmts[0]->stmts[0]->namespacedName);
$this->assertSame('B', (string) $stmts[0]->stmts[1]->namespacedName);
$this->assertSame('C', (string) $stmts[0]->stmts[2]->namespacedName);
$this->assertSame('D', (string) $stmts[0]->stmts[3]->consts[0]->namespacedName);
$this->assertSame('E', (string) $stmts[0]->stmts[4]->namespacedName);
$this->assertObjectNotHasAttribute('namespacedName', $stmts[0]->stmts[5]->class);
}
public function testAddRuntimeResolvedNamespacedName() {
$stmts = [
new Stmt\Namespace_(new Name('NS'), [
new Expr\FuncCall(new Name('foo')),
new Expr\ConstFetch(new Name('FOO')),
]),
new Stmt\Namespace_(null, [
new Expr\FuncCall(new Name('foo')),
new Expr\ConstFetch(new Name('FOO')),
]),
];
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $traverser->traverse($stmts);
$this->assertSame('NS\\foo', (string) $stmts[0]->stmts[0]->name->getAttribute('namespacedName'));
$this->assertSame('NS\\FOO', (string) $stmts[0]->stmts[1]->name->getAttribute('namespacedName'));
$this->assertFalse($stmts[1]->stmts[0]->name->hasAttribute('namespacedName'));
$this->assertFalse($stmts[1]->stmts[1]->name->hasAttribute('namespacedName'));
}
/**
* @dataProvider provideTestError
*/
public function testError(Node $stmt, $errorMsg) {
$this->expectException(\PhpParser\Error::class);
$this->expectExceptionMessage($errorMsg);
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$traverser->traverse([$stmt]);
}
public function provideTestError() {
return [
[
new Stmt\Use_([
new Stmt\UseUse(new Name('A\B'), 'B', 0, ['startLine' => 1]),
new Stmt\UseUse(new Name('C\D'), 'B', 0, ['startLine' => 2]),
], Stmt\Use_::TYPE_NORMAL),
'Cannot use C\D as B because the name is already in use on line 2'
],
[
new Stmt\Use_([
new Stmt\UseUse(new Name('a\b'), 'b', 0, ['startLine' => 1]),
new Stmt\UseUse(new Name('c\d'), 'B', 0, ['startLine' => 2]),
], Stmt\Use_::TYPE_FUNCTION),
'Cannot use function c\d as B because the name is already in use on line 2'
],
[
new Stmt\Use_([
new Stmt\UseUse(new Name('A\B'), 'B', 0, ['startLine' => 1]),
new Stmt\UseUse(new Name('C\D'), 'B', 0, ['startLine' => 2]),
], Stmt\Use_::TYPE_CONSTANT),
'Cannot use const C\D as B because the name is already in use on line 2'
],
[
new Expr\New_(new Name\FullyQualified('self', ['startLine' => 3])),
"'\\self' is an invalid class name on line 3"
],
[
new Expr\New_(new Name\Relative('self', ['startLine' => 3])),
"'\\self' is an invalid class name on line 3"
],
[
new Expr\New_(new Name\FullyQualified('PARENT', ['startLine' => 3])),
"'\\PARENT' is an invalid class name on line 3"
],
[
new Expr\New_(new Name\Relative('STATIC', ['startLine' => 3])),
"'\\STATIC' is an invalid class name on line 3"
],
];
}
public function testClassNameIsCaseInsensitive()
{
$source = <<<'EOC'
<?php
namespace Foo;
use Bar\Baz;
$test = new baz();
EOC;
$parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative);
$stmts = $parser->parse($source);
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $traverser->traverse($stmts);
$stmt = $stmts[0];
$assign = $stmt->stmts[1]->expr;
$this->assertSame(['Bar', 'Baz'], $assign->expr->class->parts);
}
public function testSpecialClassNamesAreCaseInsensitive() {
$source = <<<'EOC'
<?php
namespace Foo;
class Bar
{
public static function method()
{
SELF::method();
PARENT::method();
STATIC::method();
}
}
EOC;
$parser = new PhpParser\Parser\Php7(new PhpParser\Lexer\Emulative);
$stmts = $parser->parse($source);
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver);
$stmts = $traverser->traverse($stmts);
$classStmt = $stmts[0];
$methodStmt = $classStmt->stmts[0]->stmts[0];
$this->assertSame('SELF', (string) $methodStmt->stmts[0]->expr->class);
$this->assertSame('PARENT', (string) $methodStmt->stmts[1]->expr->class);
$this->assertSame('STATIC', (string) $methodStmt->stmts[2]->expr->class);
}
public function testAddOriginalNames() {
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver(null, ['preserveOriginalNames' => true]));
$n1 = new Name('Bar');
$n2 = new Name('bar');
$origStmts = [
new Stmt\Namespace_(new Name('Foo'), [
new Expr\ClassConstFetch($n1, 'FOO'),
new Expr\FuncCall($n2),
])
];
$stmts = $traverser->traverse($origStmts);
$this->assertSame($n1, $stmts[0]->stmts[0]->class->getAttribute('originalName'));
$this->assertSame($n2, $stmts[0]->stmts[1]->name->getAttribute('originalName'));
}
public function testAttributeOnlyMode() {
$traverser = new PhpParser\NodeTraverser;
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$n1 = new Name('Bar');
$n2 = new Name('bar');
$origStmts = [
new Stmt\Namespace_(new Name('Foo'), [
new Expr\ClassConstFetch($n1, 'FOO'),
new Expr\FuncCall($n2),
])
];
$traverser->traverse($origStmts);
$this->assertEquals(
new Name\FullyQualified('Foo\Bar'), $n1->getAttribute('resolvedName'));
$this->assertFalse($n2->hasAttribute('resolvedName'));
$this->assertEquals(
new Name\FullyQualified('Foo\bar'), $n2->getAttribute('namespacedName'));
}
}

View file

@ -0,0 +1,94 @@
<?php declare(strict_types=1);
namespace PhpParser\Parser;
use PhpParser\Error;
use PhpParser\Lexer;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
use PhpParser\ParserTest;
class MultipleTest extends ParserTest
{
// This provider is for the generic parser tests, just pick an arbitrary order here
protected function getParser(Lexer $lexer) {
return new Multiple([new Php5($lexer), new Php7($lexer)]);
}
private function getPrefer7() {
$lexer = new Lexer(['usedAttributes' => []]);
return new Multiple([new Php7($lexer), new Php5($lexer)]);
}
private function getPrefer5() {
$lexer = new Lexer(['usedAttributes' => []]);
return new Multiple([new Php5($lexer), new Php7($lexer)]);
}
/** @dataProvider provideTestParse */
public function testParse($code, Multiple $parser, $expected) {
$this->assertEquals($expected, $parser->parse($code));
}
public function provideTestParse() {
return [
[
// PHP 7 only code
'<?php class Test { function function() {} }',
$this->getPrefer5(),
[
new Stmt\Class_('Test', ['stmts' => [
new Stmt\ClassMethod('function')
]]),
]
],
[
// PHP 5 only code
'<?php global $$a->b;',
$this->getPrefer7(),
[
new Stmt\Global_([
new Expr\Variable(new Expr\PropertyFetch(new Expr\Variable('a'), 'b'))
])
]
],
[
// Different meaning (PHP 5)
'<?php $$a[0];',
$this->getPrefer5(),
[
new Stmt\Expression(new Expr\Variable(
new Expr\ArrayDimFetch(new Expr\Variable('a'), LNumber::fromString('0'))
))
]
],
[
// Different meaning (PHP 7)
'<?php $$a[0];',
$this->getPrefer7(),
[
new Stmt\Expression(new Expr\ArrayDimFetch(
new Expr\Variable(new Expr\Variable('a')), LNumber::fromString('0')
))
]
],
];
}
public function testThrownError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('FAIL A');
$parserA = $this->getMockBuilder(\PhpParser\Parser::class)->getMock();
$parserA->expects($this->at(0))
->method('parse')->willThrowException(new Error('FAIL A'));
$parserB = $this->getMockBuilder(\PhpParser\Parser::class)->getMock();
$parserB->expects($this->at(0))
->method('parse')->willThrowException(new Error('FAIL B'));
$parser = new Multiple([$parserA, $parserB]);
$parser->parse('dummy');
}
}

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace PhpParser\Parser;
use PhpParser\Lexer;
use PhpParser\ParserTest;
class Php5Test extends ParserTest
{
protected function getParser(Lexer $lexer) {
return new Php5($lexer);
}
}

View file

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace PhpParser\Parser;
use PhpParser\Lexer;
use PhpParser\ParserTest;
class Php7Test extends ParserTest
{
protected function getParser(Lexer $lexer) {
return new Php7($lexer);
}
}

View file

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace PhpParser;
/* This test is very weak, because PHPUnit's assertEquals assertion is way too slow dealing with the
* large objects involved here. So we just do some basic instanceof tests instead. */
class ParserFactoryTest extends \PHPUnit\Framework\TestCase
{
/** @dataProvider provideTestCreate */
public function testCreate($kind, $lexer, $expected) {
$this->assertInstanceOf($expected, (new ParserFactory)->create($kind, $lexer));
}
public function provideTestCreate() {
$lexer = new Lexer();
return [
[
ParserFactory::PREFER_PHP7, $lexer,
Parser\Multiple::class
],
[
ParserFactory::PREFER_PHP5, null,
Parser\Multiple::class
],
[
ParserFactory::ONLY_PHP7, null,
Parser\Php7::class
],
[
ParserFactory::ONLY_PHP5, $lexer,
Parser\Php5::class
]
];
}
}

View file

@ -0,0 +1,183 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
abstract class ParserTest extends \PHPUnit\Framework\TestCase
{
/** @returns Parser */
abstract protected function getParser(Lexer $lexer);
public function testParserThrowsSyntaxError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('Syntax error, unexpected EOF on line 1');
$parser = $this->getParser(new Lexer());
$parser->parse('<?php foo');
}
public function testParserThrowsSpecialError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('Cannot use foo as self because \'self\' is a special class name on line 1');
$parser = $this->getParser(new Lexer());
$parser->parse('<?php use foo as self;');
}
public function testParserThrowsLexerError() {
$this->expectException(Error::class);
$this->expectExceptionMessage('Unterminated comment on line 1');
$parser = $this->getParser(new Lexer());
$parser->parse('<?php /*');
}
public function testAttributeAssignment() {
$lexer = new Lexer([
'usedAttributes' => [
'comments', 'startLine', 'endLine',
'startTokenPos', 'endTokenPos',
]
]);
$code = <<<'EOC'
<?php
/** Doc comment */
function test($a) {
// Line
// Comments
echo $a;
}
EOC;
$code = canonicalize($code);
$parser = $this->getParser($lexer);
$stmts = $parser->parse($code);
/** @var Stmt\Function_ $fn */
$fn = $stmts[0];
$this->assertInstanceOf(Stmt\Function_::class, $fn);
$this->assertEquals([
'comments' => [
new Comment\Doc('/** Doc comment */', 2, 6, 1),
],
'startLine' => 3,
'endLine' => 7,
'startTokenPos' => 3,
'endTokenPos' => 21,
], $fn->getAttributes());
$param = $fn->params[0];
$this->assertInstanceOf(Node\Param::class, $param);
$this->assertEquals([
'startLine' => 3,
'endLine' => 3,
'startTokenPos' => 7,
'endTokenPos' => 7,
], $param->getAttributes());
/** @var Stmt\Echo_ $echo */
$echo = $fn->stmts[0];
$this->assertInstanceOf(Stmt\Echo_::class, $echo);
$this->assertEquals([
'comments' => [
new Comment("// Line\n", 4, 49, 12),
new Comment("// Comments\n", 5, 61, 14),
],
'startLine' => 6,
'endLine' => 6,
'startTokenPos' => 16,
'endTokenPos' => 19,
], $echo->getAttributes());
/** @var \PhpParser\Node\Expr\Variable $var */
$var = $echo->exprs[0];
$this->assertInstanceOf(Expr\Variable::class, $var);
$this->assertEquals([
'startLine' => 6,
'endLine' => 6,
'startTokenPos' => 18,
'endTokenPos' => 18,
], $var->getAttributes());
}
public function testInvalidToken() {
$this->expectException(\RangeException::class);
$this->expectExceptionMessage('The lexer returned an invalid token (id=999, value=foobar)');
$lexer = new InvalidTokenLexer;
$parser = $this->getParser($lexer);
$parser->parse('dummy');
}
/**
* @dataProvider provideTestExtraAttributes
*/
public function testExtraAttributes($code, $expectedAttributes) {
$parser = $this->getParser(new Lexer\Emulative);
$stmts = $parser->parse("<?php $code;");
$node = $stmts[0] instanceof Stmt\Expression ? $stmts[0]->expr : $stmts[0];
$attributes = $node->getAttributes();
foreach ($expectedAttributes as $name => $value) {
$this->assertSame($value, $attributes[$name]);
}
}
public function provideTestExtraAttributes() {
return [
['0', ['kind' => Scalar\LNumber::KIND_DEC]],
['9', ['kind' => Scalar\LNumber::KIND_DEC]],
['07', ['kind' => Scalar\LNumber::KIND_OCT]],
['0xf', ['kind' => Scalar\LNumber::KIND_HEX]],
['0XF', ['kind' => Scalar\LNumber::KIND_HEX]],
['0b1', ['kind' => Scalar\LNumber::KIND_BIN]],
['0B1', ['kind' => Scalar\LNumber::KIND_BIN]],
['[]', ['kind' => Expr\Array_::KIND_SHORT]],
['array()', ['kind' => Expr\Array_::KIND_LONG]],
["'foo'", ['kind' => String_::KIND_SINGLE_QUOTED]],
["b'foo'", ['kind' => String_::KIND_SINGLE_QUOTED]],
["B'foo'", ['kind' => String_::KIND_SINGLE_QUOTED]],
['"foo"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
['b"foo"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
['B"foo"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
['"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
['b"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
['B"foo$bar"', ['kind' => String_::KIND_DOUBLE_QUOTED]],
["<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<<STR\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<<\"STR\"\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["b<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["B<<<'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<< \t 'STR'\nSTR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<<'\xff'\n\xff\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => "\xff", 'docIndentation' => '']],
["<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["b<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["B<<<\"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<< \t \"STR\"\n\$a\nSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => '']],
["<<<STR\n STR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => ' ']],
["<<<STR\n\tSTR\n", ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR', 'docIndentation' => "\t"]],
["<<<'STR'\n Foo\n STR\n", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR', 'docIndentation' => ' ']],
["die", ['kind' => Expr\Exit_::KIND_DIE]],
["die('done')", ['kind' => Expr\Exit_::KIND_DIE]],
["exit", ['kind' => Expr\Exit_::KIND_EXIT]],
["exit(1)", ['kind' => Expr\Exit_::KIND_EXIT]],
["?>Foo", ['hasLeadingNewline' => false]],
["?>\nFoo", ['hasLeadingNewline' => true]],
["namespace Foo;", ['kind' => Stmt\Namespace_::KIND_SEMICOLON]],
["namespace Foo {}", ['kind' => Stmt\Namespace_::KIND_BRACED]],
["namespace {}", ['kind' => Stmt\Namespace_::KIND_BRACED]],
["(float) 5.0", ['kind' => Expr\Cast\Double::KIND_FLOAT]],
["(double) 5.0", ['kind' => Expr\Cast\Double::KIND_DOUBLE]],
["(real) 5.0", ['kind' => Expr\Cast\Double::KIND_REAL]],
[" ( REAL ) 5.0", ['kind' => Expr\Cast\Double::KIND_REAL]],
];
}
}
class InvalidTokenLexer extends Lexer
{
public function getNextToken(&$value = null, &$startAttributes = null, &$endAttributes = null) : int {
$value = 'foobar';
return 999;
}
}

View file

@ -0,0 +1,307 @@
<?php declare(strict_types=1);
namespace PhpParser;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\DNumber;
use PhpParser\Node\Scalar\Encapsed;
use PhpParser\Node\Scalar\EncapsedStringPart;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
use PhpParser\PrettyPrinter\Standard;
class PrettyPrinterTest extends CodeTestAbstract
{
protected function doTestPrettyPrintMethod($method, $name, $code, $expected, $modeLine) {
$lexer = new Lexer\Emulative;
$parser5 = new Parser\Php5($lexer);
$parser7 = new Parser\Php7($lexer);
list($version, $options) = $this->parseModeLine($modeLine);
$prettyPrinter = new Standard($options);
try {
$output5 = canonicalize($prettyPrinter->$method($parser5->parse($code)));
} catch (Error $e) {
$output5 = null;
if ('php7' !== $version) {
throw $e;
}
}
try {
$output7 = canonicalize($prettyPrinter->$method($parser7->parse($code)));
} catch (Error $e) {
$output7 = null;
if ('php5' !== $version) {
throw $e;
}
}
if ('php5' === $version) {
$this->assertSame($expected, $output5, $name);
$this->assertNotSame($expected, $output7, $name);
} elseif ('php7' === $version) {
$this->assertSame($expected, $output7, $name);
$this->assertNotSame($expected, $output5, $name);
} else {
$this->assertSame($expected, $output5, $name);
$this->assertSame($expected, $output7, $name);
}
}
/**
* @dataProvider provideTestPrettyPrint
* @covers \PhpParser\PrettyPrinter\Standard<extended>
*/
public function testPrettyPrint($name, $code, $expected, $mode) {
$this->doTestPrettyPrintMethod('prettyPrint', $name, $code, $expected, $mode);
}
/**
* @dataProvider provideTestPrettyPrintFile
* @covers \PhpParser\PrettyPrinter\Standard<extended>
*/
public function testPrettyPrintFile($name, $code, $expected, $mode) {
$this->doTestPrettyPrintMethod('prettyPrintFile', $name, $code, $expected, $mode);
}
public function provideTestPrettyPrint() {
return $this->getTests(__DIR__ . '/../code/prettyPrinter', 'test');
}
public function provideTestPrettyPrintFile() {
return $this->getTests(__DIR__ . '/../code/prettyPrinter', 'file-test');
}
public function testPrettyPrintExpr() {
$prettyPrinter = new Standard;
$expr = new Expr\BinaryOp\Mul(
new Expr\BinaryOp\Plus(new Expr\Variable('a'), new Expr\Variable('b')),
new Expr\Variable('c')
);
$this->assertEquals('($a + $b) * $c', $prettyPrinter->prettyPrintExpr($expr));
$expr = new Expr\Closure([
'stmts' => [new Stmt\Return_(new String_("a\nb"))]
]);
$this->assertEquals("function () {\n return 'a\nb';\n}", $prettyPrinter->prettyPrintExpr($expr));
}
public function testCommentBeforeInlineHTML() {
$prettyPrinter = new PrettyPrinter\Standard;
$comment = new Comment\Doc("/**\n * This is a comment\n */");
$stmts = [new Stmt\InlineHTML('Hello World!', ['comments' => [$comment]])];
$expected = "<?php\n\n/**\n * This is a comment\n */\n?>\nHello World!";
$this->assertSame($expected, $prettyPrinter->prettyPrintFile($stmts));
}
private function parseModeLine($modeLine) {
$parts = explode(' ', (string) $modeLine, 2);
$version = $parts[0] ?? 'both';
$options = isset($parts[1]) ? json_decode($parts[1], true) : [];
return [$version, $options];
}
public function testArraySyntaxDefault() {
$prettyPrinter = new Standard(['shortArraySyntax' => true]);
$expr = new Expr\Array_([
new Expr\ArrayItem(new String_('val'), new String_('key'))
]);
$expected = "['key' => 'val']";
$this->assertSame($expected, $prettyPrinter->prettyPrintExpr($expr));
}
/**
* @dataProvider provideTestKindAttributes
*/
public function testKindAttributes($node, $expected) {
$prttyPrinter = new PrettyPrinter\Standard;
$result = $prttyPrinter->prettyPrintExpr($node);
$this->assertSame($expected, $result);
}
public function provideTestKindAttributes() {
$nowdoc = ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR'];
$heredoc = ['kind' => String_::KIND_HEREDOC, 'docLabel' => 'STR'];
return [
// Defaults to single quoted
[new String_('foo'), "'foo'"],
// Explicit single/double quoted
[new String_('foo', ['kind' => String_::KIND_SINGLE_QUOTED]), "'foo'"],
[new String_('foo', ['kind' => String_::KIND_DOUBLE_QUOTED]), '"foo"'],
// Fallback from doc string if no label
[new String_('foo', ['kind' => String_::KIND_NOWDOC]), "'foo'"],
[new String_('foo', ['kind' => String_::KIND_HEREDOC]), '"foo"'],
// Fallback if string contains label
[new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'A']), "'A\nB\nC'"],
[new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'B']), "'A\nB\nC'"],
[new String_("A\nB\nC", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'C']), "'A\nB\nC'"],
[new String_("STR;", ['kind' => String_::KIND_NOWDOC, 'docLabel' => 'STR']), "'STR;'"],
// Doc string if label not contained (or not in ending position)
[new String_("foo", $nowdoc), "<<<'STR'\nfoo\nSTR\n"],
[new String_("foo", $heredoc), "<<<STR\nfoo\nSTR\n"],
[new String_("STRx", $nowdoc), "<<<'STR'\nSTRx\nSTR\n"],
[new String_("xSTR", $nowdoc), "<<<'STR'\nxSTR\nSTR\n"],
// Empty doc string variations (encapsed variant does not occur naturally)
[new String_("", $nowdoc), "<<<'STR'\nSTR\n"],
[new String_("", $heredoc), "<<<STR\nSTR\n"],
[new Encapsed([new EncapsedStringPart('')], $heredoc), "<<<STR\nSTR\n"],
// Encapsed doc string variations
[new Encapsed([new EncapsedStringPart('foo')], $heredoc), "<<<STR\nfoo\nSTR\n"],
[new Encapsed([new EncapsedStringPart('foo'), new Expr\Variable('y')], $heredoc), "<<<STR\nfoo{\$y}\nSTR\n"],
[new Encapsed([new EncapsedStringPart("\nSTR"), new Expr\Variable('y')], $heredoc), "<<<STR\n\nSTR{\$y}\nSTR\n"],
[new Encapsed([new EncapsedStringPart("\nSTR"), new Expr\Variable('y')], $heredoc), "<<<STR\n\nSTR{\$y}\nSTR\n"],
[new Encapsed([new Expr\Variable('y'), new EncapsedStringPart("STR\n")], $heredoc), "<<<STR\n{\$y}STR\n\nSTR\n"],
// Encapsed doc string fallback
[new Encapsed([new Expr\Variable('y'), new EncapsedStringPart("\nSTR")], $heredoc), '"{$y}\\nSTR"'],
[new Encapsed([new EncapsedStringPart("STR\n"), new Expr\Variable('y')], $heredoc), '"STR\\n{$y}"'],
[new Encapsed([new EncapsedStringPart("STR")], $heredoc), '"STR"'],
];
}
/** @dataProvider provideTestUnnaturalLiterals */
public function testUnnaturalLiterals($node, $expected) {
$prttyPrinter = new PrettyPrinter\Standard;
$result = $prttyPrinter->prettyPrintExpr($node);
$this->assertSame($expected, $result);
}
public function provideTestUnnaturalLiterals() {
return [
[new LNumber(-1), '-1'],
[new LNumber(-PHP_INT_MAX - 1), '(-' . PHP_INT_MAX . '-1)'],
[new LNumber(-1, ['kind' => LNumber::KIND_BIN]), '-0b1'],
[new LNumber(-1, ['kind' => LNumber::KIND_OCT]), '-01'],
[new LNumber(-1, ['kind' => LNumber::KIND_HEX]), '-0x1'],
[new DNumber(\INF), '\INF'],
[new DNumber(-\INF), '-\INF'],
[new DNumber(-\NAN), '\NAN'],
];
}
public function testPrettyPrintWithError() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot pretty-print AST with Error nodes');
$stmts = [new Stmt\Expression(
new Expr\PropertyFetch(new Expr\Variable('a'), new Expr\Error())
)];
$prettyPrinter = new PrettyPrinter\Standard;
$prettyPrinter->prettyPrint($stmts);
}
public function testPrettyPrintWithErrorInClassConstFetch() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot pretty-print AST with Error nodes');
$stmts = [new Stmt\Expression(
new Expr\ClassConstFetch(new Name('Foo'), new Expr\Error())
)];
$prettyPrinter = new PrettyPrinter\Standard;
$prettyPrinter->prettyPrint($stmts);
}
public function testPrettyPrintEncapsedStringPart() {
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot directly print EncapsedStringPart');
$expr = new Node\Scalar\EncapsedStringPart('foo');
$prettyPrinter = new PrettyPrinter\Standard;
$prettyPrinter->prettyPrintExpr($expr);
}
/**
* @dataProvider provideTestFormatPreservingPrint
* @covers \PhpParser\PrettyPrinter\Standard<extended>
*/
public function testFormatPreservingPrint($name, $code, $modification, $expected, $modeLine) {
$lexer = new Lexer\Emulative([
'usedAttributes' => [
'comments',
'startLine', 'endLine',
'startTokenPos', 'endTokenPos',
],
]);
$parser = new Parser\Php7($lexer);
$traverser = new NodeTraverser();
$traverser->addVisitor(new NodeVisitor\CloningVisitor());
$printer = new PrettyPrinter\Standard();
$oldStmts = $parser->parse($code);
$oldTokens = $lexer->getTokens();
$newStmts = $traverser->traverse($oldStmts);
/** @var callable $fn */
eval(<<<CODE
use PhpParser\Comment;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
\$fn = function(&\$stmts) { $modification };
CODE
);
$fn($newStmts);
$newCode = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
$this->assertSame(canonicalize($expected), canonicalize($newCode), $name);
}
public function provideTestFormatPreservingPrint() {
return $this->getTests(__DIR__ . '/../code/formatPreservation', 'test', 3);
}
/**
* @dataProvider provideTestRoundTripPrint
* @covers \PhpParser\PrettyPrinter\Standard<extended>
*/
public function testRoundTripPrint($name, $code, $expected, $modeLine) {
/**
* This test makes sure that the format-preserving pretty printer round-trips for all
* the pretty printer tests (i.e. returns the input if no changes occurred).
*/
list($version) = $this->parseModeLine($modeLine);
$lexer = new Lexer\Emulative([
'usedAttributes' => [
'comments',
'startLine', 'endLine',
'startTokenPos', 'endTokenPos',
],
]);
$parserClass = $version === 'php5' ? Parser\Php5::class : Parser\Php7::class;
/** @var Parser $parser */
$parser = new $parserClass($lexer);
$traverser = new NodeTraverser();
$traverser->addVisitor(new NodeVisitor\CloningVisitor());
$printer = new PrettyPrinter\Standard();
try {
$oldStmts = $parser->parse($code);
} catch (Error $e) {
// Can't do a format-preserving print on a file with errors
return;
}
$oldTokens = $lexer->getTokens();
$newStmts = $traverser->traverse($oldStmts);
$newCode = $printer->printFormatPreserving($newStmts, $oldStmts, $oldTokens);
$this->assertSame(canonicalize($code), canonicalize($newCode), $name);
}
public function provideTestRoundTripPrint() {
return array_merge(
$this->getTests(__DIR__ . '/../code/prettyPrinter', 'test'),
$this->getTests(__DIR__ . '/../code/parser', 'test')
);
}
}