/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.js.translate.declaration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.backend.common.DataClassMethodGenerator;
import org.jetbrains.kotlin.descriptors.*;
import org.jetbrains.kotlin.js.backend.ast.*;
import org.jetbrains.kotlin.js.translate.context.Namer;
import org.jetbrains.kotlin.js.translate.context.TranslationContext;
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils;
import org.jetbrains.kotlin.js.translate.utils.UtilsKt;
import org.jetbrains.kotlin.psi.KtClassOrObject;
import org.jetbrains.kotlin.psi.KtParameter;
import org.jetbrains.kotlin.resolve.BindingContext;
import org.jetbrains.kotlin.resolve.BindingContextUtils;
import org.jetbrains.kotlin.resolve.descriptorUtil.DescriptorUtilsKt;
import java.util.ArrayList;
import java.util.List;
import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.and;
import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.or;
class JsDataClassGenerator extends DataClassMethodGenerator {
private final TranslationContext context;
JsDataClassGenerator(KtClassOrObject klass, TranslationContext context) {
super(klass, context.bindingContext());
this.context = context;
}
@Override
public void generateComponentFunction(@NotNull FunctionDescriptor function, @NotNull ValueParameterDescriptor parameter) {
PropertyDescriptor propertyDescriptor = context.bindingContext().get(BindingContext.VALUE_PARAMETER_AS_PROPERTY, parameter);
assert propertyDescriptor != null : "Property descriptor is expected to be non-null";
JsFunction functionObject = generateJsMethod(function);
JsExpression returnExpression = JsAstUtils.pureFqn(context.getNameForDescriptor(propertyDescriptor), JsLiteral.THIS);
functionObject.getBody().getStatements().add(new JsReturn(returnExpression));
}
@Override
public void generateCopyFunction(@NotNull FunctionDescriptor function, @NotNull List<? extends KtParameter> constructorParameters) {
JsFunction functionObj = generateJsMethod(function);
assert function.getValueParameters().size() == constructorParameters.size();
List<JsExpression> constructorArguments = new ArrayList<>(constructorParameters.size());
for (int i = 0; i < constructorParameters.size(); i++) {
KtParameter constructorParam = constructorParameters.get(i);
ValueParameterDescriptor parameterDescriptor = (ValueParameterDescriptor) BindingContextUtils.getNotNull(
context.bindingContext(), BindingContext.VALUE_PARAMETER, constructorParam);
PropertyDescriptor propertyDescriptor = BindingContextUtils.getNotNull(
context.bindingContext(), BindingContext.VALUE_PARAMETER_AS_PROPERTY, parameterDescriptor);
JsName fieldName = context.getNameForDescriptor(propertyDescriptor);
JsName paramName = JsScope.declareTemporaryName(context.getNameForDescriptor(parameterDescriptor).getIdent());
functionObj.getParameters().add(new JsParameter(paramName));
JsExpression argumentValue;
JsExpression parameterValue = new JsNameRef(paramName);
if (!constructorParam.hasValOrVar()) {
assert !DescriptorUtilsKt.hasDefaultValue(function.getValueParameters().get(i));
// Caller cannot rely on default value and pass undefined here.
argumentValue = parameterValue;
}
else {
JsExpression defaultCondition = JsAstUtils.equality(new JsNameRef(paramName), Namer.getUndefinedExpression());
argumentValue = new JsConditional(defaultCondition, new JsNameRef(fieldName, JsLiteral.THIS), parameterValue);
}
constructorArguments.add(argumentValue);
}
ClassDescriptor classDescriptor = (ClassDescriptor) function.getContainingDeclaration();
ClassConstructorDescriptor constructor = classDescriptor.getUnsubstitutedPrimaryConstructor();
assert constructor != null : "Data class should have primary constructor: " + classDescriptor;
JsExpression constructorRef = context.getInnerReference(constructor);
JsNew returnExpression = new JsNew(constructorRef, constructorArguments);
if (context.shouldBeDeferred(constructor)) {
context.deferConstructorCall(constructor, returnExpression.getArguments());
}
functionObj.getBody().getStatements().add(new JsReturn(returnExpression));
}
@Override
public void generateToStringMethod(@NotNull FunctionDescriptor function, @NotNull List<? extends PropertyDescriptor> classProperties) {
// TODO: relax this limitation, with the data generation logic fixed.
assert !classProperties.isEmpty();
JsFunction functionObj = generateJsMethod(function);
JsProgram jsProgram = context.program();
JsExpression result = null;
for (int i = 0; i < classProperties.size(); i++) {
String printName = classProperties.get(i).getName().asString();
JsName name = context.getNameForDescriptor(classProperties.get(i));
JsExpression literal = jsProgram.getStringLiteral((i == 0 ? (getClassDescriptor().getName() + "(") : ", ") + printName + "=");
JsExpression expr = new JsInvocation(context.namer().kotlin("toString"), new JsNameRef(name, JsLiteral.THIS));
JsExpression component = JsAstUtils.sum(literal, expr);
if (result == null) {
result = component;
}
else {
result = JsAstUtils.sum(result, component);
}
}
assert result != null;
result = JsAstUtils.sum(result, jsProgram.getStringLiteral(")"));
functionObj.getBody().getStatements().add(new JsReturn(result));
}
@Override
public void generateHashCodeMethod(@NotNull FunctionDescriptor function, @NotNull List<? extends PropertyDescriptor> classProperties) {
JsFunction functionObj = generateJsMethod(function);
JsProgram jsProgram = context.program();
List<JsStatement> statements = functionObj.getBody().getStatements();
JsName varName = functionObj.getScope().declareName("result");
statements.add(new JsVars(new JsVars.JsVar(varName, JsNumberLiteral.ZERO)));
for (PropertyDescriptor prop : classProperties) {
// TODO: we should statically check that we can call hashCode method directly.
JsName name = context.getNameForDescriptor(prop);
JsExpression component = new JsInvocation(context.namer().kotlin("hashCode"), new JsNameRef(name, JsLiteral.THIS));
JsExpression newHashValue = JsAstUtils.sum(JsAstUtils.mul(new JsNameRef(varName), jsProgram.getNumberLiteral(31)), component);
JsExpression assignment = JsAstUtils.assignment(new JsNameRef(varName),
new JsBinaryOperation(JsBinaryOperator.BIT_OR, newHashValue,
jsProgram.getNumberLiteral(0)));
statements.add(assignment.makeStmt());
}
statements.add(new JsReturn(new JsNameRef(varName)));
}
@Override
public void generateEqualsMethod(@NotNull FunctionDescriptor function, @NotNull List<? extends PropertyDescriptor> classProperties) {
assert !classProperties.isEmpty();
JsFunction functionObj = generateJsMethod(function);
JsFunctionScope funScope = functionObj.getScope();
JsName paramName = funScope.declareName("other");
functionObj.getParameters().add(new JsParameter(paramName));
JsExpression referenceEqual = JsAstUtils.equality(JsLiteral.THIS, new JsNameRef(paramName));
JsExpression isNotNull = JsAstUtils.inequality(new JsNameRef(paramName), JsLiteral.NULL);
JsExpression otherIsObject = JsAstUtils.typeOfIs(paramName.makeRef(), context.program().getStringLiteral("object"));
JsExpression prototypeEqual =
JsAstUtils.equality(new JsInvocation(new JsNameRef("getPrototypeOf", new JsNameRef("Object")), JsLiteral.THIS),
new JsInvocation(new JsNameRef("getPrototypeOf", new JsNameRef("Object")), new JsNameRef(paramName)));
JsExpression fieldChain = null;
for (PropertyDescriptor prop : classProperties) {
JsName name = context.getNameForDescriptor(prop);
JsExpression next = new JsInvocation(context.namer().kotlin("equals"),
new JsNameRef(name, JsLiteral.THIS),
new JsNameRef(name, new JsNameRef(paramName)));
if (fieldChain == null) {
fieldChain = next;
}
else {
fieldChain = and(fieldChain, next);
}
}
assert fieldChain != null;
JsExpression returnExpression = or(referenceEqual, and(isNotNull, and(otherIsObject, and(prototypeEqual, fieldChain))));
functionObj.getBody().getStatements().add(new JsReturn(returnExpression));
}
private JsFunction generateJsMethod(@NotNull FunctionDescriptor functionDescriptor) {
JsFunction functionObject = context.createRootScopedFunction(functionDescriptor);
ClassDescriptor containingClass = (ClassDescriptor) functionDescriptor.getContainingDeclaration();
context.addDeclarationStatement(UtilsKt.addFunctionToPrototype(context, containingClass, functionDescriptor, functionObject));
return functionObject;
}
}