mirror of
https://github.com/apple/pkl.git
synced 2026-08-26 21:54:03 +02:00
Enforce that abstract methods are implemented (#1785)
This adds a check that abstract members must be implemented. If any members lack an implementation, an error is thrown describing the missing members. Co-authored-by: Dan Chao <dan.chao@apple.com>
This commit is contained in:
@@ -123,37 +123,45 @@ public final class ClassNode extends ExpressionNode {
|
||||
typeParameters,
|
||||
prototype);
|
||||
|
||||
if (unresolvedSupertypeNode != null) {
|
||||
var supertypeNode = unresolvedSupertypeNode.execute(frame);
|
||||
var superclass = supertypeNode.getVmClass();
|
||||
var localContext = VmLanguage.get(this).localContext.get();
|
||||
localContext.beginClassInit(cachedClass);
|
||||
|
||||
checkSupertype(supertypeNode, superclass);
|
||||
cachedClass.initSupertype(supertypeNode, superclass);
|
||||
try {
|
||||
if (unresolvedSupertypeNode != null) {
|
||||
var supertypeNode = unresolvedSupertypeNode.execute(frame);
|
||||
var superclass = supertypeNode.getVmClass();
|
||||
|
||||
checkSupertype(supertypeNode, superclass);
|
||||
cachedClass.initSupertype(supertypeNode, superclass);
|
||||
}
|
||||
|
||||
// The superclass resolved above may not itself have completed the below initializations yet.
|
||||
// That's because these initializations may have indirectly or directly triggered
|
||||
// resolution of this class, in which case the `resolveSuperclass()` call above
|
||||
// will have returned the partially initialized `cachedClass` of the superclass.
|
||||
// As a consequence, initializations that require a fully initialized class hierarchy
|
||||
// are done lazily in VmClass rather than here.
|
||||
// A fully initialized class hierarchy is only required for initialization of internal caches,
|
||||
// which is guaranteed to succeed (no impact on eager vs. lazy error reporting) and easy to
|
||||
// defer.
|
||||
|
||||
VmUtils.evaluateAnnotations(frame, annotationNodes, annotations);
|
||||
|
||||
for (var node : unresolvedPropertyNodes) {
|
||||
cachedClass.addProperty(node.execute(frame, cachedClass));
|
||||
}
|
||||
|
||||
for (var node : unresolvedMethodNodes) {
|
||||
cachedClass.addMethod(node.execute(frame, cachedClass));
|
||||
}
|
||||
|
||||
cachedClass.onOwnClassInitialized();
|
||||
localContext.endClassInit();
|
||||
return cachedClass;
|
||||
} catch (Throwable e) {
|
||||
localContext.clearClassInitState();
|
||||
throw e;
|
||||
}
|
||||
|
||||
// The superclass resolved above may not itself have completed the below initializations yet.
|
||||
// That's because these initializations may have indirectly or directly triggered
|
||||
// resolution of this class, in which case the `resolveSuperclass()` call above
|
||||
// will have returned the partially initialized `cachedClass` of the superclass.
|
||||
// As a consequence, initializations that require a fully initialized class hierarchy
|
||||
// are done lazily in VmClass rather than here.
|
||||
// A fully initialized class hierarchy is only required for initialization of internal caches,
|
||||
// which is guaranteed to succeed (no impact on eager vs. lazy error reporting) and easy to
|
||||
// defer.
|
||||
|
||||
VmUtils.evaluateAnnotations(frame, annotationNodes, annotations);
|
||||
|
||||
for (var node : unresolvedPropertyNodes) {
|
||||
cachedClass.addProperty(node.execute(frame, cachedClass));
|
||||
}
|
||||
|
||||
for (var node : unresolvedMethodNodes) {
|
||||
cachedClass.addMethod(node.execute(frame, cachedClass));
|
||||
}
|
||||
|
||||
cachedClass.notifyInitialized();
|
||||
|
||||
return cachedClass;
|
||||
}
|
||||
|
||||
private void checkSupertype(TypeNode supertypeNode, @Nullable VmClass superclass) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.pkl.core.TypeParameter;
|
||||
import org.pkl.core.ast.*;
|
||||
import org.pkl.core.ast.member.*;
|
||||
import org.pkl.core.ast.type.TypeNode;
|
||||
import org.pkl.core.runtime.VmExceptionBuilder.MultilineValue;
|
||||
import org.pkl.core.util.CollectionUtils;
|
||||
import org.pkl.core.util.EconomicMaps;
|
||||
import org.pkl.core.util.LateInit;
|
||||
@@ -150,6 +151,45 @@ public final class VmClass extends VmValue {
|
||||
prototype.lateInitParent(superclass.getPrototype());
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
private void checkAbstractMethods() {
|
||||
if (isAbstract()) return;
|
||||
// minimize allocations in the non-error case
|
||||
var abstractMethods = getAbstractMethods();
|
||||
if (abstractMethods.isEmpty()) return;
|
||||
if (abstractMethods.size() == 1) {
|
||||
throw new VmExceptionBuilder()
|
||||
.evalError(
|
||||
"noImplementationForAbstractMethod",
|
||||
getDisplayName(),
|
||||
abstractMethods.get(0).getCallSignature())
|
||||
.withSourceSection(getHeaderSection())
|
||||
.build();
|
||||
}
|
||||
var methodList = new ArrayList<String>(abstractMethods.size());
|
||||
for (var method : abstractMethods) {
|
||||
methodList.add(method.getCallSignature());
|
||||
}
|
||||
throw new VmExceptionBuilder()
|
||||
.evalError(
|
||||
"noImplementationForAbstractMethods", getDisplayName(), MultilineValue.of(methodList))
|
||||
.withSourceSection(getHeaderSection())
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<ClassMethod> getAbstractMethods() {
|
||||
assert this.superclass != null;
|
||||
var result = new ArrayList<ClassMethod>();
|
||||
var methodCursor = getAllMethods().getEntries();
|
||||
while (methodCursor.advance()) {
|
||||
var method = methodCursor.getValue();
|
||||
if (method.isAbstract()) {
|
||||
result.add(method);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@TruffleBoundary
|
||||
public void addProperty(ClassProperty property) {
|
||||
prototype.addProperty(property.getInitializer());
|
||||
@@ -190,11 +230,20 @@ public final class VmClass extends VmValue {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Superclasses may not have finished their initialization when this method is called.
|
||||
public void notifyInitialized() {
|
||||
/**
|
||||
* Called when this class itself has been initialized.
|
||||
*
|
||||
* <p>Superclasses may not have been initialized yet.
|
||||
*/
|
||||
public void onOwnClassInitialized() {
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
/** Called when the entire class hierarchy is completely initialized, including superclasses. */
|
||||
public void onFullyInitialized() {
|
||||
checkAbstractMethods();
|
||||
}
|
||||
|
||||
public int getTypeParameterCount() {
|
||||
return typeParameters.size();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
/** A per-context thread-local value that can be used to influence execution. */
|
||||
public class VmLocalContext {
|
||||
private boolean shouldEagerTypecheck = false;
|
||||
@@ -22,6 +25,12 @@ public class VmLocalContext {
|
||||
/** Whether we are currently inside a type test ({@code is} check). */
|
||||
private boolean inTypeTest = false;
|
||||
|
||||
/** The number of classes currently being initialized. */
|
||||
private int classDepth = 0;
|
||||
|
||||
/** The classes currently being initialized. */
|
||||
private final Deque<VmClass> pendingClasses = new ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* Number of active {@link VmValueTracker} instances. Used to determine if instrumentation is
|
||||
* already active.
|
||||
@@ -48,6 +57,27 @@ public class VmLocalContext {
|
||||
return inTypeTest;
|
||||
}
|
||||
|
||||
public void beginClassInit(VmClass vmClass) {
|
||||
classDepth++;
|
||||
pendingClasses.add(vmClass);
|
||||
}
|
||||
|
||||
public void endClassInit() {
|
||||
classDepth--;
|
||||
if (classDepth > 0) {
|
||||
return;
|
||||
}
|
||||
while (!pendingClasses.isEmpty()) {
|
||||
var clazz = pendingClasses.pop();
|
||||
clazz.onFullyInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearClassInitState() {
|
||||
pendingClasses.clear();
|
||||
classDepth = 0;
|
||||
}
|
||||
|
||||
public void enterTracker() {
|
||||
activeTrackerDepth++;
|
||||
instrumentationEverUsed = true;
|
||||
|
||||
@@ -1208,3 +1208,10 @@ invalidReferenceTypeAnnotationWithConstraint=\
|
||||
|
||||
cannotInstallPackageWithNoCache=\
|
||||
Cannot install package to module cache dir when module cache is disabled.
|
||||
|
||||
noImplementationForAbstractMethod=\
|
||||
Class `{0}` should either be declared `abstract`, or should implement method `{1}`.
|
||||
|
||||
noImplementationForAbstractMethods=\
|
||||
Class `{0}` should either be declared `abstract`, or should implement the following methods:\n\
|
||||
{1}
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
abstract module Foo
|
||||
|
||||
abstract function bar(): Int
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
abstract class AbstractMethod {
|
||||
abstract function foo(): Int
|
||||
}
|
||||
|
||||
class MyClass extends AbstractMethod {
|
||||
}
|
||||
|
||||
foo: MyClass
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
abstract class AbstractMethods {
|
||||
abstract function foo(): Int
|
||||
abstract function bar(): Int
|
||||
}
|
||||
|
||||
class MyClass extends AbstractMethods {
|
||||
}
|
||||
|
||||
foo: MyClass
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
extends "../../input-helper/classes/AbstractModule.pkl"
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
abstract class AbstractMethod {
|
||||
abstract function foo(): Int
|
||||
}
|
||||
|
||||
abstract class AbstractIntermediate extends AbstractMethod
|
||||
|
||||
class MyClass extends AbstractIntermediate {
|
||||
}
|
||||
|
||||
foo: MyClass
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
–– Pkl Error ––
|
||||
Class `abstractMethodNotImplemented1#MyClass` should either be declared `abstract`, or should implement method `foo()`.
|
||||
|
||||
x | class MyClass extends AbstractMethod {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at abstractMethodNotImplemented1 (file:///$snippetsDir/input/errors/abstractMethodNotImplemented1.pkl)
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
–– Pkl Error ––
|
||||
Class `abstractMethodNotImplemented2#MyClass` should either be declared `abstract`, or should implement the following methods:
|
||||
foo()
|
||||
bar()
|
||||
|
||||
x | class MyClass extends AbstractMethods {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at abstractMethodNotImplemented2 (file:///$snippetsDir/input/errors/abstractMethodNotImplemented2.pkl)
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
–– Pkl Error ––
|
||||
Class `abstractMethodNotImplemented3` should either be declared `abstract`, or should implement method `bar()`.
|
||||
|
||||
x | extends "../../input-helper/classes/AbstractModule.pkl"
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at abstractMethodNotImplemented3 (file:///$snippetsDir/input/errors/abstractMethodNotImplemented3.pkl)
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
–– Pkl Error ––
|
||||
Class `abstractMethodNotImplemented4#MyClass` should either be declared `abstract`, or should implement method `foo()`.
|
||||
|
||||
x | class MyClass extends AbstractIntermediate {
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
at abstractMethodNotImplemented4 (file:///$snippetsDir/input/errors/abstractMethodNotImplemented4.pkl)
|
||||
Reference in New Issue
Block a user