// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // @dart = 2.9 library kernel.transformations.value_class; import 'package:kernel/type_environment.dart'; import '../ast.dart'; import '../kernel.dart'; import '../core_types.dart' show CoreTypes; import '../class_hierarchy.dart' show ClassHierarchy; import './scanner.dart'; class ValueClassScanner extends ClassScanner { ValueClassScanner() : super(null); bool predicate(Class node) => isValueClass(node); } class JenkinsClassScanner extends ClassScanner { JenkinsClassScanner(Scanner next) : super(next); bool predicate(Class node) { return node.name == "JenkinsSmiHash"; } } class HashCombineMethodsScanner extends ProcedureScanner { HashCombineMethodsScanner() : super(null); bool predicate(Procedure node) { return node.name.text == "combine" || node.name.text == "finish"; } } class AllMemberScanner extends MemberScanner { AllMemberScanner(Scanner next) : super(next); bool predicate(Member member) => true; } // Scans and matches all copyWith invocations were the reciever is _ as dynamic // It will filter out the results that are not value classes afterwards class ValueClassCopyWithScanner extends MethodInvocationScanner { ValueClassCopyWithScanner() : super(null); // The matching construct followed in unit-tests is: // @valueClass V {} // V v; // (v as dynamic).copyWith() as V bool predicate(MethodInvocation node) { return node.name.text == "copyWith" && _isValueClassAsConstruct(node.receiver); } bool _isValueClassAsConstruct(Expression node) { return node is AsExpression && node.type is DynamicType; } } void transformComponent(Component node, CoreTypes coreTypes, ClassHierarchy hierarchy, TypeEnvironment typeEnvironment) { ValueClassScanner scanner = new ValueClassScanner(); ScanResult valueClasses = scanner.scan(node); for (Class valueClass in valueClasses.targets.keys) { transformValueClass(valueClass, coreTypes, hierarchy, typeEnvironment); } treatCopyWithCallSites(node, coreTypes, typeEnvironment, hierarchy); for (Class valueClass in valueClasses.targets.keys) { removeValueClassAnnotation(valueClass); } } void transformValueClass(Class cls, CoreTypes coreTypes, ClassHierarchy hierarchy, TypeEnvironment typeEnvironment) { Constructor syntheticConstructor = null; for (Constructor constructor in cls.constructors) { if (constructor.isSynthetic) { syntheticConstructor = constructor; } } List allVariables = queryAllInstanceVariables(cls); List allVariablesList = allVariables.toList(); allVariablesList.sort((a, b) => a.name.compareTo(b.name)); addConstructor(cls, coreTypes, syntheticConstructor); addEqualsOperator(cls, coreTypes, hierarchy, allVariablesList); addHashCode(cls, coreTypes, hierarchy, allVariablesList); addToString(cls, coreTypes, hierarchy, allVariablesList); addCopyWith(cls, coreTypes, hierarchy, allVariablesList, syntheticConstructor, typeEnvironment); } void addConstructor( Class cls, CoreTypes coreTypes, Constructor syntheticConstructor) { Constructor superConstructor = null; for (Constructor constructor in cls.superclass.constructors) { if (constructor.name.text == "") { superConstructor = constructor; } } List superParameters = superConstructor .function.namedParameters .map((e) => VariableDeclaration(e.name, type: e.type) ..parent = syntheticConstructor.function) .toList(); Map ownFields = Map.fromIterable(cls.fields, key: (f) => f.name.text, value: (f) => VariableDeclaration(f.name.text, type: f.type, isRequired: true) ..parent = syntheticConstructor.function); List initializersConstructor = cls.fields .map((f) => FieldInitializer(f, VariableGet(ownFields[f.name.text])) ..parent = syntheticConstructor) .toList(); initializersConstructor.add(SuperInitializer(superConstructor, Arguments(superParameters.map((f) => VariableGet(f)).toList())) ..parent = syntheticConstructor); syntheticConstructor.function.namedParameters ..clear() ..addAll(ownFields.values) ..addAll(superParameters); syntheticConstructor.initializers = initializersConstructor; } void addEqualsOperator(Class cls, CoreTypes coreTypes, ClassHierarchy hierarchy, List allVariablesList) { List allVariables = allVariablesList.toList(); for (Procedure procedure in cls.procedures) { if (procedure.kind == ProcedureKind.Operator && procedure.name.text == "==") { // ==operator is already implemented, spec is to do nothing return; } } DartType returnType = coreTypes.boolRawType(cls.enclosingLibrary.nonNullable); DartType myType = coreTypes.thisInterfaceType(cls, Nullability.nonNullable); VariableDeclaration other = VariableDeclaration("other", type: coreTypes.objectRawType(Nullability.nonNullable)); Map targetsEquals = new Map(); Map targets = new Map(); for (VariableDeclaration variable in allVariables) { Member target = coreTypes.objectEquals; Member targetEquals = coreTypes.objectEquals; DartType fieldsType = variable.type; if (fieldsType is InterfaceType) { targetEquals = hierarchy.getInterfaceMember(fieldsType.classNode, Name("==")); target = hierarchy.getInterfaceMember(cls, Name(variable.name)); } targetsEquals[variable] = targetEquals; targets[variable] = target; } Procedure equalsOperator = Procedure( Name("=="), ProcedureKind.Operator, FunctionNode( ReturnStatement(allVariables .map((f) => MethodInvocation( PropertyGet(ThisExpression(), Name(f.name), targets[f]), Name("=="), Arguments([ PropertyGet( VariableGet(other, myType), Name(f.name), targets[f]) ]), targetsEquals[f])) .fold( IsExpression(VariableGet(other), myType), (previousValue, element) => LogicalExpression( previousValue, LogicalExpressionOperator.AND, element))), returnType: returnType, positionalParameters: [other]), fileUri: cls.fileUri) ..fileOffset = cls.fileOffset; cls.addProcedure(equalsOperator); } void addHashCode(Class cls, CoreTypes coreTypes, ClassHierarchy hierarchy, List allVariablesList) { List allVariables = allVariablesList.toList(); for (Procedure procedure in cls.procedures) { if (procedure.kind == ProcedureKind.Getter && procedure.name.text == "hashCode") { // hashCode getter is already implemented, spec is to do nothing return; } } DartType returnType = coreTypes.intRawType(cls.enclosingLibrary.nonNullable); Procedure hashCombine, hashFinish; HashCombineMethodsScanner hashCombineMethodsScanner = new HashCombineMethodsScanner(); JenkinsClassScanner jenkinsScanner = new JenkinsClassScanner(hashCombineMethodsScanner); ScanResult hashMethodsResult = jenkinsScanner.scan(cls.enclosingLibrary.enclosingComponent); for (Class clazz in hashMethodsResult.targets.keys) { for (Procedure procedure in hashMethodsResult.targets[clazz].targets.keys) { if (procedure.name.text == "combine") hashCombine = procedure; if (procedure.name.text == "finish") hashFinish = procedure; } } Map targetsHashcode = new Map(); Map targets = new Map(); for (VariableDeclaration variable in allVariables) { Member target = coreTypes.objectEquals; Member targetHashcode = coreTypes.objectEquals; DartType fieldsType = variable.type; if (fieldsType is InterfaceType) { targetHashcode = hierarchy.getInterfaceMember(fieldsType.classNode, Name("hashCode")); target = hierarchy.getInterfaceMember(cls, Name(variable.name)); } targetsHashcode[variable] = targetHashcode; targets[variable] = target; } cls.addProcedure(Procedure( Name("hashCode"), ProcedureKind.Getter, FunctionNode( ReturnStatement(StaticInvocation( hashFinish, Arguments([ allVariables .map((f) => (PropertyGet( PropertyGet(ThisExpression(), Name(f.name), targets[f]), Name("hashCode"), targetsHashcode[f]))) .fold( PropertyGet( StringLiteral( cls.enclosingLibrary.importUri.toString() + cls.name), Name("hashCode"), hierarchy.getInterfaceMember( coreTypes.stringClass, Name("hashCode"))), (previousValue, element) => StaticInvocation( hashCombine, Arguments([previousValue, element]))) ]))), returnType: returnType), fileUri: cls.fileUri) ..fileOffset = cls.fileOffset); } void addToString(Class cls, CoreTypes coreTypes, ClassHierarchy hierarchy, List allVariablesList) { List wording = [StringLiteral("${cls.name}(")]; for (VariableDeclaration variable in allVariablesList) { wording.add(StringLiteral("${variable.name}: ")); wording.add(MethodInvocation( PropertyGet(ThisExpression(), Name(variable.name), hierarchy.getInterfaceMember(cls, Name(variable.name))), Name("toString"), Arguments([]), (variable.type is InterfaceType) ? hierarchy.getInterfaceMember( (variable.type as InterfaceType).classNode, Name("toString")) : null)); wording.add(StringLiteral(", ")); } if (allVariablesList.length != 0) { wording[wording.length - 1] = StringLiteral(")"); } else { wording.add(StringLiteral(")")); } DartType returnType = coreTypes.stringRawType(cls.enclosingLibrary.nonNullable); cls.addProcedure(Procedure( Name("toString"), ProcedureKind.Method, FunctionNode(ReturnStatement(StringConcatenation(wording)), returnType: returnType), fileUri: cls.fileUri) ..fileOffset = cls.fileOffset); } void addCopyWith( Class cls, CoreTypes coreTypes, ClassHierarchy hierarchy, List allVariablesList, Constructor syntheticConstructor, TypeEnvironment typeEnvironment) { List allVariables = allVariablesList.toList(); Map targetsEquals = new Map(); Map targets = new Map(); for (VariableDeclaration variable in allVariables) { Member target = coreTypes.objectEquals; Member targetEquals = coreTypes.objectEquals; DartType fieldsType = variable.type; if (fieldsType is InterfaceType) { targetEquals = hierarchy.getInterfaceMember(fieldsType.classNode, Name("==")); target = hierarchy.getInterfaceMember(cls, Name(variable.name)); } targetsEquals[variable] = targetEquals; targets[variable] = target; } cls.addProcedure(Procedure( Name("copyWith"), ProcedureKind.Method, FunctionNode( ReturnStatement(ConstructorInvocation( syntheticConstructor, Arguments([], named: allVariables .map((f) => NamedExpression(f.name, VariableGet(f))) .toList()))), namedParameters: allVariables), fileUri: cls.fileUri) ..fileOffset = cls.fileOffset); } List queryAllInstanceVariables(Class cls) { Constructor superConstructor = null; for (Constructor constructor in cls.superclass.constructors) { if (constructor.name.text == "") { superConstructor = constructor; } } return superConstructor.function.namedParameters .map( (f) => VariableDeclaration(f.name, type: f.type)) .toList() ..addAll(cls.fields.map( (f) => VariableDeclaration(f.name.text, type: f.type))); } void removeValueClassAnnotation(Class cls) { int valueClassAnnotationIndex; for (int annotationIndex = 0; annotationIndex < cls.annotations.length; annotationIndex++) { Expression annotation = cls.annotations[annotationIndex]; if (annotation is ConstantExpression && annotation.constant is StringConstant) { StringConstant constant = annotation.constant; if (constant.value == 'valueClass') { valueClassAnnotationIndex = annotationIndex; } } } cls.annotations.removeAt(valueClassAnnotationIndex); } void treatCopyWithCallSites(Component component, CoreTypes coreTypes, TypeEnvironment typeEnvironment, ClassHierarchy hierarchy) { ValueClassCopyWithScanner valueCopyWithScanner = new ValueClassCopyWithScanner(); AllMemberScanner copyWithScanner = AllMemberScanner(valueCopyWithScanner); ScanResult copyWithCallSites = copyWithScanner.scan(component); for (Member memberWithCopyWith in copyWithCallSites.targets.keys) { if (copyWithCallSites.targets[memberWithCopyWith].targets != null) { StaticTypeContext staticTypeContext = StaticTypeContext(memberWithCopyWith, typeEnvironment); for (MethodInvocation copyWithCall in copyWithCallSites.targets[memberWithCopyWith].targets.keys) { AsExpression receiver = copyWithCall.receiver as AsExpression; Expression valueClassInstance = receiver.operand; DartType valueClassType = valueClassInstance.getStaticType(staticTypeContext); if (valueClassType is InterfaceType) { Class valueClass = valueClassType.classNode; if (isValueClass(valueClass)) { treatCopyWithCallSite( valueClass, copyWithCall, coreTypes, hierarchy); } } } } } } void treatCopyWithCallSite(Class valueClass, MethodInvocation copyWithCall, CoreTypes coreTypes, ClassHierarchy hierarchy) { Map preTransformationArguments = new Map(); for (NamedExpression argument in copyWithCall.arguments.named) { preTransformationArguments[argument.name] = argument.value; } Constructor syntheticConstructor; for (Constructor constructor in valueClass.constructors) { if (constructor.isSynthetic) { syntheticConstructor = constructor; } } List allArguments = syntheticConstructor.function.namedParameters; VariableDeclaration letVariable = VariableDeclaration.forValue(copyWithCall.receiver); Arguments postTransformationArguments = Arguments.empty(); for (VariableDeclaration argument in allArguments) { if (preTransformationArguments.containsKey(argument.name)) { postTransformationArguments.named.add(NamedExpression( argument.name, preTransformationArguments[argument.name]) ..parent = postTransformationArguments); } else { postTransformationArguments.named.add(NamedExpression(argument.name, PropertyGet(VariableGet(letVariable), Name(argument.name))) ..parent = postTransformationArguments); } } copyWithCall.replaceWith(Let( letVariable, MethodInvocation(VariableGet(letVariable), Name("copyWith"), postTransformationArguments))); } bool isValueClass(Class node) { for (Expression annotation in node.annotations) { if (annotation is ConstantExpression && annotation.constant is StringConstant) { StringConstant constant = annotation.constant; if (constant.value == "valueClass") { return true; } } } return false; }