2020package com .puppycrawl .tools .checkstyle .filters ;
2121
2222import 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 ;
2327import java .util .List ;
2428import java .util .Set ;
29+ import java .util .regex .Matcher ;
30+ import java .util .regex .Pattern ;
2531
2632import com .puppycrawl .tools .checkstyle .api .AuditEvent ;
2733import com .puppycrawl .tools .checkstyle .api .Filter ;
3238 */
3339public 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
0 commit comments