package com.intellij.lang.javascript.generation;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.ide.util.MemberChooser;
import com.intellij.lang.javascript.psi.*;
import com.intellij.lang.javascript.psi.ecmal4.JSAttributeList;
import com.intellij.lang.javascript.psi.ecmal4.JSClass;
import com.intellij.lang.javascript.psi.impl.JSPsiImplUtils;
import com.intellij.lang.javascript.psi.resolve.JSInheritanceUtil;
import com.intellij.lang.javascript.psi.resolve.JSResolveUtil;
import com.intellij.lang.javascript.psi.resolve.ResolveProcessor;
import com.intellij.lang.javascript.psi.util.JSUtils;
import com.intellij.lang.javascript.validation.fixes.BaseCreateMethodsFix;
import com.intellij.lang.javascript.validation.fixes.JSAttributeListWrapper;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.ResolveState;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public class JavaScriptGenerateDelegatesHandler extends BaseJSGenerateHandler {
public static final String[] PRIMITIVE_TYPES = JSCommonTypeNames.ALL;
private static final Logger LOG = Logger.getInstance("#com.intellij.lang.javascript.generation.JavaScriptGenerateDelegatesHandler");
@Override
public boolean isValidFor(Editor editor, PsiFile file) {
if (!super.isValidFor(editor, file)) {
return false;
}
return !findCandidateFields(findClass(file, editor)).isEmpty();
}
@Override
public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
final JSClass jsClass = findClass(file, editor);
if (jsClass == null) return;
Collection<JSField> fields = findCandidateFields(jsClass);
final JSField field;
if (ApplicationManager.getApplication().isUnitTestMode()) {
LOG.assertTrue(fields.size() == 1);
field = fields.iterator().next();
}
else {
final MemberChooser<JSNamedElementNode> targetChooser = createMemberChooserDialog(project, jsClass, wrap(fields), false, false,
CodeInsightBundle
.message(
"generate.delegate.target.chooser.title"));
targetChooser.show();
if (targetChooser.getExitCode() != DialogWrapper.OK_EXIT_CODE) return;
field = (JSField)targetChooser.getSelectedElements().get(0).getPsiElement();
}
JSType fieldType = field.getType();
if (fieldType == null) return;
JSClass fieldClass = fieldType.resolveClass();
if (fieldClass == null) return;
final boolean allowPackageLocal = !JSPsiImplUtils.differentPackageName(StringUtil.getPackageName(fieldClass.getQualifiedName()),
StringUtil.getPackageName(jsClass.getQualifiedName()));
// don't add members along with their supers
class MemberDescriptor {
private final String name;
@Nullable private final JSFunction.FunctionKind kind;
public MemberDescriptor(JSFunction method) {
name = method.getName();
kind = method.getKind();
}
public MemberDescriptor(JSVariable field) {
name = field.getName();
kind = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MemberDescriptor that = (MemberDescriptor)o;
if (kind != that.kind) return false;
if (!name.equals(that.name)) return false;
return true;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + (kind != null ? kind.hashCode() : 0);
return result;
}
}
final Map<MemberDescriptor, JSNamedElement> memberCandidates = new HashMap<>();
ResolveProcessor p = new ResolveProcessor(null) {
{
setToProcessHierarchy(true);
}
@Override
public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) {
JSClass clazz = JSUtils.getMemberContainingClass(element);
if (clazz == null || JSResolveUtil.isObjectClass(clazz) || clazz == jsClass) {
return true;
}
if (element instanceof JSFunction) {
JSFunction method = (JSFunction)element;
if (memberCandidates.containsKey(method.getName())) {
return true;
}
JSAttributeList attributeList = method.getAttributeList();
if (attributeList.getAccessType() == JSAttributeList.AccessType.PRIVATE ||
attributeList.getAccessType() == JSAttributeList.AccessType.PROTECTED) {
return true;
}
if (!allowPackageLocal &&
attributeList.getNamespace() == null &&
attributeList.getAccessType() == JSAttributeList.AccessType.PACKAGE_LOCAL) {
return true;
}
if (method.getKind() == JSFunction.FunctionKind.CONSTRUCTOR) {
return true;
}
if (attributeList.hasModifier(JSAttributeList.ModifierType.STATIC)) {
return true;
}
if (JSInheritanceUtil.findMethodInClass(method, jsClass, true) != null) {
return true;
}
memberCandidates.put(new MemberDescriptor(method), method);
}
else if (element instanceof JSVariable) {
JSVariable f = (JSVariable)element;
if (memberCandidates.containsKey(f.getName())) {
return true;
}
JSAttributeList attributeList = f.getAttributeList();
if (attributeList.getAccessType() == JSAttributeList.AccessType.PRIVATE ||
attributeList.getAccessType() == JSAttributeList.AccessType.PROTECTED) {
return true;
}
if (!allowPackageLocal && attributeList.getAccessType() == JSAttributeList.AccessType.PACKAGE_LOCAL) {
return true;
}
if (jsClass.findFunctionByName(f.getName()) != null) {
return true;
}
memberCandidates.put(new MemberDescriptor(f), f);
}
return true;
}
};
fieldClass.processDeclarations(p, ResolveState.initial(), fieldClass, fieldClass);
Collection<JSNamedElementNode> selected;
if (ApplicationManager.getApplication().isUnitTestMode()) {
LOG.assertTrue(!memberCandidates.isEmpty());
selected = wrap(memberCandidates.values());
}
else {
final MemberChooser<JSNamedElementNode> methodsChooser =
createMemberChooserDialog(project, jsClass, wrap(memberCandidates.values()), false,
true, CodeInsightBundle.message("generate.delegate.method.chooser.title"));
methodsChooser.show();
if (methodsChooser.getExitCode() != DialogWrapper.OK_EXIT_CODE) return;
selected = methodsChooser.getSelectedElements();
}
BaseCreateMethodsFix fix = new BaseCreateMethodsFix<JSNamedElement>(jsClass) {
final JavaScriptGenerateAccessorHandler.MyBaseCreateMethodsFix generateGetterFix =
new JavaScriptGenerateAccessorHandler.MyBaseCreateMethodsFix(JavaScriptGenerateAccessorHandler.GenerationMode.Getter, jsClass,
null, false, field.getName());
final JavaScriptGenerateAccessorHandler.MyBaseCreateMethodsFix generateSetterFix =
new JavaScriptGenerateAccessorHandler.MyBaseCreateMethodsFix(JavaScriptGenerateAccessorHandler.GenerationMode.Setter, jsClass,
null, false, field.getName());
@Override
protected void adjustAttributeList(JSAttributeListWrapper attributeListWrapper, JSNamedElement function) {
attributeListWrapper.overrideAccessType(JSAttributeList.AccessType.PUBLIC);
attributeListWrapper
.overrideModifier(JSAttributeList.ModifierType.STATIC, field.getAttributeList().hasModifier(JSAttributeList.ModifierType.STATIC));
for (JSAttributeList.ModifierType modifierType : new JSAttributeList.ModifierType[]{JSAttributeList.ModifierType.NATIVE,
JSAttributeList.ModifierType.DYNAMIC, JSAttributeList.ModifierType.FINAL, JSAttributeList.ModifierType.OVERRIDE,
JSAttributeList.ModifierType.VIRTUAL}) {
attributeListWrapper.overrideModifier(modifierType, false);
}
}
@Override
protected void processElements(Project project, MultiMap<String, String> types, Set<JSNamedElement> elementsToProcess) {
for (JSNamedElement e : elementsToProcess) {
if (e instanceof JSFunction) {
anchor = doAddOneMethod(project, buildFunctionText(e, types), anchor);
}
else {
anchor = doAddOneMethod(project, generateGetterFix.buildFunctionText(e, types), anchor);
anchor = doAddOneMethod(project, generateSetterFix.buildFunctionText(e, types), anchor);
}
}
}
@Override
protected String buildFunctionBodyText(final String retType, final JSParameterList parameterList, final JSNamedElement element) {
return OverrideMethodsFix.buildDelegatingText(retType, parameterList, ((JSFunction)element), field.getName(),
anchor != null ? anchor : myJsClass);
}
};
doInvoke(project, editor, file, selected, fix);
}
private static Collection<JSNamedElementNode> wrap(Collection<? extends JSNamedElement> items) {
final List<JSNamedElementNode> targetCandidates = new ArrayList<>(items.size());
for (JSNamedElement field : items) {
targetCandidates.add(new JSNamedElementNode(field));
}
if (ApplicationManager.getApplication().isUnitTestMode()) {
Collections.sort(targetCandidates, (o1, o2) -> o1.getText().compareTo(o2.getText()));
}
return targetCandidates;
}
@Override
protected String getTitleKey() {
return null;
}
@Override
protected void collectCandidates(JSClass clazz, Collection<JSNamedElementNode> candidates) {
Collection<JSField> fields = findCandidateFields(clazz);
for (JSField field : fields) {
candidates.add(new JSNamedElementNode(field));
}
}
@Override
protected BaseCreateMethodsFix createFix(JSClass clazz) {
return null;
}
private static Collection<JSField> findCandidateFields(JSClass clazz) {
Collection<JSField> result = new ArrayList<>();
for (JSField field : clazz.getFields()) {
JSType type = field.getType();
JSClass fieldType = type != null ? type.resolveClass() : null;
if (fieldType != null &&
!ArrayUtil.contains(fieldType.getQualifiedName(), PRIMITIVE_TYPES) &&
!JSInheritanceUtil.isParentClass(clazz, fieldType, false)) {
result.add(field);
}
}
return result;
}
}