View Javadoc
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.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 keyNameWithEscapedSpaces = SPACE_PATTERN.matcher(keyName)
147                 .replaceAll(Matcher.quoteReplacement("\\ "));
148         final String keyPatternString = "^"
149                 + Pattern.quote(keyNameWithEscapedSpaces)
150                 + "[\\s:=].*$";
151         return Pattern.compile(keyPatternString);
152     }
153 
154     /**
155      * Properties subclass to store duplicated property keys in a separate map.
156      *
157      * @noinspection ClassExtendsConcreteCollection
158      * @noinspectionreason ClassExtendsConcreteCollection - we require custom
159      *      {@code put} method to find duplicate keys
160      */
161     private static final class UniqueProperties extends Properties {
162 
163         /** A unique serial version identifier. */
164         @Serial
165         private static final long serialVersionUID = 1L;
166         /**
167          * Map, holding duplicated keys and their count. Keys are added here only if they
168          * already exist in Properties' inner map.
169          */
170         private final Map<String, Integer> duplicatedKeys = new HashMap<>();
171 
172         /**
173          * Creates a new {@code UniqueProperties} instance.
174          */
175         private UniqueProperties() {
176             // no code by default
177         }
178 
179         /**
180          * Puts the value into properties by the key specified.
181          *
182          * @noinspection UseOfPropertiesAsHashtable
183          * @noinspectionreason We must override put to intercept properties loading.
184          */
185         @Override
186         public synchronized Object put(Object key, Object value) {
187             final Object oldValue = super.put(key, value);
188             if (oldValue != null && key instanceof String keyString) {
189 
190                 duplicatedKeys.put(keyString,
191                         duplicatedKeys.getOrDefault(keyString, 0) + 1);
192             }
193             return oldValue;
194         }
195 
196         /**
197          * Retrieves a collections of duplicated properties keys.
198          *
199          * @return A collection of duplicated keys.
200          */
201         /* package */ Map<String, Integer> getDuplicatedKeys() {
202             return new HashMap<>(duplicatedKeys);
203         }
204 
205     }
206 
207 }