From ec800a11bd29e18bc4a2fc9ffbd48ff7bc788de0 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Sun, 9 Jun 2024 10:30:05 +0200 Subject: [PATCH 1/9] Add caching for the JBehave step reference resolution and Given-When-Then step annotation lookup. --- CHANGELOG.md | 4 + gradle.properties | 2 +- .../JBehaveStepReferenceProvider.java | 19 ----- .../resolver/StepPsiReference.java | 63 +++++++++++----- .../resolver/StepPsiReferenceContributor.java | 17 ++++- .../service/JBehaveStepsIndex.java | 26 +------ .../service/StepAnnotationsCache.java | 73 +++++++++++++++++++ 7 files changed, 142 insertions(+), 62 deletions(-) delete mode 100644 src/main/java/com/github/kumaraman21/intellijbehave/resolver/JBehaveStepReferenceProvider.java create mode 100644 src/main/java/com/github/kumaraman21/intellijbehave/service/StepAnnotationsCache.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b05aa7d..3590643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## [1.65.0] +### Changed +- Added caching for the JBehave step reference resolution and Given-When-Then step annotation lookup. + ## [1.64.0] ### Changed - New supported IDE version range: 2023.1.6-2024.2-EAP diff --git a/gradle.properties b/gradle.properties index c62647f..5e1e4d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.kumaraman21.intellijbehave pluginName = JBehave Support pluginRepositoryUrl = https://github.com/witspirit/IntelliJBehave # SemVer format -> https://semver.org -pluginVersion = 1.64.0 +pluginVersion = 1.65.0 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 231.9414.13 diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/JBehaveStepReferenceProvider.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/JBehaveStepReferenceProvider.java deleted file mode 100644 index 2505a3b..0000000 --- a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/JBehaveStepReferenceProvider.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.kumaraman21.intellijbehave.resolver; - -import com.github.kumaraman21.intellijbehave.parser.JBehaveStep; -import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReference; -import com.intellij.psi.PsiReferenceProvider; -import com.intellij.util.ProcessingContext; -import org.jetbrains.annotations.NotNull; - -public final class JBehaveStepReferenceProvider extends PsiReferenceProvider { - @NotNull - @Override - public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { - return element instanceof JBehaveStep step - ? new PsiReference[]{new StepPsiReference(step, TextRange.from(0, element.getTextLength()))} - : PsiReference.EMPTY_ARRAY; - } -} diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java index fbac3eb..bdec52b 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java @@ -20,11 +20,13 @@ import com.github.kumaraman21.intellijbehave.service.JavaStepDefinition; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiPolyVariantReference; import com.intellij.psi.ResolveResult; import com.intellij.psi.impl.PsiManagerEx; -import com.intellij.util.ArrayUtil; +import com.intellij.psi.impl.source.resolve.ResolveCache; +import com.intellij.psi.impl.source.resolve.reference.impl.CachingReference; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -32,7 +34,14 @@ import java.util.ArrayList; import java.util.Collection; -public class StepPsiReference implements PsiPolyVariantReference { +/** + * This reference extends {@link CachingReference}, so that resolved results are cached. + *

+ * This prevents other parts of the IntelliJ Platform, that constantly call {@code resolve()}, + * to call {@link #resolveToDefinitions()} and in turn {@link JBehaveStepsIndex#findStepDefinitions(JBehaveStep)} + * over and over again. + */ +public class StepPsiReference extends CachingReference implements PsiPolyVariantReference { private final JBehaveStep myStep; private final TextRange myRange; @@ -41,6 +50,8 @@ public StepPsiReference(JBehaveStep element, TextRange range) { myRange = range; } + //Getters and handlers + @Override public @NotNull JBehaveStep getElement() { return myStep; @@ -51,12 +62,6 @@ public StepPsiReference(JBehaveStep element, TextRange range) { return myRange; } - @Override - public PsiElement resolve() { - ResolveResult[] result = multiResolve(true); - return result.length == 1 ? result[0].getElement() : null; - } - @NotNull @Override public String getCanonicalText() { @@ -73,6 +78,13 @@ public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOpe return myStep; } + @Override + public boolean isSoft() { + return false; + } + + //Resolution + @Override public boolean isReferenceTo(@NotNull PsiElement element) { PsiManagerEx manager = null; @@ -95,16 +107,6 @@ public boolean isReferenceTo(@NotNull PsiElement element) { return false; } - @Override - public Object @NotNull [] getVariants() { - return ArrayUtil.EMPTY_OBJECT_ARRAY; - } - - @Override - public boolean isSoft() { - return false; - } - /** * Returns the first Java step definition found for this reference. */ @@ -119,6 +121,15 @@ private Collection resolveToDefinitions() { return JBehaveStepsIndex.getInstance(myStep.getProject()).findStepDefinitions(myStep); } + @Override + public @Nullable PsiElement resolveInner() { + var resolveResults = ResolveCache.getInstance(myStep.getProject()) + .resolveWithCaching(this, DelegatingResolver.INSTANCE, false, false, myStep.getContainingFile()); + return resolveResults.length > 0 + ? resolveResults[0].getElement() + : null; + } + @Override public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { var result = new ArrayList(4); @@ -142,4 +153,20 @@ public boolean isValidResult() { return result.toArray(ResolveResult.EMPTY_ARRAY); } + + /** + * {@link ResolveCache#resolveWithCaching} calls this to resolve and cache the resolve results. + *

+ * This resolver delegates to {@link #multiResolve(boolean)}. + *

+ * Currently, it doesn't utilize the passed in {@link PsiFile} instance. + */ + private static final class DelegatingResolver implements ResolveCache.PolyVariantContextResolver { + private static final DelegatingResolver INSTANCE = new DelegatingResolver(); + + @Override + public ResolveResult @NotNull [] resolve(@NotNull StepPsiReference ref, @NotNull PsiFile containingFile, boolean incompleteCode) { + return ref.multiResolve(false); + } + } } diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributor.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributor.java index 66d70e0..807edd1 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributor.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReferenceContributor.java @@ -1,8 +1,13 @@ package com.github.kumaraman21.intellijbehave.resolver; import com.github.kumaraman21.intellijbehave.parser.JBehaveStep; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; import com.intellij.psi.PsiReferenceContributor; +import com.intellij.psi.PsiReferenceProvider; import com.intellij.psi.PsiReferenceRegistrar; +import com.intellij.util.ProcessingContext; import org.jetbrains.annotations.NotNull; import static com.intellij.patterns.PlatformPatterns.psiElement; @@ -10,13 +15,21 @@ /** * Provides references for JBehave steps in Story files. * - * @see JBehaveStepReferenceProvider * @see StepPsiReference */ public final class StepPsiReferenceContributor extends PsiReferenceContributor { + private static final PsiReferenceProvider REFERENCE_PROVIDER = new PsiReferenceProvider() { + @Override + public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { + return element instanceof JBehaveStep step + ? new PsiReference[]{new StepPsiReference(step, TextRange.from(0, element.getTextLength()))} + : PsiReference.EMPTY_ARRAY; + } + }; + @Override public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { - registrar.registerReferenceProvider(psiElement(JBehaveStep.class), new JBehaveStepReferenceProvider()); + registrar.registerReferenceProvider(psiElement(JBehaveStep.class), REFERENCE_PROVIDER); } } diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java index 577f254..cb03664 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java @@ -17,14 +17,10 @@ import com.intellij.psi.PsiAnnotation; import com.intellij.psi.PsiClass; import com.intellij.psi.impl.java.stubs.index.JavaAnnotationIndex; -import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.CachedValueProvider; import com.intellij.psi.util.CachedValuesManager; import com.intellij.psi.util.QualifiedName; -import org.jbehave.core.annotations.Given; -import org.jbehave.core.annotations.Then; -import org.jbehave.core.annotations.When; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -77,16 +73,13 @@ public Collection findStepDefinitions(@NotNull JBehaveStep s private List loadStepsFor(@NotNull Module module) { GlobalSearchScope dependenciesScope = module.getModuleWithDependenciesAndLibrariesScope(true); - PsiClass givenAnnotationClass = findStepAnnotation(Given.class.getName(), module, dependenciesScope); - if (givenAnnotationClass == null) return emptyList(); - PsiClass whenAnnotationClass = findStepAnnotation(When.class.getName(), module, dependenciesScope); - if (whenAnnotationClass == null) return emptyList(); - PsiClass thenAnnotationClass = findStepAnnotation(Then.class.getName(), module, dependenciesScope); - if (thenAnnotationClass == null) return emptyList(); + var stepAnnotations = StepAnnotationsCache.getInstance(module.getProject()).cacheStepAnnotations(module, dependenciesScope); + if (stepAnnotations.isAnyAnnotationMissing()) + return emptyList(); List result = new ArrayList<>(); - for (PsiClass stepAnnotation : asList(givenAnnotationClass, whenAnnotationClass, thenAnnotationClass)) { + for (PsiClass stepAnnotation : asList(stepAnnotations.given(), stepAnnotations.when(), stepAnnotations.then())) { for (PsiAnnotation stepDefAnnotation : getAllStepAnnotations(stepAnnotation, dependenciesScope)) { result.add(new JavaStepDefinition(stepDefAnnotation)); } @@ -130,17 +123,6 @@ private static Collection getAllStepAnnotations(@NotNull final Ps }); } - @Nullable - private PsiClass findStepAnnotation(String stepClass, Module module, GlobalSearchScope dependenciesScope) { - var stepDefAnnotationCandidates = JavaFullClassNameIndex.getInstance().get(stepClass, module.getProject(), dependenciesScope); - for (PsiClass stepDefAnnotations : stepDefAnnotationCandidates) { - if (stepClass.equals(stepDefAnnotations.getQualifiedName())) { - return stepDefAnnotations; - } - } - return null; - } - @Override public void dispose() { //No-op. Used for ProjectStartupActivity as parent Disposable diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/StepAnnotationsCache.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/StepAnnotationsCache.java new file mode 100644 index 0000000..3980df4 --- /dev/null +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/StepAnnotationsCache.java @@ -0,0 +1,73 @@ +package com.github.kumaraman21.intellijbehave.service; + +import com.intellij.openapi.components.Service; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiClass; +import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex; +import com.intellij.psi.search.GlobalSearchScope; +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.annotations.When; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Caches the {@link PsiClass} instances of the {@link Given}, {@link When} and {@link Then} classes + * for each module in the project. + */ +@Service(Service.Level.PROJECT) +final class StepAnnotationsCache { + private final Map stepAnnotationClasses = new ConcurrentHashMap<>(4); + private final Project project; + + public StepAnnotationsCache(Project project) { + this.project = project; + } + + /** + * Caches the Given-When-Then annotations' PsiClass instances for the provided module if they haven't been. + * + * @return the cached PsiClass instances wrapped in a {@link StepAnnotations}. + */ + public StepAnnotations cacheStepAnnotations(Module module, GlobalSearchScope dependenciesScope) { + return stepAnnotationClasses.computeIfAbsent(module, __ -> new StepAnnotations( + findStepAnnotation(Given.class.getName(), dependenciesScope), + findStepAnnotation(When.class.getName(), dependenciesScope), + findStepAnnotation(Then.class.getName(), dependenciesScope) + )); + } + + /** + * Finds the {@code PsiClass} instance of the {@code stepAnnotationClassFqn}. + * + * @param stepAnnotationClassFqn the fully qualified name of the step annotation to find + * @param dependenciesScope the dependencies scope within the current module. See {@link JBehaveStepsIndex#loadStepsFor(Module)}. + * @return the PsiClass instance for the step annotation, or null if not found + */ + @Nullable("When there is no annotation class found.") + private PsiClass findStepAnnotation(String stepAnnotationClassFqn, GlobalSearchScope dependenciesScope) { + var stepDefAnnotationCandidates = JavaFullClassNameIndex.getInstance().get(stepAnnotationClassFqn, project, dependenciesScope); + for (PsiClass stepDefAnnotations : stepDefAnnotationCandidates) { + if (stepAnnotationClassFqn.equals(stepDefAnnotations.getQualifiedName())) { + return stepDefAnnotations; + } + } + return null; + } + + public static StepAnnotationsCache getInstance(Project project) { + return project.getService(StepAnnotationsCache.class); + } + + /** + * Wrapper class for the Given-When-Then annotation classes for easier caching and handling. + */ + record StepAnnotations(@Nullable PsiClass given, @Nullable PsiClass when, @Nullable PsiClass then) { + boolean isAnyAnnotationMissing() { + return given == null || when == null || then == null; + } + } +} From b58c71f18090f4fd719f714a78a32a347891b3c1 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Thu, 4 Jul 2024 16:14:30 +0200 Subject: [PATCH 2/9] Rename a few variables, and remove an unnecessary variable creation --- .../resolver/StepPsiReference.java | 8 ++++---- .../service/JBehaveStepsIndex.java | 18 +++++++++--------- .../intellijbehave/service/JBehaveUtil.java | 5 ++--- .../service/JavaStepDefinition.java | 3 +-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java index bdec52b..3409e41 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepPsiReference.java @@ -93,8 +93,8 @@ public boolean isReferenceTo(@NotNull PsiElement element) { // so that unnecessary calls to 'getAnnotatedMethod()' and instantiation of ResolveResults can be avoided. var resolvedElements = new ArrayList(4); - for (JavaStepDefinition resolvedStepDefinition : resolveToDefinitions()) { - final PsiMethod method = resolvedStepDefinition.getAnnotatedMethod(); + for (var resolvedJavaStepDefinition : resolveToDefinitions()) { + final PsiMethod method = resolvedJavaStepDefinition.getAnnotatedMethod(); if (method != null && !resolvedElements.contains(method)) { if (manager == null) manager = getElement().getManager(); if (manager.areElementsEquivalent(method, element)) { @@ -135,8 +135,8 @@ private Collection resolveToDefinitions() { var result = new ArrayList(4); var resolvedElements = new ArrayList(4); - for (JavaStepDefinition resolvedStepDefinition : resolveToDefinitions()) { - final PsiMethod method = resolvedStepDefinition.getAnnotatedMethod(); + for (var resolvedJavaStepDefinition : resolveToDefinitions()) { + final PsiMethod method = resolvedJavaStepDefinition.getAnnotatedMethod(); if (method != null && !resolvedElements.contains(method)) { resolvedElements.add(method); result.add(new ResolveResult() { diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java index cb03664..dd90a3c 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java @@ -52,16 +52,16 @@ public Collection findStepDefinitions(@NotNull JBehaveStep s return emptyList(); } - Map definitionsByClass = new HashMap<>(2); + var definitionsByClass = new HashMap(2); String stepText = step.getStepText(); - for (JavaStepDefinition stepDefinition : loadStepsFor(module)) { - if (stepDefinition.matches(stepText) && stepDefinition.supportsStep(step)) { - Integer currentHighestPriority = getPriorityByDefinition(definitionsByClass.get(stepDefinition.getClass())); - Integer newPriority = getPriorityByDefinition(stepDefinition); + for (var javaStepDefinition : loadStepsFor(module)) { + if (javaStepDefinition.matches(stepText) && javaStepDefinition.supportsStep(step)) { + Integer currentHighestPriority = getPriorityByDefinition(definitionsByClass.get(javaStepDefinition.getClass())); + Integer newPriority = getPriorityByDefinition(javaStepDefinition); if (newPriority > currentHighestPriority) { - definitionsByClass.put(stepDefinition.getClass(), stepDefinition); + definitionsByClass.put(javaStepDefinition.getClass(), javaStepDefinition); } } } @@ -77,15 +77,15 @@ private List loadStepsFor(@NotNull Module module) { if (stepAnnotations.isAnyAnnotationMissing()) return emptyList(); - List result = new ArrayList<>(); + var javaStepDefs = new ArrayList(); for (PsiClass stepAnnotation : asList(stepAnnotations.given(), stepAnnotations.when(), stepAnnotations.then())) { for (PsiAnnotation stepDefAnnotation : getAllStepAnnotations(stepAnnotation, dependenciesScope)) { - result.add(new JavaStepDefinition(stepDefAnnotation)); + javaStepDefs.add(new JavaStepDefinition(stepDefAnnotation)); } } - return result; + return javaStepDefs; } @NotNull diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java index 5af49fc..459ba8d 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java @@ -93,7 +93,7 @@ public static boolean isStepDefinition(@NotNull PsiMethod method) { */ @NotNull public static Set getAnnotationTexts(@NotNull PsiAnnotation stepAnnotation) { - Set annotationTexts = new HashSet<>(4); + var annotationTexts = new HashSet(4); getAnnotationText(stepAnnotation).ifPresent(annotationTexts::add); PsiMethod method = PsiTreeUtil.getParentOfType(stepAnnotation, PsiMethod.class); @@ -109,8 +109,7 @@ public static Set getAnnotationTexts(@NotNull PsiAnnotation stepAnnotati return annotationTexts.stream() .map(PatternVariantBuilder::new) - .map(PatternVariantBuilder::allVariants) - .flatMap(Set::stream) + .flatMap(builder -> builder.allVariants().stream()) .collect(Collectors.toSet()); } diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java index 2164dbf..479a33b 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java @@ -38,8 +38,7 @@ public JavaStepDefinition(PsiAnnotation annotation) { public boolean matches(String stepText) { final StepType annotationType = getAnnotationType(); for (String annotationText : getAnnotationTexts()) { - OptimizedStepMatcher stepMatcher = new OptimizedStepMatcher(stepPatternParser.parseStep(annotationType, annotationText)); - if (stepMatcher.matches(stepText)) + if (new OptimizedStepMatcher(stepPatternParser.parseStep(annotationType, annotationText)).matches(stepText)) return true; } From 0bd7bf9d6f98d7f134e47f1794ac6aeb4188dcf5 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Thu, 4 Jul 2024 16:15:55 +0200 Subject: [PATCH 3/9] Implement a simplified and optimized version of JBehave's PatternVariantBuilder --- build.gradle.kts | 2 +- .../core/steps/PatternVariantBuilder.java | 194 ++++++++++++++++++ .../StepDefinitionAnnotationConverter.java | 2 +- .../service/JBehaveStepsIndex.java | 1 - .../intellijbehave/service/JBehaveUtil.java | 2 +- .../core/steps/PatternVariantBuilderTest.java | 69 +++++++ 6 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java create mode 100644 src/test/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilderTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 20e434e..2013d3e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -93,7 +93,7 @@ tasks { //Required for running tests in 2021.3 due to it not finding test classes properly. //See https://app.slack.com/client/T5P9YATH9/C5U8BM1MK/thread/C5U8BM1MK-1639934273.054400 isScanForTestClasses = false - include("**/codeInspector/*Test.class", "**/resolver/*Test.class", "**/utility/*Test.class", "**/service/*Test.class") + include("**/codeInspector/*Test.class", "**/resolver/*Test.class", "**/utility/*Test.class", "**/service/*Test.class", "**/jbehave/core/steps/*Test.class") exclude("**/highlighter/*Test.class", "**/parser/*Test.class", "**/spellchecker/*Test.class", "**/structure/*Test.class") } diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java b/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java new file mode 100644 index 0000000..ec43360 --- /dev/null +++ b/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java @@ -0,0 +1,194 @@ +package com.github.kumaraman21.intellijbehave.jbehave.core.steps; + +import static java.util.Arrays.asList; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

+ * Builds a set of pattern variants of given pattern input, supporting a custom + * directives. Depending on the directives present, one or more resulting + * variants are created. + *

+ *

+ * Currently supported directives are + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PatternResult
..A {x|y} B.. + *
    + *
  • ..A x B..
  • + *
  • ..A y B..
  • + *
+ *
..A {x|y|} B.. + *
    + *
  • ..A x B..
  • + *
  • ..A y B..
  • + *
  • ..A B..
  • + *
+ *
..A {x} B.. + *
    + *
  • ..A x B..
  • + *
+ *
+ *

+ * These directives can be used to conveniently create several variants of a + * step pattern, without having to repeat it as a whole as one or more aliases. + *

+ *

+ * Examples: + *

+ * + *

+ * This is the modified and optimized version of {@link org.jbehave.core.steps.PatternVariantBuilder}. + * + * @author Daniel Schneller + */ +public class PatternVariantBuilder { + + /** + * Regular expression that locates patterns to be evaluated in the input + * pattern. + */ + private static final Pattern REGEX = Pattern.compile("([^\\n{]*+)(\\{(([^|}]++)(\\|)?+)*+\\})([^\\n]*+)"); + + private final Set variants; + + private final String input; + + /** + * Creates a builder and calculates all variants for given input. When there + * are no variants found in the input, it will itself be the only result. + * + * @param input to be evaluated + */ + public PatternVariantBuilder(String input) { + this.input = input; + this.variants = variantsFor(input); + } + + public String getInput() { + return input; + } + + /** + *

+ * Parses the {@link #input} received at construction and generates the + * variants. When there are multiple patterns in the input, the method will + * recurse on itself to generate the variants for the tailing end after the + * first matched pattern. + *

+ *

+ * Generated variants are stored in a {@link Set}, so there will never be + * any duplicates, even if the input's patterns were to result in such. + *

+ */ + private Set variantsFor(String input) { + Matcher m = REGEX.matcher(input); + + if (!m.matches()) { + // if the regex does not find any patterns, + // simply add the input as is + return Collections.singleton(input); + } + + // isolate the pattern itself, removing its wrapping {} + String patternGroup = m.group(2).replaceAll("[\\{\\}]", ""); + + // split the pattern into its options and add an empty + // string if it ends with a separator + List patternParts = new ArrayList<>(asList(patternGroup.split("\\|"))); + if (patternGroup.endsWith("|")) { + patternParts.add(""); + } + + // Store current invocation's results + Set variants = new HashSet<>(); + + if (!patternParts.isEmpty()) { + // isolate the part before the first pattern + String head = m.group(1); + + // isolate the remaining part of the input + String tail = m.group(6); + + var variantsForTail = variantsFor(tail); + + // Iterate over the current pattern's + // variants and construct the result. + for (String part : patternParts) { + var partString = head != null ? head + part : part; + + // recurse on the tail of the input + // to handle the next pattern + + // append all variants of the tail end + // and add each of them to the part we have + // built up so far. + for (String tailVariant : variantsForTail) { + variants.add(partString + tailVariant); + } + } + } + return variants; + } + + /** + * Returns a new copy set of all variants with no whitespace compression. + * + * @return a {@link Set} of all variants without whitespace compression + */ + public Set allVariants() { + return new HashSet<>(variants); + } +} diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java index 14ed13f..2ad75d3 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/resolver/StepDefinitionAnnotationConverter.java @@ -15,10 +15,10 @@ */ package com.github.kumaraman21.intellijbehave.resolver; +import com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilder; import com.intellij.psi.*; import org.jbehave.core.annotations.Alias; import org.jbehave.core.annotations.Aliases; -import org.jbehave.core.steps.PatternVariantBuilder; import org.jbehave.core.steps.StepType; import java.util.Collections; diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java index dd90a3c..e33d1ff 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java @@ -28,7 +28,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; -import java.util.Map; /** * Project service that provides Java step definitions for JBehave Story steps. diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java index 459ba8d..2d5c243 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java @@ -2,6 +2,7 @@ import static com.intellij.openapi.util.text.StringUtil.isEmptyOrSpaces; +import com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilder; import com.github.kumaraman21.intellijbehave.language.StoryFileType; import com.intellij.codeInsight.AnnotationUtil; import com.intellij.openapi.application.ReadAction; @@ -22,7 +23,6 @@ import org.jbehave.core.annotations.Given; import org.jbehave.core.annotations.Then; import org.jbehave.core.annotations.When; -import org.jbehave.core.steps.PatternVariantBuilder; import org.jetbrains.annotations.NotNull; import java.lang.annotation.Annotation; diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilderTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilderTest.java new file mode 100644 index 0000000..0f32fe7 --- /dev/null +++ b/src/test/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilderTest.java @@ -0,0 +1,69 @@ +package com.github.kumaraman21.intellijbehave.jbehave.core.steps; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +/** + * Unit test for {@link PatternVariantBuilder}. + *

+ * Modified version of {@code org.jbehave.core.steps.PatternVariantBuilderBehaviour}. + */ +class PatternVariantBuilderTest { + + @Test + void shouldReturnItselfForNoPatternString() { + PatternVariantBuilder builder = new PatternVariantBuilder("No variants"); + assertThat(builder.getInput()).isEqualTo("No variants"); + Set variants = builder.allVariants(); + assertThat(variants.iterator().next()).isEqualTo("No variants"); + assertThat(variants).hasSize(1); + } + + @Test + void shouldReturnTwoVariantsForOnePattern() { + PatternVariantBuilder builder = new PatternVariantBuilder("There are {Two|One} variants"); + assertThat(builder.getInput()).isEqualTo("There are {Two|One} variants"); + Set result = builder.allVariants(); + assertThat(result).hasSize(2).contains("There are One variants", "There are Two variants"); + } + + @Test + void shouldReturnFourVariantsForTwoPatterns() { + PatternVariantBuilder builder = new PatternVariantBuilder("There are {Two|One} variants, {hooray|alas}!"); + Set result = builder.allVariants(); + assertThat(result).hasSize(4) + .contains("There are One variants, hooray!", "There are Two variants, hooray!", "There are One variants, alas!", "There are Two variants, alas!"); + } + + @Test + void shouldReturnFourVariantsForTwoPatternsWithOptionElements() { + PatternVariantBuilder builder = new PatternVariantBuilder("There are {One|} variants{, hooray|}!"); + Set result = builder.allVariants(); + assertThat(result).hasSize(4) + .contains("There are One variants, hooray!", "There are variants, hooray!", "There are One variants!", "There are variants!"); + } + + @Test + void shouldHandleSpecialCharacters() { + PatternVariantBuilder builder = new PatternVariantBuilder("When $A {+|plus|is added to} $B"); + Set result = builder.allVariants(); + assertThat(result).hasSize(3).contains("When $A + $B", "When $A plus $B", "When $A is added to $B"); + } + + @Test + void hasUnclosedBracket() { + PatternVariantBuilder builder = new PatternVariantBuilder("When $A {+|plus|is added to $B"); + Set result = builder.allVariants(); + assertThat(result).hasSize(1).contains("When $A {+|plus|is added to $B"); + } + + @Test + void hasUnclosedBrackets() { + PatternVariantBuilder builder = new PatternVariantBuilder("When $A {+|plus|is added to} $B and }{$C"); + Set result = builder.allVariants(); + assertThat(result).hasSize(3).contains("When $A + $B and }{$C", "When $A plus $B and }{$C", "When $A is added to $B and }{$C"); + } +} From 5c76117e3240de98dd59a48ed399546715dac54e Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Thu, 4 Jul 2024 16:44:13 +0200 Subject: [PATCH 4/9] Use already available parent method, if available, for annotations in JBehaveUtil.getAnnotationTexts() --- .../intellijbehave/service/JBehaveUtil.java | 8 +++-- .../service/JavaStepDefinition.java | 2 +- .../service/JBehaveUtilTest.java | 33 ++++++++++--------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java index 2d5c243..6175e5f 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java @@ -24,6 +24,7 @@ import org.jbehave.core.annotations.Then; import org.jbehave.core.annotations.When; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.annotation.Annotation; import java.util.HashSet; @@ -92,11 +93,12 @@ public static boolean isStepDefinition(@NotNull PsiMethod method) { * @return the collection of step patterns */ @NotNull - public static Set getAnnotationTexts(@NotNull PsiAnnotation stepAnnotation) { + public static Set getAnnotationTexts(@NotNull PsiAnnotation stepAnnotation, @Nullable PsiMethod parentMethod) { var annotationTexts = new HashSet(4); getAnnotationText(stepAnnotation).ifPresent(annotationTexts::add); - PsiMethod method = PsiTreeUtil.getParentOfType(stepAnnotation, PsiMethod.class); + //If the parent method is available, e.g. from JBehaveJavaStepDefinitionSearch, then use that, otherwise compute it + PsiMethod method = parentMethod != null ? parentMethod : PsiTreeUtil.getParentOfType(stepAnnotation, PsiMethod.class); if (method != null) { for (PsiAnnotation annotation : method.getModifierList().getAnnotations()) { if (isAnnotationOfClass(annotation, Alias.class)) { @@ -140,7 +142,7 @@ private static Set getAliasesAnnotationTexts(@NotNull PsiAnnotation alia public static List getAnnotationTexts(@NotNull PsiMethod method) { return getJBehaveStepAnnotations(method) .stream() - .map(JBehaveUtil::getAnnotationTexts) + .map(annotation -> JBehaveUtil.getAnnotationTexts(annotation, method)) .flatMap(Set::stream) .collect(Collectors.toList()); } diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java index 479a33b..9f8822a 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java @@ -89,7 +89,7 @@ public PsiMethod getAnnotatedMethod() { private Set getAnnotationTexts() { PsiAnnotation annotation = getAnnotation(); - return annotation == null ? Collections.emptySet() : JBehaveUtil.getAnnotationTexts(annotation); + return annotation == null ? Collections.emptySet() : JBehaveUtil.getAnnotationTexts(annotation, null); } /** diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilTest.java index cc8a588..6c44329 100644 --- a/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilTest.java +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtilTest.java @@ -5,6 +5,7 @@ import com.github.kumaraman21.intellijbehave.JBehaveSupportTestBase; import com.intellij.psi.PsiAnnotation; import com.intellij.psi.PsiMethod; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.testFramework.junit5.RunInEdt; import org.jbehave.core.annotations.Given; import org.jbehave.core.annotations.When; @@ -195,7 +196,7 @@ void stepDefMethod(int price) { var annotation = stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); assertThat(annotation).isInstanceOf(PsiAnnotation.class); - assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation)).isEmpty(); + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, null)).isEmpty(); } @Test @@ -212,7 +213,7 @@ void stepDefMethod(int price) { var annotation = stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); assertThat(annotation).isInstanceOf(PsiAnnotation.class); - assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation)).containsExactlyInAnyOrder( + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, null)).containsExactlyInAnyOrder( "the price of the product should be $price", "the cost of the product should be $price"); } @@ -233,12 +234,13 @@ void stepDefMethod(int price) { var annotation = stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); assertThat(annotation).isInstanceOf(PsiAnnotation.class); - assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation)).containsExactlyInAnyOrder( - "the price of the product should be $price", - "the product should cost $price", - "the cost of the product should be $price", - "the product should be sold for $price" - ); + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, PsiTreeUtil.getParentOfType(annotation, PsiMethod.class))) + .containsExactlyInAnyOrder( + "the price of the product should be $price", + "the product should cost $price", + "the cost of the product should be $price", + "the product should be sold for $price" + ); } @Test @@ -258,12 +260,13 @@ void stepDefMethod(int price) { var annotation = stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); assertThat(annotation).isInstanceOf(PsiAnnotation.class); - assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation)).containsExactlyInAnyOrder( - "the price of the product should be $price", - "the product should cost $price", - "the cost of the product should be $price", - "the product should be sold for $price" - ); + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, PsiTreeUtil.getParentOfType(annotation, PsiMethod.class))) + .containsExactlyInAnyOrder( + "the price of the product should be $price", + "the product should cost $price", + "the cost of the product should be $price", + "the product should be sold for $price" + ); } @Test @@ -285,7 +288,7 @@ void stepDefMethod(int price) { var annotation = stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); assertThat(annotation).isInstanceOf(PsiAnnotation.class); - assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation)).containsExactlyInAnyOrder( + assertThat(JBehaveUtil.getAnnotationTexts((PsiAnnotation) annotation, null)).containsExactlyInAnyOrder( "the product should worth $price", "the price of the product should be $price", "the product should cost $price", From 0b597371c475b7a5c4d10ec3e956ed60bddace85 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Thu, 4 Jul 2024 17:05:38 +0200 Subject: [PATCH 5/9] Add some collection related fine-tunings in PatternVariantBuilder --- .../jbehave/core/steps/PatternVariantBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java b/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java index ec43360..19edc77 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/jbehave/core/steps/PatternVariantBuilder.java @@ -153,7 +153,7 @@ private Set variantsFor(String input) { } // Store current invocation's results - Set variants = new HashSet<>(); + Set variants = new HashSet<>(8); if (!patternParts.isEmpty()) { // isolate the part before the first pattern @@ -189,6 +189,6 @@ private Set variantsFor(String input) { * @return a {@link Set} of all variants without whitespace compression */ public Set allVariants() { - return new HashSet<>(variants); + return variants.size() == 1 ? Collections.singleton(variants.iterator().next()) : new HashSet<>(variants); } } From 50a24e35faf4a5d5a5997bc60d565fef0576f1ed Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Thu, 4 Jul 2024 17:54:45 +0200 Subject: [PATCH 6/9] Deduplicate annotation type retrieval, and StepPatternParser static, in JavaStepDefinition. --- .../service/JBehaveStepsIndex.java | 2 +- .../service/JavaStepDefinition.java | 27 ++- .../service/JavaStepDefinitionTest.java | 160 +++++++++++------- 3 files changed, 112 insertions(+), 77 deletions(-) diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java index e33d1ff..a02b42b 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java @@ -55,7 +55,7 @@ public Collection findStepDefinitions(@NotNull JBehaveStep s String stepText = step.getStepText(); for (var javaStepDefinition : loadStepsFor(module)) { - if (javaStepDefinition.matches(stepText) && javaStepDefinition.supportsStep(step)) { + if (javaStepDefinition.supportsStepAndMatches(step, stepText)) { Integer currentHighestPriority = getPriorityByDefinition(definitionsByClass.get(javaStepDefinition.getClass())); Integer newPriority = getPriorityByDefinition(javaStepDefinition); diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java index 9f8822a..bca1387 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinition.java @@ -23,22 +23,27 @@ * Represents a Java step definition as a step annotation. */ public final class JavaStepDefinition { + private static final StepPatternParser STEP_PATTERN_PARSER = new RegexPrefixCapturingPatternParser(); private final SmartPsiElementPointer annotationPointer; - private final StepPatternParser stepPatternParser = new RegexPrefixCapturingPatternParser(); public JavaStepDefinition(PsiAnnotation annotation) { annotationPointer = SmartPointerManager.getInstance(annotation.getProject()).createSmartPsiElementPointer(annotation); } /** - * Returns if any of the step annotation patterns match the provided Story step text. + * Returns if the step type of the provided Story step is the same as the step + * type of the current annotation, and if any of the step annotation patterns match + * the provided Story step text. * + * @param step a step from a Story file * @param stepText the text of a step in a Story file */ - public boolean matches(String stepText) { - final StepType annotationType = getAnnotationType(); + public boolean supportsStepAndMatches(@NotNull JBehaveStep step, String stepText) { + StepType annotationType = getAnnotationType(); + if (!Objects.equals(step.getStepType(), annotationType)) return false; + for (String annotationText : getAnnotationTexts()) { - if (new OptimizedStepMatcher(stepPatternParser.parseStep(annotationType, annotationText)).matches(stepText)) + if (new OptimizedStepMatcher(STEP_PATTERN_PARSER.parseStep(annotationType, annotationText)).matches(stepText)) return true; } @@ -63,7 +68,7 @@ public String getAnnotationTextFor(String stepText) { final StepType annotationType = getAnnotationType(); for (String annotationText : annotationTexts) { - OptimizedStepMatcher stepMatcher = new OptimizedStepMatcher(stepPatternParser.parseStep(annotationType, annotationText)); + OptimizedStepMatcher stepMatcher = new OptimizedStepMatcher(STEP_PATTERN_PARSER.parseStep(annotationType, annotationText)); if (stepMatcher.matches(stepText)) { return stepMatcher.pattern().annotated(); } @@ -114,16 +119,6 @@ public Integer getAnnotationPriority() { return annotation != null ? JBehaveUtil.getAnnotationPriority(annotation) : -1; } - /** - * Returns if the step type of the provided Story step is the same as the step - * type of the current annotation. - * - * @param step a step from a Story file - */ - public boolean supportsStep(@NotNull JBehaveStep step) { - return Objects.equals(step.getStepType(), getAnnotationType()); - } - //Equals and hashcode public boolean equals(Object o) { diff --git a/src/test/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinitionTest.java b/src/test/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinitionTest.java index 847dbc1..8d35b01 100644 --- a/src/test/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinitionTest.java +++ b/src/test/java/com/github/kumaraman21/intellijbehave/service/JavaStepDefinitionTest.java @@ -23,32 +23,113 @@ protected String getTestDataPath() { return ""; } - //matches + //supportsStepAndMatches @Test void shouldMatchStepText() { var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ - import org.jbehave.core.annotations.Aliases; - + import org.jbehave.core.annotations.Then; + class JavaStepDefinition { - @Aliases(values = { - "the price should be $price", - "the cost should be $price" - }) + @Then(value = "the price should be $price", priority = 500) void steDefMethod(int price) { } } """); var annotation = (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); - assertThat(new JavaStepDefinition(annotation).matches("the cost should be 200")).isTrue(); + + var storyFile = getFixture().configureByText("matches_step_text.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) storyFile.findElementAt(getFixture().getCaretOffset()).getParent(); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price should be 200")).isTrue(); } @Test void shouldNotMatchStepText() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then(value = "the price should be $price", priority = 500) + void steDefMethod(int price) { + } + } + """); + + var annotation = (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); + + var storyFile = getFixture().configureByText("matches_step_text.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) storyFile.findElementAt(getFixture().getCaretOffset()).getParent(); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price is 200")).isFalse(); + } + + @Test + void shouldSupportStep() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Then; + + class JavaStepDefinition { + @Then(value = "the price should be $price", priority = 500) + void steDefMethod(int price) { + } + } + """); + + var annotation = (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); + + var storyFile = getFixture().configureByText("supports_step.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) storyFile.findElementAt(getFixture().getCaretOffset()).getParent(); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price should be 200")).isTrue(); + } + + @Test + void shouldNotSupportStep() { + var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ + import org.jbehave.core.annotations.Given; + + class JavaStepDefinition { + @Given(value = "the price should be $price", priority = 500) + void steDefMethod(int price) { + } + } + """); + + var annotation = (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); + + var storyFile = getFixture().configureByText("supports_step.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) storyFile.findElementAt(getFixture().getCaretOffset()).getParent(); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price should be 200")).isFalse(); + } + + @Test + void shouldNotSupportStepForAliasAnnotation() { var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ import org.jbehave.core.annotations.Aliases; - + class JavaStepDefinition { @Aliases(values = { "the price should be $price", @@ -60,7 +141,16 @@ void steDefMethod(int price) { """); var annotation = (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); - assertThat(new JavaStepDefinition(annotation).matches("the price is 200")).isFalse(); + + var storyFile = getFixture().configureByText("supports_step.story", """ + Scenario: Product price + + Then the price should be 200 + """); + + var step = (JBehaveStep) storyFile.findElementAt(getFixture().getCaretOffset()).getParent(); + + assertThat(new JavaStepDefinition(annotation).supportsStepAndMatches(step, "the price should be 200")).isFalse(); } //getAnnotationTextFor @@ -212,54 +302,4 @@ void steDefMethod(int price) { // @Test // void shouldReturnFallbackValueForNoAnnotation() { // } - - //supportsStep - - @Test - void shouldSupportStep() { - var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ - import org.jbehave.core.annotations.Then; - - class JavaStepDefinition { - @Then(value = "the price should be $price", priority = 500) - void steDefMethod(int price) { - } - } - """); - - var annotation = (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); - - var storyFile = getFixture().configureByText("supports_step.story", """ - Scenario: Product price - - Then the price should be 200 - """); - - var step = (JBehaveStep) storyFile.findElementAt(getFixture().getCaretOffset()).getParent(); - assertThat(new JavaStepDefinition(annotation).supportsStep(step)).isTrue(); - } - - @Test - void shouldNotSupportStep() { - var stepDefFile = getFixture().configureByText("JavaStepDefinition.java", """ - import org.jbehave.core.annotations.Given; - - class JavaStepDefinition { - @Given(value = "the price should be $price", priority = 500) - void steDefMethod(int price) { - } - } - """); - - var annotation = (PsiAnnotation) stepDefFile.findElementAt(getFixture().getCaretOffset()).getParent().getParent(); - - var storyFile = getFixture().configureByText("supports_step.story", """ - Scenario: Product price - - Then the price should be 200 - """); - - var step = (JBehaveStep) storyFile.findElementAt(getFixture().getCaretOffset()).getParent(); - assertThat(new JavaStepDefinition(annotation).supportsStep(step)).isFalse(); - } } From 9c312075e1dba9f4d8a760e4b007cc3f35e304f9 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Thu, 4 Jul 2024 18:32:58 +0200 Subject: [PATCH 7/9] Lower the number of search scope creations in JBehaveJavaStepDefinitionSearch --- .../JBehaveJavaStepDefinitionSearch.java | 2 +- .../intellijbehave/service/JBehaveUtil.java | 21 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearch.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearch.java index f4fb283..ead2a44 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearch.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveJavaStepDefinitionSearch.java @@ -35,7 +35,7 @@ public boolean execute(@NotNull SearchParameters queryParameters, @NotNull Proce //Lazy-initializing the search scope in case the first step text is null if (searchScope == null) { - searchScope = ReadAction.compute(queryParameters::getEffectiveSearchScope); + searchScope = JBehaveUtil.restrictScopeToJBehaveFiles(ReadAction.compute(queryParameters::getEffectiveSearchScope)); } result &= findJBehaveReferencesToElement(method, stepText, consumer, searchScope); diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java index 6175e5f..082ba82 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveUtil.java @@ -168,27 +168,26 @@ public static Integer getAnnotationPriority(@NotNull PsiAnnotation stepAnnotatio * @param stepDefinitionElement a step definition method * @param stepText the step pattern value of the step annotation * @param consumer - * @param effectiveSearchScope the search scope to find references in + * @param searchScope the search scope to find references in. Already restricted to JBehave Story files. + * See {@link JBehaveJavaStepDefinitionSearch}. * @return true if the corresponding query execution in {@link JBehaveJavaStepDefinitionSearch} should continue, * false if it should stop */ - public static boolean findJBehaveReferencesToElement(@NotNull PsiElement stepDefinitionElement, @NotNull String stepText, @NotNull Processor consumer, @NotNull final SearchScope effectiveSearchScope) { + public static boolean findJBehaveReferencesToElement(@NotNull PsiElement stepDefinitionElement, + @NotNull String stepText, + @NotNull Processor consumer, + @NotNull final SearchScope searchScope) { String word = getTheBiggestWordToSearchByIndex(stepText); - if (isEmptyOrSpaces(word)) { - return true; - } - - SearchScope searchScope = restrictScopeToJBehaveFiles(effectiveSearchScope); - - return PsiSearchHelper.getInstance(stepDefinitionElement.getProject()) - .processElementsWithWord(new MyReferenceCheckingProcessor(stepDefinitionElement, consumer), searchScope, word, (short) 5, true); + return isEmptyOrSpaces(word) + || PsiSearchHelper.getInstance(stepDefinitionElement.getProject()) + .processElementsWithWord(new MyReferenceCheckingProcessor(stepDefinitionElement, consumer), searchScope, word, (short) 5, true); } /** * Returns a search scope that is based on the {@code originalScopeComputation} but that is restricted to JBehave Story file types. */ - private static SearchScope restrictScopeToJBehaveFiles(final SearchScope originalScope) { + public static SearchScope restrictScopeToJBehaveFiles(final SearchScope originalScope) { return ReadAction.compute(() -> originalScope instanceof GlobalSearchScope globalSearchScope ? GlobalSearchScope.getScopeRestrictedByFileTypes(globalSearchScope, StoryFileType.STORY_FILE_TYPE) From 580ef5829e9399cc01c936c98de2f62a1c088cab Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Thu, 4 Jul 2024 18:33:46 +0200 Subject: [PATCH 8/9] Cache step definition finding results per JBehaveStep in JBehaveStepsIndex --- .../service/JBehaveStepsIndex.java | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java index a02b42b..be2616f 100644 --- a/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java +++ b/src/main/java/com/github/kumaraman21/intellijbehave/service/JBehaveStepsIndex.java @@ -45,27 +45,34 @@ public static JBehaveStepsIndex getInstance(Project project) { @NotNull public Collection findStepDefinitions(@NotNull JBehaveStep step) { - Module module = ModuleUtilCore.findModuleForPsiElement(step); - - if (module == null) { - return emptyList(); - } + return CachedValuesManager.getCachedValue(step, (CachedValueProvider>) () -> { + Module module = ModuleUtilCore.findModuleForPsiElement(step); + + if (module == null) { + return new CachedValueProvider.Result<>( + emptyList(), + JBehaveStepDefClassesModificationTracker.getInstance(step.getProject()), + ProjectRootModificationTracker.getInstance(step.getProject())); + } - var definitionsByClass = new HashMap(2); - String stepText = step.getStepText(); + var definitionsByClass = new HashMap(2); + String stepText = step.getStepText(); - for (var javaStepDefinition : loadStepsFor(module)) { - if (javaStepDefinition.supportsStepAndMatches(step, stepText)) { - Integer currentHighestPriority = getPriorityByDefinition(definitionsByClass.get(javaStepDefinition.getClass())); - Integer newPriority = getPriorityByDefinition(javaStepDefinition); + for (var javaStepDefinition : loadStepsFor(module)) { + if (javaStepDefinition.supportsStepAndMatches(step, stepText)) { + Integer currentHighestPriority = getPriorityByDefinition(definitionsByClass.get(javaStepDefinition.getClass())); + Integer newPriority = getPriorityByDefinition(javaStepDefinition); - if (newPriority > currentHighestPriority) { - definitionsByClass.put(javaStepDefinition.getClass(), javaStepDefinition); + if (newPriority > currentHighestPriority) { + definitionsByClass.put(javaStepDefinition.getClass(), javaStepDefinition); + } } } - } - return definitionsByClass.values(); + return new CachedValueProvider.Result<>(definitionsByClass.values(), + JBehaveStepDefClassesModificationTracker.getInstance(step.getProject()), + ProjectRootModificationTracker.getInstance(step.getProject())); + }); } @NotNull From c645dff0d339f5eae89a64afd90e493507eec9e9 Mon Sep 17 00:00:00 2001 From: Tamas Balog Date: Thu, 4 Jul 2024 19:44:26 +0200 Subject: [PATCH 9/9] Add licence NOTICE --- NOTICE.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 NOTICE.txt diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..16ac35a --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,7 @@ +This product includes software developed in the JBehave project. (https://github.com/jbehave/jbehave-core). + +The classes 'com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilder' and +'com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilderTest' are the simplified +and modified versions of 'org.jbehave.core.steps.PatternVariantBuilder' and 'org.jbehave.core.steps.PatternVariantBuilderBehaviour'. + +Those classes are licensed under the BSD-3-Clause license.