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