Defer trigger event qualification and resolve types from bindings
Stop rewriting trigger events in TriggerPoint construction; qualify with context during property resolution and export. Upgrade heuristic simple names to JDT binding FQNs for user types only, preserving java.* platform types. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
|
|||||||
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
|
||||||
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
|
||||||
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
import click.kamil.springstatemachineexporter.model.Transition;
|
import click.kamil.springstatemachineexporter.model.Transition;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@@ -46,6 +47,10 @@ public class AnalysisResult {
|
|||||||
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
private CodebaseMetadata metadata = CodebaseMetadata.empty();
|
||||||
|
|
||||||
public void applyResolution(Map<String, String> properties) {
|
public void applyResolution(Map<String, String> properties) {
|
||||||
|
applyResolution(properties, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyResolution(Map<String, String> properties, CodebaseContext context) {
|
||||||
var resolver = new click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver();
|
var resolver = new click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver();
|
||||||
|
|
||||||
// 1. Resolve start/end states strings
|
// 1. Resolve start/end states strings
|
||||||
@@ -89,12 +94,12 @@ public class AnalysisResult {
|
|||||||
if (metadata != null) {
|
if (metadata != null) {
|
||||||
List<TriggerPoint> resolvedTriggers = metadata.getTriggers() == null ? null
|
List<TriggerPoint> resolvedTriggers = metadata.getTriggers() == null ? null
|
||||||
: metadata.getTriggers().stream()
|
: metadata.getTriggers().stream()
|
||||||
.map(trigger -> resolveTrigger(trigger, resolver, properties))
|
.map(trigger -> resolveTrigger(trigger, resolver, properties, context))
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
List<CallChain> resolvedCallChains = metadata.getCallChains() == null ? null
|
List<CallChain> resolvedCallChains = metadata.getCallChains() == null ? null
|
||||||
: metadata.getCallChains().stream()
|
: metadata.getCallChains().stream()
|
||||||
.map(chain -> resolveCallChain(chain, resolver, properties))
|
.map(chain -> resolveCallChain(chain, resolver, properties, context))
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
|
||||||
if (metadata.getEntryPoints() != null) {
|
if (metadata.getEntryPoints() != null) {
|
||||||
@@ -136,17 +141,26 @@ public class AnalysisResult {
|
|||||||
private static TriggerPoint resolveTrigger(
|
private static TriggerPoint resolveTrigger(
|
||||||
TriggerPoint trigger,
|
TriggerPoint trigger,
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
||||||
Map<String, String> properties) {
|
Map<String, String> properties,
|
||||||
|
CodebaseContext context) {
|
||||||
List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
|
List<String> polymorphicEvents = trigger.getPolymorphicEvents() == null ? null
|
||||||
: trigger.getPolymorphicEvents().stream()
|
: trigger.getPolymorphicEvents().stream()
|
||||||
.map(value -> resolver.resolveValue(value, properties))
|
.map(value -> resolver.resolveValue(value, properties))
|
||||||
.map(value -> MachineEnumCanonicalizer.qualifyEventIdentifier(
|
.map(value -> MachineEnumCanonicalizer.qualifyEventIdentifier(
|
||||||
value, trigger.getEventTypeFqn()))
|
value, trigger.getEventTypeFqn(), context))
|
||||||
.collect(java.util.stream.Collectors.toList());
|
.collect(java.util.stream.Collectors.toList());
|
||||||
return trigger.toBuilder()
|
String resolvedEvent = trigger.getEvent() != null ? resolver.resolveValue(trigger.getEvent(), properties) : null;
|
||||||
.event(trigger.getEvent() != null ? resolver.resolveValue(trigger.getEvent(), properties) : null)
|
String resolvedSource = trigger.getSourceState() != null
|
||||||
.sourceState(trigger.getSourceState() != null
|
|
||||||
? resolver.resolveValue(trigger.getSourceState(), properties)
|
? resolver.resolveValue(trigger.getSourceState(), properties)
|
||||||
|
: null;
|
||||||
|
return trigger.toBuilder()
|
||||||
|
.event(resolvedEvent != null
|
||||||
|
? MachineEnumCanonicalizer.qualifyEventIdentifier(
|
||||||
|
resolvedEvent, trigger.getEventTypeFqn(), context)
|
||||||
|
: null)
|
||||||
|
.sourceState(resolvedSource != null
|
||||||
|
? MachineEnumCanonicalizer.canonicalizeLabel(
|
||||||
|
resolvedSource, trigger.getStateTypeFqn(), context)
|
||||||
: null)
|
: null)
|
||||||
.polymorphicEvents(polymorphicEvents)
|
.polymorphicEvents(polymorphicEvents)
|
||||||
.build();
|
.build();
|
||||||
@@ -155,10 +169,11 @@ public class AnalysisResult {
|
|||||||
private static CallChain resolveCallChain(
|
private static CallChain resolveCallChain(
|
||||||
CallChain chain,
|
CallChain chain,
|
||||||
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver resolver,
|
||||||
Map<String, String> properties) {
|
Map<String, String> properties,
|
||||||
|
CodebaseContext context) {
|
||||||
TriggerPoint trigger = chain.getTriggerPoint() == null
|
TriggerPoint trigger = chain.getTriggerPoint() == null
|
||||||
? null
|
? null
|
||||||
: resolveTrigger(chain.getTriggerPoint(), resolver, properties);
|
: resolveTrigger(chain.getTriggerPoint(), resolver, properties, context);
|
||||||
List<MatchedTransition> matchedTransitions = chain.getMatchedTransitions() == null ? null
|
List<MatchedTransition> matchedTransitions = chain.getMatchedTransitions() == null ? null
|
||||||
: chain.getMatchedTransitions().stream()
|
: chain.getMatchedTransitions().stream()
|
||||||
.map(matched -> MatchedTransition.builder()
|
.map(matched -> MatchedTransition.builder()
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ public class TriggerPoint {
|
|||||||
this.sourceFile = sourceFile;
|
this.sourceFile = sourceFile;
|
||||||
this.sourceModule = sourceModule;
|
this.sourceModule = sourceModule;
|
||||||
this.stateMachineId = stateMachineId;
|
this.stateMachineId = stateMachineId;
|
||||||
this.sourceState = MachineEnumCanonicalizer.canonicalizeLabel(sourceState, stateTypeFqn);
|
this.sourceState = sourceState;
|
||||||
this.lineNumber = lineNumber;
|
this.lineNumber = lineNumber;
|
||||||
this.stateTypeFqn = stateTypeFqn;
|
this.stateTypeFqn = stateTypeFqn;
|
||||||
this.eventTypeFqn = eventTypeFqn;
|
this.eventTypeFqn = eventTypeFqn;
|
||||||
this.event = MachineEnumCanonicalizer.qualifyEventIdentifier(event, eventTypeFqn);
|
this.event = event;
|
||||||
this.polymorphicEvents = polymorphicEvents;
|
this.polymorphicEvents = polymorphicEvents;
|
||||||
this.external = external;
|
this.external = external;
|
||||||
this.constraint = constraint;
|
this.constraint = constraint;
|
||||||
|
|||||||
@@ -533,6 +533,10 @@ public final class MachineEnumCanonicalizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String qualifyEventIdentifier(String event, String eventTypeFqn) {
|
public static String qualifyEventIdentifier(String event, String eventTypeFqn) {
|
||||||
|
return qualifyEventIdentifier(event, eventTypeFqn, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String qualifyEventIdentifier(String event, String eventTypeFqn, CodebaseContext context) {
|
||||||
if (event == null || eventTypeFqn == null || event.isEmpty()) {
|
if (event == null || eventTypeFqn == null || event.isEmpty()) {
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
@@ -554,8 +558,8 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (isStringOrPrimitiveType(eventTypeFqn)) {
|
if (isStringOrPrimitiveType(eventTypeFqn)) {
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
if (isMachineEnumReference(event, eventTypeFqn)) {
|
if (isMachineEnumReference(event, eventTypeFqn, context)) {
|
||||||
return canonicalizeLabel(event, stripGenerics(eventTypeFqn));
|
return canonicalizeLabel(event, stripGenerics(eventTypeFqn), context);
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
@@ -742,6 +746,10 @@ public final class MachineEnumCanonicalizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isMachineEnumReference(String value, String enumTypeFqn) {
|
public static boolean isMachineEnumReference(String value, String enumTypeFqn) {
|
||||||
|
return isMachineEnumReference(value, enumTypeFqn, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isMachineEnumReference(String value, String enumTypeFqn, CodebaseContext context) {
|
||||||
if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
if (value == null || value.isBlank() || enumTypeFqn == null || enumTypeFqn.isBlank()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -765,7 +773,10 @@ public final class MachineEnumCanonicalizer {
|
|||||||
if (typePart == null) {
|
if (typePart == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return enumTypesMatch(enumTypeFqn, typePart);
|
if (context != null && context.isAmbiguousSimpleName(typePart)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return enumTypesMatch(enumTypeFqn, typePart, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String canonicalizeLabel(String value, String enumTypeFqn) {
|
public static String canonicalizeLabel(String value, String enumTypeFqn) {
|
||||||
|
|||||||
@@ -79,7 +79,25 @@ public class TypeResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return simpleName;
|
return preferBindingFqn(type, simpleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String preferBindingFqn(Type type, String heuristic) {
|
||||||
|
if (!context.isResolveBindings() || heuristic == null || heuristic.contains(".")) {
|
||||||
|
return heuristic;
|
||||||
|
}
|
||||||
|
ITypeBinding binding = type.resolveBinding();
|
||||||
|
if (binding == null || binding.isRecovered()) {
|
||||||
|
return heuristic;
|
||||||
|
}
|
||||||
|
String bindingFqn = binding.getErasure().getQualifiedName();
|
||||||
|
if (bindingFqn == null || bindingFqn.isBlank() || !bindingFqn.contains(".") || bindingFqn.startsWith("<")) {
|
||||||
|
return heuristic;
|
||||||
|
}
|
||||||
|
if (bindingFqn.startsWith("java.") || bindingFqn.startsWith("javax.") || bindingFqn.startsWith("jakarta.")) {
|
||||||
|
return heuristic;
|
||||||
|
}
|
||||||
|
return bindingFqn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getParameterIndex(String methodFqn, String paramName) {
|
public int getParameterIndex(String methodFqn, String paramName) {
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ public class ExportService {
|
|||||||
JsonImportService jsonImportService = new JsonImportService();
|
JsonImportService jsonImportService = new JsonImportService();
|
||||||
AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
|
AnalysisResult result = jsonImportService.importAnalysisResult(jsonFile);
|
||||||
|
|
||||||
resolveProperties(result, activeProfiles);
|
resolveProperties(result, activeProfiles, null);
|
||||||
|
|
||||||
CodebaseContext context = null;
|
CodebaseContext context = null;
|
||||||
Path sourceRoot = optionalSourceDir;
|
Path sourceRoot = optionalSourceDir;
|
||||||
@@ -215,7 +215,7 @@ public class ExportService {
|
|||||||
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
generateOutputs(outputDir, result, selectedFormats, eventFormat, stateFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resolveProperties(AnalysisResult result, List<String> activeProfiles) {
|
private void resolveProperties(AnalysisResult result, List<String> activeProfiles, CodebaseContext context) {
|
||||||
Map<String, Map<String, String>> allProps = result.getMetadata().getProperties();
|
Map<String, Map<String, String>> allProps = result.getMetadata().getProperties();
|
||||||
if (allProps == null || allProps.isEmpty()) return;
|
if (allProps == null || allProps.isEmpty()) return;
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ public class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Delegate to result for resolution
|
// 2. Delegate to result for resolution
|
||||||
result.applyResolution(merged);
|
result.applyResolution(merged, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
private void exportAll(CodebaseContext context, CodebaseIntelligenceProvider intelligence, Path outputDir, List<String> selectedFormats, boolean renderChoicesAsDiamonds, List<BusinessFlow> flows, String machineFilter, List<String> activeProfiles, click.kamil.springstatemachineexporter.exporter.EnumFormat eventFormat, click.kamil.springstatemachineexporter.exporter.EnumFormat stateFormat) throws IOException {
|
||||||
@@ -305,7 +305,7 @@ public class ExportService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
enrichmentService.enrichPreProperty(result, context, intelligence);
|
enrichmentService.enrichPreProperty(result, context, intelligence);
|
||||||
resolveProperties(result, activeProfiles);
|
resolveProperties(result, activeProfiles, context);
|
||||||
enrichmentService.enrichPostProperty(result, context, intelligence);
|
enrichmentService.enrichPostProperty(result, context, intelligence);
|
||||||
AnalysisResultFinalizer.finalizeResult(result, context);
|
AnalysisResultFinalizer.finalizeResult(result, context);
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ public class ExportService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
enrichmentService.enrichPreProperty(result, context, intelligence);
|
enrichmentService.enrichPreProperty(result, context, intelligence);
|
||||||
resolveProperties(result, activeProfiles);
|
resolveProperties(result, activeProfiles, context);
|
||||||
enrichmentService.enrichPostProperty(result, context, intelligence);
|
enrichmentService.enrichPostProperty(result, context, intelligence);
|
||||||
AnalysisResultFinalizer.finalizeResult(result, context);
|
AnalysisResultFinalizer.finalizeResult(result, context);
|
||||||
|
|
||||||
|
|||||||
@@ -92,4 +92,22 @@ class MachineEnumCanonicalizerCrossPackageTest {
|
|||||||
"com.example.order.OrderEvent.PAY",
|
"com.example.order.OrderEvent.PAY",
|
||||||
"com.example.order.OrderEvent.SHIP");
|
"com.example.order.OrderEvent.SHIP");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotQualifyAmbiguousImportStyleEventReference(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.createDirectories(tempDir.resolve("a"));
|
||||||
|
Files.createDirectories(tempDir.resolve("b"));
|
||||||
|
Files.writeString(tempDir.resolve("a/OrderEvent.java"),
|
||||||
|
"package a; public enum OrderEvent { PAY }");
|
||||||
|
Files.writeString(tempDir.resolve("b/OrderEvent.java"),
|
||||||
|
"package b; public enum OrderEvent { CANCEL }");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
|
assertThat(MachineEnumCanonicalizer.qualifyEventIdentifier(
|
||||||
|
"OrderEvent.PAY", "a.OrderEvent", context)).isEqualTo("OrderEvent.PAY");
|
||||||
|
assertThat(MachineEnumCanonicalizer.isMachineEnumReference(
|
||||||
|
"OrderEvent.PAY", "a.OrderEvent", context)).isFalse();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,9 +110,12 @@ class AnalysisResultFinalizerTest {
|
|||||||
.metadata(CodebaseMetadata.builder().triggers(List.of(trigger)).build())
|
.metadata(CodebaseMetadata.builder().triggers(List.of(trigger)).build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.scan(tempDir);
|
||||||
|
|
||||||
result.applyResolution(Map.of(
|
result.applyResolution(Map.of(
|
||||||
"app.event", "OrderEvent.PAY",
|
"app.event", "OrderEvent.PAY",
|
||||||
"app.state", "OrderState.NEW"));
|
"app.state", "OrderState.NEW"), context);
|
||||||
|
|
||||||
TriggerPoint resolved = result.getMetadata().getTriggers().get(0);
|
TriggerPoint resolved = result.getMetadata().getTriggers().get(0);
|
||||||
assertThat(resolved.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
|
assertThat(resolved.getEvent()).isEqualTo("com.example.order.OrderEvent.PAY");
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package click.kamil.springstatemachineexporter.analysis.service;
|
package click.kamil.springstatemachineexporter.analysis.service;
|
||||||
|
|
||||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||||
|
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||||
|
import org.eclipse.jdt.core.dom.MethodDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
|
||||||
|
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
@@ -48,4 +52,54 @@ class TypeResolverTest {
|
|||||||
assertThat(resolver.isTypeCompatible("a.OrderEvent", "b.OrderEvent")).isFalse();
|
assertThat(resolver.isTypeCompatible("a.OrderEvent", "b.OrderEvent")).isFalse();
|
||||||
assertThat(resolver.isTypeCompatible("a.OrderEvent", "a.OrderEvent")).isTrue();
|
assertThat(resolver.isTypeCompatible("a.OrderEvent", "a.OrderEvent")).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpgradeSimpleHeuristicTypeToBindingFqnForUserTypes(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.createDirectories(tempDir.resolve("com/example/order"));
|
||||||
|
Files.writeString(tempDir.resolve("com/example/order/OrderEvent.java"), """
|
||||||
|
package com.example.order;
|
||||||
|
public enum OrderEvent { PAY }
|
||||||
|
""");
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), """
|
||||||
|
package com.example;
|
||||||
|
import com.example.order.OrderEvent;
|
||||||
|
class StateMachine {
|
||||||
|
void sendEvent(OrderEvent e) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
TypeResolver resolver = new TypeResolver(context);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.example.StateMachine");
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, "sendEvent", true);
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(0);
|
||||||
|
|
||||||
|
assertThat(resolver.resolveTypeToFqn(param.getType(), (CompilationUnit) td.getRoot()))
|
||||||
|
.isEqualTo("com.example.order.OrderEvent");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepHeuristicStringTypeWhenBindingsWouldReturnJavaLangString(@TempDir Path tempDir) throws Exception {
|
||||||
|
Files.writeString(tempDir.resolve("App.java"), """
|
||||||
|
package com.example;
|
||||||
|
class Api {
|
||||||
|
void handle(String value) {}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
CodebaseContext context = new CodebaseContext();
|
||||||
|
context.setResolveBindings(true);
|
||||||
|
context.scan(tempDir);
|
||||||
|
TypeResolver resolver = new TypeResolver(context);
|
||||||
|
|
||||||
|
TypeDeclaration td = context.getTypeDeclaration("com.example.Api");
|
||||||
|
MethodDeclaration md = context.findMethodDeclaration(td, "handle", true);
|
||||||
|
SingleVariableDeclaration param = (SingleVariableDeclaration) md.parameters().get(0);
|
||||||
|
|
||||||
|
assertThat(resolver.resolveTypeToFqn(param.getType(), (CompilationUnit) td.getRoot()))
|
||||||
|
.isEqualTo("String");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user