Skip to content

Commit e9d90e7

Browse files
authored
Add S2209/S3252 recipe: Static members via class name (#836)
* Add S2209/S3252 recipe: Static members should be accessed via class name Implements OpenRewrite recipe to replace static field/method access through instance references with access via the declaring class name. Transforms instance.staticField to ClassName.staticField and instance.staticMethod() to ClassName.staticMethod(). Covers both RSPEC-S2209 (MAJOR) and RSPEC-S3252 rules. * Fix nested class handling and chained side-effect detection Handle nested class names (e.g. Outer.Inner) by building a J.FieldAccess chain instead of using getClassName() as a single identifier. Add recursive side-effect check for chained field access to avoid dropping method calls in chains like getObj().field.STATIC. * Reuse JavaElementFactory.className() for class reference building Replace hand-rolled nested class handling with the existing utility, which correctly handles type attribution, parameterized types, and local classes. * Add inner class and anonymous class tests for StaticAccessViaInstance
1 parent ee4199a commit e9d90e7

3 files changed

Lines changed: 520 additions & 0 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (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://docs.moderne.io/licensing/moderne-source-available-license
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.staticanalysis;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Preconditions;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.java.JavaIsoVisitor;
25+
import org.openrewrite.java.tree.Expression;
26+
import org.openrewrite.java.tree.Flag;
27+
import org.openrewrite.java.tree.J;
28+
import org.openrewrite.java.tree.JavaType;
29+
import org.openrewrite.java.tree.TypeUtils;
30+
import org.openrewrite.staticanalysis.java.JavaFileChecker;
31+
32+
import java.time.Duration;
33+
import java.util.Arrays;
34+
import java.util.LinkedHashSet;
35+
import java.util.Set;
36+
37+
@Value
38+
@EqualsAndHashCode(callSuper = false)
39+
public class StaticAccessViaInstance extends Recipe {
40+
41+
String displayName = "Static members should be accessed via the class name";
42+
String description = "Accessing static fields or calling static methods on an instance reference is misleading. " +
43+
"Static members should be accessed using the declaring class name instead.";
44+
Set<String> tags = new LinkedHashSet<>(Arrays.asList("RSPEC-S2209", "RSPEC-S3252"));
45+
Duration estimatedEffortPerOccurrence = Duration.ofMinutes(2);
46+
47+
@Override
48+
public TreeVisitor<?, ExecutionContext> getVisitor() {
49+
return Preconditions.check(new JavaFileChecker<>(), new JavaIsoVisitor<ExecutionContext>() {
50+
51+
@Override
52+
public J.FieldAccess visitFieldAccess(J.FieldAccess fieldAccess, ExecutionContext ctx) {
53+
J.FieldAccess fa = super.visitFieldAccess(fieldAccess, ctx);
54+
JavaType.Variable fieldType = fa.getName().getFieldType();
55+
if (fieldType == null || !fieldType.hasFlags(Flag.Static)) {
56+
return fa;
57+
}
58+
Expression select = fa.getTarget();
59+
if (!isInstanceAccess(select)) {
60+
return fa;
61+
}
62+
JavaType.FullyQualified declaringType = TypeUtils.asFullyQualified(fieldType.getOwner());
63+
if (declaringType == null) {
64+
return fa;
65+
}
66+
maybeAddImport(declaringType.getFullyQualifiedName());
67+
return fa.withTarget(buildClassReference(select, declaringType));
68+
}
69+
70+
@Override
71+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
72+
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
73+
JavaType.Method methodType = mi.getMethodType();
74+
if (methodType == null || !methodType.hasFlags(Flag.Static)) {
75+
return mi;
76+
}
77+
Expression select = mi.getSelect();
78+
if (select == null || !isInstanceAccess(select)) {
79+
return mi;
80+
}
81+
JavaType.FullyQualified declaringType = TypeUtils.asFullyQualified(methodType.getDeclaringType());
82+
if (declaringType == null) {
83+
return mi;
84+
}
85+
maybeAddImport(declaringType.getFullyQualifiedName());
86+
return mi.withSelect(buildClassReference(select, declaringType));
87+
}
88+
89+
private boolean isInstanceAccess(Expression select) {
90+
if (select instanceof J.Identifier) {
91+
J.Identifier ident = (J.Identifier) select;
92+
if ("this".equals(ident.getSimpleName()) || "super".equals(ident.getSimpleName())) {
93+
return true;
94+
}
95+
return ident.getFieldType() != null;
96+
}
97+
if (select instanceof J.FieldAccess) {
98+
J.FieldAccess fa = (J.FieldAccess) select;
99+
// Only safe when the entire chain is side-effect-free
100+
return fa.getName().getFieldType() != null && isSideEffectFree(fa.getTarget());
101+
}
102+
// Skip method invocations, new expressions, etc. to preserve side effects
103+
return false;
104+
}
105+
106+
private boolean isSideEffectFree(Expression expr) {
107+
if (expr instanceof J.Identifier) {
108+
return true;
109+
}
110+
if (expr instanceof J.FieldAccess) {
111+
return isSideEffectFree(((J.FieldAccess) expr).getTarget());
112+
}
113+
return false;
114+
}
115+
116+
private Expression buildClassReference(Expression select, JavaType.FullyQualified declaringType) {
117+
return JavaElementFactory.className(declaringType, false)
118+
.withPrefix(select.getPrefix());
119+
}
120+
});
121+
}
122+
}

src/main/resources/META-INF/rewrite/recipes.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanaly
140140
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.ReplaceWeekYearWithYear,Week Year (YYYY) should not be used for date formatting,"For most dates Week Year (YYYY) and Year (yyyy) yield the same results. However, on the last week of December and the first week of January, Week Year could produce unexpected results. This is a common source of off-by-one-year bugs that typically only manifest around New Year's Eve, making them difficult to catch during development and testing.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,
141141
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.SillyEqualsCheck,Silly equality checks should not be made,Detects `.equals()` calls that compare incompatible types and will always return `false`. Replaces `.equals(null)` with `== null` and array `.equals()` with `Arrays.equals()`. Flags comparisons between unrelated types or between arrays and non-arrays.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,
142142
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.SimplifyArraysAsList,Simplify `Arrays.asList(..)` with varargs,"Simplifies `Arrays.asList()` method calls that use explicit array creation to use varargs instead. For example, `Arrays.asList(new String[]{""a"", ""b"", ""c""})` becomes `Arrays.asList(""a"", ""b"", ""c"")`. Explicitly constructing an array to pass to a varargs parameter adds visual clutter without changing behavior, since the compiler generates the array automatically.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,
143+
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.StaticAccessViaInstance,Static members should be accessed via the class name,Accessing static fields or calling static methods on an instance reference is misleading. Static members should be accessed using the declaring class name instead.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,
143144
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.SimplifyBooleanExpression,Simplify boolean expression,"Checks for overly complicated boolean expressions, such as `if (b == true)`, `b || true`, `!false`, etc. Needlessly complex boolean logic makes code harder to reason about and increases the chance of introducing errors during future modifications.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,
144145
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.SimplifyBooleanExpressionWithDeMorgan,Simplify boolean expressions using De Morgan's laws,"Applies De Morgan's laws to simplify boolean expressions with negation. Transforms `!(a && b)` to `!a || !b` and `!(a || b)` to `!a && !b`. Distributing negations inward eliminates the outer `!` and makes each individual condition's polarity immediately visible, which aids comprehension.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,
145146
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.SimplifyBooleanReturn,Simplify boolean return,"Simplifies Boolean expressions by removing redundancies. For example, `a && true` simplifies to `a`. Wrapping a boolean expression in an if-then-else just to return `true` or `false` adds unnecessary control flow that obscures the straightforward intent of the expression.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,

0 commit comments

Comments
 (0)