Skip to content

Commit 4dad597

Browse files
authored
Merge pull request #10273 from neo-technology/instance-convert-with
Allow for ConvertWith to use instance methods
2 parents ca54780 + 5cdde78 commit 4dad597

File tree

6 files changed

+158
-91
lines changed

6 files changed

+158
-91
lines changed

annotations/src/main/java/org/neo4j/gds/annotation/Configuration.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,28 @@
6262

6363
/**
6464
* This annotation can be used together with {@link org.neo4j.gds.annotation.Configuration.Key} or {@link org.neo4j.gds.annotation.Configuration.Parameter}.
65-
* The value must be a method reference of format `package.class#function` to a static and public method.
65+
* The value must be a method reference of format `package.class#function` to a static and public method
66+
* or a method name, referring to a public static or instance method in the surrounding interface or its supertypes.
6667
* The input for the specific field will be transformed using the method-reference.
6768
*/
6869
@Documented
6970
@Target(ElementType.METHOD)
7071
@Retention(RetentionPolicy.RUNTIME)
7172
@interface ConvertWith {
73+
/**
74+
* The method to be used for converting the configuration value.
75+
* <p>
76+
* This can have one of two forms:
77+
* <ul>
78+
* <li>A method reference of format `package.class#function` to a static and public method.</li>
79+
* <li>An unqualified method name, referring to a public static or instance method in the surrounding interface or its supertypes.</li>
80+
* </ul>
81+
* <p>
82+
* In the second case, the method can be an instance method, which allows for the conversion to make use of
83+
* other configuration values.
84+
* <b>FOOT-GUN WARNING:</b> Using other configuration values only works correctly if they are already initialized.
85+
* That is, they must be written above this configuration option, in source code order.
86+
*/
7287
String method();
7388

7489
String INVERSE_IS_TO_MAP = "__USE_TO_MAP_METHOD__";

config-generator/src/main/java/org/neo4j/gds/proc/GenerateConfiguration.java

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import com.squareup.javapoet.NameAllocator;
3030
import com.squareup.javapoet.TypeName;
3131
import com.squareup.javapoet.TypeSpec;
32+
import com.squareup.javapoet.TypeVariableName;
33+
import org.intellij.lang.annotations.PrintFormat;
3234
import org.jetbrains.annotations.NotNull;
3335
import org.jetbrains.annotations.Nullable;
3436
import org.neo4j.gds.annotation.Configuration;
@@ -108,7 +110,7 @@ private TypeSpec process(Spec config, String packageName, String generatedClassN
108110
builder.addFields(fieldDefinitions.fields());
109111

110112
String configParameterName = fieldDefinitions.names().newName(CONFIG_VAR, CONFIG_VAR);
111-
List<MemberDefinition> implMembers = config.members().stream()
113+
List<MemberDefinition> implMembers = config.members().stream()
112114
.flatMap(parserMember -> memberDefinition(fieldDefinitions.names(), parserMember).stream())
113115
.collect(Collectors.toList());
114116

@@ -351,7 +353,7 @@ private void addValidationCode(MemberDefinition definition, CodeBlock.Builder fi
351353
definition.fieldName(),
352354
validatorCode
353355
);
354-
} else if (isTypeOf(List.class, definition.fieldType())){
356+
} else if (isTypeOf(List.class, definition.fieldType())) {
355357
validationConsumer = validatorCode -> fieldCodeBuilder.addStatement(
356358
"this.$1N.forEach($1N -> $2L)",
357359
definition.fieldName(),
@@ -480,7 +482,7 @@ private Optional<MemberDefinition> memberDefinition(NameAllocator names, Spec.Me
480482
targetType,
481483
asType(method.getEnclosingElement()),
482484
converter,
483-
true
485+
false
484486
);
485487
}
486488

@@ -490,8 +492,8 @@ private Optional<MemberDefinition> memberDefinition(NameAllocator names, Spec.Me
490492
return converterError(
491493
method,
492494
"[%s] is not a valid fully qualified method name: " +
493-
"it must start with a fully qualified class name followed by a '#' " +
494-
"and then the method name.",
495+
"it must start with a fully qualified class name followed by a '#' " +
496+
"and then the method name.",
495497
converter
496498
);
497499
}
@@ -502,13 +504,13 @@ private Optional<MemberDefinition> memberDefinition(NameAllocator names, Spec.Me
502504
return converterError(
503505
method,
504506
"[%s] is not a valid fully qualified method name: " +
505-
"The class [%s] cannot be found.",
507+
"The class [%s] cannot be found.",
506508
converter,
507509
className
508510
);
509511
}
510512

511-
return memberDefinition(names, member, targetType, classElement, methodName, false);
513+
return memberDefinition(names, member, targetType, classElement, methodName, true);
512514
}
513515

514516
private Optional<MemberDefinition> memberDefinition(
@@ -517,7 +519,7 @@ private Optional<MemberDefinition> memberDefinition(
517519
TypeMirror targetType,
518520
TypeElement classElement,
519521
CharSequence methodName,
520-
boolean scanInheritance
522+
boolean isFullyQualified
521523
) {
522524
String converter = member.method().getAnnotation(ConvertWith.class).method();
523525
List<ExecutableElement> validCandidates = new ArrayList<>();
@@ -531,44 +533,53 @@ private Optional<MemberDefinition> memberDefinition(
531533
return converterError(classElement, "Inherited interface was null, this could be a bug in the JDK.");
532534
}
533535

534-
validCandidates.clear();
535536

536537
for (ExecutableElement candidate : methodsIn(currentClass.getEnclosedElements())) {
537538
if (!candidate.getSimpleName().contentEquals(methodName)) {
538539
continue;
539540
}
540541

541-
int sizeBeforeValidation = invalidCandidates.size();
542-
543-
validateCandidateModifiers(targetType, invalidCandidates, candidate, candidate.getModifiers());
544-
545-
if (invalidCandidates.size() == sizeBeforeValidation) {
542+
if (validateCandidateModifiers(
543+
targetType,
544+
invalidCandidates,
545+
isFullyQualified,
546+
candidate,
547+
candidate.getModifiers()
548+
)) {
546549
validCandidates.add(candidate);
547550
}
548551
}
549552

550553
if (validCandidates.size() > 1) {
551554
for (ExecutableElement candidate : validCandidates) {
552555
error(
553-
String.format(Locale.ENGLISH,"Method is ambiguous and a possible candidate for [%s].", converter),
556+
String.format(
557+
Locale.ENGLISH,
558+
"Method is ambiguous and a possible candidate for [%s].",
559+
converter
560+
),
554561
candidate
555562
);
556563
}
557564
return converterError(member.method(), "Multiple possible candidates found: %s.", validCandidates);
558565
}
559566

560567
if (validCandidates.size() == 1) {
561-
ExecutableElement candidate = validCandidates.get(0);
562-
TypeMirror currentType = currentClass.asType();
563-
TypeMirror converterInputType = candidate.getParameters().get(0).asType();
568+
ExecutableElement candidate = validCandidates.getFirst();
569+
var receiverType = candidate.getModifiers().contains(Modifier.STATIC)
570+
? TypeName.get(currentClass.asType())
571+
: TypeVariableName.get("this"); // this isn't really the proper thing, but it works
572+
TypeMirror converterInputType = candidate.getParameters().getFirst().asType();
573+
574+
564575
if (isTypeOf(Optional.class, targetType)) {
565576
return memberDefinition(names, member, targetType, Optional.of(converterInputType))
566577
.map(definition -> ImmutableMemberDefinition.builder()
567578
.from(definition)
568579
.addConverter(codeBlock -> CodeBlock.of(
569580
"$L.map($T::$N)",
570581
codeBlock,
571-
currentType,
582+
receiverType,
572583
candidate.getSimpleName().toString()
573584
))
574585
.build()
@@ -579,42 +590,48 @@ private Optional<MemberDefinition> memberDefinition(
579590
.from(d)
580591
.addConverter(c -> CodeBlock.of(
581592
"$T.$N($L)",
582-
currentType,
593+
receiverType,
583594
candidate.getSimpleName().toString(),
584595
c
585596
))
586597
.build()
587598
);
588599
}
589600

590-
if (scanInheritance) {
601+
if (!isFullyQualified) {
591602
for (TypeMirror superInterface : currentClass.getInterfaces()) {
592603
classesToSearch.addLast(asTypeElement(superInterface));
593604
}
594605
}
595606
} while (!classesToSearch.isEmpty());
596607

597608
for (InvalidCandidate invalidCandidate : invalidCandidates) {
598-
error(String.format(Locale.ENGLISH,invalidCandidate.message(), invalidCandidate.args()), invalidCandidate.element());
609+
error(
610+
String.format(Locale.ENGLISH, invalidCandidate.message(), invalidCandidate.args()),
611+
invalidCandidate.element()
612+
);
599613
}
600614

601615
return converterError(
602616
member.method(),
603-
"No suitable method found that matches [%s]. " +
604-
"Make sure that the method is static, public, unary, not generic, " +
605-
"does not declare any exception and returns [%s].",
617+
"No suitable method found that matches [%1$s]. " +
618+
"Make sure that the method is %3$spublic, unary, not generic, " +
619+
"does not declare any exception and returns [%2$s].",
606620
converter,
607-
targetType
621+
targetType,
622+
isFullyQualified ? "static, " : ""
608623
);
609624
}
610625

611-
private void validateCandidateModifiers(
626+
private boolean validateCandidateModifiers(
612627
TypeMirror targetType,
613628
Collection<InvalidCandidate> invalidCandidates,
629+
boolean requireStatic,
614630
ExecutableElement candidate,
615631
Set<Modifier> modifiers
616632
) {
617-
if (!modifiers.contains(Modifier.STATIC)) {
633+
int numberOfInvalids = invalidCandidates.size();
634+
if (requireStatic && !modifiers.contains(Modifier.STATIC)) {
618635
invalidCandidates.add(InvalidCandidate.of(candidate, "Must be static"));
619636
}
620637
if (!modifiers.contains(Modifier.PUBLIC)) {
@@ -640,6 +657,8 @@ private void validateCandidateModifiers(
640657
targetType
641658
));
642659
}
660+
661+
return numberOfInvalids == invalidCandidates.size();
643662
}
644663

645664
private Optional<MemberDefinition> memberDefinition(
@@ -755,7 +774,7 @@ private <T> Optional<T> error(CharSequence message, Element element) {
755774
return Optional.empty();
756775
}
757776

758-
private <T> Optional<T> converterError(Element element, String message, Object... args) {
777+
private <T> Optional<T> converterError(Element element, @PrintFormat String message, Object... args) {
759778
messager.printMessage(
760779
Diagnostic.Kind.ERROR,
761780
String.format(Locale.ENGLISH, message, args),

0 commit comments

Comments
 (0)