Skip to content

Commit 669381d

Browse files
amishra-ugithub-actions[bot]timtebeek
authored
[2/x] Implement joda to java time migration recipe (#582)
* [2/x] Implement joda to java time migration recipe * Update src/test/java/org/openrewrite/java/migrate/joda/JodaTimeScannerTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/test/java/org/openrewrite/java/migrate/joda/JodaTimeScannerTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/main/java/org/openrewrite/java/migrate/joda/JodaTimeFlowSpec.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/main/java/org/openrewrite/java/migrate/joda/JodaTimeScanner.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/main/java/org/openrewrite/java/migrate/joda/SafeCheckMarker.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/test/java/org/openrewrite/java/migrate/joda/JodaTimeFlowSpecTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/test/java/org/openrewrite/java/migrate/joda/JodaTimeFlowSpecTest.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/main/java/org/openrewrite/java/migrate/joda/templates/TimeClassMap.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update src/main/java/org/openrewrite/java/migrate/joda/templates/AbstractInstantTemplates.java Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * github suggestion * github auto commit mess * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Adopt AtomicBoolean * Apply formatter --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek <[email protected]> Co-authored-by: Tim te Beek <[email protected]>
1 parent 4d09463 commit 669381d

File tree

10 files changed

+907
-5
lines changed

10 files changed

+907
-5
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.joda;
17+
18+
import lombok.NonNull;
19+
import org.openrewrite.analysis.dataflow.DataFlowNode;
20+
import org.openrewrite.analysis.dataflow.DataFlowSpec;
21+
import org.openrewrite.java.tree.J;
22+
import org.openrewrite.java.tree.JavaType;
23+
24+
import static org.openrewrite.java.migrate.joda.templates.TimeClassNames.JODA_CLASS_PATTERN;
25+
26+
public class JodaTimeFlowSpec extends DataFlowSpec {
27+
28+
@Override
29+
public boolean isSource(@NonNull DataFlowNode srcNode) {
30+
Object value = srcNode.getCursor().getParentTreeCursor().getValue();
31+
32+
if (value instanceof J.Assignment && ((J.Assignment) value).getVariable() instanceof J.Identifier) {
33+
return isJodaType(((J.Assignment) value).getVariable().getType());
34+
}
35+
36+
if (value instanceof J.VariableDeclarations.NamedVariable) {
37+
return isJodaType(((J.VariableDeclarations.NamedVariable) value).getType());
38+
}
39+
return false;
40+
}
41+
42+
@Override
43+
public boolean isSink(@NonNull DataFlowNode sinkNode) {
44+
Object value = sinkNode.getCursor().getValue();
45+
Object parent = sinkNode.getCursor().getParentTreeCursor().getValue();
46+
if (parent instanceof J.MethodInvocation) {
47+
J.MethodInvocation method = (J.MethodInvocation) parent;
48+
return (method.getSelect() != null && method.getSelect().equals(value)) ||
49+
method.getArguments().stream().anyMatch(a -> a.equals(value));
50+
}
51+
return parent instanceof J.VariableDeclarations.NamedVariable ||
52+
parent instanceof J.NewClass ||
53+
parent instanceof J.Assignment ||
54+
parent instanceof J.Return;
55+
}
56+
57+
static boolean isJodaType(JavaType type) {
58+
if (!(type instanceof JavaType.Class)) {
59+
return false;
60+
}
61+
return type.isAssignableFrom(JODA_CLASS_PATTERN);
62+
}
63+
}
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.joda;
17+
18+
import fj.data.Option;
19+
import lombok.Getter;
20+
import lombok.NonNull;
21+
import lombok.RequiredArgsConstructor;
22+
import lombok.Value;
23+
import org.openrewrite.Cursor;
24+
import org.openrewrite.ExecutionContext;
25+
import org.openrewrite.analysis.dataflow.Dataflow;
26+
import org.openrewrite.analysis.dataflow.analysis.SinkFlowSummary;
27+
import org.openrewrite.java.JavaIsoVisitor;
28+
import org.openrewrite.java.tree.Expression;
29+
import org.openrewrite.java.tree.J;
30+
import org.openrewrite.java.tree.J.VariableDeclarations.NamedVariable;
31+
import org.openrewrite.java.tree.JavaType;
32+
33+
import java.util.*;
34+
import java.util.concurrent.atomic.AtomicBoolean;
35+
36+
import static org.openrewrite.java.migrate.joda.templates.TimeClassNames.JODA_CLASS_PATTERN;
37+
38+
public class JodaTimeScanner extends JavaIsoVisitor<ExecutionContext> {
39+
40+
@Getter
41+
private final Set<NamedVariable> unsafeVars = new HashSet<>();
42+
43+
private final LinkedList<VariablesInScope> scopes = new LinkedList<>();
44+
45+
private final Map<NamedVariable, Set<NamedVariable>> varDependencies = new HashMap<>();
46+
47+
@Override
48+
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
49+
cu = super.visitCompilationUnit(cu, ctx);
50+
Set<NamedVariable> allReachable = new HashSet<>();
51+
for (NamedVariable var : unsafeVars) {
52+
dfs(var, allReachable);
53+
}
54+
unsafeVars.addAll(allReachable);
55+
return cu;
56+
}
57+
58+
@Override
59+
public J.Block visitBlock(J.Block block, ExecutionContext ctx) {
60+
scopes.push(new VariablesInScope(getCursor()));
61+
J.Block b = super.visitBlock(block, ctx);
62+
scopes.pop();
63+
return b;
64+
}
65+
66+
@Override
67+
public NamedVariable visitVariable(NamedVariable variable, ExecutionContext ctx) {
68+
assert !scopes.isEmpty();
69+
scopes.peek().variables.add(variable);
70+
if (!variable.getType().isAssignableFrom(JODA_CLASS_PATTERN)) {
71+
return variable;
72+
}
73+
// TODO: handle class variables && method parameters
74+
if (!isLocalVar(variable)) {
75+
unsafeVars.add(variable);
76+
return variable;
77+
}
78+
variable = super.visitVariable(variable, ctx);
79+
80+
if (!variable.getType().isAssignableFrom(JODA_CLASS_PATTERN) || variable.getInitializer() == null) {
81+
return variable;
82+
}
83+
List<Expression> sinks = findSinks(variable.getInitializer());
84+
assert !scopes.isEmpty();
85+
Cursor currentScope = scopes.peek().getScope();
86+
J.Block block = currentScope.getValue();
87+
new AddSafeCheckMarker(sinks).visit(block, ctx, currentScope.getParent());
88+
processMarkersOnExpression(sinks, variable);
89+
return variable;
90+
}
91+
92+
@Override
93+
public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ctx) {
94+
Expression var = assignment.getVariable();
95+
// not joda expr or not local variable
96+
if (!isJodaExpr(var) || !(var instanceof J.Identifier)) {
97+
return assignment;
98+
}
99+
J.Identifier ident = (J.Identifier) var;
100+
Optional<NamedVariable> mayBeVar = findVarInScope(ident.getSimpleName());
101+
if (!mayBeVar.isPresent()) {
102+
return assignment;
103+
}
104+
NamedVariable variable = mayBeVar.get();
105+
Cursor varScope = findScope(variable);
106+
List<Expression> sinks = findSinks(assignment.getAssignment());
107+
new AddSafeCheckMarker(sinks).visit(varScope.getValue(), ctx, varScope.getParent());
108+
processMarkersOnExpression(sinks, variable);
109+
return assignment;
110+
}
111+
112+
private void processMarkersOnExpression(List<Expression> expressions, NamedVariable var) {
113+
for (Expression expr : expressions) {
114+
Optional<SafeCheckMarker> mayBeMarker = expr.getMarkers().findFirst(SafeCheckMarker.class);
115+
if (!mayBeMarker.isPresent()) {
116+
continue;
117+
}
118+
SafeCheckMarker marker = mayBeMarker.get();
119+
if (!marker.isSafe()) {
120+
unsafeVars.add(var);
121+
}
122+
if (!marker.getReferences().isEmpty()) {
123+
varDependencies.compute(var, (k, v) -> v == null ? new HashSet<>() : v).addAll(marker.getReferences());
124+
for (NamedVariable ref : marker.getReferences()) {
125+
varDependencies.compute(ref, (k, v) -> v == null ? new HashSet<>() : v).add(var);
126+
}
127+
}
128+
}
129+
}
130+
131+
private boolean isJodaExpr(Expression expression) {
132+
return expression.getType() != null && expression.getType().isAssignableFrom(JODA_CLASS_PATTERN);
133+
}
134+
135+
private List<Expression> findSinks(Expression expr) {
136+
Cursor cursor = new Cursor(getCursor(), expr);
137+
Option<SinkFlowSummary> mayBeSinks = Dataflow.startingAt(cursor).findSinks(new JodaTimeFlowSpec());
138+
if (mayBeSinks.isNone()) {
139+
return Collections.emptyList();
140+
}
141+
return mayBeSinks.some().getExpressionSinks();
142+
}
143+
144+
private boolean isLocalVar(NamedVariable variable) {
145+
if (!(variable.getVariableType().getOwner() instanceof JavaType.Method)) {
146+
return false;
147+
}
148+
J j = getCursor().dropParentUntil(t -> t instanceof J.Block || t instanceof J.MethodDeclaration).getValue();
149+
return j instanceof J.Block;
150+
}
151+
152+
// Returns the variable in the closest scope
153+
private Optional<NamedVariable> findVarInScope(String varName) {
154+
for (VariablesInScope scope : scopes) {
155+
for (NamedVariable var : scope.variables) {
156+
if (var.getSimpleName().equals(varName)) {
157+
return Optional.of(var);
158+
}
159+
}
160+
}
161+
return Optional.empty();
162+
}
163+
164+
private Cursor findScope(NamedVariable variable) {
165+
for (VariablesInScope scope : scopes) {
166+
if (scope.variables.contains(variable)) {
167+
return scope.scope;
168+
}
169+
}
170+
return null;
171+
}
172+
173+
private void dfs(NamedVariable root, Set<NamedVariable> visited) {
174+
if (visited.contains(root)) {
175+
return;
176+
}
177+
visited.add(root);
178+
for (NamedVariable dep : varDependencies.getOrDefault(root, Collections.emptySet())) {
179+
dfs(dep, visited);
180+
}
181+
}
182+
183+
@Value
184+
private static class VariablesInScope {
185+
Cursor scope;
186+
Set<NamedVariable> variables;
187+
188+
public VariablesInScope(Cursor scope) {
189+
this.scope = scope;
190+
this.variables = new HashSet<>();
191+
}
192+
}
193+
194+
@RequiredArgsConstructor
195+
private class AddSafeCheckMarker extends JavaIsoVisitor<ExecutionContext> {
196+
197+
@NonNull
198+
private List<Expression> expressions;
199+
200+
@Override
201+
public Expression visitExpression(Expression expression, ExecutionContext ctx) {
202+
int index = expressions.indexOf(expression);
203+
if (index == -1) {
204+
return super.visitExpression(expression, ctx);
205+
}
206+
Expression withMarker = expression.withMarkers(expression.getMarkers().addIfAbsent(getMarker(expression, ctx)));
207+
expressions.set(index, withMarker);
208+
return withMarker;
209+
}
210+
211+
private SafeCheckMarker getMarker(Expression expr, ExecutionContext ctx) {
212+
Optional<SafeCheckMarker> mayBeMarker = expr.getMarkers().findFirst(SafeCheckMarker.class);
213+
if (mayBeMarker.isPresent()) {
214+
return mayBeMarker.get();
215+
}
216+
217+
Cursor boundary = findBoundaryCursorForJodaExpr();
218+
boolean isSafe = true;
219+
// TODO: handle return statement
220+
if (boundary.getParentTreeCursor().getValue() instanceof J.Return) {
221+
isSafe = false;
222+
}
223+
Expression boundaryExpr = boundary.getValue();
224+
J j = new JodaTimeVisitor(true).visit(boundaryExpr, ctx, boundary.getParentTreeCursor());
225+
Set<NamedVariable> referencedVars = new HashSet<>();
226+
new FindVarReferences().visit(expr, referencedVars, getCursor().getParentTreeCursor());
227+
AtomicBoolean hasJodaType = new AtomicBoolean();
228+
new HasJodaType().visit(j, hasJodaType);
229+
isSafe = isSafe && !hasJodaType.get() && !referencedVars.contains(null);
230+
referencedVars.remove(null);
231+
return new SafeCheckMarker(UUID.randomUUID(), isSafe, referencedVars);
232+
}
233+
234+
/**
235+
* Traverses the cursor to find the first non-Joda expression in the path.
236+
* If no non-Joda expression is found, it returns the cursor pointing
237+
* to the last Joda expression whose parent is not an Expression.
238+
*/
239+
private Cursor findBoundaryCursorForJodaExpr() {
240+
Cursor cursor = getCursor();
241+
while (cursor.getValue() instanceof Expression && isJodaExpr(cursor.getValue())) {
242+
Cursor parent = cursor.getParentTreeCursor();
243+
if (parent.getValue() instanceof J && !(parent.getValue() instanceof Expression)) {
244+
return cursor;
245+
}
246+
cursor = parent;
247+
}
248+
return cursor;
249+
}
250+
}
251+
252+
private class FindVarReferences extends JavaIsoVisitor<Set<NamedVariable>> {
253+
254+
@Override
255+
public J.Identifier visitIdentifier(J.Identifier ident, Set<NamedVariable> vars) {
256+
if (!isJodaExpr(ident) || ident.getFieldType() == null) {
257+
return ident;
258+
}
259+
if (ident.getFieldType().getOwner() instanceof JavaType.Class) {
260+
vars.add(null); // class variable not supported yet.
261+
}
262+
263+
// find variable in the closest scope
264+
findVarInScope(ident.getSimpleName()).ifPresent(vars::add);
265+
return ident;
266+
}
267+
}
268+
269+
private static class HasJodaType extends JavaIsoVisitor<AtomicBoolean> {
270+
@Override
271+
public Expression visitExpression(Expression expression, AtomicBoolean hasJodaType) {
272+
if (hasJodaType.get()) {
273+
return expression;
274+
}
275+
if (expression.getType() != null && expression.getType().isAssignableFrom(JODA_CLASS_PATTERN)) {
276+
hasJodaType.set(true);
277+
}
278+
return super.visitExpression(expression, hasJodaType);
279+
}
280+
}
281+
}

0 commit comments

Comments
 (0)