├── .gitignore ├── .idea └── codeStyleSettings.xml ├── bin └── phptc ├── composer.json ├── composer.lock ├── phpdoc.xml └── src ├── JesseSchalken ├── PhpTypeChecker.php └── PhpTypeChecker │ ├── Call.php │ ├── Constants.php │ ├── Context.php │ ├── ControlStructure.php │ ├── Defns.php │ ├── Expr.php │ ├── Function_.php │ ├── LValue.php │ ├── Parser.php │ ├── Stmt.php │ ├── Test.php │ ├── Type.php │ └── functions.php └── autoload.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /composer.phar 3 | /vendor -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 45 | 47 | -------------------------------------------------------------------------------- /bin/phptc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | loc()->format($message) . "\n"; 65 | } 66 | }); 67 | if ($output) { 68 | foreach ($files as $file) { 69 | $path = $file->path(); 70 | $pos = strlen($input); 71 | 72 | if (substr($path, 0, $pos) === $input) { 73 | $path = $output . substr($path, $pos); 74 | } else { 75 | continue; 76 | } 77 | 78 | $dir = dirname($path); 79 | if (!is_dir($dir)) { 80 | mkdir($dir, 0777, true); 81 | } 82 | file_put_contents($path, $file->unparse()); 83 | } 84 | } 85 | } 86 | 87 | ini_set('memory_limit', '1000M'); 88 | ini_set('xdebug.max_nesting_level', '10000'); 89 | 90 | main(); 91 | 92 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "nikic/php-parser": "^2.0.0", 4 | "phpunit/phpunit": "^4.7", 5 | "zendframework/zendframework": "2.5.2", 6 | "jesseschalken/magic-utils": "^0.2.0", 7 | "docopt/docopt": "1.0.2", 8 | "phpdocumentor/type-resolver": "*", 9 | "phpdocumentor/reflection-docblock": "*", 10 | "jesseschalken/enum": "v1.0", 11 | "jesseschalken/set": "v1.0.2" 12 | }, 13 | "autoload": { 14 | "files": ["src/autoload.php"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /phpdoc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | doc 5 | 6 | 7 | doc 8 | 9 | 10 | src 11 | 12 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker.php: -------------------------------------------------------------------------------- 1 | path = $path; 29 | $this->line = $line; 30 | $this->column = $column; 31 | } 32 | 33 | public function format(string $message):string { 34 | return "$this->path($this->line,$this->column): $message"; 35 | } 36 | 37 | public function toDocBlockLocation() { 38 | return new \phpDocumentor\Reflection\DocBlock\Location($this->line, $this->column); 39 | } 40 | 41 | public function loc():CodeLoc { 42 | return $this; 43 | } 44 | } 45 | 46 | /** 47 | * Base class for all things which can be tracked to some position in a source file. 48 | */ 49 | abstract class Node implements HasCodeLoc { 50 | /** @var HasCodeLoc */ 51 | private $loc; 52 | 53 | public function __construct(HasCodeLoc $loc) { 54 | $this->loc = $loc->loc(); 55 | } 56 | 57 | public final function loc():CodeLoc { 58 | return $this->loc; 59 | } 60 | } 61 | 62 | /** 63 | * Something to throw errors at in the process of parsing and type checking. 64 | */ 65 | abstract class ErrorReceiver { 66 | /** 67 | * @param string $message 68 | * @param HasCodeLoc $loc 69 | */ 70 | public abstract function add(string $message, HasCodeLoc $loc); 71 | 72 | /** 73 | * Returns an error receiver which reports errors against the given location instead of the one given when 74 | * add() is called. 75 | * @param HasCodeLoc $loc 76 | * @return self 77 | */ 78 | public final function bind(HasCodeLoc $loc):self { 79 | return new class($this, $loc) extends ErrorReceiver { 80 | /** @var ErrorReceiver */ 81 | private $self; 82 | /** @var HasCodeLoc */ 83 | private $loc; 84 | 85 | public function __construct(ErrorReceiver $self, HasCodeLoc $loc) { 86 | $this->self = $self; 87 | $this->loc = $loc; 88 | } 89 | 90 | public function add(string $message, HasCodeLoc $loc) { 91 | $this->self->add($message, $this->loc); 92 | } 93 | }; 94 | } 95 | } 96 | 97 | class NullErrorReceiver extends ErrorReceiver { 98 | public function add(string $message, HasCodeLoc $loc) { 99 | } 100 | } 101 | 102 | class File extends Node { 103 | /** 104 | * @param string[] $files 105 | * @param ErrorReceiver $errors 106 | * @return File[] 107 | */ 108 | public static function parse(array $files, ErrorReceiver $errors):array { 109 | /** 110 | * @var Parser\ParsedFile[] $parsed 111 | * @var self[] $result 112 | */ 113 | $parsed = []; 114 | $defined = new Parser\GlobalDefinedNames; 115 | $result = []; 116 | $context = new Context\Context($errors); 117 | foreach ($files as $path => $contents) { 118 | $file = new Parser\ParsedFile($path, $contents, $errors); 119 | $defined->addNodes($file->nodes); 120 | $parsed[] = $file; 121 | } 122 | foreach ($parsed as $file) { 123 | $self = new self($file->nullLoc()); 124 | $self->path = $file->path; 125 | $self->shebang = $file->shebang; 126 | $self->contents = (new Parser\Parser($file, $defined, clone $context))->parseStmts($self, $file->nodes); 127 | $result[] = $self; 128 | } 129 | return $result; 130 | } 131 | 132 | /** @var string */ 133 | private $path; 134 | /** @var string */ 135 | private $shebang = ''; 136 | /** @var Stmt\Block */ 137 | private $contents; 138 | 139 | public function path():string { 140 | return $this->path; 141 | } 142 | 143 | public function unparse():string { 144 | /** @var \PhpParser\PrettyPrinter\Standard $prettyPrinter */ 145 | $prettyPrinter = new class() extends \PhpParser\PrettyPrinter\Standard { 146 | public function pStmt_Interface(\PhpParser\Node\Stmt\Interface_ $node) { 147 | return 'interface ' . $node->name 148 | . (!empty($node->extends) ? ' extends ' . $this->pCommaSeparated($node->extends) : '') 149 | . ' {' . $this->pStmts($node->stmts) . "\n" . '}'; 150 | } 151 | 152 | public function pStmt_Function(\PhpParser\Node\Stmt\Function_ $node) { 153 | return 'function ' . ($node->byRef ? '&' : '') . $node->name 154 | . '(' . $this->pCommaSeparated($node->params) . ')' 155 | . (null !== $node->returnType ? ' : ' . $this->pType($node->returnType) : '') 156 | . ' {' . $this->pStmts($node->stmts) . "\n" . '}'; 157 | } 158 | 159 | public function pStmt_Trait(\PhpParser\Node\Stmt\Trait_ $node) { 160 | return 'trait ' . $node->name 161 | . ' {' . $this->pStmts($node->stmts) . "\n" . '}'; 162 | } 163 | 164 | protected function pClassCommon(\PhpParser\Node\Stmt\Class_ $node, $afterClassToken) { 165 | return $this->pModifiers($node->type) 166 | . 'class' . $afterClassToken 167 | . (null !== $node->extends ? ' extends ' . $this->p($node->extends) : '') 168 | . (!empty($node->implements) ? ' implements ' . $this->pCommaSeparated($node->implements) : '') 169 | . ' {' . $this->pStmts($node->stmts) . "\n" . '}'; 170 | } 171 | 172 | public function pStmt_ClassMethod(\PhpParser\Node\Stmt\ClassMethod $node) { 173 | return $this->pModifiers($node->type) 174 | . 'function ' . ($node->byRef ? '&' : '') . $node->name 175 | . '(' . $this->pCommaSeparated($node->params) . ')' 176 | . (null !== $node->returnType ? ' : ' . $this->pType($node->returnType) : '') 177 | . (null !== $node->stmts 178 | ? ' {' . $this->pStmts($node->stmts) . "\n" . '}' 179 | : ';'); 180 | } 181 | 182 | public function pExpr_Array(\PhpParser\Node\Expr\Array_ $node) { 183 | $items = $node->items ? "\n" . $this->pImplode($node->items, ",\n") . ',' : ''; 184 | $items = preg_replace('~\n(?!$|' . $this->noIndentToken . ')~', "\n ", $items); 185 | 186 | if ($this->options['shortArraySyntax']) { 187 | return $items ? "[$items\n]" : "[]"; 188 | } else { 189 | return $items ? "array($items\n)" : "array()"; 190 | } 191 | } 192 | }; 193 | $parserNodes = $this->contents->unparseWithNamespaces(); 194 | return $this->shebang . $prettyPrinter->prettyPrintFile($parserNodes); 195 | } 196 | 197 | /** 198 | * @param Context\Context $context 199 | * @return void 200 | */ 201 | public function gatherGlobalDecls(Context\Context $context) { 202 | $this->contents->gatherGlobalDecls($context); 203 | } 204 | 205 | /** 206 | * @param Context\Context $context 207 | */ 208 | public function typeCheck(Context\Context $context) { 209 | $context = $context->withoutLocals($this); 210 | $this->contents->gatherLocalDecls($context); 211 | $this->contents->gatherInferedLocals($context); 212 | $this->contents->checkStmt($context); 213 | } 214 | } 215 | 216 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Call.php: -------------------------------------------------------------------------------- 1 | args = $args; 24 | } 25 | 26 | /** @return CallArg[] */ 27 | public function args():array { 28 | return $this->args; 29 | } 30 | 31 | /** 32 | * @param Context\Context $context 33 | * @param bool $noErrors 34 | * @return EvaledCallArg[] 35 | */ 36 | public function evalArgs(Context\Context $context, bool $noErrors):array { 37 | $args = []; 38 | foreach ($this->args as $arg) { 39 | $args[] = $arg->checkExpr($context, $noErrors); 40 | } 41 | return $args; 42 | } 43 | 44 | public function subStmts(bool $deep):array { 45 | $stmts = []; 46 | foreach ($this->args as $arg) { 47 | $stmts[] = $arg->expr(); 48 | } 49 | return $stmts; 50 | } 51 | 52 | protected function unparseArgs():array { 53 | $args = []; 54 | foreach ($this->args as $arg) { 55 | $args[] = $arg->unparse(); 56 | } 57 | return $args; 58 | } 59 | } 60 | 61 | class FunctionCall extends Call { 62 | /** @var Expr */ 63 | private $function; 64 | 65 | /** 66 | * @param HasCodeLoc $loc 67 | * @param Expr $function 68 | * @param CallArg[] $args 69 | */ 70 | public function __construct(HasCodeLoc $loc, Expr $function, array $args) { 71 | parent::__construct($loc, $args); 72 | $this->function = $function; 73 | } 74 | 75 | public function subStmts(bool $deep):array { 76 | $stmts = parent::subStmts($deep); 77 | $stmts[] = $this->function; 78 | return $stmts; 79 | } 80 | 81 | public function unparseExpr():\PhpParser\Node\Expr { 82 | return new \PhpParser\Node\Expr\FuncCall( 83 | $this->function->unparseExprOrName(), 84 | $this->unparseArgs() 85 | ); 86 | } 87 | 88 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 89 | return $this->function->checkExpr($context, $noErrors)->call( 90 | $this, 91 | $context, 92 | $this->evalArgs($context, $noErrors), 93 | $noErrors 94 | ); 95 | } 96 | } 97 | 98 | class StaticMethodCall extends Call { 99 | /** @var Expr */ 100 | private $class; 101 | /** @var Expr */ 102 | private $method; 103 | 104 | /** 105 | * @param HasCodeLoc $loc 106 | * @param CallArg[] $args 107 | * @param Expr $class 108 | * @param Expr $method 109 | */ 110 | public function __construct(HasCodeLoc $loc, array $args, Expr $class, Expr $method) { 111 | parent::__construct($loc, $args); 112 | $this->class = $class; 113 | $this->method = $method; 114 | } 115 | 116 | public function subStmts(bool $deep):array { 117 | $stmts = parent::subStmts($deep); 118 | $stmts[] = $this->class; 119 | $stmts[] = $this->method; 120 | return $stmts; 121 | } 122 | 123 | public function unparseExpr():\PhpParser\Node\Expr { 124 | return new \PhpParser\Node\Expr\StaticCall( 125 | $this->class->unparseExprOrName(), 126 | $this->method->unparseExprOrString(), 127 | $this->unparseArgs() 128 | ); 129 | } 130 | 131 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 132 | // TODO: Implement getType() method. 133 | } 134 | } 135 | 136 | class MethodCall extends Call { 137 | /** @var Expr */ 138 | private $object; 139 | /** @var Expr */ 140 | private $method; 141 | 142 | /** 143 | * @param HasCodeLoc $loc 144 | * @param CallArg[] $args 145 | * @param Expr $object 146 | * @param Expr $method 147 | */ 148 | public function __construct(HasCodeLoc $loc, array $args, Expr $object, Expr $method) { 149 | parent::__construct($loc, $args); 150 | $this->object = $object; 151 | $this->method = $method; 152 | } 153 | 154 | public function subStmts(bool $deep):array { 155 | $stmts = parent::subStmts($deep); 156 | $stmts[] = $this->object; 157 | $stmts[] = $this->method; 158 | return $stmts; 159 | } 160 | 161 | public function unparseExpr():\PhpParser\Node\Expr { 162 | return new \PhpParser\Node\Expr\MethodCall( 163 | $this->object->unparseExpr(), 164 | $this->method->unparseExprOrString(), 165 | $this->unparseArgs() 166 | ); 167 | } 168 | 169 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 170 | // TODO: Implement getType() method. 171 | } 172 | } 173 | 174 | class CallArg extends Node { 175 | /** @var Expr */ 176 | private $expr; 177 | /** @var bool */ 178 | private $splat = false; 179 | 180 | public function __construct(HasCodeLoc $loc, Expr $expr, bool $splat) { 181 | parent::__construct($loc); 182 | $this->expr = $expr; 183 | $this->splat = $splat; 184 | } 185 | 186 | public function checkExpr(Context\Context $context, bool $noErrors):EvaledCallArg { 187 | $evaled = new EvaledCallArg($this); 188 | 189 | $evaled->type = $this->expr->checkExpr($context, $noErrors); 190 | $evaled->referrable = $this->expr->isReferrable(); 191 | $evaled->splat = $this->splat; 192 | 193 | // Unpack the value if splatted 194 | if ($this->splat) { 195 | $evaled->type = $evaled->type->doForeach($this, $context)->val; 196 | } 197 | 198 | return $evaled; 199 | } 200 | 201 | public function expr():Expr { 202 | return $this->expr; 203 | } 204 | 205 | public function unparse():\PhpParser\Node\Arg { 206 | return new \PhpParser\Node\Arg( 207 | $this->expr->unparseExpr(), 208 | false, 209 | $this->splat 210 | ); 211 | } 212 | } 213 | 214 | class EvaledCallArg extends Node { 215 | /** @var Type\Type The unpacked type, if $splat = true */ 216 | public $type; 217 | /** @var bool */ 218 | public $referrable; 219 | /** @var bool */ 220 | public $splat; 221 | } 222 | 223 | class New_ extends Call { 224 | /** @var Expr */ 225 | private $class; 226 | 227 | /** 228 | * @param HasCodeLoc $loc 229 | * @param Expr $class 230 | * @param CallArg[] $args 231 | */ 232 | public function __construct(HasCodeLoc $loc, Expr $class, array $args) { 233 | parent::__construct($loc, $args); 234 | $this->class = $class; 235 | } 236 | 237 | public function subStmts(bool $deep):array { 238 | $stmts = parent::subStmts($deep); 239 | $stmts[] = $this->class; 240 | return $stmts; 241 | } 242 | 243 | public function unparseExpr():\PhpParser\Node\Expr { 244 | return new \PhpParser\Node\Expr\New_( 245 | $this->class->unparseExprOrName(), 246 | $this->unparseArgs() 247 | ); 248 | } 249 | 250 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 251 | // TODO: Implement getType() method. 252 | } 253 | } 254 | 255 | class AnonymousNew extends Call { 256 | /** @var Class_ */ 257 | private $class; 258 | 259 | /** 260 | * @param HasCodeLoc $loc 261 | * @param Class_ $class 262 | * @param CallArg[] $args 263 | */ 264 | public function __construct(HasCodeLoc $loc, Class_ $class, array $args) { 265 | parent::__construct($loc, $args); 266 | $this->class = $class; 267 | } 268 | 269 | public function subStmts(bool $deep):array { 270 | $stmts = parent::subStmts($deep); 271 | $stmts[] = $this->class; 272 | return $stmts; 273 | } 274 | 275 | public function unparseExpr():\PhpParser\Node\Expr { 276 | return new \PhpParser\Node\Expr\New_( 277 | $this->class->unparseStmt(), 278 | $this->unparseArgs() 279 | ); 280 | } 281 | 282 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 283 | // TODO: Implement getType() method. 284 | } 285 | } 286 | 287 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Constants.php: -------------------------------------------------------------------------------- 1 | name = $name; 17 | } 18 | 19 | public function subStmts(bool $deep):array { 20 | return []; 21 | } 22 | 23 | public function unparseExpr():\PhpParser\Node\Expr { 24 | return new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name\FullyQualified($this->name)); 25 | } 26 | 27 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 28 | $expr = $context->getConstant($this->name); 29 | if (!$expr) { 30 | return $context->addError("Undefined constant '$this->name'", $this); 31 | } else { 32 | return $expr->checkExpr($context, $noErrors); 33 | } 34 | } 35 | } 36 | 37 | class GetClassConstant extends Expr\Expr { 38 | /** @var Expr\Expr */ 39 | private $class; 40 | /** @var string */ 41 | private $const; 42 | 43 | public function __construct(HasCodeLoc $loc, Expr\Expr $class, string $const) { 44 | parent::__construct($loc); 45 | $this->class = $class; 46 | $this->const = $const; 47 | } 48 | 49 | public function subStmts(bool $deep):array { 50 | return [$this->class]; 51 | } 52 | 53 | public function unparseExpr():\PhpParser\Node\Expr { 54 | return new \PhpParser\Node\Expr\ClassConstFetch( 55 | $this->class->unparseExprOrName(), 56 | $this->const 57 | ); 58 | } 59 | 60 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 61 | // TODO 62 | } 63 | } 64 | 65 | final class MagicConst extends \JesseSchalken\Enum\StringEnum { 66 | const LINE = '__LINE__'; 67 | const FILE = '__FILE__'; 68 | const DIR = '__DIR__'; 69 | const FUNCTION = '__FUNCTION__'; 70 | const CLASS_ = '__CLASS__'; 71 | const TRAIT = '__TRAIT__'; 72 | const METHOD = '__METHOD__'; 73 | const NAMESPACE = '__NAMESPACE__'; 74 | 75 | public static function values() { 76 | return [ 77 | self::LINE, 78 | self::FILE, 79 | self::DIR, 80 | self::FUNCTION, 81 | self::CLASS_, 82 | self::TRAIT, 83 | self::METHOD, 84 | self::NAMESPACE, 85 | ]; 86 | } 87 | } 88 | 89 | class GetMagicConst extends Expr\Expr { 90 | /** @var string */ 91 | private $type; 92 | /** @var int|string */ 93 | private $value; 94 | 95 | /** 96 | * @param HasCodeLoc $loc 97 | * @param string $type 98 | * @param int|string $value 99 | */ 100 | public function __construct(HasCodeLoc $loc, string $type, $value) { 101 | parent::__construct($loc); 102 | $this->type = $type; 103 | $this->value = $value; 104 | } 105 | 106 | public function subStmts(bool $deep):array { 107 | return []; 108 | } 109 | 110 | public function unparseExpr():\PhpParser\Node\Expr { 111 | switch ($this->type) { 112 | case MagicConst::LINE: 113 | return new \PhpParser\Node\Scalar\MagicConst\Line(); 114 | case MagicConst::FILE: 115 | return new \PhpParser\Node\Scalar\MagicConst\File(); 116 | case MagicConst::DIR: 117 | return new \PhpParser\Node\Scalar\MagicConst\Dir(); 118 | case MagicConst::FUNCTION: 119 | return new \PhpParser\Node\Scalar\MagicConst\Function_(); 120 | case MagicConst::CLASS_: 121 | return new \PhpParser\Node\Scalar\MagicConst\Class_(); 122 | case MagicConst::TRAIT: 123 | return new \PhpParser\Node\Scalar\MagicConst\Trait_(); 124 | case MagicConst::METHOD: 125 | return new \PhpParser\Node\Scalar\MagicConst\Method(); 126 | case MagicConst::NAMESPACE: 127 | return new \PhpParser\Node\Scalar\MagicConst\Namespace_(); 128 | default: 129 | throw new \Exception('Invlaid magic constant type: ' . $this->type); 130 | } 131 | } 132 | 133 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 134 | return new Type\SingleValue($this, $this->value); 135 | } 136 | } 137 | 138 | class Literal extends Expr\Expr { 139 | private static function literalToNode($value):\PhpParser\Node\Expr { 140 | if (is_string($value)) { 141 | return new \PhpParser\Node\Scalar\String_($value); 142 | } elseif (is_bool($value)) { 143 | $constant = $value ? 'true' : 'false'; 144 | return new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name\FullyQualified($constant)); 145 | } elseif (is_float($value)) { 146 | return new \PhpParser\Node\Scalar\DNumber($value); 147 | } elseif (is_int($value)) { 148 | return new \PhpParser\Node\Scalar\LNumber($value); 149 | } elseif (is_null($value)) { 150 | return new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name\FullyQualified('null')); 151 | } elseif (is_array($value)) { 152 | $items = []; 153 | foreach ($value as $k => $v) { 154 | $items[] = new \PhpParser\Node\Expr\ArrayItem( 155 | self::literalToNode($v), 156 | self::literalToNode($k), 157 | false 158 | ); 159 | } 160 | return new \PhpParser\Node\Expr\Array_($items); 161 | } else { 162 | throw new \Exception('Invalid literal type: ' . gettype($value)); 163 | } 164 | } 165 | 166 | /** @var bool|float|int|null|string */ 167 | private $value; 168 | 169 | /** 170 | * @param HasCodeLoc $loc 171 | * @param string|int|float|bool|null $value 172 | */ 173 | public function __construct(HasCodeLoc $loc, $value) { 174 | parent::__construct($loc); 175 | $this->value = $value; 176 | } 177 | 178 | public function subStmts(bool $deep):array { 179 | return []; 180 | } 181 | 182 | public function unparseExpr():\PhpParser\Node\Expr { 183 | return self::literalToNode($this->value); 184 | } 185 | 186 | public function unparseExprOrString() { 187 | $value = $this->value; 188 | if (is_string($value)) { 189 | return $value; 190 | } else { 191 | return parent::unparseExprOrString(); 192 | } 193 | } 194 | 195 | public function unparseExprOrName() { 196 | if (is_string($this->value)) { 197 | return new \PhpParser\Node\Name\FullyQualified($this->value); 198 | } else { 199 | return parent::unparseExprOrName(); 200 | } 201 | } 202 | 203 | public function value() { 204 | return $this->value; 205 | } 206 | 207 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 208 | return new Type\SingleValue($this, $this->value); 209 | } 210 | } 211 | 212 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Context.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 40 | $this->return = new Type\Mixed(new CodeLoc('', 1, 1)); 41 | } 42 | 43 | public function setReturn(Type\Type $type) { 44 | $this->return = $type; 45 | } 46 | 47 | public function setReturnRef(bool $ref) { 48 | $this->returnRef = $ref; 49 | } 50 | 51 | public function addGlobal(string $name, Type\Type $type) { 52 | $this->globals[$name] = $type; 53 | } 54 | 55 | public function addFunction(string $name, Function_\Function_ $function) { 56 | $this->functions[strtolower($name)] = $function; 57 | } 58 | 59 | public function addConstant(string $name, Expr\Expr $expr) { 60 | $this->constants[normalize_constant($name)] = $expr; 61 | } 62 | 63 | public function hasLocal(string $name):bool { 64 | return isset($this->locals[$name]); 65 | } 66 | 67 | public function hasGlobal(string $name):bool { 68 | return isset($this->globals[$name]); 69 | } 70 | 71 | public function hasFunction(string $name):bool { 72 | return isset($this->functions[strtolower($name)]); 73 | } 74 | 75 | public function hasConstant(string $name):bool { 76 | return isset($this->constants[normalize_constant($name)]); 77 | } 78 | 79 | public function isCompatible(string $sub, string $sup):bool { 80 | if (str_ieq($sub, $sup)) { 81 | return true; 82 | } 83 | foreach ($this->getClassParents($sub) as $parent) { 84 | if ($this->isCompatible($parent, $sup)) { 85 | return true; 86 | } 87 | } 88 | return false; 89 | } 90 | 91 | public function getClass(string $name) { 92 | return $this->classes[strtolower($name)] ?? null; 93 | } 94 | 95 | /** 96 | * @param string $name 97 | * @return Expr\Expr|null 98 | */ 99 | public function getConstant(string $name) { 100 | return $this->constants[$name] ?? null; 101 | } 102 | 103 | public function getClassParents(string $name):array { 104 | $class = $this->getClass($name); 105 | return $class ? $class->parents : []; 106 | } 107 | 108 | /** 109 | * @param string $name 110 | * @return Function_\Function_|null 111 | */ 112 | public function getFunction(string $name) { 113 | return $this->functions[strtolower($name)] ?? null; 114 | } 115 | 116 | public function functionExists(string $name):bool { 117 | return !!$this->getFunction($name); 118 | } 119 | 120 | public function methodExists(string $class, string $method, bool $staticOnly):bool { 121 | if ($class_ = $this->getClass($class)) { 122 | if ($method = $class_->methods->get($method)) { 123 | return $staticOnly && $method->static ? false : true; 124 | } 125 | } 126 | return false; 127 | } 128 | 129 | public function getGlobal(string $value) { 130 | return $this->globals[$value] ?? null; 131 | } 132 | 133 | /** 134 | * @param HasCodeLoc $loc 135 | * @param string $name 136 | * @param Call\EvaledCallArg[] $args 137 | * @param bool $noErrors 138 | * @return Type\Type 139 | */ 140 | public function callFunction(HasCodeLoc $loc, string $name, array $args, bool $noErrors):Type\Type { 141 | $function = $this->getFunction($name); 142 | if ($function) { 143 | return $function->call($loc, $this, $args, $noErrors); 144 | } else { 145 | return $this->addError("Undefined function '$name'", $loc); 146 | } 147 | } 148 | 149 | public function withoutLocals(HasCodeLoc $loc):self { 150 | $clone = clone $this; 151 | $clone->labels = []; 152 | $clone->locals = []; 153 | $clone->return = new Type\Mixed($loc); 154 | $clone->returnRef = false; 155 | return $clone; 156 | } 157 | 158 | public function withClass(string $class):self { 159 | $clone = clone $this; 160 | $clone->class = $class; 161 | return $clone; 162 | } 163 | 164 | public function addLocal(string $name, Type\Type $type) { 165 | if (!$type->isEmpty()) { 166 | $this->locals[$name] = $type; 167 | } 168 | } 169 | 170 | public function addLabel(string $name) { 171 | $this->labels[$name] = true; 172 | } 173 | 174 | public function hasLabel(string $name):bool { 175 | return $this->labels[$name] ?? false; 176 | } 177 | 178 | public function getLocal(string $name) { 179 | return $this->locals[$name] ?? null; 180 | } 181 | 182 | public function addError(string $message, HasCodeLoc $loc):Type\Type { 183 | $this->errors->add($message, $loc); 184 | return Type\Type::none($loc); 185 | } 186 | 187 | /** 188 | * @return Type\Type[] 189 | */ 190 | public function getLocals():array { 191 | return $this->locals; 192 | } 193 | } 194 | 195 | class ClassDecl { 196 | /** @var ClassConstants */ 197 | public $constants = []; 198 | /** @var ClassMethods */ 199 | public $methods = []; 200 | /** @var ClassProperties */ 201 | public $properties = []; 202 | /** @var bool */ 203 | public $final = false; 204 | /** @var bool */ 205 | public $abstract = false; 206 | /** @var string[] */ 207 | public $parents = []; 208 | 209 | public function __construct() { 210 | $this->constants = new ClassConstants(); 211 | $this->methods = new ClassMethods(); 212 | $this->properties = new ClassProperties(); 213 | } 214 | } 215 | 216 | abstract class ClassMembers { 217 | public final function exists(string $name):bool { 218 | return $this->get($name) !== null; 219 | } 220 | 221 | /** 222 | * @param string $name 223 | * @return ClassMember|object 224 | * @internal param string $class 225 | */ 226 | public abstract function get(string $name); 227 | 228 | public abstract function fromClass(ClassDecl $class):ClassMembers; 229 | 230 | /** 231 | * @param string $self 232 | * @param string $name 233 | * @param Context $dfns 234 | * @return null|string 235 | */ 236 | public final function findDefiningClass(string $self, string $name, Context $dfns) { 237 | if ($this->exists($name)) { 238 | return $self; 239 | } 240 | foreach ($dfns->getClassParents($self) as $parent) { 241 | $dfn = $dfns->getClass($parent); 242 | if (!$dfn) { 243 | continue; 244 | } 245 | $result = $this->fromClass($dfn)->findDefiningClass($parent, $name, $dfns); 246 | if ($result !== null) { 247 | return $result; 248 | } 249 | } 250 | return null; 251 | } 252 | } 253 | 254 | class ClassProperties extends ClassMembers { 255 | private $properties = []; 256 | 257 | public function get(string $name) { 258 | return $this->properties[$name] ?? null; 259 | } 260 | 261 | public function add(string $name, PropertyDecl $dfn) { 262 | $this->properties[$name] = $dfn; 263 | } 264 | 265 | public function fromClass(ClassDecl $class):ClassMembers { 266 | return $class->properties; 267 | } 268 | } 269 | 270 | class ClassMethods extends ClassMembers { 271 | /** @var MethodDecl[] */ 272 | private $methods = []; 273 | 274 | public function get(string $name) { 275 | return $this->methods[strtolower($name)] ?? null; 276 | } 277 | 278 | public function add(string $method, MethodDecl $obj) { 279 | $this->methods[strtolower($method)] = $obj; 280 | } 281 | 282 | public function fromClass(ClassDecl $class):ClassMembers { 283 | return $class->constants; 284 | } 285 | } 286 | 287 | class ClassConstants extends ClassMembers { 288 | /** @var Type\Type */ 289 | private $constants = []; 290 | 291 | public function get(string $name) { 292 | return $this->constants[$name] ?? null; 293 | } 294 | 295 | public function add(string $name, Type\Type $type) { 296 | $this->constants[$name] = $type; 297 | } 298 | 299 | public function fromClass(ClassDecl $class):ClassMembers { 300 | return $class->constants; 301 | } 302 | } 303 | 304 | class ClassMember { 305 | /** @var string */ 306 | public $visibility = 'public'; 307 | /** @var bool */ 308 | public $static = false; 309 | 310 | public function __construct( 311 | string $visibility, 312 | bool $static 313 | ) { 314 | $this->visibility = $visibility; 315 | $this->static = $static; 316 | } 317 | } 318 | 319 | class PropertyDecl extends ClassMember { 320 | /** @var Type\Type */ 321 | public $type; 322 | 323 | public function __construct( 324 | Type\Type $type, 325 | string $visibility, 326 | bool $static 327 | ) { 328 | parent::__construct($visibility, $static); 329 | 330 | $this->type = $type; 331 | } 332 | } 333 | 334 | class MethodDecl extends ClassMember { 335 | /** @var Function_\Function_ */ 336 | public $signature; 337 | /** @var bool */ 338 | public $final = false; 339 | /** @var bool */ 340 | public $abstract = false; 341 | 342 | public function __construct( 343 | Function_\Function_ $signature, 344 | string $visibility, 345 | bool $abstract, 346 | bool $final, 347 | bool $static 348 | ) { 349 | parent::__construct($visibility, $static); 350 | 351 | $this->signature = $signature; 352 | $this->abstract = $abstract; 353 | $this->final = $final; 354 | } 355 | } 356 | 357 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/ControlStructure.php: -------------------------------------------------------------------------------- 1 | body = $body; 35 | $this->cond = $cond; 36 | } 37 | 38 | public function subStmts(bool $deep):array { 39 | return [$this->body, $this->cond]; 40 | } 41 | 42 | public function unparseStmt() { 43 | return new \PhpParser\Node\Stmt\While_( 44 | $this->cond->unparseExpr(), 45 | $this->body->unparseNodes() 46 | ); 47 | } 48 | } 49 | 50 | class If_ extends ControlStructure { 51 | /** @var Expr\Expr */ 52 | private $cond; 53 | /** @var Stmt\Block */ 54 | private $true; 55 | /** @var Stmt\Block */ 56 | private $false; 57 | 58 | public function __construct(HasCodeLoc $loc, Expr\Expr $cond, Stmt\Block $true, Stmt\Block $false) { 59 | parent::__construct($loc); 60 | $this->cond = $cond; 61 | $this->true = $true; 62 | $this->false = $false; 63 | } 64 | 65 | public function subStmts(bool $deep):array { 66 | return [$this->cond, $this->true, $this->false]; 67 | } 68 | 69 | public function unparseStmt() { 70 | $elseIfs = []; 71 | $else = $this->false->unparseNodes(); 72 | 73 | if (count($else) == 1) { 74 | $if_ = $else[0]; 75 | if ($if_ instanceof \PhpParser\Node\Stmt\If_) { 76 | $elseIfs = array_merge( 77 | [new \PhpParser\Node\Stmt\ElseIf_( 78 | $if_->cond, 79 | $if_->stmts 80 | )], 81 | $if_->elseifs 82 | ); 83 | $else = $if_->else ? $if_->else->stmts : []; 84 | } 85 | } 86 | 87 | return new \PhpParser\Node\Stmt\If_( 88 | $this->cond->unparseExpr(), 89 | [ 90 | 'stmts' => $this->true->unparseNodes(), 91 | 'elseifs' => $elseIfs, 92 | 'else' => $else ? new \PhpParser\Node\Stmt\Else_($else) : null, 93 | ] 94 | ); 95 | } 96 | } 97 | 98 | class Foreach_ extends ControlStructure { 99 | /** @var Expr\Expr */ 100 | private $array; 101 | /** @var Expr\Expr|null */ 102 | private $key; 103 | /** @var Expr\Expr */ 104 | private $value; 105 | /** @var Stmt\Block */ 106 | private $body; 107 | /** @var bool */ 108 | private $byRef; 109 | 110 | /** 111 | * @param HasCodeLoc $loc 112 | * @param Expr\Expr $array 113 | * @param Expr\Expr|null $key 114 | * @param Expr\Expr $value 115 | * @param bool $byRef 116 | * @param Stmt\Block $body 117 | */ 118 | public function __construct( 119 | HasCodeLoc $loc, 120 | Expr\Expr $array, 121 | Expr\Expr $key = null, 122 | Expr\Expr $value, 123 | bool $byRef, 124 | Stmt\Block $body 125 | ) { 126 | parent::__construct($loc); 127 | $this->array = $array; 128 | $this->key = $key; 129 | $this->value = $value; 130 | $this->body = $body; 131 | $this->byRef = $byRef; 132 | } 133 | 134 | public function subStmts(bool $deep):array { 135 | $stmts = [ 136 | $this->array, 137 | $this->value, 138 | $this->body, 139 | ]; 140 | if ($this->key) { 141 | $stmts[] = $this->key; 142 | } 143 | return $stmts; 144 | } 145 | 146 | public function unparseStmt() { 147 | return new \PhpParser\Node\Stmt\Foreach_( 148 | $this->array->unparseExpr(), 149 | $this->value->unparseExpr(), 150 | [ 151 | 'keyVar' => $this->key ? $this->key->unparseExpr() : null, 152 | 'byRef' => $this->byRef, 153 | 'stmts' => $this->body->unparseNodes(), 154 | ] 155 | ); 156 | } 157 | } 158 | 159 | /** 160 | * Special statements used for the init/cond/loop parts of a for loop. 161 | * Is like the comma operator in C and JavaScript but can actually only 162 | * be used in the head of a for loop. 163 | */ 164 | class ForComma extends Stmt\Stmt { 165 | /** @var Expr\Expr[] */ 166 | private $exprs = []; 167 | 168 | /** 169 | * @param HasCodeLoc $loc 170 | * @param Expr\Expr[] $exprs 171 | */ 172 | public function __construct(HasCodeLoc $loc, array $exprs) { 173 | parent::__construct($loc); 174 | $this->exprs = $exprs; 175 | } 176 | 177 | public function subStmts(bool $deep):array { 178 | return $this->exprs; 179 | } 180 | 181 | /** @return \PhpParser\Node\Expr[] */ 182 | public function unparseNodes():array { 183 | $nodes = []; 184 | foreach ($this->exprs as $expr) { 185 | $nodes[] = $expr->unparseExpr(); 186 | } 187 | return $nodes; 188 | } 189 | 190 | public function split():array { 191 | return $this->exprs; 192 | } 193 | } 194 | 195 | class For_ extends ControlStructure { 196 | /** @var ForComma */ 197 | private $init; 198 | /** @var ForComma */ 199 | private $cond; 200 | /** @var ForComma */ 201 | private $loop; 202 | /** @var Stmt\Block */ 203 | private $body; 204 | 205 | public function __construct(HasCodeLoc $loc, ForComma $init, ForComma $cond, ForComma $loop, Stmt\Block $body) { 206 | parent::__construct($loc); 207 | $this->init = $init; 208 | $this->cond = $cond; 209 | $this->loop = $loop; 210 | $this->body = $body; 211 | } 212 | 213 | public function subStmts(bool $deep):array { 214 | return [ 215 | $this->init, 216 | $this->cond, 217 | $this->loop, 218 | $this->body, 219 | ]; 220 | } 221 | 222 | public function unparseStmt() { 223 | return new \PhpParser\Node\Stmt\For_( 224 | [ 225 | 'init' => $this->init->unparseNodes(), 226 | 'cond' => $this->cond->unparseNodes(), 227 | 'loop' => $this->loop->unparseNodes(), 228 | 'stmts' => $this->body->unparseNodes(), 229 | ] 230 | ); 231 | } 232 | } 233 | 234 | class Switch_ extends ControlStructure { 235 | /** @var Expr\Expr */ 236 | private $expr; 237 | /** @var SwitchCase[] */ 238 | private $cases; 239 | 240 | /** 241 | * @param HasCodeLoc $loc 242 | * @param Expr\Expr $expr 243 | * @param SwitchCase[] $cases 244 | */ 245 | public function __construct(HasCodeLoc $loc, Expr\Expr $expr, array $cases) { 246 | parent::__construct($loc); 247 | $this->expr = $expr; 248 | $this->cases = $cases; 249 | } 250 | 251 | public function subStmts(bool $deep):array { 252 | $stmts = [$this->expr]; 253 | foreach ($this->cases as $case) { 254 | foreach ($case->subStmts() as $stmt) { 255 | $stmts[] = $stmt; 256 | } 257 | } 258 | return $stmts; 259 | } 260 | 261 | public function unparseStmt() { 262 | $cases = []; 263 | foreach ($this->cases as $case) { 264 | $cases[] = $case->unparse(); 265 | } 266 | return new \PhpParser\Node\Stmt\Switch_( 267 | $this->expr->unparseExpr(), 268 | $cases 269 | ); 270 | } 271 | } 272 | 273 | class SwitchCase extends Node { 274 | /** @var Expr\Expr|null */ 275 | private $expr; 276 | /** @var Stmt\Block */ 277 | private $stmt; 278 | 279 | /** 280 | * @param HasCodeLoc $loc 281 | * @param Expr\Expr|null $expr 282 | * @param Stmt\Block $stmt 283 | */ 284 | public function __construct(HasCodeLoc $loc, Expr\Expr $expr = null, Stmt\Block $stmt) { 285 | parent::__construct($loc); 286 | $this->expr = $expr; 287 | $this->stmt = $stmt; 288 | } 289 | 290 | public function subStmts():array { 291 | $stmts = [$this->stmt]; 292 | if ($this->expr) { 293 | $stmts[] = $this->expr; 294 | } 295 | return $stmts; 296 | } 297 | 298 | public function unparse():\PhpParser\Node\Stmt\Case_ { 299 | return new \PhpParser\Node\Stmt\Case_( 300 | $this->expr ? $this->expr->unparseExpr() : null, 301 | $this->stmt->unparseNodes() 302 | ); 303 | } 304 | } 305 | 306 | class While_ extends ControlStructure { 307 | /** @var Expr\Expr */ 308 | private $cond; 309 | /** @var Stmt\Block */ 310 | private $body; 311 | 312 | /** 313 | * @param HasCodeLoc $loc 314 | * @param Expr\Expr $cond 315 | * @param Stmt\Block $body 316 | */ 317 | public function __construct(HasCodeLoc $loc, Expr\Expr $cond, Stmt\Block $body) { 318 | parent::__construct($loc); 319 | $this->cond = $cond; 320 | $this->body = $body; 321 | } 322 | 323 | public function subStmts(bool $deep):array { 324 | return [$this->cond, $this->body]; 325 | } 326 | 327 | public function unparseStmt() { 328 | return new \PhpParser\Node\Stmt\While_( 329 | $this->cond->unparseExpr(), 330 | $this->body->unparseNodes() 331 | ); 332 | } 333 | } 334 | 335 | class Try_ extends ControlStructure { 336 | /** @var Stmt\Block */ 337 | private $body; 338 | /** @var TryCatch[] */ 339 | private $catches; 340 | /** @var Stmt\Block */ 341 | private $finally; 342 | 343 | /** 344 | * @param HasCodeLoc $loc 345 | * @param Stmt\Block $body 346 | * @param TryCatch[] $catches 347 | * @param Stmt\Block $finally 348 | */ 349 | public function __construct(HasCodeLoc $loc, Stmt\Block $body, array $catches, Stmt\Block $finally) { 350 | parent::__construct($loc); 351 | $this->body = $body; 352 | $this->catches = $catches; 353 | $this->finally = $finally; 354 | } 355 | 356 | public function subStmts(bool $deep):array { 357 | $stmts = [$this->body]; 358 | foreach ($this->catches as $catch) { 359 | foreach ($catch->subStmts() as $stmt) { 360 | $stmts[] = $stmt; 361 | } 362 | } 363 | return $stmts; 364 | } 365 | 366 | public function unparseStmt() { 367 | $cathes = []; 368 | foreach ($this->catches as $catch) { 369 | $cathes[] = $catch->unparse(); 370 | } 371 | return new \PhpParser\Node\Stmt\TryCatch( 372 | $this->body->unparseNodes(), 373 | $cathes, 374 | $this->finally->unparseNodes() ?: null 375 | ); 376 | } 377 | } 378 | 379 | class TryCatch extends Node { 380 | /** @var string */ 381 | private $class; 382 | /** @var string */ 383 | private $variable; 384 | /** @var Stmt\Block */ 385 | private $body; 386 | 387 | /** 388 | * @param HasCodeLoc $loc 389 | * @param string $class 390 | * @param string $variable 391 | * @param Stmt\Block $body 392 | */ 393 | public function __construct(HasCodeLoc $loc, string $class, string $variable, Stmt\Block $body) { 394 | parent::__construct($loc); 395 | $this->class = $class; 396 | $this->variable = $variable; 397 | $this->body = $body; 398 | } 399 | 400 | public function subStmts():array { 401 | return [$this->body]; 402 | } 403 | 404 | public function unparse():\PhpParser\Node\Stmt\Catch_ { 405 | return new \PhpParser\Node\Stmt\Catch_( 406 | new \PhpParser\Node\Name\FullyQualified($this->class), 407 | $this->variable, 408 | $this->body->unparseNodes() 409 | ); 410 | } 411 | } 412 | 413 | 414 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Defns.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->type = $type; 38 | } 39 | 40 | public function name():string { 41 | return $this->name; 42 | } 43 | 44 | public function type():Type\Type { 45 | return $this->type; 46 | } 47 | 48 | public function subStmts(bool $deep):array { 49 | return []; 50 | } 51 | 52 | public function unparseStmt() { 53 | return null; 54 | } 55 | } 56 | 57 | class GlobalVariableType extends VariableType { 58 | public function gatherGlobalDecls(Context\Context $context) { 59 | parent::gatherGlobalDecls($context); 60 | $context->addGlobal($this->name(), $this->type()); 61 | } 62 | } 63 | 64 | class LocalVariableType extends VariableType { 65 | public function gatherLocalDecls(Context\Context $context) { 66 | parent::gatherLocalDecls($context); 67 | $context->addLocal($this->name(), $this->type()); 68 | } 69 | } 70 | 71 | class Label_ extends LocalDefinition { 72 | /** @var string */ 73 | private $name; 74 | 75 | public function __construct(HasCodeLoc $loc, string $name) { 76 | parent::__construct($loc); 77 | $this->name = $name; 78 | } 79 | 80 | public function subStmts(bool $deep):array { 81 | return []; 82 | } 83 | 84 | public function unparseStmt() { 85 | return new \PhpParser\Node\Stmt\Label($this->name); 86 | } 87 | 88 | public function gatherLocalDecls(Context\Context $context) { 89 | parent::gatherLocalDecls($context); 90 | $context->addLabel($this->name); 91 | } 92 | } 93 | 94 | class Const_ extends GlobalDefinition implements HasNamespace { 95 | /** @var string */ 96 | private $name; 97 | /** @var Expr\Expr */ 98 | private $value; 99 | 100 | /** 101 | * @param HasCodeLoc $loc 102 | * @param string $name 103 | * @param Expr\Expr $value 104 | */ 105 | public function __construct(HasCodeLoc $loc, string $name, Expr\Expr $value) { 106 | parent::__construct($loc); 107 | $this->name = $name; 108 | $this->value = $value; 109 | } 110 | 111 | public function name():string { 112 | return $this->name; 113 | } 114 | 115 | public function subStmts(bool $deep):array { 116 | return [$this->value]; 117 | } 118 | 119 | public function unparseStmt() { 120 | return new \PhpParser\Node\Stmt\Const_( 121 | [ 122 | new \PhpParser\Node\Const_( 123 | remove_namespace($this->name), 124 | $this->value->unparseExpr() 125 | ), 126 | ] 127 | ); 128 | } 129 | 130 | public final function namespace_():string { 131 | return extract_namespace($this->name()); 132 | } 133 | 134 | public function gatherGlobalDecls(Context\Context $context) { 135 | parent::gatherGlobalDecls($context); 136 | $context->addConstant($this->name, $this->value); 137 | } 138 | } 139 | 140 | class FunctionDefinition extends GlobalDefinition implements HasNamespace { 141 | /** @var string */ 142 | private $name; 143 | /** @var Stmt\Block */ 144 | private $body; 145 | /** @var Function_\Function_ */ 146 | private $type; 147 | 148 | public function __construct(HasCodeLoc $loc, string $name, Function_\Function_ $type, Stmt\Block $body) { 149 | parent::__construct($loc); 150 | $this->name = $name; 151 | $this->type = $type; 152 | $this->body = $body; 153 | } 154 | 155 | public function name():string { 156 | return $this->name; 157 | } 158 | 159 | public function subStmts(bool $deep):array { 160 | $stmts = $this->type->subStmts(); 161 | if ($deep) { 162 | $stmts[] = $this->body; 163 | } 164 | return $stmts; 165 | } 166 | 167 | public function unparseStmt() { 168 | return new \PhpParser\Node\Stmt\Function_( 169 | remove_namespace($this->name), 170 | array_replace( 171 | $this->type->unparseAttributes(), 172 | [ 173 | 'stmts' => $this->body->unparseNodes(), 174 | ] 175 | ) 176 | ); 177 | } 178 | 179 | public final function namespace_():string { 180 | return extract_namespace($this->name()); 181 | } 182 | 183 | public function gatherGlobalDecls(Context\Context $context) { 184 | parent::gatherGlobalDecls($context); 185 | $context->addFunction($this->name, $this->type); 186 | } 187 | 188 | public function checkStmt(Context\Context $context) { 189 | $context = $context->withoutLocals($this); 190 | $this->type->addLocals($context); 191 | $this->body->gatherLocalDecls($context); 192 | $this->body->gatherInferedLocals($context); 193 | $this->body->checkStmt($context); 194 | } 195 | } 196 | 197 | abstract class Classish extends GlobalDefinition implements HasNamespace { 198 | private $name; 199 | 200 | public function __construct(HasCodeLoc $loc, string $name) { 201 | parent::__construct($loc); 202 | $this->name = $name; 203 | } 204 | 205 | public function name():string { 206 | return $this->name; 207 | } 208 | 209 | public function basename() { 210 | return remove_namespace($this->name); 211 | } 212 | 213 | /** 214 | * @return AbstractClassMember[] 215 | */ 216 | public abstract function members():array; 217 | 218 | public final function subStmts(bool $deep):array { 219 | if ($deep) { 220 | $stmts = []; 221 | foreach ($this->members() as $member) { 222 | foreach ($member->subStmts() as $stmt) { 223 | $stmts[] = $stmt; 224 | } 225 | } 226 | return $stmts; 227 | } else { 228 | return []; 229 | } 230 | } 231 | 232 | /** 233 | * @return \PhpParser\Node[] 234 | */ 235 | public function unparseMembers():array { 236 | $nodes = []; 237 | foreach ($this->members() as $member) { 238 | $nodes[] = $member->unparse(); 239 | } 240 | return $nodes; 241 | } 242 | 243 | public final function namespace_():string { 244 | return extract_namespace($this->name()); 245 | } 246 | } 247 | 248 | class Trait_ extends Classish { 249 | /** @var ClassMember[] */ 250 | private $members = []; 251 | 252 | /** 253 | * @param HasCodeLoc $loc 254 | * @param string $name 255 | * @param ClassMember[] $members 256 | */ 257 | public function __construct(HasCodeLoc $loc, string $name, array $members) { 258 | parent::__construct($loc, $name); 259 | $this->members = $members; 260 | } 261 | 262 | public function members():array { 263 | return $this->members; 264 | } 265 | 266 | public function unparseStmt() { 267 | return new \PhpParser\Node\Stmt\Trait_( 268 | $this->basename(), 269 | $this->unparseMembers() 270 | ); 271 | } 272 | } 273 | 274 | class Class_ extends Classish { 275 | /** @var ClassMember */ 276 | private $members = []; 277 | /** @var string|null */ 278 | private $parent; 279 | /** @var string[] */ 280 | private $implements; 281 | /** @var bool */ 282 | private $abstract; 283 | /** @var bool */ 284 | private $final; 285 | 286 | /** 287 | * @param HasCodeLoc $loc 288 | * @param string $name 289 | * @param ClassMember[] $members 290 | * @param string|null $parent 291 | * @param string[] $implements 292 | * @param bool $abstract 293 | * @param bool $final 294 | */ 295 | public function __construct( 296 | HasCodeLoc $loc, 297 | string $name, 298 | array $members, 299 | string $parent = null, 300 | array $implements = [], 301 | bool $abstract, 302 | bool $final 303 | ) { 304 | parent::__construct($loc, $name); 305 | $this->members = $members; 306 | $this->parent = $parent; 307 | $this->implements = $implements; 308 | $this->abstract = $abstract; 309 | $this->final = $final; 310 | } 311 | 312 | public function members():array { 313 | return $this->members; 314 | } 315 | 316 | public function unparseStmt() { 317 | $type = 0; 318 | if ($this->abstract) { 319 | $type |= \PhpParser\Node\Stmt\Class_::MODIFIER_ABSTRACT; 320 | } 321 | if ($this->final) { 322 | $type |= \PhpParser\Node\Stmt\Class_::MODIFIER_FINAL; 323 | } 324 | $implements = []; 325 | foreach ($this->implements as $interface) { 326 | $implements[] = new \PhpParser\Node\Name\FullyQualified($interface); 327 | } 328 | return new \PhpParser\Node\Stmt\Class_( 329 | $this->basename(), 330 | [ 331 | 'type' => $type, 332 | 'extends' => $this->parent ? new \PhpParser\Node\Name\FullyQualified($this->parent) : null, 333 | 'implements' => $implements, 334 | 'stmts' => $this->unparseMembers(), 335 | ] 336 | ); 337 | } 338 | } 339 | 340 | class Interface_ extends Classish { 341 | /** @var Method_[] */ 342 | private $methods = []; 343 | /** @var string[] */ 344 | private $extends = []; 345 | 346 | /** 347 | * @param HasCodeLoc $loc 348 | * @param string $name 349 | * @param string[] $extends 350 | * @param Method_[] $methods 351 | */ 352 | public function __construct(HasCodeLoc $loc, string $name, array $extends, array $methods) { 353 | parent::__construct($loc, $name); 354 | $this->methods = $methods; 355 | $this->extends = $extends; 356 | } 357 | 358 | public function members():array { 359 | return $this->methods; 360 | } 361 | 362 | public function unparseStmt() { 363 | $extends = []; 364 | foreach ($this->extends as $extend) { 365 | $extends[] = new \PhpParser\Node\Name\FullyQualified($extend); 366 | } 367 | return new \PhpParser\Node\Stmt\Interface_( 368 | $this->basename(), 369 | [ 370 | 'stmts' => $this->unparseMembers(), 371 | 'extends' => $extends, 372 | ] 373 | ); 374 | } 375 | } 376 | 377 | abstract class AbstractClassMember extends Node { 378 | public abstract function unparse():\PhpParser\Node; 379 | 380 | /** 381 | * @return Stmt\Stmt[] 382 | */ 383 | public abstract function subStmts():array; 384 | } 385 | 386 | class ClassConstant extends AbstractClassMember { 387 | /** @var string */ 388 | private $name; 389 | /** @var Expr\Expr */ 390 | private $value; 391 | 392 | public function __construct(HasCodeLoc $loc, string $name, Expr\Expr $value) { 393 | parent::__construct($loc); 394 | $this->name = $name; 395 | $this->value = $value; 396 | } 397 | 398 | public function unparse():\PhpParser\Node { 399 | return new \PhpParser\Node\Stmt\ClassConst([ 400 | new \PhpParser\Node\Const_( 401 | $this->name, 402 | $this->value->unparseExpr() 403 | ), 404 | ]); 405 | } 406 | 407 | public function subStmts():array { 408 | if ($this->value) { 409 | return [$this->value]; 410 | } else { 411 | return []; 412 | } 413 | } 414 | } 415 | 416 | class UseTrait extends AbstractClassMember { 417 | /** @var string[] */ 418 | private $traits = []; 419 | /** @var UseTraitInsteadof[] */ 420 | private $insteadOfs = []; 421 | /** @var UseTraitAlias[] */ 422 | private $aliases = []; 423 | 424 | /** 425 | * @param HasCodeLoc $loc 426 | * @param string[] $traits 427 | */ 428 | public function __construct(HasCodeLoc $loc, array $traits) { 429 | parent::__construct($loc); 430 | $this->traits = $traits; 431 | } 432 | 433 | public function addInsteadOf(UseTraitInsteadof $insteadof) { 434 | $lower = strtolower($insteadof->method()); 435 | if (isset($this->insteadOfs[$lower])) { 436 | throw new \Exception('An *insteadof* already exists for method ' . $insteadof->method()); 437 | } 438 | $this->insteadOfs[$lower] = $insteadof; 439 | } 440 | 441 | public function addAlias(UseTraitAlias $alias) { 442 | $lower = strtolower($alias->alias()); 443 | if (isset($this->aliases[$lower])) { 444 | throw new \Exception('An *as* already exists for method ' . $alias->alias()); 445 | } 446 | $this->aliases[$lower] = $alias; 447 | } 448 | 449 | public function subStmts():array { 450 | return []; 451 | } 452 | 453 | public function unparse():\PhpParser\Node { 454 | $traits = []; 455 | foreach ($this->traits as $trait) { 456 | $traits[] = new \PhpParser\Node\Name\FullyQualified($trait); 457 | } 458 | $adaptions = []; 459 | foreach ($this->insteadOfs as $method => $adaption) { 460 | foreach ($adaption->unparse() as $item) { 461 | $adaptions[] = $item; 462 | } 463 | } 464 | return new \PhpParser\Node\Stmt\TraitUse($traits, $adaptions); 465 | } 466 | } 467 | 468 | class UseTraitInsteadof extends Node { 469 | /** 470 | * The method in question 471 | * @var string 472 | */ 473 | private $method; 474 | /** 475 | * The trait this method should come from 476 | * @var string 477 | */ 478 | private $trait; 479 | /** 480 | * The traits this method should *not* come from :P 481 | * These traits must be used in the class. 482 | * @var string[] 483 | */ 484 | private $insteadOf; 485 | 486 | public function __construct(HasCodeLoc $loc, string $trait, string $method, array $insteadOf) { 487 | parent::__construct($loc); 488 | $this->trait = $trait; 489 | $this->method = $method; 490 | $this->insteadOf = $insteadOf; 491 | } 492 | 493 | public function method():array { 494 | return $this->method; 495 | } 496 | 497 | public function unparse():\PhpParser\Node\Stmt\TraitUseAdaptation\Precedence { 498 | $insteadOf = []; 499 | foreach ($this->insteadOf as $trait) { 500 | $insteadOf[] = new \PhpParser\Node\Name\FullyQualified($trait); 501 | } 502 | return new \PhpParser\Node\Stmt\TraitUseAdaptation\Precedence( 503 | new \PhpParser\Node\Name\FullyQualified($this->trait), 504 | $this->method, 505 | $insteadOf 506 | ); 507 | } 508 | } 509 | 510 | class UseTraitAlias extends Node { 511 | /** 512 | * The name of the alias 513 | * @var string 514 | */ 515 | private $alias; 516 | /** 517 | * The method being aliased 518 | * @var string 519 | */ 520 | private $method; 521 | /** 522 | * The trait the method should come from. If none, use whatever trait implements the method after the 523 | * "insteadof" rules have been applied. 524 | * @var string|null 525 | */ 526 | private $trait; 527 | /** 528 | * Any adjustments to visibility. Default to visibility from the trait. 529 | * @var string|null 530 | */ 531 | private $visibility; 532 | 533 | /** 534 | * @param HasCodeLoc $loc 535 | * @param string $alias 536 | * @param string $method 537 | * @param null|string $trait 538 | * @param null|string $visibility 539 | */ 540 | public function __construct(HasCodeLoc $loc, string $alias, string $method, $trait, $visibility) { 541 | parent::__construct($loc); 542 | $this->alias = $alias; 543 | $this->method = $method; 544 | $this->trait = $trait; 545 | $this->visibility = $visibility; 546 | } 547 | 548 | public function alias():string { 549 | return $this->alias; 550 | } 551 | 552 | public function unparse():\PhpParser\Node\Stmt\TraitUseAdaptation\Alias { 553 | return new \PhpParser\Node\Stmt\TraitUseAdaptation\Alias( 554 | $this->trait ? new \PhpParser\Node\Name\FullyQualified($this->trait) : null, 555 | $this->method, 556 | $this->visibility ? Visibility::unparse($this->visibility) : null, 557 | $this->alias === $this->method ? null : $this->alias 558 | ); 559 | } 560 | } 561 | 562 | abstract class ClassMember extends AbstractClassMember { 563 | /** @var string */ 564 | private $visibility; 565 | /** @var bool */ 566 | private $static; 567 | 568 | /** 569 | * @param HasCodeLoc $loc 570 | * @param string $visibility 571 | * @param bool $static 572 | */ 573 | public function __construct(HasCodeLoc $loc, string $visibility, bool $static) { 574 | parent::__construct($loc); 575 | $this->visibility = $visibility; 576 | $this->static = $static; 577 | } 578 | 579 | public function visibility():string { 580 | return $this->visibility; 581 | } 582 | 583 | public final function modifiers():int { 584 | $type = 0; 585 | switch ($this->visibility) { 586 | case 'public': 587 | $type |= \PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC; 588 | break; 589 | case 'private'; 590 | $type |= \PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE; 591 | break; 592 | case 'protected'; 593 | $type |= \PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED; 594 | break; 595 | } 596 | 597 | if ($this->static) { 598 | $type |= \PhpParser\Node\Stmt\Class_::MODIFIER_STATIC; 599 | } 600 | 601 | return $type; 602 | } 603 | } 604 | 605 | class Property extends ClassMember { 606 | /** @var string */ 607 | private $name; 608 | /** @var Type\Type */ 609 | private $type; 610 | /** @var Expr\Expr|null */ 611 | private $default = null; 612 | 613 | /** 614 | * @param HasCodeLoc $loc 615 | * @param string $name 616 | * @param Type\Type $type 617 | * @param Expr\Expr|null $default 618 | * @param string $visibility 619 | * @param bool $static 620 | */ 621 | public function __construct( 622 | HasCodeLoc $loc, 623 | string $name, 624 | Type\Type $type, 625 | Expr\Expr $default = null, 626 | string $visibility, 627 | bool $static 628 | ) { 629 | parent::__construct($loc, $visibility, $static); 630 | $this->name = $name; 631 | $this->type = $type; 632 | $this->default = $default; 633 | } 634 | 635 | public function subStmts():array { 636 | return $this->default ? [$this->default] : []; 637 | } 638 | 639 | public function unparse():\PhpParser\Node { 640 | return new \PhpParser\Node\Stmt\Property( 641 | $this->modifiers(), 642 | [new \PhpParser\Node\Stmt\PropertyProperty( 643 | $this->name, 644 | $this->default ? $this->default->unparseExpr() : null 645 | )] 646 | ); 647 | } 648 | } 649 | 650 | class Method_ extends ClassMember { 651 | /** @var string */ 652 | private $name; 653 | /** @var Function_\Function_ */ 654 | private $type; 655 | /** @var Stmt\Block|null */ 656 | private $body; 657 | /** @var bool */ 658 | private $final; 659 | 660 | /** 661 | * @param HasCodeLoc $loc 662 | * @param string $name 663 | * @param Function_\Function_ $type 664 | * @param Stmt\Block|null $body 665 | * @param bool $final 666 | * @param string $visibility 667 | * @param bool $static 668 | */ 669 | public function __construct( 670 | HasCodeLoc $loc, 671 | string $name, 672 | Function_\Function_ $type, 673 | Stmt\Block $body = null, 674 | bool $final, 675 | string $visibility, 676 | bool $static 677 | ) { 678 | parent::__construct($loc, $visibility, $static); 679 | $this->final = $final; 680 | $this->name = $name; 681 | $this->type = $type; 682 | $this->body = $body; 683 | } 684 | 685 | public function isAbstract():bool { 686 | return $this->body ? false : true; 687 | } 688 | 689 | public function isFinal():bool { 690 | return $this->final; 691 | } 692 | 693 | public function subStmts():array { 694 | $stmts = $this->type->subStmts(); 695 | if ($this->body) { 696 | $stmts[] = $this->body; 697 | } 698 | return $stmts; 699 | } 700 | 701 | public function unparse():\PhpParser\Node { 702 | $params = $this->type->unparseAttributes(); 703 | $params = array_replace( 704 | $params, [ 705 | 'type' => $this->modifiers(), 706 | 'stmts' => $this->body ? $this->body->unparseNodes() : null, 707 | ] 708 | ); 709 | return new \PhpParser\Node\Stmt\ClassMethod($this->name, $params); 710 | } 711 | } 712 | 713 | class Visibility { 714 | static function unparse(string $visibility):int { 715 | switch ($visibility) { 716 | case self::PUBLIC: 717 | return \PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC; 718 | case self::PROTECTED: 719 | return \PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED; 720 | case self::PRIVATE: 721 | return \PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE; 722 | default: 723 | throw new \Exception('Invalid visibility: ' . $visibility); 724 | } 725 | } 726 | 727 | const PUBLIC = 'public'; 728 | const PROTECTED = 'protected'; 729 | const PRIVATE = 'private'; 730 | } 731 | 732 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Expr.php: -------------------------------------------------------------------------------- 1 | isLValue() || $this->isCall(); 24 | } 25 | 26 | public final function unparseStmt() { 27 | return $this->unparseExpr(); 28 | } 29 | 30 | public abstract function unparseExpr():\PhpParser\Node\Expr; 31 | 32 | /** 33 | * @return \PhpParser\Node\Expr|\PhpParser\Node\Name 34 | */ 35 | public function unparseExprOrName() { 36 | return $this->unparseExpr(); 37 | } 38 | 39 | /** 40 | * @return \PhpParser\Node\Expr|string 41 | */ 42 | public function unparseExprOrString() { 43 | return $this->unparseExpr(); 44 | } 45 | 46 | public final function checkStmt(Context\Context $context) { 47 | $this->checkExpr($context, false); 48 | } 49 | 50 | /** 51 | * @param Context\Context $context 52 | * @param bool $noErrors If true, don't report any errors. Just do the minimal amount of work required 53 | * to get the return type of this expression. 54 | * @return Type\Type 55 | */ 56 | public abstract function checkExpr(Context\Context $context, bool $noErrors):Type\Type; 57 | 58 | /** 59 | * @param Type\Type $type 60 | * @param Context\Context $context 61 | * @return array|Type\Type[] 62 | */ 63 | public function inferLocal(Type\Type $type, Context\Context $context):array { 64 | return []; 65 | } 66 | } 67 | 68 | class Yield_ extends Expr { 69 | /** @var Expr|null */ 70 | private $key; 71 | /** @var Expr|null */ 72 | private $val; 73 | 74 | public function __construct(HasCodeLoc $loc, Expr $key = null, Expr $val = null) { 75 | parent::__construct($loc); 76 | $this->key = $key; 77 | $this->val = $val; 78 | } 79 | 80 | public function subStmts(bool $deep):array { 81 | $stmts = []; 82 | if ($this->key) { 83 | $stmts[] = $this->key; 84 | } 85 | if ($this->val) { 86 | $stmts[] = $this->val; 87 | } 88 | return $stmts; 89 | } 90 | 91 | public function unparseExpr():\PhpParser\Node\Expr { 92 | return new \PhpParser\Node\Expr\Yield_( 93 | $this->val ? $this->val->unparseExpr() : null, 94 | $this->key ? $this->key->unparseExpr() : null 95 | ); 96 | } 97 | 98 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 99 | return new Type\Mixed($this); 100 | } 101 | } 102 | 103 | class Include_ extends Expr { 104 | /** @var bool */ 105 | private $require = true; 106 | /** @var bool */ 107 | private $once = true; 108 | /** @var Expr */ 109 | private $expr; 110 | 111 | public function __construct(HasCodeLoc $loc, Expr $expr, bool $require, bool $once) { 112 | parent::__construct($loc); 113 | $this->require = $require; 114 | $this->once = $once; 115 | $this->expr = $expr; 116 | } 117 | 118 | public function subStmts(bool $deep):array { 119 | return [$this->expr]; 120 | } 121 | 122 | public function unparseExpr():\PhpParser\Node\Expr { 123 | $type = $this->require 124 | ? ($this->once 125 | ? \PhpParser\Node\Expr\Include_::TYPE_REQUIRE_ONCE 126 | : \PhpParser\Node\Expr\Include_::TYPE_REQUIRE) 127 | : ($this->once 128 | ? \PhpParser\Node\Expr\Include_::TYPE_INCLUDE_ONCE 129 | : \PhpParser\Node\Expr\Include_::TYPE_INCLUDE); 130 | 131 | return new \PhpParser\Node\Expr\Include_($this->expr->unparseExpr(), $type); 132 | } 133 | 134 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 135 | return new Type\Mixed($this); 136 | } 137 | } 138 | 139 | class Array_ extends Expr { 140 | /** @var ArrayItem[] */ 141 | private $items = []; 142 | 143 | /** 144 | * @param HasCodeLoc $loc 145 | * @param ArrayItem[] $items 146 | */ 147 | public function __construct(HasCodeLoc $loc, array $items) { 148 | parent::__construct($loc); 149 | $this->items = $items; 150 | } 151 | 152 | public function subStmts(bool $deep):array { 153 | $stmts = []; 154 | foreach ($this->items as $item) { 155 | foreach ($item->subStmts() as $stmt) { 156 | $stmts[] = $stmt; 157 | } 158 | } 159 | return $stmts; 160 | } 161 | 162 | public function unparseExpr():\PhpParser\Node\Expr { 163 | $items = []; 164 | foreach ($this->items as $item) { 165 | $items[] = $item->unparse(); 166 | } 167 | return new \PhpParser\Node\Expr\Array_($items); 168 | } 169 | 170 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 171 | $array = new Type\Shape($this, []); 172 | foreach ($this->items as $item) { 173 | $array = $item->apply($array, $context, $noErrors); 174 | } 175 | return $array; 176 | } 177 | } 178 | 179 | class ArrayItem extends Node { 180 | /** @var Expr|null */ 181 | private $key; 182 | /** @var Expr */ 183 | private $value; 184 | /** @var bool */ 185 | private $byRef; 186 | 187 | /** 188 | * @param HasCodeLoc $loc 189 | * @param Expr|null $key 190 | * @param Expr $value 191 | * @param bool $byByRef 192 | */ 193 | public function __construct(HasCodeLoc $loc, Expr $key = null, Expr $value, bool $byByRef) { 194 | parent::__construct($loc); 195 | $this->key = $key; 196 | $this->value = $value; 197 | $this->byRef = $byByRef; 198 | } 199 | 200 | public function subStmts():array { 201 | $stmts = [$this->value]; 202 | if ($this->key) { 203 | $stmts[] = $this->key; 204 | } 205 | return $stmts; 206 | } 207 | 208 | public function apply(Type\Type $array, Context\Context $context, bool $noErrors) { 209 | $key = $this->key; 210 | $val = $this->value->checkExpr($context, $noErrors); 211 | if ($key) { 212 | return $key->checkExpr($context, $noErrors)->useToSetArrayKey($this, $context, $array, $val, $noErrors); 213 | } else { 214 | return $array->addArrayKey($this, $context, $val, $noErrors); 215 | } 216 | } 217 | 218 | public function unparse():\PhpParser\Node\Expr\ArrayItem { 219 | return new \PhpParser\Node\Expr\ArrayItem( 220 | $this->value->unparseExpr(), 221 | $this->key ? $this->key->unparseExpr() : null, 222 | $this->byRef 223 | ); 224 | } 225 | } 226 | 227 | class Closure extends Expr { 228 | /** @var bool */ 229 | private $static; 230 | /** @var Function_\Function_ */ 231 | private $type; 232 | /** @var ClosureUse[] */ 233 | private $uses; 234 | /** @var Stmt\Block */ 235 | private $body; 236 | 237 | /** 238 | * @param HasCodeLoc $loc 239 | * @param bool $static 240 | * @param Function_\Function_ $type 241 | * @param ClosureUse[] $uses 242 | * @param Stmt\Block $body 243 | */ 244 | public function __construct( 245 | HasCodeLoc $loc, 246 | bool $static, 247 | Function_\Function_ $type, 248 | array $uses, 249 | Stmt\Block $body 250 | ) { 251 | parent::__construct($loc); 252 | $this->static = $static; 253 | $this->type = $type; 254 | $this->uses = $uses; 255 | $this->body = $body; 256 | } 257 | 258 | public function subStmts(bool $deep):array { 259 | $stmts = $this->type->subStmts(); 260 | if ($deep) { 261 | $stmts[] = $this->body; 262 | } 263 | return $stmts; 264 | } 265 | 266 | public function unparseExpr():\PhpParser\Node\Expr { 267 | $subNodes = $this->type->unparseAttributes(); 268 | 269 | $uses = []; 270 | foreach ($this->uses as $use) { 271 | $uses[] = $use->unparse(); 272 | } 273 | 274 | return new \PhpParser\Node\Expr\Closure(array_replace($subNodes, [ 275 | 'static' => $this->static, 276 | 'uses' => $uses, 277 | 'stmts' => $this->body->unparseNodes(), 278 | ])); 279 | } 280 | 281 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 282 | return new Type\Class_($this, 'Closure'); 283 | } 284 | } 285 | 286 | class ClosureUse extends Node { 287 | /** @var string */ 288 | private $name; 289 | /** @var bool */ 290 | private $byRef; 291 | 292 | public function __construct(HasCodeLoc $loc, string $name, bool $byRef) { 293 | parent::__construct($loc); 294 | $this->name = $name; 295 | $this->byRef = $byRef; 296 | } 297 | 298 | public function unparse():\PhpParser\Node\Expr\ClosureUse { 299 | return new \PhpParser\Node\Expr\ClosureUse($this->name, $this->byRef); 300 | } 301 | } 302 | 303 | class Ternary extends Expr { 304 | /** @var Expr */ 305 | private $cond; 306 | /** @var Expr|null */ 307 | private $true; 308 | /** @var Expr */ 309 | private $false; 310 | 311 | public function __construct(HasCodeLoc $loc, Expr $cond, Expr $true = null, Expr $false) { 312 | parent::__construct($loc); 313 | $this->cond = $cond; 314 | $this->true = $true; 315 | $this->false = $false; 316 | } 317 | 318 | public function subStmts(bool $deep):array { 319 | $stmts = [$this->cond, $this->false]; 320 | if ($this->true) { 321 | $stmts[] = $this->true; 322 | } 323 | return $stmts; 324 | } 325 | 326 | public function unparseExpr():\PhpParser\Node\Expr { 327 | return new \PhpParser\Node\Expr\Ternary( 328 | $this->cond->unparseExpr(), 329 | $this->true ? $this->true->unparseExpr() : null, 330 | $this->false->unparseExpr() 331 | ); 332 | } 333 | 334 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 335 | // TODO Apply type gaurds in $this->cond 336 | 337 | if ($this->true) { 338 | $true = $this->true->checkExpr($context, $noErrors); 339 | } else { 340 | $true = $this->cond->checkExpr($context, $noErrors)->removeFalsy($context); 341 | } 342 | 343 | $false = $this->false->checkExpr($context, $noErrors); 344 | 345 | return Type\Type::union($this, [$true, $false,]); 346 | } 347 | } 348 | 349 | class ConcatMany extends Expr { 350 | /** 351 | * @param Expr[] $exprs 352 | * @return \PhpParser\Node\Expr[] 353 | */ 354 | public static function unparseEncaps(array $exprs):array { 355 | $parts = []; 356 | foreach ($exprs as $expr) { 357 | $expr = $expr->unparseExprOrString(); 358 | if (is_string($expr)) { 359 | $expr = new \PhpParser\Node\Scalar\EncapsedStringPart($expr); 360 | } 361 | $parts[] = $expr; 362 | } 363 | return $parts; 364 | } 365 | 366 | /** @var Expr[] */ 367 | private $exprs; 368 | 369 | /** 370 | * @param HasCodeLoc $loc 371 | * @param Expr[] $exprs 372 | */ 373 | public function __construct(HasCodeLoc $loc, array $exprs) { 374 | parent::__construct($loc); 375 | $this->exprs = $exprs; 376 | } 377 | 378 | public function subStmts(bool $deep):array { 379 | return $this->exprs; 380 | } 381 | 382 | public function unparseExpr():\PhpParser\Node\Expr { 383 | return new \PhpParser\Node\Scalar\Encapsed(self::unparseEncaps($this->exprs)); 384 | } 385 | 386 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 387 | // TODO Make this really smart and return a SingleValue (or union thereof) if it can 388 | return new Type\String_($this); 389 | } 390 | } 391 | 392 | class Isset_ extends Expr { 393 | /** @var Expr[] */ 394 | private $exprs; 395 | 396 | /** 397 | * @param HasCodeLoc $loc 398 | * @param Expr[] $exprs 399 | */ 400 | public function __construct(HasCodeLoc $loc, array $exprs) { 401 | parent::__construct($loc); 402 | $this->exprs = $exprs; 403 | } 404 | 405 | public function subStmts(bool $deep):array { 406 | return $this->exprs; 407 | } 408 | 409 | public function unparseExpr():\PhpParser\Node\Expr { 410 | $exprs = []; 411 | foreach ($this->exprs as $expr) { 412 | $exprs[] = $expr->unparseExpr(); 413 | } 414 | return new \PhpParser\Node\Expr\Isset_($exprs); 415 | } 416 | 417 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 418 | return new Type\Int_($this); 419 | } 420 | } 421 | 422 | abstract class AbstractBinOp extends Expr { 423 | /** @var Expr */ 424 | protected $left; 425 | /** @var Expr */ 426 | protected $right; 427 | 428 | public function __construct(HasCodeLoc $loc, Expr $left, Expr $right) { 429 | parent::__construct($loc); 430 | $this->left = $left; 431 | $this->right = $right; 432 | } 433 | 434 | public function subStmts(bool $deep):array { 435 | return [$this->left, $this->right]; 436 | } 437 | } 438 | 439 | class BinOpType extends \JesseSchalken\Enum\StringEnum { 440 | const ADD = '+'; 441 | const SUBTRACT = '-'; 442 | const MULTIPLY = '*'; 443 | const DIVIDE = '/'; 444 | const MODULUS = '%'; 445 | const EXPONENT = '**'; 446 | 447 | const BIT_AND = '&'; 448 | const BIT_OR = '|'; 449 | const BIT_XOR = '^'; 450 | const SHIFT_LEFT = '<<'; 451 | const SHIFT_RIGHT = '>>'; 452 | 453 | const CONCAT = '.'; 454 | 455 | public static function values() { 456 | return [ 457 | self::ADD, 458 | self::SUBTRACT, 459 | self::MULTIPLY, 460 | self::DIVIDE, 461 | self::MODULUS, 462 | self::EXPONENT, 463 | self::BIT_AND, 464 | self::BIT_OR, 465 | self::BIT_XOR, 466 | self::SHIFT_LEFT, 467 | self::SHIFT_RIGHT, 468 | self::CONCAT, 469 | ]; 470 | } 471 | 472 | public function __toString() { 473 | return $this->value(); 474 | } 475 | 476 | public function evaluate($lhs, $rhs) { 477 | switch ($this->value()) { 478 | case self::ADD: 479 | return $lhs + $rhs; 480 | case self::SUBTRACT: 481 | return $lhs - $rhs; 482 | case self::MULTIPLY: 483 | return $lhs * $rhs; 484 | case self::DIVIDE: 485 | return $lhs / $rhs; 486 | case self::MODULUS: 487 | return $lhs % $rhs; 488 | case self::EXPONENT: 489 | return $lhs ** $rhs; 490 | case self::BIT_AND: 491 | return $lhs & $rhs; 492 | case self::BIT_OR: 493 | return $lhs | $rhs; 494 | case self::BIT_XOR: 495 | return $lhs ^ $rhs; 496 | case self::SHIFT_LEFT: 497 | return $lhs << $rhs; 498 | case self::SHIFT_RIGHT: 499 | return $lhs >> $rhs; 500 | case self::CONCAT: 501 | return $lhs . $rhs; 502 | default: 503 | throw new \Exception("Invalid binary op: $this"); 504 | } 505 | } 506 | } 507 | 508 | class LogicalOpType extends \JesseSchalken\Enum\StringEnum { 509 | const BOOl_AND = '&&'; 510 | const BOOl_OR = '||'; 511 | const LOGIC_AND = 'and'; 512 | const LOGIC_OR = 'or'; 513 | const LOGIC_XOR = 'xor'; 514 | 515 | public static function values() { 516 | return [ 517 | self::BOOl_AND, 518 | self::BOOl_OR, 519 | self::LOGIC_AND, 520 | self::LOGIC_OR, 521 | self::LOGIC_XOR, 522 | ]; 523 | } 524 | 525 | public function __toString() { 526 | return $this->value(); 527 | } 528 | } 529 | 530 | class AssignOp extends AbstractBinOp { 531 | /** @var BinOpType */ 532 | private $type; 533 | 534 | public function __construct(HasCodeLoc $loc, Expr $right, BinOpType $type, Expr $left) { 535 | parent::__construct($loc, $left, $right); 536 | $this->type = $type; 537 | } 538 | 539 | public function unparseExpr():\PhpParser\Node\Expr { 540 | $left = $this->left->unparseExpr(); 541 | $right = $this->right->unparseExpr(); 542 | switch ($this->type->value()) { 543 | case BinOpType::ADD: 544 | return new \PhpParser\Node\Expr\AssignOp\Plus($left, $right); 545 | case BinOpType::SUBTRACT: 546 | return new \PhpParser\Node\Expr\AssignOp\Minus($left, $right); 547 | case BinOpType::MULTIPLY: 548 | return new \PhpParser\Node\Expr\AssignOp\Mul($left, $right); 549 | case BinOpType::DIVIDE: 550 | return new \PhpParser\Node\Expr\AssignOp\Div($left, $right); 551 | case BinOpType::MODULUS: 552 | return new \PhpParser\Node\Expr\AssignOp\Mod($left, $right); 553 | case BinOpType::EXPONENT: 554 | return new \PhpParser\Node\Expr\AssignOp\Pow($left, $right); 555 | case BinOpType::CONCAT: 556 | return new \PhpParser\Node\Expr\AssignOp\Concat($left, $right); 557 | case BinOpType::BIT_AND: 558 | return new \PhpParser\Node\Expr\AssignOp\BitwiseAnd($left, $right); 559 | case BinOpType::BIT_OR: 560 | return new \PhpParser\Node\Expr\AssignOp\BitwiseOr($left, $right); 561 | case BinOpType::BIT_XOR: 562 | return new \PhpParser\Node\Expr\AssignOp\BitwiseXor($left, $right); 563 | case BinOpType::SHIFT_LEFT: 564 | return new \PhpParser\Node\Expr\AssignOp\ShiftLeft($left, $right); 565 | case BinOpType::SHIFT_RIGHT: 566 | return new \PhpParser\Node\Expr\AssignOp\ShiftRight($left, $right); 567 | default: 568 | throw new \Exception('Invalid binary operator type: ' . $this->type->value()); 569 | } 570 | } 571 | 572 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 573 | $lhs = $this->left->checkExpr($context, $noErrors); 574 | $rhs = $this->right->checkExpr($context, $noErrors); 575 | $val = $lhs->doBinOp($this, $rhs, $this->type, $context, $noErrors); 576 | if (!$noErrors) { 577 | $lhs->checkContains($this, $val, $context); 578 | } 579 | // The result of an assignment operation is the value assigned back to the LHS 580 | return $val; 581 | } 582 | } 583 | 584 | class Assign extends Expr { 585 | /** @var Expr */ 586 | private $left; 587 | /** @var Expr */ 588 | private $right; 589 | /** @var bool */ 590 | private $byRef; 591 | 592 | public function __construct(HasCodeLoc $loc, Expr $left, Expr $right, $byRef) { 593 | parent::__construct($loc); 594 | $this->left = $left; 595 | $this->right = $right; 596 | $this->byRef = $byRef; 597 | } 598 | 599 | public function unparseExpr():\PhpParser\Node\Expr { 600 | $left = $this->left->unparseExpr(); 601 | $right = $this->right->unparseExpr(); 602 | if ($this->byRef) { 603 | return new \PhpParser\Node\Expr\AssignRef($left, $right); 604 | } else { 605 | return new \PhpParser\Node\Expr\Assign($left, $right); 606 | } 607 | } 608 | 609 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 610 | $right = $this->right->checkExpr($context, $noErrors); 611 | if (!$noErrors) { 612 | $left = $this->left->checkExpr($context, $noErrors); 613 | if ($this->byRef) { 614 | $left->checkEquivelant($this, $right, $context); 615 | } else { 616 | $left->checkContains($this, $right, $context); 617 | } 618 | } 619 | return $right; 620 | } 621 | 622 | protected function inferLocals(Context\Context $context):array { 623 | return merge_types( 624 | parent::inferLocals($context), 625 | $this->left->inferLocal($this->right->checkExpr($context, true), $context), 626 | $context 627 | ); 628 | } 629 | 630 | public function subStmts(bool $deep):array { 631 | return [$this->left, $this->right]; 632 | } 633 | } 634 | 635 | class InstanceOf_ extends AbstractBinOp { 636 | public function unparseExpr():\PhpParser\Node\Expr { 637 | return new \PhpParser\Node\Expr\Instanceof_( 638 | $this->left->unparseExpr(), 639 | $this->right->unparseExprOrName() 640 | ); 641 | } 642 | 643 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 644 | // TODO do some actual checking 645 | // TODO maybe return "true" or "false" single values if the LHS is known to be/not to be an instanceof RHS 646 | return Type\Type::bool($this); 647 | } 648 | } 649 | 650 | class LogicalOp extends AbstractBinOp { 651 | private $type; 652 | 653 | public function __construct(HasCodeLoc $loc, Expr $left, LogicalOpType $type, Expr $right) { 654 | parent::__construct($loc, $left, $right); 655 | $this->type = $type; 656 | } 657 | 658 | public function unparseExpr():\PhpParser\Node\Expr { 659 | $left = $this->left->unparseExpr(); 660 | $right = $this->right->unparseExpr(); 661 | switch ($this->type->value()) { 662 | case LogicalOpType::BOOl_AND: 663 | return new \PhpParser\Node\Expr\BinaryOp\BooleanAnd($left, $right); 664 | case LogicalOpType::BOOl_OR: 665 | return new \PhpParser\Node\Expr\BinaryOp\BooleanOr($left, $right); 666 | case LogicalOpType::LOGIC_AND: 667 | return new \PhpParser\Node\Expr\BinaryOp\LogicalAnd($left, $right); 668 | case LogicalOpType::LOGIC_OR: 669 | return new \PhpParser\Node\Expr\BinaryOp\LogicalOr($left, $right); 670 | case LogicalOpType::LOGIC_XOR: 671 | return new \PhpParser\Node\Expr\BinaryOp\LogicalXor($left, $right); 672 | default: 673 | throw new \Exception('huh?'); 674 | } 675 | } 676 | 677 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 678 | // TODO do some actual checking 679 | // TODO maybe return "true" or "false" single values if the value is known 680 | return Type\Type::bool($this); 681 | } 682 | } 683 | 684 | class Coalesce extends AbstractBinOp { 685 | public function unparseExpr():\PhpParser\Node\Expr { 686 | $left = $this->left->unparseExpr(); 687 | $right = $this->right->unparseExpr(); 688 | return new \PhpParser\Node\Expr\BinaryOp\Coalesce($left, $right); 689 | } 690 | 691 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 692 | $left = $this->left->checkExpr($context, $noErrors)->removeNull($context); 693 | $right = $this->right->checkExpr($context, $noErrors); 694 | return Type\Type::union($this, [$left, $right]); 695 | } 696 | } 697 | 698 | class Spaceship extends AbstractBinOp { 699 | public function unparseExpr():\PhpParser\Node\Expr { 700 | $left = $this->left->unparseExpr(); 701 | $right = $this->right->unparseExpr(); 702 | return new \PhpParser\Node\Expr\BinaryOp\Spaceship($left, $right); 703 | } 704 | 705 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 706 | // TODO do some actual checking here 707 | return Type\Type::union($this, [ 708 | new Type\SingleValue($this, -1), 709 | new Type\SingleValue($this, 0), 710 | new Type\SingleValue($this, 1), 711 | ]); 712 | } 713 | } 714 | 715 | class ComparisonOpType extends \JesseSchalken\Enum\StringEnum { 716 | const EQUAL = '=='; 717 | const IDENTICAL = '==='; 718 | const NOT_EQUAL = '!='; 719 | const NOT_IDENTICAL = '!=='; 720 | const GREATER = '>'; 721 | const LESS = '<'; 722 | const GREATER_OR_EQUAL = '>='; 723 | const LESS_OR_EQUAL = '<='; 724 | 725 | public static function values() { 726 | return [ 727 | self::EQUAL, 728 | self::IDENTICAL, 729 | self::NOT_EQUAL, 730 | self::NOT_IDENTICAL, 731 | self::GREATER, 732 | self::LESS, 733 | self::GREATER_OR_EQUAL, 734 | self::LESS_OR_EQUAL, 735 | ]; 736 | } 737 | 738 | public function __toString() { 739 | return $this->value(); 740 | } 741 | } 742 | 743 | class Comparison extends AbstractBinOp { 744 | /** @var ComparisonOpType */ 745 | private $type; 746 | 747 | public function __construct(HasCodeLoc $loc, Expr $left, Expr $right, ComparisonOpType $type) { 748 | parent::__construct($loc, $left, $right); 749 | $this->type = $type; 750 | } 751 | 752 | public function unparseExpr():\PhpParser\Node\Expr { 753 | $left = $this->left->unparseExpr(); 754 | $right = $this->right->unparseExpr(); 755 | switch ($this->type->value()) { 756 | case ComparisonOpType::EQUAL: 757 | return new \PhpParser\Node\Expr\BinaryOp\Equal($left, $right); 758 | case ComparisonOpType::IDENTICAL: 759 | return new \PhpParser\Node\Expr\BinaryOp\Identical($left, $right); 760 | case ComparisonOpType::NOT_EQUAL: 761 | return new \PhpParser\Node\Expr\BinaryOp\NotEqual($left, $right); 762 | case ComparisonOpType::NOT_IDENTICAL: 763 | return new \PhpParser\Node\Expr\BinaryOp\NotIdentical($left, $right); 764 | case ComparisonOpType::GREATER: 765 | return new \PhpParser\Node\Expr\BinaryOp\Greater($left, $right); 766 | case ComparisonOpType::LESS: 767 | return new \PhpParser\Node\Expr\BinaryOp\Smaller($left, $right); 768 | case ComparisonOpType::GREATER_OR_EQUAL: 769 | return new \PhpParser\Node\Expr\BinaryOp\GreaterOrEqual($left, $right); 770 | case ComparisonOpType::LESS_OR_EQUAL: 771 | return new \PhpParser\Node\Expr\BinaryOp\SmallerOrEqual($left, $right); 772 | default: 773 | throw new \Exception("huh? $this->type"); 774 | } 775 | } 776 | 777 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 778 | // TODO do some actual checking 779 | // TODO should probably just check the lhs/rhs against int|bool|float 780 | return Type\Type::bool($this); 781 | } 782 | } 783 | 784 | class BinOp extends AbstractBinOp { 785 | /** @var BinOpType */ 786 | private $type; 787 | 788 | public function __construct(HasCodeLoc $loc, Expr $left, BinOpType $type, Expr $right) { 789 | parent::__construct($loc, $left, $right); 790 | $this->type = $type; 791 | } 792 | 793 | public function unparseExpr():\PhpParser\Node\Expr { 794 | $left = $this->left->unparseExpr(); 795 | $right = $this->right->unparseExpr(); 796 | switch ($this->type) { 797 | case BinOpType::ADD: 798 | return new \PhpParser\Node\Expr\BinaryOp\Plus($left, $right); 799 | case BinOpType::SUBTRACT: 800 | return new \PhpParser\Node\Expr\BinaryOp\Minus($left, $right); 801 | case BinOpType::MULTIPLY: 802 | return new \PhpParser\Node\Expr\BinaryOp\Mul($left, $right); 803 | case BinOpType::DIVIDE: 804 | return new \PhpParser\Node\Expr\BinaryOp\Div($left, $right); 805 | case BinOpType::MODULUS: 806 | return new \PhpParser\Node\Expr\BinaryOp\Mod($left, $right); 807 | case BinOpType::EXPONENT: 808 | return new \PhpParser\Node\Expr\BinaryOp\Pow($left, $right); 809 | case BinOpType::BIT_AND: 810 | return new \PhpParser\Node\Expr\BinaryOp\BitwiseAnd($left, $right); 811 | case BinOpType::BIT_OR: 812 | return new \PhpParser\Node\Expr\BinaryOp\BitwiseOr($left, $right); 813 | case BinOpType::BIT_XOR: 814 | return new \PhpParser\Node\Expr\BinaryOp\BitwiseXor($left, $right); 815 | case BinOpType::SHIFT_LEFT: 816 | return new \PhpParser\Node\Expr\BinaryOp\ShiftLeft($left, $right); 817 | case BinOpType::SHIFT_RIGHT: 818 | return new \PhpParser\Node\Expr\BinaryOp\ShiftRight($left, $right); 819 | case BinOpType::CONCAT: 820 | return new \PhpParser\Node\Expr\BinaryOp\Concat($left, $right); 821 | default: 822 | throw new \Exception('Invalid binary operator type: ' . $this->type); 823 | } 824 | } 825 | 826 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 827 | $left = $this->left->checkExpr($context, $noErrors); 828 | $right = $this->right->checkExpr($context, $noErrors); 829 | return $left->doBinOp($this, $right, $this->type, $context, $noErrors); 830 | } 831 | } 832 | 833 | class CastType extends \JesseSchalken\Enum\StringEnum { 834 | const INT = 'int'; 835 | const BOOL = 'bool'; 836 | const FLOAT = 'float'; 837 | const STRING = 'string'; 838 | const ARRAY = 'array'; 839 | const OBJECT = 'object'; 840 | const UNSET = 'unset'; 841 | 842 | public static function values() { 843 | return [ 844 | self::INT, 845 | self::BOOL, 846 | self::FLOAT, 847 | self::ARRAY, 848 | self::OBJECT, 849 | self::UNSET, 850 | ]; 851 | } 852 | 853 | public function toType(HasCodeLoc $loc):Type\Type { 854 | switch ($this->value()) { 855 | case self::INT: 856 | return new Type\Int_($loc); 857 | case self::BOOL: 858 | return Type\Type::bool($loc); 859 | case self::FLOAT: 860 | return new Type\Float_($loc); 861 | case self::STRING: 862 | return new Type\String_($loc); 863 | case self::ARRAY: 864 | return new Type\Array_($loc, new Type\Mixed($loc)); 865 | case self::OBJECT: 866 | return new Type\Object($loc); 867 | case self::UNSET: 868 | return new Type\SingleValue($loc, null); 869 | default: 870 | throw new \Exception('Invalid cast type: ' . $this); 871 | } 872 | } 873 | 874 | public function evaluate($value) { 875 | switch ($this->value()) { 876 | case self::INT: 877 | return (int)$value; 878 | case self::BOOL: 879 | return (bool)$value; 880 | case self::FLOAT: 881 | return (float)$value; 882 | case self::STRING: 883 | return (string)$value; 884 | case self::ARRAY: 885 | return (array)$value; 886 | case self::OBJECT: 887 | return (object)$value; 888 | case self::UNSET: 889 | return (unset)$value; 890 | default: 891 | throw new \Exception('Invalid cast type: ' . $this); 892 | } 893 | } 894 | 895 | public function __toString() { 896 | return $this->value(); 897 | } 898 | } 899 | 900 | class Cast extends Expr { 901 | /** @var string */ 902 | private $type; 903 | /** @var CastType */ 904 | private $expr; 905 | 906 | public function __construct(HasCodeLoc $loc, CastType $type, Expr $expr) { 907 | parent::__construct($loc); 908 | $this->type = $type; 909 | $this->expr = $expr; 910 | } 911 | 912 | public function subStmts(bool $deep):array { 913 | return [$this->expr]; 914 | } 915 | 916 | public function unparseExpr():\PhpParser\Node\Expr { 917 | $expr = $this->expr->unparseExpr(); 918 | switch ($this->type->value()) { 919 | case CastType::INT: 920 | return new \PhpParser\Node\Expr\Cast\Int_($expr); 921 | case CastType::BOOL: 922 | return new \PhpParser\Node\Expr\Cast\Bool_($expr); 923 | case CastType::FLOAT: 924 | return new \PhpParser\Node\Expr\Cast\Double($expr); 925 | case CastType::STRING: 926 | return new \PhpParser\Node\Expr\Cast\String_($expr); 927 | case CastType::ARRAY: 928 | return new \PhpParser\Node\Expr\Cast\Array_($expr); 929 | case CastType::OBJECT: 930 | return new \PhpParser\Node\Expr\Cast\Object_($expr); 931 | case CastType::UNSET: 932 | return new \PhpParser\Node\Expr\Cast\Unset_($expr); 933 | default: 934 | throw new \Exception('Invalid cast type: ' . $this->type); 935 | } 936 | } 937 | 938 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 939 | return $this->expr->checkExpr($context, $noErrors)->doCast($this, $this->type, $context, $noErrors); 940 | } 941 | } 942 | 943 | class UnOp extends Expr { 944 | const PRE_INC = '++ '; 945 | const POST_INC = ' ++'; 946 | const PRE_DEC = '-- '; 947 | const POST_DEC = ' --'; 948 | const PRINT = 'print'; 949 | const BOOL_NOT = '!'; 950 | const BIT_NOT = '~'; 951 | const NEGATE = '-'; 952 | const PLUS = '+'; 953 | const SUPPRESS = '@'; 954 | const EMPTY = 'empty'; 955 | const EVAL = 'eval'; 956 | const CLONE = 'clone'; 957 | 958 | /** @var string */ 959 | private $type; 960 | /** @var Expr */ 961 | private $expr; 962 | 963 | public function __construct(HasCodeLoc $loc, string $type, Expr $expr) { 964 | parent::__construct($loc); 965 | $this->type = $type; 966 | $this->expr = $expr; 967 | } 968 | 969 | public function subStmts(bool $deep):array { 970 | return [$this->expr]; 971 | } 972 | 973 | public function unparseExpr():\PhpParser\Node\Expr { 974 | $expr = $this->expr->unparseExpr(); 975 | switch ($this->type) { 976 | case self::PRE_INC: 977 | return new \PhpParser\Node\Expr\PreInc($expr); 978 | case self::PRE_DEC: 979 | return new \PhpParser\Node\Expr\PreDec($expr); 980 | case self::POST_INC: 981 | return new \PhpParser\Node\Expr\PostInc($expr); 982 | case self::POST_DEC: 983 | return new \PhpParser\Node\Expr\PostDec($expr); 984 | case self::PRINT: 985 | return new \PhpParser\Node\Expr\Print_($expr); 986 | case self::BOOL_NOT: 987 | return new \PhpParser\Node\Expr\BooleanNot($expr); 988 | case self::BIT_NOT: 989 | return new \PhpParser\Node\Expr\BitwiseNot($expr); 990 | case self::PLUS: 991 | return new \PhpParser\Node\Expr\UnaryPlus($expr); 992 | case self::NEGATE: 993 | return new \PhpParser\Node\Expr\UnaryMinus($expr); 994 | case self::SUPPRESS: 995 | return new \PhpParser\Node\Expr\ErrorSuppress($expr); 996 | case self::EMPTY: 997 | return new \PhpParser\Node\Expr\Empty_($expr); 998 | case self::EVAL: 999 | return new \PhpParser\Node\Expr\Eval_($expr); 1000 | case self::CLONE: 1001 | return new \PhpParser\Node\Expr\Clone_($expr); 1002 | default: 1003 | throw new \Exception('Invalid unary operator type: ' . $this->type); 1004 | } 1005 | } 1006 | 1007 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 1008 | switch ($this->type) { 1009 | case self::PRE_INC: 1010 | case self::PRE_DEC: 1011 | return Type\Type::number($this); 1012 | 1013 | case self::POST_INC: 1014 | case self::POST_DEC: 1015 | return $this->expr->checkExpr($context, $noErrors); 1016 | 1017 | case self::PRINT: 1018 | // "print ..." always evaluates to int(1) 1019 | return new Type\SingleValue($this, 1); 1020 | 1021 | case self::BOOL_NOT: 1022 | return Type\Type::bool($this); 1023 | 1024 | case self::BIT_NOT: 1025 | return new Type\Int_($this); 1026 | 1027 | case self::PLUS: 1028 | case self::NEGATE: 1029 | return Type\Type::number($this); 1030 | 1031 | case self::SUPPRESS: 1032 | return $this->expr->checkExpr($context, $noErrors); 1033 | 1034 | case self::EMPTY: 1035 | return Type\Type::bool($this); 1036 | 1037 | case self::EVAL: 1038 | return new Type\Mixed($this); 1039 | 1040 | case self::CLONE: 1041 | return $this->expr->checkExpr($context, $noErrors); 1042 | 1043 | default: 1044 | throw new \Exception('Invalid unary operator type: ' . $this->type); 1045 | } 1046 | } 1047 | } 1048 | 1049 | class Exit_ extends Expr { 1050 | /** @var Expr|null */ 1051 | private $expr; 1052 | 1053 | public function __construct(HasCodeLoc $loc, Expr $expr = null) { 1054 | parent::__construct($loc); 1055 | $this->expr = $expr; 1056 | } 1057 | 1058 | public function subStmts(bool $deep):array { 1059 | return $this->expr ? [$this->expr] : []; 1060 | } 1061 | 1062 | public function unparseExpr():\PhpParser\Node\Expr { 1063 | $expr = $this->expr ? $this->expr->unparseExpr() : null; 1064 | return new \PhpParser\Node\Expr\Exit_($expr); 1065 | } 1066 | 1067 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 1068 | // "exit" is an expression??? How do you use the result? 1069 | return Type\Type::none($this); 1070 | } 1071 | } 1072 | 1073 | class ShellExec extends Expr { 1074 | /** @var Expr[] */ 1075 | private $parts; 1076 | 1077 | /** 1078 | * @param HasCodeLoc $loc 1079 | * @param Expr[] $parts 1080 | */ 1081 | public function __construct(HasCodeLoc $loc, array $parts) { 1082 | parent::__construct($loc); 1083 | $this->parts = $parts; 1084 | } 1085 | 1086 | public function subStmts(bool $deep):array { 1087 | return $this->parts; 1088 | } 1089 | 1090 | public function unparseExpr():\PhpParser\Node\Expr { 1091 | return new \PhpParser\Node\Expr\ShellExec(ConcatMany::unparseEncaps($this->parts)); 1092 | } 1093 | 1094 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 1095 | return new Type\String_($this); 1096 | } 1097 | } 1098 | 1099 | abstract class AbstractClassName extends Expr { 1100 | public abstract function toString(string $static = null):string; 1101 | 1102 | public abstract function toType(string $static = null, bool $strict = false):Type\Type; 1103 | } 1104 | 1105 | /** 1106 | * Foo\Bar::class 1107 | */ 1108 | class ClassName extends AbstractClassName { 1109 | /** @var string */ 1110 | private $class; 1111 | 1112 | public function __construct(HasCodeLoc $loc, string $class) { 1113 | parent::__construct($loc); 1114 | if (substr($class, 0, 1) === '\\') { 1115 | throw new \Exception("Illegal class name: $class"); 1116 | } 1117 | $this->class = $class; 1118 | } 1119 | 1120 | public function toType(string $static = null, bool $strict = false):Type\Type { 1121 | return new Type\Class_($this, $this->class); 1122 | } 1123 | 1124 | public function toString(string $static = null):string { 1125 | return $this->class; 1126 | } 1127 | 1128 | public function subStmts(bool $deep):array { 1129 | return []; 1130 | } 1131 | 1132 | public function unparseExpr():\PhpParser\Node\Expr { 1133 | return new \PhpParser\Node\Expr\ClassConstFetch( 1134 | new \PhpParser\Node\Name\FullyQualified($this->class), 1135 | 'class' 1136 | ); 1137 | } 1138 | 1139 | public function unparseExprOrName() { 1140 | return new \PhpParser\Node\Name\FullyQualified($this->class); 1141 | } 1142 | 1143 | public function unparseExprOrString() { 1144 | return $this->class; 1145 | } 1146 | 1147 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 1148 | return new Type\SingleValue($this, $this->class); 1149 | } 1150 | } 1151 | 1152 | /** 1153 | * static::name 1154 | */ 1155 | class StaticClassName extends AbstractClassName { 1156 | public function toString(string $static = null):string { 1157 | if ($static === null) { 1158 | throw new \Exception('"static" used in disallowed context'); 1159 | } else { 1160 | return $static; 1161 | } 1162 | } 1163 | 1164 | public function toType(string $static = null, bool $strict = false):Type\Type { 1165 | $type = new Type\Class_($this, $this->toString($static)); 1166 | $type = new Type\TypeVar($this, Type\TypeVar::STATIC, $type); 1167 | return $type; 1168 | } 1169 | 1170 | public function subStmts(bool $deep):array { 1171 | return []; 1172 | } 1173 | 1174 | public function unparseExpr():\PhpParser\Node\Expr { 1175 | return new \PhpParser\Node\Expr\ClassConstFetch( 1176 | new \PhpParser\Node\Name('static'), 1177 | 'class' 1178 | ); 1179 | } 1180 | 1181 | public function unparseExprOrName() { 1182 | return new \PhpParser\Node\Name('static'); 1183 | } 1184 | 1185 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 1186 | return new Type\String_($this); 1187 | } 1188 | } 1189 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Function_.php: -------------------------------------------------------------------------------- 1 | returnType = $returnType; 38 | $this->returnRef = $returnByRef; 39 | $this->params = $params; 40 | $this->varArg = $varArg; 41 | } 42 | 43 | public function addLocals(Context\Context $context) { 44 | foreach ($this->params as $i => $param) { 45 | $type = $param->type(); 46 | $name = $param->name(); 47 | $context->addLocal($name, $type); 48 | } 49 | 50 | $varArg = $this->varArg; 51 | if ($varArg) { 52 | $name = $varArg->name(); 53 | $type = $varArg->type(); 54 | $type = new Type\Array_($varArg, $type); 55 | $context->addLocal($name, $type); 56 | } 57 | 58 | $context->setReturn($this->returnType); 59 | $context->setReturnRef($this->returnRef); 60 | } 61 | 62 | public function subStmts():array { 63 | $stmts = []; 64 | foreach ($this->params as $param) { 65 | foreach ($param->subStmts() as $stmt) { 66 | $stmts[] = $stmt; 67 | } 68 | } 69 | return $stmts; 70 | } 71 | 72 | public function toString(string $name):string { 73 | $params = []; 74 | $varArg = $this->varArg; 75 | foreach ($this->params as $i => $param) { 76 | $params[] = $param->toString(false); 77 | } 78 | if ($varArg) { 79 | $params[] = $varArg->toString(true); 80 | } 81 | return 82 | ($this->returnRef ? '&' : '') . 83 | $name . 84 | '(' . join(', ', $params) . ')' . 85 | ':' . $this->returnType->toString(); 86 | } 87 | 88 | public function contains(Function_ $that, Context\Context $ctx):bool { 89 | if ( 90 | $this->returnRef != $that->returnRef || 91 | !$this->returnType->containsType($that->returnType, $ctx) 92 | ) { 93 | return false; 94 | } 95 | 96 | $len = max( 97 | count($this->params), 98 | count($that->params) 99 | ) + 1 /* for the variadic param */; 100 | 101 | for ($i = 0; $i < $len; $i++) { 102 | if ( 103 | $this->isParamOptional($i) && 104 | $that->isParamRequired($i) 105 | ) { 106 | // Optional/missing parameters cannot be made required 107 | return false; 108 | } 109 | $thatParam = $that->param($i); 110 | $thisParam = $this->param($i); 111 | if (!$thisParam) { 112 | // We don't define this param, so they can do what they want with it, but it can't be required. 113 | // Continue to make sore any extra params are optional. 114 | continue; 115 | } 116 | if (!$thatParam) { 117 | // They didn't define this param, but we did. They have to be prepared to accept just as many 118 | // parameters as us. 119 | return false; 120 | } 121 | if (!$thisParam->contains($thatParam, $ctx)) { 122 | return false; 123 | } 124 | } 125 | 126 | return true; 127 | } 128 | 129 | public function acceptsParam(int $i):bool { 130 | return $this->varArg || isset($this->params[$i]); 131 | } 132 | 133 | /** 134 | * @param int $i 135 | * @return Param|null 136 | */ 137 | public function param(int $i) { 138 | return $this->params[$i] ?? $this->varArg; 139 | } 140 | 141 | public function isParamOptional(int $i):bool { 142 | $param = $this->params[$i] ?? null; 143 | if ($param) { 144 | return $param->isOptional(); 145 | } else { 146 | // Varargs are optional, and superfluous parameters are also optional 147 | return true; 148 | } 149 | } 150 | 151 | public function isParamRequired(int $i):bool { 152 | return !$this->isParamOptional($i); 153 | } 154 | 155 | public function paramType(int $i):Type\Type { 156 | if (isset($this->params[$i])) { 157 | return $this->params[$i]->type(); 158 | } else if ($this->varArg && $this->params) { 159 | return $this->params[count($this->params) - 1]->type(); 160 | } else { 161 | // Any superfluous parameters accept nothing 162 | return Type\Type::none($this); 163 | } 164 | } 165 | 166 | public function isParamRef(int $i):bool { 167 | if (isset($this->params[$i])) { 168 | return $this->params[$i]->isByRef(); 169 | } else if ($this->varArg && $this->params) { 170 | return $this->params[count($this->params) - 1]->isByRef(); 171 | } else { 172 | // Any superfluous parameters are not passed by reference 173 | return false; 174 | } 175 | } 176 | 177 | public function unparseAttributes():array { 178 | $params = []; 179 | $varArg = $this->varArg; 180 | foreach ($this->params as $k => $param) { 181 | $params[] = $param->unparse(false); 182 | } 183 | if ($varArg) { 184 | $params[] = $varArg->unparse(true); 185 | } 186 | return [ 187 | 'byRef' => $this->returnRef, 188 | 'params' => $params, 189 | 'returnType' => $this->returnType->toTypeHint(), 190 | ]; 191 | } 192 | 193 | /** 194 | * @param HasCodeLoc $loc 195 | * @param Context\Context $context 196 | * @param Call\EvaledCallArg[] $args 197 | * @param bool $noErrors 198 | * @return Type\Type 199 | */ 200 | public function call(HasCodeLoc $loc, Context\Context $context, array $args, bool $noErrors):Type\Type { 201 | if ($noErrors) { 202 | return $this->returnType; 203 | } 204 | 205 | foreach ($this->params as $i => $param) { 206 | if ($param->isRequired() && !isset($args[$i])) { 207 | $context->addError("Missing parameter #" . ($i + 1) . " ($param)", $loc); 208 | } 209 | } 210 | 211 | $i = 0; 212 | foreach ($args as $i => $arg) { 213 | if ($arg->splat) { 214 | break; 215 | } 216 | $param = $this->param($i); 217 | if ($param) { 218 | $param->checkAgainst($i, $arg, $context); 219 | } else { 220 | $context->addError("Excess parameter #" . ($i + 1), $arg); 221 | } 222 | } 223 | $i++; 224 | 225 | // Splat mode 226 | // Check all remaining arguments against all remaining parameters 227 | /** @var Param[] $params */ 228 | $args2 = array_slice($args, $i, null, true); 229 | $params = array_slice($this->params, $i); 230 | $varArg = $this->varArg; 231 | if ($varArg) { 232 | $params[] = $varArg; 233 | } 234 | foreach ($args2 as $i => $arg) { 235 | foreach ($params as $param) { 236 | $param->checkAgainst($i, $arg, $context); 237 | } 238 | } 239 | 240 | return $this->returnType; 241 | } 242 | } 243 | 244 | class Param extends Node { 245 | /** @var string */ 246 | private $name; 247 | /** @var Type\Type */ 248 | private $type; 249 | /** @var bool */ 250 | private $byRef; 251 | /** @var Expr\Expr|null */ 252 | private $default = null; 253 | 254 | public function __construct(HasCodeLoc $loc, string $name, Type\Type $type, bool $byRef, Expr\Expr $default = null) { 255 | parent::__construct($loc); 256 | 257 | $this->name = $name; 258 | $this->type = $type; 259 | $this->byRef = $byRef; 260 | $this->default = $default; 261 | } 262 | 263 | public function subStmts():array { 264 | $default = $this->default; 265 | return $default ? [$default] : []; 266 | } 267 | 268 | public function isRequired():bool { 269 | return $this->default === null; 270 | } 271 | 272 | public function isOptional():bool { 273 | return $this->default !== null; 274 | } 275 | 276 | public function contains(self $other, Context\Context $context) { 277 | return 278 | $other->byRef == $this->byRef && 279 | $other->type->containsType($this->type, $context); 280 | } 281 | 282 | public function isByRef():bool { 283 | return $this->byRef; 284 | } 285 | 286 | public function type():Type\Type { 287 | return $this->type; 288 | } 289 | 290 | public function toString(bool $variadic):string { 291 | return 292 | $this->type->toString() . ' ' . 293 | ($this->byRef ? '&' : '') . 294 | ($variadic ? '...' : '') . 295 | '$' . $this->name . 296 | ($this->isOptional() ? ' = ?' : ''); 297 | } 298 | 299 | public function checkAgainst(int $i, Call\EvaledCallArg $arg, Context\Context $context) { 300 | if ($this->byRef) { 301 | if (!$arg->referrable) { 302 | $context->addError("Argument #" . ($i + 1) . " must be referrable (lvalue or call).", $arg); 303 | } 304 | $this->type->checkEquivelant($arg, $arg->type, $context); 305 | } else { 306 | $this->type->checkContains($arg, $arg->type, $context); 307 | } 308 | } 309 | 310 | public function __toString() { 311 | return $this->toString(false); 312 | } 313 | 314 | public function unparse(bool $variadic):\PhpParser\Node\Param { 315 | return new \PhpParser\Node\Param( 316 | $this->name, 317 | $this->default ? $this->default->unparseExpr() : null, 318 | $this->type->toTypeHint(), 319 | $this->byRef, 320 | $variadic 321 | ); 322 | } 323 | 324 | public function name():string { 325 | return $this->name; 326 | } 327 | } 328 | 329 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/LValue.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | } 26 | 27 | public function subStmts(bool $deep):array { 28 | return [$this->name]; 29 | } 30 | 31 | public function unparseExpr():\PhpParser\Node\Expr { 32 | return new \PhpParser\Node\Expr\Variable($this->name->unparseExprOrString()); 33 | } 34 | 35 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 36 | return $this->name->checkExpr($context, $noErrors)->useAsVariableName($this, $context); 37 | } 38 | 39 | public function inferLocal(Type\Type $type, Context\Context $context):array { 40 | $locals = []; 41 | foreach ($this->name->checkExpr($context, true)->getStringValues($context) as $string) { 42 | $locals[$string] = $type; 43 | } 44 | return $locals; 45 | } 46 | } 47 | 48 | class SuperGlobal extends \JesseSchalken\Enum\StringEnum { 49 | const GLOBALS = 'GLOBALS'; 50 | const _SERVER = '_SERVER'; 51 | const _GET = '_GET'; 52 | const _POST = '_POST'; 53 | const _FILES = '_FILES'; 54 | const _COOKIE = '_COOKIE'; 55 | const _SESSION = '_SESSION'; 56 | const _REQUEST = '_REQUEST'; 57 | const _ENV = '_ENV'; 58 | 59 | public static function values() { 60 | return [ 61 | self::GLOBALS, 62 | self::_SERVER, 63 | self::_GET, 64 | self::_POST, 65 | self::_FILES, 66 | self::_COOKIE, 67 | self::_SESSION, 68 | self::_REQUEST, 69 | self::_ENV, 70 | ]; 71 | } 72 | } 73 | 74 | class SuperGlobalAccess extends LValue { 75 | /** @var SuperGlobal */ 76 | private $global; 77 | 78 | public function __construct(HasCodeLoc $loc, SuperGlobal $global) { 79 | parent::__construct($loc); 80 | $this->global = $global; 81 | } 82 | 83 | public function subStmts(bool $deep):array { 84 | return []; 85 | } 86 | 87 | public function unparseExpr():\PhpParser\Node\Expr { 88 | return new \PhpParser\Node\Expr\Variable($this->global->value()); 89 | } 90 | 91 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 92 | $global = $this->global->value(); 93 | $type = $context->getGlobal($global); 94 | if ($type === null) { 95 | return $context->addError("Undefined global '$global'", $this); 96 | } else { 97 | return $type; 98 | } 99 | } 100 | } 101 | 102 | class Property extends LValue { 103 | /** @var Expr\Expr */ 104 | private $object; 105 | /** @var Expr\Expr */ 106 | private $property; 107 | 108 | /** 109 | * @param HasCodeLoc $loc 110 | * @param Expr\Expr $object 111 | * @param Expr\Expr $property 112 | */ 113 | public function __construct(HasCodeLoc $loc, Expr\Expr $object, Expr\Expr $property) { 114 | parent::__construct($loc); 115 | $this->object = $object; 116 | $this->property = $property; 117 | } 118 | 119 | public function isLValue():bool { 120 | return $this->object->isLValue(); 121 | } 122 | 123 | public function subStmts(bool $deep):array { 124 | return [$this->object, $this->property]; 125 | } 126 | 127 | public function unparseExpr():\PhpParser\Node\Expr { 128 | return new \PhpParser\Node\Expr\PropertyFetch( 129 | $this->object->unparseExpr(), 130 | $this->property->unparseExprOrString() 131 | ); 132 | } 133 | 134 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 135 | // TODO: Implement getType() method. 136 | } 137 | } 138 | 139 | class StaticProperty extends LValue { 140 | /** @var Expr\Expr */ 141 | private $class; 142 | /** @var Expr\Expr */ 143 | private $property; 144 | 145 | public function __construct(HasCodeLoc $loc, Expr\Expr $class, Expr\Expr $property) { 146 | parent::__construct($loc); 147 | $this->class = $class; 148 | $this->property = $property; 149 | } 150 | 151 | public function isLValue():bool { 152 | return true; 153 | } 154 | 155 | public function subStmts(bool $deep):array { 156 | return [$this->class, $this->property]; 157 | } 158 | 159 | public function unparseExpr():\PhpParser\Node\Expr { 160 | return new \PhpParser\Node\Expr\StaticPropertyFetch( 161 | $this->class->unparseExprOrName(), 162 | $this->property->unparseExprOrString() 163 | ); 164 | } 165 | 166 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 167 | // TODO: Implement getType() method. 168 | } 169 | } 170 | 171 | class ArrayAccess extends LValue { 172 | /** @var Expr\Expr */ 173 | private $array; 174 | /** @var Expr\Expr|null */ 175 | private $key; 176 | 177 | /** 178 | * @param HasCodeLoc $loc 179 | * @param Expr\Expr $array 180 | * @param Expr\Expr|null $key 181 | */ 182 | public function __construct(HasCodeLoc $loc, Expr\Expr $array, Expr\Expr $key = null) { 183 | parent::__construct($loc); 184 | $this->array = $array; 185 | $this->key = $key; 186 | } 187 | 188 | public function isLValue():bool { 189 | return $this->array->isLValue(); 190 | } 191 | 192 | public function subStmts(bool $deep):array { 193 | return $this->key ? [$this->array, $this->key] : [$this->array]; 194 | } 195 | 196 | public function unparseExpr():\PhpParser\Node\Expr { 197 | return new \PhpParser\Node\Expr\ArrayDimFetch( 198 | $this->array->unparseExpr(), 199 | $this->key ? $this->key->unparseExpr() : null 200 | ); 201 | } 202 | 203 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 204 | // TODO: Implement getType() method. 205 | } 206 | } 207 | 208 | class List_ extends Expr\Expr { 209 | /** @var (Expr|null)[] */ 210 | private $exprs; 211 | 212 | /** 213 | * @param HasCodeLoc $loc 214 | * @param (Expr|null)[] $exprs 215 | */ 216 | public function __construct(HasCodeLoc $loc, array $exprs) { 217 | parent::__construct($loc); 218 | $this->exprs = $exprs; 219 | } 220 | 221 | public function subStmts(bool $deep):array { 222 | $stmts = []; 223 | foreach ($this->exprs as $expr) { 224 | if ($expr) { 225 | $stmts[] = $expr; 226 | } 227 | } 228 | return $stmts; 229 | } 230 | 231 | public function unparseExpr():\PhpParser\Node\Expr { 232 | $exprs = []; 233 | /** @var Expr\Expr|null $expr */ 234 | foreach ($this->exprs as $expr) { 235 | $exprs[] = $expr ? $expr->unparseExpr() : null; 236 | } 237 | return new \PhpParser\Node\Expr\List_($exprs); 238 | } 239 | 240 | public function checkExpr(Context\Context $context, bool $noErrors):Type\Type { 241 | $items = []; 242 | foreach ($this->exprs as $k => $v) { 243 | if ($v) { 244 | $items[] = new Expr\ArrayItem($v, new Constants\Literal($v, $k), $v); 245 | } 246 | } 247 | return new Expr\Array_($this, $items); 248 | } 249 | } 250 | 251 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Stmt.php: -------------------------------------------------------------------------------- 1 | getLocals(); 34 | do { 35 | $added = false; 36 | $types = $this->inferLocals($context); 37 | foreach ($types as $name => $type) { 38 | // Ignore variables that were already declared at the outset 39 | if (isset($declared[$name])) { 40 | continue; 41 | } 42 | $type2 = $context->getLocal($name); 43 | if (!$type2) { 44 | // Add the variable if it doesn't exist 45 | $context->addLocal($name, $type); 46 | $added = true; 47 | } else if (!$type2->containsType($type, $context)) { 48 | // Merge it in if we have discovered new types for it 49 | $context->addLocal($name, $type->addType($type2, $context)); 50 | $added = true; 51 | } 52 | } 53 | } while ($added); 54 | } 55 | 56 | /** 57 | * Infer local variables based on direct assignments of the form "$foo = ...". "$context" will already have local 58 | * variables based on explicit "@var" doc comments, and parameters. 59 | * @param Context\Context $context 60 | * @return Type\Type[] 61 | */ 62 | protected function inferLocals(Context\Context $context):array { 63 | $types = []; 64 | foreach ($this->subStmts(false) as $stmt) { 65 | $types = merge_types($stmt->inferLocals($context), $types, $context); 66 | } 67 | return $types; 68 | } 69 | 70 | public final function namespaces():array { 71 | $namespaces = []; 72 | foreach ($this->subStmts(true) as $stmt) { 73 | if ($stmt instanceof Defns\HasNamespace) { 74 | $namespaces[] = $stmt->namespace_(); 75 | } 76 | } 77 | return array_unique($namespaces); 78 | } 79 | 80 | public function gatherGlobalDecls(Context\Context $context) { 81 | foreach ($this->subStmts(true) as $stmt) { 82 | $stmt->gatherGlobalDecls($context); 83 | } 84 | } 85 | 86 | public function gatherLocalDecls(Context\Context $context) { 87 | foreach ($this->subStmts(false) as $stmt) { 88 | $stmt->gatherLocalDecls($context); 89 | } 90 | } 91 | 92 | public function checkStmt(Context\Context $context) { 93 | foreach ($this->subStmts(true) as $stmt) { 94 | $stmt->checkStmt($context); 95 | } 96 | } 97 | } 98 | 99 | class Block extends Stmt { 100 | /** @var SingleStmt[] */ 101 | private $stmts; 102 | 103 | /** 104 | * @param HasCodeLoc $loc 105 | * @param SingleStmt[] $stmts 106 | */ 107 | public function __construct(HasCodeLoc $loc, array $stmts = []) { 108 | parent::__construct($loc); 109 | $this->stmts = $stmts; 110 | } 111 | 112 | /** 113 | * @return \PhpParser\Node[] 114 | * @throws \Exception 115 | */ 116 | public final function unparseWithNamespaces():array { 117 | $nodes = []; 118 | 119 | $currentNamespace = null; 120 | $currentNodes = []; 121 | 122 | foreach ($this->stmts as $stmt) { 123 | $namespaces = $stmt->namespaces(); 124 | if (count($namespaces) > 1) { 125 | throw new \Exception('Cant unparse single statement defining symbols in multiple namespaces'); 126 | } 127 | 128 | $stmtNode = $stmt->unparseStmt(); 129 | $stmtNamespace = $namespaces ? $namespaces[0] : null; 130 | 131 | if ($stmtNode) { 132 | if ($stmtNamespace === null) { 133 | $currentNodes[] = $stmtNode; 134 | } else if ( 135 | $currentNamespace !== null && 136 | $stmtNamespace !== $currentNamespace 137 | ) { 138 | $nodes[] = new \PhpParser\Node\Stmt\Namespace_( 139 | $currentNamespace ? new \PhpParser\Node\Name($currentNamespace) : null, 140 | $currentNodes 141 | ); 142 | 143 | $currentNamespace = $stmtNamespace; 144 | $currentNodes = [$stmtNode]; 145 | } else { 146 | $currentNamespace = $stmtNamespace; 147 | $currentNodes[] = $stmtNode; 148 | } 149 | } 150 | } 151 | 152 | if ($currentNodes) { 153 | $nodes[] = new \PhpParser\Node\Stmt\Namespace_( 154 | $currentNamespace ? new \PhpParser\Node\Name($currentNamespace) : null, 155 | $currentNodes 156 | ); 157 | } 158 | 159 | if (count($nodes) == 1) { 160 | $node = $nodes[0]; 161 | if ($node instanceof \PhpParser\Node\Stmt\Namespace_ && !$node->name) { 162 | $nodes = $node->stmts; 163 | } 164 | } 165 | 166 | return $nodes; 167 | } 168 | 169 | public function split():array { 170 | $result = []; 171 | foreach ($this->stmts as $stmt) { 172 | foreach ($stmt->split() as $stmt_) { 173 | $result[] = $stmt_; 174 | } 175 | } 176 | return $result; 177 | } 178 | 179 | public function add(SingleStmt $stmt) { 180 | $this->stmts[] = $stmt; 181 | } 182 | 183 | public function subStmts(bool $deep):array { 184 | return $this->stmts; 185 | } 186 | 187 | public function unparseNodes():array { 188 | $nodes = []; 189 | foreach ($this->stmts as $stmt) { 190 | $node = $stmt->unparseStmt(); 191 | if ($node) { 192 | $nodes[] = $node; 193 | } 194 | } 195 | return $nodes; 196 | } 197 | } 198 | 199 | /** 200 | * A statement that isn't a block. 201 | */ 202 | abstract class SingleStmt extends Stmt { 203 | public final function split():array { 204 | return [$this]; 205 | } 206 | 207 | /** 208 | * @return \PhpParser\Node|null 209 | */ 210 | public abstract function unparseStmt(); 211 | } 212 | 213 | class Return_ extends SingleStmt { 214 | /** @var Expr\Expr|null */ 215 | private $expr; 216 | 217 | public function __construct(HasCodeLoc $loc, Expr\Expr $expr = null) { 218 | parent::__construct($loc); 219 | $this->expr = $expr; 220 | } 221 | 222 | public function subStmts(bool $deep):array { 223 | return $this->expr ? [$this->expr] : []; 224 | } 225 | 226 | public function unparseStmt() { 227 | return new \PhpParser\Node\Stmt\Return_($this->expr ? $this->expr->unparseExpr() : null); 228 | } 229 | } 230 | 231 | class InlineHTML extends SingleStmt { 232 | /** @var string */ 233 | private $html; 234 | 235 | public function __construct(HasCodeLoc $loc, string $html) { 236 | parent::__construct($loc); 237 | $this->html = $html; 238 | } 239 | 240 | public function subStmts(bool $deep):array { 241 | return []; 242 | } 243 | 244 | public function unparseStmt() { 245 | return new \PhpParser\Node\Stmt\InlineHTML($this->html); 246 | } 247 | } 248 | 249 | class Echo_ extends SingleStmt { 250 | /** @var Expr\Expr[] */ 251 | private $exprs; 252 | 253 | /** 254 | * @param HasCodeLoc $loc 255 | * @param Expr\Expr[] $exprs 256 | */ 257 | public function __construct(HasCodeLoc $loc, array $exprs) { 258 | parent::__construct($loc); 259 | $this->exprs = $exprs; 260 | } 261 | 262 | public function subStmts(bool $deep):array { 263 | return $this->exprs; 264 | } 265 | 266 | public function unparseStmt() { 267 | $exprs = []; 268 | foreach ($this->exprs as $expr) { 269 | $exprs[] = $expr->unparseExpr(); 270 | } 271 | return new \PhpParser\Node\Stmt\Echo_($exprs); 272 | } 273 | } 274 | 275 | class Throw_ extends SingleStmt { 276 | /** @var Expr\Expr */ 277 | private $expr; 278 | 279 | /** 280 | * @param HasCodeLoc $loc 281 | * @param Expr\Expr $expr 282 | */ 283 | public function __construct(HasCodeLoc $loc, Expr\Expr $expr) { 284 | parent::__construct($loc); 285 | $this->expr = $expr; 286 | } 287 | 288 | public function subStmts(bool $deep):array { 289 | return [$this->expr]; 290 | } 291 | 292 | public function unparseStmt() { 293 | return new \PhpParser\Node\Stmt\Throw_( 294 | $this->expr->unparseExpr() 295 | ); 296 | } 297 | } 298 | 299 | class StaticVar extends SingleStmt { 300 | /** @var string */ 301 | private $name; 302 | /** @var Expr\Expr|null */ 303 | private $value; 304 | 305 | /** 306 | * @param HasCodeLoc $loc 307 | * @param string $name 308 | * @param Expr\Expr|null $value 309 | */ 310 | public function __construct(HasCodeLoc $loc, string $name, Expr\Expr $value = null) { 311 | parent::__construct($loc); 312 | $this->name = $name; 313 | $this->value = $value; 314 | } 315 | 316 | public function subStmts(bool $deep):array { 317 | return $this->value ? [$this->value] : []; 318 | } 319 | 320 | protected function inferLocals(Context\Context $context):array { 321 | $locals = parent::inferLocals($context); 322 | $value = $this->value; 323 | if ($value) { 324 | return merge_types($locals, [$this->name => $value->checkExpr($context, true)], $context); 325 | } else { 326 | return $locals; 327 | } 328 | } 329 | 330 | public function unparseStmt() { 331 | return new \PhpParser\Node\Stmt\Static_([ 332 | new \PhpParser\Node\Stmt\StaticVar( 333 | $this->name, 334 | $this->value ? $this->value->unparseExpr() : null 335 | ), 336 | ]); 337 | } 338 | } 339 | 340 | class Break_ extends SingleStmt { 341 | /** @var int */ 342 | private $levels; 343 | 344 | /** 345 | * @param HasCodeLoc $loc 346 | * @param int $levels 347 | */ 348 | public function __construct(HasCodeLoc $loc, int $levels = 1) { 349 | parent::__construct($loc); 350 | $this->levels = $levels; 351 | } 352 | 353 | public function subStmts(bool $deep):array { 354 | return []; 355 | } 356 | 357 | public function unparseStmt() { 358 | $levels = $this->levels == 1 359 | ? null 360 | : new \PhpParser\Node\Scalar\LNumber($this->levels); 361 | return new \PhpParser\Node\Stmt\Break_($levels); 362 | } 363 | } 364 | 365 | class Continue_ extends SingleStmt { 366 | /** @var int */ 367 | private $levels; 368 | 369 | /** 370 | * @param HasCodeLoc $loc 371 | * @param int $levels 372 | */ 373 | public function __construct(HasCodeLoc $loc, int $levels) { 374 | parent::__construct($loc); 375 | $this->levels = $levels; 376 | } 377 | 378 | public function subStmts(bool $deep):array { 379 | return []; 380 | } 381 | 382 | public function unparseStmt() { 383 | $levels = $this->levels == 1 384 | ? null 385 | : new \PhpParser\Node\Scalar\LNumber($this->levels); 386 | return new \PhpParser\Node\Stmt\Continue_($levels); 387 | } 388 | } 389 | 390 | class Unset_ extends SingleStmt { 391 | /** @var Expr\Expr[] */ 392 | private $exprs; 393 | 394 | /** 395 | * @param HasCodeLoc $loc 396 | * @param Expr\Expr[] $exprs 397 | */ 398 | public function __construct(HasCodeLoc $loc, array $exprs) { 399 | parent::__construct($loc); 400 | $this->exprs = $exprs; 401 | } 402 | 403 | public function subStmts(bool $deep):array { 404 | return $this->exprs; 405 | } 406 | 407 | public function unparseStmt() { 408 | $exprs = []; 409 | foreach ($this->exprs as $expr) { 410 | $exprs[] = $expr->unparseExpr(); 411 | } 412 | return new \PhpParser\Node\Stmt\Unset_($exprs); 413 | } 414 | } 415 | 416 | class Global_ extends SingleStmt { 417 | /** @var Expr\Expr */ 418 | private $expr; 419 | 420 | /** 421 | * @param HasCodeLoc $loc 422 | * @param Expr\Expr $expr 423 | */ 424 | public function __construct(HasCodeLoc $loc, Expr\Expr $expr) { 425 | parent::__construct($loc); 426 | $this->expr = $expr; 427 | } 428 | 429 | public function subStmts(bool $deep):array { 430 | return [$this->expr]; 431 | } 432 | 433 | public function unparseStmt() { 434 | return new \PhpParser\Node\Stmt\Global_([ 435 | $this->expr->unparseExpr(), 436 | ]); 437 | } 438 | 439 | protected function inferLocals(Context\Context $context):array { 440 | $locals = []; 441 | foreach ($this->expr->checkExpr($context, false)->getStringValues($context) as $string) { 442 | $global = $context->getGlobal($string); 443 | if ($global && !$global->isEmpty()) { 444 | $locals[$string] = $global; 445 | } 446 | } 447 | return merge_types(parent::inferLocals($context), $locals, $context); 448 | } 449 | } 450 | 451 | class Goto_ extends SingleStmt { 452 | /** @var string */ 453 | private $name; 454 | 455 | public function __construct(HasCodeLoc $loc, string $name) { 456 | parent::__construct($loc); 457 | $this->name = $name; 458 | } 459 | 460 | public function subStmts(bool $deep):array { 461 | return []; 462 | } 463 | 464 | public function unparseStmt() { 465 | return new \PhpParser\Node\Stmt\Goto_($this->name); 466 | } 467 | 468 | public function checkStmt(Context\Context $context) { 469 | if (!$context->hasLabel($this->name)) { 470 | $context->addError("Undefined label '$this->name'", $this); 471 | } 472 | parent::checkStmt($context); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Test.php: -------------------------------------------------------------------------------- 1 | $contents])); 128 | } 129 | } -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/Type.php: -------------------------------------------------------------------------------- 1 | subtractType(self::falsy($this), $ctx); 91 | } 92 | 93 | public final function removeNull(Context\Context $ctx):self { 94 | return $this->subtractType(new SingleValue($this, null), $ctx); 95 | } 96 | 97 | /** 98 | * @return null|\PhpParser\Node\Name|string 99 | */ 100 | public function toTypeHint() { 101 | return null; 102 | } 103 | 104 | public abstract function toString(bool $atomic = false):string; 105 | 106 | public abstract function containsType(Type $type, Context\Context $ctx):bool; 107 | 108 | /** 109 | * @return SingleType[] 110 | */ 111 | public abstract function split():array; 112 | 113 | public final function __toString():string { 114 | return $this->toString(false); 115 | } 116 | 117 | public final function isEmpty():bool { 118 | return count($this->split()) == 0; 119 | } 120 | 121 | public final function isEquivelant(self $type, Context\Context $ctx):bool { 122 | return 123 | $type->containsType($this, $ctx) && 124 | $this->containsType($type, $ctx); 125 | } 126 | 127 | /** 128 | * Removes any redundant types inside unions. 129 | * @param Context\Context|null $ctx_ 130 | * @return Type 131 | */ 132 | public final function simplify(Context\Context $ctx_ = null):self { 133 | $ctx = $ctx_ ?? new Context\Context(new NullErrorReceiver()); 134 | $union = new Union($this); 135 | foreach ($this->split() as $type) { 136 | $union = $union->addType($type, $ctx); 137 | } 138 | return $union; 139 | } 140 | 141 | public final function addType(self $other, Context\Context $ctx):self { 142 | if ($this->containsType($other, $ctx)) { 143 | return $this; 144 | } else { 145 | return self::union($this, array_merge( 146 | $this->subtractType($other, $ctx)->split(), 147 | $this->split() 148 | )); 149 | } 150 | } 151 | 152 | public final function subtractType(self $other, Context\Context $ctx):self { 153 | $types = []; 154 | foreach ($this->split() as $t) { 155 | if (!$other->containsType($t, $ctx)) { 156 | $types[] = $t; 157 | } 158 | } 159 | return self::union($this, $types); 160 | } 161 | 162 | public final function checkAgainst(HasCodeLoc $loc, self $that, Context\Context $context) { 163 | $that->checkContains($loc, $this, $context); 164 | } 165 | 166 | public final function checkContains(HasCodeLoc $loc, self $that, Context\Context $context) { 167 | if (!$this->containsType($that, $context)) { 168 | $context->addError("$that is incompatible with $this", $loc); 169 | } 170 | } 171 | 172 | public final function checkEquivelant(HasCodeLoc $loc, self $that, Context\Context $context) { 173 | if (!$this->isEquivelant($that, $context)) { 174 | $context->addError("$that is not requivelant to $this", $loc); 175 | } 176 | } 177 | 178 | /** 179 | * Replace type vars with specific types. Used on the result of a method call to replace "$this" and "static" with 180 | * the class the method was called on, so method chaining works properly. 181 | * @param self[] $vars 182 | * @param Context\Context $ctx 183 | * @return Type 184 | */ 185 | public function fillTypeVars(array $vars, Context\Context $ctx):self { 186 | return $this; 187 | } 188 | 189 | public function isCallableMethodOf(Type $type, Context\Context $ctx):bool { 190 | return false; 191 | } 192 | 193 | public function hasCallableMethod(string $method, Context\Context $ctx):bool { 194 | return false; 195 | } 196 | 197 | public function getKnownArrayKey(string $key, HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 198 | return $this->getUnknownArrayKey($loc, $context, $noErrors); 199 | } 200 | 201 | public function getUnknownArrayKey(HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 202 | return $context->addError("Cannot use {$this->toString()} as array", $loc); 203 | } 204 | 205 | public function useAsArrayKey(HasCodeLoc $loc, Type $array, Context\Context $context, bool $noErrors):Type { 206 | if (!$noErrors) { 207 | // By default, a type is not allowed to be used as an array key 208 | $context->addError("Cannot use {$this->toString()} as array key", $loc); 209 | } 210 | return $array->getUnknownArrayKey($loc, $context, $noErrors); 211 | } 212 | 213 | public function isCallable(Context\Context $ctx):bool { 214 | return false; 215 | } 216 | 217 | /** 218 | * @param int|string|bool|float|null $value 219 | * @return bool 220 | */ 221 | public function isSingleValue($value):bool { 222 | return false; 223 | } 224 | 225 | public function isString():bool { 226 | return false; 227 | } 228 | 229 | public function isFloat():bool { 230 | return false; 231 | } 232 | 233 | public function isNull():bool { 234 | return false; 235 | } 236 | 237 | public function isBool():bool { 238 | return false; 239 | } 240 | 241 | public function isInt():bool { 242 | return false; 243 | } 244 | 245 | public function isResource():bool { 246 | return false; 247 | } 248 | 249 | public function isFalsy():bool { 250 | return false; 251 | } 252 | 253 | public function isTruthy():bool { 254 | return false; 255 | } 256 | 257 | public function isArrayOf(Type $type, Context\Context $ctx):bool { 258 | return false; 259 | } 260 | 261 | /** 262 | * @param Type[] $keys 263 | * @param Context\Context $ctx 264 | * @return bool 265 | */ 266 | public function isShape(array $keys, Context\Context $ctx):bool { 267 | return false; 268 | } 269 | 270 | public function isObject():bool { 271 | return false; 272 | } 273 | 274 | public function isClass(string $class, Context\Context $ctx):bool { 275 | return false; 276 | } 277 | 278 | public function isTypeVar(string $var):bool { 279 | return false; 280 | } 281 | 282 | public function addArrayKey(HasCodeLoc $loc, Context\Context $context, Type $type, bool $noErrors):Type { 283 | if (!$noErrors) { 284 | $context->addError("Cannot use $this as array", $loc); 285 | } 286 | return $this; 287 | } 288 | 289 | public function setArrayKey(HasCodeLoc $loc, Context\Context $context, string $key, Type $type, bool $noErrors):Type { 290 | if (!$noErrors) { 291 | $context->addError("Cannot use $this as array", $loc); 292 | } 293 | return $this; 294 | } 295 | 296 | public function useToSetArrayKey(HasCodeLoc $loc, Context\Context $context, Type $array, Type $value, bool $noErrors):Type { 297 | if (!$noErrors) { 298 | $context->addError("Cannot use $this as array key", $loc); 299 | } 300 | return $array; 301 | } 302 | 303 | /** 304 | * @param HasCodeLoc $loc 305 | * @param Context\Context $context 306 | * @param Call\EvaledCallArg[] $args 307 | * @param bool $noErrors 308 | * @return Type 309 | */ 310 | public function call(HasCodeLoc $loc, Context\Context $context, array $args, bool $noErrors):self { 311 | return $context->addError("Cannot call $this as a function", $this); 312 | } 313 | 314 | /** 315 | * Shortcut for `->isEquivelantTo(new Mixed())` that doesn't need a context. 316 | * @return bool 317 | */ 318 | public function isExactlyMixed():bool { 319 | return false; 320 | } 321 | 322 | public function useAsVariableName(HasCodeLoc $loc, Context\Context $context):Type { 323 | return $context->addError("$this cannot be used as the name of a variable", $loc); 324 | } 325 | 326 | public function doForeach(HasCodeLoc $loc, Context\Context $context):ForeachResult { 327 | $type = $context->addError("$this cannot be used in 'foreach' or ... (unpack).", $loc); 328 | return new ForeachResult($type, $type); 329 | } 330 | 331 | /** 332 | * @param Context\Context $context 333 | * @return string[] 334 | */ 335 | public function getStringValues(Context\Context $context):array { 336 | return []; 337 | } 338 | 339 | public function doBinOp(HasCodeLoc $loc, Type $rhs, Expr\BinOpType $type, Context\Context $context, bool $noErrors):Type { 340 | if ($noErrors) { 341 | return Type::none($loc); 342 | } else { 343 | return $context->addError("Cannot evaluate $this $type $rhs", $loc); 344 | } 345 | } 346 | 347 | public function doBinOpSingleValue(HasCodeLoc $loc, $lhs, Expr\BinOpType $type, Context\Context $context, bool $noErrors):Type { 348 | // TODO 349 | } 350 | 351 | public function doCast(HasCodeLoc $loc, Expr\CastType $type, Context\Context $context, bool $noErrors):Type { 352 | // Cast to "unset" is allowed for everything 353 | if ($type->value() !== Expr\CastType::UNSET && !$noErrors) { 354 | $context->addError("Cannot cast $this to '$type'", $loc); 355 | } 356 | return $type->toType($loc); 357 | } 358 | } 359 | 360 | class ForeachResult { 361 | /** @var Type */ 362 | public $key; 363 | /** @var Type */ 364 | public $val; 365 | 366 | public function __construct(Type $key, Type $val) { 367 | $this->key = $key; 368 | $this->val = $val; 369 | } 370 | } 371 | 372 | class Union extends Type { 373 | /** @var SingleType[] */ 374 | private $types = []; 375 | 376 | /** 377 | * @param HasCodeLoc $loc 378 | * @param SingleType[] $types 379 | */ 380 | public function __construct(HasCodeLoc $loc, array $types = []) { 381 | parent::__construct($loc); 382 | $this->types = $types; 383 | } 384 | 385 | public function toTypeHint() { 386 | $types = $this->simplify()->split(); 387 | if (count($types) == 1) { 388 | return $types[0]->toTypeHint(); 389 | } else { 390 | return null; 391 | } 392 | } 393 | 394 | public function containsType(Type $type, Context\Context $ctx):bool { 395 | return $this->any(function (SingleType $t) use ($type, $ctx) { 396 | return $t->containsType($type, $ctx); 397 | }); 398 | } 399 | 400 | /** @return SingleType[] */ 401 | public function split():array { 402 | return $this->types; 403 | } 404 | 405 | public final function toString(bool $atomic = false):string { 406 | $parts = []; 407 | foreach ($this->simplify()->split() as $type) { 408 | $parts[$type->toString(false)] = true; 409 | } 410 | switch (count($parts)) { 411 | case 0: 412 | return '()'; 413 | case 1: 414 | return array_keys($parts)[0]; 415 | default: 416 | // Convert true|false to bool 417 | if ( 418 | isset($parts['true']) && 419 | isset($parts['false']) 420 | ) { 421 | $parts['bool'] = true; 422 | unset($parts['true']); 423 | unset($parts['false']); 424 | } 425 | 426 | ksort($parts, SORT_STRING); 427 | $join = join('|', array_keys($parts)); 428 | return $atomic ? "($join)" : $join; 429 | } 430 | } 431 | 432 | public function call(HasCodeLoc $loc, Context\Context $context, array $args, bool $noErrors):Type { 433 | return $this->map(function (Type $t) use ($loc, $context, $args, $noErrors) { 434 | return $t->call($loc, $context, $args, $noErrors); 435 | }); 436 | } 437 | 438 | public function getStringValues(Context\Context $context):array { 439 | $strings = []; 440 | foreach ($this->types as $t) { 441 | foreach ($t->getStringValues($context) as $string) { 442 | $strings[] = $string; 443 | } 444 | } 445 | return $strings; 446 | } 447 | 448 | public function useAsVariableName(HasCodeLoc $loc, Context\Context $context):Type { 449 | return $this->map(function (Type $t) use ($loc, $context) { 450 | return $t->useAsVariableName($loc, $context); 451 | }); 452 | } 453 | 454 | public function useToSetArrayKey(HasCodeLoc $loc, Context\Context $context, Type $array, Type $value, bool $noErrors):Type { 455 | return $this->map(function (Type $t) use ($loc, $context, $array, $value, $noErrors) { 456 | return $t->useToSetArrayKey($loc, $context, $array, $value, $noErrors); 457 | }); 458 | } 459 | 460 | public function addArrayKey(HasCodeLoc $loc, Context\Context $context, Type $type, bool $noErrors):Type { 461 | return $this->map(function (Type $t) use ($loc, $context, $type, $noErrors) { 462 | return $t->addArrayKey($loc, $context, $type, $noErrors); 463 | }); 464 | } 465 | 466 | public function setArrayKey(HasCodeLoc $loc, Context\Context $context, string $key, Type $type, bool $noErrors):Type { 467 | return $this->map(function (Type $t) use ($loc, $context, $key, $type, $noErrors) { 468 | return $t->setArrayKey($loc, $context, $key, $type, $noErrors); 469 | }); 470 | } 471 | 472 | public function fillTypeVars(array $vars, Context\Context $ctx):Type { 473 | return $this->map(function (Type $t) use ($vars, $ctx) { 474 | return $t->fillTypeVars($vars, $ctx); 475 | }); 476 | } 477 | 478 | public function isCallableMethodOf(Type $type, Context\Context $ctx):bool { 479 | return $this->all(function (SingleType $t) use ($type, $ctx) { 480 | return $t->isCallableMethodOf($type, $ctx); 481 | }); 482 | } 483 | 484 | public function hasCallableMethod(string $method, Context\Context $ctx):bool { 485 | return $this->all(function (SingleType $t) use ($method, $ctx) { 486 | return $t->hasCallableMethod($method, $ctx); 487 | }); 488 | } 489 | 490 | public function useAsArrayKey(HasCodeLoc $loc, Type $array, Context\Context $context, bool $noErrors):Type { 491 | return $this->map(function (Type $t) use ($loc, $array, $context, $noErrors) { 492 | return $t->useAsArrayKey($loc, $array, $context, $noErrors); 493 | }); 494 | } 495 | 496 | public function isExactlyMixed():bool { 497 | return $this->all(function (Type $t) { 498 | return $t->isExactlyMixed(); 499 | }); 500 | } 501 | 502 | public function isCallable(Context\Context $ctx):bool { 503 | return $this->all(function (SingleType $t) use ($ctx) { 504 | return $t->isCallable($ctx); 505 | }); 506 | } 507 | 508 | public function doForeach(HasCodeLoc $loc, Context\Context $context):ForeachResult { 509 | $empty = new Union($loc); 510 | $result = new ForeachResult($empty, $empty); 511 | foreach ($this->types as $type) { 512 | $foreach = $type->doForeach($loc, $context); 513 | 514 | $result->key = $result->key->addType($foreach->key, $context); 515 | $result->val = $result->val->addType($foreach->val, $context); 516 | } 517 | return $result; 518 | } 519 | 520 | public function getKnownArrayKey(string $key, HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 521 | return $this->map(function (Type $t) use ($key, $loc, $context, $noErrors) { 522 | return $t->getKnownArrayKey($key, $loc, $context, $noErrors); 523 | }); 524 | } 525 | 526 | public function getUnknownArrayKey(HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 527 | return $this->map(function (Type $t) use ($loc, $context, $noErrors) { 528 | return $t->getUnknownArrayKey($loc, $context, $noErrors); 529 | }); 530 | } 531 | 532 | public function isSingleValue($value):bool { 533 | return $this->all(function (SingleType $t) use ($value) { 534 | return $t->isSingleValue($value); 535 | }); 536 | } 537 | 538 | public function isString():bool { 539 | return $this->all(function (SingleType $t) { 540 | return $t->isString(); 541 | }); 542 | } 543 | 544 | public function isFloat():bool { 545 | return $this->all(function (SingleType $t) { 546 | return $t->isFloat(); 547 | }); 548 | } 549 | 550 | public function isNull():bool { 551 | return $this->all(function (SingleType $t) { 552 | return $t->isNull(); 553 | }); 554 | } 555 | 556 | public function isFalsy():bool { 557 | return $this->all(function (SingleType $t) { 558 | return $t->isFalsy(); 559 | }); 560 | } 561 | 562 | public function isTruthy():bool { 563 | return $this->all(function (SingleType $t) { 564 | return $t->isTruthy(); 565 | }); 566 | } 567 | 568 | public function isBool():bool { 569 | return $this->all(function (SingleType $t) { 570 | return $t->isBool(); 571 | }); 572 | } 573 | 574 | public function isInt():bool { 575 | return $this->all(function (SingleType $t) { 576 | return $t->isInt(); 577 | }); 578 | } 579 | 580 | public function isResource():bool { 581 | return $this->all(function (SingleType $t) { 582 | return $t->isResource(); 583 | }); 584 | } 585 | 586 | public function isArrayOf(Type $type, Context\Context $ctx):bool { 587 | return $this->all(function (SingleType $t) use ($type, $ctx) { 588 | return $t->isArrayOf($type, $ctx); 589 | }); 590 | } 591 | 592 | public function isShape(array $keys, Context\Context $ctx):bool { 593 | return $this->all(function (SingleType $t) use ($keys, $ctx) { 594 | return $t->isShape($keys, $ctx); 595 | }); 596 | } 597 | 598 | public function isObject():bool { 599 | return $this->all(function (SingleType $t) { 600 | return $t->isObject(); 601 | }); 602 | } 603 | 604 | public function isClass(string $class, Context\Context $ctx):bool { 605 | return $this->all(function (SingleType $t) use ($class, $ctx) { 606 | return $t->isClass($class, $ctx); 607 | }); 608 | } 609 | 610 | public function all(callable $f):bool { 611 | foreach ($this->types as $t) { 612 | if (!$f($t)) { 613 | return false; 614 | } 615 | } 616 | return true; 617 | } 618 | 619 | public function any(callable $f):bool { 620 | foreach ($this->types as $t) { 621 | if ($f($t)) { 622 | return true; 623 | } 624 | } 625 | return false; 626 | } 627 | 628 | public function map(callable $f):Type { 629 | $types = []; 630 | foreach ($this->types as $t) { 631 | /** @var Type $type2 */ 632 | $type2 = $f($t); 633 | foreach ($type2->split() as $type) { 634 | $types[] = $type; 635 | } 636 | } 637 | return $types; 638 | } 639 | 640 | public function isTypeVar(string $var):bool { 641 | return $this->all(function (SingleType $t) use ($var) { 642 | return $t->isTypeVar($var); 643 | }); 644 | } 645 | } 646 | 647 | /** 648 | * A type that is not a union. 649 | */ 650 | abstract class SingleType extends Type { 651 | public final function split():array { 652 | return [$this]; 653 | } 654 | } 655 | 656 | /** 657 | * Used for generics. Even though PHP/PhpDoc don't support generics, the '$this' and 'static' types are effectively a 658 | * kind of type parameter and should be modelled as such. 659 | */ 660 | class TypeVar extends SingleType { 661 | const THIS = '$this'; 662 | const STATIC = 'static'; 663 | 664 | /** @var string */ 665 | private $var; 666 | /** 667 | * @var Type The type the type var is contained by, i.e. "Foo" in "class Blah". Can be "mixed" 668 | * if not specified. This doesn't actually have to be stored here, a map from type vars to constraining 669 | * types could be stored somewhere else, but it's more convenient here. 670 | */ 671 | private $type; 672 | 673 | public function __construct(HasCodeLoc $loc, string $var, Type $type) { 674 | parent::__construct($loc); 675 | $this->var = $var; 676 | $this->type = $type; 677 | } 678 | 679 | public function toTypeHint() { 680 | if ($this->var === self::STATIC) { 681 | return 'static'; 682 | } else { 683 | return $this->type->toTypeHint(); 684 | } 685 | } 686 | 687 | public function toString(bool $atomic = false):string { 688 | return $this->var; 689 | } 690 | 691 | public function getKnownArrayKey(string $key, HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 692 | return $this->type->getKnownArrayKey($key, $loc, $context, $noErrors); 693 | } 694 | 695 | public function getUnknownArrayKey(HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 696 | return $this->type->getUnknownArrayKey($loc, $context, $noErrors); 697 | } 698 | 699 | public function getStringValues(Context\Context $context):array { 700 | return $this->type->getStringValues($context); 701 | } 702 | 703 | public function useAsVariableName(HasCodeLoc $loc, Context\Context $context):Type { 704 | return $this->type->useAsVariableName($loc, $context); 705 | } 706 | 707 | public function useToSetArrayKey(HasCodeLoc $loc, Context\Context $context, Type $array, Type $value, bool $noErrors):Type { 708 | return $this->type->useToSetArrayKey($loc, $context, $array, $value, $noErrors); 709 | } 710 | 711 | public function call(HasCodeLoc $loc, Context\Context $context, array $args, bool $noErrors):Type { 712 | return $this->type->call($loc, $context, $args, $noErrors); 713 | } 714 | 715 | public function isExactlyMixed():bool { 716 | return false; 717 | } 718 | 719 | public function isFalsy():bool { 720 | return $this->type->isFalsy(); 721 | } 722 | 723 | public function isTruthy():bool { 724 | return $this->type->isTruthy(); 725 | } 726 | 727 | public function containsType(Type $type, Context\Context $ctx):bool { 728 | return $type->isTypeVar($this->var); 729 | } 730 | 731 | public function isTypeVar(string $var):bool { 732 | return $this->var === $var; 733 | } 734 | 735 | public function addArrayKey(HasCodeLoc $loc, Context\Context $context, Type $type, bool $noErrors):Type { 736 | return $this->type->addArrayKey($loc, $context, $type, $noErrors); 737 | } 738 | 739 | public function setArrayKey(HasCodeLoc $loc, Context\Context $context, string $key, Type $type, bool $noErrors):Type { 740 | return $this->type->setArrayKey($loc, $context, $key, $type, $noErrors); 741 | } 742 | 743 | public function doForeach(HasCodeLoc $loc, Context\Context $context):ForeachResult { 744 | return $this->type->doForeach($loc, $context); 745 | } 746 | 747 | public function useAsArrayKey(HasCodeLoc $loc, Type $array, Context\Context $context, bool $noErrors):Type { 748 | return $this->type->useAsArrayKey($loc, $array, $context, $noErrors); 749 | } 750 | 751 | public function isCallable(Context\Context $ctx):bool { 752 | return $this->type->isCallable($ctx); 753 | } 754 | 755 | public function isSingleValue($value):bool { 756 | return $this->type->isSingleValue($value); 757 | } 758 | 759 | public function isString():bool { 760 | return $this->type->isString(); 761 | } 762 | 763 | public function isFloat():bool { 764 | return $this->type->isFloat(); 765 | } 766 | 767 | public function isNull():bool { 768 | return $this->type->isNull(); 769 | } 770 | 771 | public function isBool():bool { 772 | return $this->type->isBool(); 773 | } 774 | 775 | public function isInt():bool { 776 | return $this->type->isInt(); 777 | } 778 | 779 | public function isResource():bool { 780 | return $this->type->isResource(); 781 | } 782 | 783 | public function isArrayOf(Type $type, Context\Context $ctx):bool { 784 | return $this->type->isArrayOf($type, $ctx); 785 | } 786 | 787 | public function isShape(array $keys, Context\Context $ctx):bool { 788 | return $this->type->isShape($keys, $ctx); 789 | } 790 | 791 | public function isObject():bool { 792 | return $this->type->isObject(); 793 | } 794 | 795 | public function isClass(string $class, Context\Context $ctx):bool { 796 | return $this->type->isClass($class, $ctx); 797 | } 798 | 799 | public function fillTypeVars(array $vars, Context\Context $ctx):Type { 800 | return $vars[$this->var] ?? $this; 801 | } 802 | 803 | public function isCallableMethodOf(Type $type, Context\Context $ctx):bool { 804 | return $this->type->isCallableMethodOf($type, $ctx); 805 | } 806 | 807 | public function hasCallableMethod(string $method, Context\Context $ctx):bool { 808 | return $this->type->hasCallableMethod($method, $ctx); 809 | } 810 | } 811 | 812 | class Mixed extends SingleType { 813 | public function toString(bool $atomic = false):string { 814 | return 'mixed'; 815 | } 816 | 817 | public function containsType(Type $type, Context\Context $ctx):bool { 818 | return true; 819 | } 820 | 821 | public function isExactlyMixed():bool { 822 | return true; 823 | } 824 | } 825 | 826 | class SingleValue extends SingleType { 827 | /** @var int|string|bool|float|null */ 828 | private $value; 829 | 830 | /** 831 | * @param HasCodeLoc $loc 832 | * @param int|string|float|bool|null $value 833 | */ 834 | public function __construct(HasCodeLoc $loc, $value) { 835 | parent::__construct($loc); 836 | $this->value = $value; 837 | } 838 | 839 | public function toTypeHint() { 840 | switch (true) { 841 | case $this->isFloat(): 842 | return 'float'; 843 | case $this->isInt(): 844 | return 'int'; 845 | case $this->isBool(): 846 | return 'bool'; 847 | case $this->isString(): 848 | return 'string'; 849 | case $this->isNull(): 850 | // "void" when PHP gets void return types and the type hint is for a return type 851 | return null; 852 | default: 853 | throw new \Exception('Invalid type for SingleValue: ' . gettype($this->value)); 854 | } 855 | } 856 | 857 | public function toString(bool $atomic = false):string { 858 | if ($this->value === null) { 859 | return 'null'; 860 | } else if (is_bool($this->value)) { 861 | return $this->value ? 'true' : 'false'; 862 | } else { 863 | $result = var_export($this->value, true); 864 | // Make sure a float has a decimal point 865 | if ($this->isFloat() && strpos($result, '.') === false) { 866 | $result .= '.0'; 867 | } 868 | return $result; 869 | } 870 | } 871 | 872 | public function isCallable(Context\Context $ctx):bool { 873 | $parts = explode('::', (string)$this->value, 2); 874 | switch (count($parts)) { 875 | case 1: 876 | return $ctx->functionExists($parts[0]); 877 | case 2: 878 | return $ctx->methodExists($parts[0], $parts[1], true); 879 | default: 880 | return false; 881 | } 882 | } 883 | 884 | public function isSingleValue($value):bool { 885 | return $this->value === $value; 886 | } 887 | 888 | public function containsType(Type $type, Context\Context $ctx):bool { 889 | return $type->isSingleValue($this->value); 890 | } 891 | 892 | public function isInt():bool { 893 | return is_int($this->value); 894 | } 895 | 896 | public function isFloat():bool { 897 | return is_float($this->value); 898 | } 899 | 900 | public function isBool():bool { 901 | return is_bool($this->value); 902 | } 903 | 904 | public function isString():bool { 905 | return is_string($this->value); 906 | } 907 | 908 | public function isNull():bool { 909 | return is_null($this->value); 910 | } 911 | 912 | public function isCallableMethodOf(Type $type, Context\Context $ctx):bool { 913 | return $type->hasCallableMethod((string)$this->value, $ctx); 914 | } 915 | 916 | public function hasCallableMethod(string $method, Context\Context $ctx):bool { 917 | return $ctx->methodExists((string)$this->value, $method, true); 918 | } 919 | 920 | public function useAsArrayKey(HasCodeLoc $loc, Type $array, Context\Context $context, bool $noErrors):Type { 921 | return $array->getKnownArrayKey(PhpTypeChecker\to_array_key($this->value), $loc, $context, $noErrors); 922 | } 923 | 924 | public function doBinOp(HasCodeLoc $loc, Type $rhs, Expr\BinOpType $type, Context\Context $context, bool $noErrors):Type { 925 | return $rhs->doBinOpSingleValue($loc, $this->value, $type, $context, $noErrors); 926 | } 927 | 928 | public function doBinOpSingleValue(HasCodeLoc $loc, $lhs, Expr\BinOpType $type, Context\Context $context, bool $noErrors):Type { 929 | return new self($loc, $type->evaluate($lhs, $this->value)); 930 | } 931 | 932 | public function doCast(HasCodeLoc $loc, Expr\CastType $type, Context\Context $context, bool $noErrors):Type { 933 | return new self($loc, $type->evaluate($this->value)); 934 | } 935 | 936 | public function useToSetArrayKey(HasCodeLoc $loc, Context\Context $context, Type $array, Type $value, bool $noErrors):Type { 937 | return $array->setArrayKey($loc, $context, PhpTypeChecker\to_array_key($this->value), $value, $noErrors); 938 | } 939 | 940 | public function isFalsy():bool { 941 | return !$this->value; 942 | } 943 | 944 | public function isTruthy():bool { 945 | return !!$this->value; 946 | } 947 | 948 | public function call(HasCodeLoc $loc, Context\Context $context, array $args, bool $noErrors):Type { 949 | $parts = explode('::', (string)$this->value, 2); 950 | switch (count($parts)) { 951 | case 1: 952 | return $context->callFunction($loc, $parts[0], $args, $noErrors); 953 | case 2: 954 | // TODO 955 | // return $globals->callMethod($parts[0], $parts[1], $locals, $errors, $args); 956 | default: 957 | return $context->addError("Undefined function/method: $this->value", $loc); 958 | } 959 | } 960 | 961 | public function useAsVariableName(HasCodeLoc $loc, Context\Context $context):Type { 962 | $name = (string)$this->value; 963 | $var = $context->getLocal($name); 964 | if ($var && !$var->isEmpty()) { 965 | return $var; 966 | } else { 967 | return $context->addError("Undefined variable: $name", $loc); 968 | } 969 | } 970 | 971 | public function getStringValues(Context\Context $context):array { 972 | return [(string)$this->value]; 973 | } 974 | } 975 | 976 | /** 977 | * Either a: 978 | * - string representing a global function 979 | * - object implementing the __invoke() method 980 | * - string of format "class::method" 981 | * - array of form [$object, 'method'] 982 | * - array of form ['class', 'method'] 983 | */ 984 | class Callable_ extends SingleType { 985 | public function toTypeHint() { 986 | return 'callable'; 987 | } 988 | 989 | public function toString(bool $atomic = false):string { 990 | return 'callable'; 991 | } 992 | 993 | public function isCallable(Context\Context $ctx):bool { 994 | return true; 995 | } 996 | 997 | public function containsType(Type $type, Context\Context $ctx):bool { 998 | return $type->isCallable($ctx); 999 | } 1000 | 1001 | public function isTruthy():bool { 1002 | // The only possible falsy callable would be a string '0' if there is a global function called '0'. 1003 | // You can't actually define functions starting with digits, so that's impossible. 1004 | return true; 1005 | } 1006 | 1007 | /** 1008 | * @param HasCodeLoc $loc 1009 | * @param Context\Context $context 1010 | * @param Call\EvaledCallArg[] $args 1011 | * @param bool $noErrors 1012 | * @return Type 1013 | * @internal param bool $asRef 1014 | */ 1015 | public function call(HasCodeLoc $loc, Context\Context $context, array $args, bool $noErrors):Type { 1016 | // We don't know what the function signature is going to be, so just return mixed 1017 | return new Mixed($loc); 1018 | } 1019 | } 1020 | 1021 | class Float_ extends SingleType { 1022 | public function toTypeHint() { 1023 | return 'float'; 1024 | } 1025 | 1026 | public function toString(bool $atomic = false):string { 1027 | return 'float'; 1028 | } 1029 | 1030 | public function containsType(Type $type, Context\Context $ctx):bool { 1031 | return $type->isFloat(); 1032 | } 1033 | 1034 | public function useAsArrayKey(HasCodeLoc $loc, Type $array, Context\Context $context, bool $noErrors):Type { 1035 | return $array->getUnknownArrayKey($loc, $context, $noErrors); 1036 | } 1037 | } 1038 | 1039 | class String_ extends SingleType { 1040 | public function toTypeHint() { 1041 | return 'string'; 1042 | } 1043 | 1044 | public function toString(bool $atomic = false):string { 1045 | return 'string'; 1046 | } 1047 | 1048 | public function isString():bool { 1049 | return true; 1050 | } 1051 | 1052 | public function containsType(Type $type, Context\Context $ctx):bool { 1053 | return $type->isString(); 1054 | } 1055 | 1056 | public function useAsArrayKey(HasCodeLoc $loc, Type $array, Context\Context $context, bool $noErrors):Type { 1057 | return $array->getUnknownArrayKey($loc, $context, $noErrors); 1058 | } 1059 | } 1060 | 1061 | class Int_ extends SingleType { 1062 | public function toTypeHint() { 1063 | return 'int'; 1064 | } 1065 | 1066 | public function toString(bool $atomic = false):string { 1067 | return 'int'; 1068 | } 1069 | 1070 | public function isInt():bool { 1071 | return true; 1072 | } 1073 | 1074 | public function containsType(Type $type, Context\Context $ctx):bool { 1075 | return $type->isInt(); 1076 | } 1077 | 1078 | public function useAsArrayKey(HasCodeLoc $loc, Type $array, Context\Context $context, bool $noErrors):Type { 1079 | return $array->getUnknownArrayKey($loc, $context, $noErrors); 1080 | } 1081 | } 1082 | 1083 | class Object extends SingleType { 1084 | public function toTypeHint() { 1085 | return null; 1086 | } 1087 | 1088 | public function toString(bool $atomic = false):string { 1089 | return 'object'; 1090 | } 1091 | 1092 | public function isObject():bool { 1093 | return true; 1094 | } 1095 | 1096 | public function containsType(Type $type, Context\Context $ctx):bool { 1097 | return $type->isObject(); 1098 | } 1099 | } 1100 | 1101 | class Resource extends SingleType { 1102 | public function toString(bool $atomic = false):string { 1103 | return 'resource'; 1104 | } 1105 | 1106 | public function isResource():bool { 1107 | return true; 1108 | } 1109 | 1110 | public function containsType(Type $type, Context\Context $ctx):bool { 1111 | return $type->isResource(); 1112 | } 1113 | 1114 | public function useAsArrayKey(HasCodeLoc $loc, Type $array, Context\Context $context, bool $noErrors):Type { 1115 | // Apparently you can use resoureces as array keys. Who knew? 1116 | return $array->getUnknownArrayKey($loc, $context, $noErrors); 1117 | } 1118 | } 1119 | 1120 | /** 1121 | * An instance of a class or interface. 1122 | */ 1123 | class Class_ extends SingleType { 1124 | /** @var string */ 1125 | private $class; 1126 | 1127 | public function __construct(HasCodeLoc $loc, string $class) { 1128 | parent::__construct($loc); 1129 | $this->class = $class; 1130 | } 1131 | 1132 | public function toTypeHint() { 1133 | return new \PhpParser\Node\Name\FullyQualified($this->class); 1134 | } 1135 | 1136 | public function toString(bool $atomic = false):string { 1137 | return $this->class; 1138 | } 1139 | 1140 | public function isClass(string $class, Context\Context $ctx):bool { 1141 | return $ctx->isCompatible($class, $this->class); 1142 | } 1143 | 1144 | public function containsType(Type $type, Context\Context $ctx):bool { 1145 | return $type->isClass($this->class, $ctx); 1146 | } 1147 | 1148 | public function hasCallableMethod(string $method, Context\Context $ctx):bool { 1149 | return $ctx->methodExists($this->class, $method, false); 1150 | } 1151 | 1152 | public function getStringValues(Context\Context $context):array { 1153 | // TODO use __toString() if defined on $this->class 1154 | return parent::getStringValues($context); 1155 | } 1156 | } 1157 | 1158 | class Array_ extends SingleType { 1159 | /** @var Type */ 1160 | private $inner; 1161 | 1162 | public function __construct(HasCodeLoc $loc, Type $inner) { 1163 | parent::__construct($loc); 1164 | $this->inner = $inner; 1165 | } 1166 | 1167 | public function toTypeHint() { 1168 | return 'array'; 1169 | } 1170 | 1171 | public function getUnknownArrayKey(HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 1172 | return $this->inner; 1173 | } 1174 | 1175 | public function toString(bool $atomic = false):string { 1176 | if ($this->inner->isExactlyMixed()) { 1177 | return 'array'; 1178 | } else { 1179 | return $this->inner->toString(true) . '[]'; 1180 | } 1181 | } 1182 | 1183 | public function isArrayOf(Type $type, Context\Context $ctx):bool { 1184 | return $type->containsType($this->inner, $ctx); 1185 | } 1186 | 1187 | public function containsType(Type $type, Context\Context $ctx):bool { 1188 | return $type->isArrayOf($this->inner, $ctx); 1189 | } 1190 | 1191 | public function addArrayKey(HasCodeLoc $loc, Context\Context $context, Type $type, bool $noErrors):Type { 1192 | return new self($this, $this->inner->addType($type, $context)); 1193 | } 1194 | 1195 | public function setArrayKey(HasCodeLoc $loc, Context\Context $context, string $key, Type $type, bool $noErrors):Type { 1196 | return $this->addArrayKey($loc, $context, $type, $noErrors); 1197 | } 1198 | 1199 | public function doForeach(HasCodeLoc $loc, Context\Context $context):ForeachResult { 1200 | return new ForeachResult( 1201 | new Union($this, [new Int_($this), new String_($this)]), 1202 | $this->inner 1203 | ); 1204 | } 1205 | } 1206 | 1207 | /** 1208 | * A "shape" is an array with a fixed set of (key, type) pairs. 1209 | */ 1210 | class Shape extends SingleType { 1211 | /** 1212 | * @var Type[] Mapping from keys to types 1213 | */ 1214 | private $keys = []; 1215 | 1216 | /** 1217 | * @param HasCodeLoc $loc 1218 | * @param Type[] $keys 1219 | */ 1220 | public function __construct(HasCodeLoc $loc, array $keys) { 1221 | parent::__construct($loc); 1222 | $this->keys = $keys; 1223 | } 1224 | 1225 | public function getKnownArrayKey(string $key, HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 1226 | $res = $this->keys[$key] ?? Type::none($loc); 1227 | if (!$noErrors && $res->isEmpty()) { 1228 | $context->addError("Array key '$key' is not defined on $this", $loc); 1229 | } 1230 | return $res; 1231 | } 1232 | 1233 | public function getUnknownArrayKey(HasCodeLoc $loc, Context\Context $context, bool $noErrors):Type { 1234 | return $this->all($context); 1235 | } 1236 | 1237 | public function merge(Context\Context $context, self $that):self { 1238 | return new self($this, merge_types($this->keys, $that->keys, $context)); 1239 | } 1240 | 1241 | public function isCallable(Context\Context $ctx):bool { 1242 | if ( 1243 | isset($this->keys[0]) && 1244 | isset($this->keys[1]) 1245 | ) { 1246 | return $this->keys[1]->isCallableMethodOf($this->keys[0], $ctx); 1247 | } else { 1248 | return false; 1249 | } 1250 | } 1251 | 1252 | public function all(Context\Context $context):Type { 1253 | $type = Type::none($this); 1254 | foreach ($this->keys as $t) { 1255 | $type = $type->addType($t, $context); 1256 | } 1257 | return $type; 1258 | } 1259 | 1260 | public function toTypeHint() { 1261 | return 'array'; 1262 | } 1263 | 1264 | public function doForeach(HasCodeLoc $loc, Context\Context $context):ForeachResult { 1265 | $empty = new Union($this); 1266 | $foreach = new ForeachResult($empty, $empty); 1267 | foreach ($this->keys as $k => $v) { 1268 | $key = new SingleValue($this, $k); 1269 | $val = $v; 1270 | 1271 | $foreach->key = $foreach->key->addType($key, $context); 1272 | $foreach->val = $foreach->val->addType($val, $context); 1273 | } 1274 | return $foreach; 1275 | } 1276 | 1277 | public function isArrayOf(Type $type, Context\Context $ctx):bool { 1278 | return $type->containsType($this->all($ctx), $ctx); 1279 | } 1280 | 1281 | public function addArrayKey(HasCodeLoc $loc, Context\Context $context, Type $type, bool $noErrors):Type { 1282 | return (new Array_($this, $this->all($context)))->addArrayKey($loc, $context, $type, $noErrors); 1283 | } 1284 | 1285 | public function setArrayKey(HasCodeLoc $loc, Context\Context $context, string $key, Type $type, bool $noErrors):Type { 1286 | $keys = $this->keys; 1287 | $keys[$key] = $type; 1288 | return new self($this, $keys); 1289 | } 1290 | 1291 | /** 1292 | * @param Type[] $keys 1293 | * @param Context\Context $ctx 1294 | * @return bool 1295 | */ 1296 | public function isShape(array $keys, Context\Context $ctx):bool { 1297 | // Any difference in keys means they are not compatible 1298 | if ( 1299 | array_diff_key($keys, $this->keys) || 1300 | array_diff_key($this->keys, $keys) 1301 | ) { 1302 | return false; 1303 | } 1304 | 1305 | foreach ($keys as $key => $type) { 1306 | if (!$type->containsType($this->keys[$key], $ctx)) { 1307 | return false; 1308 | } 1309 | } 1310 | 1311 | return true; 1312 | } 1313 | 1314 | public function containsType(Type $type, Context\Context $ctx):bool { 1315 | return $type->isShape($this->keys, $ctx); 1316 | } 1317 | 1318 | public function toString(bool $atomic = false):string { 1319 | $parts = []; 1320 | $assoc = $this->isAssoc(); 1321 | foreach ($this->keys as $key => $type) { 1322 | if ($assoc) { 1323 | $parts[] = var_export($key, true) . ' => ' . $type->toString($atomic); 1324 | } else { 1325 | $parts[] = $type->toString($atomic); 1326 | } 1327 | } 1328 | return '[' . join(', ', $parts) . ']'; 1329 | } 1330 | 1331 | public function isAssoc():bool { 1332 | $i = 0; 1333 | foreach ($this->keys as $k => $v) { 1334 | if ($k !== $i++) { 1335 | return true; 1336 | } 1337 | } 1338 | return false; 1339 | } 1340 | 1341 | public function isFalsy():bool { 1342 | return !$this->keys; 1343 | } 1344 | 1345 | public function isTruthy():bool { 1346 | return !!$this->keys; 1347 | } 1348 | } 1349 | 1350 | -------------------------------------------------------------------------------- /src/JesseSchalken/PhpTypeChecker/functions.php: -------------------------------------------------------------------------------- 1 | getSubNodeNames() as $prop) { 14 | $value = $node->$prop; 15 | if (is_array($value)) { 16 | foreach ($value as $value2) { 17 | if ($value2 instanceof \PhpParser\Node) { 18 | $result[] = $value2; 19 | } 20 | } 21 | } elseif ($value instanceof \PhpParser\Node) { 22 | $result[] = $value; 23 | } 24 | } 25 | return $result; 26 | } 27 | 28 | function str_eq(string $a, string $b):bool { 29 | return strcmp($a, $b) == 0; 30 | } 31 | 32 | function str_ieq(string $a, string $b):bool { 33 | return strcasecmp($a, $b) == 0; 34 | } 35 | 36 | function normalize_constant(string $name):string { 37 | // $name is the name of the constant including the namespace. 38 | // Namespaces are case insensitive, but constants are case sensitive, 39 | // therefore split the name after the last "\" and strtolower() the left side. 40 | $pos = strrpos($name, '\\'); 41 | $pos = $pos === false ? 0 : $pos + 1; 42 | 43 | $prefix = substr($name, 0, $pos); 44 | $constant = substr($name, $pos); 45 | 46 | return strtolower($prefix) . $constant; 47 | } 48 | 49 | /** 50 | * @param string[] $phpFiles 51 | * @return string 52 | */ 53 | function type_check(array $phpFiles):string { 54 | $errors = new class () extends ErrorReceiver { 55 | public $errors = []; 56 | 57 | public function add(string $message, HasCodeLoc $loc) { 58 | $this->errors[] = $loc->loc()->format($message); 59 | } 60 | }; 61 | $files = File::parse($phpFiles, $errors); 62 | $context = new Context\Context($errors); 63 | foreach ($files as $file) { 64 | $file->gatherGlobalDecls($context); 65 | } 66 | foreach ($files as $file) { 67 | $file->typeCheck($context); 68 | } 69 | return join("\n", $errors->errors); 70 | } 71 | 72 | function extract_namespace(string $name):string { 73 | $pos = strrpos($name, '\\'); 74 | return $pos === false ? '' : substr($name, 0, $pos); 75 | } 76 | 77 | function remove_namespace(string $name):string { 78 | $pos = strrpos($name, '\\'); 79 | return $pos === false ? $name : substr($name, $pos + 1); 80 | } 81 | 82 | /** 83 | * @param Type\Type[] $types1 84 | * @param Type\Type[] $types2 85 | * @param Context\Context $context 86 | * @return Type\Type[] 87 | */ 88 | function merge_types(array $types1, array $types2, Context\Context $context):array { 89 | foreach ($types2 as $key => $type) { 90 | $types1[$key] = isset($types1[$key]) 91 | ? $types1[$key]->addType($type, $context) 92 | : $type; 93 | } 94 | return $types1; 95 | } 96 | 97 | function to_array_key($value):string { 98 | return (string)(array_keys([$value => null])[0]); 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/autoload.php: -------------------------------------------------------------------------------- 1 |