From 085ae8bbda0272abfc2040f880edbe02b9953921 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 13 Sep 2014 18:16:14 +1000 Subject: [PATCH 01/20] Implement new static analysis tools and infrastructure for expression trees under the Analysis namespace. --- CHANGELOG.md | 1 + Source/Analysis/AnalysisContext.php | 60 ++ .../BinaryOperations/BinaryOperation.php | 65 +++ Source/Analysis/ExpressionAnalyser.php | 351 ++++++++++++ Source/Analysis/Functions/Func.php | 59 ++ Source/Analysis/IAnalysisContext.php | 67 +++ Source/Analysis/IBinaryOperation.php | 41 ++ Source/Analysis/ICallable.php | 43 ++ Source/Analysis/ICompositeType.php | 20 + Source/Analysis/IConstructor.php | 29 + Source/Analysis/IExpressionAnalyser.php | 30 + Source/Analysis/IField.php | 28 + Source/Analysis/IFunction.php | 27 + Source/Analysis/IIndexer.php | 23 + Source/Analysis/IMethod.php | 25 + Source/Analysis/INativeType.php | 29 + Source/Analysis/IObjectType.php | 27 + Source/Analysis/IType.php | 142 +++++ Source/Analysis/ITypeAnalysis.php | 147 +++++ Source/Analysis/ITypeOperation.php | 27 + Source/Analysis/ITypeSystem.php | 101 ++++ Source/Analysis/ITyped.php | 18 + Source/Analysis/PhpTypeSystem.php | 529 ++++++++++++++++++ Source/Analysis/TypeAnalysis.php | 138 +++++ Source/Analysis/TypeData/DateTime.php | 85 +++ .../Analysis/TypeData/InternalFunctions.php | 248 ++++++++ Source/Analysis/TypeData/InternalTypes.php | 57 ++ Source/Analysis/TypeData/PinqAPI.php | 147 +++++ Source/Analysis/TypeData/TypeDataModule.php | 35 ++ Source/Analysis/TypeException.php | 15 + Source/Analysis/TypeId.php | 46 ++ Source/Analysis/TypeOperations/Cast.php | 13 + .../Analysis/TypeOperations/Constructor.php | 35 ++ Source/Analysis/TypeOperations/Field.php | 41 ++ Source/Analysis/TypeOperations/Indexer.php | 24 + Source/Analysis/TypeOperations/Method.php | 46 ++ .../Analysis/TypeOperations/TypeOperation.php | 42 ++ Source/Analysis/TypeSystem.php | 261 +++++++++ Source/Analysis/Typed.php | 26 + Source/Analysis/Types/CompositeType.php | 103 ++++ Source/Analysis/Types/MixedType.php | 79 +++ Source/Analysis/Types/NativeType.php | 46 ++ Source/Analysis/Types/ObjectType.php | 199 +++++++ Source/Analysis/Types/Type.php | 138 +++++ .../Analysis/BasicExpressionAnalysisTest.php | 433 ++++++++++++++ .../ComplexExpressionAnalysisTest.php | 89 +++ .../Analysis/ExpressionAnalysisTestCase.php | 148 +++++ .../Integration/Analysis/TypeAnalysisTest.php | 364 ++++++++++++ Tests/Integration/Analysis/TypeSystemTest.php | 196 +++++++ phpunit.xml.dist | 3 + 50 files changed, 4946 insertions(+) create mode 100644 Source/Analysis/AnalysisContext.php create mode 100644 Source/Analysis/BinaryOperations/BinaryOperation.php create mode 100644 Source/Analysis/ExpressionAnalyser.php create mode 100644 Source/Analysis/Functions/Func.php create mode 100644 Source/Analysis/IAnalysisContext.php create mode 100644 Source/Analysis/IBinaryOperation.php create mode 100644 Source/Analysis/ICallable.php create mode 100644 Source/Analysis/ICompositeType.php create mode 100644 Source/Analysis/IConstructor.php create mode 100644 Source/Analysis/IExpressionAnalyser.php create mode 100644 Source/Analysis/IField.php create mode 100644 Source/Analysis/IFunction.php create mode 100644 Source/Analysis/IIndexer.php create mode 100644 Source/Analysis/IMethod.php create mode 100644 Source/Analysis/INativeType.php create mode 100644 Source/Analysis/IObjectType.php create mode 100644 Source/Analysis/IType.php create mode 100644 Source/Analysis/ITypeAnalysis.php create mode 100644 Source/Analysis/ITypeOperation.php create mode 100644 Source/Analysis/ITypeSystem.php create mode 100644 Source/Analysis/ITyped.php create mode 100644 Source/Analysis/PhpTypeSystem.php create mode 100644 Source/Analysis/TypeAnalysis.php create mode 100644 Source/Analysis/TypeData/DateTime.php create mode 100644 Source/Analysis/TypeData/InternalFunctions.php create mode 100644 Source/Analysis/TypeData/InternalTypes.php create mode 100644 Source/Analysis/TypeData/PinqAPI.php create mode 100644 Source/Analysis/TypeData/TypeDataModule.php create mode 100644 Source/Analysis/TypeException.php create mode 100644 Source/Analysis/TypeId.php create mode 100644 Source/Analysis/TypeOperations/Cast.php create mode 100644 Source/Analysis/TypeOperations/Constructor.php create mode 100644 Source/Analysis/TypeOperations/Field.php create mode 100644 Source/Analysis/TypeOperations/Indexer.php create mode 100644 Source/Analysis/TypeOperations/Method.php create mode 100644 Source/Analysis/TypeOperations/TypeOperation.php create mode 100644 Source/Analysis/TypeSystem.php create mode 100644 Source/Analysis/Typed.php create mode 100644 Source/Analysis/Types/CompositeType.php create mode 100644 Source/Analysis/Types/MixedType.php create mode 100644 Source/Analysis/Types/NativeType.php create mode 100644 Source/Analysis/Types/ObjectType.php create mode 100644 Source/Analysis/Types/Type.php create mode 100644 Tests/Integration/Analysis/BasicExpressionAnalysisTest.php create mode 100644 Tests/Integration/Analysis/ComplexExpressionAnalysisTest.php create mode 100644 Tests/Integration/Analysis/ExpressionAnalysisTestCase.php create mode 100644 Tests/Integration/Analysis/TypeAnalysisTest.php create mode 100644 Tests/Integration/Analysis/TypeSystemTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 00bb6c0..c1d8e60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ dev-master - Extracted interfaces from `Request`/`Operation`/`Segment` visitor classes. - Removed obsolete query providers (`Loadable`, `Caching`) in favour of a new integrated helper `Providers\Utilities\IQueryResultCollection` - Implemented new DSL query provider under `Providers\DSL`. + - Implemented new static analysis tools and infrastructure for expression trees under the `Analysis` namespace. - New structure of query providers - `RepositoryProvider` decorates the `QueryProvider` - New configuration classes (under `Providers\Configuration` namespace) diff --git a/Source/Analysis/AnalysisContext.php b/Source/Analysis/AnalysisContext.php new file mode 100644 index 0000000..98a5ce5 --- /dev/null +++ b/Source/Analysis/AnalysisContext.php @@ -0,0 +1,60 @@ + + */ +class AnalysisContext implements IAnalysisContext +{ + /** + * @var O\IEvaluationContext + */ + protected $evaluationContext; + + /** + * @var IType[] + */ + protected $expressionTypes = []; + + public function __construct(O\IEvaluationContext $evaluationContext) + { + + $this->evaluationContext = $evaluationContext; + } + + public function getEvaluationContext() + { + return $this->evaluationContext; + } + + public function getExpressionType(O\Expression $expression) + { + $hash = $expression->hash(); + return isset($this->expressionTypes[$hash]) ? $this->expressionTypes[$hash] : null; + } + + public function setExpressionType(O\Expression $expression, IType $type) + { + $this->expressionTypes[$expression->hash()] = $type; + } + + public function removeExpressionType(O\Expression $expression) + { + unset($this->expressionTypes[$expression->hash()]); + } + + public function createReference(O\Expression $expression, O\Expression $referencedExpression) + { + $this->expressionTypes[$expression->hash()] =& $this->expressionTypes[$referencedExpression->hash()]; + } + + public function inNewScope() + { + return new self($this->evaluationContext); + } +} \ No newline at end of file diff --git a/Source/Analysis/BinaryOperations/BinaryOperation.php b/Source/Analysis/BinaryOperations/BinaryOperation.php new file mode 100644 index 0000000..eeb39e3 --- /dev/null +++ b/Source/Analysis/BinaryOperations/BinaryOperation.php @@ -0,0 +1,65 @@ + + */ +class BinaryOperation extends Typed implements IBinaryOperation +{ + /** + * @var string + */ + protected $leftOperandType; + + /** + * @var int + */ + protected $operator; + + /** + * @var string + */ + protected $rightOperandType; + + /** + * @var string + */ + protected $returnType; + + public function __construct(ITypeSystem $typeSystem, $leftOperandType, $operator, $rightOperandType, $returnType) + { + parent::__construct($typeSystem); + $this->leftOperandType = $leftOperandType; + $this->operator = $operator; + $this->rightOperandType = $rightOperandType; + $this->returnType = $returnType; + } + + public function getLeftOperandType() + { + return $this->typeSystem->getType($this->leftOperandType); + } + + public function getOperator() + { + return $this->operator; + } + + public function getRightOperandType() + { + return $this->typeSystem->getType($this->rightOperandType); + } + + public function getReturnType() + { + return $this->typeSystem->getType($this->returnType); + } +} \ No newline at end of file diff --git a/Source/Analysis/ExpressionAnalyser.php b/Source/Analysis/ExpressionAnalyser.php new file mode 100644 index 0000000..156d5d1 --- /dev/null +++ b/Source/Analysis/ExpressionAnalyser.php @@ -0,0 +1,351 @@ + + */ +class ExpressionAnalyser extends O\ExpressionVisitor implements IExpressionAnalyser +{ + /** + * @var ITypeSystem + */ + protected $typeSystem; + + /** + * @var IAnalysisContext + */ + protected $analysisContext; + + /** + * @var \SplObjectStorage|IType[] + */ + protected $analysis; + + /** + * @var \SplObjectStorage + */ + protected $metadata; + + public function __construct(ITypeSystem $typeSystem) + { + $this->typeSystem = $typeSystem; + } + + public function getTypeSystem() + { + return $this->typeSystem; + } + + public function analyse(IAnalysisContext $analysisContext, O\Expression $expression) + { + $this->analysisContext = $analysisContext; + $this->analysis = new \SplObjectStorage(); + $this->metadata = new \SplObjectStorage(); + + $this->walk($expression); + + return new TypeAnalysis($this->typeSystem, $expression, $this->analysis, $this->metadata); + } + + public function visitArray(O\ArrayExpression $expression) + { + $this->walkAll($expression->getItems()); + $this->analysis[$expression] = $this->typeSystem->getNativeType(INativeType::TYPE_ARRAY); + } + + public function visitArrayItem(O\ArrayItemExpression $expression) + { + $this->walk($expression->getKey()); + $this->walk($expression->getValue()); + } + + public function visitAssignment(O\AssignmentExpression $expression) + { + $assignTo = $expression->getAssignTo(); + $assignmentValue = $expression->getAssignmentValue(); + + //$this->walk($assignTo); + $this->walk($assignmentValue); + + $operator = $expression->getOperator(); + if ($operator === O\Operators\Assignment::EQUAL) { + $this->analysisContext->setExpressionType($assignTo, $this->analysis[$assignmentValue]); + } elseif ($operator === O\Operators\Assignment::EQUAL_REFERENCE) { + $this->analysisContext->removeExpressionType($assignTo); + $this->analysisContext->setExpressionType($assignTo, $this->analysis[$assignmentValue]); + $this->analysisContext->createReference($assignTo, $assignmentValue); + } else { + $binaryOperation = $this->typeSystem->getBinaryOperation( + $this->analysis[$assignTo], + O\Operators\Assignment::toBinaryOperator($operator), + $this->analysis[$assignmentValue] + ); + $this->analysis[$expression] = $binaryOperation->getReturnType(); + } + } + + public function visitBinaryOperation(O\BinaryOperationExpression $expression) + { + $this->walk($expression->getLeftOperand()); + $this->walk($expression->getRightOperand()); + + $binaryOperation = $this->typeSystem->getBinaryOperation( + $this->analysis[$expression->getLeftOperand()], + $expression->getOperator(), + $this->analysis[$expression->getRightOperand()] + ); + $this->metadata[$expression] = $binaryOperation; + $this->analysis[$expression] = $binaryOperation->getReturnType(); + } + + protected function addTypeOperation(O\Expression $expression, ITypeOperation $typeOperation) + { + $this->metadata[$expression] = $typeOperation; + $this->analysis[$expression] = $typeOperation->getReturnType(); + } + + public function visitUnaryOperation(O\UnaryOperationExpression $expression) + { + $this->walk($expression->getOperand()); + $this->addTypeOperation( + $expression, + $this->analysis[$expression->getOperand()]->getUnaryOperation($expression) + ); + } + + public function visitCast(O\CastExpression $expression) + { + $this->walk($expression->getCastValue()); + $this->addTypeOperation( + $expression, + $this->analysis[$expression->getCastValue()]->getCast($expression) + ); + } + + public function visitConstant(O\ConstantExpression $expression) + { + $this->analysis[$expression] = $this->typeSystem->getTypeFromValue($expression->evaluate($this->analysisContext->getEvaluationContext())); + } + + public function visitClassConstant(O\ClassConstantExpression $expression) + { + $this->validateStaticClassName($expression->getClass(), 'class constant'); + $this->analysis[$expression] = $this->typeSystem->getTypeFromValue($expression->evaluate($this->analysisContext->getEvaluationContext())); + } + + public function visitEmpty(O\EmptyExpression $expression) + { + $this->walk($expression->getValue()); + $this->analysis[$expression] = $this->typeSystem->getNativeType(INativeType::TYPE_BOOL); + } + + public function visitIsset(O\IssetExpression $expression) + { + $this->walkAll($expression->getValues()); + $this->analysis[$expression] = $this->typeSystem->getNativeType(INativeType::TYPE_BOOL); + } + + public function visitUnset(O\UnsetExpression $expression) + { + $this->walkAll($expression->getValues()); + $this->analysis[$expression] = $this->typeSystem->getType(INativeType::TYPE_NULL); + } + + public function visitField(O\FieldExpression $expression) + { + $this->walk($expression->getValue()); + $this->walk($expression->getName()); + + $this->addTypeOperation( + $expression, + $this->analysis[$expression->getValue()]->getField($expression) + ); + } + + public function visitMethodCall(O\MethodCallExpression $expression) + { + $this->walk($expression->getValue()); + $this->walk($expression->getName()); + $this->walkAll($expression->getArguments()); + + $this->addTypeOperation( + $expression, + $this->analysis[$expression->getValue()]->getMethod($expression) + ); + } + + public function visitIndex(O\IndexExpression $expression) + { + $this->walk($expression->getValue()); + $this->walk($expression->getIndex()); + + $this->addTypeOperation( + $expression, + $this->analysis[$expression->getValue()]->getIndex($expression) + ); + } + + public function visitInvocation(O\InvocationExpression $expression) + { + $this->walk($expression->getValue()); + $this->walkAll($expression->getArguments()); + + $this->addTypeOperation( + $expression, + $this->analysis[$expression->getValue()]->getInvocation($expression) + ); + } + + public function visitFunctionCall(O\FunctionCallExpression $expression) + { + $nameExpression = $expression->getName(); + $this->walk($nameExpression); + $this->walkAll($expression->getArguments()); + + if ($nameExpression instanceof O\ValueExpression) { + $function = $this->typeSystem->getFunction($nameExpression->getValue()); + $this->metadata[$expression] = $function; + $this->analysis[$expression] = $function->getReturnType(); + } else { + throw new TypeException('Invalid function expression: dynamic function calls are not allowed'); + } + } + + protected function validateStaticClassName(O\Expression $expression, $type) + { + if ($expression instanceof O\ValueExpression) { + return $expression->getValue(); + } else { + throw new TypeException('Invalid %s expression: dynamic class types are not supported', $type); + } + } + + public function visitStaticMethodCall(O\StaticMethodCallExpression $expression) + { + $classExpression = $expression->getClass(); + $this->walk($classExpression); + $this->walk($expression->getName()); + $this->walkAll($expression->getArguments()); + + $class = $this->validateStaticClassName($classExpression, 'static method call'); + $this->addTypeOperation( + $expression, + $this->typeSystem->getObjectType($class)->getStaticMethod($expression) + ); + } + + public function visitStaticField(O\StaticFieldExpression $expression) + { + $classExpression = $expression->getClass(); + $this->walk($classExpression); + $this->walk($expression->getName()); + + $class = $this->validateStaticClassName($classExpression, 'static field'); + + $this->addTypeOperation( + $expression, + $this->typeSystem->getObjectType($class)->getStaticField($expression) + ); + } + + public function visitNew(O\NewExpression $expression) + { + $classExpression = $expression->getClass(); + $this->walk($classExpression); + $this->walkAll($expression->getArguments()); + + $class = $this->validateStaticClassName($classExpression, 'new'); + $this->addTypeOperation( + $expression, + $this->typeSystem->getObjectType($class)->getConstructor($expression) + ); + } + + public function visitTernary(O\TernaryExpression $expression) + { + $this->walk($expression->getCondition()); + $this->walk($expression->getIfTrue()); + $this->walk($expression->getIfFalse()); + + $this->analysis[$expression] = $this->typeSystem->getCommonAncestorType( + $this->analysis[$expression->hasIfTrue() ? $expression->getIfTrue() : $expression->getCondition()], + $this->analysis[$expression->getIfFalse()] + ); + } + + public function visitVariable(O\VariableExpression $expression) + { + $nameExpression = $expression->getName(); + $this->walk($nameExpression); + + $type = $this->analysisContext->getExpressionType($expression); + if ($type === null) { + throw new TypeException( + 'Invalid variable expression: \'%s\' type is unknown', + $nameExpression->compileDebug()); + } + + $this->analysis[$expression] = $type; + } + + public function visitValue(O\ValueExpression $expression) + { + $this->analysis[$expression] = $this->typeSystem->getTypeFromValue($expression->getValue()); + } + + public function visitClosure(O\ClosureExpression $expression) + { + $originalContext = $this->analysisContext; + $this->analysisContext = $originalContext->inNewScope(); + + foreach ($expression->getParameters() as $parameter) { + $this->walk($parameter); + $typeHintType = $this->typeSystem->getTypeFromTypeHint($parameter->getTypeHint()); + if (!$parameter->hasDefaultValue() + || $this->analysis[$parameter->getDefaultValue()]->isEqualTo($typeHintType) + ) { + $this->analysisContext->setExpressionType($parameter->asVariable(), $typeHintType); + } else { + $this->analysisContext->setExpressionType( + $parameter->asVariable(), + $this->typeSystem->getNativeType(INativeType::TYPE_MIXED) + ); + } + } + + foreach ($expression->getUsedVariables() as $usedVariable) { + $variable = $usedVariable->asVariable(); + //TODO: handle references with used variables. Probably impossible though. + $this->analysisContext->setExpressionType($variable, $originalContext->getExpressionType($variable)); + } + + $this->walkAll($expression->getBodyExpressions()); + $this->analysis[$expression] = $this->typeSystem->getObjectType('Closure'); + $this->analysisContext = $originalContext; + } + + public function visitReturn(O\ReturnExpression $expression) + { + $this->walk($expression->getValue()); + } + + public function visitThrow(O\ThrowExpression $expression) + { + $this->walk($expression->getException()); + } + + public function visitParameter(O\ParameterExpression $expression) + { + $this->walk($expression->getDefaultValue()); + } + + public function visitArgument(O\ArgumentExpression $expression) + { + $this->walk($expression->getValue()); + } +} \ No newline at end of file diff --git a/Source/Analysis/Functions/Func.php b/Source/Analysis/Functions/Func.php new file mode 100644 index 0000000..8c92cea --- /dev/null +++ b/Source/Analysis/Functions/Func.php @@ -0,0 +1,59 @@ + + */ +class Func extends Typed implements IFunction +{ + /** + * @var string + */ + protected $name; + + /** + * @var \ReflectionFunction + */ + protected $reflection; + + /** + * @var string + */ + protected $returnType; + + public function __construct(ITypeSystem $typeSystem, $name, $returnType) + { + parent::__construct($typeSystem); + $this->name = $name; + $this->reflection = new \ReflectionFunction($name); + $this->returnType = $returnType; + } + + public function getName() + { + return $this->name; + } + + public function getReflection() + { + return $this->reflection; + } + + public function getReturnType() + { + return $this->typeSystem->getType($this->returnType); + } + + public function getReturnTypeWithArguments(array $staticArguments) + { + return $this->typeSystem->getType($this->returnType); + } +} \ No newline at end of file diff --git a/Source/Analysis/IAnalysisContext.php b/Source/Analysis/IAnalysisContext.php new file mode 100644 index 0000000..9cf1c83 --- /dev/null +++ b/Source/Analysis/IAnalysisContext.php @@ -0,0 +1,67 @@ + + */ +interface IAnalysisContext +{ + /** + * Gets the evaluation context. + * + * @return O\IEvaluationContext + */ + public function getEvaluationContext(); + + /** + * Gets the type of the expression. + * Null if no type has been set. + * The expression is compared using value equality (same code). + * + * @param O\Expression $expression + * + * @return IType|null + */ + public function getExpressionType(O\Expression $expression); + + /** + * Sets the type of the expression. + * + * @param O\Expression $expression + * @param IType $type + * + * @return void + */ + public function setExpressionType(O\Expression $expression, IType $type); + + /** + * Removes the type of the expression. + * + * @param O\Expression $expression + * + * @return void + */ + public function removeExpressionType(O\Expression $expression); + + /** + * Creates a reference between the supplied expressions. + * + * @param O\Expression $expression + * @param O\Expression $referencedExpression + * + * @return void + */ + public function createReference(O\Expression $expression, O\Expression $referencedExpression); + + /** + * Creates a new analysis context with an empty expression type list. + * + * @return IAnalysisContext + */ + public function inNewScope(); +} \ No newline at end of file diff --git a/Source/Analysis/IBinaryOperation.php b/Source/Analysis/IBinaryOperation.php new file mode 100644 index 0000000..100b67b --- /dev/null +++ b/Source/Analysis/IBinaryOperation.php @@ -0,0 +1,41 @@ + + */ +interface IBinaryOperation extends ITyped +{ + /** + * Gets the type of the left operand. + * + * @return IType + */ + public function getLeftOperandType(); + + /** + * Gets the operator of the binary operation. + * + * @return int The binary operator from the Expressions\Operators\Binary::* constants + */ + public function getOperator(); + + /** + * Gets the type of the right operand. + * + * @return IType + */ + public function getRightOperandType(); + + /** + * Gets the returned type of the binary operation. + * + * @return IType + */ + public function getReturnType(); +} \ No newline at end of file diff --git a/Source/Analysis/ICallable.php b/Source/Analysis/ICallable.php new file mode 100644 index 0000000..e192943 --- /dev/null +++ b/Source/Analysis/ICallable.php @@ -0,0 +1,43 @@ + + */ +interface ICallable extends ITyped +{ + /** + * Gets the name. + * + * @return string + */ + public function getName(); + + /** + * Gets the reflection of the function. + * + * @return \ReflectionFunctionAbstract + */ + public function getReflection(); + + /** + * Gets the return type of the function. + * + * @return IType + */ + public function getReturnType(); + + /** + * Gets the return type of the function with the supplied arguments array. + * + * @param array $staticArguments The argument values indexed by their position. + * + * @return IType + */ + public function getReturnTypeWithArguments(array $staticArguments); +} \ No newline at end of file diff --git a/Source/Analysis/ICompositeType.php b/Source/Analysis/ICompositeType.php new file mode 100644 index 0000000..3b0d032 --- /dev/null +++ b/Source/Analysis/ICompositeType.php @@ -0,0 +1,20 @@ + + */ +interface ICompositeType extends IType +{ + /** + * Gets the composed types. + * + * @return IType[] + */ + public function getComposedTypes(); +} \ No newline at end of file diff --git a/Source/Analysis/IConstructor.php b/Source/Analysis/IConstructor.php new file mode 100644 index 0000000..ea07e89 --- /dev/null +++ b/Source/Analysis/IConstructor.php @@ -0,0 +1,29 @@ + + * new stdClass(); + * + * + * @author Elliot Levin + */ +interface IConstructor extends ITypeOperation +{ + /** + * Whether the type has a __construct method. + * + * @return boolean + */ + public function hasMethod(); + + /** + * Gets the reflection of the constructor. + * Null if there is no __construct method. + * + * @return \ReflectionMethod|null + */ + public function getReflection(); +} \ No newline at end of file diff --git a/Source/Analysis/IExpressionAnalyser.php b/Source/Analysis/IExpressionAnalyser.php new file mode 100644 index 0000000..bd1ea66 --- /dev/null +++ b/Source/Analysis/IExpressionAnalyser.php @@ -0,0 +1,30 @@ + + */ +interface IExpressionAnalyser +{ + /** + * Gets the type system for the expression analyser. + * + * @return ITypeSystem + */ + public function getTypeSystem(); + + /** + * Analyses the supplied expression tree. + * + * @param IAnalysisContext $analysisContext + * @param O\Expression $expression + * + * @return ITypeAnalysis + */ + public function analyse(IAnalysisContext $analysisContext, O\Expression $expression); +} \ No newline at end of file diff --git a/Source/Analysis/IField.php b/Source/Analysis/IField.php new file mode 100644 index 0000000..295d405 --- /dev/null +++ b/Source/Analysis/IField.php @@ -0,0 +1,28 @@ + + * $val->field; + * + * + * @author Elliot Levin + */ +interface IField extends ITypeOperation +{ + /** + * Gets the name of the field. + * + * @return IType + */ + public function getName(); + + /** + * Whether the field is static. + * + * @return boolean + */ + public function isStatic(); +} \ No newline at end of file diff --git a/Source/Analysis/IFunction.php b/Source/Analysis/IFunction.php new file mode 100644 index 0000000..6ff9fbf --- /dev/null +++ b/Source/Analysis/IFunction.php @@ -0,0 +1,27 @@ + + */ +interface IFunction extends ICallable +{ + /** + * Gets the function name. + * + * @return string + */ + public function getName(); + + /** + * Gets the reflection of the function. + * + * @return \ReflectionFunction + */ + public function getReflection(); +} \ No newline at end of file diff --git a/Source/Analysis/IIndexer.php b/Source/Analysis/IIndexer.php new file mode 100644 index 0000000..3ad6df6 --- /dev/null +++ b/Source/Analysis/IIndexer.php @@ -0,0 +1,23 @@ + + * $val['index']; + * + * + * @author Elliot Levin + */ +interface IIndexer extends ITypeOperation +{ + /** + * Gets the return type of the indexer with the supplied index value. + * + * @param mixed $index + * + * @return IType + */ + public function getReturnTypeOfIndex($index); +} \ No newline at end of file diff --git a/Source/Analysis/IMethod.php b/Source/Analysis/IMethod.php new file mode 100644 index 0000000..975c5d5 --- /dev/null +++ b/Source/Analysis/IMethod.php @@ -0,0 +1,25 @@ + + */ +interface IMethod extends ICallable, ITypeOperation +{ + /** + * Gets the name of the method. + * + * @return string + */ + public function getName(); + + /** + * Gets the reflection of the method. + * + * @return \ReflectionMethod + */ + public function getReflection(); +} \ No newline at end of file diff --git a/Source/Analysis/INativeType.php b/Source/Analysis/INativeType.php new file mode 100644 index 0000000..b354fa8 --- /dev/null +++ b/Source/Analysis/INativeType.php @@ -0,0 +1,29 @@ + + */ +interface INativeType extends IType +{ + const TYPE_MIXED = 'native:mixed'; + const TYPE_STRING = 'native:string'; + const TYPE_INT = 'native:int'; + const TYPE_ARRAY = 'native:array'; + const TYPE_DOUBLE = 'native:double'; + const TYPE_BOOL = 'native:boolean'; + const TYPE_NULL = 'native:null'; + const TYPE_RESOURCE = 'native:resource'; + + /** + * Gets the type of the type represented by the TYPE_* constants. + * + * @return string + */ + public function getTypeOfType(); +} \ No newline at end of file diff --git a/Source/Analysis/IObjectType.php b/Source/Analysis/IObjectType.php new file mode 100644 index 0000000..7dea66b --- /dev/null +++ b/Source/Analysis/IObjectType.php @@ -0,0 +1,27 @@ + + */ +interface IObjectType extends IType +{ + /** + * Gets the qualified class name. + * + * @return string + */ + public function getClassType(); + + /** + * Gets the reflection of the class type. + * + * @return \ReflectionClass + */ + public function getReflection(); +} \ No newline at end of file diff --git a/Source/Analysis/IType.php b/Source/Analysis/IType.php new file mode 100644 index 0000000..f0879da --- /dev/null +++ b/Source/Analysis/IType.php @@ -0,0 +1,142 @@ + + */ +interface IType +{ + /** + * Whether the type has a parent type. + * + * @return boolean + */ + public function hasParentType(); + + /** + * Gets the parent type or null if their is no parent. + * + * @return IType|null + */ + public function getParentType(); + + /** + * Whether the supplied type is equivalent to the current type. + * + * @param IType $type + * + * @return boolean + */ + public function isEqualTo(IType $type); + + /** + * Whether the supplied type is a subtype of or equal to the current type. + * + * @param IType $type + * + * @return boolean + */ + public function isParentTypeOf(IType $type); + + /** + * Gets a unique string representation of the type. + * + * @return string + */ + public function getIdentifier(); + + /** + * Gets the supplied expression matches the type's constructor. + * + * @param O\NewExpression $expression + * + * @return IConstructor + * @throws TypeException if the constructor is not supported. + */ + public function getConstructor(O\NewExpression $expression); + + /** + * Gets the matched method of the supplied expression. + * + * @param O\MethodCallExpression $expression + * + * @return IMethod + * @throws TypeException if the method is not supported. + */ + public function getMethod(O\MethodCallExpression $expression); + + /** + * Gets the matched method of the supplied expression. + * + * @param O\StaticMethodCallExpression $expression + * + * @return IMethod + * @throws TypeException if the static method is not supported. + */ + public function getStaticMethod(O\StaticMethodCallExpression $expression); + + /** + * Gets the matched field of the supplied expression. + * + * @param O\FieldExpression $expression + * + * @return IField + * @throws TypeException if the field is not supported. + */ + public function getField(O\FieldExpression $expression); + + /** + * Gets the matched field of the supplied expression. + * + * @param O\StaticFieldExpression $expression + * + * @return IField + * @throws TypeException if the static field is not supported. + */ + public function getStaticField(O\StaticFieldExpression $expression); + + /** + * Get the supplied index expression matches the type. + * + * @param O\IndexExpression $expression + * + * @return ITypeOperation + * @throws TypeException if the indexer is not supported. + */ + public function getIndex(O\IndexExpression $expression); + + /** + * Gets the invocation expression matches the type. + * + * @param O\InvocationExpression $expression + * + * @return ITypeOperation|IMethod + * @throws TypeException if the invocation is not supported. + */ + public function getInvocation(O\InvocationExpression $expression); + + /** + * Gets the matched unary operator from the supplied expression. + * + * @param O\CastExpression $expression + * + * @return ITypeOperation|IMethod + * @throws TypeException if the cast is not supported. + */ + public function getCast(O\CastExpression $expression); + + /** + * Gets the matched unary operator from the supplied expression. + * + * @param O\UnaryOperationExpression $expression + * + * @return ITypeOperation|IMethod + * @throws TypeException if the unary operation is not supported. + */ + public function getUnaryOperation(O\UnaryOperationExpression $expression); +} \ No newline at end of file diff --git a/Source/Analysis/ITypeAnalysis.php b/Source/Analysis/ITypeAnalysis.php new file mode 100644 index 0000000..c0fc2e2 --- /dev/null +++ b/Source/Analysis/ITypeAnalysis.php @@ -0,0 +1,147 @@ + + */ +interface ITypeAnalysis extends ITyped +{ + /** + * Gets the analysed expression tree. + * + * @return O\Expression + */ + public function getExpression(); + + /** + * Gets the returned type analysed expression. + * + * @return IType + */ + public function getReturnedType(); + + /** + * Gets the returned type of the supplied expression. + * + * @param O\Expression $expression + * + * @return IType + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getReturnTypeOf(O\Expression $expression); + + /** + * Gets the type data for the supplied function expression. + * + * @param O\FunctionCallExpression $expression + * + * @return IFunction + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getFunction(O\FunctionCallExpression $expression); + + /** + * Gets the type data for the supplied static method expression. + * + * @param O\StaticMethodCallExpression $expression + * + * @return IMethod + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getStaticMethod(O\StaticMethodCallExpression $expression); + + /** + * Gets the type data for the supplied static field expression. + * + * @param O\StaticFieldExpression $expression + * + * @return IField + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getStaticField(O\StaticFieldExpression $expression); + + /** + * Gets the type data for the supplied method expression. + * + * @param O\MethodCallExpression $expression + * + * @return IMethod + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getMethod(O\MethodCallExpression $expression); + + /** + * Gets the type data for the supplied field expression. + * + * @param O\FieldExpression $expression + * + * @return IField + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getField(O\FieldExpression $expression); + + /** + * Gets the type data for the supplied index expression. + * + * @param O\IndexExpression $expression + * + * @return ITypeOperation + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getIndex(O\IndexExpression $expression); + + /** + * Gets the type data for the supplied invocation expression. + * + * @param O\InvocationExpression $expression + * + * @return ITypeOperation + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getInvocation(O\InvocationExpression $expression); + + /** + * Gets the type data for the supplied unary operation operation. + * + * @param O\UnaryOperationExpression $expression + * + * @return ITypeOperation + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getUnaryOperation(O\UnaryOperationExpression $expression); + + /** + * Gets the type data for the supplied unary cast operation. + * + * @param O\CastExpression $expression + * + * @return ITypeOperation + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getCast(O\CastExpression $expression); + + /** + * Gets the type data for the supplied new operation. + * + * @param O\NewExpression $expression + * + * @return IConstructor + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getConstructor(O\NewExpression $expression); + + /** + * Gets the type data for the supplied binary operation. + * + * @param O\BinaryOperationExpression $expression + * + * @return IBinaryOperation + * @throws TypeException if the supplied expression was not in the analysed expression tree. + */ + public function getBinaryOperation(O\BinaryOperationExpression $expression); +} \ No newline at end of file diff --git a/Source/Analysis/ITypeOperation.php b/Source/Analysis/ITypeOperation.php new file mode 100644 index 0000000..03e796b --- /dev/null +++ b/Source/Analysis/ITypeOperation.php @@ -0,0 +1,27 @@ + + */ +interface ITypeOperation extends ITyped +{ + /** + * Gets the type being operated on. + * + * @return IType + */ + public function getSourceType(); + + /** + * Gets return type of the operation. + * + * @return IType + */ + public function getReturnType(); +} \ No newline at end of file diff --git a/Source/Analysis/ITypeSystem.php b/Source/Analysis/ITypeSystem.php new file mode 100644 index 0000000..8c45793 --- /dev/null +++ b/Source/Analysis/ITypeSystem.php @@ -0,0 +1,101 @@ + + */ +interface ITypeSystem +{ + /** + * Gets of the type with the supplied identifier. + * + * @param string $typeIdentifier + * + * @return IType + */ + public function getType($typeIdentifier); + + /** + * Gets of the type of the supplied value. + * + * @param mixed $value + * + * @return IType + * @throws TypeException If the type is not supported. + */ + public function getTypeFromValue($value); + + /** + * Gets of the type from the supplied parameter type hint. + * + * @param string|null $typeHint + * + * @return IType + */ + public function getTypeFromTypeHint($typeHint); + + /** + * Gets of a common ancestor type of the supplied types. + * + * @param IType $type + * @param IType $otherType + * + * @return IType + */ + public function getCommonAncestorType(IType $type, IType $otherType); + + /** + * Gets the native type with the supplied int from the INativeType::TYPE_* constants. + * + * @param string $nativeType + * + * @return INativeType + * @throws TypeException If the native type is not supported. + */ + public function getNativeType($nativeType); + + /** + * Gets the object type with the supplied class name. + * + * @param string $classType + * + * @return IObjectType + * @throws TypeException If the class is not supported. + */ + public function getObjectType($classType); + + /** + * Gets a type composed of the supplied types. + * + * @param IType[] $types + * + * @return IType + * @throws TypeException If the types is not supported. + */ + public function getCompositeType(array $types); + + /** + * Gets the function with the supplied name. + * + * @param string $name + * + * @return IFunction + * @throws TypeException If the function is not supported. + */ + public function getFunction($name); + + /** + * Gets the binary operation matching the supplied types. + * + * @param IType $leftOperandType + * @param int $operator + * @param IType $rightOperandType + * + * @return IBinaryOperation + * @throws TypeException If the binary operation is not supported + */ + public function getBinaryOperation(IType $leftOperandType, $operator, IType $rightOperandType); +} \ No newline at end of file diff --git a/Source/Analysis/ITyped.php b/Source/Analysis/ITyped.php new file mode 100644 index 0000000..98b0b63 --- /dev/null +++ b/Source/Analysis/ITyped.php @@ -0,0 +1,18 @@ + + */ +interface ITyped +{ + /** + * Gets the type system. + * + * @return ITypeSystem + */ + public function getTypeSystem(); +} \ No newline at end of file diff --git a/Source/Analysis/PhpTypeSystem.php b/Source/Analysis/PhpTypeSystem.php new file mode 100644 index 0000000..aebf9d2 --- /dev/null +++ b/Source/Analysis/PhpTypeSystem.php @@ -0,0 +1,529 @@ + + */ +class PhpTypeSystem extends TypeSystem +{ + const TYPE_SELF = '~~SELF_TYPE~~'; + + /** + * @var string[] + */ + protected $functionTypeMap = []; + + /** + * @var array[] + */ + protected $classTypeMap = []; + + /** + * @param TypeData\TypeDataModule[] $customTypeDataModules + */ + public function __construct(array $customTypeDataModules = []) + { + parent::__construct(); + + $modules = array_merge($this->typeDataModules(), $customTypeDataModules); + /** @var $module TypeData\TypeDataModule */ + foreach($modules as $module) { + foreach ($module->functions() as $name => $returnType) { + $this->functionTypeMap[$this->normalizeFunctionName($name)] = $returnType; + } + + foreach ($module->types() as $name => $typeData) { + $this->classTypeMap[$this->normalizeClassName($name)] = $typeData; + } + } + } + + /** + * @return TypeData\TypeDataModule[] + */ + protected function typeDataModules() + { + return [ + new TypeData\InternalFunctions(), + new TypeData\InternalTypes(), + new TypeData\DateTime(), + new TypeData\PinqAPI(), + ]; + } + + // Normalize function / type names using reflection to get originally defined name + // but fallback to lower casing due to some functions that are not universally available + // such as 'money_format' which will fail with reflection if not available. + protected function normalizeClassName($name) + { + try { + return (new \ReflectionClass($name))->getName(); + } catch (\Exception $exception) { + return strtolower($name); + } + } + + protected function normalizeFunctionName($name) + { + try { + return (new \ReflectionFunction($name))->getName(); + } catch (\Exception $exception) { + return strtolower($name); + } + } + + protected function buildFunction($name) + { + return new Func( + $this, + $name, + isset($this->functionTypeMap[$name]) ? $this->functionTypeMap[$name] : INativeType::TYPE_MIXED); + } + + protected function buildCompositeType($typeId, array $types) + { + return new CompositeType( + $typeId, + $this->nativeTypes[INativeType::TYPE_MIXED], + $types); + } + + public function getCommonAncestorType(IType $type, IType $otherType) + { + if ($type->isEqualTo($otherType)) { + return $type; + } elseif ($type->isParentTypeOf($otherType)) { + return $type; + } elseif ($otherType->isParentTypeOf($type)) { + return $otherType; + } + + $parentTypes = $this->getAncestorTypes($type); + $otherParentTypes = $this->getAncestorTypes($otherType); + + /** @var $commonParentTypes IType[] */ + $commonParentTypes = array_intersect_key($parentTypes, $otherParentTypes); + + return $this->getCompositeType($commonParentTypes); + } + + public function getTypeFromValue($value) + { + switch (gettype($value)) { + case 'string': + return $this->nativeTypes[INativeType::TYPE_STRING]; + + case 'integer': + return $this->nativeTypes[INativeType::TYPE_INT]; + + case 'boolean': + return $this->nativeTypes[INativeType::TYPE_BOOL]; + + case 'double': + return $this->nativeTypes[INativeType::TYPE_DOUBLE]; + + case 'NULL': + return $this->nativeTypes[INativeType::TYPE_NULL]; + + case 'array': + return $this->nativeTypes[INativeType::TYPE_ARRAY]; + + case 'resource': + case 'unknown type': + return $this->nativeTypes[INativeType::TYPE_RESOURCE]; + + case 'object': + return $this->getObjectType(get_class($value)); + } + } + + public function getTypeFromTypeHint($typeHint) + { + if ($typeHint === null || $typeHint === '') { + return $this->nativeTypes[INativeType::TYPE_MIXED]; + } + + if (strcasecmp($typeHint, 'callable') === 0) { + return $this->nativeTypes[INativeType::TYPE_MIXED]; + } elseif (strcasecmp($typeHint, 'array') === 0) { + return $this->nativeTypes[INativeType::TYPE_ARRAY]; + } else { + return $this->getObjectType($typeHint); + } + } + + protected function nativeCasts() + { + return [ + Operators\Cast::STRING => INativeType::TYPE_STRING, + Operators\Cast::BOOLEAN => INativeType::TYPE_BOOL, + Operators\Cast::INTEGER => INativeType::TYPE_INT, + Operators\Cast::DOUBLE => INativeType::TYPE_DOUBLE, + Operators\Cast::ARRAY_CAST => INativeType::TYPE_ARRAY, + Operators\Cast::OBJECT => TypeId::getObject('stdClass'), + ]; + } + + protected function nativeType( + $typeOfType, + IIndexer $indexer = null, + array $unaryOperatorMap = [], + array $castMap = [] + ) { + return new NativeType( + $typeOfType, + $this->nativeTypes[INativeType::TYPE_MIXED], + $typeOfType, + $indexer, + $this->buildTypeOperations($typeOfType, array_filter($castMap + $this->nativeCasts())), + $this->buildTypeOperations( + $typeOfType, + array_filter($unaryOperatorMap + $this->commonNativeUnaryOperations()) + )); + } + + protected function commonNativeUnaryOperations() + { + return [ + Operators\Unary::NOT => INativeType::TYPE_BOOL, + Operators\Unary::PLUS => INativeType::TYPE_INT, + Operators\Unary::NEGATION => INativeType::TYPE_INT, + ]; + } + + protected function nativeTypes() + { + $this->nativeTypes[INativeType::TYPE_MIXED] = new MixedType(INativeType::TYPE_MIXED); + return [ + $this->nativeTypes[INativeType::TYPE_MIXED], + $this->nativeType( + INativeType::TYPE_STRING, + new Indexer($this, INativeType::TYPE_STRING, INativeType::TYPE_STRING), + [ + Operators\Unary::BITWISE_NOT => INativeType::TYPE_STRING, + Operators\Unary::INCREMENT => INativeType::TYPE_STRING, + Operators\Unary::DECREMENT => INativeType::TYPE_STRING, + Operators\Unary::PRE_INCREMENT => INativeType::TYPE_MIXED, + Operators\Unary::PRE_DECREMENT => INativeType::TYPE_MIXED, + ] + ), + $this->nativeType( + INativeType::TYPE_ARRAY, + new Indexer($this, INativeType::TYPE_ARRAY, INativeType::TYPE_MIXED), + [ + Operators\Unary::PLUS => null, + Operators\Unary::NEGATION => null, + ], + [ + Operators\Cast::STRING => null, + ] + ), + $this->nativeType( + INativeType::TYPE_INT, + null, + [ + Operators\Unary::BITWISE_NOT => INativeType::TYPE_INT, + Operators\Unary::INCREMENT => INativeType::TYPE_INT, + Operators\Unary::DECREMENT => INativeType::TYPE_INT, + Operators\Unary::PRE_INCREMENT => INativeType::TYPE_INT, + Operators\Unary::PRE_DECREMENT => INativeType::TYPE_INT, + ] + ), + $this->nativeType( + INativeType::TYPE_BOOL, + null, + [ + Operators\Unary::INCREMENT => INativeType::TYPE_BOOL, + Operators\Unary::DECREMENT => INativeType::TYPE_BOOL, + Operators\Unary::PRE_INCREMENT => INativeType::TYPE_BOOL, + Operators\Unary::PRE_DECREMENT => INativeType::TYPE_BOOL, + ] + ), + $this->nativeType( + INativeType::TYPE_DOUBLE, + null, + [ + Operators\Unary::BITWISE_NOT => INativeType::TYPE_INT, + Operators\Unary::PLUS => INativeType::TYPE_DOUBLE, + Operators\Unary::NEGATION => INativeType::TYPE_DOUBLE, + Operators\Unary::INCREMENT => INativeType::TYPE_DOUBLE, + Operators\Unary::DECREMENT => INativeType::TYPE_DOUBLE, + Operators\Unary::PRE_INCREMENT => INativeType::TYPE_DOUBLE, + Operators\Unary::PRE_DECREMENT => INativeType::TYPE_DOUBLE, + ] + ), + $this->nativeType(INativeType::TYPE_NULL), + $this->nativeType(INativeType::TYPE_RESOURCE), + ]; + } + + protected function getAncestorTypes(IType $type) + { + $ancestorTypes = [$type->getIdentifier() => $type]; + + if (!$type->hasParentType()) { + return $ancestorTypes; + } + + if ($type instanceof ICompositeType) { + foreach ($type->getComposedTypes() as $composedType) { + $ancestorTypes += $this->getAncestorTypes($composedType); + } + } else { + $parentType = $type->getParentType(); + $ancestorTypes[$parentType->getIdentifier()] = $parentType; + $ancestorTypes += $this->getAncestorTypes($parentType); + } + + return $ancestorTypes; + } + + protected function getObjectTypeData($classType) + { + $classType = $this->normalizeClassName($classType); + $data = isset($this->classTypeMap[$classType]) ? $this->classTypeMap[$classType] : []; + + foreach (['methods', 'fields', 'static-fields'] as $property) { + if (!isset($data[$property])) { + $data[$property] = []; + } + + foreach ($data[$property] as &$returnType) { + if ($returnType === self::TYPE_SELF) { + $returnType = TypeId::getObject($classType); + } + } + } + + return $data; + } + + protected function buildObjectType($typeId, $classType) + { + $typeData = $this->getObjectTypeData($classType); + $methodReturnTypeMap = $typeData['methods']; + $fieldTypeMap = $typeData['fields']; + $staticFieldTypeMap = $typeData['static-fields']; + + $reflection = new \ReflectionClass($classType); + $parentType = null; + $constructor = new Constructor($this, $typeId, $reflection->getConstructor()); + $methods = []; + $fields = []; + $indexer = null; + $invoker = null; + $unaryOperations = $this->buildTypeOperations($this->objectUnaryOperations($typeId)); + $casts = $this->buildTypeOperations($this->objectCasts($typeId)); + + $parentTypes = array_map([$this, 'getObjectType'], $reflection->getInterfaceNames()); + if ($parentClass = $reflection->getParentClass()) { + $parentTypes[] = $this->getObjectType($parentClass->getName()); + } + + $parentType = $this->getCompositeType($parentTypes); + + if ($reflection->hasMethod('__toString')) { + $methodReturnTypeMap += ['__toString' => INativeType::TYPE_STRING]; + } + + foreach ($methodReturnTypeMap as $name => $type) { + $methods[$name] = new Method($this, $typeId, $reflection->getMethod($name), $type); + } + + foreach ($reflection->getMethods() as $method) { + if ($method->getDeclaringClass()->getName() === $classType + && !isset($methods[$method->getName()]) + ) { + $methods[$method->getName()] = new Method($this, $typeId, $method, INativeType::TYPE_MIXED); + } + } + + foreach ($staticFieldTypeMap + $fieldTypeMap as $name => $type) { + $fields[$name] = new Field($this, $typeId, $name, isset($staticFieldTypeMap[$name]), $type); + } + + foreach ($reflection->getProperties() as $field) { + if ($field->getDeclaringClass()->getName() === $classType + && !isset($fields[$field->getName()]) + ) { + $fields[$field->getName()] = new Field($this, $typeId, $field->getName(), $field->isStatic( + ), INativeType::TYPE_MIXED); + } + } + + if ($reflection->implementsInterface('ArrayAccess') && isset($methods['offsetGet'])) { + $indexer = $methods['offsetGet']; + } + + if (isset($methods['__invoke'])) { + $invoker = $methods['__invoke']; + } + + if (isset($methods['__toString'])) { + $casts[Operators\Cast::STRING] = $methods['__toString']; + } + + return new ObjectType( + $typeId, + $reflection, + $parentType, + $constructor, + $methods, + $fields, + $unaryOperations, + $casts, + $invoker, + $indexer); + } + + protected function objectCasts($objectTypeId) + { + return [ + Operators\Cast::ARRAY_CAST => INativeType::TYPE_ARRAY, + Operators\Cast::OBJECT => $objectTypeId, + ]; + } + + protected function objectUnaryOperations($objectTypeId) + { + return [ + Operators\Unary::NOT => INativeType::TYPE_BOOL, + Operators\Unary::INCREMENT => $objectTypeId, + Operators\Unary::DECREMENT => $objectTypeId, + Operators\Unary::PRE_INCREMENT => $objectTypeId, + Operators\Unary::PRE_DECREMENT => $objectTypeId, + ]; + } + + protected function booleanOperator($operator) + { + return [INativeType::TYPE_MIXED, $operator, INativeType::TYPE_MIXED, 'return' => INativeType::TYPE_BOOL]; + } + + protected function mathOperators($operator, $otherIntReturnType = INativeType::TYPE_INT) + { + //TODO: remove duplicate operators with types on opposite sides (binary operators are symmetrical) + $operators = []; + foreach ([ + INativeType::TYPE_INT, + INativeType::TYPE_DOUBLE, + INativeType::TYPE_STRING, + INativeType::TYPE_RESOURCE, + INativeType::TYPE_BOOL, + INativeType::TYPE_NULL + ] as $type) { + $operators = array_merge( + $operators, + [ + [$type, $operator, INativeType::TYPE_NULL, 'return' => $otherIntReturnType], + [$type, $operator, INativeType::TYPE_BOOL, 'return' => $otherIntReturnType], + [$type, $operator, INativeType::TYPE_STRING, 'return' => INativeType::TYPE_MIXED], + [$type, $operator, INativeType::TYPE_RESOURCE, 'return' => $otherIntReturnType], + ] + ); + } + + $operators[] = [INativeType::TYPE_INT, $operator, INativeType::TYPE_INT, 'return' => $otherIntReturnType]; + $operators[] = [ + INativeType::TYPE_INT, + $operator, + INativeType::TYPE_DOUBLE, + 'return' => INativeType::TYPE_DOUBLE + ]; + $operators[] = [ + INativeType::TYPE_DOUBLE, + $operator, + INativeType::TYPE_DOUBLE, + 'return' => INativeType::TYPE_DOUBLE + ]; + + return $operators; + } + + protected function bitwiseOperators($operator) + { + //TODO: remove duplicate operators with types on opposite sides (binary operators are symmetrical) + $operators = []; + foreach ([ + INativeType::TYPE_INT, + INativeType::TYPE_DOUBLE, + INativeType::TYPE_STRING, + INativeType::TYPE_RESOURCE, + INativeType::TYPE_BOOL, + INativeType::TYPE_NULL + ] as $type) { + $operators = array_merge( + $operators, + [ + [$type, $operator, INativeType::TYPE_INT, 'return' => INativeType::TYPE_INT], + [$type, $operator, INativeType::TYPE_DOUBLE, 'return' => INativeType::TYPE_INT], + [$type, $operator, INativeType::TYPE_NULL, 'return' => INativeType::TYPE_INT], + [$type, $operator, INativeType::TYPE_BOOL, 'return' => INativeType::TYPE_INT], + [$type, $operator, INativeType::TYPE_STRING, 'return' => INativeType::TYPE_INT], + [$type, $operator, INativeType::TYPE_RESOURCE, 'return' => INativeType::TYPE_INT], + ] + ); + } + + return $operators; + } + + protected function binaryOperations() + { + return array_merge( + [ + $this->booleanOperator(Operators\Binary::EQUALITY), + $this->booleanOperator(Operators\Binary::INEQUALITY), + $this->booleanOperator(Operators\Binary::IDENTITY), + $this->booleanOperator(Operators\Binary::NOT_IDENTICAL), + $this->booleanOperator(Operators\Binary::GREATER_THAN), + $this->booleanOperator(Operators\Binary::GREATER_THAN_OR_EQUAL_TO), + $this->booleanOperator(Operators\Binary::LESS_THAN), + $this->booleanOperator(Operators\Binary::LESS_THAN_OR_EQUAL_TO), + $this->booleanOperator(Operators\Binary::IS_INSTANCE_OF), + $this->booleanOperator(Operators\Binary::EQUALITY), + $this->booleanOperator(Operators\Binary::LOGICAL_AND), + $this->booleanOperator(Operators\Binary::LOGICAL_OR), + [ + INativeType::TYPE_MIXED, + Operators\Binary::CONCATENATION, + INativeType::TYPE_MIXED, + 'return' => INativeType::TYPE_STRING + ], + [ + INativeType::TYPE_ARRAY, + Operators\Binary::ADDITION, + INativeType::TYPE_ARRAY, + 'return' => INativeType::TYPE_ARRAY + ], + ], + $this->mathOperators(Operators\Binary::ADDITION), + $this->mathOperators(Operators\Binary::SUBTRACTION), + $this->mathOperators(Operators\Binary::MULTIPLICATION), + $this->mathOperators(Operators\Binary::DIVISION, INativeType::TYPE_MIXED), + $this->mathOperators(Operators\Binary::MODULUS), + $this->bitwiseOperators(Operators\Binary::BITWISE_AND), + $this->bitwiseOperators(Operators\Binary::BITWISE_OR), + $this->bitwiseOperators(Operators\Binary::BITWISE_XOR), + $this->bitwiseOperators(Operators\Binary::SHIFT_RIGHT), + $this->bitwiseOperators(Operators\Binary::SHIFT_LEFT) + ); + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeAnalysis.php b/Source/Analysis/TypeAnalysis.php new file mode 100644 index 0000000..786a300 --- /dev/null +++ b/Source/Analysis/TypeAnalysis.php @@ -0,0 +1,138 @@ + + */ +class TypeAnalysis implements ITypeAnalysis +{ + /** + * @var ITypeSystem + */ + private $typeSystem; + + /** + * @var O\Expression + */ + private $expression; + + /** + * @var \SplObjectStorage + */ + private $analysis; + + /** + * @var \SplObjectStorage + */ + private $metadata; + + public function __construct( + ITypeSystem $typeSystem, + O\Expression $expression, + \SplObjectStorage $analysis, + \SplObjectStorage $metadata + ) { + + $this->typeSystem = $typeSystem; + $this->expression = $expression; + $this->analysis = $analysis; + $this->metadata = $metadata; + } + + public function getTypeSystem() + { + return $this->typeSystem; + } + + public function getExpression() + { + return $this->expression; + } + + public function getReturnedType() + { + return $this->getReturnTypeOf($this->expression); + } + + public function getReturnTypeOf(O\Expression $expression) + { + if (!isset($this->analysis[$expression])) { + throw new TypeException( + 'Cannot get return type for expression of type \'%s\': the expression has no associated return type', + $expression->getType()); + } + + return $this->analysis[$expression]; + } + + protected function getMetadata(O\Expression $expression) + { + if (!isset($this->metadata[$expression])) { + throw new TypeException( + 'Cannot get metadata for expression of type \'%s\': the expression has no associated metadata', + $expression->getType()); + } + + return $this->metadata[$expression]; + } + + public function getFunction(O\FunctionCallExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getStaticMethod(O\StaticMethodCallExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getStaticField(O\StaticFieldExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getMethod(O\MethodCallExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getField(O\FieldExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getIndex(O\IndexExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getInvocation(O\InvocationExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getUnaryOperation(O\UnaryOperationExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getCast(O\CastExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getConstructor(O\NewExpression $expression) + { + return $this->getMetadata($expression); + } + + public function getBinaryOperation(O\BinaryOperationExpression $expression) + { + return $this->getMetadata($expression); + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeData/DateTime.php b/Source/Analysis/TypeData/DateTime.php new file mode 100644 index 0000000..42c5eb5 --- /dev/null +++ b/Source/Analysis/TypeData/DateTime.php @@ -0,0 +1,85 @@ + + */ +class DateTime extends TypeDataModule +{ + public function functions() + { + return [ + 'time' => INativeType::TYPE_INT, + 'mktime' => INativeType::TYPE_INT, + 'gmmktime' => INativeType::TYPE_INT, + 'strtotime' => INativeType::TYPE_INT, + 'date' => INativeType::TYPE_STRING, + 'idate' => INativeType::TYPE_INT, + 'gmdate' => INativeType::TYPE_STRING, + 'gmstrftime' => INativeType::TYPE_STRING, + 'strptime' => INativeType::TYPE_ARRAY, + 'strftime' => INativeType::TYPE_STRING, + 'localtime' => INativeType::TYPE_ARRAY, + 'getdate' => INativeType::TYPE_ARRAY, + 'checkdate' => INativeType::TYPE_BOOL, + ]; + } + + + public function types() + { + return [ + 'DateTime' => [ + 'methods' => [ + 'add' => self::TYPE_SELF, + 'createFromFormat' => self::TYPE_SELF, + 'getLastErrors' => INativeType::TYPE_ARRAY, + 'modify' => self::TYPE_SELF, + 'setDate' => self::TYPE_SELF, + 'setISODate' => self::TYPE_SELF, + 'setTime' => self::TYPE_SELF, + 'setTimestamp' => self::TYPE_SELF, + 'setTimezone' => self::TYPE_SELF, + 'sub' => self::TYPE_SELF, + 'diff' => TypeId::getObject('DateInterval'), + 'format' => INativeType::TYPE_STRING, + 'getOffset' => INativeType::TYPE_INT, + 'getTimestamp' => INativeType::TYPE_INT, + 'getTimezone' => TypeId::getObject('DateTimeZone'), + ] + ], + 'DateInterval' => [ + 'fields' => [ + 'y' => INativeType::TYPE_INT, + 'm' => INativeType::TYPE_INT, + 'd' => INativeType::TYPE_INT, + 'h' => INativeType::TYPE_INT, + 'i' => INativeType::TYPE_INT, + 's' => INativeType::TYPE_INT, + 'invert' => INativeType::TYPE_INT, + 'days' => INativeType::TYPE_MIXED, + ], + 'methods' => [ + 'createFromDateString' => self::TYPE_SELF, + 'format' => INativeType::TYPE_STRING, + ] + ], + 'DateTimeZone' => [ + 'methods' => [ + 'getLocation' => INativeType::TYPE_ARRAY, + 'getName' => INativeType::TYPE_STRING, + 'getOffset' => TypeId::getObject('DateTime'), + 'getTransitions' => INativeType::TYPE_ARRAY, + 'listAbbreviations' => INativeType::TYPE_ARRAY, + 'listIdentifiers' => INativeType::TYPE_ARRAY, + ] + ] + ]; + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeData/InternalFunctions.php b/Source/Analysis/TypeData/InternalFunctions.php new file mode 100644 index 0000000..2d75d82 --- /dev/null +++ b/Source/Analysis/TypeData/InternalFunctions.php @@ -0,0 +1,248 @@ + + */ +class InternalFunctions extends TypeDataModule +{ + public function functions() + { + return $this->dataTypes() + $this->string() + $this->arrays() + $this->math(); + } + + protected function dataTypes() + { + return [ + 'is_string' => INativeType::TYPE_BOOL, + 'is_int' => INativeType::TYPE_BOOL, + 'is_bool' => INativeType::TYPE_BOOL, + 'is_float' => INativeType::TYPE_BOOL, + 'is_object' => INativeType::TYPE_BOOL, + 'is_array' => INativeType::TYPE_BOOL, + 'is_resource' => INativeType::TYPE_BOOL, + 'is_scalar' => INativeType::TYPE_BOOL, + 'is_null' => INativeType::TYPE_BOOL, + 'is_callable' => INativeType::TYPE_BOOL, + 'gettype' => INativeType::TYPE_STRING, + 'serialize' => INativeType::TYPE_STRING, + 'boolval' => INativeType::TYPE_BOOL, + 'intval' => INativeType::TYPE_INT, + 'strval' => INativeType::TYPE_STRING, + 'floatval' => INativeType::TYPE_DOUBLE, + ]; + } + + protected function string() + { + return [ + 'addcslashes' => INativeType::TYPE_STRING, + 'addslashes' => INativeType::TYPE_STRING, + 'bin2hex' => INativeType::TYPE_STRING, + 'chr' => INativeType::TYPE_STRING, + 'chunk_split' => INativeType::TYPE_STRING, + 'convert_cyr_string' => INativeType::TYPE_STRING, + 'convert_uudecode' => INativeType::TYPE_STRING, + 'convert_uuencode' => INativeType::TYPE_STRING, + 'crc32' => INativeType::TYPE_INT, + 'crypt' => INativeType::TYPE_STRING, + 'explode' => INativeType::TYPE_ARRAY, + 'fprintf' => INativeType::TYPE_INT, + 'get_html_translation_table' => INativeType::TYPE_ARRAY, + 'hebrev' => INativeType::TYPE_STRING, + 'hebrevc' => INativeType::TYPE_STRING, + 'hex2bin' => INativeType::TYPE_STRING, + 'html_entity_decode' => INativeType::TYPE_STRING, + 'htmlentities' => INativeType::TYPE_STRING, + 'htmlspecialchars_decode' => INativeType::TYPE_STRING, + 'htmlspecialchars' => INativeType::TYPE_STRING, + 'implode' => INativeType::TYPE_STRING, + 'lcfirst' => INativeType::TYPE_STRING, + 'levenshtein' => INativeType::TYPE_INT, + 'localeconv' => INativeType::TYPE_ARRAY, + 'ltrim' => INativeType::TYPE_STRING, + 'md5_file' => INativeType::TYPE_STRING, + 'md5' => INativeType::TYPE_STRING, + 'metaphone' => INativeType::TYPE_STRING, + 'money_format' => INativeType::TYPE_STRING, + 'nl_langinfo' => INativeType::TYPE_STRING, + 'nl2br' => INativeType::TYPE_STRING, + 'number_format' => INativeType::TYPE_STRING, + 'ord' => INativeType::TYPE_INT, + 'parse_str' => INativeType::TYPE_NULL, + 'print' => INativeType::TYPE_INT, + 'printf' => INativeType::TYPE_INT, + 'quoted_printable_decode' => INativeType::TYPE_STRING, + 'quoted_printable_encode' => INativeType::TYPE_STRING, + 'quotemeta' => INativeType::TYPE_STRING, + 'rtrim' => INativeType::TYPE_STRING, + 'setlocale' => INativeType::TYPE_STRING, + 'sha1_file' => INativeType::TYPE_STRING, + 'sha1' => INativeType::TYPE_STRING, + 'similar_text' => INativeType::TYPE_INT, + 'soundex' => INativeType::TYPE_STRING, + 'sprintf' => INativeType::TYPE_STRING, + 'str_getcsv' => INativeType::TYPE_ARRAY, + 'str_pad' => INativeType::TYPE_STRING, + 'str_repeat' => INativeType::TYPE_STRING, + 'str_rot13' => INativeType::TYPE_STRING, + 'str_shuffle' => INativeType::TYPE_STRING, + 'str_split' => INativeType::TYPE_ARRAY, + 'strcasecmp' => INativeType::TYPE_INT, + 'strcmp' => INativeType::TYPE_INT, + 'strcoll' => INativeType::TYPE_INT, + 'strcspn' => INativeType::TYPE_INT, + 'strip_tags' => INativeType::TYPE_STRING, + 'stripcslashes' => INativeType::TYPE_STRING, + 'stripos' => INativeType::TYPE_INT, + 'stripslashes' => INativeType::TYPE_STRING, + 'stristr' => INativeType::TYPE_STRING, + 'strlen' => INativeType::TYPE_INT, + 'strnatcasecmp' => INativeType::TYPE_INT, + 'strnatcmp' => INativeType::TYPE_INT, + 'strncasecmp' => INativeType::TYPE_INT, + 'strncmp' => INativeType::TYPE_INT, + 'strpbrk' => INativeType::TYPE_STRING, + 'strrchr' => INativeType::TYPE_STRING, + 'strrev' => INativeType::TYPE_STRING, + 'strripos' => INativeType::TYPE_INT, + 'strrpos' => INativeType::TYPE_INT, + 'strspn' => INativeType::TYPE_INT, + 'strstr' => INativeType::TYPE_STRING, + 'strtok' => INativeType::TYPE_STRING, + 'strtolower' => INativeType::TYPE_STRING, + 'strtoupper' => INativeType::TYPE_STRING, + 'strtr' => INativeType::TYPE_STRING, + 'substr_compare' => INativeType::TYPE_INT, + 'substr_count' => INativeType::TYPE_INT, + 'substr' => INativeType::TYPE_STRING, + 'trim' => INativeType::TYPE_STRING, + 'ucfirst' => INativeType::TYPE_STRING, + 'ucwords' => INativeType::TYPE_STRING, + 'vfprintf' => INativeType::TYPE_INT, + 'vprintf' => INativeType::TYPE_INT, + 'vsprintf' => INativeType::TYPE_STRING, + 'wordwrap' => INativeType::TYPE_STRING, + ]; + } + + protected function arrays() + { + return [ + 'array_change_key_case' => INativeType::TYPE_ARRAY, + 'array_chunk' => INativeType::TYPE_ARRAY, + 'array_column' => INativeType::TYPE_ARRAY, + 'array_combine' => INativeType::TYPE_ARRAY, + 'array_count_values' => INativeType::TYPE_ARRAY, + 'array_diff_assoc' => INativeType::TYPE_ARRAY, + 'array_diff_key' => INativeType::TYPE_ARRAY, + 'array_diff_uassoc' => INativeType::TYPE_ARRAY, + 'array_diff_ukey' => INativeType::TYPE_ARRAY, + 'array_diff' => INativeType::TYPE_ARRAY, + 'array_fill_keys' => INativeType::TYPE_ARRAY, + 'array_fill' => INativeType::TYPE_ARRAY, + 'array_filter' => INativeType::TYPE_ARRAY, + 'array_flip' => INativeType::TYPE_ARRAY, + 'array_intersect_assoc' => INativeType::TYPE_ARRAY, + 'array_intersect_key' => INativeType::TYPE_ARRAY, + 'array_intersect_uassoc' => INativeType::TYPE_ARRAY, + 'array_intersect_ukey' => INativeType::TYPE_ARRAY, + 'array_intersect' => INativeType::TYPE_ARRAY, + 'array_key_exists' => INativeType::TYPE_BOOL, + 'array_keys' => INativeType::TYPE_ARRAY, + 'array_map' => INativeType::TYPE_ARRAY, + 'array_merge_recursive' => INativeType::TYPE_ARRAY, + 'array_merge' => INativeType::TYPE_ARRAY, + 'array_multisort' => INativeType::TYPE_BOOL, + 'array_pad' => INativeType::TYPE_ARRAY, + 'array_product' => INativeType::TYPE_MIXED, + 'array_push' => INativeType::TYPE_INT, + 'array_replace_recursive' => INativeType::TYPE_ARRAY, + 'array_replace' => INativeType::TYPE_ARRAY, + 'array_reverse' => INativeType::TYPE_ARRAY, + 'array_slice' => INativeType::TYPE_ARRAY, + 'array_splice' => INativeType::TYPE_ARRAY, + 'array_sum' => INativeType::TYPE_MIXED, + 'array_udiff_assoc' => INativeType::TYPE_ARRAY, + 'array_udiff_uassoc' => INativeType::TYPE_ARRAY, + 'array_udiff' => INativeType::TYPE_ARRAY, + 'array_uintersect_assoc' => INativeType::TYPE_ARRAY, + 'array_uintersect_uassoc' => INativeType::TYPE_ARRAY, + 'array_uintersect' => INativeType::TYPE_ARRAY, + 'array_unique' => INativeType::TYPE_ARRAY, + 'array_unshift' => INativeType::TYPE_INT, + 'array_values' => INativeType::TYPE_ARRAY, + 'array_walk_recursive' => INativeType::TYPE_BOOL, + 'array_walk' => INativeType::TYPE_BOOL, + 'arsort' => INativeType::TYPE_BOOL, + 'asort' => INativeType::TYPE_BOOL, + 'compact' => INativeType::TYPE_ARRAY, + 'count' => INativeType::TYPE_INT, + 'each' => INativeType::TYPE_ARRAY, + 'extract' => INativeType::TYPE_INT, + 'in_array' => INativeType::TYPE_BOOL, + 'krsort' => INativeType::TYPE_BOOL, + 'ksort' => INativeType::TYPE_BOOL, + 'list' => INativeType::TYPE_ARRAY, + 'natcasesort' => INativeType::TYPE_BOOL, + 'natsort' => INativeType::TYPE_BOOL, + 'range' => INativeType::TYPE_ARRAY, + 'rsort' => INativeType::TYPE_BOOL, + 'shuffle' => INativeType::TYPE_BOOL, + 'sort' => INativeType::TYPE_BOOL, + 'uasort' => INativeType::TYPE_BOOL, + 'uksort' => INativeType::TYPE_BOOL, + 'usort' => INativeType::TYPE_BOOL, + ]; + } + + protected function math() + { + return [ + 'acos' => INativeType::TYPE_DOUBLE, + 'acosh' => INativeType::TYPE_DOUBLE, + 'asin' => INativeType::TYPE_DOUBLE, + 'asinh' => INativeType::TYPE_DOUBLE, + 'atan2' => INativeType::TYPE_DOUBLE, + 'atan' => INativeType::TYPE_DOUBLE, + 'atanh' => INativeType::TYPE_DOUBLE, + 'base_convert' => INativeType::TYPE_STRING, + 'ceil' => INativeType::TYPE_DOUBLE, + 'cos' => INativeType::TYPE_DOUBLE, + 'cosh' => INativeType::TYPE_DOUBLE, + 'decbin' => INativeType::TYPE_STRING, + 'dechex' => INativeType::TYPE_STRING, + 'decoct' => INativeType::TYPE_STRING, + 'deg2rad' => INativeType::TYPE_DOUBLE, + 'exp' => INativeType::TYPE_DOUBLE, + 'expm1' => INativeType::TYPE_DOUBLE, + 'floor' => INativeType::TYPE_DOUBLE, + 'fmod' => INativeType::TYPE_DOUBLE, + 'getrandmax' => INativeType::TYPE_INT, + 'hypot' => INativeType::TYPE_DOUBLE, + 'is_finite' => INativeType::TYPE_BOOL, + 'is_infinite' => INativeType::TYPE_BOOL, + 'is_nan' => INativeType::TYPE_BOOL, + 'lcg_value' => INativeType::TYPE_DOUBLE, + 'log10' => INativeType::TYPE_DOUBLE, + 'log1p' => INativeType::TYPE_DOUBLE, + 'log' => INativeType::TYPE_DOUBLE, + 'mt_getrandmax' => INativeType::TYPE_INT, + 'mt_rand' => INativeType::TYPE_INT, + 'pi' => INativeType::TYPE_DOUBLE, + 'rad2deg' => INativeType::TYPE_DOUBLE, + 'rand' => INativeType::TYPE_INT, + 'round' => INativeType::TYPE_DOUBLE, + 'sin' => INativeType::TYPE_DOUBLE, + 'sinh' => INativeType::TYPE_DOUBLE, + 'sqrt' => INativeType::TYPE_DOUBLE, + 'tan' => INativeType::TYPE_DOUBLE, + 'tanh' => INativeType::TYPE_DOUBLE, + ]; + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeData/InternalTypes.php b/Source/Analysis/TypeData/InternalTypes.php new file mode 100644 index 0000000..1ecb66e --- /dev/null +++ b/Source/Analysis/TypeData/InternalTypes.php @@ -0,0 +1,57 @@ + + */ +class InternalTypes extends TypeDataModule +{ + public function types() + { + return [ + 'ArrayAccess' => [ + 'methods' => [ + 'offsetExists' => INativeType::TYPE_BOOL, + 'offsetGet' => INativeType::TYPE_MIXED, + 'offsetSet' => INativeType::TYPE_NULL, + 'offsetUnset' => INativeType::TYPE_NULL, + ] + ], + 'ArrayAccess' => [ + 'methods' => [ + 'offsetExists' => INativeType::TYPE_BOOL, + 'offsetGet' => INativeType::TYPE_MIXED, + 'offsetSet' => INativeType::TYPE_NULL, + 'offsetUnset' => INativeType::TYPE_NULL, + ] + ], + 'Closure' => [ + 'methods' => [ + 'bind' => self::TYPE_SELF, + 'bindTo' => self::TYPE_SELF, + '__invoke' => INativeType::TYPE_MIXED + ] + ], + 'Exception' => [ + 'fields' => [ + 'message' => INativeType::TYPE_STRING, + 'code' => INativeType::TYPE_INT, + 'file' => INativeType::TYPE_STRING, + 'line' => INativeType::TYPE_INT, + ], + 'methods' => [ + 'getMessage' => INativeType::TYPE_STRING, + 'getFile' => INativeType::TYPE_STRING, + 'getLine' => INativeType::TYPE_INT, + 'getTrace' => INativeType::TYPE_ARRAY, + 'getTraceAsString' => INativeType::TYPE_STRING, + ] + ], + ]; + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeData/PinqAPI.php b/Source/Analysis/TypeData/PinqAPI.php new file mode 100644 index 0000000..b9c57ff --- /dev/null +++ b/Source/Analysis/TypeData/PinqAPI.php @@ -0,0 +1,147 @@ + + */ +class PinqAPI extends TypeDataModule +{ + public function types() + { + $traversableInterfaceGroups = [ + Pinq\ITraversable::ITRAVERSABLE_TYPE => [ + 'ordered' => Interfaces\IOrderedTraversable::IORDERED_TRAVERSABLE_TYPE, + 'joining-on' => Interfaces\IJoiningOnTraversable::IJOINING_ON_TRAVERSABLE_TYPE, + 'joining-to' => Interfaces\IJoiningToTraversable::IJOINING_TO_TRAVERSABLE_TYPE + ], + Pinq\ICollection::ICOLLECTION_TYPE => [ + 'mutable' => true, + 'ordered' => Interfaces\IOrderedCollection::IORDERED_COLLECTION_TYPE, + 'joining-on' => Interfaces\IJoiningOnCollection::IJOINING_ON_COLLECTION_TYPE, + 'joining-to' => Interfaces\IJoiningToCollection::IJOINING_TO_COLLECTION_TYPE + ], + Pinq\IQueryable::IQUERYABLE_TYPE => [ + 'ordered' => Interfaces\IOrderedQueryable::IORDERED_QUERYABLE_TYPE, + 'joining-on' => Interfaces\IJoiningOnQueryable::IJOINING_ON_QUERYABLE_TYPE, + 'joining-to' => Interfaces\IJoiningToQueryable::IJOINING_TO_QUERYABLE_TYPE + ], + Pinq\IRepository::IREPOSITORY_TYPE => [ + 'mutable' => true, + 'ordered' => Interfaces\IOrderedRepository::IORDERED_REPOSITORY_TYPE, + 'joining-on' => Interfaces\IJoiningOnRepository::IJOINING_ON_REPOSITORY_TYPE, + 'joining-to' => Interfaces\IJoiningToRepository::IJOINING_TO_REPOSITORY_TYPE + ], + ]; + + $pinqTypes = []; + foreach ($traversableInterfaceGroups as $traversableInterface => $traversableGroup) { + $traversableType = TypeId::getObject($traversableInterface); + $orderedTraversableType = TypeId::getObject($traversableGroup['ordered']); + $joiningOnTraversableType = TypeId::getObject($traversableGroup['joining-on']); + $joiningToTraversableType = TypeId::getObject($traversableGroup['joining-to']); + + $commonMethods = [ + 'asArray' => INativeType::TYPE_ARRAY, + 'asTraversable' => TypeId::getObject(Pinq\ITraversable::ITRAVERSABLE_TYPE), + 'asCollection' => TypeId::getObject(Pinq\ICollection::ICOLLECTION_TYPE), + 'isSource' => INativeType::TYPE_BOOL, + 'getSource' => $traversableType, + 'iterate' => INativeType::TYPE_NULL, + 'getIterator' => TypeId::getObject('Traversable'), + 'getTrueIterator' => TypeId::getObject('Traversable'), + 'getIteratorScheme' => TypeId::getObject(IIteratorScheme::IITERATOR_SCHEME_TYPE), + 'first' => INativeType::TYPE_MIXED, + 'last' => INativeType::TYPE_MIXED, + 'count' => INativeType::TYPE_INT, + 'isEmpty' => INativeType::TYPE_BOOL, + 'aggregate' => INativeType::TYPE_MIXED, + 'maximum' => INativeType::TYPE_MIXED, + 'minimum' => INativeType::TYPE_MIXED, + 'sum' => INativeType::TYPE_MIXED, + 'average' => INativeType::TYPE_MIXED, + 'all' => INativeType::TYPE_BOOL, + 'any' => INativeType::TYPE_BOOL, + 'implode' => INativeType::TYPE_STRING, + 'contains' => INativeType::TYPE_BOOL, + 'where' => $traversableType, + 'orderBy' => $orderedTraversableType, + 'orderByAscending' => $orderedTraversableType, + 'orderByDescending' => $orderedTraversableType, + 'skip' => $traversableType, + 'take' => $traversableType, + 'slice' => $traversableType, + 'indexBy' => $traversableType, + 'keys' => $traversableType, + 'reindex' => $traversableType, + 'groupBy' => $traversableType, + 'join' => $joiningOnTraversableType, + 'groupJoin' => $joiningOnTraversableType, + 'select' => $traversableType, + 'selectMany' => $traversableType, + 'unique' => $traversableType, + 'append' => $traversableType, + 'whereIn' => $traversableType, + 'except' => $traversableType, + 'union' => $traversableType, + 'intersect' => $traversableType, + 'difference' => $traversableType, + ]; + + if (!empty($traversableGroup['mutable'])) { + $commonMethods += [ + 'apply' => INativeType::TYPE_NULL, + 'addRange' => INativeType::TYPE_NULL, + 'remove' => INativeType::TYPE_NULL, + 'removeRange' => INativeType::TYPE_NULL, + 'removeWhere' => INativeType::TYPE_NULL, + 'clear' => INativeType::TYPE_NULL, + ]; + } + + $pinqTypes[$traversableInterface] = [ + 'methods' => $commonMethods + ]; + + $pinqTypes[$traversableGroup['ordered']] = [ + 'methods' => [ + 'thenBy' => $orderedTraversableType, + 'thenByAscending' => $orderedTraversableType, + 'thenByDescending' => $orderedTraversableType, + ] + $commonMethods + ]; + + $joiningMethods = [ + 'withDefault' => $joiningToTraversableType, + 'to' => $traversableType, + ]; + + if (!empty($traversableGroup['mutable'])) { + $joiningMethods += [ + 'apply' => INativeType::TYPE_NULL, + ]; + } + + $pinqTypes[$traversableGroup['joining-to']] = [ + 'methods' => $joiningMethods + ]; + + $pinqTypes[$traversableGroup['joining-on']] = [ + 'methods' => [ + 'on' => $joiningToTraversableType, + 'onEquality' => $joiningToTraversableType, + ] + $joiningMethods + ]; + } + + return $pinqTypes; + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeData/TypeDataModule.php b/Source/Analysis/TypeData/TypeDataModule.php new file mode 100644 index 0000000..c76fed3 --- /dev/null +++ b/Source/Analysis/TypeData/TypeDataModule.php @@ -0,0 +1,35 @@ + + */ +abstract class TypeDataModule +{ + const TYPE_SELF = PhpTypeSystem::TYPE_SELF; + + public function __construct() + { + + } + + public static function getType() + { + return get_called_class(); + } + + public function functions() + { + return []; + } + + public function types() + { + return []; + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeException.php b/Source/Analysis/TypeException.php new file mode 100644 index 0000000..7f93ab0 --- /dev/null +++ b/Source/Analysis/TypeException.php @@ -0,0 +1,15 @@ + + */ +class TypeException extends PinqException +{ + +} \ No newline at end of file diff --git a/Source/Analysis/TypeId.php b/Source/Analysis/TypeId.php new file mode 100644 index 0000000..404be1b --- /dev/null +++ b/Source/Analysis/TypeId.php @@ -0,0 +1,46 @@ + + */ +final class TypeId +{ + private function __construct() + { + + } + + public static function getObject($class) + { + return 'object:' . $class; + } + + public static function isObject($id) + { + return strpos($id, 'object:') === 0; + } + + public static function getClassType($objectId) + { + return substr($objectId, strlen('object:')); + } + + public static function getComposite(array $typeIds) + { + return 'composite<' . implode('|', $typeIds) . '>'; + } + + public static function isComposite($id) + { + return strpos($id, 'composite<') === 0 && $id[strlen($id) - 1] === '>'; + } + + public static function getComposedTypeIds($compositeId) + { + return explode('|', substr($id, strlen('composite<'), -strlen('>'))); + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeOperations/Cast.php b/Source/Analysis/TypeOperations/Cast.php new file mode 100644 index 0000000..1bd03ff --- /dev/null +++ b/Source/Analysis/TypeOperations/Cast.php @@ -0,0 +1,13 @@ + + */ +class Cast extends TypeOperation +{ + +} \ No newline at end of file diff --git a/Source/Analysis/TypeOperations/Constructor.php b/Source/Analysis/TypeOperations/Constructor.php new file mode 100644 index 0000000..2c0bd94 --- /dev/null +++ b/Source/Analysis/TypeOperations/Constructor.php @@ -0,0 +1,35 @@ + + */ +class Constructor extends TypeOperation implements IConstructor +{ + /** + * @var \ReflectionMethod|null + */ + protected $reflection; + + public function __construct(ITypeSystem $typeSystem, $type, \ReflectionMethod $reflection = null) + { + parent::__construct($typeSystem, $type, $type); + $this->reflection = $reflection; + } + + public function hasMethod() + { + return $this->reflection !== null; + } + + public function getReflection() + { + return $this->reflection; + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeOperations/Field.php b/Source/Analysis/TypeOperations/Field.php new file mode 100644 index 0000000..bb8fca1 --- /dev/null +++ b/Source/Analysis/TypeOperations/Field.php @@ -0,0 +1,41 @@ + + */ +class Field extends TypeOperation implements IField +{ + /** + * @var string + */ + protected $name; + + /** + * @var boolean + */ + protected $isStatic; + + public function __construct(ITypeSystem $typeSystem, $sourceType, $name, $isStatic, $returnType) + { + parent::__construct($typeSystem, $sourceType, $returnType); + $this->name = $name; + $this->isStatic = $isStatic; + } + + public function getName() + { + return $this->name; + } + + public function isStatic() + { + return $this->isStatic; + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeOperations/Indexer.php b/Source/Analysis/TypeOperations/Indexer.php new file mode 100644 index 0000000..98b727c --- /dev/null +++ b/Source/Analysis/TypeOperations/Indexer.php @@ -0,0 +1,24 @@ + + */ +class Indexer extends TypeOperation implements IIndexer +{ + public function __construct(ITypeSystem $typeSystem, $sourceType, $returnType) + { + parent::__construct($typeSystem, $sourceType, $returnType); + } + + public function getReturnTypeOfIndex($index) + { + return $this->getReturnType(); + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeOperations/Method.php b/Source/Analysis/TypeOperations/Method.php new file mode 100644 index 0000000..629dadc --- /dev/null +++ b/Source/Analysis/TypeOperations/Method.php @@ -0,0 +1,46 @@ + + */ +class Method extends TypeOperation implements IMethod +{ + /** + * @var string + */ + protected $name; + + /** + * @var \ReflectionMethod + */ + protected $reflection; + + public function __construct(ITypeSystem $typeSystem, $sourceType, \ReflectionMethod $reflection, $returnType) + { + parent::__construct($typeSystem, $sourceType, $returnType); + $this->name = $reflection->getName(); + $this->reflection = $reflection; + } + + public function getName() + { + return $this->name; + } + + public function getReflection() + { + return $this->reflection; + } + + public function getReturnTypeWithArguments(array $staticArguments) + { + return $this->getReturnType(); + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeOperations/TypeOperation.php b/Source/Analysis/TypeOperations/TypeOperation.php new file mode 100644 index 0000000..eea985c --- /dev/null +++ b/Source/Analysis/TypeOperations/TypeOperation.php @@ -0,0 +1,42 @@ + + */ +class TypeOperation extends Typed implements ITypeOperation +{ + /** + * @var string + */ + protected $sourceType; + + /** + * @var string + */ + protected $returnType; + + public function __construct(ITypeSystem $typeSystem, $sourceType, $returnType) + { + parent::__construct($typeSystem); + $this->sourceType = $sourceType; + $this->returnType = $returnType; + } + + public function getSourceType() + { + return $this->typeSystem->getType($this->sourceType); + } + + public function getReturnType() + { + return $this->typeSystem->getType($this->returnType); + } +} \ No newline at end of file diff --git a/Source/Analysis/TypeSystem.php b/Source/Analysis/TypeSystem.php new file mode 100644 index 0000000..4f5dc19 --- /dev/null +++ b/Source/Analysis/TypeSystem.php @@ -0,0 +1,261 @@ + + */ +abstract class TypeSystem implements ITypeSystem +{ + /** + * @var INativeType[] + */ + protected $nativeTypes = []; + + /** + * @var IObjectType[] + */ + protected $objectTypes = []; + + /** + * @var ICompositeType[] + */ + protected $compositeTypes = []; + + /** + * @var IType[] + */ + protected $customTypes = []; + + /** + * @var IFunction[] + */ + protected $functions = []; + + /** + * @var IBinaryOperation[] + */ + protected $binaryOperations = []; + + public function __construct() + { + foreach ($this->buildNativeTypes() as $nativeType) { + $this->nativeTypes[$nativeType->getTypeOfType()] = $nativeType; + } + + foreach ($this->buildBinaryOperations() as $binaryOperation) { + $this->binaryOperations[] = $binaryOperation; + } + } + + /** + * Performs all necessary normalization to the class name. + * + * @param string $name + * + * @return string + */ + protected function normalizeClassName($name) + { + return $name; + } + + /** + * Performs all necessary normalization the function name. + * + * @param string $name + * + * @return string + */ + protected function normalizeFunctionName($name) + { + return $name; + } + + protected function buildTypeOperations($type, array $operatorTypeMap = []) + { + return array_map( + function ($returnType) use ($type) { + return new TypeOperation($this, $type, $returnType); + }, + $operatorTypeMap + ); + } + + /** + * @return INativeType[] + */ + abstract protected function nativeTypes(); + + /** + * @return INativeType[] + */ + protected function buildNativeTypes() + { + return $this->nativeTypes(); + } + + /** + * @return array[] + */ + abstract protected function binaryOperations(); + + /** + * @return IBinaryOperation[] + */ + protected function buildBinaryOperations() + { + $binaryOperations = []; + foreach ($this->binaryOperations() as $operator) { + $binaryOperations[] = new BinaryOperation($this, $operator[0], $operator[1], $operator[2], $operator['return']); + } + + return $binaryOperations; + } + + public function getType($typeIdentifier) + { + if (TypeId::isObject($typeIdentifier)) { + return $this->getObjectType(TypeId::getClassType($typeIdentifier)); + } elseif (TypeId::isComposite($typeIdentifier)) { + return $this->getCompositeType( + array_map([$this, __FUNCTION__], TypeId::getComposedTypeIds($typeIdentifier)) + ); + } else { + return $this->getNativeType($typeIdentifier); + } + } + + public function getNativeType($nativeType) + { + if (!isset($this->nativeTypes[$nativeType])) { + throw new TypeException('Cannot get native type \'%s\': type is not supported', $nativeType); + } + + return $this->nativeTypes[$nativeType]; + } + + /** + * @param string $typeId + * @param string $classType + * + * @return IObjectType + */ + abstract protected function buildObjectType($typeId, $classType); + + public function getObjectType($classType) + { + $normalizedClassType = $this->normalizeClassName($classType); + $typeId = TypeId::getObject($normalizedClassType); + if (!isset($this->objectTypes[$typeId])) { + $this->objectTypes[$typeId] = $this->buildObjectType($typeId, $normalizedClassType); + } + + return $this->objectTypes[$typeId]; + } + + /** + * @param string $typeId + * @param IType[] $types + * + * @return ICompositeType + */ + abstract protected function buildCompositeType($typeId, array $types); + + public function getCompositeType(array $types) + { + $types = $this->flattenComposedTypes($types); + + //Remove any redundant types: (\Iterator and \Traversable) becomes \Iterator + /** @var $types IType[] */ + foreach ($types as $outer => $outerType) { + foreach ($types as $inner => $innerType) { + if ($outer !== $inner && $innerType->isParentTypeOf($outerType)) { + unset($types[$inner]); + } + } + } + + if (count($types) === 0) { + return $this->getNativeType(INativeType::TYPE_MIXED); + } elseif (count($types) === 1) { + return reset($types); + } + + ksort($types, SORT_STRING); + $typeId = TypeId::getComposite(array_keys($types)); + if (!isset($this->compositeTypes[$typeId])) { + $this->compositeTypes[$typeId] = $this->buildCompositeType($typeId, $types); + } + + return $this->compositeTypes[$typeId]; + } + + /** + * Flattens all the composed types. + * + * @param IType[] $types + * + * @return IType[] + */ + protected function flattenComposedTypes(array $types) + { + $composedTypes = []; + foreach ($types as $type) { + if ($type instanceof ICompositeType) { + $composedTypes += $this->flattenComposedTypes($type->getComposedTypes()); + } else { + $composedTypes[$type->getIdentifier()] = $type; + } + } + + return $composedTypes; + } + + /** + * @param string $name + * + * @return IFunction + */ + abstract protected function buildFunction($name); + + public function getFunction($name) + { + $normalizedName = $this->normalizeFunctionName($name); + if (!isset($this->functions[$normalizedName])) { + $this->functions[$normalizedName] = $this->buildFunction($normalizedName); + } + + return $this->functions[$normalizedName]; + } + + public function getBinaryOperation(IType $leftOperandType, $operator, IType $rightOperandType) + { + foreach ($this->binaryOperations as $binaryOperation) { + $leftOperand = $binaryOperation->getLeftOperandType(); + $rightOperand = $binaryOperation->getRightOperandType(); + + if ($binaryOperation->getOperator() === $operator) { + if (($leftOperand->isParentTypeOf($leftOperandType) && $rightOperand->isParentTypeOf($rightOperandType)) + //Binary operators are symmetrical: test for flipped operands + || ($leftOperand->isParentTypeOf($rightOperandType) + && $rightOperand->isParentTypeOf($leftOperandType)) + ) { + return $binaryOperation; + } + } + } + + throw new TypeException( + 'Cannot get binary operation: operation for \'%s\' %s \'%s\' is not supported', + $leftOperandType->getIdentifier(), + $operator, + $rightOperandType->getIdentifier()); + } +} \ No newline at end of file diff --git a/Source/Analysis/Typed.php b/Source/Analysis/Typed.php new file mode 100644 index 0000000..c142548 --- /dev/null +++ b/Source/Analysis/Typed.php @@ -0,0 +1,26 @@ + + */ +class Typed implements ITyped +{ + /** + * @var ITypeSystem + */ + protected $typeSystem; + + public function __construct(ITypeSystem $typeSystem) + { + $this->typeSystem = $typeSystem; + } + + public function getTypeSystem() + { + return $this->typeSystem; + } +} \ No newline at end of file diff --git a/Source/Analysis/Types/CompositeType.php b/Source/Analysis/Types/CompositeType.php new file mode 100644 index 0000000..171f526 --- /dev/null +++ b/Source/Analysis/Types/CompositeType.php @@ -0,0 +1,103 @@ + + */ +class CompositeType extends Type implements ICompositeType +{ + /** + * @var IType[] + */ + protected $composedTypes; + + public function __construct( + $identifier, + IType $parentType, + array $composedTypes + ) { + parent::__construct($identifier, $parentType); + $this->composedTypes = $composedTypes; + } + + public function isParentTypeOf(IType $type) + { + foreach ($this->composedTypes as $composedType) { + if ($composedType->isParentTypeOf($type)) { + return true; + } + } + + return false; + } + + public function getComposedTypes() + { + return $this->composedTypes; + } + + protected function getTypeData($function, O\Expression $expression) + { + foreach ($this->composedTypes as $composedType) { + try { + return $composedType->$function($expression); + } catch (\Exception $exception) { + + } + } + + return parent::$function($expression); + } + + public function getConstructor(O\NewExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } + + public function getMethod(O\MethodCallExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } + + public function getStaticMethod(O\StaticMethodCallExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } + + public function getField(O\FieldExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } + + public function getStaticField(O\StaticFieldExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } + + public function getInvocation(O\InvocationExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } + + public function getIndex(O\IndexExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } + + public function getCast(O\CastExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } + + public function getUnaryOperation(O\UnaryOperationExpression $expression) + { + return $this->getTypeData(__FUNCTION__, $expression); + } +} \ No newline at end of file diff --git a/Source/Analysis/Types/MixedType.php b/Source/Analysis/Types/MixedType.php new file mode 100644 index 0000000..ce8e5ae --- /dev/null +++ b/Source/Analysis/Types/MixedType.php @@ -0,0 +1,79 @@ + + */ +class MixedType extends NativeType +{ + public function __construct($identifier) + { + parent::__construct($identifier, null, INativeType::TYPE_MIXED); + } + + public function isParentTypeOf(IType $type) + { + return true; + } + + protected function unsupported(O\Expression $expression, $message) + { + return new TypeException( + 'Type does not support expression \'%s\': %s', + $expression->compileDebug(), + $message); + } + + public function getConstructor(O\NewExpression $expression) + { + throw $this->unsupported($expression, 'constructor is not supported'); + } + + public function getMethod(O\MethodCallExpression $expression) + { + throw $this->unsupported($expression, 'method is not supported'); + } + + public function getStaticMethod(O\StaticMethodCallExpression $expression) + { + throw $this->unsupported($expression, 'static method is not supported'); + } + + public function getField(O\FieldExpression $expression) + { + throw $this->unsupported($expression, 'field is not supported'); + } + + public function getStaticField(O\StaticFieldExpression $expression) + { + throw $this->unsupported($expression, 'static field is not supported'); + } + + public function getInvocation(O\InvocationExpression $expression) + { + throw $this->unsupported($expression, 'invocation is not supported'); + } + + public function getIndex(O\IndexExpression $expression) + { + throw $this->unsupported($expression, 'indexer is not supported'); + } + + public function getCast(O\CastExpression $expression) + { + throw $this->unsupported($expression, 'cast is not supported'); + } + + public function getUnaryOperation(O\UnaryOperationExpression $expression) + { + throw $this->unsupported($expression, 'unary operator is not supported'); + } +} \ No newline at end of file diff --git a/Source/Analysis/Types/NativeType.php b/Source/Analysis/Types/NativeType.php new file mode 100644 index 0000000..8295f8c --- /dev/null +++ b/Source/Analysis/Types/NativeType.php @@ -0,0 +1,46 @@ + + */ +class NativeType extends Type implements INativeType +{ + /** + * @var string + */ + protected $typeOfType; + + public function __construct( + $identifier, + IType $parentType = null, + $typeOfType, + ITypeOperation $indexer = null, + array $castOperations = [], + array $unaryOperations = [] + ) { + parent::__construct($identifier, $parentType); + $this->typeOfType = $typeOfType; + $this->indexer = $indexer; + $this->castOperations = $castOperations; + $this->unaryOperations = $unaryOperations; + } + + public function getTypeOfType() + { + return $this->typeOfType; + } + + public function isParentTypeOf(IType $type) + { + return $this->isEqualTo($type); + } +} \ No newline at end of file diff --git a/Source/Analysis/Types/ObjectType.php b/Source/Analysis/Types/ObjectType.php new file mode 100644 index 0000000..e87f5e5 --- /dev/null +++ b/Source/Analysis/Types/ObjectType.php @@ -0,0 +1,199 @@ + + */ +class ObjectType extends Type implements IObjectType +{ + /** + * @var string + */ + protected $classType; + + /** + * @var \ReflectionClass + */ + protected $reflection; + + /** + * @var IConstructor|null + */ + protected $constructor; + + /** + * @var IMethod[] + */ + protected $methods = []; + + /** + * @var IField[] + */ + protected $fields = []; + + /** + * @var ITypeOperation|IMethod|null + */ + protected $invoker; + + /** + * @param string $identifier + * @param \ReflectionClass $reflection + * @param IType $parentType + * @param IConstructor $constructor + * @param IMethod[] $methods + * @param IField[] $fields + * @param ITypeOperation[] $unaryOperations + * @param ITypeOperation[] $castOperations + * @param ITypeOperation|IMethod|null $invoker + * @param ITypeOperation|null $indexer + */ + public function __construct( + $identifier, + \ReflectionClass $reflection, + IType $parentType, + IConstructor $constructor = null, + array $methods = [], + array $fields = [], + array $unaryOperations = [], + array $castOperations = [], + ITypeOperation $invoker = null, + ITypeOperation $indexer = null + ) { + parent::__construct($identifier, $parentType, $indexer, $unaryOperations, $castOperations); + $this->classType = $reflection->getName(); + $this->reflection = $reflection; + $this->invoker = $invoker; + $this->constructor = $constructor; + $this->methods = $methods; + $this->fields = $fields; + $this->invoker = $invoker; + } + + public function getClassType() + { + return $this->reflection->getName(); + } + + public function getReflection() + { + return $this->reflection; + } + + public function isParentTypeOf(IType $type) + { + if ($type instanceof IObjectType) { + return is_a($type->getClassType(), $this->classType, true); + } + + return false; + } + + public function getMethods() + { + return $this->methods; + } + + public function getFields() + { + return $this->fields; + } + + public function getConstructor(O\NewExpression $expression) + { + if ($this->constructor !== null) { + return $this->constructor; + } + + return parent::getConstructor($expression); + } + + public function getMethod(O\MethodCallExpression $expression) + { + if ($method = $this->getMethodByName($expression->getName(), false)) { + return $method; + } + + return parent::getMethod($expression); + } + + public function getStaticMethod(O\StaticMethodCallExpression $expression) + { + if ($method = $this->getMethodByName($expression->getName(), true)) { + return $method; + } + + return parent::getStaticMethod($expression); + } + + protected function getMethodByName(O\Expression $nameExpression, $static) + { + if ($nameExpression instanceof O\ValueExpression) { + $methodName = $nameExpression->getValue(); + foreach ($this->methods as $otherMethodName => $method) { + if ($method->getReflection()->isStatic() === $static + && strcasecmp($methodName, $otherMethodName) === 0 + ) { + return $method; + } + } + } + + return null; + } + + public function getField(O\FieldExpression $expression) + { + if ($field = $this->getFieldByName($expression->getName(), false)) { + return $field; + } + + return parent::getField($expression); + } + + public function getStaticField(O\StaticFieldExpression $expression) + { + if ($field = $this->getFieldByName($expression->getName(), true)) { + return $field; + } + + return parent::getStaticField($expression); + } + + protected function getFieldByName(O\Expression $nameExpression, $static) + { + if ($nameExpression instanceof O\ValueExpression) { + $fieldName = $nameExpression->getValue(); + + foreach ($this->fields as $otherFieldName => $field) { + if ($field->isStatic() === $static + && strcasecmp($fieldName, $otherFieldName) === 0 + ) { + return $field; + } + } + } + + return null; + } + + public function getInvocation(O\InvocationExpression $expression) + { + if ($this->invoker !== null) { + return $this->invoker; + } + + return parent::getInvocation($expression); + } +} \ No newline at end of file diff --git a/Source/Analysis/Types/Type.php b/Source/Analysis/Types/Type.php new file mode 100644 index 0000000..436aed2 --- /dev/null +++ b/Source/Analysis/Types/Type.php @@ -0,0 +1,138 @@ + + */ +abstract class Type implements IType +{ + /** + * @var IType[] + */ + protected $parentType; + + /** + * @var string + */ + protected $identifier; + + /** + * @var ITypeOperation|null + */ + protected $indexer; + + /** + * @var ITypeOperation[] + */ + protected $unaryOperations; + + /** + * @var ITypeOperation[] + */ + protected $castOperations; + + /** + * @param string $identifier + * @param IType $parentType + * @param ITypeOperation $indexer + * @param ITypeOperation[] $castOperations + * @param ITypeOperation[] $unaryOperations + */ + public function __construct( + $identifier, + IType $parentType = null, + ITypeOperation $indexer = null, + array $castOperations = [], + array $unaryOperations = [] + ) { + $this->identifier = $identifier; + $this->parentType = $parentType; + $this->indexer = $indexer; + $this->castOperations = $castOperations; + $this->unaryOperations = $unaryOperations; + } + + public function hasParentType() + { + return $this->parentType !== null; + } + + public function getParentType() + { + return $this->parentType; + } + + public function getIdentifier() + { + return $this->identifier; + } + + public function isEqualTo(IType $type) + { + return $this->identifier === $type->getIdentifier(); + } + + public function getConstructor(O\NewExpression $expression) + { + return $this->parentType->getConstructor($expression); + } + + public function getMethod(O\MethodCallExpression $expression) + { + return $this->parentType->getMethod($expression); + } + + public function getStaticMethod(O\StaticMethodCallExpression $expression) + { + return $this->parentType->getStaticMethod($expression); + } + + public function getField(O\FieldExpression $expression) + { + return $this->parentType->getField($expression); + } + + public function getStaticField(O\StaticFieldExpression $expression) + { + return $this->parentType->getStaticField($expression); + } + + public function getInvocation(O\InvocationExpression $expression) + { + return $this->parentType->getInvocation($expression); + } + + public function getIndex(O\IndexExpression $expression) + { + if ($this->indexer !== null) { + return $this->indexer; + } + + return $this->parentType->getIndex($expression); + } + + public function getCast(O\CastExpression $expression) + { + if (isset($this->castOperations[$expression->getCastType()])) { + return $this->castOperations[$expression->getCastType()]; + } + + return $this->parentType->getCast($expression); + } + + public function getUnaryOperation(O\UnaryOperationExpression $expression) + { + if (isset($this->unaryOperations[$expression->getOperator()])) { + return $this->unaryOperations[$expression->getOperator()]; + } + + return $this->parentType->getUnaryOperation($expression); + } +} \ No newline at end of file diff --git a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php new file mode 100644 index 0000000..afa793f --- /dev/null +++ b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php @@ -0,0 +1,433 @@ + + */ +class BasicExpressionAnalysisTest extends ExpressionAnalysisTestCase +{ + protected static $field = true; + + public function testNativeTypes() + { + $this->assertReturnsNativeType(function () { ''; }, INativeType::TYPE_STRING); + $this->assertReturnsNativeType(function () { 'abcef'; }, INativeType::TYPE_STRING); + $this->assertReturnsNativeType(function () { 1; }, INativeType::TYPE_INT); + $this->assertReturnsNativeType(function () { 133453; }, INativeType::TYPE_INT); + $this->assertReturnsNativeType(function () { true; }, INativeType::TYPE_BOOL); + $this->assertReturnsNativeType(function () { false; }, INativeType::TYPE_BOOL); + $this->assertReturnsNativeType(function () { null; }, INativeType::TYPE_NULL); + $this->assertReturnsNativeType(function () { 3.14; }, INativeType::TYPE_DOUBLE); + $this->assertReturnsNativeType(function () { []; }, INativeType::TYPE_ARRAY); + $this->assertReturnsNativeType(function () { [1,2 , 'ddsad' => 2, 'abc']; }, INativeType::TYPE_ARRAY); + } + + public function testNativeTypesWithVariables() + { + $values = [ + INativeType::TYPE_STRING => 'abc', + INativeType::TYPE_INT => -34, + INativeType::TYPE_BOOL => true, + INativeType::TYPE_DOUBLE => -4.2454, + INativeType::TYPE_NULL => null, + INativeType::TYPE_ARRAY => [222, ''] + ]; + + foreach($values as $type => $value) { + $this->assertReturnsNativeType(function () { $var; }, $type, ['var' => $this->typeSystem->getTypeFromValue($value)]); + } + } + + public function testResourceWithVariable() + { + $this->assertReturnsNativeType( + function () { $var; }, + INativeType::TYPE_RESOURCE, + ['var' => $this->typeSystem->getTypeFromValue(fopen('php://memory', 'r'))]); + } + + public function testCasts() + { + $values = [ + INativeType::TYPE_STRING => 'abc', + INativeType::TYPE_INT => -34, + INativeType::TYPE_BOOL => true, + INativeType::TYPE_DOUBLE => -4.2454, + INativeType::TYPE_NULL => null, + INativeType::TYPE_ARRAY => [222, ''] + ]; + + foreach($values as $value) { + $variableType = ['var' => $this->typeSystem->getTypeFromValue($value)]; + if(!is_array($value)) { + $this->assertReturnsNativeType(function () { (string)$var; }, INativeType::TYPE_STRING, $variableType); + } + $this->assertReturnsNativeType(function () { (int)$var; }, INativeType::TYPE_INT, $variableType); + $this->assertReturnsNativeType(function () { (bool)$var; }, INativeType::TYPE_BOOL, $variableType); + $this->assertReturnsNativeType(function () { (double)$var; }, INativeType::TYPE_DOUBLE, $variableType); + $this->assertReturnsNativeType(function () { (array)$var; }, INativeType::TYPE_ARRAY, $variableType); + $this->assertReturnsObjectType(function () { (object)$var; }, 'stdClass', $variableType); + } + } + + public function testInvalidCasts() + { + $this->assertAnalysisFails(function () { (string)['abc']; }); + } + + public function testUnaryOperators() + { + $asserts = [ + [function () { +1; }, INativeType::TYPE_INT], + [function () { -1; }, INativeType::TYPE_INT], + [function () { ~1; }, INativeType::TYPE_INT], + [function () { ++$i; }, INativeType::TYPE_INT, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { --$i; }, INativeType::TYPE_INT, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i++; }, INativeType::TYPE_INT, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i--; }, INativeType::TYPE_INT, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { +''; }, INativeType::TYPE_INT], + [function () { -''; }, INativeType::TYPE_INT], + [function () { ~''; }, INativeType::TYPE_STRING], + [function () { ++$i; }, INativeType::TYPE_MIXED, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_STRING)]], + [function () { --$i; }, INativeType::TYPE_MIXED, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_STRING)]], + [function () { $i++; }, INativeType::TYPE_STRING, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_STRING)]], + [function () { $i--; }, INativeType::TYPE_STRING, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_STRING)]], + ]; + + foreach($asserts as $assert) { + $this->assertReturnsNativeType($assert[0], $assert[1], isset($assert[2]) ? $assert[2] : []); + } + } + + public function testInvalidUnaryOperators() + { + $asserts = [ + [function () { +[]; }], + [function () { -[]; }], + [function () { ~[]; }], + [function () { ++$i; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_ARRAY)]], + [function () { --$i; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_ARRAY)]], + [function () { $i++; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_ARRAY)]], + [function () { $i--; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_ARRAY)]], + ]; + + foreach($asserts as $assert) { + $this->assertAnalysisFails($assert[0], isset($assert[1]) ? $assert[1] : []); + } + } + + public function testFunctionCalls() + { + $this->assertReturnsNativeType(function () { strlen(''); }, INativeType::TYPE_INT); + $this->assertReturnsNativeType(function () { is_string(''); }, INativeType::TYPE_BOOL); + } + + public function testInvalidFunctionCall() + { + $this->assertAnalysisFails(function () { qwertyuiop(); }); + $this->assertAnalysisFails(function ($var) { $var(); }); + } + + public function testStaticMethodCall() + { + $this->assertReturnsObjectType(function () { \DateTime::createFromFormat(); }, 'DateTime'); + $this->assertReturnsNativeType(function () { \DateTime::getLastErrors(); }, INativeType::TYPE_ARRAY); + } + + public function testInvalidStaticMethodCall() + { + $this->assertAnalysisFails(function () { \DateTime::AasfFFD(); }); + $this->assertAnalysisFails(function ($var) { \DateTime::$var(); }); + } + + public function testStaticField() + { + $this->assertReturnsNativeType(function () { self::$field; }, INativeType::TYPE_MIXED); + } + + public function testInvalidStaticField() + { + $this->assertAnalysisFails(function () { \DateTimeZone::$abcdef; }); + $this->assertAnalysisFails(function ($var) { \DateTime::$$var; }); + } + + public function testNew() + { + $this->assertReturnsObjectType(function () { new \stdClass; }, 'stdClass'); + $this->assertReturnsObjectType(function () { new \DateTime(); }, 'DateTime'); + } + + public function testInvalidNew() + { + $this->assertAnalysisFails(function () { new sdsdsdvds(); }); + $this->assertAnalysisFails(function ($var) { new $var(); }); + } + + public function testMethodCalls() + { + $this->assertReturnsNativeType(function (\DateTime $dateTime) { $dateTime->format(''); }, INativeType::TYPE_STRING); + $this->assertReturnsNativeType(function (ITraversable $traversable) { $traversable->count(); }, INativeType::TYPE_INT); + } + + public function testInvocation() + { + $this->assertReturnsNativeType(function (\Closure $closure) { $closure(); }, INativeType::TYPE_MIXED); + } + + public function testInvalidInvocation() + { + $this->assertAnalysisFails(function (\stdClass $class) { $class(); }); + } + + public function testInvalidMethodCall() + { + $this->assertAnalysisFails(function (\DateTime $invalid) { $invalid->abcd(); }); + $this->assertAnalysisFails(function (\DateTime $invalid, $var) { $invalid->$var(); }); + } + + public function testFields() + { + $this->assertReturnsNativeType(function (\DateInterval $dateInterval) { $dateInterval->y; }, INativeType::TYPE_INT); + $this->assertReturnsNativeType(function (\DateInterval $dateInterval) { $dateInterval->m; }, INativeType::TYPE_INT); + $this->assertReturnsNativeType(function (\DateInterval $dateInterval) { $dateInterval->d; }, INativeType::TYPE_INT); + $this->assertReturnsNativeType(function (\DateInterval $dateInterval) { $dateInterval->days; }, INativeType::TYPE_MIXED); + } + + public function testInvalidField() + { + $this->assertAnalysisFails(function (\DateTime $invalid) { $invalid->foo; }); + $this->assertAnalysisFails(function (\DateTime $invalid, $var) { $invalid->$var; }); + } + + public function testIndexers() + { + $this->assertReturnsNativeType(function (array $array) { $array['foo']; }, INativeType::TYPE_MIXED); + $this->assertReturnsNativeType(function (\ArrayAccess $arrayAccess) { $arrayAccess[3]; }, INativeType::TYPE_MIXED); + $this->assertReturnsNativeType(function (\ArrayAccess $arrayAccess) { $arrayAccess['var']; }, INativeType::TYPE_MIXED); + $this->assertReturnsNativeType(function (ITraversable $traversable) { $traversable['bar']; }, INativeType::TYPE_MIXED); + $this->assertReturnsNativeType(function (IQueryable $traversable) { $traversable['bar']; }, INativeType::TYPE_MIXED); + $this->assertReturnsNativeType(function (IRepository $traversable) { $traversable['bar']; }, INativeType::TYPE_MIXED); + } + + public function testInvalidIndexers() + { + $this->assertAnalysisFails(function (\DateTime $invalid) { $invalid['123a']; }); + } + + public function testIsset() + { + $this->assertReturnsNativeType(function ($foo) { isset($foo); }, INativeType::TYPE_BOOL); + $this->assertReturnsNativeType(function ($foo, \DateInterval $bar) { isset($foo, $bar->y); }, INativeType::TYPE_BOOL); + $this->assertReturnsNativeType(function (\ArrayAccess $foo) { isset($foo['abc']); }, INativeType::TYPE_BOOL); + } + + public function testEmpty() + { + $this->assertReturnsNativeType(function ($foo) { empty($foo); }, INativeType::TYPE_BOOL); + $this->assertReturnsNativeType(function (\DateInterval $foo) { empty($foo->m); }, INativeType::TYPE_BOOL); + $this->assertReturnsNativeType(function (array $foo) { empty($foo['abc']); }, INativeType::TYPE_BOOL); + } + + public function testClosure() + { + $this->assertReturnsObjectType(function () { function ($i) {}; }, 'Closure'); + $this->assertReturnsObjectType(function () { function ($i) { return 3454; }; }, 'Closure'); + $this->assertReturnsObjectType(function (\Closure $var) { $var->bindTo(__CLASS__)->bindTo(__CLASS__)->bindTo(__CLASS__)->bindTo(__CLASS__); }, 'Closure'); + } + + public function testBinaryOperators() + { + $asserts = [ + INativeType::TYPE_INT => [ + function () { 1 + 1; }, + function () { 1 - 1; }, + function () { 1 * 1; }, + function () { 1 & 1; }, + function () { '' & 1; }, + function () { 1 | 1; }, + function () { '' | 1; }, + function () { 1 << 1; }, + function () { 1.0 << 1; }, + function () { 1 >> 1; }, + function () { 1 >> 1.0; }, + function () { 1 ^ 1; }, + function () { 1 ^ 1.0; }, + function () { 1.0 ^ 1.0; }, + ], + INativeType::TYPE_DOUBLE => [ + function () { 1 + 1.0; }, + function () { 1.0 + 1; }, + function () { 1.0 + 1.0; }, + function () { 1 - 1.0; }, + function () { 1.0 - 1; }, + function () { 1.0 - 1.0; }, + function () { 1 * 1.0; }, + function () { 1.0 * 1; }, + function () { 1.0 * 1.0; }, + function () { 3.4 / 24; }, + function () { 34 / 2.4; }, + function () { 3.4 / 2.34; }, + ], + INativeType::TYPE_BOOL => [ + function () { 1 && 1.0; }, + function () { 1 && 0; }, + function () { true && 0; }, + function () { 0 && true; }, + function () { false && true; }, + function () { '' && true; }, + function () { false && ''; }, + function () { 2.3 && true; }, + function () { false && 2.1; }, + function () { [] && true; }, + function () { false && [1,2]; }, + function () { 1 || 1.0; }, + function () { 1 || 0; }, + function () { true || 0; }, + function () { 0 || true; }, + function () { false || true; }, + function () { '' || true; }, + function () { false || ''; }, + function () { 2.3 || true; }, + function () { false || 2.1; }, + function () { [] || true; }, + function () { false || [1,2]; }, + ], + INativeType::TYPE_ARRAY => [ + function () { [] + [1,2]; }, + function () { [] + [1,2,3] + [2] + ['abc']; }, + ], + INativeType::TYPE_STRING => [ + function () { 'abc' . '123'; }, + function () { 'abc' . 123; }, + function () { 'abc' . 123.42; }, + function () { 123 . 'ab'; }, + function () { 123.42 . 'a'; }, + function () { 2 . 3.45; }, + function () { false . ''; }, + function () { '' . true; }, + function () { false . true; }, + function () { false . 3.2; }, + function () { 3 . 9; }, + ], + INativeType::TYPE_MIXED => [ + function () { '123' + 1; }, + function () { '123' - 1; }, + function () { '123' * 1; }, + function () { '123' + 1; }, + function () { 3 + 'av1'; }, + function () { 3 - 'av1'; }, + function () { 3 * 'av1'; }, + function () { 3 / 'av1'; }, + function () { 'as' + 'av1'; }, + function () { 1 / 2; }, + function () { 1 / 1; }, + function () { '123' / 24; }, + ], + ]; + + foreach($asserts as $expectedType => $expressions) + { + foreach($expressions as $expression) { + $this->assertReturnsNativeType($expression, $expectedType); + } + } + } + + public function testInvalidBinaryOperator() + { + $this->assertAnalysisFails(function () { [] - 3.4; }); + } + + public function testTernaryWithNativeTypes() + { + $asserts = [ + INativeType::TYPE_INT => [ + function () { true ? 1 : 2; }, + function () { true ? 31 : -2; }, + function () { true ? strlen('') : 2; }, + ], + INativeType::TYPE_DOUBLE => [ + function () { true ? 1.0 : 2.0; }, + function () { true ? 1.23 : 2.34; }, + ], + INativeType::TYPE_BOOL => [ + function () { true ? true : false; }, + function () { true ? true : (bool)0; }, + function () { true ? : (bool)0; }, + ], + INativeType::TYPE_ARRAY => [ + function () { true ? [] : []; }, + function () { true ? [] : [2434]; }, + function () { true ? [1,2, []] : ([4] + []); }, + ], + INativeType::TYPE_STRING => [ + function () { true ? '22' : ''; }, + function () { true ? 'abc' : '343'; }, + function () { true ? (string)2 : '343'; }, + ], + INativeType::TYPE_MIXED => [ + function () { true ? (string)2 : 343; }, + function () { true ? 1 : 2.0; }, + function () { true ? strlen('') : 'abc'; }, + function () { true ? [] : 123; }, + function () { true ? [] : new \stdClass(); }, + function () { true ? 2434 : new \stdClass(); }, + function () { true ? new \DateTime() : new \stdClass(); }, + function () { 'abc' ? : new \stdClass(); }, + ], + ]; + + foreach($asserts as $expectedType => $expressions) + { + foreach($expressions as $expression) { + $this->assertReturnsNativeType($expression, $expectedType); + } + } + + $this->assertReturnsNativeType($expression, $expectedType); + } + + public function testTernaryWithObjectTypes() + { + $asserts = [ + 'stdClass' => [ + function () { true ? new \stdClass() : new \stdClass(); }, + function () { true ? new \stdClass() : (object)[]; }, + function () { true ? (object)[1,2,4] : (object)[]; }, + ], + 'Traversable' => [ + function (\Iterator $a, \IteratorAggregate $b) { true ? $a : $b; }, + ], + 'ArrayObject' => [ + function (\ArrayObject $a, \ArrayObject $b) { true ? $a : $b; }, + ], + ]; + + foreach($asserts as $expectedType => $expressions) + { + foreach($expressions as $expression) { + $this->assertReturnsObjectType($expression, $expectedType); + } + } + + $this->assertReturnsType( + function (\ArrayObject $a, \ArrayIterator $b) { + 0 ? $a : $b; + }, + $this->typeSystem->getCompositeType( + [ + $this->typeSystem->getObjectType('Countable'), + $this->typeSystem->getObjectType('ArrayAccess'), + $this->typeSystem->getObjectType('Traversable'), + $this->typeSystem->getObjectType('Serializable'), + ] + ) + ); + } +} \ No newline at end of file diff --git a/Tests/Integration/Analysis/ComplexExpressionAnalysisTest.php b/Tests/Integration/Analysis/ComplexExpressionAnalysisTest.php new file mode 100644 index 0000000..84c6354 --- /dev/null +++ b/Tests/Integration/Analysis/ComplexExpressionAnalysisTest.php @@ -0,0 +1,89 @@ + + */ +class ComplexExpressionAnalysisTest extends ExpressionAnalysisTestCase +{ + public function testExampleFromDocs() + { + $this->assertReturnsNativeType( + function (ITraversable $traversable) { + $traversable + ->where(function (array $row) { return $row['age'] <= 50; }) + ->orderByAscending(function (array $row) { return $row['firstName']; }) + ->thenByAscending(function (array $row) { return $row['lastName']; }) + ->take(50) + ->indexBy(function (array $row) { return $row['phoneNumber']; }) + ->select(function (array $row) { + return [ + 'fullName' => $row['firstName'] . ' ' . $row['lastName'], + 'address' => $row['address'], + 'dateOfBirth' => $row['dateOfBirth'], + ]; + }) + ->implode(':', function (array $row) { return $row['fullName']; }); + }, + INativeType::TYPE_STRING + ); + } + + public function testReferences() + { + $this->assertReturnsNativeType( + function (\stdClass $foo) { + $var =& $foo; + $bar =& $var; + $abcd =& $bar; + $dsc =& $bar; + $bar = 'abc'; + + return $foo; + }, + INativeType::TYPE_STRING + ); + + $this->assertReturnsNativeType( + function (\stdClass $foo) { + $var =& $foo; + $bar =& $var; + $abcd =& $bar; + $dsc =& $bar; + $bar = 'abc'; + + return $abcd; + }, + INativeType::TYPE_STRING + ); + + $this->assertReturnsNativeType( + function (\stdClass $foo) { + $var =& $foo; + $bar =& $var; + $abcd =& $bar; + $dsc =& $bar; + $foo = 3.42; + + return $dsc; + }, + INativeType::TYPE_DOUBLE + ); + + + $this->assertReturnsNativeType( + function (array $foo) { + $var =& $foo; + $bar = 3.42; + $var =& $bar; + + return $foo; + }, + INativeType::TYPE_ARRAY + ); + } +} \ No newline at end of file diff --git a/Tests/Integration/Analysis/ExpressionAnalysisTestCase.php b/Tests/Integration/Analysis/ExpressionAnalysisTestCase.php new file mode 100644 index 0000000..3b97d59 --- /dev/null +++ b/Tests/Integration/Analysis/ExpressionAnalysisTestCase.php @@ -0,0 +1,148 @@ + + */ +class ExpressionAnalysisTestCase extends PinqTestCase +{ + /** + * @var IFunctionInterpreter + */ + protected $functionInterpreter; + + /** + * @var ITypeSystem + */ + protected $typeSystem; + + /** + * @var IExpressionAnalyser + */ + protected $expressionAnalyser; + + protected function setUp() + { + $this->functionInterpreter = $this->functionInterpreter(); + $this->typeSystem = $this->setUpTypeSystem(); + $this->expressionAnalyser = $this->setUpExpressionAnalyser(); + } + + /** + * @return IFunctionInterpreter + */ + protected function functionInterpreter() + { + return FunctionInterpreter::getDefault(); + } + + /** + * @return ITypeSystem + */ + protected function setUpTypeSystem() + { + return new PhpTypeSystem(); + } + + /** + * @return ITypeSystem + */ + protected function setUpExpressionAnalyser() + { + return new ExpressionAnalyser($this->typeSystem); + } + + protected function assertReturnsType(callable $expression, IType $expected, array $variableTypeMap = []) + { + $analysis = $this->getAnalysis($expression, $variableTypeMap); + $compiled = $analysis->getExpression()->compileDebug(); + $returnedType = $analysis->getReturnedType(); + + $this->assertEqualTypes($expected, $returnedType, $compiled); + } + + protected function assertEqualTypes(IType $expected, IType $actual, $message = '') + { + $this->assertSame($expected->getIdentifier(), $actual->getIdentifier(), $message); + $this->assertTrue($expected->isEqualTo($actual), $message); + $this->assertTrue($actual->isEqualTo($expected), $message); + $this->assertSame($expected, $actual, $message); + } + + protected function assertEqualsNativeType($nativeType, IType $actual, $message = '') + { + $this->assertEqualTypes($this->typeSystem->getNativeType($nativeType), $actual, $message); + } + + protected function assertEqualsObjectType($classType, IType $actual, $message = '') + { + $this->assertEqualTypes($this->typeSystem->getObjectType($classType), $actual, $message); + } + + protected function assertReturnsNativeType(callable $expression, $nativeType, array $variableTypeMap = []) + { + $this->assertReturnsType($expression, $this->typeSystem->getNativeType($nativeType), $variableTypeMap); + } + + protected function assertReturnsObjectType(callable $expression, $objectType, array $variableTypeMap = []) + { + $this->assertReturnsType($expression, $this->typeSystem->getObjectType($objectType), $variableTypeMap); + } + + protected function assertAnalysisFails(callable $expression, array $variableTypeMap = [], $message = '') + { + try { + $this->getAnalysis($expression, $variableTypeMap); + $this->fail( + 'Expecting analysis to fail with exception of type \\Pinq\\Analysis\\TypeException: no exception was thrown' . $message); + } catch (\Exception $exception) { + + } + } + + /** + * @param callable $function + * @param array $variableTypeMap + * @param mixed $expression + * + * @return ITypeAnalysis + */ + protected function getAnalysis(callable $function, array $variableTypeMap = [], &$expression = null) + { + $reflection = $this->functionInterpreter->getReflection($function); + foreach ($reflection->getSignature()->getParameterExpressions() as $parameterExpression) { + $variableTypeMap[$parameterExpression->getName()] = $this->typeSystem->getTypeFromTypeHint( + $parameterExpression->getTypeHint() + ); + } + $analysisContext = new AnalysisContext(new EvaluationContext(__NAMESPACE__, get_called_class(), $this)); + foreach ($variableTypeMap as $variable => $type) { + $analysisContext->setExpressionType(O\Expression::variable(O\Expression::value($variable)), $type); + } + + $bodyExpressions = $this->functionInterpreter->getStructure($reflection)->getBodyExpressions(); + foreach($bodyExpressions as $expression) { + if($expression instanceof O\ReturnExpression) { + return $this->expressionAnalyser->analyse($analysisContext, $expression->getValue()); + } elseif( count($bodyExpressions) === 1) { + return $this->expressionAnalyser->analyse($analysisContext, $expression); + } else { + $this->expressionAnalyser->analyse($analysisContext, $expression); + } + } + } +} \ No newline at end of file diff --git a/Tests/Integration/Analysis/TypeAnalysisTest.php b/Tests/Integration/Analysis/TypeAnalysisTest.php new file mode 100644 index 0000000..3bb6ea0 --- /dev/null +++ b/Tests/Integration/Analysis/TypeAnalysisTest.php @@ -0,0 +1,364 @@ + + */ +class TypeAnalysisTest extends ExpressionAnalysisTestCase +{ + protected function doAnalysisTest(callable $expression, callable $test, array $variableTypeMap = []) + { + $analysis = $this->getAnalysis($expression, $variableTypeMap, $expression); + $this->assertSame($this->typeSystem, $analysis->getTypeSystem()); + $test($analysis, $expression); + } + + protected function assertCorrectType(ITypeAnalysis $analysis, $type, O\Expression $expression) + { + $this->assertEqualTypes($type, $analysis->getReturnTypeOf($expression)); + } + + protected function assertTypeMatchesValue(ITypeAnalysis $analysis, O\Expression $expression, IType $metadataType = null) + { + $type = $this->typeSystem->getTypeFromValue($expression->evaluate(O\EvaluationContext::staticContext(__NAMESPACE__, __CLASS__))); + $this->assertEqualTypes($type, $analysis->getReturnTypeOf($expression)); + if($metadataType !== null) { + $this->assertEqualTypes($metadataType, $type, $expression->compileDebug()); + } + } + + public function testNativeTypes() + { + $values = [ + INativeType::TYPE_STRING, + INativeType::TYPE_INT, + INativeType::TYPE_BOOL, + INativeType::TYPE_DOUBLE, + INativeType::TYPE_NULL, + INativeType::TYPE_ARRAY, + INativeType::TYPE_RESOURCE, + ]; + + foreach($values as $expectedType) { + $this->doAnalysisTest( + function () { $var; }, + function (ITypeAnalysis $analysis, O\VariableExpression $expression) use ($expectedType) { + $this->assertCorrectType($analysis, + $this->typeSystem->getNativeType($expectedType), + $expression); + }, + ['var' => $this->typeSystem->getNativeType($expectedType)] + ); + } + } + + public function testCasts() + { + $values = [ + INativeType::TYPE_STRING => function () { (string)'abc'; }, + INativeType::TYPE_INT => function () { (int)'abc'; }, + INativeType::TYPE_BOOL => function () { (bool)1; }, + INativeType::TYPE_DOUBLE => function () { (double)false; }, + INativeType::TYPE_ARRAY => function () { (array)'abc'; }, + ]; + + foreach($values as $expectedType => $expression) { + $this->doAnalysisTest($expression, + function (ITypeAnalysis $analysis, O\CastExpression $expression) use ($expectedType) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getCastValue(), + $analysis->getCast($expression)->getSourceType()); + $this->assertEqualsNativeType( + $expectedType, + $analysis->getCast($expression)->getReturnType()); + } + ); + } + } + + public function testUnaryOperators() + { + $values = [ + INativeType::TYPE_INT => function () { +4; }, + INativeType::TYPE_BOOL => function () { !true; }, + INativeType::TYPE_DOUBLE => function () { -343.23; }, + INativeType::TYPE_STRING => function () { ~'abce'; }, + ]; + + foreach($values as $expectedType => $expression) { + $this->doAnalysisTest($expression, + function (ITypeAnalysis $analysis, O\UnaryOperationExpression $expression) use ($expectedType) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getOperand(), + $analysis->getUnaryOperation($expression)->getSourceType()); + $this->assertEqualsNativeType( + $expectedType, + $analysis->getUnaryOperation($expression)->getReturnType()); + } + ); + } + } + + public function testFunctionCalls() + { + $this->doAnalysisTest( + function () { strlen('abc'); }, + function (ITypeAnalysis $analysis, O\FunctionCallExpression $expression) { + $this->assertEqualsNativeType( + INativeType::TYPE_INT, + $analysis->getFunction($expression)->getReturnType()); + $this->assertTypeMatchesValue( + $analysis, + $expression->getArguments()[0]->getValue()); + $this->assertSame('strlen', $analysis->getFunction($expression)->getName()); + $this->assertSame('strlen', $analysis->getFunction($expression)->getReflection()->getName()); + } + ); + } + + public function testStaticMethodCall() + { + $this->doAnalysisTest( + function () { \DateTime::createFromFormat('U', 1993); }, + function (ITypeAnalysis $analysis, O\StaticMethodCallExpression $expression) { + $this->assertEqualsObjectType( + 'DateTime', + $analysis->getStaticMethod($expression)->getReturnType()); + $this->assertEqualsObjectType('DateTime', $analysis->getStaticMethod($expression)->getSourceType()); + $this->assertTypeMatchesValue( + $analysis, + $expression->getArguments()[0]->getValue()); + $this->assertTypeMatchesValue( + $analysis, + $expression->getArguments()[1]->getValue()); + $this->assertSame('createFromFormat', $analysis->getStaticMethod($expression)->getName()); + $this->assertSame('createFromFormat', $analysis->getStaticMethod($expression)->getReflection()->getName()); + } + ); + } + + protected static $foo; + + public function testStaticField() + { + $this->doAnalysisTest( + function () { self::$foo; }, + function (ITypeAnalysis $analysis, O\StaticFieldExpression $expression) { + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $analysis->getReturnTypeOf($expression)); + $this->assertEqualsNativeType( + INativeType::TYPE_MIXED, + $analysis->getStaticField($expression)->getReturnType() + ); + $this->assertEqualsObjectType(__CLASS__, $analysis->getStaticField($expression)->getSourceType()); + $this->assertSame('foo', $analysis->getStaticField($expression)->getName()); + $this->assertSame(true, $analysis->getStaticField($expression)->isStatic()); + } + ); + } + + public function testNew() + { + $this->doAnalysisTest( + function () { new \DateTimeZone('123'); }, + function (ITypeAnalysis $analysis, O\NewExpression $expression) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getArguments()[0]->getValue(), + $this->typeSystem->getNativeType(INativeType::TYPE_STRING)); + $this->assertEqualsObjectType('DateTimeZone', $analysis->getConstructor($expression)->getSourceType()); + $this->assertEqualsObjectType('DateTimeZone', $analysis->getConstructor($expression)->getReturnType()); + $this->assertSame(true, $analysis->getConstructor($expression)->getReflection()->isConstructor()); + $this->assertSame('DateTimeZone', $analysis->getConstructor($expression)->getReflection()->getDeclaringClass()->getName()); + } + ); + } + + public function testMethodCalls() + { + $this->doAnalysisTest( + function (\DateTime $dateTime) { $dateTime->format('abc'); }, + function (ITypeAnalysis $analysis, O\MethodCallExpression $expression) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getArguments()[0]->getValue()); + $this->assertEqualsObjectType('DateTime', $analysis->getMethod($expression)->getSourceType()); + $this->assertEqualsNativeType(INativeType::TYPE_STRING, $analysis->getMethod($expression)->getReturnType()); + $this->assertSame('format', $analysis->getMethod($expression)->getReflection()->getName()); + $this->assertSame('format', $analysis->getMethod($expression)->getName()); + } + ); + } + + public function testInvocation() + { + $this->doAnalysisTest( + function (\Closure $closure) { $closure('abc'); }, + function (ITypeAnalysis $analysis, O\InvocationExpression $expression) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getArguments()[0]->getValue(), + $this->typeSystem->getNativeType(INativeType::TYPE_STRING)); + $this->assertEqualsObjectType('Closure', $analysis->getInvocation($expression)->getSourceType()); + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $analysis->getInvocation($expression)->getReturnType()); + } + ); + $this->assertReturnsNativeType(function (\Closure $closure) { $closure(); }, INativeType::TYPE_MIXED); + } + + public function testFields() + { + $this->doAnalysisTest( + function (\DateInterval $interval) { $interval->d; }, + function (ITypeAnalysis $analysis, O\FieldExpression $expression) { + $this->assertEqualsObjectType('DateInterval', $analysis->getField($expression)->getSourceType()); + $this->assertEqualsNativeType(INativeType::TYPE_INT, $analysis->getField($expression)->getReturnType()); + $this->assertSame('d', $analysis->getField($expression)->getName()); + $this->assertSame(false, $analysis->getField($expression)->isStatic()); + } + ); + } + + public function testIndexers() + { + $this->doAnalysisTest( + function (array $array) { $array['abc']; }, + function (ITypeAnalysis $analysis, O\IndexExpression $expression) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getIndex()); + $this->assertEqualsNativeType(INativeType::TYPE_ARRAY, $analysis->getIndex($expression)->getSourceType()); + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $analysis->getIndex($expression)->getReturnType()); + } + ); + } + + public function testIsset() + { + $this->doAnalysisTest( + function () { isset(self::$foo); }, + function (ITypeAnalysis $analysis, O\IssetExpression $expression) { + $this->assertEqualsNativeType( + INativeType::TYPE_MIXED, + $analysis->getReturnTypeOf($expression->getValues()[0]) + ); + $this->assertEqualsNativeType(INativeType::TYPE_BOOL, $analysis->getReturnTypeOf($expression)); + } + ); + } + + public function testEmpty() + { + $this->doAnalysisTest( + function () { empty(self::$foo); }, + function (ITypeAnalysis $analysis, O\EmptyExpression $expression) { + $this->assertEqualsNativeType( + INativeType::TYPE_MIXED, + $analysis->getReturnTypeOf($expression->getValue()) + ); + $this->assertEqualsNativeType(INativeType::TYPE_BOOL, $analysis->getReturnTypeOf($expression)); + } + ); + } + + public function testClosure() + { + $this->doAnalysisTest( + function (\stdClass $foo) { function (array $bar) use ($foo) { $foo; $bar;}; }, + function (ITypeAnalysis $analysis, O\ClosureExpression $expression) { + $this->assertEqualsObjectType( + 'stdClass', + $analysis->getReturnTypeOf($expression->getBodyExpressions()[0])); + $this->assertEqualsNativeType( + INativeType::TYPE_ARRAY, + $analysis->getReturnTypeOf($expression->getBodyExpressions()[1])); + $this->assertEqualsObjectType('Closure', $analysis->getReturnTypeOf($expression)); + } + ); + } + + public function testTernary() + { + $values = [ + INativeType::TYPE_INT => function () { 3.2 ? 1 : -56; }, + INativeType::TYPE_BOOL => function () { 'abc' ? true : false; }, + INativeType::TYPE_DOUBLE => function () { 3 ? 343.23 : 2.34; }, + INativeType::TYPE_STRING => function () { false ? 'abce' : '1234.3'; }, + INativeType::TYPE_ARRAY => function () { false ? ['abc'] : [1,2,3]; }, + INativeType::TYPE_MIXED => function () { '' ? 3 : 4.3; }, + ]; + + foreach($values as $expectedType => $expression) { + $this->doAnalysisTest($expression, + function (ITypeAnalysis $analysis, O\TernaryExpression $expression) use ($expectedType) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getIfFalse() + ); + $this->assertTypeMatchesValue( + $analysis, + $expression->getIfTrue() + ); + $this->assertEqualsNativeType( + $expectedType, + $analysis->getReturnTypeOf($expression) + ); + } + ); + } + } + + public function testBinaryOperators() + { + $values = [ + INativeType::TYPE_INT => function () { 2 + 4; }, + INativeType::TYPE_BOOL => function () { true && false; }, + INativeType::TYPE_DOUBLE => function () { 343.23 * 2.34; }, + INativeType::TYPE_STRING => function () { 'abce' . 1234.3; }, + INativeType::TYPE_ARRAY => function () { ['abc'] + [1,2,3]; }, + ]; + + foreach($values as $expectedType => $expression) { + $this->doAnalysisTest($expression, + function (ITypeAnalysis $analysis, O\BinaryOperationExpression $expression) use ($expectedType) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getLeftOperand() + ); + $this->assertTypeMatchesValue( + $analysis, + $expression->getRightOperand() + ); + + $this->assertSame($expression->getOperator(), $analysis->getBinaryOperation($expression)->getOperator()); + } + ); + } + } + + /** + * @expectedException \Pinq\Analysis\TypeException + */ + public function testReturnTypeWithInvalidExpressionThrowsException() + { + //Data stored through identity + $this->getAnalysis(function () { 1; })->getReturnTypeOf(O\Expression::value(1)); + } + + /** + * @expectedException \Pinq\Analysis\TypeException + */ + public function testMetaDataWithInvalidExpressionThrowsException() + { + $this->getAnalysis(function () { 1 + 2; })->getFunction(O\Expression::functionCall(O\Expression::value('s'))); + } + +} \ No newline at end of file diff --git a/Tests/Integration/Analysis/TypeSystemTest.php b/Tests/Integration/Analysis/TypeSystemTest.php new file mode 100644 index 0000000..cf43541 --- /dev/null +++ b/Tests/Integration/Analysis/TypeSystemTest.php @@ -0,0 +1,196 @@ + + */ +class TypeSystemTest extends ExpressionAnalysisTestCase +{ + public function testTypeValueResolution() + { + $values = [ + INativeType::TYPE_STRING => 'abc', + INativeType::TYPE_INT => -34, + INativeType::TYPE_BOOL => true, + INativeType::TYPE_DOUBLE => -4.2454, + INativeType::TYPE_NULL => null, + INativeType::TYPE_ARRAY => [222, ''], + INativeType::TYPE_RESOURCE => fopen('php://memory', 'r') + ]; + + foreach ($values as $expectedType => $value) { + $this->assertEqualsNativeType($expectedType, $this->typeSystem->getTypeFromValue($value)); + } + + $this->assertEqualTypes($this->typeSystem->getObjectType('stdClass'), $this->typeSystem->getTypeFromValue(new \stdClass())); + } + + public function testTypeHintTypeResolution() + { + $this->assertEqualsNativeType(INativeType::TYPE_ARRAY, $this->typeSystem->getTypeFromTypeHint('array')); + $this->assertEqualsNativeType(INativeType::TYPE_ARRAY, $this->typeSystem->getTypeFromTypeHint('aRRay')); + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $this->typeSystem->getTypeFromTypeHint('callable')); + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $this->typeSystem->getTypeFromTypeHint('cAllABle')); + $this->assertEqualsObjectType('\\stdClass', $this->typeSystem->getTypeFromTypeHint('\\stdClass')); + $this->assertEqualsObjectType('\\stdClass', $this->typeSystem->getTypeFromTypeHint('\\stdCLASS')); + $this->assertEqualsObjectType('\\stdClass', $this->typeSystem->getTypeFromTypeHint('\\STDclASS')); + $this->assertEqualsObjectType('\\DateTime', $this->typeSystem->getTypeFromTypeHint('\\DateTime')); + $this->assertEqualsObjectType('\\DateTime', $this->typeSystem->getTypeFromTypeHint('\\dAtEtImE')); + } + + protected function assertCommonAncestor(IType $ancestor, IType $type1, IType $type2) + { + $this->assertEqualTypes($ancestor, $this->typeSystem->getCommonAncestorType($type1, $type2)); + $this->assertEqualTypes($ancestor, $this->typeSystem->getCommonAncestorType($type2, $type1)); + } + + protected function getObjectType($class) + { + if(is_array($class)) { + return $this->typeSystem->getCompositeType(array_map([$this->typeSystem, 'getObjectType'], $class)); + } else { + return $this->typeSystem->getObjectType($class); + } + } + + protected function assertObjectCommonAncestor($ancestor, $class1, $class2) + { + $this->assertCommonAncestor( + $this->getObjectType($ancestor), + $this->getObjectType($class1), + $this->getObjectType($class2) + ); + } + + public function testCommonAncestorResolution() + { + $mixed = $this->typeSystem->getNativeType(INativeType::TYPE_MIXED); + $this->assertCommonAncestor($mixed, + $this->typeSystem->getNativeType(INativeType::TYPE_MIXED), + $this->typeSystem->getNativeType(INativeType::TYPE_STRING) + ); + + $this->assertCommonAncestor( + $this->typeSystem->getNativeType(INativeType::TYPE_STRING), + $this->typeSystem->getNativeType(INativeType::TYPE_STRING), + $this->typeSystem->getNativeType(INativeType::TYPE_STRING) + ); + + $this->assertCommonAncestor($mixed, + $this->typeSystem->getNativeType(INativeType::TYPE_STRING), + $this->typeSystem->getObjectType('stdClass') + ); + + $this->assertObjectCommonAncestor('stdClass', 'stdClass', 'stdClass'); + + $this->assertObjectCommonAncestor( + ITraversable::ITRAVERSABLE_TYPE, + ICollection::ICOLLECTION_TYPE, + ITraversable::ITRAVERSABLE_TYPE + ); + + $this->assertObjectCommonAncestor( + 'IteratorAggregate', + 'IteratorAggregate', + IRepository::IREPOSITORY_TYPE + ); + + $this->assertObjectCommonAncestor( + ['IteratorAggregate', 'Iterator'], + ['IteratorAggregate', 'Iterator', 'Traversable'], + IRepository::IREPOSITORY_TYPE + ); + + $this->assertObjectCommonAncestor('Traversable', 'Traversable', 'ArrayObject'); + + $this->assertObjectCommonAncestor( + ['ArrayAccess', 'Countable', 'Serializable', 'Traversable'], + 'ArrayObject', + 'ArrayIterator' + ); + + $this->assertObjectCommonAncestor('Iterator', 'SeekableIterator', 'RecursiveIterator'); + + $this->assertObjectCommonAncestor('Traversable', 'Iterator', ['IteratorAggregate', 'ArrayObject', 'ArrayObject']); + + $this->assertObjectCommonAncestor('Traversable', 'ArrayObject', 'DatePeriod'); + } + + public function testFunction() + { + foreach(['strlen', '\\strlen', 'StrLEN', '\\stRlen'] as $strlenName) { + $function = $this->typeSystem->getFunction($strlenName); + $this->assertSame('strlen', $function->getName()); + $this->assertSame('strlen', $function->getReflection()->getName()); + $this->assertSame($this->typeSystem, $function->getTypeSystem()); + $this->assertEqualsNativeType(INativeType::TYPE_INT, $function->getReturnType()); + $this->assertEqualsNativeType(INativeType::TYPE_INT, $function->getReturnTypeWithArguments(['abc'])); + $this->assertEqualsNativeType(INativeType::TYPE_INT, $function->getReturnTypeWithArguments(['sdsscsc'])); + } + } + + public function testClass() + { + foreach(['stdClass', '\\stdClass', 'stdCLASS', '\\sTDClass'] as $stdClassName) { + $class = $this->typeSystem->getObjectType($stdClassName); + $this->assertSame('stdClass', $class->getClassType()); + $this->assertSame('stdClass', $class->getReflection()->getName()); + $constructor = $class->getConstructor(O\Expression::newExpression(O\Expression::value($stdClassName))); + $this->assertSame($this->typeSystem, $constructor->getTypeSystem()); + $this->assertEqualTypes($this->typeSystem->getObjectType('stdClass') , $constructor->getReturnType()); + $this->assertEqualTypes($this->typeSystem->getObjectType('stdClass') , $constructor->getSourceType()); + $this->assertSame(false , $constructor->hasMethod()); + $this->assertSame(null, $constructor->getReflection()); + } + } + + public function testClassMembers() + { + $class = $this->typeSystem->getObjectType('DateInterval'); + $this->assertSame('DateInterval', $class->getClassType()); + $this->assertSame('DateInterval', $class->getReflection()->getName()); + + $method = $class->getMethod(O\Expression::methodCall(O\Expression::value(''), O\Expression::value('FORmat'))); + $this->assertSame('format', $method->getName()); + $this->assertSame($this->typeSystem, $method->getTypeSystem()); + $this->assertEqualsNativeType(INativeType::TYPE_STRING , $method->getReturnType()); + $this->assertEqualsNativeType(INativeType::TYPE_STRING , $method->getReturnTypeWithArguments(['ssd'])); + $this->assertEqualsObjectType('DateInterval', $method->getSourceType()); + $this->assertSame('format', $method->getReflection()->getName()); + + $field = $class->getField(O\Expression::field(O\Expression::value(''), O\Expression::value('y'))); + $this->assertSame('y', $field->getName()); + $this->assertSame(false, $field->isStatic()); + } + + /** + * @expectedException \Pinq\Analysis\TypeException + */ + public function testFieldsAreCaseSensitive() + { + $class = $this->typeSystem->getObjectType('DateInterval'); + $class->getField(O\Expression::field(O\Expression::value(''), O\Expression::value('T'))); + } + + public function testArray() + { + $array = $this->typeSystem->getNativeType(INativeType::TYPE_ARRAY); + $indexer = $array->getIndex(O\Expression::index(O\Expression::value([]), O\Expression::value('s'))); + $this->assertSame($this->typeSystem, $indexer->getTypeSystem()); + $this->assertEqualsNativeType(INativeType::TYPE_ARRAY, $indexer->getSourceType()); + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $indexer->getReturnType()); + if($indexer instanceof IIndexer) { + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $indexer->getReturnTypeOfIndex(3)); + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $indexer->getReturnTypeOfIndex('abc')); + } + } +} \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f7b5638..353db00 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,6 +9,9 @@ ./Tests/Integration/ + + ./Tests/Integration/Analysis/ + ./Tests/Integration/Caching/ From 0d222729d3ba6ee8af025b4c339a927f4e5f0828 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 13 Sep 2014 18:44:16 +1000 Subject: [PATCH 02/20] Implement type inference in the analysis context from values in the variable table from the evaluation context --- Source/Analysis/AnalysisContext.php | 11 +++++++---- Source/Analysis/ExpressionAnalyser.php | 5 +++++ Source/Analysis/IAnalysisContext.php | 2 +- Source/Analysis/IExpressionAnalyser.php | 10 ++++++++++ .../Analysis/BasicExpressionAnalysisTest.php | 11 +++++++++++ .../Analysis/ExpressionAnalysisTestCase.php | 2 +- 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Source/Analysis/AnalysisContext.php b/Source/Analysis/AnalysisContext.php index 98a5ce5..3bc9a77 100644 --- a/Source/Analysis/AnalysisContext.php +++ b/Source/Analysis/AnalysisContext.php @@ -9,7 +9,7 @@ * * @author Elliot Levin */ -class AnalysisContext implements IAnalysisContext +class AnalysisContext extends Typed implements IAnalysisContext { /** * @var O\IEvaluationContext @@ -21,10 +21,13 @@ class AnalysisContext implements IAnalysisContext */ protected $expressionTypes = []; - public function __construct(O\IEvaluationContext $evaluationContext) + public function __construct(ITypeSystem $typeSystem, O\IEvaluationContext $evaluationContext) { - + parent::__construct($typeSystem); $this->evaluationContext = $evaluationContext; + foreach($evaluationContext->getVariableTable() as $variable => $value) { + $this->setExpressionType(O\Expression::variable(O\Expression::value($variable)), $typeSystem->getTypeFromValue($value)); + } } public function getEvaluationContext() @@ -55,6 +58,6 @@ public function createReference(O\Expression $expression, O\Expression $referenc public function inNewScope() { - return new self($this->evaluationContext); + return new self($this->typeSystem, $this->evaluationContext); } } \ No newline at end of file diff --git a/Source/Analysis/ExpressionAnalyser.php b/Source/Analysis/ExpressionAnalyser.php index 156d5d1..b48fd06 100644 --- a/Source/Analysis/ExpressionAnalyser.php +++ b/Source/Analysis/ExpressionAnalyser.php @@ -41,6 +41,11 @@ public function getTypeSystem() return $this->typeSystem; } + public function createAnalysisContext(O\IEvaluationContext $evaluationContext) + { + return new AnalysisContext($this->typeSystem, $evaluationContext); + } + public function analyse(IAnalysisContext $analysisContext, O\Expression $expression) { $this->analysisContext = $analysisContext; diff --git a/Source/Analysis/IAnalysisContext.php b/Source/Analysis/IAnalysisContext.php index 9cf1c83..c702452 100644 --- a/Source/Analysis/IAnalysisContext.php +++ b/Source/Analysis/IAnalysisContext.php @@ -9,7 +9,7 @@ * * @author Elliot Levin */ -interface IAnalysisContext +interface IAnalysisContext extends ITyped { /** * Gets the evaluation context. diff --git a/Source/Analysis/IExpressionAnalyser.php b/Source/Analysis/IExpressionAnalyser.php index bd1ea66..ff7091b 100644 --- a/Source/Analysis/IExpressionAnalyser.php +++ b/Source/Analysis/IExpressionAnalyser.php @@ -18,6 +18,16 @@ interface IExpressionAnalyser */ public function getTypeSystem(); + /** + * Creates a new analysis context with the supplied evaluation context. + * + * @param O\IEvaluationContext $evaluationContext + * + * @return IAnalysisContext + */ + public function createAnalysisContext(O\IEvaluationContext $evaluationContext); + + /** * Analyses the supplied expression tree. * diff --git a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php index afa793f..58fc64d 100644 --- a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php +++ b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php @@ -3,6 +3,7 @@ namespace Pinq\Tests\Integration\Analysis; use Pinq\Analysis\INativeType; +use Pinq\Analysis\TypeData\DateTime; use Pinq\IQueryable; use Pinq\IRepository; use Pinq\ITraversable; @@ -430,4 +431,14 @@ function (\ArrayObject $a, \ArrayIterator $b) { ) ); } + + public function testVariableTableFromEvaluationContextFromScopedVariables() + { + foreach ([1, null, true, 3.4, 'abc', new DateTime()] as $value) { + $this->assertReturnsType( + function () use ($value) { $value; }, + $this->typeSystem->getTypeFromValue($value) + ); + } + } } \ No newline at end of file diff --git a/Tests/Integration/Analysis/ExpressionAnalysisTestCase.php b/Tests/Integration/Analysis/ExpressionAnalysisTestCase.php index 3b97d59..a3f3590 100644 --- a/Tests/Integration/Analysis/ExpressionAnalysisTestCase.php +++ b/Tests/Integration/Analysis/ExpressionAnalysisTestCase.php @@ -129,7 +129,7 @@ protected function getAnalysis(callable $function, array $variableTypeMap = [], $parameterExpression->getTypeHint() ); } - $analysisContext = new AnalysisContext(new EvaluationContext(__NAMESPACE__, get_called_class(), $this)); + $analysisContext = $this->expressionAnalyser->createAnalysisContext($reflection->asEvaluationContext()); foreach ($variableTypeMap as $variable => $type) { $analysisContext->setExpressionType(O\Expression::variable(O\Expression::value($variable)), $type); } From b0a89369c6436a7f5c2922ab5931987c5074d94a Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 13 Sep 2014 18:53:57 +1000 Subject: [PATCH 03/20] Add more tests for expression analyser --- .../Analysis/BasicExpressionAnalysisTest.php | 36 +++++++++++++++++++ .../Integration/Analysis/TypeAnalysisTest.php | 13 +++++++ 2 files changed, 49 insertions(+) diff --git a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php index 58fc64d..f422216 100644 --- a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php +++ b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php @@ -53,6 +53,11 @@ function () { $var; }, ['var' => $this->typeSystem->getTypeFromValue(fopen('php://memory', 'r'))]); } + public function testInvalidVariable() + { + $this->assertAnalysisFails(function ($foo) { $bar; }); + } + public function testCasts() { $values = [ @@ -441,4 +446,35 @@ function () use ($value) { $value; }, ); } } + + public function testConstantTypeAnalysis() + { + $this->assertReturnsNativeType( + function () { SORT_ASC; }, + INativeType::TYPE_INT + ); + + $this->assertReturnsNativeType( + function () { M_PI; }, + INativeType::TYPE_DOUBLE + ); + } + + public function testClassConstantTypeAnalysis() + { + $this->assertReturnsNativeType( + function () { \ArrayObject::ARRAY_AS_PROPS; }, + INativeType::TYPE_INT + ); + + $this->assertReturnsNativeType( + function () { \DateTime::ATOM; }, + INativeType::TYPE_STRING + ); + } + + public function testInvalidClassConstantTypeAnalysis() + { + $this->assertAnalysisFails(function ($foo) { $foo::ARRAY_AS_PROPS; }); + } } \ No newline at end of file diff --git a/Tests/Integration/Analysis/TypeAnalysisTest.php b/Tests/Integration/Analysis/TypeAnalysisTest.php index 3bb6ea0..d00ecf7 100644 --- a/Tests/Integration/Analysis/TypeAnalysisTest.php +++ b/Tests/Integration/Analysis/TypeAnalysisTest.php @@ -344,6 +344,19 @@ function (ITypeAnalysis $analysis, O\BinaryOperationExpression $expression) use } } + public function testThrow() + { + $this->doAnalysisTest( + function () { throw new \LogicException(); }, + function (ITypeAnalysis $analysis, O\ThrowExpression $expression) { + $this->assertTypeMatchesValue( + $analysis, + $expression->getException(), + $this->typeSystem->getObjectType('LogicException')); + } + ); + } + /** * @expectedException \Pinq\Analysis\TypeException */ From 0c4c14b8577616fec865561003fc4d73035278c8 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 13 Sep 2014 19:01:17 +1000 Subject: [PATCH 04/20] Add test for instanceof operator expression analysis --- Tests/Integration/Analysis/BasicExpressionAnalysisTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php index f422216..8eb4b45 100644 --- a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php +++ b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php @@ -303,6 +303,7 @@ function () { 2.3 || true; }, function () { false || 2.1; }, function () { [] || true; }, function () { false || [1,2]; }, + function () { false instanceof \stdClass; }, ], INativeType::TYPE_ARRAY => [ function () { [] + [1,2]; }, From 6d7470ddc380988637f029856f3d7a27868215f4 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 13 Sep 2014 19:03:47 +1000 Subject: [PATCH 05/20] Add tests for comparison operator expression analysis --- .../Analysis/BasicExpressionAnalysisTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php index 8eb4b45..9b94488 100644 --- a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php +++ b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php @@ -303,6 +303,19 @@ function () { 2.3 || true; }, function () { false || 2.1; }, function () { [] || true; }, function () { false || [1,2]; }, + function () { 3 < 3; }, + function () { 3 < 3.0; }, + function () { 3 < '3'; }, + function () { 3 <= 3; }, + function () { 3 <= '3'; }, + function () { 3.0 <= 3; }, + function () { 3 > 3; }, + function () { 3.0 > 3; }, + function () { 3 > '3'; }, + function () { 3 > 3.0; }, + function () { 3.0 >= 3; }, + function () { 3 >= 3.0; }, + function () { 3 >= '3'; }, function () { false instanceof \stdClass; }, ], INativeType::TYPE_ARRAY => [ From 6099b5212fa86c250f30f3ece29689e099ef2ac3 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 13 Sep 2014 19:07:33 +1000 Subject: [PATCH 06/20] Add missing ** binary operator in Analysis\PhpTypeSystem --- Source/Analysis/PhpTypeSystem.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Analysis/PhpTypeSystem.php b/Source/Analysis/PhpTypeSystem.php index aebf9d2..7950d67 100644 --- a/Source/Analysis/PhpTypeSystem.php +++ b/Source/Analysis/PhpTypeSystem.php @@ -519,6 +519,7 @@ protected function binaryOperations() $this->mathOperators(Operators\Binary::MULTIPLICATION), $this->mathOperators(Operators\Binary::DIVISION, INativeType::TYPE_MIXED), $this->mathOperators(Operators\Binary::MODULUS), + $this->mathOperators(Operators\Binary::POWER), $this->bitwiseOperators(Operators\Binary::BITWISE_AND), $this->bitwiseOperators(Operators\Binary::BITWISE_OR), $this->bitwiseOperators(Operators\Binary::BITWISE_XOR), From 206da29141db1ae9bea556147d505ee3dfb48729 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 13 Sep 2014 19:20:34 +1000 Subject: [PATCH 07/20] Add tests for and fix assignment operator to binary operator conversion in expression analyser --- Source/Analysis/ExpressionAnalyser.php | 4 +- .../Analysis/BasicExpressionAnalysisTest.php | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Source/Analysis/ExpressionAnalyser.php b/Source/Analysis/ExpressionAnalyser.php index b48fd06..89005f7 100644 --- a/Source/Analysis/ExpressionAnalyser.php +++ b/Source/Analysis/ExpressionAnalyser.php @@ -74,17 +74,19 @@ public function visitAssignment(O\AssignmentExpression $expression) $assignTo = $expression->getAssignTo(); $assignmentValue = $expression->getAssignmentValue(); - //$this->walk($assignTo); $this->walk($assignmentValue); $operator = $expression->getOperator(); if ($operator === O\Operators\Assignment::EQUAL) { $this->analysisContext->setExpressionType($assignTo, $this->analysis[$assignmentValue]); + $this->analysis[$expression] = $this->analysis[$assignmentValue]; } elseif ($operator === O\Operators\Assignment::EQUAL_REFERENCE) { $this->analysisContext->removeExpressionType($assignTo); $this->analysisContext->setExpressionType($assignTo, $this->analysis[$assignmentValue]); $this->analysisContext->createReference($assignTo, $assignmentValue); + $this->analysis[$expression] = $this->analysis[$assignmentValue]; } else { + $this->walk($assignTo); $binaryOperation = $this->typeSystem->getBinaryOperation( $this->analysis[$assignTo], O\Operators\Assignment::toBinaryOperator($operator), diff --git a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php index 9b94488..2e2272b 100644 --- a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php +++ b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php @@ -359,6 +359,51 @@ function () { '123' / 24; }, } } + public function testAssignmentOperators() + { + $asserts = [ + INativeType::TYPE_INT => [ + [function () { $var = 1; }], + [function () { $i %= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i ^= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i &= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i |= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i >>= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i <<= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i += 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + [function () { $i -= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_INT)]], + ], + INativeType::TYPE_DOUBLE => [ + [function ($var) { $i = 3.22; }], + [function () { $i += 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_DOUBLE)]], + [function () { $i -= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_DOUBLE)]], + ], + INativeType::TYPE_BOOL => [ + [function ($var) { $i = true; }], + ], + INativeType::TYPE_ARRAY => [ + [function () { $i = [1,12]; }], + [function () { $i += [1,12]; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_ARRAY)]], + ], + INativeType::TYPE_STRING => [ + [function ($var) { $var .= 1; }], + ], + INativeType::TYPE_MIXED => [ + [function ($var) { $i = $var; }], + [function ($var) { $i =& $var; }], + [function () { $i += 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_STRING)]], + [function () { $i -= 1; }, ['i' => $this->typeSystem->getNativeType(INativeType::TYPE_STRING)]], + ], + ]; + + foreach($asserts as $expectedType => $expressions) + { + foreach($expressions as $assert) { + $this->assertReturnsNativeType($assert[0], $expectedType, isset($assert[1]) ? $assert[1] : []); + } + } + } + public function testInvalidBinaryOperator() { $this->assertAnalysisFails(function () { [] - 3.4; }); From 374f971d1cfe9db7d29006f9cec52c689729d198 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 13 Sep 2014 19:34:51 +1000 Subject: [PATCH 08/20] Remove duplicate type data from Analysis\TypeData\InternalTypes --- Source/Analysis/TypeData/InternalTypes.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Source/Analysis/TypeData/InternalTypes.php b/Source/Analysis/TypeData/InternalTypes.php index 1ecb66e..acc7d25 100644 --- a/Source/Analysis/TypeData/InternalTypes.php +++ b/Source/Analysis/TypeData/InternalTypes.php @@ -14,14 +14,6 @@ class InternalTypes extends TypeDataModule public function types() { return [ - 'ArrayAccess' => [ - 'methods' => [ - 'offsetExists' => INativeType::TYPE_BOOL, - 'offsetGet' => INativeType::TYPE_MIXED, - 'offsetSet' => INativeType::TYPE_NULL, - 'offsetUnset' => INativeType::TYPE_NULL, - ] - ], 'ArrayAccess' => [ 'methods' => [ 'offsetExists' => INativeType::TYPE_BOOL, From 8023eea4cbe492be3af9e9c7832d4bb8013b79ca Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sun, 14 Sep 2014 11:51:22 +1000 Subject: [PATCH 09/20] Fix formatting in Analysis\TypeData\InternalFunctions --- Source/Analysis/TypeData/InternalFunctions.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Analysis/TypeData/InternalFunctions.php b/Source/Analysis/TypeData/InternalFunctions.php index 2d75d82..2143eaf 100644 --- a/Source/Analysis/TypeData/InternalFunctions.php +++ b/Source/Analysis/TypeData/InternalFunctions.php @@ -31,10 +31,10 @@ protected function dataTypes() 'is_callable' => INativeType::TYPE_BOOL, 'gettype' => INativeType::TYPE_STRING, 'serialize' => INativeType::TYPE_STRING, - 'boolval' => INativeType::TYPE_BOOL, - 'intval' => INativeType::TYPE_INT, - 'strval' => INativeType::TYPE_STRING, - 'floatval' => INativeType::TYPE_DOUBLE, + 'boolval' => INativeType::TYPE_BOOL, + 'intval' => INativeType::TYPE_INT, + 'strval' => INativeType::TYPE_STRING, + 'floatval' => INativeType::TYPE_DOUBLE, ]; } From b2dbcd8701ad1b295dd6bf978b437b1aaa4adb8f Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sun, 14 Sep 2014 11:57:49 +1000 Subject: [PATCH 10/20] Fix formatting in Analysis\IExpressionAnalyser --- Source/Analysis/IExpressionAnalyser.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Analysis/IExpressionAnalyser.php b/Source/Analysis/IExpressionAnalyser.php index ff7091b..ffb7c2e 100644 --- a/Source/Analysis/IExpressionAnalyser.php +++ b/Source/Analysis/IExpressionAnalyser.php @@ -27,7 +27,6 @@ public function getTypeSystem(); */ public function createAnalysisContext(O\IEvaluationContext $evaluationContext); - /** * Analyses the supplied expression tree. * From 91b8e64345db07f8b91075b77eb7fecdfb2ab399 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sun, 14 Sep 2014 16:34:31 +1000 Subject: [PATCH 11/20] Extract interface from Analysis\TypeData\TypeDataModule --- Source/Analysis/PhpTypeSystem.php | 7 +++-- Source/Analysis/TypeData/ITypeDataModule.php | 30 ++++++++++++++++++++ Source/Analysis/TypeData/TypeDataModule.php | 27 ++++++++++++------ 3 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 Source/Analysis/TypeData/ITypeDataModule.php diff --git a/Source/Analysis/PhpTypeSystem.php b/Source/Analysis/PhpTypeSystem.php index 7950d67..021869f 100644 --- a/Source/Analysis/PhpTypeSystem.php +++ b/Source/Analysis/PhpTypeSystem.php @@ -4,6 +4,7 @@ use Pinq; use Pinq\Analysis\Functions\Func; +use Pinq\Analysis\TypeData\ITypeDataModule; use Pinq\Analysis\TypeOperations\Constructor; use Pinq\Analysis\TypeOperations\Field; use Pinq\Analysis\TypeOperations\Indexer; @@ -36,14 +37,14 @@ class PhpTypeSystem extends TypeSystem protected $classTypeMap = []; /** - * @param TypeData\TypeDataModule[] $customTypeDataModules + * @param ITypeDataModule $customTypeDataModules */ public function __construct(array $customTypeDataModules = []) { parent::__construct(); $modules = array_merge($this->typeDataModules(), $customTypeDataModules); - /** @var $module TypeData\TypeDataModule */ + /** @var $module ITypeDataModule */ foreach($modules as $module) { foreach ($module->functions() as $name => $returnType) { $this->functionTypeMap[$this->normalizeFunctionName($name)] = $returnType; @@ -56,7 +57,7 @@ public function __construct(array $customTypeDataModules = []) } /** - * @return TypeData\TypeDataModule[] + * @return ITypeDataModule */ protected function typeDataModules() { diff --git a/Source/Analysis/TypeData/ITypeDataModule.php b/Source/Analysis/TypeData/ITypeDataModule.php new file mode 100644 index 0000000..4366e88 --- /dev/null +++ b/Source/Analysis/TypeData/ITypeDataModule.php @@ -0,0 +1,30 @@ + + */ +interface ITypeDataModule +{ + /** + * Gets a structured array of type data. + * + * @see Pinq\Analysis\TypeData\DateTime::types + * + * @return array + */ + public function types(); + + /** + * Gets an array of function names as keys with their + * returning type as values. + * + * @see Pinq\Analysis\TypeData\InternalFunctions::dataTypes + * + * @return array + */ + public function functions(); +} \ No newline at end of file diff --git a/Source/Analysis/TypeData/TypeDataModule.php b/Source/Analysis/TypeData/TypeDataModule.php index c76fed3..0fba520 100644 --- a/Source/Analysis/TypeData/TypeDataModule.php +++ b/Source/Analysis/TypeData/TypeDataModule.php @@ -5,17 +5,28 @@ use Pinq\Analysis\PhpTypeSystem; /** - * Base class for type data modules. + * Base class of the type data module. * * @author Elliot Levin */ -abstract class TypeDataModule +abstract class TypeDataModule implements ITypeDataModule { const TYPE_SELF = PhpTypeSystem::TYPE_SELF; - public function __construct() - { + /** + * @var array + */ + private $types; + + /** + * @var array + */ + private $functions; + public function __construct(array $types = [], array $functions = []) + { + $this->types = $types; + $this->functions = $functions; } public static function getType() @@ -23,13 +34,13 @@ public static function getType() return get_called_class(); } - public function functions() + public function types() { - return []; + return $this->types; } - public function types() + public function functions() { - return []; + return $this->functions; } } \ No newline at end of file From 2e0059280fdbc1703ea3155be81b65520c1474b3 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sun, 14 Sep 2014 16:59:14 +1000 Subject: [PATCH 12/20] Make Analysis\TypeData\TypeDataModule non abstract --- Source/Analysis/TypeData/TypeDataModule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Analysis/TypeData/TypeDataModule.php b/Source/Analysis/TypeData/TypeDataModule.php index 0fba520..585338f 100644 --- a/Source/Analysis/TypeData/TypeDataModule.php +++ b/Source/Analysis/TypeData/TypeDataModule.php @@ -5,11 +5,11 @@ use Pinq\Analysis\PhpTypeSystem; /** - * Base class of the type data module. + * Implementation of the type data module. * * @author Elliot Levin */ -abstract class TypeDataModule implements ITypeDataModule +class TypeDataModule implements ITypeDataModule { const TYPE_SELF = PhpTypeSystem::TYPE_SELF; From 4c340dd41158b3b9b9f06fe97a04740f56af3dac Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Fri, 19 Sep 2014 17:42:53 +1000 Subject: [PATCH 13/20] Added Countable interface to Analysis\TypeData\InternalTypes type data module --- Source/Analysis/TypeData/InternalTypes.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Analysis/TypeData/InternalTypes.php b/Source/Analysis/TypeData/InternalTypes.php index acc7d25..de72bc3 100644 --- a/Source/Analysis/TypeData/InternalTypes.php +++ b/Source/Analysis/TypeData/InternalTypes.php @@ -22,6 +22,11 @@ public function types() 'offsetUnset' => INativeType::TYPE_NULL, ] ], + 'Countable' => [ + 'methods' => [ + 'count' => INativeType::TYPE_INT, + ] + ], 'Closure' => [ 'methods' => [ 'bind' => self::TYPE_SELF, From 4d2875ed5aae2a52defce257e7338a711c40d31b Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Fri, 19 Sep 2014 17:44:07 +1000 Subject: [PATCH 14/20] Fix variable name in Analysis\TypeId::getComposedTypeIds and add composite types test --- Source/Analysis/TypeId.php | 2 +- Tests/Integration/Analysis/TypeSystemTest.php | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Source/Analysis/TypeId.php b/Source/Analysis/TypeId.php index 404be1b..501577a 100644 --- a/Source/Analysis/TypeId.php +++ b/Source/Analysis/TypeId.php @@ -41,6 +41,6 @@ public static function isComposite($id) public static function getComposedTypeIds($compositeId) { - return explode('|', substr($id, strlen('composite<'), -strlen('>'))); + return explode('|', substr($compositeId, strlen('composite<'), -strlen('>'))); } } \ No newline at end of file diff --git a/Tests/Integration/Analysis/TypeSystemTest.php b/Tests/Integration/Analysis/TypeSystemTest.php index cf43541..62392b9 100644 --- a/Tests/Integration/Analysis/TypeSystemTest.php +++ b/Tests/Integration/Analysis/TypeSystemTest.php @@ -5,6 +5,7 @@ use Pinq\Analysis\IIndexer; use Pinq\Analysis\INativeType; use Pinq\Analysis\IType; +use Pinq\Analysis\TypeId; use Pinq\Expressions as O; use Pinq\ICollection; use Pinq\IRepository; @@ -193,4 +194,21 @@ public function testArray() $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $indexer->getReturnTypeOfIndex('abc')); } } -} \ No newline at end of file + + public function testCompositeType() + { + $compositeType = $this->typeSystem->getType(TypeId::getComposite([TypeId::getObject('ArrayAccess'), TypeId::getObject('Countable')])); + + $indexer = $compositeType->getIndex(O\Expression::index(O\Expression::value([]), O\Expression::value('s'))); + $this->assertEqualTypes($this->typeSystem->getObjectType('ArrayAccess'), $indexer->getSourceType()); + $this->assertEqualsNativeType(INativeType::TYPE_MIXED, $indexer->getReturnType()); + + $method = $compositeType->getMethod(O\Expression::methodCall(O\Expression::value([]), O\Expression::value('offsetExists'))); + $this->assertEqualTypes($this->typeSystem->getObjectType('ArrayAccess'), $method->getSourceType()); + $this->assertEqualsNativeType(INativeType::TYPE_BOOL, $method->getReturnType()); + + $method = $compositeType->getMethod(O\Expression::methodCall(O\Expression::value([]), O\Expression::value('count'))); + $this->assertEqualTypes($this->typeSystem->getObjectType('Countable'), $method->getSourceType()); + $this->assertEqualsNativeType(INativeType::TYPE_INT, $method->getReturnType()); + } +} \ No newline at end of file From a5b73b5bd6c4604b482400ba6a9dd8d962267e85 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Fri, 19 Sep 2014 17:48:34 +1000 Subject: [PATCH 15/20] Renamed Analysis\TypeId::getClassType to getClassTypeFromId and Analysis\TypeId::getComposedTypeIds to getComposedTypeIdsFromId --- Source/Analysis/TypeId.php | 4 ++-- Source/Analysis/TypeSystem.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Analysis/TypeId.php b/Source/Analysis/TypeId.php index 501577a..0c69780 100644 --- a/Source/Analysis/TypeId.php +++ b/Source/Analysis/TypeId.php @@ -24,7 +24,7 @@ public static function isObject($id) return strpos($id, 'object:') === 0; } - public static function getClassType($objectId) + public static function getClassTypeFromId($objectId) { return substr($objectId, strlen('object:')); } @@ -39,7 +39,7 @@ public static function isComposite($id) return strpos($id, 'composite<') === 0 && $id[strlen($id) - 1] === '>'; } - public static function getComposedTypeIds($compositeId) + public static function getComposedTypeIdsFromId($compositeId) { return explode('|', substr($compositeId, strlen('composite<'), -strlen('>'))); } diff --git a/Source/Analysis/TypeSystem.php b/Source/Analysis/TypeSystem.php index 4f5dc19..ca4f49f 100644 --- a/Source/Analysis/TypeSystem.php +++ b/Source/Analysis/TypeSystem.php @@ -122,10 +122,10 @@ protected function buildBinaryOperations() public function getType($typeIdentifier) { if (TypeId::isObject($typeIdentifier)) { - return $this->getObjectType(TypeId::getClassType($typeIdentifier)); + return $this->getObjectType(TypeId::getClassTypeFromId($typeIdentifier)); } elseif (TypeId::isComposite($typeIdentifier)) { return $this->getCompositeType( - array_map([$this, __FUNCTION__], TypeId::getComposedTypeIds($typeIdentifier)) + array_map([$this, __FUNCTION__], TypeId::getComposedTypeIdsFromId($typeIdentifier)) ); } else { return $this->getNativeType($typeIdentifier); From f95fd0df7fa6e5cbe002cb634efa6adc3535d252 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 20 Sep 2014 10:07:11 +1000 Subject: [PATCH 16/20] Extract method to generate PINQ API type data in Analysis\TypeData\PinqAPI --- Source/Analysis/TypeData/PinqAPI.php | 200 +++++++++++++++------------ 1 file changed, 114 insertions(+), 86 deletions(-) diff --git a/Source/Analysis/TypeData/PinqAPI.php b/Source/Analysis/TypeData/PinqAPI.php index b9c57ff..0f5c455 100644 --- a/Source/Analysis/TypeData/PinqAPI.php +++ b/Source/Analysis/TypeData/PinqAPI.php @@ -44,104 +44,132 @@ public function types() $pinqTypes = []; foreach ($traversableInterfaceGroups as $traversableInterface => $traversableGroup) { - $traversableType = TypeId::getObject($traversableInterface); - $orderedTraversableType = TypeId::getObject($traversableGroup['ordered']); - $joiningOnTraversableType = TypeId::getObject($traversableGroup['joining-on']); - $joiningToTraversableType = TypeId::getObject($traversableGroup['joining-to']); + $pinqTypes += $this->generatePinqTypeData( + $traversableInterface, + $traversableGroup['ordered'], + $traversableGroup['joining-on'], + $traversableGroup['joining-to'], + !empty($traversableGroup['mutable']) + ); + } - $commonMethods = [ - 'asArray' => INativeType::TYPE_ARRAY, - 'asTraversable' => TypeId::getObject(Pinq\ITraversable::ITRAVERSABLE_TYPE), - 'asCollection' => TypeId::getObject(Pinq\ICollection::ICOLLECTION_TYPE), - 'isSource' => INativeType::TYPE_BOOL, - 'getSource' => $traversableType, - 'iterate' => INativeType::TYPE_NULL, - 'getIterator' => TypeId::getObject('Traversable'), - 'getTrueIterator' => TypeId::getObject('Traversable'), - 'getIteratorScheme' => TypeId::getObject(IIteratorScheme::IITERATOR_SCHEME_TYPE), - 'first' => INativeType::TYPE_MIXED, - 'last' => INativeType::TYPE_MIXED, - 'count' => INativeType::TYPE_INT, - 'isEmpty' => INativeType::TYPE_BOOL, - 'aggregate' => INativeType::TYPE_MIXED, - 'maximum' => INativeType::TYPE_MIXED, - 'minimum' => INativeType::TYPE_MIXED, - 'sum' => INativeType::TYPE_MIXED, - 'average' => INativeType::TYPE_MIXED, - 'all' => INativeType::TYPE_BOOL, - 'any' => INativeType::TYPE_BOOL, - 'implode' => INativeType::TYPE_STRING, - 'contains' => INativeType::TYPE_BOOL, - 'where' => $traversableType, - 'orderBy' => $orderedTraversableType, - 'orderByAscending' => $orderedTraversableType, - 'orderByDescending' => $orderedTraversableType, - 'skip' => $traversableType, - 'take' => $traversableType, - 'slice' => $traversableType, - 'indexBy' => $traversableType, - 'keys' => $traversableType, - 'reindex' => $traversableType, - 'groupBy' => $traversableType, - 'join' => $joiningOnTraversableType, - 'groupJoin' => $joiningOnTraversableType, - 'select' => $traversableType, - 'selectMany' => $traversableType, - 'unique' => $traversableType, - 'append' => $traversableType, - 'whereIn' => $traversableType, - 'except' => $traversableType, - 'union' => $traversableType, - 'intersect' => $traversableType, - 'difference' => $traversableType, - ]; + return $pinqTypes; + } - if (!empty($traversableGroup['mutable'])) { - $commonMethods += [ - 'apply' => INativeType::TYPE_NULL, - 'addRange' => INativeType::TYPE_NULL, - 'remove' => INativeType::TYPE_NULL, - 'removeRange' => INativeType::TYPE_NULL, - 'removeWhere' => INativeType::TYPE_NULL, - 'clear' => INativeType::TYPE_NULL, - ]; - } + /** + * @param string $traversableType + * @param string $orderedTraversableType + * @param string $joiningOnTraversableType + * @param string $joiningToTraversableType + * @param bool $mutable + * + * @return array + */ + protected function generatePinqTypeData( + $traversableType, + $orderedTraversableType, + $joiningOnTraversableType, + $joiningToTraversableType, + $mutable = false + ) { + $pinqTypes = []; + $traversableTypeId = TypeId::getObject($traversableType); + $orderedTraversableTypeId = TypeId::getObject($orderedTraversableType); + $joiningOnTraversableTypeId = TypeId::getObject($joiningOnTraversableType); + $joiningToTraversableTypeId = TypeId::getObject($joiningToTraversableType); - $pinqTypes[$traversableInterface] = [ - 'methods' => $commonMethods - ]; + $commonMethods = [ + 'asArray' => INativeType::TYPE_ARRAY, + 'asTraversable' => TypeId::getObject(Pinq\ITraversable::ITRAVERSABLE_TYPE), + 'asCollection' => TypeId::getObject(Pinq\ICollection::ICOLLECTION_TYPE), + 'isSource' => INativeType::TYPE_BOOL, + 'getSource' => $traversableTypeId, + 'iterate' => INativeType::TYPE_NULL, + 'getIterator' => TypeId::getObject('Traversable'), + 'getTrueIterator' => TypeId::getObject('Traversable'), + 'getIteratorScheme' => TypeId::getObject(IIteratorScheme::IITERATOR_SCHEME_TYPE), + 'first' => INativeType::TYPE_MIXED, + 'last' => INativeType::TYPE_MIXED, + 'count' => INativeType::TYPE_INT, + 'isEmpty' => INativeType::TYPE_BOOL, + 'aggregate' => INativeType::TYPE_MIXED, + 'maximum' => INativeType::TYPE_MIXED, + 'minimum' => INativeType::TYPE_MIXED, + 'sum' => INativeType::TYPE_MIXED, + 'average' => INativeType::TYPE_MIXED, + 'all' => INativeType::TYPE_BOOL, + 'any' => INativeType::TYPE_BOOL, + 'implode' => INativeType::TYPE_STRING, + 'contains' => INativeType::TYPE_BOOL, + 'where' => $traversableTypeId, + 'orderBy' => $orderedTraversableTypeId, + 'orderByAscending' => $orderedTraversableTypeId, + 'orderByDescending' => $orderedTraversableTypeId, + 'skip' => $traversableTypeId, + 'take' => $traversableTypeId, + 'slice' => $traversableTypeId, + 'indexBy' => $traversableTypeId, + 'keys' => $traversableTypeId, + 'reindex' => $traversableTypeId, + 'groupBy' => $traversableTypeId, + 'join' => $joiningOnTraversableTypeId, + 'groupJoin' => $joiningOnTraversableTypeId, + 'select' => $traversableTypeId, + 'selectMany' => $traversableTypeId, + 'unique' => $traversableTypeId, + 'append' => $traversableTypeId, + 'whereIn' => $traversableTypeId, + 'except' => $traversableTypeId, + 'union' => $traversableTypeId, + 'intersect' => $traversableTypeId, + 'difference' => $traversableTypeId, + ]; - $pinqTypes[$traversableGroup['ordered']] = [ - 'methods' => [ - 'thenBy' => $orderedTraversableType, - 'thenByAscending' => $orderedTraversableType, - 'thenByDescending' => $orderedTraversableType, - ] + $commonMethods + if ($mutable) { + $commonMethods += [ + 'apply' => INativeType::TYPE_NULL, + 'addRange' => INativeType::TYPE_NULL, + 'remove' => INativeType::TYPE_NULL, + 'removeRange' => INativeType::TYPE_NULL, + 'removeWhere' => INativeType::TYPE_NULL, + 'clear' => INativeType::TYPE_NULL, ]; + } - $joiningMethods = [ - 'withDefault' => $joiningToTraversableType, - 'to' => $traversableType, - ]; + $pinqTypes[$traversableType] = [ + 'methods' => $commonMethods + ]; - if (!empty($traversableGroup['mutable'])) { - $joiningMethods += [ - 'apply' => INativeType::TYPE_NULL, - ]; - } + $pinqTypes[$orderedTraversableType] = [ + 'methods' => [ + 'thenBy' => $orderedTraversableTypeId, + 'thenByAscending' => $orderedTraversableTypeId, + 'thenByDescending' => $orderedTraversableTypeId, + ] + $commonMethods + ]; - $pinqTypes[$traversableGroup['joining-to']] = [ - 'methods' => $joiningMethods - ]; + $joiningMethods = [ + 'withDefault' => $joiningToTraversableTypeId, + 'to' => $traversableTypeId, + ]; - $pinqTypes[$traversableGroup['joining-on']] = [ - 'methods' => [ - 'on' => $joiningToTraversableType, - 'onEquality' => $joiningToTraversableType, - ] + $joiningMethods + if ($mutable) { + $joiningMethods += [ + 'apply' => INativeType::TYPE_NULL, ]; } + $pinqTypes[$joiningToTraversableType] = [ + 'methods' => $joiningMethods + ]; + + $pinqTypes[$joiningOnTraversableType] = [ + 'methods' => [ + 'on' => $joiningToTraversableTypeId, + 'onEquality' => $joiningToTraversableTypeId, + ] + $joiningMethods + ]; + return $pinqTypes; } } \ No newline at end of file From d81b7952eea8af6a076f55f5b199db576b77699f Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 20 Sep 2014 10:08:39 +1000 Subject: [PATCH 17/20] Add more informative exception messages for unknown type operations --- Source/Analysis/Types/MixedType.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/Analysis/Types/MixedType.php b/Source/Analysis/Types/MixedType.php index ce8e5ae..9dc7006 100644 --- a/Source/Analysis/Types/MixedType.php +++ b/Source/Analysis/Types/MixedType.php @@ -24,12 +24,12 @@ public function isParentTypeOf(IType $type) return true; } - protected function unsupported(O\Expression $expression, $message) + protected function unsupported(O\Expression $expression, $message, array $formatValues = []) { return new TypeException( 'Type does not support expression \'%s\': %s', $expression->compileDebug(), - $message); + vsprintf($message, $formatValues)); } public function getConstructor(O\NewExpression $expression) @@ -39,22 +39,22 @@ public function getConstructor(O\NewExpression $expression) public function getMethod(O\MethodCallExpression $expression) { - throw $this->unsupported($expression, 'method is not supported'); + throw $this->unsupported($expression, 'method %s is not supported', [$expression->getName()->compileDebug()]); } public function getStaticMethod(O\StaticMethodCallExpression $expression) { - throw $this->unsupported($expression, 'static method is not supported'); + throw $this->unsupported($expression, 'static method %s is not supported', [$expression->getName()->compileDebug()]); } public function getField(O\FieldExpression $expression) { - throw $this->unsupported($expression, 'field is not supported'); + throw $this->unsupported($expression, 'field %s is not supported', [$expression->getName()->compileDebug()]); } public function getStaticField(O\StaticFieldExpression $expression) { - throw $this->unsupported($expression, 'static field is not supported'); + throw $this->unsupported($expression, 'static field %s is not supported', [$expression->getName()->compileDebug()]); } public function getInvocation(O\InvocationExpression $expression) @@ -69,11 +69,11 @@ public function getIndex(O\IndexExpression $expression) public function getCast(O\CastExpression $expression) { - throw $this->unsupported($expression, 'cast is not supported'); + throw $this->unsupported($expression, 'cast \'%s\' is not supported', [$expression->getCastType()]); } public function getUnaryOperation(O\UnaryOperationExpression $expression) { - throw $this->unsupported($expression, 'unary operator is not supported'); + throw $this->unsupported($expression, 'unary operator \'%s\' is not supported', [$expression->getOperator()]); } } \ No newline at end of file From 77aee8287529b2704d7c24167b35ff9a7a61cee4 Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 20 Sep 2014 10:12:32 +1000 Subject: [PATCH 18/20] Fix docblocks in Analysis\PhpTypeSystem --- Source/Analysis/PhpTypeSystem.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Analysis/PhpTypeSystem.php b/Source/Analysis/PhpTypeSystem.php index 021869f..2a64bcd 100644 --- a/Source/Analysis/PhpTypeSystem.php +++ b/Source/Analysis/PhpTypeSystem.php @@ -37,7 +37,7 @@ class PhpTypeSystem extends TypeSystem protected $classTypeMap = []; /** - * @param ITypeDataModule $customTypeDataModules + * @param ITypeDataModule[] $customTypeDataModules */ public function __construct(array $customTypeDataModules = []) { @@ -57,7 +57,7 @@ public function __construct(array $customTypeDataModules = []) } /** - * @return ITypeDataModule + * @return ITypeDataModule[] */ protected function typeDataModules() { From 9362cc3c34f35f9ae7f5f09487116a0a6634689d Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 20 Sep 2014 12:09:54 +1000 Subject: [PATCH 19/20] Allow instance calls to static methods and add tests --- Source/Analysis/Types/ObjectType.php | 4 ++-- Tests/Integration/Analysis/BasicExpressionAnalysisTest.php | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/Analysis/Types/ObjectType.php b/Source/Analysis/Types/ObjectType.php index e87f5e5..ba0f97e 100644 --- a/Source/Analysis/Types/ObjectType.php +++ b/Source/Analysis/Types/ObjectType.php @@ -137,12 +137,12 @@ public function getStaticMethod(O\StaticMethodCallExpression $expression) return parent::getStaticMethod($expression); } - protected function getMethodByName(O\Expression $nameExpression, $static) + protected function getMethodByName(O\Expression $nameExpression, $mustBeStatic) { if ($nameExpression instanceof O\ValueExpression) { $methodName = $nameExpression->getValue(); foreach ($this->methods as $otherMethodName => $method) { - if ($method->getReflection()->isStatic() === $static + if ((!$mustBeStatic || $method->getReflection()->isStatic() === true) && strcasecmp($methodName, $otherMethodName) === 0 ) { return $method; diff --git a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php index 2e2272b..82b845a 100644 --- a/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php +++ b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php @@ -146,6 +146,11 @@ public function testStaticMethodCall() $this->assertReturnsNativeType(function () { \DateTime::getLastErrors(); }, INativeType::TYPE_ARRAY); } + public function testStaticMethodCallOnInstance() + { + $this->assertReturnsObjectType(function (\DateTime $instance) { $instance->createFromFormat(); }, 'DateTime'); + } + public function testInvalidStaticMethodCall() { $this->assertAnalysisFails(function () { \DateTime::AasfFFD(); }); @@ -161,6 +166,8 @@ public function testInvalidStaticField() { $this->assertAnalysisFails(function () { \DateTimeZone::$abcdef; }); $this->assertAnalysisFails(function ($var) { \DateTime::$$var; }); + //PHP does not allow instance access of static fields unlike methods + $this->assertAnalysisFails(function (self $instance) { $instance->field; }); } public function testNew() From 257464275c2f300161a244a5807ff2b80740786e Mon Sep 17 00:00:00 2001 From: Elliot Levin Date: Sat, 20 Sep 2014 12:13:42 +1000 Subject: [PATCH 20/20] Implement methods to add type data modules to Analysis\PhpTypeSystem with tests --- Source/Analysis/PhpTypeSystem.php | 53 +++++++++++++++---- Tests/Integration/Analysis/TypeSystemTest.php | 28 ++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/Source/Analysis/PhpTypeSystem.php b/Source/Analysis/PhpTypeSystem.php index 2a64bcd..3140d8c 100644 --- a/Source/Analysis/PhpTypeSystem.php +++ b/Source/Analysis/PhpTypeSystem.php @@ -26,6 +26,11 @@ class PhpTypeSystem extends TypeSystem { const TYPE_SELF = '~~SELF_TYPE~~'; + /** + * @var ITypeDataModule[] + */ + protected $typeDataModules = []; + /** * @var string[] */ @@ -43,16 +48,10 @@ public function __construct(array $customTypeDataModules = []) { parent::__construct(); - $modules = array_merge($this->typeDataModules(), $customTypeDataModules); - /** @var $module ITypeDataModule */ - foreach($modules as $module) { - foreach ($module->functions() as $name => $returnType) { - $this->functionTypeMap[$this->normalizeFunctionName($name)] = $returnType; - } - - foreach ($module->types() as $name => $typeData) { - $this->classTypeMap[$this->normalizeClassName($name)] = $typeData; - } + $typeDataModules = array_merge($this->typeDataModules(), $customTypeDataModules); + /** @var $typeDataModules ITypeDataModule[] */ + foreach($typeDataModules as $module) { + $this->registerTypeDataModule($module); } } @@ -69,6 +68,40 @@ protected function typeDataModules() ]; } + /** + * Gets the type data modules from the type system. + * + * @return ITypeDataModule[] + */ + public function getTypeDataModules() + { + return $this->typeDataModules; + } + + /** + * Adds the type data module to the type system. + * + * @param ITypeDataModule $module + * + * @return void + */ + public function registerTypeDataModule(ITypeDataModule $module) + { + $this->typeDataModules[] = $module; + foreach ($module->functions() as $name => $returnType) { + $normalizedFunctionName = $this->normalizeFunctionName($name); + $this->functionTypeMap[$normalizedFunctionName] = $returnType; + unset($this->functions[$normalizedFunctionName]); + } + + foreach ($module->types() as $name => $typeData) { + $normalizedClassName = $this->normalizeClassName($name); + $this->classTypeMap[$normalizedClassName] = $typeData; + unset($this->objectTypes[$normalizedClassName]); + } + } + + // Normalize function / type names using reflection to get originally defined name // but fallback to lower casing due to some functions that are not universally available // such as 'money_format' which will fail with reflection if not available. diff --git a/Tests/Integration/Analysis/TypeSystemTest.php b/Tests/Integration/Analysis/TypeSystemTest.php index 62392b9..e0104b7 100644 --- a/Tests/Integration/Analysis/TypeSystemTest.php +++ b/Tests/Integration/Analysis/TypeSystemTest.php @@ -5,6 +5,8 @@ use Pinq\Analysis\IIndexer; use Pinq\Analysis\INativeType; use Pinq\Analysis\IType; +use Pinq\Analysis\PhpTypeSystem; +use Pinq\Analysis\TypeData\TypeDataModule; use Pinq\Analysis\TypeId; use Pinq\Expressions as O; use Pinq\ICollection; @@ -211,4 +213,30 @@ public function testCompositeType() $this->assertEqualTypes($this->typeSystem->getObjectType('Countable'), $method->getSourceType()); $this->assertEqualsNativeType(INativeType::TYPE_INT, $method->getReturnType()); } + + public function testRegisteringTypeDataModules() + { + if($this->typeSystem instanceof PhpTypeSystem) { + $typeDataModule = new TypeDataModule( + [__CLASS__ => ['methods' => ['assertEquals' => INativeType::TYPE_NULL]]], + ['get_defined_functions' => INativeType::TYPE_INT] + ); + + $this->assertNotContains($typeDataModule, $this->typeSystem->getTypeDataModules()); + $this->typeSystem->registerTypeDataModule($typeDataModule); + $this->assertContains($typeDataModule, $this->typeSystem->getTypeDataModules()); + + $this->assertEqualsNativeType( + INativeType::TYPE_NULL, + $this->typeSystem->getObjectType(__CLASS__)->getMethod( + O\Expression::methodCall(O\Expression::value($this), O\Expression::value('assertEquals')) + )->getReturnType() + ); + + $this->assertEqualsNativeType( + INativeType::TYPE_INT, + $this->typeSystem->getFunction('get_defined_functions')->getReturnType() + ); + } + } } \ No newline at end of file