Skip to content

Commit

Permalink
Merge pull request #2337 from Haehnchen/feature/property-code
Browse files Browse the repository at this point in the history
rearrange public property completion / inspection code
  • Loading branch information
Haehnchen committed Apr 6, 2024
2 parents 3436a85 + 41fdf6b commit 8106dbd
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 954 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package fr.adrienbrault.idea.symfony2plugin.completion;

import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.completion.PrefixMatcher;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.config.PhpLanguageFeature;
import com.jetbrains.php.config.PhpLanguageLevel;
import com.jetbrains.php.lang.formatter.PhpCodeStyleSettings;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.PhpPsiElementFactory;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.*;
import com.jetbrains.php.lang.psi.elements.impl.PhpPromotedFieldParameterImpl;
import com.jetbrains.php.lang.psi.resolve.types.PhpType;
import com.jetbrains.php.refactoring.PhpNameStyle;
import com.jetbrains.php.refactoring.PhpNameUtil;
import com.jetbrains.php.refactoring.PhpRefactoringUtil;
import com.jetbrains.php.refactoring.changeSignature.PhpChangeSignatureProcessor;
import com.jetbrains.php.refactoring.changeSignature.PhpParameterInfo;
import com.jetbrains.php.refactoring.introduce.introduceField.PhpIntroduceFieldHandler;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.stream.Collectors;

/**
* @author Daniel Espendiller <[email protected]>
*/
public class ServicePropertyInsertUtil {
private static final String[] CLASS_TYPE_NAMES = {"interface", "abstract", "decorator"};

public static List<String> getInjectionService(@NotNull Project project, @NotNull String propertyNameFind) {
return getInjectionService(project, propertyNameFind, null);
}

public static List<String> getInjectionService(@NotNull Project project, @NotNull String propertyNameFindRaw, @Nullable String methodName) {
// @TODO: fill this list based on project usage

final Set<String> propertyNameFind = new HashSet<>();
propertyNameFind.add(normalizeClassTypeKeywords(propertyNameFindRaw));

// LoggerInterface $fooBarLogger
if (propertyNameFindRaw.endsWith("Logger") && !propertyNameFindRaw.equalsIgnoreCase("logger")) {
propertyNameFind.add("logger");
}

Map<String, Match> servicesMatch = new HashMap<>();

HashMap<String, String> alias = new HashMap<>() {{
put("twig", "\\Twig\\Environment");
put("template", "\\Twig\\Environment");
put("router", "\\Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface");
put("em", "Doctrine\\ORM\\EntityManagerInterface");
put("om", "\\Doctrine\\Persistence\\ObjectManager");
}};

for (String property : propertyNameFind) {
if (alias.containsKey(property.toLowerCase())) {
String key = property.toLowerCase();
if (!PhpIndex.getInstance(project).getAnyByFQN(alias.get(key)).isEmpty()) {
String fqn = alias.get(key);
servicesMatch.put(fqn, new Match(fqn, 4));
}
}
}

// try to find partial ending match for normalized properties: fooBarCar => barCar
String classPropertyNameForEndingMatch = fr.adrienbrault.idea.symfony2plugin.util.StringUtils.underscore(StringUtils.strip(propertyNameFindRaw, "_"));
for (String replace : CLASS_TYPE_NAMES) {
classPropertyNameForEndingMatch = StringUtils.removeEndIgnoreCase(classPropertyNameForEndingMatch, replace);
classPropertyNameForEndingMatch = StringUtils.removeStartIgnoreCase(classPropertyNameForEndingMatch, replace);
}

classPropertyNameForEndingMatch = fr.adrienbrault.idea.symfony2plugin.util.StringUtils.camelize(classPropertyNameForEndingMatch, true);

// collect partial match with least 3 parts
Set<String> endingMatches = new HashSet<>();
List<String> nameParts = PhpNameUtil.splitName(classPropertyNameForEndingMatch);
if (nameParts.size() > 2) {
PhpCodeStyleSettings settings = CodeStyle.getCustomSettings(PhpPsiElementFactory.createPsiFileFromText(project, "<?php"), PhpCodeStyleSettings.class);
endingMatches.addAll(PhpNameStyle.DECAPITALIZE.withStyle(settings.VARIABLE_NAMING_STYLE).generateNames(nameParts)
.stream()
.filter(s -> fr.adrienbrault.idea.symfony2plugin.util.StringUtils.underscore(s).split("_").length > 2)
.collect(Collectors.toSet())
);
}

HashSet<String> objects = new HashSet<>();

objects.addAll(PhpIndex.getInstance(project).getAllClassFqns(PrefixMatcher.ALWAYS_TRUE));
objects.addAll(PhpIndex.getInstance(project).getAllInterfacesFqns(PrefixMatcher.ALWAYS_TRUE));

Set<String> collect = objects.stream().filter(s -> {
int i = s.lastIndexOf("\\");
if (i > 0) {
if (s.toLowerCase().contains("\\test\\")) {
return false;
}

s = s.substring(i);
}

return !s.endsWith("Test")
&& !s.toLowerCase().contains("_phpstan_")
&& !s.toLowerCase().contains("ecsprefix")
&& !s.toLowerCase().contains("_humbugbox")
&& !s.toLowerCase().contains("rectorprefix");
}).collect(Collectors.toSet());

for (String fqn : collect) {
// Bar\Foo => Foo
int i = fqn.lastIndexOf("\\");
String classPropertyNameRaw = i > 0
? fqn.substring(i + 1)
: fqn;

String classPropertyName = normalizeClassTypeKeywords(classPropertyNameRaw);
if (StringUtils.isBlank(classPropertyName)) {
continue;
}

int weight;
if (propertyNameFind.stream().anyMatch(classPropertyName::equalsIgnoreCase)) {
// direct property match
weight = 3;
} else if(endingMatches.stream().anyMatch(s -> classPropertyName.toLowerCase().endsWith(s.toLowerCase()))) {
// partial property with ending match
weight = 1;
} else {
continue;
}

Collection<PhpClass> anyByFQN = PhpIndex.getInstance(project).getAnyByFQN(fqn);
if (anyByFQN.isEmpty()) {
continue;
}

if (methodName != null && !hasMethodMatch(methodName, anyByFQN)) {
weight -= 4;
}

if (anyByFQN.stream().anyMatch(PhpClass::isInterface)) {
weight += 2;

// Symfony\Contracts\EventDispatcher\EventDispatcherInterface
// Psr\Log\LoggerInterface
if (fqn.toLowerCase().contains("\\contracts\\") && fqn.toLowerCase().contains("\\symfony\\")) {
weight += 2;
} else if(fqn.toLowerCase().contains("\\psr\\")) {
weight += 3;
}
}

if (anyByFQN.stream().anyMatch(PhpClass::isAbstract)) {
weight += 1;
}

if (classPropertyNameRaw.toLowerCase().contains("decorator")) {
weight -= 3;
}

if (servicesMatch.containsKey(fqn)) {
servicesMatch.get(fqn).modifyWeight(weight);
} else {
servicesMatch.put(fqn, new Match(fqn, weight));
}
}

return servicesMatch.values().stream()
.sorted((o1, o2) -> Integer.compare(o2.weight, o1.weight))
.map(m -> m.fqn)
.collect(Collectors.toList());
}

private static class Match {
private final String fqn;
private int weight = 0;

public Match(@NotNull String fqn, int weight) {
this.fqn = fqn;
this.modifyWeight(weight);
}

public void modifyWeight(int weight) {
this.weight += weight;
}
}

private static boolean hasMethodMatch(@NotNull String methodName, Collection<PhpClass> anyByFQN) {
return anyByFQN.stream()
.anyMatch(phpClass -> phpClass.findMethodByName(methodName) != null);
}

private static String normalizeClassTypeKeywords(@NotNull String classPropertyName) {
classPropertyName = classPropertyName.replaceAll("_", "").toLowerCase();

for (String replace : CLASS_TYPE_NAMES) {
classPropertyName = StringUtils.removeEndIgnoreCase(classPropertyName, replace);
classPropertyName = StringUtils.removeStartIgnoreCase(classPropertyName, replace);
}

return classPropertyName;
}

public static void appendPropertyInjection(@NotNull PhpClass phpClass, @NotNull String propertyName, @NotNull String typePhpClass) {
Method constructor = PhpIntroduceFieldHandler.getOrCreateConstructor(phpClass);
if (constructor == null) {
return;
}

// use + constructor(Foo $foo)
String importedClass = PhpElementsUtil.insertUseIfNecessary(phpClass, typePhpClass);

// "private readonly Foo $foo"
if (shouldUsePropertyPromotion(constructor)) {
String readonlyProperty = !phpClass.isReadonly() ? "readonly " : "";

Parameter parameter = PhpPsiElementFactory.createComplexParameter(phpClass.getProject(), String.format("private %s%s $%s", readonlyProperty, importedClass, propertyName));
Parameter parameterToInsertAfter = PhpChangeSignatureProcessor.findParameterToInsertAfter(constructor);
if (parameterToInsertAfter != null) {
addParameterAfter(constructor, parameter, parameterToInsertAfter);
} else if (constructor.getParameters().length == 0) {
PhpChangeSignatureProcessor.appendParameterToParameterList(constructor, parameter);
}

return;
}

PhpParameterInfo phpParameterInfo = new PhpParameterInfo(0, propertyName);
phpParameterInfo.setType(new PhpType().add(typePhpClass), importedClass);

// find added parameter; should mostly the last
PhpChangeSignatureProcessor.addParameterToFunctionSignature(phpClass.getProject(), constructor, List.of(phpParameterInfo));

Parameter parameter = Arrays.stream(constructor.getParameters())
.filter(parameter1 -> propertyName.equalsIgnoreCase(parameter1.getName()))
.findFirst()
.orElse(null);

// add $this->foo
if (parameter != null) {
PhpRefactoringUtil.initializeFieldsByParameters(phpClass, List.of(parameter), PhpModifier.Access.PRIVATE);
}
}

private static void addParameterAfter(@NotNull Function function, @NotNull Parameter parameter, @NotNull Parameter parameterToInsertAfter) {
PsiElement parameterList = PhpPsiUtil.getChildOfType(function, PhpElementTypes.PARAMETER_LIST);
assert parameterList != null;
parameterList.addAfter(parameter, parameterList.addAfter(PhpPsiElementFactory.createComma(parameterList.getProject()), parameterToInsertAfter));
}

public static boolean shouldUsePropertyPromotion(@NotNull Function function) {
Parameter[] parameters = function.getParameters();
if (parameters.length == 0) {
return PhpLanguageLevel.current(function.getProject()).hasFeature(PhpLanguageFeature.PROPERTY_PROMOTION);
}

for (Parameter parameter : parameters) {
if (parameter instanceof PhpPromotedFieldParameterImpl) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.findUsages.PhpGotoTargetRendererProvider;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.completion.IncompletePropertyServiceInjectionContributor;
import fr.adrienbrault.idea.symfony2plugin.completion.ServicePropertyInsertUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil;
import icons.SymfonyIcons;
Expand Down Expand Up @@ -104,7 +104,7 @@ public void applyFix(@NotNull Project project, PsiFile file, @Nullable Editor ed
}
}

List<String> injectionService = IncompletePropertyServiceInjectionContributor.getInjectionService(project, name, methodName)
List<String> injectionService = ServicePropertyInsertUtil.getInjectionService(project, name, methodName)
.stream()
.map(s -> StringUtils.stripStart(s, "\\"))
.toList();
Expand Down Expand Up @@ -153,7 +153,7 @@ private static void buildProperty(@NotNull Project project, @NotNull FieldRefere

WriteCommandAction.writeCommandAction(project)
.withName("Symfony: Add Property Service")
.run((ThrowableRunnable<Throwable>) () -> IncompletePropertyServiceInjectionContributor.appendPropertyInjection(phpClassScope, fieldReference.getName(), classFqn));
.run((ThrowableRunnable<Throwable>) () -> ServicePropertyInsertUtil.appendPropertyInjection(phpClassScope, fieldReference.getName(), classFqn));
});
} catch (Throwable ignored) {
}
Expand Down
5 changes: 0 additions & 5 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,6 @@

<completion.contributor language="PHP" order="last" implementationClass="fr.adrienbrault.idea.symfony2plugin.completion.PhpIncompleteCompletionContributor"/>


<!---
<completion.contributor language="PHP" order="last" implementationClass="fr.adrienbrault.idea.symfony2plugin.completion.IncompletePropertyServiceInjectionContributor"/>
-->

<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.RoutesStubIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigExtendsStubIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ServicesDefinitionStubIndex"/>
Expand Down
Loading

0 comments on commit 8106dbd

Please sign in to comment.