Skip to content

Check for #[Override] annotation #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
"keywords": ["module", "xp"],
"require" : {
"xp-framework/core": "^11.6 | ^10.16",
"xp-framework/reflection": "^2.13",
"xp-framework/ast": "^10.1",
"php" : ">=7.0.0"
},
"require-dev" : {
"xp-framework/reflection": "^2.13",
"xp-framework/test": "^1.5"
},
"bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"],
Expand Down
3 changes: 2 additions & 1 deletion src/main/php/lang/ast/CodeGen.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
class CodeGen {
private $id= 0;
public $scope= [];
public $source= '(unknown)';

/** Creates a new, unique symbol */
public function symbol() { return '_'.($this->id++); }
Expand Down Expand Up @@ -47,7 +48,7 @@ public function lookup($type) {
if ('self' === $type || 'static' === $type) {
return new Declaration($enclosing->type);
} else if ('parent' === $type) {
return $enclosing->type->parent ? $this->lookup($enclosing->type->parent->literal()) : null;
return isset($enclosing->type->parent) ? $this->lookup($enclosing->type->parent->literal()) : null;
}

foreach ($this->scope as $scope) {
Expand Down
8 changes: 6 additions & 2 deletions src/main/php/lang/ast/Emitter.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,20 @@ protected abstract function result($target);
/**
* Emitter entry point, takes nodes and emits them to the given target.
*
*
* @param iterable $nodes
* @param io.streams.OutputStream $target
* @param ?string $source
* @return io.streams.OutputStream
* @throws lang.ast.Errors
*/
public function write($nodes, OutputStream $target) {
$result= $this->result($target);
public function write($nodes, OutputStream $target, $source= null) {
$result= $this->result($target)->from($source);
try {
$this->emitAll($result, $nodes);
return $target;
} catch (Error $e) {
throw new Errors([$e], $source);
} finally {
$result->close();
}
Expand Down
39 changes: 38 additions & 1 deletion src/main/php/lang/ast/emit/Declaration.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace lang\ast\emit;

use lang\ast\nodes\{EnumCase, Property};
use lang\ast\nodes\{EnumCase, InterfaceDeclaration, Property, Method};

class Declaration extends Type {
private $type;
Expand All @@ -15,6 +15,43 @@ public function __construct($type) {
/** @return string */
public function name() { return ltrim($this->type->name, '\\'); }

/** @return iterable */
public function implementedInterfaces() {
if ($this->type instanceof InterfaceDeclaration) {
foreach ($this->type->parents as $interface) {
yield $interface->literal();
}
} else {
foreach ($this->type->implements as $interface) {
yield $interface->literal();
}
}
}

/**
* Checks whether a given method exists
*
* @param string $named
* @return bool
*/
public function providesMethod($named) {
return isset($this->body["{$named}()"]);
}

/**
* Returns all methods annotated with a given annotation
*
* @param string $annotation
* @return iterable
*/
public function methodsAnnotated($annotation) {
foreach ($this->body as $member) {
if ($member instanceof Method && $member->annotations && $member->annotations->named($annotation)) {
yield $member->name => $member->line;
}
}
}

/**
* Returns whether a given member is an enum case
*
Expand Down
23 changes: 23 additions & 0 deletions src/main/php/lang/ast/emit/Incomplete.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ public function __construct($name) { $this->name= $name; }
/** @return string */
public function name() { return $this->name; }

/** @return iterable */
public function implementedInterfaces() { return []; }

/**
* Checks whether a given method exists
*
* @param string $named
* @return bool
*/
public function providesMethod($named) {
return false;
}

/**
* Returns all methods annotated with a given annotation
*
* @param string $annotation
* @return iterable
*/
public function methodsAnnotated($annotation) {
return [];
}

/**
* Returns whether a given member is an enum case
*
Expand Down
54 changes: 52 additions & 2 deletions src/main/php/lang/ast/emit/PHP.class.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php namespace lang\ast\emit;

use Override;
use lang\ast\emit\Escaping;
use lang\ast\nodes\{
Annotation,
Expand All @@ -16,7 +17,7 @@
Variable
};
use lang\ast\types\{IsUnion, IsFunction, IsArray, IsMap, IsNullable, IsExpression};
use lang\ast\{Emitter, Node, Type, Result};
use lang\ast\{Emitter, Error, Node, Type, Result};

abstract class PHP extends Emitter {
const PROPERTY = 0;
Expand Down Expand Up @@ -153,6 +154,39 @@ protected function enclose($result, $node, $signature, $static, $emit) {
$result->locals= array_pop($result->stack);
}

/**
* Verify `Override` if existant. Although PHP 8.3+ includes this compile-time
* check, it does not come with a measurable performance impact doing so here,
* and we prevent uncatchable errors this way.
*
* @param lang.ast.CodeGen $codegen
* @param string $method
* @param int $line
* @return void
* @throws lang.ast.Error
*/
protected function checkOverride($codegen, $method, $line) {
if ($codegen->scope[0]->type->is('trait')) return;

// Check parent class
if (($parent= $codegen->lookup('parent')) && $parent->providesMethod($method)) return;

// Check all implemented interfaces
foreach ($codegen->lookup('self')->implementedInterfaces() as $interface) {
if ($codegen->lookup($interface)->providesMethod($method)) return;
}

throw new Error(
sprintf(
'%s::%s() has #[\\Override] attribute, but no matching parent method exists',
substr($codegen->scope[0]->type->name ?? '$class@anonymous', 1),
$method
),
$codegen->source,
$line
);
}

/**
* Emits local initializations
*
Expand Down Expand Up @@ -564,6 +598,14 @@ protected function emitTrait($result, $trait) {

protected function emitUse($result, $use) {
$result->out->write('use '.implode(',', $use->types));

// Verify Override
foreach ($use->types as $type) {
foreach ($result->codegen->lookup($type)->methodsAnnotated(Override::class) as $method => $line) {
$this->checkOverride($result->codegen, $method, $line);
}
}

if ($use->aliases) {
$result->out->write('{');
foreach ($use->aliases as $reference => $alias) {
Expand Down Expand Up @@ -628,7 +670,15 @@ protected function emitMethod($result, $method) {
];

$method->comment && $this->emitOne($result, $method->comment);
$method->annotations && $this->emitOne($result, $method->annotations);
if ($method->annotations) {
$this->emitOne($result, $method->annotations);
$method->annotations->named(Override::class) && $this->checkOverride(
$result->codegen,
$method->name,
$method->line
);
}

$result->at($method->declared)->out->write(
implode(' ', $method->modifiers).
' function '.
Expand Down
29 changes: 29 additions & 0 deletions src/main/php/lang/ast/emit/Reflection.class.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php namespace lang\ast\emit;

use lang\Reflection as Reflect;
use lang\{Enum, ClassNotFoundException};

class Reflection extends Type {
Expand All @@ -23,6 +24,34 @@ public function __construct($type) {
/** @return string */
public function name() { return $this->reflect->name; }

/** @return iterable */
public function implementedInterfaces() { return $this->type->getInterfaceNames(); }

/**
* Checks whether a given method exists
*
* @param string $named
* @return bool
*/
public function providesMethod($named) {
return $this->reflect->hasMethod($named);
}

/**
* Returns all methods annotated with a given annotation
*
* @param string $annotation
* @return iterable
*/
public function methodsAnnotated($annotation) {
$meta= Reflect::meta();
foreach ($this->reflect->getMethods() as $method) {
if (isset($meta->methodAnnotations($method)[$annotation])) {
yield $method->getName() => $method->getStartLine();
}
}
}

/**
* Returns whether a given member is an enum case
*
Expand Down
12 changes: 12 additions & 0 deletions src/main/php/lang/ast/emit/Result.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ public function __construct(OutputStream $out) {
$this->initialize();
}

/**
* Set filename this result originates from, defaulting to `(unknown)`.
*
* @param ?string $file
* @return self
*/
public function from($file) {
$this->codegen->source= $file ?? '(unknown)';
return $this;
}


/**
* Initialize result. Guaranteed to be called *once* from constructor.
* Without implementation here - overwrite in subclasses.
Expand Down
19 changes: 19 additions & 0 deletions src/main/php/lang/ast/emit/Type.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ static function __static() {
/** @return string */
public abstract function name();

/** @return iterable */
public abstract function implementedInterfaces();

/**
* Checks whether a given method exists
*
* @param string $named
* @return bool
*/
public abstract function providesMethod($named);

/**
* Returns all methods annotated with a given annotation
*
* @param string $annotation
* @return iterable
*/
public abstract function methodsAnnotated($annotation);

/**
* Returns whether a given member is an enum case
*
Expand Down
3 changes: 2 additions & 1 deletion src/main/php/xp/compiler/CompileRunner.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ public static function main(array $args) {
$file= $path->toString('/');
$t->start();
try {
$emit->write($lang->parse(new Tokens($source, $file))->stream(), $output->target((string)$path));
$parse= $lang->parse(new Tokens($source, $file));
$emit->write($parse->stream(), $output->target((string)$path), $parse->file);

$t->stop();
$quiet || Console::$err->writeLinef('> %s (%.3f seconds)', $file, $t->elapsedTime());
Expand Down
Loading