blob: 47222f9327141672900c4c5982ad311599b0263e [file] [log] [blame]
package com.intellij.structuralsearch;
import com.intellij.dupLocator.PsiElementRole;
import com.intellij.dupLocator.equivalence.EquivalenceDescriptor;
import com.intellij.dupLocator.equivalence.EquivalenceDescriptorProvider;
import com.intellij.dupLocator.equivalence.MultiChildDescriptor;
import com.intellij.dupLocator.equivalence.SingleChildDescriptor;
import com.intellij.dupLocator.iterators.FilteringNodeIterator;
import com.intellij.dupLocator.iterators.NodeIterator;
import com.intellij.dupLocator.util.DuplocatorUtil;
import com.intellij.dupLocator.util.NodeFilter;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageParserDefinitions;
import com.intellij.lang.ParserDefinition;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.LeafElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.structuralsearch.impl.matcher.*;
import com.intellij.structuralsearch.impl.matcher.compiler.GlobalCompilingVisitor;
import com.intellij.structuralsearch.impl.matcher.compiler.PatternCompiler;
import com.intellij.structuralsearch.impl.matcher.filters.LexicalNodesFilter;
import com.intellij.structuralsearch.impl.matcher.handlers.*;
import com.intellij.structuralsearch.impl.matcher.iterators.SsrFilteringNodeIterator;
import com.intellij.structuralsearch.impl.matcher.strategies.MatchingStrategy;
import com.intellij.structuralsearch.plugin.replace.ReplaceOptions;
import com.intellij.structuralsearch.plugin.replace.impl.ReplacementContext;
import com.intellij.util.ArrayUtil;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.containers.HashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author Eugene.Kudelevsky
*/
public abstract class StructuralSearchProfileBase extends StructuralSearchProfile {
private static final String DELIMETER_CHARS = ",;.[]{}():";
protected static final String PATTERN_PLACEHOLDER = "$$PATTERN_PLACEHOLDER$$";
private PsiElementVisitor myLexicalNodesFilter;
@Override
public void compile(PsiElement[] elements, @NotNull final GlobalCompilingVisitor globalVisitor) {
final PsiElement topElement = elements[0].getParent();
final PsiElement element = elements.length > 1 ? topElement : elements[0];
element.accept(new MyCompilingVisitor(globalVisitor, topElement));
element.accept(new PsiRecursiveElementVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if (DuplocatorUtil.isIgnoredNode(element)) {
return;
}
CompiledPattern pattern = globalVisitor.getContext().getPattern();
MatchingHandler handler = pattern.getHandler(element);
if (!(handler instanceof SubstitutionHandler) &&
!(handler instanceof TopLevelMatchingHandler) &&
!(handler instanceof LightTopLevelMatchingHandler)) {
pattern.setHandler(element, new SkippingHandler(handler));
}
// todo: simplify logic
/*
place skipping handler under top-level handler, because when we skip top-level node we can get non top-level handler, so
depth matching won't be done!;
*/
if (handler instanceof LightTopLevelMatchingHandler) {
MatchingHandler delegate = ((LightTopLevelMatchingHandler)handler).getDelegate();
if (!(delegate instanceof SubstitutionHandler)) {
pattern.setHandler(element, new LightTopLevelMatchingHandler(new SkippingHandler(delegate)));
}
}
}
});
final Language baseLanguage = element.getContainingFile().getLanguage();
// todo: try to optimize it: too heavy strategy!
globalVisitor.getContext().getPattern().setStrategy(new MatchingStrategy() {
@Override
public boolean continueMatching(PsiElement start) {
Language language = start.getLanguage();
PsiFile file = start.getContainingFile();
if (file != null) {
Language fileLanguage = file.getLanguage();
if (fileLanguage.isKindOf(language)) {
// dialect
language = fileLanguage;
}
}
return language == baseLanguage;
}
@Override
public boolean shouldSkip(PsiElement element, PsiElement elementToMatchWith) {
return DuplocatorUtil.shouldSkip(element, elementToMatchWith);
}
});
}
@NotNull
@Override
public PsiElementVisitor createMatchingVisitor(@NotNull GlobalMatchingVisitor globalVisitor) {
return new MyMatchingVisitor(globalVisitor);
}
@NotNull
@Override
public PsiElementVisitor getLexicalNodesFilter(@NotNull final LexicalNodesFilter filter) {
if (myLexicalNodesFilter == null) {
myLexicalNodesFilter = new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if (DuplocatorUtil.isIgnoredNode(element)) {
filter.setResult(true);
}
}
};
}
return myLexicalNodesFilter;
}
public static boolean containsOnlyDelimeters(String s) {
for (int i = 0, n = s.length(); i < n; i++) {
if (DELIMETER_CHARS.indexOf(s.charAt(i)) < 0) {
return false;
}
}
return true;
}
@NotNull
protected abstract String[] getVarPrefixes();
@NotNull
@Override
public CompiledPattern createCompiledPattern() {
return new CompiledPattern() {
@Override
protected SubstitutionHandler doCreateSubstitutionHandler(String name, boolean target, int minOccurs, int maxOccurs, boolean greedy) {
return new MySubstitutionHandler(name, target, minOccurs, maxOccurs, greedy);
}
@Override
public String[] getTypedVarPrefixes() {
return getVarPrefixes();
}
@Override
public boolean isTypedVar(String str) {
for (String prefix : getVarPrefixes()) {
if (str.startsWith(prefix)) {
return true;
}
}
return false;
}
@NotNull
@Override
public String getTypedVarString(PsiElement element) {
final PsiElement initialElement = element;
PsiElement child = SkippingHandler.getOnlyNonWhitespaceChild(element);
while (child != element && child != null && !(child instanceof LeafElement)) {
element = child;
child = SkippingHandler.getOnlyNonWhitespaceChild(element);
}
return child instanceof LeafElement ? element.getText() : initialElement.getText();
}
};
}
@Override
public boolean canProcess(@NotNull FileType fileType) {
return fileType instanceof LanguageFileType &&
isMyLanguage(((LanguageFileType)fileType).getLanguage());
}
@Override
public boolean isMyLanguage(@NotNull Language language) {
return language.isKindOf(getFileType().getLanguage());
}
@NotNull
protected abstract LanguageFileType getFileType();
@NotNull
@Override
public PsiElement[] createPatternTree(@NotNull String text,
@NotNull PatternTreeContext context,
@NotNull FileType fileType,
@Nullable Language language,
@Nullable String contextName,
@Nullable String extension,
@NotNull Project project,
boolean physical) {
if (context == PatternTreeContext.Block) {
final String strContext = getContext(text, language, contextName);
return strContext != null ?
parsePattern(project, strContext, text, fileType, language, extension, physical) :
PsiElement.EMPTY_ARRAY;
}
return super.createPatternTree(text, context, fileType, language, contextName, extension, project, physical);
}
@Override
public void checkReplacementPattern(Project project, ReplaceOptions options) {
final CompiledPattern compiledPattern = PatternCompiler.compilePattern(project, options.getMatchOptions());
if (compiledPattern == null) {
return;
}
final NodeIterator it = compiledPattern.getNodes();
if (!it.hasNext()) {
return;
}
final PsiElement root = it.current().getParent();
if (!checkOptionalChildren(root) ||
!checkErrorElements(root)) {
throw new UnsupportedPatternException(": Partial and expression patterns are not supported");
}
}
private static boolean checkErrorElements(PsiElement element) {
final boolean[] result = {true};
final int endOffset = element.getTextRange().getEndOffset();
element.accept(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if (element instanceof PsiErrorElement && element.getTextRange().getEndOffset() == endOffset) {
result[0] = false;
}
}
});
return result[0];
}
private static boolean checkOptionalChildren(PsiElement root) {
final boolean[] result = {true};
root.accept(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if (element instanceof LeafElement) {
return;
}
final EquivalenceDescriptorProvider provider = EquivalenceDescriptorProvider.getInstance(element);
if (provider == null) {
return;
}
final EquivalenceDescriptor descriptor = provider.buildDescriptor(element);
if (descriptor == null) {
return;
}
for (SingleChildDescriptor childDescriptor : descriptor.getSingleChildDescriptors()) {
if (childDescriptor.getType() == SingleChildDescriptor.MyType.OPTIONALLY_IN_PATTERN &&
childDescriptor.getElement() == null) {
result[0] = false;
}
}
for (MultiChildDescriptor childDescriptor : descriptor.getMultiChildDescriptors()) {
if (childDescriptor.getType() == MultiChildDescriptor.MyType.OPTIONALLY_IN_PATTERN) {
PsiElement[] elements = childDescriptor.getElements();
if (elements == null || elements.length == 0) {
result[0] = false;
}
}
}
}
});
return result[0];
}
@Override
public StructuralReplaceHandler getReplaceHandler(@NotNull ReplacementContext context) {
return new DocumentBasedReplaceHandler(context.getProject());
}
@NotNull
public String[] getContextNames() {
return ArrayUtil.EMPTY_STRING_ARRAY;
}
@Nullable
protected String getContext(@NotNull String pattern, @Nullable Language language, @Nullable String contextName) {
return PATTERN_PLACEHOLDER;
}
private static boolean canBePatternVariable(PsiElement element) {
// can be leaf element! (ex. var a = 1 <-> var $a$ = 1)
if (element instanceof LeafElement) {
return true;
}
while (!(element instanceof LeafElement) && element != null) {
element = SkippingHandler.getOnlyNonWhitespaceChild(element);
}
return element != null;
}
private static boolean isLiteral(PsiElement element) {
if (element == null) return false;
final ASTNode astNode = element.getNode();
if (astNode == null) {
return false;
}
final IElementType elementType = astNode.getElementType();
final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(element.getLanguage());
if (parserDefinition != null) {
final TokenSet literals = parserDefinition.getStringLiteralElements();
return literals.contains(elementType);
}
return false;
}
private static boolean canBePatternVariableValue(PsiElement element) {
// can be leaf element! (ex. var a = 1 <-> var $a$ = 1)
return !containsOnlyDelimeters(element.getText());
}
@Override
public boolean canBeVarDelimeter(@NotNull PsiElement element) {
final ASTNode node = element.getNode();
return node != null && getVariableDelimiters().contains(node.getElementType());
}
protected TokenSet getVariableDelimiters() {
return TokenSet.EMPTY;
}
public static PsiElement[] parsePattern(Project project,
String context,
String pattern,
FileType fileType,
Language language,
String extension,
boolean physical) {
int offset = context.indexOf(PATTERN_PLACEHOLDER);
final int patternLength = pattern.length();
final String patternInContext = context.replace(PATTERN_PLACEHOLDER, pattern);
final String ext = extension != null ? extension : fileType.getDefaultExtension();
final String name = "__dummy." + ext;
final PsiFileFactory factory = PsiFileFactory.getInstance(project);
final PsiFile file = language == null
? factory.createFileFromText(name, fileType, patternInContext, LocalTimeCounter.currentTime(), physical, true)
: factory.createFileFromText(name, language, patternInContext, physical, true);
if (file == null) {
return PsiElement.EMPTY_ARRAY;
}
final List<PsiElement> result = new ArrayList<PsiElement>();
PsiElement element = file.findElementAt(offset);
if (element == null) {
return PsiElement.EMPTY_ARRAY;
}
PsiElement topElement = element;
element = element.getParent();
while (element != null) {
if (element.getTextRange().getStartOffset() == offset && element.getTextLength() <= patternLength) {
topElement = element;
}
element = element.getParent();
}
if (topElement instanceof PsiFile) {
return topElement.getChildren();
}
final int endOffset = offset + patternLength;
result.add(topElement);
topElement = topElement.getNextSibling();
while (topElement != null && topElement.getTextRange().getEndOffset() <= endOffset) {
result.add(topElement);
topElement = topElement.getNextSibling();
}
return result.toArray(new PsiElement[result.size()]);
}
// todo: support expression patterns
// todo: support {statement;} = statement; (node has only non-lexical child)
private static class MyCompilingVisitor extends PsiRecursiveElementVisitor {
private final GlobalCompilingVisitor myGlobalVisitor;
private final PsiElement myTopElement;
private Pattern[] mySubstitutionPatterns;
private MyCompilingVisitor(GlobalCompilingVisitor globalVisitor, PsiElement topElement) {
myGlobalVisitor = globalVisitor;
myTopElement = topElement;
}
@Override
public void visitElement(PsiElement element) {
doVisitElement(element);
if (isLiteral(element)) {
visitLiteral(element);
}
}
private void doVisitElement(PsiElement element) {
CompiledPattern pattern = myGlobalVisitor.getContext().getPattern();
if (myGlobalVisitor.getCodeBlockLevel() == 0) {
initTopLevelElement(element);
return;
}
if (canBePatternVariable(element) && pattern.isRealTypedVar(element)) {
myGlobalVisitor.handle(element);
final MatchingHandler handler = pattern.getHandler(element);
handler.setFilter(new NodeFilter() {
public boolean accepts(PsiElement other) {
return canBePatternVariableValue(other);
}
});
super.visitElement(element);
return;
}
super.visitElement(element);
if (myGlobalVisitor.getContext().getSearchHelper().doOptimizing() && element instanceof LeafElement) {
ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(element.getLanguage());
if (parserDefinition != null) {
String text = element.getText();
// todo: support variables inside comments
boolean flag = true;
if (StringUtil.isJavaIdentifier(text) && flag) {
myGlobalVisitor.processTokenizedName(text, true, GlobalCompilingVisitor.OccurenceKind.CODE);
}
}
}
}
private void visitLiteral(PsiElement literal) {
String value = literal.getText();
if (StringUtil.isQuotedString(value)) {
if (mySubstitutionPatterns == null) {
final String[] prefixes = myGlobalVisitor.getContext().getPattern().getTypedVarPrefixes();
mySubstitutionPatterns = createPatterns(prefixes);
}
for (Pattern substitutionPattern : mySubstitutionPatterns) {
@Nullable MatchingHandler handler =
myGlobalVisitor.processPatternStringWithFragments(value, GlobalCompilingVisitor.OccurenceKind.LITERAL, substitutionPattern);
if (handler != null) {
literal.putUserData(CompiledPattern.HANDLER_KEY, handler);
break;
}
}
}
}
private static Pattern[] createPatterns(String[] prefixes) {
final Pattern[] patterns = new Pattern[prefixes.length];
for (int i = 0; i < prefixes.length; i++) {
final String s = StructuralSearchUtil.shieldSpecialChars(prefixes[0]);
patterns[i] = Pattern.compile("\\b(" + s + "\\w+)\\b");
}
return patterns;
}
private void initTopLevelElement(PsiElement element) {
CompiledPattern pattern = myGlobalVisitor.getContext().getPattern();
PsiElement newElement = SkippingHandler.skipNodeIfNeccessary(element);
if (element != newElement && newElement != null) {
// way to support partial matching (ex. if ($condition$) )
newElement.accept(this);
pattern.setHandler(element, new LightTopLevelMatchingHandler(pattern.getHandler(element)));
}
else {
myGlobalVisitor.setCodeBlockLevel(myGlobalVisitor.getCodeBlockLevel() + 1);
for (PsiElement el = element.getFirstChild(); el != null; el = el.getNextSibling()) {
if (GlobalCompilingVisitor.getFilter().accepts(el)) {
if (el instanceof PsiWhiteSpace) {
myGlobalVisitor.addLexicalNode(el);
}
}
else {
el.accept(this);
MatchingHandler matchingHandler = pattern.getHandler(el);
pattern.setHandler(el, element == myTopElement ? new TopLevelMatchingHandler(matchingHandler) :
new LightTopLevelMatchingHandler(matchingHandler));
/*
do not assign light-top-level handlers through skipping, because it is incorrect;
src: if (...) { st1; st2; }
pattern: if (...) {$a$;}
$a$ will have top-level handler, so matching will be considered as correct, although "st2;" is left!
*/
}
}
myGlobalVisitor.setCodeBlockLevel(myGlobalVisitor.getCodeBlockLevel() - 1);
pattern.setHandler(element, new TopLevelMatchingHandler(pattern.getHandler(element)));
}
}
}
private static class MyMatchingVisitor extends PsiElementVisitor {
private final GlobalMatchingVisitor myGlobalVisitor;
private MyMatchingVisitor(GlobalMatchingVisitor globalVisitor) {
myGlobalVisitor = globalVisitor;
}
private boolean shouldIgnoreVarNode(PsiElement element) {
MatchingHandler handler = myGlobalVisitor.getMatchContext().getPattern().getHandlerSimple(element);
if (handler instanceof DelegatingHandler) {
handler = ((DelegatingHandler)handler).getDelegate();
}
return handler instanceof MySubstitutionHandler && ((MySubstitutionHandler)handler).myExceptedNodes.contains(element);
}
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
final EquivalenceDescriptorProvider descriptorProvider = EquivalenceDescriptorProvider.getInstance(element);
if (descriptorProvider != null) {
final EquivalenceDescriptor descriptor1 = descriptorProvider.buildDescriptor(element);
final EquivalenceDescriptor descriptor2 = descriptorProvider.buildDescriptor(myGlobalVisitor.getElement());
if (descriptor1 != null && descriptor2 != null) {
final boolean result = DuplocatorUtil
.match(descriptor1, descriptor2, myGlobalVisitor, Collections.<PsiElementRole>emptySet(), null);
myGlobalVisitor.setResult(result);
return;
}
}
if (isLiteral(element)) {
visitLiteral(element);
return;
}
if (canBePatternVariable(element) &&
myGlobalVisitor.getMatchContext().getPattern().isRealTypedVar(element) &&
!shouldIgnoreVarNode(element)) {
PsiElement matchedElement = myGlobalVisitor.getElement();
PsiElement newElement = SkippingHandler.skipNodeIfNeccessary(matchedElement);
while (newElement != matchedElement) {
matchedElement = newElement;
newElement = SkippingHandler.skipNodeIfNeccessary(matchedElement);
}
myGlobalVisitor.setResult(myGlobalVisitor.handleTypedElement(element, matchedElement));
}
else if (element instanceof LeafElement) {
myGlobalVisitor.setResult(element.getText().equals(myGlobalVisitor.getElement().getText()));
}
else if (element.getFirstChild() == null && element.getTextLength() == 0) {
myGlobalVisitor.setResult(true);
}
else {
PsiElement patternChild = element.getFirstChild();
PsiElement matchedChild = myGlobalVisitor.getElement().getFirstChild();
FilteringNodeIterator patternIterator = new SsrFilteringNodeIterator(patternChild);
FilteringNodeIterator matchedIterator = new SsrFilteringNodeIterator(matchedChild);
boolean matched = myGlobalVisitor.matchSequentially(patternIterator, matchedIterator);
myGlobalVisitor.setResult(matched);
}
}
public void visitLiteral(PsiElement literal) {
final PsiElement l2 = myGlobalVisitor.getElement();
MatchingHandler handler = (MatchingHandler)literal.getUserData(CompiledPattern.HANDLER_KEY);
if (handler instanceof SubstitutionHandler) {
int offset = 0;
int length = l2.getTextLength();
final String text = l2.getText();
if (length > 2 &&
(text.charAt(0) == '"' && text.charAt(length - 1) == '"') ||
(text.charAt(0) == '\'' && text.charAt(length - 1) == '\'')) {
length--;
offset++;
}
myGlobalVisitor.setResult(((SubstitutionHandler)handler).handle(l2, offset, length, myGlobalVisitor.getMatchContext()));
}
else if (handler != null) {
myGlobalVisitor.setResult(handler.match(literal, l2, myGlobalVisitor.getMatchContext()));
}
else {
myGlobalVisitor.setResult(literal.textMatches(l2));
}
}
}
private static class MySubstitutionHandler extends SubstitutionHandler {
final Set<PsiElement> myExceptedNodes;
public MySubstitutionHandler(String name, boolean target, int minOccurs, int maxOccurs, boolean greedy) {
super(name, target, minOccurs, maxOccurs, greedy);
myExceptedNodes = new HashSet<PsiElement>();
}
@Override
public boolean matchSequentially(NodeIterator nodes, NodeIterator nodes2, MatchContext context) {
if (doMatchSequentially(nodes, nodes2, context)) {
return true;
}
final PsiElement current = nodes.current();
if (current != null) {
myExceptedNodes.add(current);
}
final boolean result = doMatchSequentiallyBySimpleHandler(nodes, nodes2, context);
myExceptedNodes.remove(current);
return result;
}
}
}