string resolution

This commit is contained in:
2026-06-17 15:15:20 +02:00
parent f48cdf080a
commit 566a814671
9 changed files with 741 additions and 20 deletions

View File

@@ -4,10 +4,17 @@ import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jdt.core.dom.*;
import java.util.HashSet;
import java.util.Set;
@Slf4j
public class ConstantResolver {
public String resolve(Expression expr, CodebaseContext context) {
return resolveInternal(expr, context, new HashSet<>());
}
private String resolveInternal(Expression expr, CodebaseContext context, Set<String> visited) {
if (expr == null) return null;
// 1. Literal?
@@ -38,27 +45,30 @@ public class ConstantResolver {
// 3. Fallback: Manual AST Traversal (if bindings not available or resolved)
if (expr instanceof InfixExpression infix) {
return resolveInfix(infix, context);
return resolveInfix(infix, context, visited);
}
if (expr instanceof QualifiedName qn) {
return resolveManual(qn, context);
return resolveManual(qn, context, visited);
}
if (expr instanceof SimpleName sn) {
return resolveManual(sn, context, visited);
}
return null;
}
private String resolveInfix(InfixExpression expr, CodebaseContext context) {
private String resolveInfix(InfixExpression expr, CodebaseContext context, Set<String> visited) {
if (expr.getOperator() != InfixExpression.Operator.PLUS) return null;
String left = resolve(expr.getLeftOperand(), context);
String right = resolve(expr.getRightOperand(), context);
String left = resolveInternal(expr.getLeftOperand(), context, visited);
String right = resolveInternal(expr.getRightOperand(), context, visited);
if (left == null || right == null) return null;
StringBuilder sb = new StringBuilder(left + right);
if (expr.hasExtendedOperands()) {
for (Object operand : expr.extendedOperands()) {
String val = resolve((Expression) operand, context);
String val = resolveInternal((Expression) operand, context, visited);
if (val == null) return null;
sb.append(val);
}
@@ -66,7 +76,27 @@ public class ConstantResolver {
return sb.toString();
}
private String resolveManual(QualifiedName qn, CodebaseContext context) {
private String resolveManual(SimpleName sn, CodebaseContext context, Set<String> visited) {
TypeDeclaration td = findEnclosingType(sn);
if (td != null) {
String fqn = context.getFqn(td);
String result = resolveFieldInType(td, sn.getIdentifier(), fqn, context, visited);
if (result != null) return result;
}
// Fallback: Static imports
ASTNode root = sn.getRoot();
if (root instanceof CompilationUnit cu) {
TypeDeclaration staticTd = context.resolveStaticImport(sn.getIdentifier(), cu);
if (staticTd != null) {
return resolveFieldInType(staticTd, sn.getIdentifier(), context.getFqn(staticTd), context, visited);
}
}
return null;
}
private String resolveManual(QualifiedName qn, CodebaseContext context, Set<String> visited) {
String qualifier = qn.getQualifier().toString();
String name = qn.getName().getIdentifier();
@@ -74,19 +104,81 @@ public class ConstantResolver {
TypeDeclaration td = context.getTypeDeclaration(qualifier, contextCu);
if (td != null) {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragmentObj : fd.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(name)) {
Expression initializer = fragment.getInitializer();
// Simple recursive resolve (be careful of cycles)
if (initializer instanceof StringLiteral sl) return sl.getLiteralValue();
}
}
}
}
String fqn = context.getFqn(td);
return resolveFieldInType(td, name, fqn, context, visited);
}
return null;
}
private String resolveFieldInType(TypeDeclaration td, String fieldName, String typeFqn, CodebaseContext context, Set<String> visited) {
String fieldId = typeFqn + "#" + fieldName;
if (visited.contains(fieldId)) {
log.warn("Circular reference detected during constant resolution: {}", fieldId);
return null;
}
visited.add(fieldId);
try {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragmentObj : fd.fragments()) {
if (fragmentObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(fieldName)) {
// Check for @Value annotation first
String valueFromAnnotation = findValueFromAnnotation(fd);
if (valueFromAnnotation != null) return valueFromAnnotation;
Expression initializer = fragment.getInitializer();
if (initializer != null) {
return resolveInternal(initializer, context, visited);
}
}
}
}
}
} finally {
visited.remove(fieldId);
}
return null;
}
private String findValueFromAnnotation(FieldDeclaration fd) {
for (Object mod : fd.modifiers()) {
if (mod instanceof Annotation ann) {
String name = ann.getTypeName().getFullyQualifiedName();
if (name.endsWith("Value")) {
return extractAnnotationValue(ann);
}
}
}
return null;
}
private String extractAnnotationValue(Annotation ann) {
Expression valueExpr = null;
if (ann instanceof SingleMemberAnnotation sma) {
valueExpr = sma.getValue();
} else if (ann instanceof NormalAnnotation na) {
for (Object pairObj : na.values()) {
MemberValuePair pair = (MemberValuePair) pairObj;
if ("value".equals(pair.getName().getIdentifier())) {
valueExpr = pair.getValue();
break;
}
}
}
if (valueExpr instanceof StringLiteral sl) {
return sl.getLiteralValue();
}
return valueExpr != null ? valueExpr.toString().replaceAll("^\"|\"$", "") : null;
}
private TypeDeclaration findEnclosingType(ASTNode node) {
ASTNode parent = node.getParent();
while (parent != null && !(parent instanceof TypeDeclaration)) {
parent = parent.getParent();
}
return (TypeDeclaration) parent;
}
}

View File

@@ -45,7 +45,12 @@ public class SpringMvcDetector {
if (verb != null && !call.arguments().isEmpty()) {
Expression pathArg = (Expression) call.arguments().get(0);
String path = constantResolver.resolve(pathArg, context);
if (path == null) path = pathArg.toString().replaceAll("\"", "");
if (path != null) {
path = context.resolveString(path);
} else {
path = pathArg.toString().replaceAll("\"", "");
path = context.resolveString(path);
}
entryPoints.add(EntryPoint.builder()
.type(EntryPoint.Type.REST)
@@ -220,7 +225,10 @@ public class SpringMvcDetector {
if (value != null) {
String resolved = constantResolver.resolve(value, context);
return resolved != null ? resolved : value.toString();
if (resolved == null) {
resolved = value.toString().replaceAll("^\"|\"$", "");
}
return context.resolveString(resolved);
}
return "";
}

View File

@@ -0,0 +1,72 @@
package click.kamil.springstatemachineexporter.analysis.util;
import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility for resolving Spring-style placeholders like ${property.name} or ${property.name:default-value}.
* Supports recursive resolution.
*/
@Slf4j
public class PlaceholderResolver {
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^:}]+)(?::([^}]*))?}");
/**
* Resolves all placeholders in the given text using the provided properties map.
*
* @param text The text containing placeholders.
* @param properties The map of property keys and values.
* @return The resolved string.
*/
public static String resolve(String text, Map<String, String> properties) {
return resolveInternal(text, properties, new HashSet<>());
}
private static String resolveInternal(String text, Map<String, String> properties, Set<String> visited) {
if (text == null || !text.contains("${")) {
return text;
}
StringBuilder result = new StringBuilder();
Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
int lastMatchEnd = 0;
while (matcher.find()) {
result.append(text, lastMatchEnd, matcher.start());
String key = matcher.group(1);
String defaultValue = matcher.group(2);
if (visited.contains(key)) {
log.warn("Circular placeholder reference detected: {}", key);
result.append(matcher.group(0)); // Keep as is
} else {
String value = properties.get(key);
if (value == null) {
value = defaultValue;
}
if (value != null) {
visited.add(key);
try {
result.append(resolveInternal(value, properties, visited));
} finally {
visited.remove(key);
}
} else {
log.debug("Property not found and no default value for: {}", key);
result.append(matcher.group(0)); // Keep as is if not resolvable
}
}
lastMatchEnd = matcher.end();
}
result.append(text.substring(lastMatchEnd));
return result.toString();
}
}

View File

@@ -3,9 +3,11 @@ package click.kamil.springstatemachineexporter.ast.common;
import click.kamil.springstatemachineexporter.analysis.model.LibraryHint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.PropertyResolver;
import click.kamil.springstatemachineexporter.analysis.util.PlaceholderResolver;
import click.kamil.springstatemachineexporter.model.State;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.*;
import java.io.IOException;
@@ -29,6 +31,7 @@ public class CodebaseContext {
private final Set<String> ambiguousSimpleNames = new HashSet<>();
private final ConstantResolver constantResolver = new ConstantResolver();
private final PropertyResolver propertyResolver = new PropertyResolver();
private final List<String> activeProfiles = new ArrayList<>();
private Map<String, Map<String, String>> allProperties = new HashMap<>();
private List<LibraryHint> libraryHints = new ArrayList<>();
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -84,9 +87,71 @@ public class CodebaseContext {
}
public void setActiveProfiles(List<String> profiles) {
this.activeProfiles.clear();
if (profiles != null) {
this.activeProfiles.addAll(profiles);
}
this.propertyResolver.setActiveProfiles(profiles);
}
public String resolveString(String input) {
if (input == null) return null;
Map<String, String> merged = new HashMap<>();
// 1. Default properties
Map<String, String> defaultProps = allProperties.get("default");
if (defaultProps != null) {
merged.putAll(defaultProps);
}
// 2. Active profiles (later ones override earlier ones)
for (String profile : activeProfiles) {
Map<String, String> profileProps = allProperties.get(profile);
if (profileProps != null) {
merged.putAll(profileProps);
}
}
return PlaceholderResolver.resolve(input, merged);
}
public TypeDeclaration resolveStaticImport(String memberName, CompilationUnit contextCu) {
if (contextCu == null) return null;
for (Object impObj : contextCu.imports()) {
ImportDeclaration imp = (ImportDeclaration) impObj;
if (imp.isStatic()) {
String impName = imp.getName().getFullyQualifiedName();
if (imp.isOnDemand()) {
// Star import: import static com.example.C.*;
TypeDeclaration td = getTypeDeclaration(impName, contextCu);
if (td != null && hasField(td, memberName)) {
return td;
}
} else {
// Single import: import static com.example.C.A;
if (impName.endsWith("." + memberName)) {
String typeFqn = impName.substring(0, impName.lastIndexOf('.'));
return getTypeDeclaration(typeFqn, contextCu);
}
}
}
}
return null;
}
public boolean hasField(TypeDeclaration td, String name) {
for (FieldDeclaration fd : td.getFields()) {
for (Object fragObj : fd.fragments()) {
if (fragObj instanceof VariableDeclarationFragment fragment) {
if (fragment.getName().getIdentifier().equals(name)) {
return true;
}
}
}
}
return false;
}
public void scan(Path rootDir) throws IOException {
scan(Set.of(rootDir), Collections.emptySet());
}
@@ -271,6 +336,11 @@ public class CodebaseContext {
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setResolveBindings(resolveBindings);
Map<String, String> options = JavaCore.getOptions();
JavaCore.setComplianceOptions(JavaCore.VERSION_21, options);
parser.setCompilerOptions(options);
if (resolveBindings) {
parser.setUnitName(unitName);
parser.setEnvironment(classpath, sourcepath, null, true);