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..3bc9a77 --- /dev/null +++ b/Source/Analysis/AnalysisContext.php @@ -0,0 +1,63 @@ + + */ +class AnalysisContext extends Typed implements IAnalysisContext +{ + /** + * @var O\IEvaluationContext + */ + protected $evaluationContext; + + /** + * @var IType[] + */ + protected $expressionTypes = []; + + 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() + { + 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->typeSystem, $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..89005f7 --- /dev/null +++ b/Source/Analysis/ExpressionAnalyser.php @@ -0,0 +1,358 @@ + + */ +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 createAnalysisContext(O\IEvaluationContext $evaluationContext) + { + return new AnalysisContext($this->typeSystem, $evaluationContext); + } + + 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($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), + $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..c702452 --- /dev/null +++ b/Source/Analysis/IAnalysisContext.php @@ -0,0 +1,67 @@ + + */ +interface IAnalysisContext extends ITyped +{ + /** + * 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..ffb7c2e --- /dev/null +++ b/Source/Analysis/IExpressionAnalyser.php @@ -0,0 +1,39 @@ + + */ +interface IExpressionAnalyser +{ + /** + * Gets the type system for the expression analyser. + * + * @return ITypeSystem + */ + 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. + * + * @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..3140d8c --- /dev/null +++ b/Source/Analysis/PhpTypeSystem.php @@ -0,0 +1,564 @@ + + */ +class PhpTypeSystem extends TypeSystem +{ + const TYPE_SELF = '~~SELF_TYPE~~'; + + /** + * @var ITypeDataModule[] + */ + protected $typeDataModules = []; + + /** + * @var string[] + */ + protected $functionTypeMap = []; + + /** + * @var array[] + */ + protected $classTypeMap = []; + + /** + * @param ITypeDataModule[] $customTypeDataModules + */ + public function __construct(array $customTypeDataModules = []) + { + parent::__construct(); + + $typeDataModules = array_merge($this->typeDataModules(), $customTypeDataModules); + /** @var $typeDataModules ITypeDataModule[] */ + foreach($typeDataModules as $module) { + $this->registerTypeDataModule($module); + } + } + + /** + * @return ITypeDataModule[] + */ + protected function typeDataModules() + { + return [ + new TypeData\InternalFunctions(), + new TypeData\InternalTypes(), + new TypeData\DateTime(), + new TypeData\PinqAPI(), + ]; + } + + /** + * 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. + 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->mathOperators(Operators\Binary::POWER), + $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/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/InternalFunctions.php b/Source/Analysis/TypeData/InternalFunctions.php new file mode 100644 index 0000000..2143eaf --- /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..de72bc3 --- /dev/null +++ b/Source/Analysis/TypeData/InternalTypes.php @@ -0,0 +1,54 @@ + + */ +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, + ] + ], + 'Countable' => [ + 'methods' => [ + 'count' => INativeType::TYPE_INT, + ] + ], + '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..0f5c455 --- /dev/null +++ b/Source/Analysis/TypeData/PinqAPI.php @@ -0,0 +1,175 @@ + + */ +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) { + $pinqTypes += $this->generatePinqTypeData( + $traversableInterface, + $traversableGroup['ordered'], + $traversableGroup['joining-on'], + $traversableGroup['joining-to'], + !empty($traversableGroup['mutable']) + ); + } + + return $pinqTypes; + } + + /** + * @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); + + $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, + ]; + + 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, + ]; + } + + $pinqTypes[$traversableType] = [ + 'methods' => $commonMethods + ]; + + $pinqTypes[$orderedTraversableType] = [ + 'methods' => [ + 'thenBy' => $orderedTraversableTypeId, + 'thenByAscending' => $orderedTraversableTypeId, + 'thenByDescending' => $orderedTraversableTypeId, + ] + $commonMethods + ]; + + $joiningMethods = [ + 'withDefault' => $joiningToTraversableTypeId, + 'to' => $traversableTypeId, + ]; + + 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 diff --git a/Source/Analysis/TypeData/TypeDataModule.php b/Source/Analysis/TypeData/TypeDataModule.php new file mode 100644 index 0000000..585338f --- /dev/null +++ b/Source/Analysis/TypeData/TypeDataModule.php @@ -0,0 +1,46 @@ + + */ +class TypeDataModule implements ITypeDataModule +{ + const TYPE_SELF = PhpTypeSystem::TYPE_SELF; + + /** + * @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() + { + return get_called_class(); + } + + public function types() + { + return $this->types; + } + + public function functions() + { + return $this->functions; + } +} \ 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..0c69780 --- /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 getClassTypeFromId($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 getComposedTypeIdsFromId($compositeId) + { + return explode('|', substr($compositeId, 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..ca4f49f --- /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::getClassTypeFromId($typeIdentifier)); + } elseif (TypeId::isComposite($typeIdentifier)) { + return $this->getCompositeType( + array_map([$this, __FUNCTION__], TypeId::getComposedTypeIdsFromId($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..9dc7006 --- /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, array $formatValues = []) + { + return new TypeException( + 'Type does not support expression \'%s\': %s', + $expression->compileDebug(), + vsprintf($message, $formatValues)); + } + + 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 %s is not supported', [$expression->getName()->compileDebug()]); + } + + public function getStaticMethod(O\StaticMethodCallExpression $expression) + { + throw $this->unsupported($expression, 'static method %s is not supported', [$expression->getName()->compileDebug()]); + } + + public function getField(O\FieldExpression $expression) + { + throw $this->unsupported($expression, 'field %s is not supported', [$expression->getName()->compileDebug()]); + } + + public function getStaticField(O\StaticFieldExpression $expression) + { + throw $this->unsupported($expression, 'static field %s is not supported', [$expression->getName()->compileDebug()]); + } + + 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 \'%s\' is not supported', [$expression->getCastType()]); + } + + public function getUnaryOperation(O\UnaryOperationExpression $expression) + { + throw $this->unsupported($expression, 'unary operator \'%s\' is not supported', [$expression->getOperator()]); + } +} \ 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..ba0f97e --- /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, $mustBeStatic) + { + if ($nameExpression instanceof O\ValueExpression) { + $methodName = $nameExpression->getValue(); + foreach ($this->methods as $otherMethodName => $method) { + if ((!$mustBeStatic || $method->getReflection()->isStatic() === true) + && 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..82b845a --- /dev/null +++ b/Tests/Integration/Analysis/BasicExpressionAnalysisTest.php @@ -0,0 +1,546 @@ + + */ +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 testInvalidVariable() + { + $this->assertAnalysisFails(function ($foo) { $bar; }); + } + + 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 testStaticMethodCallOnInstance() + { + $this->assertReturnsObjectType(function (\DateTime $instance) { $instance->createFromFormat(); }, 'DateTime'); + } + + 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; }); + //PHP does not allow instance access of static fields unlike methods + $this->assertAnalysisFails(function (self $instance) { $instance->field; }); + } + + 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]; }, + 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 => [ + 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 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; }); + } + + 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'), + ] + ) + ); + } + + public function testVariableTableFromEvaluationContextFromScopedVariables() + { + foreach ([1, null, true, 3.4, 'abc', new DateTime()] as $value) { + $this->assertReturnsType( + function () use ($value) { $value; }, + $this->typeSystem->getTypeFromValue($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/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..a3f3590 --- /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 = $this->expressionAnalyser->createAnalysisContext($reflection->asEvaluationContext()); + 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..d00ecf7 --- /dev/null +++ b/Tests/Integration/Analysis/TypeAnalysisTest.php @@ -0,0 +1,377 @@ + + */ +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()); + } + ); + } + } + + 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 + */ + 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..e0104b7 --- /dev/null +++ b/Tests/Integration/Analysis/TypeSystemTest.php @@ -0,0 +1,242 @@ + + */ +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')); + } + } + + 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()); + } + + 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 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/