package org.jetbrains.plugins.cucumber; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; import com.intellij.psi.search.PsiSearchHelper; import com.intellij.psi.search.PsiSearchHelper.SERVICE; import com.intellij.psi.search.SearchScope; import com.intellij.psi.search.TextOccurenceProcessor; import com.intellij.psi.search.UsageSearchContext; import com.intellij.util.Processor; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.cucumber.steps.search.CucumberStepSearchUtil; public class CucumberUtil { @NonNls public static final String STEP_DEFINITIONS_DIR_NAME = "step_definitions"; public static final String[][] ARR = { {"\\\\", "\\\\\\\\"}, {"\\|", "\\\\|"}, {"\\$", "\\\\\\$"}, {"\\^", "\\\\^"}, {"\\+", "\\+"}, {"\\-", "\\\\-"}, {"\\#", "\\\\#"}, {"\\?", "\\\\?"}, {"\\*", "\\\\*"}, {"\\/", "\\\\/"}, {"\\{", "\\\\{"}, {"\\}", "\\\\}"}, {"\\[", "\\\\["}, {"\\]", "\\\\]"}, {"\\(", "\\\\("}, {"\\)", "\\\\)"}, {"\\+", "\\\\+"}, {"\"([^\\\\\"]*)\"", "\"([^\"]*)\""}, {"(?<=^|[ .,])\\d+[ ]", "(\\\\d+) "}, {"(?<=^|[ .,])\\d+[,]", "(\\\\d+),"}, {"(?<=^|[ .,])\\d+[.]", "(\\\\d+)."}, {"(?<=^|[ .,])\\d+$", "(\\\\d+)"}, {"\\.", "\\\\."}, {"(<[^>]*>)", "(.*)"}, }; public static final char LEFT_PAR = '('; public static final char RIGHT_PAR = ')'; public static final char LEFT_SQUARE_BRACE = '['; public static final char RIGHT_SQUARE_BRACE = ']'; public static final char LEFT_BRACE = '{'; public static final char RIGHT_BRACE = '}'; public static final char ESCAPE_SLASH = '\\'; public static final String PREFIX_CHAR = "^"; public static final String SUFFIX_CHAR = "$"; /** * Searches for the all references to element, representing step definition from Gherkin steps. * Each step should have poly reference that resolves to this element. * Uses {@link #findPossibleGherkinElementUsages(com.intellij.psi.PsiElement, String, com.intellij.psi.search.TextOccurenceProcessor, com.intellij.psi.search.SearchScope)} * to find elements. Than, checks for references. * * @param stepDefinitionElement step defining element (most probably method) * @param regexp regexp step should match * @param consumer each reference would be reported here * @param effectiveSearchScope search scope * @return whether reference was found and reported to consumer * @see #findPossibleGherkinElementUsages(com.intellij.psi.PsiElement, String, com.intellij.psi.search.TextOccurenceProcessor, com.intellij.psi.search.SearchScope) */ public static boolean findGherkinReferencesToElement(@NotNull final PsiElement stepDefinitionElement, @NotNull final String regexp, @NotNull final Processor<PsiReference> consumer, @NotNull final SearchScope effectiveSearchScope) { return findPossibleGherkinElementUsages(stepDefinitionElement, regexp, new MyReferenceCheckingProcessor(stepDefinitionElement, consumer), effectiveSearchScope); } /** * Passes to {@link com.intellij.psi.search.TextOccurenceProcessor} all elements in gherkin files that <em>may</em> have reference to * provided argument. I.e: calling this function for string literal "(.+)foo" would find step "Given I am foo". * To extract search text, {@link #getTheBiggestWordToSearchByIndex(String)} is used. * * @param stepDefinitionElement step defining element to search refs for. * @param regexp regexp step should match * @param processor each text occurence would be reported here * @param effectiveSearchScope search scope * @return whether reference was found and passed to processor * @see #findGherkinReferencesToElement(com.intellij.psi.PsiElement, String, com.intellij.util.Processor, com.intellij.psi.search.SearchScope) */ public static boolean findPossibleGherkinElementUsages(@NotNull final PsiElement stepDefinitionElement, @NotNull final String regexp, @NotNull final TextOccurenceProcessor processor, @NotNull final SearchScope effectiveSearchScope) { final String word = getTheBiggestWordToSearchByIndex(regexp); if (StringUtil.isEmptyOrSpaces(word)) { return true; } final SearchScope searchScope = CucumberStepSearchUtil.restrictScopeToGherkinFiles(() -> effectiveSearchScope); final short context = (short)(UsageSearchContext.IN_STRINGS | UsageSearchContext.IN_CODE); final PsiSearchHelper instance = SERVICE.getInstance(stepDefinitionElement.getProject()); return instance.processElementsWithWord(processor, searchScope, word, context, true); } public static String getTheBiggestWordToSearchByIndex(@NotNull String regexp) { String result = ""; if (regexp.startsWith(PREFIX_CHAR)) { regexp = regexp.substring(1); } if (regexp.endsWith(SUFFIX_CHAR)) { regexp = regexp.substring(0, regexp.length() - 1); } int par = 0; int squareBrace = 0; int brace = 0; StringBuilder sb = new StringBuilder(); for (int i = 0; i < regexp.length(); i++) { char c = regexp.charAt(i); if (c == '#') { sb = new StringBuilder(); continue; } if (c != ESCAPE_SLASH) { if (c == LEFT_PAR) { par++; } if (c == RIGHT_PAR) { if (par > 0) { par--; } } if (c == LEFT_BRACE) { brace++; } if (c == RIGHT_BRACE) { if (brace > 0) { brace--; } } if (c == LEFT_SQUARE_BRACE) { squareBrace++; } if (c == RIGHT_SQUARE_BRACE) { if (squareBrace > 0) { squareBrace--; } } } else { sb = new StringBuilder(); i++; } if (par > 0 | squareBrace > 0 | brace > 0) { if (par + squareBrace + brace == 1) { // if it's first brace sb = new StringBuilder(); } continue; } if (Character.isLetterOrDigit(c)) { sb.append(c); if (sb.length() > 0) { if (sb.toString().length() > result.length()) { result = sb.toString(); } } } else { sb = new StringBuilder(); } } if (sb.length() > 0) { if (sb.toString().length() > result.length()) { result = sb.toString(); } } return result; } public static String prepareStepRegexp(String stepName) { String result = stepName; for (String[] rule : ARR) { result = result.replaceAll(rule[0], rule[1]); } return result; } /** * Accepts each element and checks if it has reference to some other element */ private static class MyReferenceCheckingProcessor implements TextOccurenceProcessor { @NotNull private final PsiElement myElementToFind; @NotNull private final Processor<PsiReference> myConsumer; private MyReferenceCheckingProcessor(@NotNull final PsiElement elementToFind, @NotNull final Processor<PsiReference> consumer) { myElementToFind = elementToFind; myConsumer = consumer; } @Override public boolean execute(@NotNull final PsiElement element, final int offsetInElement) { final PsiElement parent = element.getParent(); final boolean result = executeInternal(element); // We check element and its parent (StringLiteral is probably child of GherkinStep that has reference) // TODO: Search for GherkinStep parent? if (result && (parent != null)) { return executeInternal(parent); } return result; } /** * Gets all injected reference and checks if some of them points to {@link #myElementToFind} * * @param referenceOwner element with injected references * @return true if element found and consumed */ private boolean executeInternal(@NotNull final PsiElement referenceOwner) { for (final PsiReference ref : referenceOwner.getReferences()) { if ((ref != null) && ref.isReferenceTo(myElementToFind)) { if (!myConsumer.process(ref)) { return false; } } } return true; } } }