blob: 07808d33c0039bbe6dbc4c1919875fd5944787f9 [file] [log] [blame]
package com.intellij.structuralsearch.impl.matcher.compiler;
import com.intellij.codeInsight.template.Template;
import com.intellij.codeInsight.template.TemplateManager;
import com.intellij.dupLocator.util.NodeFilter;
import com.intellij.lang.Language;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.extensions.Extensions;
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.PsiFileImpl;
import com.intellij.psi.impl.source.tree.LeafElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.structuralsearch.*;
import com.intellij.structuralsearch.impl.matcher.CompiledPattern;
import com.intellij.structuralsearch.impl.matcher.MatchPredicateProvider;
import com.intellij.structuralsearch.impl.matcher.MatcherImplUtil;
import com.intellij.structuralsearch.impl.matcher.PatternTreeContext;
import com.intellij.structuralsearch.impl.matcher.filters.LexicalNodesFilter;
import com.intellij.structuralsearch.impl.matcher.handlers.MatchPredicate;
import com.intellij.structuralsearch.impl.matcher.handlers.MatchingHandler;
import com.intellij.structuralsearch.impl.matcher.handlers.SubstitutionHandler;
import com.intellij.structuralsearch.impl.matcher.predicates.*;
import com.intellij.structuralsearch.plugin.ui.Configuration;
import com.intellij.util.IncorrectOperationException;
import gnu.trove.TIntArrayList;
import gnu.trove.TIntHashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Compiles the handlers for usability
*/
public class PatternCompiler {
private static CompileContext lastTestingContext;
public static void transformOldPattern(MatchOptions options) {
StringToConstraintsTransformer.transformOldPattern(options);
}
public static CompiledPattern compilePattern(final Project project, final MatchOptions options) throws MalformedPatternException,
UnsupportedOperationException {
FileType fileType = options.getFileType();
assert fileType instanceof LanguageFileType;
Language language = ((LanguageFileType)fileType).getLanguage();
StructuralSearchProfile profile = StructuralSearchUtil.getProfileByLanguage(language);
assert profile != null;
CompiledPattern result = profile.createCompiledPattern();
final String[] prefixes = result.getTypedVarPrefixes();
assert prefixes.length > 0;
final CompileContext context = new CompileContext();
if (ApplicationManager.getApplication().isUnitTestMode()) lastTestingContext = context;
/*CompiledPattern result = options.getFileType() == StdFileTypes.JAVA ?
new JavaCompiledPattern() :
new XmlCompiledPattern();*/
try {
context.init(result, options, project, options.getScope() instanceof GlobalSearchScope);
List<PsiElement> elements = compileByAllPrefixes(project, options, result, context, prefixes);
final CompiledPattern pattern = context.getPattern();
checkForUnknownVariables(pattern, elements);
pattern.setNodes(elements);
if (context.getSearchHelper().doOptimizing() && context.getSearchHelper().isScannedSomething()) {
final Set<PsiFile> set = context.getSearchHelper().getFilesSetToScan();
final List<PsiFile> filesToScan = new ArrayList<PsiFile>(set.size());
final GlobalSearchScope scope = (GlobalSearchScope)options.getScope();
for (final PsiFile file : set) {
if (!scope.contains(file.getVirtualFile())) {
continue;
}
if (file instanceof PsiFileImpl) {
((PsiFileImpl)file).clearCaches();
}
filesToScan.add(file);
}
if (filesToScan.size() == 0) {
throw new MalformedPatternException(SSRBundle.message("ssr.will.not.find.anything"));
}
result.setScope(
new LocalSearchScope(PsiUtilCore.toPsiElementArray(filesToScan))
);
}
} finally {
context.clear();
}
return result;
}
private static void checkForUnknownVariables(final CompiledPattern pattern, List<PsiElement> elements) {
for (PsiElement element : elements) {
element.accept(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
if (element instanceof PsiComment) {
return;
}
super.visitElement(element);
if (!(element instanceof LeafElement) || !pattern.isTypedVar(element)) {
return;
}
final MatchingHandler handler = pattern.getHandler(pattern.getTypedVarString(element));
if (handler == null) {
throw new MalformedPatternException();
}
}
});
}
}
public static String getLastFindPlan() {
return ((TestModeOptimizingSearchHelper)lastTestingContext.getSearchHelper()).getSearchPlan();
}
@NotNull
private static List<PsiElement> compileByAllPrefixes(Project project,
MatchOptions options,
CompiledPattern pattern,
CompileContext context,
String[] applicablePrefixes) {
if (applicablePrefixes.length == 0) {
return Collections.emptyList();
}
List<PsiElement> elements = doCompile(project, options, pattern, new ConstantPrefixProvider(applicablePrefixes[0]), context);
if (elements.isEmpty()) {
return elements;
}
final PsiFile file = elements.get(0).getContainingFile();
if (file == null) {
return elements;
}
final PsiElement last = elements.get(elements.size() - 1);
final Pattern[] patterns = new Pattern[applicablePrefixes.length];
for (int i = 0; i < applicablePrefixes.length; i++) {
String s = StructuralSearchUtil.shieldSpecialChars(applicablePrefixes[i]);
patterns[i] = Pattern.compile(s + "\\w+\\b");
}
final int[] varEndOffsets = findAllTypedVarOffsets(file, patterns);
final int patternEndOffset = last.getTextRange().getEndOffset();
if (elements.size() == 0 ||
checkErrorElements(file, patternEndOffset, patternEndOffset, varEndOffsets, true) != Boolean.TRUE) {
return elements;
}
final int varCount = varEndOffsets.length;
final String[] prefixSequence = new String[varCount];
for (int i = 0; i < varCount; i++) {
prefixSequence[i] = applicablePrefixes[0];
}
final List<PsiElement> finalElements =
compileByPrefixes(project, options, pattern, context, applicablePrefixes, patterns, prefixSequence, 0);
return finalElements != null
? finalElements
: doCompile(project, options, pattern, new ConstantPrefixProvider(applicablePrefixes[0]), context);
}
@Nullable
private static List<PsiElement> compileByPrefixes(Project project,
MatchOptions options,
CompiledPattern pattern,
CompileContext context,
String[] applicablePrefixes,
Pattern[] substitutionPatterns,
String[] prefixSequence,
int index) {
if (index >= prefixSequence.length) {
final List<PsiElement> elements = doCompile(project, options, pattern, new ArrayPrefixProvider(prefixSequence), context);
if (elements.isEmpty()) {
return elements;
}
final PsiElement parent = elements.get(0).getParent();
final PsiElement last = elements.get(elements.size() - 1);
final int[] varEndOffsets = findAllTypedVarOffsets(parent.getContainingFile(), substitutionPatterns);
final int patternEndOffset = last.getTextRange().getEndOffset();
return checkErrorElements(parent, patternEndOffset, patternEndOffset, varEndOffsets, false) != Boolean.TRUE
? elements
: null;
}
String[] alternativeVariant = null;
for (String applicablePrefix : applicablePrefixes) {
prefixSequence[index] = applicablePrefix;
List<PsiElement> elements = doCompile(project, options, pattern, new ArrayPrefixProvider(prefixSequence), context);
if (elements.isEmpty()) {
return elements;
}
final PsiFile file = elements.get(0).getContainingFile();
if (file == null) {
return elements;
}
final int[] varEndOffsets = findAllTypedVarOffsets(file, substitutionPatterns);
final int offset = varEndOffsets[index];
final int patternEndOffset = elements.get(elements.size() - 1).getTextRange().getEndOffset();
final Boolean result = checkErrorElements(file, offset, patternEndOffset, varEndOffsets, false);
if (result == Boolean.TRUE) {
continue;
}
if (result == Boolean.FALSE || (result == null && alternativeVariant == null)) {
final List<PsiElement> finalElements =
compileByPrefixes(project, options, pattern, context, applicablePrefixes, substitutionPatterns, prefixSequence, index + 1);
if (finalElements != null) {
if (result == Boolean.FALSE) {
return finalElements;
}
alternativeVariant = new String[prefixSequence.length];
System.arraycopy(prefixSequence, 0, alternativeVariant, 0, prefixSequence.length);
}
}
}
return alternativeVariant != null ?
compileByPrefixes(project, options, pattern, context, applicablePrefixes, substitutionPatterns, alternativeVariant, index + 1) :
null;
}
@NotNull
private static int[] findAllTypedVarOffsets(final PsiFile file, final Pattern[] substitutionPatterns) {
final TIntHashSet result = new TIntHashSet();
file.accept(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if (element instanceof LeafElement) {
final String text = element.getText();
for (Pattern pattern : substitutionPatterns) {
final Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
result.add(element.getTextRange().getStartOffset() + matcher.end());
}
}
}
}
});
final int[] resultArray = result.toArray();
Arrays.sort(resultArray);
return resultArray;
}
/**
* False: there are no error elements before offset, except patternEndOffset
* Null: there are only error elements located exactly after template variables or at the end of the pattern
* True: otherwise
*/
@Nullable
private static Boolean checkErrorElements(PsiElement element,
final int offset,
final int patternEndOffset,
final int[] varEndOffsets,
final boolean strict) {
final TIntArrayList errorOffsets = new TIntArrayList();
final boolean[] containsErrorTail = {false};
final TIntHashSet varEndOffsetsSet = new TIntHashSet(varEndOffsets);
element.accept(new PsiRecursiveElementWalkingVisitor() {
@Override
public void visitErrorElement(PsiErrorElement element) {
super.visitErrorElement(element);
final int startOffset = element.getTextRange().getStartOffset();
if ((strict || !varEndOffsetsSet.contains(startOffset)) && startOffset != patternEndOffset) {
errorOffsets.add(startOffset);
}
if (startOffset == offset) {
containsErrorTail[0] = true;
}
}
});
for (int i = 0; i < errorOffsets.size(); i++) {
final int errorOffset = errorOffsets.get(i);
if (errorOffset <= offset) {
return true;
}
}
return containsErrorTail[0] ? null : false;
}
private interface PrefixProvider {
String getPrefix(int varIndex);
}
private static class ConstantPrefixProvider implements PrefixProvider {
private final String myPrefix;
private ConstantPrefixProvider(String prefix) {
myPrefix = prefix;
}
@Override
public String getPrefix(int varIndex) {
return myPrefix;
}
}
private static class ArrayPrefixProvider implements PrefixProvider {
private final String[] myPrefixes;
private ArrayPrefixProvider(String[] prefixes) {
myPrefixes = prefixes;
}
@Override
public String getPrefix(int varIndex) {
if (varIndex >= myPrefixes.length) return null;
return myPrefixes[varIndex];
}
}
private static List<PsiElement> doCompile(Project project,
MatchOptions options,
CompiledPattern result,
PrefixProvider prefixProvider,
CompileContext context) {
result.clearHandlers();
context.init(result, options, project, options.getScope() instanceof GlobalSearchScope);
final StringBuilder buf = new StringBuilder();
Template template = TemplateManager.getInstance(project).createTemplate("","",options.getSearchPattern());
int segmentsCount = template.getSegmentsCount();
String text = template.getTemplateText();
buf.setLength(0);
int prevOffset = 0;
for(int i=0;i<segmentsCount;++i) {
final int offset = template.getSegmentOffset(i);
final String name = template.getSegmentName(i);
final String prefix = prefixProvider.getPrefix(i);
if (prefix == null) {
throw new MalformedPatternException();
}
buf.append(text.substring(prevOffset,offset));
buf.append(prefix);
buf.append(name);
MatchVariableConstraint constraint = options.getVariableConstraint(name);
if (constraint==null) {
// we do not edited the constraints
constraint = new MatchVariableConstraint();
constraint.setName( name );
options.addVariableConstraint(constraint);
}
SubstitutionHandler handler = result.createSubstitutionHandler(
name,
prefix + name,
constraint.isPartOfSearchResults(),
constraint.getMinCount(),
constraint.getMaxCount(),
constraint.isGreedy()
);
if(constraint.isWithinHierarchy()) {
handler.setSubtype(true);
}
if(constraint.isStrictlyWithinHierarchy()) {
handler.setStrictSubtype(true);
}
MatchPredicate predicate;
if (!StringUtil.isEmptyOrSpaces(constraint.getRegExp())) {
predicate = new RegExpPredicate(
constraint.getRegExp(),
options.isCaseSensitiveMatch(),
name,
constraint.isWholeWordsOnly(),
constraint.isPartOfSearchResults()
);
if (constraint.isInvertRegExp()) {
predicate = new NotPredicate(predicate);
}
addPredicate(handler,predicate);
}
if (constraint.isReference()) {
predicate = new ReferencePredicate( constraint.getNameOfReferenceVar() );
if (constraint.isInvertReference()) {
predicate = new NotPredicate(predicate);
}
addPredicate(handler,predicate);
}
Set<MatchPredicate> predicates = new LinkedHashSet<MatchPredicate>();
for (MatchPredicateProvider matchPredicateProvider : Extensions.getExtensions(MatchPredicateProvider.EP_NAME)) {
matchPredicateProvider.collectPredicates(constraint, name, options, predicates);
}
for (MatchPredicate matchPredicate : predicates) {
addPredicate(handler, matchPredicate);
}
addScriptConstraint(name, constraint, handler);
if (!StringUtil.isEmptyOrSpaces(constraint.getContainsConstraint())) {
predicate = new ContainsPredicate(name, constraint.getContainsConstraint());
if (constraint.isInvertContainsConstraint()) {
predicate = new NotPredicate(predicate);
}
addPredicate(handler,predicate);
}
if (!StringUtil.isEmptyOrSpaces(constraint.getWithinConstraint())) {
assert false;
}
prevOffset = offset;
}
MatchVariableConstraint constraint = options.getVariableConstraint(Configuration.CONTEXT_VAR_NAME);
if (constraint != null) {
SubstitutionHandler handler = result.createSubstitutionHandler(
Configuration.CONTEXT_VAR_NAME,
Configuration.CONTEXT_VAR_NAME,
constraint.isPartOfSearchResults(),
constraint.getMinCount(),
constraint.getMaxCount(),
constraint.isGreedy()
);
if (!StringUtil.isEmptyOrSpaces(constraint.getWithinConstraint())) {
MatchPredicate predicate = new WithinPredicate(Configuration.CONTEXT_VAR_NAME, constraint.getWithinConstraint(), project);
if (constraint.isInvertWithinConstraint()) {
predicate = new NotPredicate(predicate);
}
addPredicate(handler,predicate);
}
addScriptConstraint(Configuration.CONTEXT_VAR_NAME, constraint, handler);
}
buf.append(text.substring(prevOffset,text.length()));
PsiElement[] matchStatements;
try {
matchStatements = MatcherImplUtil.createTreeFromText(buf.toString(), PatternTreeContext.Block, options.getFileType(),
options.getDialect(), options.getPatternContext(), project, false);
if (matchStatements.length==0) throw new MalformedPatternException();
} catch (IncorrectOperationException e) {
throw new MalformedPatternException(e.getMessage());
}
NodeFilter filter = LexicalNodesFilter.getInstance();
GlobalCompilingVisitor compilingVisitor = new GlobalCompilingVisitor();
compilingVisitor.compile(matchStatements,context);
ArrayList<PsiElement> elements = new ArrayList<PsiElement>();
for (PsiElement matchStatement : matchStatements) {
if (!filter.accepts(matchStatement)) {
elements.add(matchStatement);
}
}
new DeleteNodesAction(compilingVisitor.getLexicalNodes()).run();
return elements;
}
private static void addScriptConstraint(String name, MatchVariableConstraint constraint, SubstitutionHandler handler) {
if (constraint.getScriptCodeConstraint()!= null && constraint.getScriptCodeConstraint().length() > 2) {
final String script = StringUtil.stripQuotesAroundValue(constraint.getScriptCodeConstraint());
final String s = ScriptSupport.checkValidScript(script);
if (s != null) throw new MalformedPatternException("Script constraint for " + constraint.getName() + " has problem "+s);
MatchPredicate predicate = new ScriptPredicate(name, script);
addPredicate(handler,predicate);
}
}
static void addPredicate(SubstitutionHandler handler, MatchPredicate predicate) {
if (handler.getPredicate()==null) {
handler.setPredicate(predicate);
} else {
handler.setPredicate(new BinaryPredicate(handler.getPredicate(), predicate, false));
}
}
}