Correctly type check Class<T> - #1698
Conversation
7c64e69 to
5adcbf7
Compare
c2f4029 to
d31129f
Compare
17b645c to
bfb8210
Compare
096cb0d to
acd6e0a
Compare
|
General comment: given this: typealias MyNumber = Number
foo: Class<MyNumber> = NumberThis seems like an incorrect error, because Might also be okay to not throw for type arguments which don't point to classes. In these cases, the type would just never typecheck, and it would behave just like the a: Class<Foo?>
b: Class<"foo" | "bar> |
Can you say more about how this would be useful? The only case I can think of is the compatibility shim typealias when a class is moved out into its own module. If this was supported, it would only work for aliases to bare class types, which is of limited utility. An alias may hide that type is not an ordinary class and result in an error (as-is) or a
Again, I think it's more surprising that users could inadvertently build a |
An invariant of a typealias is that: on the type level, it just stands for its aliased type; therefore
I'd expect the failures to be loud, rather than silent! If you try to assign anything to |
|
Updated this to unwrap typealiases during class calculation, so aliases (to aliases)+ to classes now behave as if the underlying class was used directly. Also updated this to error at runtime rather than during |
e8626ff to
2a6eb5f
Compare
|
While comparing #1763 with this PR, I noticed one case that may still need coverage: exported schemas. In #1763 I added a |
stackoverflow
left a comment
There was a problem hiding this comment.
LGTM.
Worth adding the breaking change to the changelog already.
bioball
left a comment
There was a problem hiding this comment.
Overall, looks good! Some comments.
| public void buildHint(AnsiStringBuilder builder, String indent, boolean withPowerAssertions) { | ||
| if (expectedClass != null) return; | ||
| builder.append( | ||
| "A `Class` type check can only succeed when its type argument is an un-parameterized class, a module, `unknown`, `module`, or an alias to one of those types."); |
There was a problem hiding this comment.
[nit] Move this to errorMessages.properties
Also, what does "a module" mean? This error message already contains module; duplicated by mistake?
| /** | ||
| * Type node that corresponds to a simple, unparameterized {@link VmClass}. | ||
| * | ||
| * <p>This includes generic classes written without any type arguments like {@code List}. | ||
| */ | ||
| public interface SimpleClassTypeNode { | ||
| VmClass getVmClass(); | ||
| } | ||
|
|
||
| /** Type node that corresponds to a user-defined class (or module class). */ | ||
| public interface UserClassTypeNode extends SimpleClassTypeNode {} |
There was a problem hiding this comment.
I'm not sure if the SimpleClassTypeNode here is doing too much; TypeNode already has VmClass getVmClass().
We can keep the existing class hierarchy (only ClassTypeNode), and getViolatingNode()'s check can just be:
@Override
public @Nullable Node getViolatingNode() {
// use the validation hook to recalculate clazz after typealias instantiation
CompilerDirectives.transferToInterpreterAndInvalidate();
var node = typeNode;
while (node instanceof TypeAliasTypeNode typeAliasTypeNode) {
node = typeAliasTypeNode.getAliasedTypeNode();
}
if (node instanceof UnknownTypeNode || node instanceof TypeVariableNode) {
clazz = BaseModule.getAnyClass();
} else {
clazz = node.getVmClass();
}
return null;
}|
|
||
| @Override | ||
| protected boolean acceptTypeNode(boolean visitTypeArguments, TypeNodeConsumer consumer) { | ||
| return consumer.accept(this); |
There was a problem hiding this comment.
Class<T> is parameterized, so this should be:
| return consumer.accept(this); | |
| if (visitTypeArguments) { | |
| return consumer.accept(this) && typeNode.acceptTypeNode(true, consumer); | |
| } | |
| return consumer.accept(this); |
| } | ||
| } | ||
|
|
||
| public abstract static class ClassClassTypeNode extends ValidatingObjectSlotTypeNode { |
There was a problem hiding this comment.
Shouldn't need to extend ValidatingObjectSlotTypeNode; this can be:
public abstract static class ClassClassTypeNode extends ObjectSlotTypeNode {
@Child private TypeNode typeNode;
@CompilationFinal private @Nullable VmClass clazz;
public ClassClassTypeNode(SourceSection sourceSection, TypeNode typeNode) {
super(sourceSection);
this.typeNode = typeNode;
}
private void initVmClass() {
if (clazz != null) {
return;
}
CompilerDirectives.transferToInterpreterAndInvalidate();
var node = typeNode;
while (node instanceof TypeAliasTypeNode typeAliasTypeNode) {
node = typeAliasTypeNode.getAliasedTypeNode();
}
if (node instanceof UnknownTypeNode || node instanceof TypeVariableNode) {
clazz = BaseModule.getAnyClass();
} else {
clazz = node.getVmClass();
}
}
@Specialization
protected Object eval(VmClass value) {
initVmClass();
// rest of the method
}
}There was a problem hiding this comment.
Then, we can get rid of all the overrides that don't do anything.
| @Override | ||
| protected PType doExport() { | ||
| return new PType.Class(BaseModule.getClassClass().export(), typeNode.doExport()); | ||
| } |
There was a problem hiding this comment.
Since these are also changing other public API surface areas, can we add some tests around:
- reflect API giving type arguments for
Class<T> - Java schema evaluator giving metadata about type arguments
| res16 = D is Class<Object> | ||
| res17 = D is Class<Typed> | ||
| res18 = D is Class<Dynamic> | ||
| res19 = D is Class<Int> |
There was a problem hiding this comment.
Let's also add a test for parenthesized types:
C is Class<(C)>
C is Class<((C))>
This PR removes the unconditional type-erasure of
Class<T>toClass.This is a breaking change when
Tis a union type, nullable type, string literal type, parameterized type, ornothing; these will never type-check.For convenience, erasure still occurs when
TisunknownorAny. Similarly, the subclass check is skipped whenTis an un-replaced type variable, e.g. in generic classes/methods (type aliases replace the type arg during instantiation).Resolves #1729
Closes apple/pkl-intellij#222