1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.api;
21
22 import java.text.MessageFormat;
23 import java.util.Arrays;
24 import java.util.Locale;
25 import java.util.Objects;
26
27 import javax.annotation.Nullable;
28
29 import com.puppycrawl.tools.checkstyle.LocalizedMessage;
30 import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;
31
32 /**
33 * Represents a violation that can be localised. The translations come from
34 * message.properties files. The underlying implementation uses java.text.MessageFormat.
35 *
36 * @noinspection ClassWithTooManyConstructors
37 * @noinspectionreason ClassWithTooManyConstructors - immutable nature of class requires a
38 * bunch of constructors
39 */
40 public final class Violation implements Comparable<Violation> {
41
42 /** The default severity level if one is not specified. */
43 private static final SeverityLevel DEFAULT_SEVERITY = SeverityLevel.ERROR;
44
45 /** The line number. */
46 private final int lineNo;
47 /** The column number. */
48 private final int columnNo;
49 /** The column char index. */
50 private final int columnCharIndex;
51 /** The token type constant. See {@link TokenTypes}. */
52 private final int tokenType;
53
54 /** The severity level. */
55 private final SeverityLevel severityLevel;
56
57 /** The id of the module generating the violation. */
58 private final String moduleId;
59
60 /** Key for the violation format. */
61 private final String key;
62
63 /** Arguments for MessageFormat. */
64 private final Object[] args;
65
66 /** Name of the resource bundle to get violations from. */
67 private final String bundle;
68
69 /** Class of the source for this Violation. */
70 private final Class<?> sourceClass;
71
72 /** A custom violation overriding the default violation from the bundle. */
73 @Nullable
74 private final String customMessage;
75
76 /**
77 * Creates a new {@code Violation} instance. The column number defaults to 0.
78 *
79 * @param lineNo line number associated with the violation
80 * @param bundle name of a resource bundle that contains audit event violations
81 * @param key the key to locate the translation
82 * @param args arguments for the translation
83 * @param moduleId the id of the module the violation is associated with
84 * @param sourceClass the name of the source for the violation
85 * @param customMessage optional custom violation overriding the default
86 */
87 public Violation(
88 int lineNo,
89 String bundle,
90 String key,
91 Object[] args,
92 String moduleId,
93 Class<?> sourceClass,
94 @Nullable String customMessage) {
95 this(lineNo, 0, bundle, key, args, DEFAULT_SEVERITY, moduleId,
96 sourceClass, customMessage);
97 }
98
99 /**
100 * Creates a new {@code Violation} instance.
101 *
102 * @param lineNo line number associated with the violation
103 * @param columnNo column number associated with the violation
104 * @param bundle resource bundle name
105 * @param key the key to locate the translation
106 * @param args arguments for the translation
107 * @param moduleId the id of the module the violation is associated with
108 * @param sourceClass the Class that is the source of the violation
109 * @param customMessage optional custom violation overriding the default
110 * @noinspection ConstructorWithTooManyParameters
111 * @noinspectionreason ConstructorWithTooManyParameters - immutable class requires a large
112 * number of arguments
113 */
114 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
115 public Violation(int lineNo,
116 int columnNo,
117 String bundle,
118 String key,
119 Object[] args,
120 String moduleId,
121 Class<?> sourceClass,
122 @Nullable String customMessage) {
123 this(lineNo,
124 columnNo,
125 bundle,
126 key,
127 args,
128 DEFAULT_SEVERITY,
129 moduleId,
130 sourceClass,
131 customMessage);
132 }
133
134 /**
135 * Creates a new {@code Violation} instance.
136 *
137 * @param lineNo line number associated with the violation
138 * @param bundle resource bundle name
139 * @param key the key to locate the translation
140 * @param args arguments for the translation
141 * @param severityLevel severity level for the violation
142 * @param moduleId the id of the module the violation is associated with
143 * @param sourceClass the source class for the violation
144 * @param customMessage optional custom violation overriding the default
145 * @noinspection ConstructorWithTooManyParameters
146 * @noinspectionreason ConstructorWithTooManyParameters - immutable class requires a large
147 * number of arguments
148 */
149 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
150 public Violation(int lineNo,
151 String bundle,
152 String key,
153 Object[] args,
154 SeverityLevel severityLevel,
155 String moduleId,
156 Class<?> sourceClass,
157 @Nullable String customMessage) {
158 this(lineNo, 0, bundle, key, args, severityLevel, moduleId,
159 sourceClass, customMessage);
160 }
161
162 /**
163 * Creates a new {@code Violation} instance.
164 *
165 * @param lineNo line number associated with the violation
166 * @param columnNo column number associated with the violation
167 * @param bundle resource bundle name
168 * @param key the key to locate the translation
169 * @param args arguments for the translation
170 * @param severityLevel severity level for the violation
171 * @param moduleId the id of the module the violation is associated with
172 * @param sourceClass the Class that is the source of the violation
173 * @param customMessage optional custom violation overriding the default
174 * @noinspection ConstructorWithTooManyParameters
175 * @noinspectionreason ConstructorWithTooManyParameters - immutable class requires a large
176 * number of arguments
177 */
178 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
179 public Violation(int lineNo,
180 int columnNo,
181 String bundle,
182 String key,
183 Object[] args,
184 SeverityLevel severityLevel,
185 String moduleId,
186 Class<?> sourceClass,
187 @Nullable String customMessage) {
188 this(lineNo, columnNo, 0, bundle, key, args, severityLevel, moduleId, sourceClass,
189 customMessage);
190 }
191
192 /**
193 * Creates a new {@code Violation} instance.
194 *
195 * @param lineNo line number associated with the violation
196 * @param columnNo column number associated with the violation
197 * @param tokenType token type of the event associated with violation. See {@link TokenTypes}
198 * @param bundle resource bundle name
199 * @param key the key to locate the translation
200 * @param args arguments for the translation
201 * @param severityLevel severity level for the violation
202 * @param moduleId the id of the module the violation is associated with
203 * @param sourceClass the Class that is the source of the violation
204 * @param customMessage optional custom violation overriding the default
205 * @noinspection ConstructorWithTooManyParameters
206 * @noinspectionreason ConstructorWithTooManyParameters - immutable class requires a large
207 * number of arguments
208 */
209 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
210 public Violation(int lineNo,
211 int columnNo,
212 int tokenType,
213 String bundle,
214 String key,
215 Object[] args,
216 SeverityLevel severityLevel,
217 String moduleId,
218 Class<?> sourceClass,
219 @Nullable String customMessage) {
220 this(lineNo, columnNo, columnNo, tokenType, bundle, key, args, severityLevel, moduleId,
221 sourceClass, customMessage);
222 }
223
224 /**
225 * Creates a new {@code Violation} instance.
226 *
227 * @param lineNo line number associated with the violation
228 * @param columnNo column number associated with the violation
229 * @param columnCharIndex column char index associated with the violation
230 * @param tokenType token type of the event associated with violation. See {@link TokenTypes}
231 * @param bundle resource bundle name
232 * @param key the key to locate the translation
233 * @param args arguments for the translation
234 * @param severityLevel severity level for the violation
235 * @param moduleId the id of the module the violation is associated with
236 * @param sourceClass the Class that is the source of the violation
237 * @param customMessage optional custom violation overriding the default
238 * @noinspection ConstructorWithTooManyParameters
239 * @noinspectionreason ConstructorWithTooManyParameters - immutable class requires a large
240 * number of arguments
241 */
242 // -@cs[ParameterNumber] Class is immutable, we need that amount of arguments.
243 public Violation(int lineNo,
244 int columnNo,
245 int columnCharIndex,
246 int tokenType,
247 String bundle,
248 String key,
249 Object[] args,
250 SeverityLevel severityLevel,
251 String moduleId,
252 Class<?> sourceClass,
253 @Nullable String customMessage) {
254 this.lineNo = lineNo;
255 this.columnNo = columnNo;
256 this.columnCharIndex = columnCharIndex;
257 this.tokenType = tokenType;
258 this.key = key;
259
260 if (args == null) {
261 this.args = null;
262 }
263 else {
264 this.args = UnmodifiableCollectionUtil.copyOfArray(args, args.length);
265 }
266 this.bundle = bundle;
267 this.severityLevel = severityLevel;
268 this.moduleId = moduleId;
269 this.sourceClass = sourceClass;
270 this.customMessage = customMessage;
271 }
272
273 /**
274 * Gets the line number.
275 *
276 * @return the line number
277 */
278 public int getLineNo() {
279 return lineNo;
280 }
281
282 /**
283 * Gets the column number.
284 *
285 * @return the column number
286 */
287 public int getColumnNo() {
288 return columnNo;
289 }
290
291 /**
292 * Gets the column char index.
293 *
294 * @return the column char index
295 */
296 public int getColumnCharIndex() {
297 return columnCharIndex;
298 }
299
300 /**
301 * Gets the token type.
302 *
303 * @return the token type
304 */
305 public int getTokenType() {
306 return tokenType;
307 }
308
309 /**
310 * Gets the severity level.
311 *
312 * @return the severity level
313 */
314 public SeverityLevel getSeverityLevel() {
315 return severityLevel;
316 }
317
318 /**
319 * Returns id of module.
320 *
321 * @return the module identifier.
322 */
323 public String getModuleId() {
324 return moduleId;
325 }
326
327 /**
328 * Returns the violation key to locate the translation, can also be used
329 * in IDE plugins to map audit event violations to corrective actions.
330 *
331 * @return the violation key
332 */
333 public String getKey() {
334 return key;
335 }
336
337 /**
338 * Gets the name of the source for this Violation.
339 *
340 * @return the name of the source for this Violation
341 */
342 public String getSourceName() {
343 return sourceClass.getName();
344 }
345
346 /**
347 * Indicates whether some other object is "equal to" this one.
348 * Suppression on enumeration is needed so code stays consistent.
349 *
350 * @noinspection EqualsCalledOnEnumConstant
351 * @noinspectionreason EqualsCalledOnEnumConstant - enumeration is needed to keep
352 * code consistent
353 */
354 // -@cs[CyclomaticComplexity] equals - a lot of fields to check.
355 @Override
356 @SuppressWarnings("OverlyComplexBooleanExpression")
357 public boolean equals(Object object) {
358 if (this == object) {
359 return true;
360 }
361 if (object == null || getClass() != object.getClass()) {
362 return false;
363 }
364 final Violation violation = (Violation) object;
365 return lineNo == violation.lineNo
366 && columnNo == violation.columnNo
367 && columnCharIndex == violation.columnCharIndex
368 && tokenType == violation.tokenType
369 && Objects.equals(severityLevel, violation.severityLevel)
370 && Objects.equals(moduleId, violation.moduleId)
371 && Objects.equals(key, violation.key)
372 && Objects.equals(bundle, violation.bundle)
373 && Objects.equals(sourceClass, violation.sourceClass)
374 && Objects.equals(customMessage, violation.customMessage)
375 && Arrays.equals(args, violation.args);
376 }
377
378 @Override
379 public int hashCode() {
380 return Objects.hash(lineNo, columnNo, columnCharIndex, tokenType, severityLevel, moduleId,
381 key, bundle, sourceClass, customMessage, Arrays.hashCode(args));
382 }
383
384 ////////////////////////////////////////////////////////////////////////////
385 // Interface Comparable methods
386 ////////////////////////////////////////////////////////////////////////////
387
388 /**
389 * {@inheritDoc}
390 *
391 * <p>The same fields as in {@link #equals(Object)} take part in the
392 * comparison, so that a result of zero is consistent with equality, as the
393 * {@link Comparable} contract recommends. Line, column, module id, source
394 * class, and the rendered violation stay the leading criteria; the
395 * remaining fields only break a tie between violations that would
396 * otherwise be reported at the same place with the same text.
397 */
398 @Override
399 public int compareTo(Violation other) {
400 int result = compareLocation(other);
401 if (result == 0) {
402 result = compareContent(other);
403 }
404 return result;
405 }
406
407 /**
408 * Compares the place and the text a violation is reported with.
409 *
410 * @param other the violation to compare against
411 * @return the value {@code 0} if the violations are reported the same way
412 */
413 private int compareLocation(Violation other) {
414 int result = Integer.compare(lineNo, other.lineNo);
415 if (result == 0) {
416 result = Integer.compare(columnNo, other.columnNo);
417 }
418 if (result == 0) {
419 result = compareNullable(moduleId, other.moduleId);
420 }
421 if (result == 0) {
422 result = compareNullable(getSourceClassName(), other.getSourceClassName());
423 }
424 if (result == 0) {
425 result = getViolation().compareTo(other.getViolation());
426 }
427 return result;
428 }
429
430 /**
431 * Compares the fields that two violations reported at the same place with
432 * the same text can still differ in.
433 *
434 * @param other the violation to compare against
435 * @return the value {@code 0} if the violations carry the same content
436 */
437 private int compareContent(Violation other) {
438 int result = Integer.compare(columnCharIndex, other.columnCharIndex);
439 if (result == 0) {
440 result = Integer.compare(tokenType, other.tokenType);
441 }
442 if (result == 0) {
443 result = severityLevel.compareTo(other.severityLevel);
444 }
445 if (result == 0) {
446 result = compareNullable(key, other.key);
447 }
448 if (result == 0) {
449 result = compareNullable(bundle, other.bundle);
450 }
451 if (result == 0) {
452 result = compareNullable(customMessage, other.customMessage);
453 }
454 if (result == 0) {
455 result = Arrays.deepToString(args).compareTo(Arrays.deepToString(other.args));
456 }
457 return result;
458 }
459
460 /**
461 * Compares two values that may be {@code null}, ordering {@code null} before any other value.
462 *
463 * @param first the first value
464 * @param second the second value
465 * @return a negative integer, zero, or a positive integer as the first
466 * value is less than, equal to, or greater than the second
467 */
468 private static int compareNullable(@Nullable String first, @Nullable String second) {
469 final int result;
470 if (first == null) {
471 if (second == null) {
472 result = 0;
473 }
474 else {
475 result = -1;
476 }
477 }
478 else if (second == null) {
479 result = 1;
480 }
481 else {
482 result = first.compareTo(second);
483 }
484 return result;
485 }
486
487 /**
488 * Gets the source class name or {@code null} when no source class is set.
489 *
490 * @return the source class name or {@code null}
491 */
492 @Nullable
493 private String getSourceClassName() {
494 final String name;
495 if (sourceClass == null) {
496 name = null;
497 }
498 else {
499 name = sourceClass.getName();
500 }
501 return name;
502 }
503
504 /**
505 * Gets the translated violation.
506 *
507 * @return the translated violation
508 */
509 public String getViolation() {
510 final String violation;
511
512 if (customMessage != null) {
513 violation = new MessageFormat(customMessage, Locale.ROOT).format(args);
514 }
515 else {
516 violation = new LocalizedMessage(bundle, sourceClass, key, args).getMessage();
517 }
518
519 return violation;
520 }
521
522 }