├── .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 |
46 |
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 |