diff --git a/src/main/java/leekscript/compiler/instruction/ClassDeclarationInstruction.java b/src/main/java/leekscript/compiler/instruction/ClassDeclarationInstruction.java index dae691d8..2ab0410d 100644 --- a/src/main/java/leekscript/compiler/instruction/ClassDeclarationInstruction.java +++ b/src/main/java/leekscript/compiler/instruction/ClassDeclarationInstruction.java @@ -486,10 +486,41 @@ public void analyze(WordCompiler compiler) throws LeekCompilerException { var parentVersion = parentMethod.get(version.getKey()); if (parentVersion != null) { foundInParent = true; - if (block.getType().accepts(parentVersion.block.getType()) != CastType.EQUALS) { + // Compatibilité de type entre la méthode surchargée et celle du parent, + // comme en Java : + // - le type de retour peut être réduit (covariance) : l'enfant peut + // retourner un sous-type de ce que retourne le parent ; + // - les types des paramètres restent invariants (doivent être identiques). + var childType = block.getType(); + var parentType = parentVersion.block.getType(); + var parentReturn = parentType.returnType(); + var childReturn = childType.returnType(); + + // Type de retour : identique, ou réduit à une sous-classe (covariance). + // On limite la covariance aux types classe : c'est le seul cas que le + // Java généré peut représenter (u_Enfant extends u_Parent permet un + // retour covariant). Java n'autorise pas non plus la covariance entre + // primitifs (ex. double -> long), on reste donc cohérent. + var returnCast = parentReturn.accepts(childReturn); + boolean compatible = returnCast == CastType.EQUALS + || (returnCast == CastType.UPCAST && parentReturn instanceof ClassType && childReturn instanceof ClassType); + + // Paramètres : chaque type doit rester identique (invariant). + if (compatible) { + var parentArgs = parentType.getArguments(); + var childArgs = childType.getArguments(); + for (int a = 0; a < Math.min(parentArgs.size(), childArgs.size()); ++a) { + if (parentArgs.get(a).accepts(childArgs.get(a)) != CastType.EQUALS) { + compatible = false; + break; + } + } + } + + if (!compatible) { compiler.addError(new AnalyzeError(block.getLocation(), AnalyzeErrorLevel.ERROR, Error.OVERRIDDEN_METHOD_DIFFERENT_TYPE, new String[] { - block.getType().toString(), - parentVersion.block.getType().toString() + childType.toString(), + parentType.toString() })); } // Une override ne peut pas réduire la visibilité (LSP : ce qui est public dans le parent doit le rester). diff --git a/src/test/java/test/TestAnnotations.java b/src/test/java/test/TestAnnotations.java index 9b85f018..1ffba734 100644 --- a/src/test/java/test/TestAnnotations.java +++ b/src/test/java/test/TestAnnotations.java @@ -133,6 +133,98 @@ class Dog extends Animal { } return 0; """).error(Error.ANNOTATION_OVERRIDE_NO_PARENT); + + section("@override — covariant return type"); + + // Narrowing the return type (covariance, like Java): the child may return a + // subtype of what the parent returns. Here self() narrows Animal → Dog, so the + // statically-typed result can call a Dog-only method. + code_v4_(""" + class Animal { + Animal self() { return this; } + } + class Dog extends Animal { + @override + Dog self() { return this; } + bark() { return "woof"; } + } + return new Dog().self().bark(); + """).equals("\"woof\""); + + // Covariance is limited to class types, like Java: a primitive return type + // cannot be narrowed (Java forbids real → integer covariant returns too). + code_v4_(""" + class A { + real get() { return 1.5; } + } + class B extends A { + @override + integer get() { return 2; } + } + return new B().get(); + """).error(Error.OVERRIDDEN_METHOD_DIFFERENT_TYPE); + + // Covariance over several levels of inheritance. + code_v4_(""" + class Animal { + Animal self() { return this; } + } + class Dog extends Animal {} + class Puppy extends Dog { + @override + Puppy self() { return this; } + yip() { return "yip"; } + } + return new Puppy().self().yip(); + """).equals("\"yip\""); + + // Identical return type is still accepted. + code_v4_(""" + class Animal { + Animal self() { return this; } + } + class Dog extends Animal { + @override + Animal self() { return this; } + } + return new Dog().self() == null ? 0 : 1; + """).equals("1"); + + // Widening the return type (supertype) is rejected. + code_v4_(""" + class Animal { + Dog get() { return new Dog(); } + } + class Dog extends Animal { + @override + Animal get() { return new Animal(); } + } + return 0; + """).error(Error.OVERRIDDEN_METHOD_DIFFERENT_TYPE); + + // Unrelated return type is rejected. + code_v4_(""" + class Animal { + integer get() { return 1; } + } + class Dog extends Animal { + @override + string get() { return "x"; } + } + return 0; + """).error(Error.OVERRIDDEN_METHOD_DIFFERENT_TYPE); + + // Parameter types remain invariant (like Java): changing them is rejected. + code_v4_(""" + class Animal { + set(integer x) { return x; } + } + class Dog extends Animal { + @override + set(string x) { return x; } + } + return 0; + """).error(Error.OVERRIDDEN_METHOD_DIFFERENT_TYPE); } @Test