package com.intellij.lang.javascript.inspections.actionscript;
import com.intellij.codeInsight.highlighting.ReadWriteAccessDetector;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lang.javascript.DialectDetector;
import com.intellij.lang.javascript.JavaScriptSupportLoader;
import com.intellij.lang.javascript.findUsages.JSReadWriteAccessDetector;
import com.intellij.lang.javascript.flex.FlexBundle;
import com.intellij.lang.javascript.inspections.JSInspection;
import com.intellij.lang.javascript.inspections.actionscript.fixes.ConvertToLocalFix;
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.resolve.JSResolveUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlText;
import com.intellij.util.Processor;
import gnu.trove.THashMap;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import java.util.*;
// TODO: control flow
public class JSFieldCanBeLocalInspection extends JSInspection {
private static final Logger LOG = Logger.getInstance(JSFieldCanBeLocalInspection.class);
@NotNull
@Override
protected PsiElementVisitor createVisitor(final ProblemsHolder holder, final LocalInspectionToolSession session) {
return new MyVisitor(holder);
}
private static class MyVisitor extends JSElementVisitor {
private final ProblemsHolder myHolder;
public MyVisitor(ProblemsHolder holder) {
myHolder = holder;
}
public void visitJSVariable(final JSVariable field) {
if (!DialectDetector.isActionScript(field)) return;
final PsiElement parentParent = field.getParent().getParent();
final PsiElement context = parentParent.getContext();
if (!(parentParent instanceof JSClass) &&
!(parentParent instanceof JSFile &&
context instanceof XmlText &&
JavaScriptSupportLoader.isFlexMxmFile(context.getContainingFile()))) {
return;
}
final JSAttributeList attributeList = field.getAttributeList();
if (attributeList == null) return;
if (attributeList.getAccessType() != JSAttributeList.AccessType.PRIVATE) return;
if (field.isConst() && attributeList.hasModifier(JSAttributeList.ModifierType.STATIC)) return;
if (attributeList.findAttributeByName("Embed") != null || attributeList.findAttributeByName("Inject") != null) return;
// sorted - for predictable caret position after quick fix
final SortedMap<JSFunction, Collection<PsiReference>> functionToReferences =
new TreeMap<>(
(f1, f2) -> f1.getTextRange().getStartOffset() - f2.getTextRange().getStartOffset());
final Map<JSFunction, PsiElement> functionToFirstReadUsage = new THashMap<>();
final Map<JSFunction, PsiElement> functionToFirstWriteUsage = new THashMap<>();
final PsiFile topLevelFile = InjectedLanguageManager.getInstance(field.getProject()).getTopLevelFile(field);
final boolean ok = ReferencesSearch.search(field, new LocalSearchScope(topLevelFile)).forEach(reference -> {
final PsiElement element = reference.getElement();
if (JSResolveUtil.isSelfReference(element)) return true;
if (!(element instanceof JSReferenceExpression)) return false;
if (((JSReferenceExpression)element).getQualifier() != null) return false;
final JSFunction function = PsiTreeUtil.getParentOfType(element, JSFunction.class);
if (function == null) return false;
Collection<PsiReference> references = functionToReferences.get(function);
if (references == null) {
references = new ArrayList<>();
functionToReferences.put(function, references);
}
references.add(reference);
final ReadWriteAccessDetector.Access access = JSReadWriteAccessDetector.ourInstance.getExpressionAccess(element);
if (access == ReadWriteAccessDetector.Access.Read || access == ReadWriteAccessDetector.Access.ReadWrite) {
final PsiElement previous = functionToFirstReadUsage.get(function);
if (previous == null || element.getTextRange().getStartOffset() < previous.getTextRange().getStartOffset()) {
functionToFirstReadUsage.put(function, element);
}
}
if (access == ReadWriteAccessDetector.Access.Write || access == ReadWriteAccessDetector.Access.ReadWrite) {
final PsiElement previous = functionToFirstWriteUsage.get(function);
if (previous == null || element.getTextRange().getStartOffset() < previous.getTextRange().getStartOffset()) {
functionToFirstWriteUsage.put(function, element);
}
}
return true;
});
if (!ok) return;
if (functionToFirstWriteUsage.isEmpty() && functionToFirstReadUsage.isEmpty()) return;
// can be local if field has trivial initializer and doesn't have any more write usages
final boolean trivialInitializer = field.getInitializer() instanceof JSLiteralExpression;
if (functionToFirstWriteUsage.isEmpty() && trivialInitializer) {
registerCanBeLocal(field, functionToReferences);
return;
}
// can be local if all read usages have write usage before them
for (Map.Entry<JSFunction, PsiElement> entry : functionToFirstReadUsage.entrySet()) {
final JSFunction function = entry.getKey();
final PsiElement readUsage = entry.getValue();
final PsiElement writeUsage = functionToFirstWriteUsage.get(function);
if (writeUsage == null) return;
if (writeUsage.getTextRange().getStartOffset() >= readUsage.getTextRange().getStartOffset()) return;
final JSElement branchingParent =
PsiTreeUtil.getParentOfType(writeUsage, JSConditionalExpression.class, JSIfStatement.class, JSSwitchStatement.class);
if (branchingParent != null) return;
final PsiElement commonParent = PsiTreeUtil.findCommonParent(readUsage, writeUsage);
if (commonParent instanceof JSAssignmentExpression) return; // a = a + 1
}
registerCanBeLocal(field, functionToReferences);
}
private void registerCanBeLocal(final JSVariable field, final Map<JSFunction, Collection<PsiReference>> functionToReferences) {
LOG.assertTrue(!functionToReferences.isEmpty());
PsiElement element = field.getNameIdentifier();
if (element == null) {
element = field;
}
final LocalQuickFix[] fixes = new LocalQuickFix[]{new ConvertToLocalFix(field, functionToReferences)};
myHolder.registerProblem(element, FlexBundle.message("js.field.can.be.converted.to.local"), fixes);
}
}
@Nls
@NotNull
@Override
public String getDisplayName() {
return FlexBundle.message("js.field.can.be.local.name");
}
}