Skip to content
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

Check for #[Override] annotation #173

Merged
merged 19 commits into from
Jul 16, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions 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 @@ -45,14 +46,14 @@ public function lookup($type) {
$enclosing= $this->scope[0] ?? null;

if ('self' === $type || 'static' === $type) {
return new Declaration($enclosing->type);
return new Declaration($enclosing->type, $this);
} 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) {
if ($scope->type->name && $type === $scope->type->name->literal()) {
return new Declaration($scope->type);
return new Declaration($scope->type, $this);
}
}

Expand Down
9 changes: 6 additions & 3 deletions src/main/php/lang/ast/Emitter.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,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
83 changes: 80 additions & 3 deletions src/main/php/lang/ast/emit/Declaration.class.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,97 @@
<?php namespace lang\ast\emit;

use lang\ast\nodes\{EnumCase, Property};
use Override;
use lang\ast\Error;
use lang\ast\nodes\{EnumCase, InterfaceDeclaration, TraitDeclaration, Property, Method};
use lang\reflection\Modifiers;

class Declaration extends Type {
private $type;
private $type, $codegen;

static function __static() { }

/** @param lang.ast.nodes.TypeDeclaration $type */
public function __construct($type) {
public function __construct($type, $codegen) {
$this->type= $type;
$this->codegen= $codegen;
}

/** @return string */
public function name() { return ltrim($this->type->name, '\\'); }

/**
* Checks `#[Override]`
*
* @param lang.ast.emit.Type $type
* @return void
* @throws lang.ast.Error
*/
public function checkOverrides($type) {
foreach ($this->type->body as $member) {
if ($member instanceof Method && $member->annotations && $member->annotations->named(Override::class)) {
$type->checkOverride($member->name, $member->line);
}
}
}

/**
* Checks `#[Override]` for a given method
*
* @param string $method
* @param int $line
* @return void
* @throws lang.ast.Error
*/
public function checkOverride($method, $line) {
if ($this->type instanceof TraitDeclaration) {

// Do not check traits, this is done when including them into the type
return;
} else if ($this->type instanceof InterfaceDeclaration) {

// Check parent interfaces
foreach ($this->type->parents as $interface) {
if ($this->codegen->lookup($interface->literal())->providesMethod($method)) return;
}
} else {

// Check parent for non-private methods
if ($this->type->parent && $this->codegen->lookup($this->type->parent->literal())->providesMethod(
$method,
MODIFIER_PUBLIC | MODIFIER_PROTECTED
)) return;

// Finally, check all implemented interfaces
foreach ($this->type->implements as $interface) {
if ($this->codegen->lookup($interface->literal())->providesMethod($method)) return;
}
}

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

/**
* Checks whether a given method exists
*
* @param string $named
* @param ?int $select
* @return bool
*/
public function providesMethod($named, $select= null) {
if ($method= $this->type->body["{$named}()"] ?? null) {
return null === $select || (new Modifiers($method->modifiers))->bits() & $select;
}
return false;
}

/**
* Returns whether a given member is an enum case
*
Expand Down
34 changes: 34 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,40 @@ public function __construct($name) { $this->name= $name; }
/** @return string */
public function name() { return $this->name; }

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

/**
* Checks `#[Override]`
*
* @param lang.ast.emit.Type $type
* @return void
* @throws lang.ast.Error
*/
public function checkOverrides($type) {
// NOOP
}

/**
* Checks `#[Override]` for a given method
*
* @param string $method
* @param int $line
* @return void
* @throws lang.ast.Error
*/
public function checkOverride($method, $line) {
// NOOP
}

/**
* Returns whether a given member is an enum case
*
Expand Down
17 changes: 16 additions & 1 deletion 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 Down Expand Up @@ -564,6 +565,13 @@ protected function emitTrait($result, $trait) {

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

// Verify Override
$self= $result->codegen->lookup('self');
foreach ($use->types as $type) {
$result->codegen->lookup($type)->checkOverrides($self);
}

if ($use->aliases) {
$result->out->write('{');
foreach ($use->aliases as $reference => $alias) {
Expand Down Expand Up @@ -628,7 +636,14 @@ 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) && $result->codegen->lookup('self')->checkOverride(
$method->name,
$method->line
);
}

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

use Override, UnitEnum, ReflectionClass, ReflectionException;
use lang\Reflection as Reflect;
use lang\ast\Error;
use lang\{Enum, ClassNotFoundException};

class Reflection extends Type {
Expand All @@ -8,21 +11,85 @@ class Reflection extends Type {

/** @codeCoverageIgnore */
static function __static() {
self::$UNITENUM= interface_exists(\UnitEnum::class, false); // Compatibility with XP < 10.8.0
self::$UNITENUM= interface_exists(UnitEnum::class, false); // Compatibility with XP < 10.8.0
}

/** @param string $type */
public function __construct($type) {
try {
$this->reflect= new \ReflectionClass($type);
} catch (\ReflectionException $e) {
$this->reflect= new ReflectionClass($type);
} catch (ReflectionException $e) {
throw new ClassNotFoundException($type);
}
}

/** @return string */
public function name() { return $this->reflect->name; }

/**
* Checks whether a given method exists
*
* @param string $named
* @param ?int $select
* @return bool
*/
public function providesMethod($named, $select= null) {
if ($this->reflect->hasMethod($named)) {
return null === $select || $this->reflect->getMethod($named)->getModifiers() & $select;
}
return false;
}

/**
* Checks `#[Override]`
*
* @param lang.ast.emit.Type $type
* @return void
* @throws lang.ast.Error
*/
public function checkOverrides($type) {
$meta= Reflect::meta();
foreach ($this->reflect->getMethods() as $method) {
if (isset($meta->methodAnnotations($method)[Override::class])) {
$type->checkOverride($method->getName(), $method->getStartLine());
}
}
}

/**
* Checks `#[Override]` for a given method
*
* @param string $method
* @param int $line
* @return void
* @throws lang.ast.Error
*/
public function checkOverride($method, $line) {

// Ignore traits, check parents and interfaces for all other types
if ($this->reflect->isTrait()) {
return;
} else if ($parent= $this->reflect->getParentClass()) {
if ($parent->hasMethod($method)) {
if (!$this->reflect->getMethod($named)->isPrivate()) return;
}
} else {
foreach ($this->type->getInterfaces() as $interface) {
if ($interface->hasMethod($method)) return;
}
}

throw new Error(
sprintf(
'%s::%s() has #[\\Override] attribute, but no matching parent method exists',
$this->reflect->isAnonymous() ? 'class@anonymous' : $this->reflect->getName(),
$method
),
$this->reflect->getFileName(),
$line
);
}

/**
* Returns whether a given member is an enum case
*
Expand All @@ -32,9 +99,9 @@ public function name() { return $this->reflect->name; }
public function rewriteEnumCase($member) {
if ($this->reflect->isSubclassOf(Enum::class)) {
return $this->reflect->getStaticPropertyValue($member, null) instanceof Enum;
} else if (!self::$ENUMS && self::$UNITENUM && $this->reflect->isSubclassOf(\UnitEnum::class)) {
} else if (!self::$ENUMS && self::$UNITENUM && $this->reflect->isSubclassOf(UnitEnum::class)) {
$value= $this->reflect->getConstant($member) ?: $this->reflect->getStaticPropertyValue($member, null);
return $value instanceof \UnitEnum;
return $value instanceof UnitEnum;
}
return false;
}
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
Loading