Skip to content

Commit b944cb4

Browse files
Brijeshthummar02romani
authored andcommitted
Issue #335: Checks under neverSuppressedChecks group should give more precise violations
1 parent 0a30f36 commit b944cb4

22 files changed

Lines changed: 402 additions & 25 deletions

File tree

src/main/java/com/puppycrawl/tools/checkstyle/filters/JavaPatchFilterElement.java

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public final class JavaPatchFilterElement implements TreeWalkerFilter {
5252
private static final Map<String, List<Integer>>
5353
CHECK_TO_ANCESTOR_NODES_MAP = new HashMap<>();
5454

55+
/** Checks whose violations should be attributed at enclosing type scope. */
56+
private static final Set<String> CLASS_SCOPE_NEVER_SUPPRESSED_CHECKS = new HashSet<>(
57+
Arrays.asList("CovariantEquals"));
58+
5559
static {
5660
CHECK_TO_ANCESTOR_NODES_MAP.put("ArrayTrailingComma",
5761
Arrays.asList(TokenTypes.ARRAY_INIT));
@@ -176,13 +180,13 @@ public boolean accept(TreeWalkerAuditEvent event) {
176180

177181
if (Strategy.CONTEXT == strategy) {
178182
result = isFileNameMatching(event)
179-
&& (isNeverSuppressCheck(event)
183+
&& (isMatchingByNeverSuppressedCheck(event)
180184
|| isMatchingByContextStrategy(event)
181185
|| isLineMatching(event));
182186
}
183187
else {
184188
result = isFileNameMatching(event)
185-
&& (isNeverSuppressCheck(event)
189+
&& (isMatchingByNeverSuppressedCheck(event)
186190
|| isLineMatching(event));
187191
}
188192

@@ -228,6 +232,79 @@ private boolean isNeverSuppressCheck(TreeWalkerAuditEvent event) {
228232
return result;
229233
}
230234

235+
/**
236+
* Apply stricter causality matching for checks configured under neverSuppressedChecks.
237+
*
238+
* <p>Most checks report violations on the same line as the change, so the existing
239+
* line matching logic works for them. However, some checks have violations whose
240+
* causality is at a broader scope than the violation line itself:
241+
*
242+
* <ul>
243+
* <li>{@code CovariantEquals} - violation is reported on method AST, but causality
244+
* is type-level (missing {@code equals(Object)} in that class), so we attribute by
245+
* enclosing type scope</li>
246+
* </ul>
247+
*
248+
* <p>For all other checks, the fallback still applies and shows violations in touched
249+
* files, so nothing is missed. As more checks with similar behavior are discovered,
250+
* they can be added here with appropriate causality attribution logic.
251+
*
252+
* @param event audit event
253+
* @return true when violation can be attributed to changed lines in touched file
254+
*/
255+
private boolean isMatchingByNeverSuppressedCheck(TreeWalkerAuditEvent event) {
256+
boolean result = false;
257+
if (isNeverSuppressCheck(event)) {
258+
result = isLineMatching(event);
259+
if (!result) {
260+
final String checkShortName = getCheckShortName(event);
261+
if (CLASS_SCOPE_NEVER_SUPPRESSED_CHECKS.contains(checkShortName)
262+
&& strategy != Strategy.NEWLINE) {
263+
result = isChangedLineInsideEnclosingType(event);
264+
}
265+
else {
266+
result = true;
267+
}
268+
}
269+
}
270+
return result;
271+
}
272+
273+
/**
274+
* Checks whether any changed line falls within the enclosing type of the event.
275+
*
276+
* @param event audit event
277+
* @return true if a changed line is inside the enclosing type scope
278+
*/
279+
private boolean isChangedLineInsideEnclosingType(TreeWalkerAuditEvent event) {
280+
final DetailAST eventAst = getEventAst(event);
281+
DetailAST scopeAst = eventAst;
282+
while (scopeAst != null && scopeAst.getType() != TokenTypes.CLASS_DEF
283+
&& scopeAst.getType() != TokenTypes.INTERFACE_DEF
284+
&& scopeAst.getType() != TokenTypes.ENUM_DEF
285+
&& scopeAst.getType() != TokenTypes.RECORD_DEF) {
286+
scopeAst = scopeAst.getParent();
287+
}
288+
return isChangedLineInAstScope(scopeAst);
289+
}
290+
291+
/**
292+
* Checks whether any changed line falls within the provided AST scope.
293+
*
294+
* @param ast enclosing AST node to evaluate
295+
* @return true if a changed line is inside the AST scope
296+
*/
297+
private boolean isChangedLineInAstScope(DetailAST ast) {
298+
boolean result = false;
299+
if (ast != null) {
300+
final Map<String, Integer> childAstLineNoMap = getChildAstLineNo(ast);
301+
final int childAstStartLine = childAstLineNoMap.get(MIN);
302+
final int childAstEndLine = childAstLineNoMap.get(MAX);
303+
result = lineMatching(childAstStartLine, childAstEndLine);
304+
}
305+
return result;
306+
}
307+
231308
/**
232309
* Is matching by line number.
233310
*

src/main/java/com/puppycrawl/tools/checkstyle/filters/SuppressionPatchFilter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ private void loadPatchFile() throws CheckstyleException {
174174
final List<List<Integer>> lineRangeList =
175175
loadPatchFileUtils.getLineRangeList();
176176
final SuppressionPatchFilterElement element =
177-
new SuppressionPatchFilterElement(fileName,
178-
lineRangeList, neverSuppressedChecks);
177+
new SuppressionPatchFilterElement(fileName, lineRangeList,
178+
neverSuppressedChecks, strategy);
179179
filters.addFilter(element);
180180
}
181181
}

src/main/java/com/puppycrawl/tools/checkstyle/filters/SuppressionPatchFilterElement.java

Lines changed: 169 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@
2020
package com.puppycrawl.tools.checkstyle.filters;
2121

2222
import java.io.File;
23+
import java.io.IOException;
24+
import java.nio.charset.StandardCharsets;
25+
import java.nio.file.Files;
26+
import java.nio.file.Paths;
2327
import java.util.List;
2428
import java.util.Set;
29+
import java.util.regex.Matcher;
30+
import java.util.regex.Pattern;
2531

2632
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
2733
import com.puppycrawl.tools.checkstyle.api.Filter;
@@ -32,6 +38,22 @@
3238
*/
3339
public final class SuppressionPatchFilterElement implements Filter {
3440

41+
/** Check name. */
42+
private static final String UNIQUE_PROPERTIES_CHECK = "UniqueProperties";
43+
44+
/** Check name. */
45+
private static final String REGEXP_MULTILINE_CHECK = "RegexpMultiline";
46+
47+
/** Suffix of check class names. */
48+
private static final String CHECK_SUFFIX = "Check";
49+
50+
/** Escaped new line sequence in violation messages. */
51+
private static final String ESCAPED_NEW_LINE = "\\n";
52+
53+
/** Message pattern to extract duplicated key from UniqueProperties violation. */
54+
private static final Pattern UNIQUE_PROPERTIES_KEY_PATTERN =
55+
Pattern.compile("Duplicated property '([^']+)'");
56+
3557
/**
3658
* The String of file names.
3759
*/
@@ -47,28 +69,32 @@ public final class SuppressionPatchFilterElement implements Filter {
4769
*/
4870
private final Set<String> neverSuppressedChecks;
4971

72+
/** Strategy used to build changed line ranges. */
73+
private final Strategy strategy;
74+
5075
/**
5176
* Constructs a {@code SuppressPatchFilterElement} for a
5277
* file name pattern.
5378
*
54-
* @param fileNameValue names of filtered files
55-
* @param lineRangeListValue list of line range for line number filtering
56-
* @param neverSuppressedChecksValue set has user defined Checks to never
57-
* suppress if files are touched
79+
* @param fileName names of filtered files
80+
* @param lineRangeList list of line range for line number filtering
81+
* @param neverSuppressedChecks set has user defined Checks to never suppress
82+
* if files are touched
83+
* @param strategy strategy used to build changed line ranges
5884
*/
59-
public SuppressionPatchFilterElement(String fileNameValue,
60-
List<List<Integer>> lineRangeListValue,
61-
Set<String> neverSuppressedChecksValue) {
62-
this.fileName = fileNameValue;
63-
this.lineRangeList = lineRangeListValue;
64-
this.neverSuppressedChecks = neverSuppressedChecksValue;
85+
public SuppressionPatchFilterElement(String fileName, List<List<Integer>> lineRangeList,
86+
Set<String> neverSuppressedChecks, Strategy strategy) {
87+
this.fileName = fileName;
88+
this.lineRangeList = lineRangeList;
89+
this.neverSuppressedChecks = neverSuppressedChecks;
90+
this.strategy = strategy;
6591
}
6692

6793
@Override
6894
public boolean accept(AuditEvent event) {
6995
return isFileNameMatching(event)
7096
&& (isTreeWalkerChecksMatching(event)
71-
|| isNeverSuppressCheck(event)
97+
|| isMatchingByNeverSuppressedCheck(event)
7298
|| isLineMatching(event));
7399
}
74100

@@ -126,14 +152,30 @@ private static boolean isTreeWalkerChecksMatching(AuditEvent event) {
126152
private boolean isLineMatching(AuditEvent event) {
127153
boolean result = false;
128154
if (event.getViolation() != null) {
129-
for (List<Integer> lineRange : lineRangeList) {
130-
final int startLine = lineRange.get(0) + 1;
131-
final int endLine = lineRange.get(1) + 1;
132-
final int currentLine = event.getLine();
155+
result = lineMatching(event.getLine());
156+
}
157+
return result;
158+
}
159+
160+
/**
161+
* Check if line intersects any changed line range.
162+
*
163+
* @param currentLine line number (1-based)
164+
* @return true if line belongs to a changed range
165+
*/
166+
private boolean lineMatching(int currentLine) {
167+
boolean result = false;
168+
for (List<Integer> lineRange : lineRangeList) {
169+
final int startLine = lineRange.get(0) + 1;
170+
final int endLine = lineRange.get(1) + 1;
171+
if (startLine == endLine) {
172+
result = currentLine == startLine;
173+
}
174+
else {
133175
result = currentLine >= startLine && currentLine < endLine;
134-
if (result) {
135-
break;
136-
}
176+
}
177+
if (result) {
178+
break;
137179
}
138180
}
139181
return result;
@@ -156,6 +198,114 @@ private boolean isNeverSuppressCheck(AuditEvent event) {
156198
return result;
157199
}
158200

201+
/**
202+
* Apply stricter causality matching for checks configured under neverSuppressedChecks.
203+
*
204+
* <p>Most checks report violations on the same line as the change, so the existing
205+
* line matching logic works for them. However, a small number of checks report
206+
* violations on a different line than the actual change, requiring special handling:
207+
*
208+
* <ul>
209+
* <li>{@code UniqueProperties} - reports on first duplicate occurrence, not necessarily
210+
* the changed line</li>
211+
* <li>{@code RegexpMultiline} - reports on first line of multi-line match, change may
212+
* be on other lines</li>
213+
* </ul>
214+
*
215+
* <p>For all other checks, the fallback still applies and shows violations in touched
216+
* files, so nothing is missed. As more checks with similar behavior are discovered,
217+
* they can be added here with appropriate causality attribution logic.
218+
*
219+
* @param event event to process
220+
* @return true when violation can be attributed to changed lines in touched file
221+
*/
222+
private boolean isMatchingByNeverSuppressedCheck(AuditEvent event) {
223+
boolean result = false;
224+
if (isNeverSuppressCheck(event)) {
225+
result = isLineMatching(event);
226+
if (!result) {
227+
final String checkShortName = getCheckShortName(event)
228+
.replaceAll(CHECK_SUFFIX, "");
229+
if (UNIQUE_PROPERTIES_CHECK.equals(checkShortName)
230+
&& strategy != Strategy.NEWLINE) {
231+
result = isUniquePropertiesViolationCausedByPatch(event);
232+
}
233+
else if (REGEXP_MULTILINE_CHECK.equals(checkShortName)
234+
&& strategy != Strategy.NEWLINE) {
235+
result = isRegexpMultilineViolationCausedByPatch(event);
236+
}
237+
else {
238+
result = true;
239+
}
240+
}
241+
}
242+
return result;
243+
}
244+
245+
/**
246+
* UniqueProperties reports first duplicate occurrence line; we attribute it to patch
247+
* when any duplicate key occurrence is touched.
248+
*
249+
* @param event event to process
250+
* @return true if changed lines contain any occurrence of duplicated key
251+
*/
252+
private boolean isUniquePropertiesViolationCausedByPatch(AuditEvent event) {
253+
boolean result = false;
254+
final String message = event.getMessage();
255+
if (message != null) {
256+
final Matcher matcher = UNIQUE_PROPERTIES_KEY_PATTERN.matcher(message);
257+
if (matcher.find()) {
258+
final String key = matcher.group(1);
259+
try {
260+
final List<String> lines = Files.readAllLines(
261+
Paths.get(event.getFileName()), StandardCharsets.UTF_8);
262+
for (int index = 0; index < lines.size(); index++) {
263+
final String line = lines.get(index).trim();
264+
if (line.startsWith(key + "=") || line.startsWith(key + " =")) {
265+
if (lineMatching(index + 1)) {
266+
result = true;
267+
break;
268+
}
269+
}
270+
}
271+
}
272+
catch (IOException ioException) {
273+
result = false;
274+
}
275+
}
276+
}
277+
return result;
278+
}
279+
280+
/**
281+
* RegexpMultiline can report the first line of a multi-line match.
282+
* We expand matching window by the number of line breaks in the pattern.
283+
*
284+
* @param event event to process
285+
* @return true if any line in match window intersects patch lines
286+
*/
287+
private boolean isRegexpMultilineViolationCausedByPatch(AuditEvent event) {
288+
final String message = event.getMessage();
289+
int linesInMatch = 1;
290+
if (message != null) {
291+
int index = message.indexOf(ESCAPED_NEW_LINE);
292+
while (index != -1) {
293+
linesInMatch++;
294+
index = message.indexOf(ESCAPED_NEW_LINE, index + ESCAPED_NEW_LINE.length());
295+
}
296+
}
297+
298+
boolean result = false;
299+
final int startLine = event.getLine();
300+
for (int lineOffset = 0; lineOffset < linesInMatch; lineOffset++) {
301+
if (lineMatching(startLine + lineOffset)) {
302+
result = true;
303+
break;
304+
}
305+
}
306+
return result;
307+
}
308+
159309
/**
160310
* Checks if the check name set contains the event's check short name.
161311
*
@@ -166,7 +316,7 @@ private boolean isNeverSuppressCheck(AuditEvent event) {
166316
private static boolean containsShortName(Set<String> checkNameSet,
167317
AuditEvent event) {
168318
final String checkShortName = getCheckShortName(event);
169-
final String shortName = checkShortName.replaceAll("Check", "");
319+
final String shortName = checkShortName.replaceAll(CHECK_SUFFIX, "");
170320
return checkNameSet.contains(checkShortName)
171321
|| checkNameSet.contains(shortName);
172322

src/test/java/com/puppycrawl/tools/checkstyle/filters/SuppressionJavaPatchFilterTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,24 @@ public void testNonExistentPatchFileWithTrueOptional() throws Exception {
7474
public void testNeverSuppressedChecks() throws Exception {
7575
testByConfig("neversuppressedchecks/CovariantEquals/"
7676
+ "checkID/context/defaultContextConfig.xml");
77+
testByConfig("neversuppressedchecks/CovariantEquals/"
78+
+ "checkID/context/case2/defaultContextConfig.xml");
7779
testByConfig("neversuppressedchecks/CovariantEquals/"
7880
+ "checkID/newline/defaultContextConfig.xml");
7981
testByConfig("neversuppressedchecks/CovariantEquals"
8082
+ "/checkID/patchedline/defaultContextConfig.xml");
83+
testByConfig("neversuppressedchecks/CovariantEquals"
84+
+ "/checkID/patchedline/case2/defaultContextConfig.xml");
8185
testByConfig("neversuppressedchecks/CovariantEquals/"
8286
+ "checkShortName/context/defaultContextConfig.xml");
87+
testByConfig("neversuppressedchecks/CovariantEquals/"
88+
+ "checkShortName/context/case2/defaultContextConfig.xml");
8389
testByConfig("neversuppressedchecks/CovariantEquals/"
8490
+ "checkShortName/newline/defaultContextConfig.xml");
8591
testByConfig("neversuppressedchecks/CovariantEquals/"
8692
+ "checkShortName/patchedline/defaultContextConfig.xml");
93+
testByConfig("neversuppressedchecks/CovariantEquals/"
94+
+ "checkShortName/patchedline/case2/defaultContextConfig.xml");
8795
}
8896

8997
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package TreeWalker.coding.CovariantEquals;
2+
3+
// patched line outside any type
4+
public class Test {
5+
public boolean equals(TreeWalker.coding.CovariantEquals.Test i) { // violation without filter
6+
return false;
7+
}
8+
9+
}

0 commit comments

Comments
 (0)