Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## [Unreleased]

## [1.69.2]
### Changed
- Improved some of the step definition and annotation handling logic by reducing the number of `Stream` creations.
- Improved the performance of finding unused step declarations by reducing the number of necessary project content iterations.
- Improved and deduplicated some logic regarding the step annotations processing for step completions in Story files.

### Fixed
- [#103](https://github.com/witspirit/IntelliJBehave/issues/103): Improved the performance of the listener that processes changes in step definition methods.

## [1.69.1]
### Changed
- JUnit Jupiter test dependencies brought in by jbehave-core are now excluded from the built plugin archive.
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.69.1
pluginVersion = 1.69.2

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 251
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ opentest4j = "1.3.0"
# plugins
kotlin = "2.1.20"
changelog = "2.2.1"
intelliJPlatform = "2.6.0"
intelliJPlatform = "2.11.0"

[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import com.github.kumaraman21.intellijbehave.service.JavaStepDefinition;
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentIterator;
import com.intellij.openapi.roots.ProjectRootManager;
Expand Down Expand Up @@ -51,18 +50,18 @@ public String getShortName() {
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
var project = holder.getProject();
var stepUsageFinder = new StepUsageFinder(project);
ProjectRootManager.getInstance(project).getFileIndex().iterateContent(stepUsageFinder);
var stepUsages = stepUsageFinder.getStepUsages();

return new JavaElementVisitor() {
@Override
public void visitMethod(final @NotNull PsiMethod method) {
if (method.getNameIdentifier() == null || !ReadAction.compute(() -> isStepDefinition(method))) {
if (method.getNameIdentifier() == null || !isStepDefinition(method)) {
return;
}

Project project = method.getProject();
StepUsageFinder stepUsageFinder = new StepUsageFinder(project);
ProjectRootManager.getInstance(project).getFileIndex().iterateContent(stepUsageFinder);
Set<JBehaveStep> stepUsages = stepUsageFinder.getStepUsages();

for (JBehaveStep step : stepUsages) {
PsiReference[] references = step.getReferences();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import org.jetbrains.annotations.NotNull;

/**
* Limitations: see {@link com.github.kumaraman21.intellijbehave.resolver.StepDefinitionAnnotationConverter#convertFrom(PsiAnnotation[])}
*
* @author <a href="http://twitter.com/aloyer">@aloyer</a>
*/
public class StoryCompletionContributor extends CompletionContributor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.idea.util.findAnnotation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.isPublic
import kotlin.jvm.internal.Ref.BooleanRef

/**
Expand Down Expand Up @@ -64,7 +65,10 @@ class KotlinPsiClassesHandler private constructor() {
*/
private fun isKotlinJBehaveStepDefClass(aClass: KtClass): Boolean {
return try {
!aClass.isEnum() && !aClass.isInterface() && aClass.fqName != null && aClass.body?.functions?.any {
!aClass.isEnum()
&& !aClass.isInterface()
&& aClass.fqName != null
&& aClass.body?.functions?.asSequence()?.filter { it.isPublic }?.any {
return@any try {
it.findAnnotation(GIVEN) != null
|| it.findAnnotation(WHEN) != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,67 +22,94 @@
import static org.apache.commons.lang3.StringUtils.removeStart;

import com.github.kumaraman21.intellijbehave.jbehave.core.steps.PatternVariantBuilder;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiNameValuePair;
import org.jbehave.core.annotations.Alias;
import org.jbehave.core.annotations.Aliases;
import org.jbehave.core.steps.StepType;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public final class StepDefinitionAnnotationConverter {

/**
* Converts each of the input annotations to a {@link StepDefinitionAnnotation} by extracting
* information (the step type and annotation text) from them.
*
* <h3>Limitations</h3>
* Currently, there are a couple of cases that are either not handled or handled incorrectly:
* <ul>
* <li>When converting an @Alias or @Aliases annotation, the order of the input annotations matters.
* Having the step annotation first, i.e. {@code @When -> @Alias}, the {@code @Alias} annotation
* is created with the proper WHEN step type. If the order is switched ({@code @Alias -> @When}),
* the step type becomes null, and that step candidate is not code completed by
* {@link com.github.kumaraman21.intellijbehave.completion.StoryCompletionContributor}.</li>
* <li>The case when multiple of the {@code @Given/@When/@Then/@Alias/@Aliases} annotations
* are added to the same step definition method is not handled.</li>
* </ul>
*
* @param annotations the annotations on a given step definition method
*/
public static Set<StepDefinitionAnnotation> convertFrom(PsiAnnotation[] annotations) {
Set<StepDefinitionAnnotation> res = null;
//Refs and AnnotationsHolders are not created in this case
if (annotations.length == 0) return Collections.emptySet();

//Variable init

final var result = new AnnotationsHolder();
StepType stepType = null;
var annotationQualifiedName = new Ref<String>();

//Annotation processing

for (PsiAnnotation annotation : annotations) {
String annotationQualifiedName = compute(annotation::getQualifiedName);
// Given, When, Then
final PsiNameValuePair[] attributes = compute(() -> annotation.getParameterList().getAttributes());

// When there are no attributes for the annotation, we got nothing to do here
if (attributes.length > 0) {
if (ANNOTATION_TO_STEP_TYPE_MAPPING.containsKey(annotationQualifiedName)) {
stepType = ANNOTATION_TO_STEP_TYPE_MAPPING.get(annotationQualifiedName);
String annotationText = getTextFromValue(compute(() -> attributes[0].getValue()));
if (res == null) {
res = new HashSet<>();
}
res.addAll(getPatternVariants(stepType, annotationText, annotation));
} else if (annotationQualifiedName != null) {
if (annotationQualifiedName.equals(Alias.class.getName())) {
String annotationText = getTextFromValue(compute(() -> attributes[0].getValue()));
if (res == null) {
res = new HashSet<>();
}
res.addAll(getPatternVariants(stepType, annotationText, annotation));
} else if (annotationQualifiedName.equals(Aliases.class.getName())) {
PsiAnnotationMemberValue attributeValue = compute(() -> attributes[0].getValue());
if (attributeValue != null) {
PsiElement[] values = attributeValue.getChildren();
for (PsiElement value : values) {
if (value instanceof PsiLiteral) {
String annotationText = getTextFromValue(value);
if (res == null) {
res = new HashSet<>();
}
res.addAll(getPatternVariants(stepType, annotationText, annotation));
}
var attributes = compute(() -> {
annotationQualifiedName.set(annotation.getQualifiedName());
return annotation.getParameterList().getAttributes();
});

// When there are no attributes for the annotation, don't process this annotation
if (attributes.length == 0) continue;

//When the processed annotation is @Given, @When or @Then
if (ANNOTATION_TO_STEP_TYPE_MAPPING.containsKey(annotationQualifiedName.get())) {
stepType = ANNOTATION_TO_STEP_TYPE_MAPPING.get(annotationQualifiedName.get());
String annotationText = getTextFromValue(() -> attributes[0].getValue());
result.add(stepType, annotationText, annotation);
} else if (!annotationQualifiedName.isNull()) {

//When the processed annotation is @Alias
if (annotationQualifiedName.get().equals(Alias.class.getName())) {
String annotationText = getTextFromValue(() -> attributes[0].getValue());
result.add(stepType, annotationText, annotation);
}

//When the processed annotation is @Aliases
else if (annotationQualifiedName.get().equals(Aliases.class.getName())) {
PsiAnnotationMemberValue attributeValue = compute(() -> attributes[0].getValue());
if (attributeValue != null) {
PsiElement[] values = attributeValue.getChildren();
//Processes all specified alias values in the annotation attribute
for (PsiElement value : values) {
if (value instanceof PsiLiteral) {
String annotationText = getTextFromValue(() -> value);
result.add(stepType, annotationText, annotation);
}
}
}
}
}
}
return res == null ? Collections.emptySet() : res;

return result.getAnnotations();
}

private static Set<StepDefinitionAnnotation> getPatternVariants(final StepType stepType, String annotationText, final PsiAnnotation annotation) {
Expand All @@ -93,7 +120,24 @@ private static Set<StepDefinitionAnnotation> getPatternVariants(final StepType s
.collect(Collectors.toSet());
}

private static String getTextFromValue(PsiElement value) {
return remove(removeStart(removeEnd(compute(value::getText), "\""), "\""), "\\");
/**
* By using a Supplier here, {@code compute()} calls can be deduplicated with call sites of this method.
*/
private static String getTextFromValue(Supplier<PsiElement> value) {
return remove(removeStart(removeEnd(compute(() -> value.get().getText()), "\""), "\""), "\\");
}

private static final class AnnotationsHolder {
@Nullable
private Set<StepDefinitionAnnotation> annotations;

private Set<StepDefinitionAnnotation> getAnnotations() {
return annotations == null ? Collections.emptySet() : annotations;
}

private void add(StepType stepType, String annotationText, PsiAnnotation annotation) {
if (annotations == null) annotations = new HashSet<>();
annotations.addAll(getPatternVariants(stepType, annotationText, annotation));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ public StepType getStepType() {

@Override
public boolean processFile(@NotNull VirtualFile virtualFile) {
PsiFile psiFile = compute(() -> PsiManager.getInstance(project).findFile(virtualFile));

if (psiFile instanceof PsiClassOwner psiClassOwner) {
for (PsiClass psiClass : getPsiClasses(psiFile, psiClassOwner)) {
for (PsiMethod method : compute(psiClass::getMethods)) {
PsiAnnotation[] annotations = compute(() -> method.getModifierList().getApplicableAnnotations());

for (StepDefinitionAnnotation stepDefinitionAnnotation : StepDefinitionAnnotationConverter.convertFrom(annotations)) {
if ((stepType == null || Objects.equals(stepType, stepDefinitionAnnotation.stepType()))
&& !processStepDefinition(stepDefinitionAnnotation)) {
return false;
}
if (virtualFile.isDirectory()) return true;

var psiClasses = compute(() -> getPsiClasses(PsiManager.getInstance(project).findFile(virtualFile)));

for (PsiClass psiClass : psiClasses) {
for (PsiMethod method : compute(psiClass::getMethods)) {
PsiAnnotation[] annotations = compute(() -> method.getModifierList().getApplicableAnnotations());

for (StepDefinitionAnnotation stepDefinitionAnnotation : StepDefinitionAnnotationConverter.convertFrom(annotations)) {
if ((stepType == null || Objects.equals(stepType, stepDefinitionAnnotation.stepType()))
&& !processStepDefinition(stepDefinitionAnnotation)) {
return false;
}
}
}
Expand All @@ -73,19 +73,20 @@ public boolean processFile(@NotNull VirtualFile virtualFile) {
}

/**
* Returns the PSI classes from the provided file and class owner.
* Returns the PSI classes from the provided file.
*
* @param psiFile the Java or Kotlin step definitions file
* @param psiClassOwner {@code psiFile} as a {@link PsiClassOwner}
* @param psiFile the Java or Kotlin step definitions file
* @return the classes contained by the file
*/
private static PsiClass[] getPsiClasses(PsiFile psiFile, PsiClassOwner psiClassOwner) {
private static PsiClass[] getPsiClasses(PsiFile psiFile) {
if (!(psiFile instanceof PsiClassOwner psiClassOwner)) return PsiClass.EMPTY_ARRAY;

PsiClass[] psiClasses = null;
if (KotlinConfigKt.getPluginIsEnabled()) {
psiClasses = compute(() -> KotlinPsiClassesHandler.getPsiClasses(psiFile));
psiClasses = KotlinPsiClassesHandler.getPsiClasses(psiFile);
}

return psiClasses != null ? psiClasses : compute(psiClassOwner::getClasses);
return psiClasses != null ? psiClasses : psiClassOwner.getClasses();
}

public abstract boolean processStepDefinition(StepDefinitionAnnotation stepDefinitionAnnotation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class JBehaveJavaStepDefinitionSearch implements QueryExecutor<PsiReferen

@Override
public boolean execute(@NotNull SearchParameters queryParameters, @NotNull Processor<? super PsiReference> consumer) {
if (!(queryParameters.getElementToSearch() instanceof PsiMethod method) || !compute(() -> isStepDefinition(method))) {
if (!(queryParameters.getElementToSearch() instanceof PsiMethod method) || !isStepDefinition(method)) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
Expand All @@ -27,6 +28,7 @@
import org.jetbrains.annotations.Nullable;

import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -66,7 +68,13 @@ public static boolean isAnnotationOfClass(@NotNull PsiAnnotation annotation,
*/
@NotNull
private static List<PsiAnnotation> getJBehaveStepAnnotations(@NotNull PsiMethod method) {
return Stream.of(compute(() -> method.getModifierList().getAnnotations()))
var annotations = compute(() -> method.getModifierList().getAnnotations());

//Optimizations to avoid creating unnecessary Streams
if (annotations.length == 0) return Collections.emptyList();
if (annotations.length == 1 && JBehaveUtil.isJBehaveStepAnnotation(annotations[0])) return Collections.singletonList(annotations[0]);

return Stream.of(annotations)
.filter(JBehaveUtil::isJBehaveStepAnnotation)
.collect(Collectors.toList());
}
Expand All @@ -79,9 +87,20 @@ private static List<PsiAnnotation> getJBehaveStepAnnotations(@NotNull PsiMethod
* </ul>
*/
public static boolean isStepDefinition(@NotNull PsiMethod method) {
return getJBehaveStepAnnotations(method).stream()
.map(stepAnnotation -> compute(() -> stepAnnotation.findAttributeValue("value")))
.anyMatch(Objects::nonNull);
return ReadAction.compute(() -> {
var jBehaveStepAnnotations = getJBehaveStepAnnotations(method);

//Optimizations to avoid creating unnecessary Streams
if (jBehaveStepAnnotations.isEmpty()) return false;
if (jBehaveStepAnnotations.size() == 1) {
var attributeValue = compute(() -> jBehaveStepAnnotations.getFirst().findAttributeValue("value"));
return attributeValue != null;
} else {
return jBehaveStepAnnotations.stream()
.map(stepAnnotation -> compute(() -> stepAnnotation.findAttributeValue("value")))
.anyMatch(Objects::nonNull);
}
});
}

/**
Expand Down Expand Up @@ -140,7 +159,12 @@ private static Set<String> getAliasesAnnotationTexts(@NotNull PsiAnnotation alia
*/
@NotNull
public static List<String> getAnnotationTexts(@NotNull PsiMethod method) {
return getJBehaveStepAnnotations(method)
var annotations = getJBehaveStepAnnotations(method);

//Optimization to avoid creating unnecessary Streams
if (annotations.isEmpty()) return Collections.emptyList();

return annotations
.stream()
.map(annotation -> JBehaveUtil.getAnnotationTexts(annotation, method))
.flatMap(Set::stream)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
*/
@Service(Service.Level.PROJECT)
final class StepAnnotationsCache {

//TODO: clean up stale module entries upon project root changes or module root additions/removals
private final Map<Module, StepAnnotations> stepAnnotationClasses = new ConcurrentHashMap<>(4);
private final Project project;

Expand Down
Loading
Loading