blob: d667b068ee48e566dd03e4d6d6d528ef1c98e0f9 [file] [log] [blame]
package com.intellij.structuralsearch.impl.matcher;
import com.intellij.dupLocator.iterators.ArrayBackedNodeIterator;
import com.intellij.dupLocator.iterators.NodeIterator;
import com.intellij.lang.Language;
import com.intellij.lang.StdLanguages;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypes;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentIterator;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.search.DelegatingGlobalSearchScope;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.SearchScope;
import com.intellij.structuralsearch.*;
import com.intellij.structuralsearch.impl.matcher.compiler.PatternCompiler;
import com.intellij.structuralsearch.impl.matcher.handlers.MatchingHandler;
import com.intellij.structuralsearch.impl.matcher.handlers.TopLevelMatchingHandler;
import com.intellij.structuralsearch.impl.matcher.iterators.SsrFilteringNodeIterator;
import com.intellij.structuralsearch.impl.matcher.strategies.MatchingStrategy;
import com.intellij.structuralsearch.plugin.ui.Configuration;
import com.intellij.structuralsearch.plugin.util.CollectingMatchResultSink;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.PairProcessor;
import com.intellij.util.SmartList;
import com.intellij.util.indexing.FileBasedIndex;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* This class makes program structure tree matching:
*/
public class MatcherImpl {
private static final Logger LOG = Logger.getInstance("#com.intellij.structuralsearch.impl.matcher.MatcherImpl");
// project being worked on
private final Project project;
// context of matching
private final MatchContext matchContext;
private boolean isTesting;
// visitor to delegate the real work
private final GlobalMatchingVisitor visitor = new GlobalMatchingVisitor();
private ProgressIndicator progress;
private final TaskScheduler scheduler = new TaskScheduler();
private int totalFilesToScan;
private int scannedFilesCount;
public MatcherImpl(final Project project, final MatchOptions matchOptions) {
this.project = project;
matchContext = new MatchContext();
matchContext.setMatcher(visitor);
if (matchOptions != null) {
matchContext.setOptions(matchOptions);
cacheCompiledPattern(matchOptions, PatternCompiler.compilePattern(project,matchOptions));
}
}
static class LastMatchData {
CompiledPattern lastPattern;
MatchOptions lastOptions;
}
private static SoftReference<LastMatchData> lastMatchData;
protected MatcherImpl(Project project) {
this(project, null);
}
public static void validate(Project project, MatchOptions options) {
PsiDocumentManager.getInstance(project).commitAllDocuments();
synchronized(MatcherImpl.class) {
final LastMatchData data = new LastMatchData();
data.lastPattern = PatternCompiler.compilePattern(project, options);
data.lastOptions = options;
lastMatchData = new SoftReference<LastMatchData>(data);
}
final StructuralSearchProfile profile = StructuralSearchUtil.getProfileByFileType(options.getFileType());
profile.checkSearchPattern(project, options);
}
public static class CompiledOptions {
public final List<Pair<MatchContext, Configuration>> matchContexts;
public CompiledOptions(final List<Pair<MatchContext, Configuration>> matchContexts) {
this.matchContexts = matchContexts;
}
public List<Pair<MatchContext, Configuration>> getMatchContexts() {
return matchContexts;
}
}
public static boolean checkIfShouldAttemptToMatch(MatchContext context, NodeIterator matchedNodes) {
final CompiledPattern pattern = context.getPattern();
final NodeIterator patternNodes = pattern.getNodes();
try {
while (true) {
final PsiElement patternNode = patternNodes.current();
if (patternNode == null) {
return true;
}
final PsiElement matchedNode = matchedNodes.current();
if (matchedNode == null) {
return false;
}
final MatchingHandler matchingHandler = pattern.getHandler(patternNode);
if (matchingHandler == null || !matchingHandler.canMatch(patternNode, matchedNode)) {
return false;
}
matchedNodes.advance();
patternNodes.advance();
}
} finally {
patternNodes.reset();
matchedNodes.reset();
}
}
public void processMatchesInElement(MatchContext context, Configuration configuration,
NodeIterator matchedNodes,
PairProcessor<MatchResult, Configuration> processor) {
try {
configureOptions(context, configuration, matchedNodes.current(), processor);
context.setShouldRecursivelyMatch(false);
visitor.matchContext(matchedNodes);
} finally {
matchedNodes.reset();
}
}
public void clearContext() {
matchContext.clear();
}
private void configureOptions(MatchContext context,
final Configuration configuration,
PsiElement psiFile,
final PairProcessor<MatchResult, Configuration> processor) {
if (psiFile == null) return;
LocalSearchScope scope = new LocalSearchScope(psiFile);
matchContext.clear();
matchContext.setMatcher(visitor);
MatchOptions options = context.getOptions();
matchContext.setOptions(options);
matchContext.setPattern(context.getPattern());
matchContext.setShouldRecursivelyMatch(context.shouldRecursivelyMatch());
visitor.setMatchContext(matchContext);
matchContext.setSink(
new MatchConstraintsSink(
new MatchResultSink() {
public void newMatch(MatchResult result) {
processor.process(result, configuration);
}
public void processFile(PsiFile element) {
}
public void setMatchingProcess(MatchingProcess matchingProcess) {
}
public void matchingFinished() {
}
public ProgressIndicator getProgressIndicator() {
return null;
}
},
options.getMaxMatchesCount(),
options.isDistinct(),
options.isCaseSensitiveMatch()
)
);
options.setScope(scope);
}
public CompiledOptions precompileOptions(List<Configuration> configurations) {
List<Pair<MatchContext, Configuration>> contexts = new ArrayList<Pair<MatchContext, Configuration>>();
for (Configuration configuration : configurations) {
MatchContext matchContext = new MatchContext();
matchContext.setMatcher(visitor);
MatchOptions matchOptions = configuration.getMatchOptions();
matchContext.setOptions(matchOptions);
try {
CompiledPattern compiledPattern = PatternCompiler.compilePattern(project, matchOptions);
matchContext.setPattern(compiledPattern);
contexts.add(Pair.create(matchContext, configuration));
}
catch (UnsupportedPatternException ignored) {}
catch (MalformedPatternException ignored) {}
}
return new CompiledOptions(contexts);
}
Project getProject() {
return project;
}
/**
* Finds the matches of given pattern starting from given tree element.
* @throws MalformedPatternException
* @throws UnsupportedPatternException
*/
protected void findMatches(MatchResultSink sink, final MatchOptions options) throws MalformedPatternException, UnsupportedPatternException
{
CompiledPattern compiledPattern = prepareMatching(sink, options);
if (compiledPattern== null) {
return;
}
matchContext.getSink().setMatchingProcess( scheduler );
scheduler.init();
progress = matchContext.getSink().getProgressIndicator();
if (/*TokenBasedSearcher.canProcess(compiledPattern)*/ false) {
//TokenBasedSearcher searcher = new TokenBasedSearcher(this);
//searcher.search(compiledPattern);
if (isTesting) {
matchContext.getSink().matchingFinished();
return;
}
}
else {
if (isTesting) {
// testing mode;
final PsiElement[] elements = ((LocalSearchScope)options.getScope()).getScope();
PsiElement parent = elements[0].getParent();
if (elements.length > 0 && matchContext.getPattern().getStrategy().continueMatching(parent != null ? parent : elements[0])) {
visitor.matchContext(new SsrFilteringNodeIterator(new ArrayBackedNodeIterator(elements)));
}
else {
for (PsiElement element : elements) {
match(element);
}
}
matchContext.getSink().matchingFinished();
return;
}
if (!findMatches(options, compiledPattern)) {
return;
}
}
if (scheduler.getTaskQueueEndAction()==null) {
scheduler.setTaskQueueEndAction(
new Runnable() {
public void run() {
matchContext.getSink().matchingFinished();
}
}
);
}
scheduler.executeNext();
}
private boolean findMatches(MatchOptions options, CompiledPattern compiledPattern) {
LanguageFileType languageFileType = (LanguageFileType)options.getFileType();
final StructuralSearchProfile profile = StructuralSearchUtil.getProfileByLanguage(languageFileType.getLanguage());
assert profile != null;
PsiElement node = compiledPattern.getNodes().current();
final Language ourPatternLanguage = node != null ? profile.getLanguage(node) : ((LanguageFileType)options.getFileType()).getLanguage();
final Language ourPatternLanguage2 = ourPatternLanguage == StdLanguages.XML ? StdLanguages.XHTML:null;
SearchScope searchScope = compiledPattern.getScope();
boolean ourOptimizedScope = searchScope != null;
if (!ourOptimizedScope) searchScope = options.getScope();
if (searchScope instanceof GlobalSearchScope) {
final GlobalSearchScope scope = (GlobalSearchScope)searchScope;
final ContentIterator ci = new ContentIterator() {
public boolean processFile(final VirtualFile fileOrDir) {
if (!fileOrDir.isDirectory() && scope.contains(fileOrDir) && fileOrDir.getFileType() != FileTypes.UNKNOWN) {
++totalFilesToScan;
scheduler.addOneTask(new MatchOneVirtualFile(fileOrDir, profile, ourPatternLanguage, ourPatternLanguage2));
}
return true;
}
};
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
public void run() {
FileBasedIndex.getInstance().iterateIndexableFiles(ci, project, progress);
}
});
progress.setText2("");
}
else {
final PsiElement[] elementsToScan = ((LocalSearchScope)searchScope).getScope();
totalFilesToScan = elementsToScan.length;
for (int i = 0; i < elementsToScan.length; ++i) {
final PsiElement psiElement = elementsToScan[i];
if (psiElement == null) continue;
final Language language = psiElement.getLanguage();
PsiFile file = psiElement instanceof PsiFile ? (PsiFile)psiElement : psiElement.getContainingFile();
if (profile.isMyFile(file, language, ourPatternLanguage, ourPatternLanguage2)) {
scheduler.addOneTask(new MatchOnePsiFile(psiElement));
}
if (ourOptimizedScope) elementsToScan[i] = null; // to prevent long PsiElement reference
}
}
return true;
}
private CompiledPattern prepareMatching(final MatchResultSink sink, final MatchOptions options) {
CompiledPattern savedPattern = null;
if (matchContext.getOptions() == options && matchContext.getPattern() != null &&
matchContext.getOptions().hashCode() == matchContext.getPattern().getOptionsHashStamp()) {
savedPattern = matchContext.getPattern();
}
matchContext.clear();
matchContext.setSink(
new MatchConstraintsSink(
sink,
options.getMaxMatchesCount(),
options.isDistinct(),
options.isCaseSensitiveMatch()
)
);
matchContext.setOptions(options);
matchContext.setMatcher(visitor);
visitor.setMatchContext(matchContext);
CompiledPattern compiledPattern = savedPattern;
if (compiledPattern == null) {
synchronized(getClass()) {
final LastMatchData data = com.intellij.reference.SoftReference.dereference(lastMatchData);
if (data != null && options == data.lastOptions) {
compiledPattern = data.lastPattern;
}
lastMatchData = null;
}
if (compiledPattern==null) {
compiledPattern = ApplicationManager.getApplication().runReadAction(new Computable<CompiledPattern>() {
@Override
public CompiledPattern compute() {
return PatternCompiler.compilePattern(project,options);
}
});
}
}
cacheCompiledPattern(options, compiledPattern);
return compiledPattern;
}
private void cacheCompiledPattern(final MatchOptions options, final CompiledPattern compiledPattern) {
matchContext.setPattern(compiledPattern);
compiledPattern.setOptionsHashStamp(options.hashCode());
}
/**
* Finds the matches of given pattern starting from given tree element.
* @param sink match result destination
* @throws MalformedPatternException
* @throws UnsupportedPatternException
*/
protected void testFindMatches(MatchResultSink sink, MatchOptions options)
throws MalformedPatternException, UnsupportedPatternException {
isTesting = true;
try {
findMatches(sink,options);
} finally {
isTesting = false;
}
}
/**
* Finds the matches of given pattern starting from given tree element.
* @param source string for search
* @param pattern to be searched
* @return list of matches found
* @throws MalformedPatternException
* @throws UnsupportedPatternException
*/
protected List<MatchResult> testFindMatches(String source,
String pattern,
MatchOptions options,
boolean filePattern,
FileType sourceFileType,
String sourceExtension,
boolean physicalSourceFile)
throws MalformedPatternException, UnsupportedPatternException {
CollectingMatchResultSink sink = new CollectingMatchResultSink();
try {
PsiElement[] elements = MatcherImplUtil.createSourceTreeFromText(source,
filePattern ? PatternTreeContext.File : PatternTreeContext.Block,
sourceFileType,
sourceExtension,
project, physicalSourceFile);
options.setSearchPattern(pattern);
options.setScope(new LocalSearchScope(elements));
testFindMatches(sink, options);
}
catch (IncorrectOperationException e) {
MalformedPatternException exception = new MalformedPatternException();
exception.initCause(e);
throw exception;
}
return sink.getMatches();
}
protected List<MatchResult> testFindMatches(String source, String pattern, MatchOptions options, boolean filePattern) {
return testFindMatches(source, pattern, options, filePattern, options.getFileType(), null, false);
}
class TaskScheduler implements MatchingProcess {
private LinkedList<Runnable> tasks = new LinkedList<Runnable>();
private boolean ended;
private Runnable taskQueueEndAction;
private boolean suspended;
public void stop() {
ended = true;
}
public void pause() {
suspended = true;
}
public void resume() {
if (!suspended) return;
suspended = false;
executeNext();
}
public boolean isSuspended() {
return suspended;
}
public boolean isEnded() {
return ended;
}
void setTaskQueueEndAction(Runnable taskQueueEndAction) {
this.taskQueueEndAction = taskQueueEndAction;
}
Runnable getTaskQueueEndAction () {
return taskQueueEndAction;
}
void addOneTask(Runnable runnable) {
tasks.add(runnable);
}
private void executeNext() {
while(!suspended && !ended) {
if (tasks.isEmpty()) {
ended = true;
break;
}
final Runnable task = tasks.removeFirst();
try {
task.run();
}
catch (ProcessCanceledException e) {
ended = true;
clearSchedule();
throw e;
}
catch (StructuralSearchException e) {
ended = true;
clearSchedule();
throw e;
}
catch (Throwable th) {
LOG.error(th);
}
}
if (ended) clearSchedule();
}
private void init() {
ended = false;
suspended = false;
PsiManager.getInstance(project).startBatchFilesProcessingMode();
}
private void clearSchedule() {
if (tasks != null) {
taskQueueEndAction.run();
if (!project.isDisposed()) {
PsiManager.getInstance(project).finishBatchFilesProcessingMode();
}
tasks = null;
}
}
}
private class MatchOnePsiFile extends MatchOneFile {
private PsiElement file;
MatchOnePsiFile(PsiElement file) {
this.file = file;
}
@Nullable
@Override
protected List<PsiElement> getPsiElementsToProcess() {
PsiElement file = this.file;
this.file = null;
return new SmartList<PsiElement>(file);
}
}
private abstract class MatchOneFile implements Runnable {
public void run() {
List<PsiElement> files = getPsiElementsToProcess();
if (progress!=null) {
progress.setFraction((double)scannedFilesCount/totalFilesToScan);
}
++scannedFilesCount;
if (files == null || files.size() == 0) return;
final PsiFile psiFile = files.get(0).getContainingFile();
if (psiFile!=null) {
final Runnable action = new Runnable() {
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
if (project.isDisposed()) return;
final PsiDocumentManager manager = PsiDocumentManager.getInstance(project);
Document document = manager.getDocument(psiFile);
if (document != null) manager.commitDocument(document);
}
});
}
};
if (ApplicationManager.getApplication().isDispatchThread()) {
action.run();
} else {
ApplicationManager.getApplication().invokeAndWait(
action,
ModalityState.defaultModalityState()
);
}
}
if (project.isDisposed()) return;
for(PsiElement file:files) {
if (file instanceof PsiFile) {
matchContext.getSink().processFile((PsiFile)file);
}
final PsiElement finalFile = file;
ApplicationManager.getApplication().runReadAction(
new Runnable() {
public void run() {
PsiElement file = finalFile;
if (!file.isValid()) return;
file = StructuralSearchUtil.getProfileByLanguage(file.getLanguage()).extendMatchOnePsiFile(file);
match(file);
}
}
);
}
}
protected abstract @Nullable List<PsiElement> getPsiElementsToProcess();
}
// Initiates the matching process for given element
// @param element the current search tree element
public void match(PsiElement element) {
MatchingStrategy strategy = matchContext.getPattern().getStrategy();
if (strategy.continueMatching(element)) {
visitor.matchContext(new ArrayBackedNodeIterator(new PsiElement[] {element}));
return;
}
for(PsiElement el=element.getFirstChild();el!=null;el=el.getNextSibling()) {
match(el);
}
if (element instanceof PsiLanguageInjectionHost) {
InjectedLanguageUtil.enumerate(element, new PsiLanguageInjectionHost.InjectedPsiVisitor() {
@Override
public void visit(@NotNull PsiFile injectedPsi, @NotNull List<PsiLanguageInjectionHost.Shred> places) {
match(injectedPsi);
}
});
}
}
@Nullable
protected MatchResult isMatchedByDownUp(PsiElement element, final MatchOptions options) {
final CollectingMatchResultSink sink = new CollectingMatchResultSink();
CompiledPattern compiledPattern = prepareMatching(sink, options);
if (compiledPattern== null) {
assert false;
return null;
}
PsiElement targetNode = compiledPattern.getTargetNode();
PsiElement elementToStartMatching = null;
if (targetNode == null) {
targetNode = compiledPattern.getNodes().current();
if (targetNode != null) {
compiledPattern.getNodes().advance();
assert !compiledPattern.getNodes().hasNext();
compiledPattern.getNodes().rewind();
while (element.getClass() != targetNode.getClass()) {
element = element.getParent();
if (element == null) return null;
}
elementToStartMatching = element;
}
} else {
targetNode = StructuralSearchUtil.getProfileByPsiElement(element).extendMatchedByDownUp(targetNode);
MatchingHandler handler = null;
while (element.getClass() == targetNode.getClass() ||
compiledPattern.isTypedVar(targetNode) && compiledPattern.getHandler(targetNode).canMatch(targetNode, element)
) {
handler = compiledPattern.getHandler(targetNode);
handler.setPinnedElement(element);
elementToStartMatching = element;
if (handler instanceof TopLevelMatchingHandler) break;
element = element.getParent();
targetNode = targetNode.getParent();
if (options.isLooseMatching()) {
element = StructuralSearchUtil.getProfileByPsiElement(element).updateCurrentNode(element);
targetNode = StructuralSearchUtil.getProfileByPsiElement(element).updateCurrentNode(targetNode);
}
}
if (!(handler instanceof TopLevelMatchingHandler)) return null;
}
assert targetNode != null : "Could not match down up when no target node";
match(elementToStartMatching);
matchContext.getSink().matchingFinished();
final int matchCount = sink.getMatches().size();
assert matchCount <= 1;
return matchCount > 0 ? sink.getMatches().get(0) : null;
}
private class MatchOneVirtualFile extends MatchOneFile {
private final VirtualFile myFileOrDir;
private final StructuralSearchProfile myProfile;
private final Language myOurPatternLanguage;
private final Language myOurPatternLanguage2;
public MatchOneVirtualFile(VirtualFile fileOrDir,
StructuralSearchProfile profile,
Language ourPatternLanguage,
Language ourPatternLanguage2) {
myFileOrDir = fileOrDir;
myProfile = profile;
myOurPatternLanguage = ourPatternLanguage;
myOurPatternLanguage2 = ourPatternLanguage2;
}
@Nullable
@Override
protected List<PsiElement> getPsiElementsToProcess() {
return ApplicationManager.getApplication().runReadAction(new Computable<List<PsiElement>>() {
@Override
public List<PsiElement> compute() {
PsiFile file = PsiManager.getInstance(project).findFile(myFileOrDir);
if (file == null) {
return null;
}
final FileViewProvider viewProvider = file.getViewProvider();
List<PsiElement> elementsToProcess = new SmartList<PsiElement>();
for(Language lang: viewProvider.getLanguages()) {
if (myProfile.isMyFile(file, lang, myOurPatternLanguage, myOurPatternLanguage2)) {
elementsToProcess.add(viewProvider.getPsi(lang));
}
}
return elementsToProcess;
}
});
}
}
}