package com.intellij.lang.javascript.validation.fixes; import com.intellij.codeInsight.template.Template; import com.intellij.lang.ASTNode; import com.intellij.lang.javascript.DialectDetector; import com.intellij.lang.javascript.JSBundle; import com.intellij.lang.javascript.JSTokenTypes; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.lang.javascript.flex.ECMAScriptImportOptimizer; import com.intellij.lang.javascript.flex.ImportUtils; 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.ecmal4.JSSuperExpression; import com.intellij.lang.javascript.psi.ecmal4.XmlBackedJSClass; import com.intellij.lang.javascript.psi.impl.JSChangeUtil; 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.refactoring.FormatFixer; import com.intellij.lang.javascript.refactoring.changeSignature.*; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; import com.intellij.psi.search.searches.ReferencesSearch; import com.intellij.refactoring.changeSignature.CallerChooserBase; import com.intellij.refactoring.changeSignature.MethodNodeBase; import com.intellij.ui.treeStructure.Tree; import com.intellij.usageView.UsageInfo; import com.intellij.util.Consumer; import com.intellij.util.containers.HashSet; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; public class CreateConstructorFix extends CreateJSFunctionIntentionAction { @NotNull private final JSClass myClass; private final JSReferenceExpression myRefExpr; private final JSCallExpression myNode; private CreateConstructorFix(@NotNull JSClass clazz, JSReferenceExpression refExpr, JSCallExpression node) { super(clazz.getName(), true); myClass = clazz; myRefExpr = refExpr; myNode = node; } @Nullable public static CreateConstructorFix createIfApplicable(final JSCallExpression node) { final JSClass clazz; final JSReferenceExpression reference; if (node instanceof JSNewExpression) { JSExpression methodExpression = node.getMethodExpression(); if (!(methodExpression instanceof JSReferenceExpression)) { return null; } PsiElement resolved = ((JSReferenceExpression)methodExpression).resolve(); if (!(resolved instanceof JSClass) || resolved instanceof XmlBackedJSClass || ((JSClass)resolved).isInterface()) { return null; } clazz = (JSClass)resolved; reference = (JSReferenceExpression)methodExpression; } else { JSExpression methodExpression = node.getMethodExpression(); if (!(methodExpression instanceof JSSuperExpression)) { return null; } JSClass containingClass = JSResolveUtil.getClassOfContext(node); if (containingClass == null) { return null; } clazz = containingClass.getSuperClasses()[0]; if (clazz.isInterface()) { return null; } reference = (JSReferenceExpression)clazz.findNameIdentifier().getPsi(); } return new CreateConstructorFix(clazz, reference, node); } @NotNull @Override protected Pair<JSReferenceExpression, PsiElement> calculateAnchors(PsiElement psiElement) { ASTNode lbrace = myClass.getNode().findChildByType(JSTokenTypes.LBRACE); return Pair.create(myRefExpr, lbrace.getPsi()); } @Override protected void applyFix(final Project project, final PsiElement psiElement, PsiFile file, Editor editor) { final AtomicInteger count = new AtomicInteger(); ReferencesSearch.search(myClass, myClass.getUseScope()).forEach( psiReference -> !isClassInstantiation(psiReference) || count.incrementAndGet() < 2); int usages = count.get(); if (usages < 2) { usages += JSInheritanceUtil.findSuperConstructorCalls(myClass).size(); } if (usages < 2) { final Collection<String> toImport = new ArrayList<>(); for (JSExpression argument : myNode.getArguments()) { String type = JSResolveUtil.getQualifiedExpressionType(argument, argument.getContainingFile()); if (StringUtil.isNotEmpty(type) && ImportUtils.needsImport(myClass, StringUtil.getPackageName(type))) { toImport.add(type); } } WriteAction.run(() -> { if (!toImport.isEmpty()) { FormatFixer formatFixer = ImportUtils.insertImportStatements(myClass, toImport); if (formatFixer != null) { formatFixer.fixFormat(); } } super.applyFix(project, psiElement, myClass.getContainingFile(), getEditor(myClass.getProject(), myClass.getContainingFile())); }); } else { String text = "function " + myClass.getName() + "(){}"; JSFunction fakeFunction = (JSFunction)JSChangeUtil.createStatementFromText(project, text, JavaScriptSupportLoader.ECMA_SCRIPT_L4) .getPsi(); new ChangeSignatureFix(fakeFunction, myNode.getArgumentList()) { @Override protected Pair<Boolean, List<JSParameterInfo>> handleCall(@NotNull JSFunction function, JSExpression[] arguments, boolean dummy) { List<JSParameterInfo> parameterInfos = super.handleCall(function, arguments, dummy).second; return Pair.create(true, parameterInfos); // always show dialog } @Override protected JSChangeSignatureDialog createDialog(PsiElement context, final List<JSParameterInfo> paramInfos) { JSMethodDescriptor descriptor = new JSMethodDescriptor(myFunction.getElement(), true) { @Override public List<JSParameterInfo> getParameters() { return paramInfos; } }; return new MyDialog(descriptor, context); } @Override protected JSChangeSignatureProcessor createProcessor(List<JSParameterInfo> paramInfos, JSAttributeList attributeList, @NotNull JSFunction function) { return new MyProcessor(function, attributeList != null ? attributeList.getAccessType() : JSAttributeList.AccessType.PACKAGE_LOCAL, myClass.getName(), "", paramInfos.toArray(new JSParameterInfo[paramInfos.size()]), Collections.emptySet()); } }.invoke(project, editor, file); } } private static boolean isClassInstantiation(PsiReference psiReference) { return psiReference instanceof JSReferenceExpression && ((JSReferenceExpression)psiReference).getParent() instanceof JSNewExpression; } @Override protected void buildTemplate(Template template, JSReferenceExpression referenceExpression, boolean staticContext, PsiFile file, PsiElement anchorParent) { if (constructorShouldBePublic()) { template.addTextSegment("public "); } writeFunctionAndName(template, myClass.getName(), file, null, referenceExpression); template.addTextSegment("("); addParameters(template, myNode.getArgumentList(), myNode, file); template.addTextSegment("){"); addBody(template, referenceExpression, file); template.addTextSegment("}"); } private boolean constructorShouldBePublic() { JSClass contextClass; return myClass.getAttributeList().getAccessType() == JSAttributeList.AccessType.PUBLIC || (contextClass = JSResolveUtil.getClassOfContext(myNode)) != null && JSPsiImplUtils.differentPackageName(JSResolveUtil.getPackageName(myClass), JSResolveUtil.getPackageName(contextClass)); } @NotNull @Override public String getName() { return JSBundle.message("javascript.create.constructor.intention.name", myClass.getName()); } private class MyDialog extends JSChangeSignatureDialog { public MyDialog(JSMethodDescriptor descriptor, PsiElement context) { super(descriptor, context); setTitle(JSBundle.message("create.constructor.dialog.title")); } @Override protected CallerChooserBase<JSFunction> createCallerChooser(String title, Tree treeToReuse, Consumer<Set<JSFunction>> callback) { return new MyCallerChooser(myMethod.getMethod(), title, treeToReuse, callback); } @Override protected JSChangeSignatureProcessor createRefactoringProcessor() { List<JSParameterInfo> parameters = getParameters(); return new MyProcessor(myMethod.getMethod(), JSAttributeList.AccessType.valueOf(getVisibility()), myClass.getName(), "", parameters.toArray(new JSParameterInfo[parameters.size()]), myMethodsToPropagateParameters != null ? myMethodsToPropagateParameters : Collections.emptySet()); } } private class MyCallerChooser extends JSCallerChooser { public MyCallerChooser(JSFunction method, String title, Tree treeToReuse, Consumer<Set<JSFunction>> callback) { super(method, method.getProject(), title, treeToReuse, callback); } @Override protected MethodNodeBase<JSFunction> createTreeNode(JSFunction method, HashSet<JSFunction> called, Runnable cancelCallback) { return new MyMethodNode(method, called, cancelCallback); } } private class MyMethodNode extends JSMethodNode { public MyMethodNode(JSFunction method, HashSet<JSFunction> called, Runnable cancelCallback) { super(method, called, myClass.getProject(), cancelCallback); } @Override protected List<JSFunction> computeCallers() { final Collection<PsiReference> refs = Collections.synchronizedCollection(new ArrayList<PsiReference>()); ReferencesSearch.search(myClass, myClass.getUseScope(), true).forEach(psiReference -> { if (isClassInstantiation(psiReference)) { refs.add(psiReference); } return true; }); Set<JSFunction> result = new java.util.HashSet<>(); for (PsiReference reference : refs) { addCallExpression((JSNewExpression)reference.getElement().getParent(), result); } for (JSCallExpression superCall : JSInheritanceUtil.findSuperConstructorCalls(myClass)) { addCallExpression(superCall, result); } return new ArrayList<>(result); } } private class MyProcessor extends JSChangeSignatureProcessor { public MyProcessor(JSFunction method, JSAttributeList.AccessType visibility, String methodName, String returnType, JSParameterInfo[] parameters, Set<JSFunction> methodsToPropagateParameters) { super(method, visibility, methodName, returnType, parameters, methodsToPropagateParameters); } @NotNull @Override protected UsageInfo[] findUsages() { final Collection<UsageInfo> declarations = Collections.synchronizedCollection(new HashSet<UsageInfo>()); final Collection<OtherUsageInfo> usages = Collections.synchronizedCollection(new HashSet<OtherUsageInfo>()); ReferencesSearch.search(myClass, myClass.getUseScope()).forEach(psiReference -> { if (isClassInstantiation(psiReference)) { PsiElement element = psiReference.getElement(); usages.add(new OtherUsageInfo(element, null, myParameters, shouldPropagate(element), 0, 0)); } return true; }); for (JSCallExpression superCall : JSInheritanceUtil.findSuperConstructorCalls(myClass)) { usages.add(new OtherUsageInfo(superCall.getMethodExpression(), null, myParameters, shouldPropagate(superCall), 0, 0)); } findPropagationUsages(declarations, usages); Collection<UsageInfo> result = new ArrayList<>(declarations); result.addAll(usages); return result.toArray(new UsageInfo[result.size()]); } @Override protected void performRefactoring(@NotNull UsageInfo[] usageInfos) { final Collection<String> toImport = new ArrayList<>(); for (JSExpression argument : myNode.getArguments()) { String type = JSResolveUtil.getQualifiedExpressionType(argument, argument.getContainingFile()); if (StringUtil.isNotEmpty(type) && ImportUtils.needsImport(myClass, StringUtil.getPackageName(type))) { toImport.add(type); } } StringBuilder newConstuctorText = new StringBuilder(); if (constructorShouldBePublic()) { newConstuctorText.append("public "); } newConstuctorText.append("function ").append(myClass.getName()); JSChangeSignatureDialog.buildParameterListText(Arrays.asList(myParameters), newConstuctorText, true, DialectDetector.dialectOfElement(myClass)); newConstuctorText.append("{}"); JSFunction constructorPrototype = (JSFunction)JSChangeUtil.createStatementFromText(myProject, newConstuctorText.toString(), JavaScriptSupportLoader.ECMA_SCRIPT_L4).getPsi(); PsiElement newConstuctor = myClass.add(constructorPrototype); // TODO anchor FormatFixer.create(newConstuctor, FormatFixer.Mode.Reformat).fixFormat(); if (!toImport.isEmpty()) { FormatFixer formatFixer = ImportUtils.insertImportStatements(myClass, toImport); if (formatFixer != null) { formatFixer.fixFormat(); } List<FormatFixer> fixers = ECMAScriptImportOptimizer.executeNoFormat(myClass.getContainingFile()); FormatFixer.fixAll(fixers); } super.performRefactoring(usageInfos); } @Override protected String getCommandName() { return getName(); } } }