Skip to content

Commit 2c08124

Browse files
authored
SONARJAVA-6102 : Implement rule S8447 - initialize subclass fields before super (#5461)
1 parent e9e93bd commit 2c08124

9 files changed

Lines changed: 685 additions & 1 deletion

File tree

its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2879,6 +2879,12 @@
28792879
"falseNegatives": 0,
28802880
"falsePositives": 0
28812881
},
2882+
{
2883+
"ruleKey": "8447",
2884+
"hasTruePositives": true,
2885+
"falseNegatives": 0,
2886+
"falsePositives": 0
2887+
},
28822888
{
28832889
"ruleKey": "S8450",
28842890
"hasTruePositives": false,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ruleKey": "S8447",
3+
"hasTruePositives": true,
4+
"falseNegatives": 0,
5+
"falsePositives": 0
6+
}
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
package checks;
2+
3+
import java.io.IO;
4+
5+
public class InitializeSubclassFieldsBeforeSuperSample {
6+
7+
// =================== Parent classes ===================
8+
9+
// Constructor calls abstract method → child overrides are invoked during super()
10+
abstract static class ParentCallsAbstract {
11+
protected ParentCallsAbstract() {
12+
IO.println("name is " + getName());
13+
}
14+
15+
abstract String getName();
16+
}
17+
18+
// Constructor calls a concrete method that the child may or may not override
19+
static class ParentCallsConcrete {
20+
protected ParentCallsConcrete() {
21+
doInit();
22+
}
23+
24+
void doInit() {
25+
IO.println("default init");
26+
}
27+
}
28+
29+
// Constructor calls private method → which calls abstract method (transitive chain)
30+
abstract static class ParentCallsTransitive {
31+
protected ParentCallsTransitive() {
32+
helper();
33+
}
34+
35+
private void helper() {
36+
IO.println(getValue());
37+
}
38+
39+
abstract String getValue();
40+
}
41+
42+
// Constructor directly reads an inherited field (no method call involved)
43+
static class ParentReadsField {
44+
protected String readField;
45+
protected String ignoredField;
46+
47+
protected ParentReadsField() {
48+
IO.println("Value: " + readField);
49+
}
50+
}
51+
52+
// =================== Noncompliant ===================
53+
54+
// Basic: field returned by abstract override, assigned after super()
55+
public static class FieldUsedViaOverride extends ParentCallsAbstract {
56+
private final String name;
57+
58+
FieldUsedViaOverride(String name) {
59+
super();
60+
this.name = name; // Noncompliant {{Initialize subclass fields before calling super constructor.}}
61+
// ^^^^^^^^^^^^^^^^
62+
}
63+
64+
@Override
65+
String getName() {
66+
return name;
67+
}
68+
}
69+
70+
// Assignment inside if/else block after super() — visitor must recurse into nested blocks
71+
public static class AssignmentInNestedBlock extends ParentCallsAbstract {
72+
private final String name;
73+
74+
AssignmentInNestedBlock(String name, boolean condition) {
75+
super();
76+
if (condition) {
77+
this.name = name; // Noncompliant
78+
} else {
79+
this.name = "default"; // Noncompliant
80+
}
81+
}
82+
83+
@Override
84+
String getName() {
85+
return name;
86+
}
87+
}
88+
89+
// Transitive: super() → private helper() → abstract getValue() → child override
90+
public static class FieldUsedTransitively extends ParentCallsTransitive {
91+
private final String data;
92+
93+
FieldUsedTransitively(String data) {
94+
super();
95+
this.data = data; // Noncompliant
96+
}
97+
98+
@Override
99+
String getValue() {
100+
return data;
101+
}
102+
}
103+
104+
// Inherited field directly read by parent constructor; only the read field is flagged
105+
public static class InheritedFieldReadByParent extends ParentReadsField {
106+
InheritedFieldReadByParent(String read, String ignored) {
107+
super();
108+
this.readField = read; // Noncompliant
109+
this.ignoredField = ignored; // Compliant - not read in parent constructor
110+
}
111+
}
112+
113+
// =================== Compliant ===================
114+
115+
// Assignment BEFORE super() — not included in the post-super statement list
116+
public static class AssignedBeforeSuper extends ParentCallsAbstract {
117+
private final String name;
118+
119+
AssignedBeforeSuper(String name) {
120+
this.name = name;
121+
super();
122+
}
123+
124+
@Override
125+
String getName() {
126+
return name;
127+
}
128+
}
129+
130+
// Parent calls concrete method, child doesn't override it → field not reachable
131+
public static class ConcreteMethodNotOverridden extends ParentCallsConcrete {
132+
private String extra;
133+
134+
ConcreteMethodNotOverridden(String extra) {
135+
super();
136+
this.extra = extra; // Compliant - doInit() doesn't use this field
137+
}
138+
}
139+
140+
// Local variable assignment after super() — not a field, should not be flagged
141+
public static class LocalVariableAfterSuper extends ParentCallsAbstract {
142+
private final String name;
143+
144+
LocalVariableAfterSuper(String name) {
145+
this.name = name;
146+
String localVar1;
147+
super();
148+
localVar1 = "test"; // Compliant - not a field assignment
149+
String localVar2 = "test"; // Compliant - not a field assignment
150+
}
151+
152+
@Override
153+
String getName() {
154+
return name;
155+
}
156+
}
157+
158+
// =================== Edge cases ===================
159+
160+
// Override is in a grandchild, not the direct child that assigns the field
161+
// → findChildOverride returns the abstract re-declaration → conservative flag
162+
static class GrandchildOverride {
163+
abstract class Base {
164+
Base() {
165+
describe();
166+
}
167+
168+
abstract void describe();
169+
}
170+
171+
abstract class Middle extends Base {
172+
protected final String label;
173+
174+
Middle() {
175+
super();
176+
this.label = "value"; // Noncompliant
177+
}
178+
179+
abstract void describe();
180+
}
181+
182+
abstract class Leaf extends Middle {
183+
Leaf() {
184+
super();
185+
}
186+
187+
@Override
188+
void describe() {
189+
IO.println(label);
190+
}
191+
}
192+
}
193+
194+
// Parameter with same name as field: bare name resolves to parameter, this.field resolves to field
195+
static class ParameterShadowing {
196+
abstract class Base {
197+
String someText;
198+
199+
Base() {
200+
doWork();
201+
}
202+
203+
abstract void doWork();
204+
}
205+
206+
// Compliant: bare 'someText' in isValid refers to the parameter, not the field
207+
class ShadowedByParameter extends Base {
208+
ShadowedByParameter(String someText) {
209+
super();
210+
this.someText = someText; // Compliant
211+
}
212+
213+
@Override
214+
void doWork() {
215+
isValid("parameter value");
216+
}
217+
218+
private boolean isValid(String someText) {
219+
return !someText.isEmpty();
220+
}
221+
}
222+
223+
// this.someText explicitly accesses the field despite parameter shadowing
224+
class ExplicitThisBypassesShadowing extends Base {
225+
ExplicitThisBypassesShadowing(String someText) {
226+
super();
227+
this.someText = someText; // Noncompliant
228+
}
229+
230+
@Override
231+
void doWork() {
232+
isValid("parameter value");
233+
}
234+
235+
private boolean isValid(String someText) {
236+
return !this.someText.isEmpty();
237+
}
238+
}
239+
}
240+
241+
// Parent only writes to the field (never reads) → assignment-only usage is not flagged
242+
class FieldOnlyWrittenByParent {
243+
class Parent {
244+
String label;
245+
246+
Parent() {
247+
this.label = "hello";
248+
label = "world";
249+
}
250+
}
251+
252+
class Child extends Parent {
253+
Child() {
254+
super();
255+
this.label = "value"; // Compliant - parent only assigns, never reads
256+
label = "value"; // Compliant
257+
}
258+
}
259+
}
260+
261+
// Tests bare field name (implicit this) assignment detection and outer-class field exclusion
262+
class ImplicitThisAndOuterField {
263+
Integer outerField;
264+
265+
class Parent {
266+
String label;
267+
268+
Parent() {
269+
IO.println(label);
270+
postInit();
271+
}
272+
273+
void postInit() {
274+
}
275+
}
276+
277+
class Child extends Parent {
278+
String message;
279+
280+
Child() {
281+
super();
282+
this.label = "value"; // Noncompliant
283+
label = "value"; // Noncompliant
284+
this.message = "msg"; // Noncompliant
285+
message = "msg"; // Noncompliant
286+
outerField = 1; // Compliant - field belongs to enclosing class, not Child
287+
}
288+
289+
@Override
290+
void postInit() {
291+
IO.println(message);
292+
IO.println(outerField);
293+
}
294+
}
295+
}
296+
}

0 commit comments

Comments
 (0)