blob: ff1170b64befe3734406578b4f4036d17db828cc [file] [log] [blame]
/*
* Copyright 2000-2012 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 com.intellij.psi.codeStyle.arrangement;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.arrangement.group.ArrangementGroupingRule;
import com.intellij.psi.codeStyle.arrangement.std.ArrangementSettingsToken;
import com.intellij.psi.codeStyle.arrangement.std.StdArrangementTokens;
import com.intellij.psi.search.searches.SuperMethodsSearch;
import com.intellij.psi.util.MethodSignatureBackedByPsiMethod;
import com.intellij.psi.util.PropertyUtil;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.intellij.psi.codeStyle.arrangement.ArrangementSectionDetector.ArrangementSectionEntryTemplate;
import static com.intellij.psi.codeStyle.arrangement.std.StdArrangementTokens.EntryType.*;
import static com.intellij.psi.codeStyle.arrangement.std.StdArrangementTokens.Modifier.*;
public class JavaArrangementVisitor extends JavaRecursiveElementVisitor {
private static final String NULL_CONTENT = "no content";
private static final Map<String, ArrangementSettingsToken> MODIFIERS = ContainerUtilRt.newHashMap();
static {
MODIFIERS.put(PsiModifier.PUBLIC, PUBLIC);
MODIFIERS.put(PsiModifier.PROTECTED, PROTECTED);
MODIFIERS.put(PsiModifier.PRIVATE, PRIVATE);
MODIFIERS.put(PsiModifier.PACKAGE_LOCAL, PACKAGE_PRIVATE);
MODIFIERS.put(PsiModifier.STATIC, STATIC);
MODIFIERS.put(PsiModifier.FINAL, FINAL);
MODIFIERS.put(PsiModifier.TRANSIENT, TRANSIENT);
MODIFIERS.put(PsiModifier.VOLATILE, VOLATILE);
MODIFIERS.put(PsiModifier.SYNCHRONIZED, SYNCHRONIZED);
MODIFIERS.put(PsiModifier.ABSTRACT, ABSTRACT);
}
private static final ArrangementSettingsToken ANON_CLASS_PARAMETER_LIST = new ArrangementSettingsToken("Dummy", "not matchable anon class argument list");
private static final ArrangementSettingsToken ANONYMOUS_CLASS_BODY = new ArrangementSettingsToken("Dummy", "not matchable anonymous class body");
@NotNull private final Stack<JavaElementArrangementEntry> myStack = new Stack<JavaElementArrangementEntry>();
@NotNull private final Map<PsiElement, JavaElementArrangementEntry> myEntries = new HashMap<PsiElement, JavaElementArrangementEntry>();
@NotNull private final JavaArrangementParseInfo myInfo;
@NotNull private final Collection<TextRange> myRanges;
@NotNull private final Set<ArrangementSettingsToken> myGroupingRules;
@NotNull private final MethodBodyProcessor myMethodBodyProcessor;
@NotNull private final ArrangementSectionDetector mySectionDetector;
@Nullable private final Document myDocument;
@NotNull private HashMap<PsiClass, Set<PsiField>> myCachedClassFields = ContainerUtil.newHashMap();
@NotNull private Set<PsiComment> myProcessedSectionsComments = ContainerUtil.newHashSet();
public JavaArrangementVisitor(@NotNull JavaArrangementParseInfo infoHolder,
@Nullable Document document,
@NotNull Collection<TextRange> ranges,
@NotNull ArrangementSettings settings)
{
myInfo = infoHolder;
myDocument = document;
myRanges = ranges;
myGroupingRules = getGroupingRules(settings);
myMethodBodyProcessor = new MethodBodyProcessor(infoHolder);
mySectionDetector = new ArrangementSectionDetector(document, settings, new Consumer<ArrangementSectionEntryTemplate>() {
@Override
public void consume(ArrangementSectionEntryTemplate data) {
TextRange range = data.getTextRange();
JavaSectionArrangementEntry entry = new JavaSectionArrangementEntry(getCurrent(), data.getToken(), range, data.getText(), true);
registerEntry(data.getElement(), entry);
}
});
}
@Override
public void visitComment(PsiComment comment) {
if (myProcessedSectionsComments.contains(comment)) {
return;
}
mySectionDetector.processComment(comment);
}
@NotNull
private static Set<ArrangementSettingsToken> getGroupingRules(@NotNull ArrangementSettings settings) {
Set<ArrangementSettingsToken> groupingRules = ContainerUtilRt.newHashSet();
for (ArrangementGroupingRule rule : settings.getGroupings()) {
groupingRules.add(rule.getGroupingType());
}
return groupingRules;
}
public void createAndProcessAnonymousClassBodyEntry(@NotNull PsiAnonymousClass aClass) {
final PsiElement lBrace = aClass.getLBrace();
final PsiElement rBrace = aClass.getRBrace();
if (lBrace == null || rBrace == null) {
return;
}
TextRange codeBlockRange = new TextRange(lBrace.getTextRange().getStartOffset(), rBrace.getTextRange().getEndOffset());
JavaElementArrangementEntry entry = createNewEntry(aClass.getLBrace(), codeBlockRange, ANONYMOUS_CLASS_BODY, aClass.getName(), true);
if (entry == null) {
return;
}
processChildrenWithinEntryScope(entry, new Runnable() {
@Override
public void run() {
PsiElement current = lBrace;
while (current != rBrace) {
current = current.getNextSibling();
if (current == null) {
break;
}
current.accept(JavaArrangementVisitor.this);
}
}
});
}
@Override
public void visitClass(PsiClass aClass) {
boolean isSectionCommentsDetected = registerSectionComments(aClass);
TextRange range = isSectionCommentsDetected ? getElementRangeWithoutComments(aClass) : aClass.getTextRange();
ArrangementSettingsToken type = CLASS;
if (aClass.isEnum()) {
type = ENUM;
}
else if (aClass.isInterface()) {
type = INTERFACE;
}
JavaElementArrangementEntry entry = createNewEntry(aClass, range, type, aClass.getName(), true);
processEntry(entry, aClass, aClass);
}
@Override
public void visitTypeParameter(PsiTypeParameter parameter) {
}
@Override
public void visitAnonymousClass(final PsiAnonymousClass aClass) {
JavaElementArrangementEntry entry = createNewEntry(aClass, aClass.getTextRange(), ANONYMOUS_CLASS, aClass.getName(), true);
if (entry == null) {
return;
}
processChildrenWithinEntryScope(entry, new Runnable() {
@Override
public void run() {
PsiExpressionList list = aClass.getArgumentList();
if (list != null && list.getTextLength() > 0) {
JavaElementArrangementEntry listEntry = createNewEntry(list, list.getTextRange(), ANON_CLASS_PARAMETER_LIST, aClass.getName(), true);
processEntry(listEntry, null, list);
}
createAndProcessAnonymousClassBodyEntry(aClass);
}
});
}
@Override
public void visitField(PsiField field) {
boolean isSectionCommentsDetected = registerSectionComments(field);
// There is a possible case that more than one field is declared for the same type like 'int i, j;'. We want to process only
// the first one then.
PsiElement fieldPrev = getPreviousNonWsComment(field.getPrevSibling(), 0);
if (fieldPrev instanceof PsiJavaToken && ((PsiJavaToken)fieldPrev).getTokenType() == JavaTokenType.COMMA) {
return;
}
// There is a possible case that fields which share the same type declaration are located on different document lines, e.g.:
// int i1,
// i2;
// We want to consider only the first declaration then but need to expand its range to all affected lines (up to semicolon).
TextRange range = isSectionCommentsDetected ? getElementRangeWithoutComments(field) : field.getTextRange();
PsiElement child = field.getLastChild();
boolean needSpecialProcessing = true;
if (isSemicolon(child)) {
needSpecialProcessing = false;
}
else if (child instanceof PsiComment) {
// There is a possible field definition like below:
// int f; // my comment.
// The comment goes into field PSI here, that's why we need to handle it properly.
PsiElement prev = getPreviousNonWsComment(child, range.getStartOffset());
needSpecialProcessing = prev != null && !isSemicolon(prev);
}
if (needSpecialProcessing) {
for (PsiElement e = field.getNextSibling(); e != null; e = e.getNextSibling()) {
if (e instanceof PsiWhiteSpace || e instanceof PsiComment) { // Skip white space and comment
continue;
}
else if (e instanceof PsiJavaToken) {
if (((PsiJavaToken)e).getTokenType() == JavaTokenType.COMMA) { // Skip comma
continue;
}
else {
break;
}
}
else if (e instanceof PsiField) {
PsiElement c = e.getLastChild();
if (c != null) {
c = getPreviousNonWsComment(c, range.getStartOffset());
}
// Stop if current field ends by a semicolon.
if (c instanceof PsiErrorElement // Incomplete field without trailing semicolon
|| (c instanceof PsiJavaToken && ((PsiJavaToken)c).getTokenType() == JavaTokenType.SEMICOLON))
{
range = TextRange.create(range.getStartOffset(), expandToCommentIfPossible(c));
}
else {
continue;
}
}
break;
}
}
JavaElementArrangementEntry entry = createNewEntry(field, range, FIELD, field.getName(), true);
if (entry == null)
return;
processEntry(entry, field, field.getInitializer());
myInfo.onFieldEntryCreated(field, entry);
List<PsiField> referencedFields = getReferencedFields(field);
for (PsiField referencedField : referencedFields) {
myInfo.registerFieldInitializationDependency(field, referencedField);
}
}
@NotNull
private List<PsiField> getReferencedFields(@NotNull PsiField field) {
final List<PsiField> referencedElements = new ArrayList<PsiField>();
PsiExpression fieldInitializer = field.getInitializer();
PsiClass containingClass = field.getContainingClass();
if (fieldInitializer == null || containingClass == null) {
return referencedElements;
}
Set<PsiField> classFields = myCachedClassFields.get(containingClass);
if (classFields == null) {
classFields = ContainerUtil.map2Set(containingClass.getFields(), new Function.Self<PsiField, PsiField>());
myCachedClassFields.put(containingClass, classFields);
}
final Set<PsiField> containingClassFields = classFields;
fieldInitializer.accept(new JavaRecursiveElementVisitor() {
@Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
PsiElement ref = expression.resolve();
if (ref instanceof PsiField && containingClassFields.contains(ref)) {
referencedElements.add((PsiField)ref);
}
super.visitReferenceExpression(expression);
}
});
return referencedElements;
}
@Nullable
private static PsiElement getPreviousNonWsComment(@Nullable PsiElement element, int minOffset) {
if (element == null) {
return null;
}
for (PsiElement e = element; e != null && e.getTextRange().getStartOffset() >= minOffset; e = e.getPrevSibling()) {
if (e instanceof PsiWhiteSpace || e instanceof PsiComment) {
continue;
}
return e;
}
return null;
}
private int expandToCommentIfPossible(@NotNull PsiElement element) {
if (myDocument == null) {
return element.getTextRange().getEndOffset();
}
CharSequence text = myDocument.getCharsSequence();
for (PsiElement e = element.getNextSibling(); e != null; e = e.getNextSibling()) {
if (e instanceof PsiWhiteSpace) {
if (hasLineBreak(text, e.getTextRange())) {
return element.getTextRange().getEndOffset();
}
}
else if (e instanceof PsiComment) {
if (!hasLineBreak(text, e.getTextRange())) {
return e.getTextRange().getEndOffset();
}
}
else {
return element.getTextRange().getEndOffset();
}
}
return element.getTextRange().getEndOffset();
}
private static boolean hasLineBreak(@NotNull CharSequence text, @NotNull TextRange range) {
for (int i = range.getStartOffset(), end = range.getEndOffset(); i < end; i++) {
if (text.charAt(i) == '\n') {
return true;
}
}
return false;
}
private static boolean isSemicolon(@Nullable PsiElement e) {
return e instanceof PsiJavaToken && ((PsiJavaToken)e).getTokenType() == JavaTokenType.SEMICOLON;
}
@Override
public void visitClassInitializer(PsiClassInitializer initializer) {
JavaElementArrangementEntry entry = createNewEntry(initializer, initializer.getTextRange(), FIELD, null, true);
if (entry == null) {
return;
}
PsiElement classLBrace = null;
PsiClass clazz = initializer.getContainingClass();
if (clazz != null) {
classLBrace = clazz.getLBrace();
}
for (PsiElement e = initializer.getPrevSibling(); e != null; e = e.getPrevSibling()) {
JavaElementArrangementEntry prevEntry;
if (e == classLBrace) {
prevEntry = myEntries.get(clazz);
}
else {
prevEntry = myEntries.get(e);
}
if (prevEntry != null) {
entry.addDependency(prevEntry);
}
if (!(e instanceof PsiWhiteSpace)) {
break;
}
}
}
@NotNull
public static TextRange getElementRangeWithoutComments(@NotNull PsiElement element) {
PsiElement[] children = element.getChildren();
assert(children.length > 1 && children[0] instanceof PsiComment);
int i = 0;
PsiElement child = children[i];
while (child instanceof PsiWhiteSpace || child instanceof PsiComment) {
child = children[++i];
}
return new TextRange(child.getTextRange().getStartOffset(), element.getTextRange().getEndOffset());
}
@NotNull
public static List<PsiComment> getComments(@NotNull PsiElement element) {
PsiElement[] children = element.getChildren();
List<PsiComment> comments = ContainerUtil.newArrayList();
for (PsiElement e : children) {
if (e instanceof PsiComment) {
comments.add((PsiComment)e);
} else if (!(e instanceof PsiWhiteSpace)) {
return comments;
}
}
return comments;
}
@Override
public void visitMethod(PsiMethod method) {
boolean isSectionCommentsDetected = registerSectionComments(method);
final TextRange range = isSectionCommentsDetected ? getElementRangeWithoutComments(method)
: method.getTextRange();
ArrangementSettingsToken type = method.isConstructor() ? CONSTRUCTOR : METHOD;
JavaElementArrangementEntry entry = createNewEntry(method, range, type, method.getName(), true);
if (entry == null) {
return;
}
processEntry(entry, method, method.getBody());
parseProperties(method, entry);
myInfo.onMethodEntryCreated(method, entry);
MethodSignatureBackedByPsiMethod overridden = SuperMethodsSearch.search(method, null, true, false).findFirst();
if (overridden != null) {
myInfo.onOverriddenMethod(overridden.getMethod(), method);
}
boolean reset = myMethodBodyProcessor.setBaseMethod(method);
try {
method.accept(myMethodBodyProcessor);
}
finally {
if (reset) {
myMethodBodyProcessor.setBaseMethod(null);
}
}
}
private boolean registerSectionComments(@NotNull PsiElement element) {
List<PsiComment> comments = getComments(element);
boolean isSectionCommentsDetected = false;
for (PsiComment comment : comments) {
if (mySectionDetector.processComment(comment)) {
isSectionCommentsDetected = true;
myProcessedSectionsComments.add(comment);
}
}
return isSectionCommentsDetected;
}
private void parseProperties(PsiMethod method, JavaElementArrangementEntry entry) {
if (!myGroupingRules.contains(StdArrangementTokens.Grouping.GETTERS_AND_SETTERS)) {
return;
}
String propertyName = null;
boolean getter = true;
if (PropertyUtil.isSimplePropertyGetter(method)) {
propertyName = PropertyUtil.getPropertyNameByGetter(method);
}
else if (PropertyUtil.isSimplePropertySetter(method)) {
propertyName = PropertyUtil.getPropertyNameBySetter(method);
getter = false;
}
if (propertyName == null) {
return;
}
PsiClass containingClass = method.getContainingClass();
String className = null;
if (containingClass != null) {
className = containingClass.getQualifiedName();
}
if (className == null) {
className = NULL_CONTENT;
}
if (getter) {
myInfo.registerGetter(propertyName, className, entry);
}
else {
myInfo.registerSetter(propertyName, className, entry);
}
}
@Override
public void visitEnumConstant(PsiEnumConstant enumConstant) {
}
private void processEntry(@Nullable JavaElementArrangementEntry entry,
@Nullable PsiModifierListOwner modifier,
@Nullable final PsiElement nextPsiRoot)
{
if (entry == null) {
return;
}
if (modifier != null) {
parseModifiers(modifier.getModifierList(), entry);
}
if (nextPsiRoot == null) {
return;
}
processChildrenWithinEntryScope(entry, new Runnable() {
@Override
public void run() {
nextPsiRoot.acceptChildren(JavaArrangementVisitor.this);
}
});
}
private void processChildrenWithinEntryScope(@NotNull JavaElementArrangementEntry entry, @NotNull Runnable childrenProcessing) {
myStack.push(entry);
try {
childrenProcessing.run();
}
finally {
myStack.pop();
}
}
private void registerEntry(@NotNull PsiElement element, @NotNull JavaElementArrangementEntry entry) {
myEntries.put(element, entry);
DefaultArrangementEntry current = getCurrent();
if (current == null) {
myInfo.addEntry(entry);
}
else {
current.addChild(entry);
}
}
@Nullable
private JavaElementArrangementEntry createNewEntry(@NotNull PsiElement element,
@NotNull TextRange range,
@NotNull ArrangementSettingsToken type,
@Nullable String name,
boolean canArrange)
{
if (!isWithinBounds(range)) {
return null;
}
DefaultArrangementEntry current = getCurrent();
JavaElementArrangementEntry entry;
if (canArrange) {
TextRange expandedRange = myDocument == null ? null : ArrangementUtil.expandToLineIfPossible(range, myDocument);
TextRange rangeToUse = expandedRange == null ? range : expandedRange;
entry = new JavaElementArrangementEntry(current, rangeToUse, type, name, true);
}
else {
entry = new JavaElementArrangementEntry(current, range, type, name, false);
}
registerEntry(element, entry);
return entry;
}
private boolean isWithinBounds(@NotNull TextRange range) {
for (TextRange textRange : myRanges) {
if (textRange.intersects(range)) {
return true;
}
}
return false;
}
@Nullable
private DefaultArrangementEntry getCurrent() {
return myStack.isEmpty() ? null : myStack.peek();
}
@SuppressWarnings("MagicConstant")
private static void parseModifiers(@Nullable PsiModifierList modifierList, @NotNull JavaElementArrangementEntry entry) {
if (modifierList == null) {
return;
}
for (String modifier : PsiModifier.MODIFIERS) {
if (modifierList.hasModifierProperty(modifier)) {
ArrangementSettingsToken arrangementModifier = MODIFIERS.get(modifier);
if (arrangementModifier != null) {
entry.addModifier(arrangementModifier);
}
}
}
if (modifierList.hasModifierProperty(PsiModifier.PACKAGE_LOCAL)) {
entry.addModifier(PACKAGE_PRIVATE);
}
}
private static class MethodBodyProcessor extends JavaRecursiveElementVisitor {
@NotNull private final JavaArrangementParseInfo myInfo;
@Nullable private PsiMethod myBaseMethod;
MethodBodyProcessor(@NotNull JavaArrangementParseInfo info) {
myInfo = info;
}
public void visitMethodCallExpression(PsiMethodCallExpression psiMethodCallExpression) {
PsiReference reference = psiMethodCallExpression.getMethodExpression().getReference();
if (reference == null) {
return;
}
PsiElement e = reference.resolve();
if (e instanceof PsiMethod) {
assert myBaseMethod != null;
PsiMethod m = (PsiMethod)e;
if (m.getContainingClass() == myBaseMethod.getContainingClass()) {
myInfo.registerMethodCallDependency(myBaseMethod, m);
}
}
// We process all method call expression children because there is a possible case like below:
// new Runnable() {
// void test();
// }.run();
// Here we want to process that 'Runnable.run()' implementation.
super.visitMethodCallExpression(psiMethodCallExpression);
}
public boolean setBaseMethod(@Nullable PsiMethod baseMethod) {
if (baseMethod == null || myBaseMethod == null /* don't override a base method in case of method-local anonymous classes */) {
myBaseMethod = baseMethod;
return true;
}
return false;
}
}
}