string resolution
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package click.kamil.springstatemachineexporter.analysis;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.SpringMvcDetector;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.CompilationUnit;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class AdvancedEndpointResolutionTest {
|
||||
|
||||
@Test
|
||||
void testComplexResolutionWithProfiles(@TempDir Path tempDir) throws IOException {
|
||||
Path srcDir = tempDir.resolve("src/main/java/com/example");
|
||||
Files.createDirectories(srcDir);
|
||||
|
||||
// 1. Define Constants
|
||||
Files.writeString(srcDir.resolve("ApiConstants.java"),
|
||||
"package com.example;\n" +
|
||||
"public class ApiConstants {\n" +
|
||||
" public static final String V1 = \"/v1\";\n" +
|
||||
" public static final String BASE = \"${app.base:api}\" + V1;\n" +
|
||||
"}");
|
||||
|
||||
// 2. Define Controller
|
||||
Files.writeString(srcDir.resolve("MyController.java"),
|
||||
"package com.example;\n" +
|
||||
"import org.springframework.web.bind.annotation.*;\n" +
|
||||
"import org.springframework.beans.factory.annotation.Value;\n" +
|
||||
"\n" +
|
||||
"@RestController\n" +
|
||||
"@RequestMapping(ApiConstants.BASE)\n" +
|
||||
"public class MyController {\n" +
|
||||
" @Value(\"${app.suffix:/users}\")\n" +
|
||||
" private String suffix;\n" +
|
||||
"\n" +
|
||||
" @GetMapping(value = \"/list\" + \"${app.extra:}\")\n" +
|
||||
" public void list() {}\n" +
|
||||
" \n" +
|
||||
" @PostMapping(\"${app.custom-path}\")\n" +
|
||||
" public void custom() {}\n" +
|
||||
"}");
|
||||
|
||||
// 3. Define Properties
|
||||
Files.writeString(tempDir.resolve("application.properties"),
|
||||
"app.custom-path=/custom\n" +
|
||||
"app.base=core");
|
||||
|
||||
Files.writeString(tempDir.resolve("application-prod.properties"),
|
||||
"app.base=enterprise\n" +
|
||||
"app.extra=-premium");
|
||||
|
||||
// --- SCENARIO 1: Default Profile ---
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
SpringMvcDetector detector = new SpringMvcDetector(context, new ConstantResolver());
|
||||
TypeDeclaration controllerTd = context.getTypeDeclaration("com.example.MyController");
|
||||
CompilationUnit cu = (CompilationUnit) controllerTd.getRoot();
|
||||
|
||||
List<EntryPoint> entryPoints = detector.detect(cu);
|
||||
|
||||
// app.base=core, V1=/v1 => base path = /core/v1
|
||||
// list() => /core/v1/list
|
||||
// custom() => /core/v1/custom
|
||||
assertThat(entryPoints).hasSize(2);
|
||||
assertThat(entryPoints).anySatisfy(ep -> assertThat(ep.getName()).isEqualTo("GET /core/v1/list"));
|
||||
assertThat(entryPoints).anySatisfy(ep -> assertThat(ep.getName()).isEqualTo("POST /core/v1/custom"));
|
||||
|
||||
// --- SCENARIO 2: Prod Profile ---
|
||||
context = new CodebaseContext();
|
||||
context.setActiveProfiles(List.of("prod"));
|
||||
context.scan(tempDir);
|
||||
|
||||
detector = new SpringMvcDetector(context, new ConstantResolver());
|
||||
controllerTd = context.getTypeDeclaration("com.example.MyController");
|
||||
cu = (CompilationUnit) controllerTd.getRoot();
|
||||
|
||||
entryPoints = detector.detect(cu);
|
||||
|
||||
// app.base=enterprise (from prod), V1=/v1 => base path = /enterprise/v1
|
||||
// list() => /enterprise/v1/list-premium (app.extra from prod)
|
||||
// custom() => /enterprise/v1/custom
|
||||
assertThat(entryPoints).hasSize(2);
|
||||
assertThat(entryPoints).anySatisfy(ep -> assertThat(ep.getName()).isEqualTo("GET /enterprise/v1/list-premium"));
|
||||
assertThat(entryPoints).anySatisfy(ep -> assertThat(ep.getName()).isEqualTo("POST /enterprise/v1/custom"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package click.kamil.springstatemachineexporter.analysis;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.FieldDeclaration;
|
||||
import org.eclipse.jdt.core.dom.TypeDeclaration;
|
||||
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class MultiModuleResolutionTest {
|
||||
|
||||
@Test
|
||||
void testResolutionAcrossModules(@TempDir Path tempDir) throws IOException {
|
||||
// Module A
|
||||
Path moduleADir = tempDir.resolve("module-a");
|
||||
Path srcA = moduleADir.resolve("src/main/java/com/modulea");
|
||||
Files.createDirectories(srcA);
|
||||
Files.writeString(srcA.resolve("CommonConstants.java"),
|
||||
"package com.modulea;\n" +
|
||||
"public class CommonConstants {\n" +
|
||||
" public static final String API_BASE = \"/api/v1\";\n" +
|
||||
"}");
|
||||
|
||||
// Module B
|
||||
Path moduleBDir = tempDir.resolve("module-b");
|
||||
Path srcB = moduleBDir.resolve("src/main/java/com/moduleb");
|
||||
Files.createDirectories(srcB);
|
||||
Files.writeString(srcB.resolve("SpecificConstants.java"),
|
||||
"package com.moduleb;\n" +
|
||||
"import com.modulea.CommonConstants;\n" +
|
||||
"public class SpecificConstants {\n" +
|
||||
" public static final String USERS = CommonConstants.API_BASE + \"/users\";\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
// Scan both module roots
|
||||
context.scan(Set.of(moduleADir, moduleBDir), Set.of());
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.moduleb.SpecificConstants");
|
||||
FieldDeclaration field = td.getFields()[0];
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(fragment.getInitializer(), context);
|
||||
|
||||
assertThat(result).isEqualTo("/api/v1/users");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class ConstantResolverTest {
|
||||
|
||||
@Test
|
||||
void testConcatenationSameClass(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Constants.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Constants {\n" +
|
||||
" public static final String A = \"/a\";\n" +
|
||||
" public static final String B = \"/b\";\n" +
|
||||
" public static final String AB = A + B;\n" +
|
||||
" public String usage = AB;\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Constants");
|
||||
FieldDeclaration usageField = td.getFields()[3]; // usage
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) usageField.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(fragment.getInitializer(), context);
|
||||
|
||||
assertThat(result).isEqualTo("/a/b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCrossClassConcatenation(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Base.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Base {\n" +
|
||||
" public static final String PATH = \"/api\";\n" +
|
||||
"}");
|
||||
Files.writeString(dir.resolve("Endpoint.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Endpoint {\n" +
|
||||
" public static final String FULL = Base.PATH + \"/v1\";\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Endpoint");
|
||||
FieldDeclaration field = td.getFields()[0];
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(fragment.getInitializer(), context);
|
||||
|
||||
assertThat(result).isEqualTo("/api/v1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecursiveResolution(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Deep.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Deep {\n" +
|
||||
" public static final String V1 = \"/v1\";\n" +
|
||||
" public static final String API = \"/api\" + V1;\n" +
|
||||
" public static final String USERS = API + \"/users\";\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Deep");
|
||||
FieldDeclaration field = td.getFields()[2]; // USERS
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(fragment.getInitializer(), context);
|
||||
|
||||
assertThat(result).isEqualTo("/api/v1/users");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCycleDetection(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Cycle.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Cycle {\n" +
|
||||
" public static final String A = B;\n" +
|
||||
" public static final String B = A;\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Cycle");
|
||||
FieldDeclaration field = td.getFields()[0];
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
// Should not crash
|
||||
String result = resolver.resolve(fragment.getInitializer(), context);
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValueAnnotationSupport(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Controller.java"),
|
||||
"package com.example;\n" +
|
||||
"import org.springframework.beans.factory.annotation.Value;\n" +
|
||||
"public class Controller {\n" +
|
||||
" @Value(\"${app.path}\")\n" +
|
||||
" private String path;\n" +
|
||||
" \n" +
|
||||
" public String getPath() { return path; }\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Controller");
|
||||
FieldDeclaration field = td.getFields()[0];
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
// Resolve a SimpleName referring to an @Value field
|
||||
SimpleName sn = td.getAST().newSimpleName("path");
|
||||
// We need to attach the SimpleName to the AST to find its enclosing type
|
||||
// Actually, resolveManual(SimpleName) uses findEnclosingType.
|
||||
// In the test, we can manually trigger it or mock the node hierarchy.
|
||||
|
||||
// Let's use a real usage in a method
|
||||
MethodDeclaration method = td.getMethods()[0];
|
||||
ReturnStatement rs = (ReturnStatement) method.getBody().statements().get(0);
|
||||
Expression returnExpr = rs.getExpression();
|
||||
|
||||
String result = resolver.resolve(returnExpr, context);
|
||||
assertThat(result).isEqualTo("${app.path}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class StaticImportResolutionTest {
|
||||
|
||||
@Test
|
||||
void testSingleStaticImport(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Constants.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Constants {\n" +
|
||||
" public static final String A = \"/a\";\n" +
|
||||
"}");
|
||||
Files.writeString(dir.resolve("Usage.java"),
|
||||
"package com.example;\n" +
|
||||
"import static com.example.Constants.A;\n" +
|
||||
"public class Usage {\n" +
|
||||
" String val = A;\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Usage");
|
||||
FieldDeclaration field = td.getFields()[0];
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(fragment.getInitializer(), context);
|
||||
|
||||
assertThat(result).isEqualTo("/a");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStarStaticImport(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Constants.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Constants {\n" +
|
||||
" public static final String B = \"/b\";\n" +
|
||||
"}");
|
||||
Files.writeString(dir.resolve("Usage.java"),
|
||||
"package com.example;\n" +
|
||||
"import static com.example.Constants.*;\n" +
|
||||
"public class Usage {\n" +
|
||||
" String val = B;\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Usage");
|
||||
FieldDeclaration field = td.getFields()[0];
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(fragment.getInitializer(), context);
|
||||
|
||||
assertThat(result).isEqualTo("/b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrecedenceLocalOverStaticImport(@TempDir Path tempDir) throws IOException {
|
||||
Path dir = tempDir.resolve("com/example");
|
||||
Files.createDirectories(dir);
|
||||
Files.writeString(dir.resolve("Constants.java"),
|
||||
"package com.example;\n" +
|
||||
"public class Constants {\n" +
|
||||
" public static final String A = \"imported\";\n" +
|
||||
"}");
|
||||
Files.writeString(dir.resolve("Usage.java"),
|
||||
"package com.example;\n" +
|
||||
"import static com.example.Constants.A;\n" +
|
||||
"public class Usage {\n" +
|
||||
" public static final String A = \"local\";\n" +
|
||||
" String val = A;\n" +
|
||||
"}");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(tempDir);
|
||||
|
||||
TypeDeclaration td = context.getTypeDeclaration("com.example.Usage");
|
||||
FieldDeclaration field = td.getFields()[1]; // val
|
||||
VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);
|
||||
|
||||
ConstantResolver resolver = new ConstantResolver();
|
||||
String result = resolver.resolve(fragment.getInitializer(), context);
|
||||
|
||||
assertThat(result).isEqualTo("local");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class PlaceholderResolverTest {
|
||||
|
||||
@Test
|
||||
void testSimpleResolution() {
|
||||
Map<String, String> props = Map.of("app.path", "/api/v1");
|
||||
String result = PlaceholderResolver.resolve("${app.path}", props);
|
||||
assertThat(result).isEqualTo("/api/v1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiplePlaceholders() {
|
||||
Map<String, String> props = Map.of("host", "localhost", "port", "8080");
|
||||
String result = PlaceholderResolver.resolve("http://${host}:${port}/api", props);
|
||||
assertThat(result).isEqualTo("http://localhost:8080/api");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultValue() {
|
||||
Map<String, String> props = Map.of();
|
||||
String result = PlaceholderResolver.resolve("${missing:/default}", props);
|
||||
assertThat(result).isEqualTo("/default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecursiveResolution() {
|
||||
Map<String, String> props = Map.of(
|
||||
"base", "/api",
|
||||
"version", "v1",
|
||||
"full", "${base}/${version}"
|
||||
);
|
||||
String result = PlaceholderResolver.resolve("${full}/users", props);
|
||||
assertThat(result).isEqualTo("/api/v1/users");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCycleDetection() {
|
||||
Map<String, String> props = Map.of(
|
||||
"a", "${b}",
|
||||
"b", "${a}"
|
||||
);
|
||||
// Should not throw StackOverflowError
|
||||
String result = PlaceholderResolver.resolve("${a}", props);
|
||||
// It should keep the unresolvable placeholder in case of cycle
|
||||
assertThat(result).isEqualTo("${a}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedContent() {
|
||||
Map<String, String> props = Map.of("name", "world");
|
||||
String result = PlaceholderResolver.resolve("Hello ${name}!", props);
|
||||
assertThat(result).isEqualTo("Hello world!");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnresolvablePlaceholder() {
|
||||
Map<String, String> props = Map.of();
|
||||
String result = PlaceholderResolver.resolve("${unknown}", props);
|
||||
assertThat(result).isEqualTo("${unknown}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user