Skip to content

Commit

Permalink
Adding constructor check to @:structInit canAssign check
Browse files Browse the repository at this point in the history
  • Loading branch information
m0rkeulv committed Aug 18, 2024
1 parent fc745d7 commit d7b6ba8
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,9 @@ public void setConstraintCheck(boolean constraintCheck) {
public boolean isConstraintCheck() {
return constraintCheck;
}

public void clearErrors() {
missingMembers.clear();
wrongTypeMembers.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,10 @@ static private boolean canAssignToFromType(
if (classModel.isAnonymous() || classModel.isStructInit() || classModel.isObjectLiteral()){
if (!from.isTypeParameter()) { // cant get members from a typeParameter
// compare members (Anonymous stucts can be "used" as interface)
return containsAllMembers(to, from, context);
if(containsAllMembers(to, from, context)) return true;
if (classModel.isStructInit()) {
return checkStructInitConstructor(to, from, context);
}
}
}
}
Expand Down Expand Up @@ -533,6 +536,74 @@ static private boolean canAssignToFromType(
return to.toStringWithoutConstant().equals(from.toStringWithoutConstant());
}

private static boolean checkStructInitConstructor(SpecificHaxeClassReference to, SpecificHaxeClassReference from,
@Nullable HaxeAssignContext context) {
HaxeClassModel toModel = to.getHaxeClassModel();
HaxeClassModel fromModel = from.getHaxeClassModel();
if (toModel != null && fromModel != null && toModel.isStructInit() && fromModel.isObjectLiteral()) {

// looks like haxe is mapping constructor parameter names to anonymous fields names.
// if names mismatch we get "Object requires field" / "has extra field value"
// note that parameters can be optional
List<HaxeObjectLiteralMemberModel> fromMembers = fromModel.getAllMembers(from.getGenericResolver()).stream()
.filter(model -> model instanceof HaxeObjectLiteralMemberModel)
.map(HaxeObjectLiteralMemberModel.class::cast)
.toList();
HaxeGenericResolver toResolver = to.getGenericResolver();
HaxeMethodModel constructor = toModel.getConstructor(toResolver);
if (constructor == null) return false;
boolean allMacteches = true;

if (context != null ) {
// remove errors from field check, we are going to add constructor errors if there are any
context.clearErrors();
}


List<HaxeParameterModel> parameters = constructor.getParameters();
for (HaxeParameterModel parameter : parameters) {
String parameterName = parameter.getName();

Optional<HaxeObjectLiteralMemberModel> fieldWithSameName = fromMembers.stream()
.filter(m -> m.getNamePsi().getIdentifier().textMatches(parameterName))
.findFirst();

if (fieldWithSameName.isPresent()) {
HaxeObjectLiteralMemberModel fromMemberModel = fieldWithSameName.get();
ResultHolder fromType = ifEnumValueGetType(fromMemberModel.getResultType(toResolver));
ResultHolder toType = ifEnumValueGetType(parameter.getType());
ResultHolder resolved = toResolver.resolve(toType);
if (resolved != null && !resolved.isUnknown())toType = resolved;
if(!toType.canAssign(fromType)) {
allMacteches = false;
if (context != null) {
context.addWrongTypeMember(fromMemberModel.getPresentableText(null), parameter.getPresentableText(toResolver));
}
}
} else {
if (parameter.isOptional()) continue;
allMacteches = false;
if (context != null) {
context.addMissingMember(parameterName);
}
}
}
return allMacteches;
}
return false;
}

private static ResultHolder ifEnumValueGetType(ResultHolder type) {
if(type.isEnumValueType()) {
SpecificEnumValueReference enumValueType = type.getEnumValueType();
if(enumValueType != null) {
return enumValueType.getEnumClass().createHolder();
}
}
return type;

}

private static RecursionGuard<PsiElement> containsMembersRecursionGuard = RecursionManager.createGuard("containsMembersRecursionGuard");

private static boolean containsAllMembers(SpecificHaxeClassReference to, SpecificHaxeClassReference from,
Expand All @@ -553,8 +624,12 @@ private static boolean containsAllMembers(SpecificHaxeClassReference to, Specifi
return false;

boolean isStruct = toClassModel.isStructInit();
List<HaxeBaseMemberModel> toMembers = toClassModel.getAllMembers(to.getGenericResolver());
List<HaxeBaseMemberModel> fromMembers = fromClassModel.getAllMembers(from.getGenericResolver());
boolean isObjectLiteral = fromClassModel.isObjectLiteral();

HaxeGenericResolver toResolver = to.getGenericResolver();
HaxeGenericResolver fromResolver = from.getGenericResolver();
List<HaxeBaseMemberModel> toMembers = toClassModel.getAllMembers(toResolver);
List<HaxeBaseMemberModel> fromMembers = fromClassModel.getAllMembers(fromResolver);

boolean allMembersMatches = true;
for (HaxeBaseMemberModel toMember : toMembers) {
Expand Down Expand Up @@ -583,18 +658,24 @@ private static boolean containsAllMembers(SpecificHaxeClassReference to, Specifi
if (checkAnonymousMemberTypes) {
HaxeBaseMemberModel fromMember = first.get();
if (context != null && context.getFromOrigin() != null) {
Boolean canAssign = containsMembersRecursionGuard.computePreventingRecursion(context.getFromOrigin(), false, () -> {
ResultHolder toType = toMember.getResultType();
ResultHolder fromType = fromMember.getResultType();
return toType.canAssign(fromType);
});

// in case recursion guard is triggered
if (canAssign == null) {


ResultHolder toType = containsMembersRecursionGuard.computePreventingRecursion(context.getFromOrigin(), false, () ->
toMember.getResultType(toResolver)
);
ResultHolder fromType = containsMembersRecursionGuard.computePreventingRecursion(context.getFromOrigin(), false, () ->
fromMember.getResultType(isObjectLiteral ? toResolver : fromResolver)
);

if(toType == null || fromType == null) {
log.warn("Unable to evaluate fieldType compatibility");
return true;
}
else if (!canAssign) {
context.addWrongTypeMember(fromMember.getPresentableText(null), toMember.getPresentableText(null));

if (!toType.canAssign(fromType)) {
String fromText = fromMember.getPresentableText(null, isObjectLiteral ? toResolver : fromResolver);
String toText = toMember.getPresentableText(null, toResolver);
context.addWrongTypeMember(fromText, toText);
allMembersMatches = false;
}
}
Expand All @@ -609,8 +690,8 @@ else if (!canAssign) {
if(context != null)context.addMissingMember(missingFieldName);
allMembersMatches = false;
}

}

// check object literals for too many fields
if (isStruct && fromClassModel.isObjectLiteral()) {
for (HaxeBaseMemberModel member : fromMembers) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
package;
@:structInit class MyStruct {
//NOTE: should not show warning about final when in structInit class
final name:String;
var age:Int;
var height:Float = 3.0; // init = same as optional
@:optional var address:String;

}

class Test {
public function new() {
// correct
// CORRECT
var s1:MyStruct = {name:"name", age:30};
var s2:MyStruct = {name:"name", age:30, height:1.0, address:"address 1"};

// WRONG
var <error descr="Incompatible type: missing member(s) age:Int">s3:MyStruct = {name:"name"}</error>; // missing field
var <error descr="Incompatible type: {...} should be MyStruct">s4:MyStruct = {name:"name", age:30, address:"address 1", extra:"field"}</error>; // to many fields

// CORRECT (TypeParameter)
var typeParamA:MyTypeParamStruct<String> = {valueA:"name", valueB:30};
// WRONG (TypeParameter)
var <error descr="Incompatible type: Incompatible member type(s) (have 'valueA:String' wants 'valueA:Int')">typeParamB:MyTypeParamStruct<Int> = {valueA:"name", valueB:30}</error>;

//CORRECT (constructor)
var NormalConstructor:ConstrcutorStruct<String> = {name:"str", value:1.0, type: ValueA};
var diffrentOrder:ConstrcutorStruct<String> = {value:1.0, type: ValueA, name:"str"};
var usingDefaults:ConstrcutorStruct<String> = {value:1.0, name:"str"};

// WRONG (constructor)
var <error descr="Incompatible type: Incompatible member type(s) (have 'value:String' wants 'value:Float')">wrongType:ConstrcutorStruct<String> = {value:"1.0", name:"str"}</error>;
var <error descr="Incompatible type: Incompatible member type(s) (have 'name:Int' wants 'name:String')">wrongTypeTypeParameter:ConstrcutorStruct<String> = {value:1.0, name:1}</error>;
var <error descr="Incompatible type: missing member(s) value">missingField:ConstrcutorStruct<String> = { name:"str"}</error>;

}
}

@:structInit class MyStruct {
//NOTE: should not show warning about final when in structInit class
final name:String;
var age:Int;
var height:Float = 3.0; // init = same as optional
@:optional var address:String;

}

@:structInit class MyTypeParamStruct<T> {
var valueA:T;
var valueB:Int;

}

enum TestNum { ValueA; ValueB; }

@:structInit
class ConstrcutorStruct<T> {
public var name(default,null) : T;
public var value(default,null) : Float;
public var type(default,null) : TestNum;
public var intenral(default,null) : String;

public inline function new( name : T, value : Float, type = ValueB ) {
this.name = name;
this.type = type;
this.value = value;
this.intenral = "internalValue";
}
}

0 comments on commit d7b6ba8

Please sign in to comment.