1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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.checks;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.Serial;
26 import java.nio.file.Files;
27 import java.util.HashMap;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.Properties;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
33
34 import com.puppycrawl.tools.checkstyle.StatelessCheck;
35 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
36 import com.puppycrawl.tools.checkstyle.api.FileText;
37
38 /**
39 * <div>
40 * Detects duplicated keys in properties files.
41 * </div>
42 *
43 * <p>
44 * Rationale: Multiple property keys usually appear after merge or rebase of
45 * several branches. While there are no problems in runtime, there can be a confusion
46 * due to having different values for the duplicated properties.
47 * </p>
48 *
49 * @since 5.7
50 */
51 @StatelessCheck
52 public class UniquePropertiesCheck extends AbstractFileSetCheck {
53
54 /**
55 * Localization key for check violation.
56 */
57 public static final String MSG_KEY = "properties.duplicate.property";
58 /**
59 * Localization key for IO exception occurred on file open.
60 */
61 public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause";
62
63 /**
64 * Pattern matching single space.
65 */
66 private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
67
68 /**
69 * Construct the check with default values.
70 */
71 public UniquePropertiesCheck() {
72 setFileExtensions("properties");
73 }
74
75 /**
76 * Setter to specify the file extensions of the files to process.
77 *
78 * @param extensions the set of file extensions. A missing
79 * initial '.' character of an extension is automatically added.
80 * @throws IllegalArgumentException is argument is null
81 */
82 @Override
83 public final void setFileExtensions(String... extensions) {
84 super.setFileExtensions(extensions);
85 }
86
87 @Override
88 protected void processFiltered(File file, FileText fileText) {
89 final UniqueProperties properties = new UniqueProperties();
90 try (InputStream inputStream = Files.newInputStream(file.toPath())) {
91 properties.load(inputStream);
92 }
93 catch (IOException exc) {
94 log(1, MSG_IO_EXCEPTION_KEY, file.getPath(),
95 exc.getLocalizedMessage());
96 }
97
98 for (Entry<String, Integer> duplication : properties
99 .getDuplicatedKeys().entrySet()) {
100 final String keyName = duplication.getKey();
101 final int lineNumber = getLineNumber(fileText, keyName);
102 // Number of occurrences is number of duplications + 1
103 log(lineNumber, MSG_KEY, keyName, duplication.getValue() + 1);
104 }
105 }
106
107 /**
108 * Method returns line number the key is detected in the checked properties
109 * files first.
110 *
111 * @param fileText
112 * {@link FileText} object contains the lines to process
113 * @param keyName
114 * key name to look for
115 * @return line number of first occurrence. If no key found in properties
116 * file, 1 is returned
117 */
118 private static int getLineNumber(FileText fileText, String keyName) {
119 final Pattern keyPattern = getKeyPattern(keyName);
120 int lineNumber = 1;
121 final Matcher matcher = keyPattern.matcher("");
122 for (int index = 0; index < fileText.size(); index++) {
123 final String line = fileText.get(index);
124 matcher.reset(line);
125 if (matcher.matches()) {
126 break;
127 }
128 ++lineNumber;
129 }
130 // -1 as check seeks for the first duplicate occurrence in file,
131 // so it cannot be the last line.
132 if (lineNumber > fileText.size() - 1) {
133 lineNumber = 1;
134 }
135 return lineNumber;
136 }
137
138 /**
139 * Method returns regular expression pattern given key name.
140 *
141 * @param keyName
142 * key name to look for
143 * @return regular expression pattern given key name
144 */
145 private static Pattern getKeyPattern(String keyName) {
146 final String keyPatternString = "^" + SPACE_PATTERN.matcher(keyName)
147 .replaceAll(Matcher.quoteReplacement("\\\\ ")) + "[\\s:=].*$";
148 return Pattern.compile(keyPatternString);
149 }
150
151 /**
152 * Properties subclass to store duplicated property keys in a separate map.
153 *
154 * @noinspection ClassExtendsConcreteCollection
155 * @noinspectionreason ClassExtendsConcreteCollection - we require custom
156 * {@code put} method to find duplicate keys
157 */
158 private static final class UniqueProperties extends Properties {
159
160 /** A unique serial version identifier. */
161 @Serial
162 private static final long serialVersionUID = 1L;
163 /**
164 * Map, holding duplicated keys and their count. Keys are added here only if they
165 * already exist in Properties' inner map.
166 */
167 private final Map<String, Integer> duplicatedKeys = new HashMap<>();
168
169 /**
170 * Puts the value into properties by the key specified.
171 */
172 @Override
173 public synchronized Object put(Object key, Object value) {
174 final Object oldValue = super.put(key, value);
175 if (oldValue != null && key instanceof String keyString) {
176
177 duplicatedKeys.put(keyString,
178 duplicatedKeys.getOrDefault(keyString, 0) + 1);
179 }
180 return oldValue;
181 }
182
183 /**
184 * Retrieves a collections of duplicated properties keys.
185 *
186 * @return A collection of duplicated keys.
187 */
188 public Map<String, Integer> getDuplicatedKeys() {
189 return new HashMap<>(duplicatedKeys);
190 }
191
192 }
193
194 }