001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks; 021 022import java.io.File; 023import java.io.InputStream; 024import java.io.Serial; 025import java.nio.file.Files; 026import java.util.HashMap; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.Properties; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import com.puppycrawl.tools.checkstyle.StatelessCheck; 034import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck; 035import com.puppycrawl.tools.checkstyle.api.FileText; 036 037/** 038 * <div> 039 * Detects duplicated keys in properties files. 040 * </div> 041 * 042 * <p> 043 * Rationale: Multiple property keys usually appear after merge or rebase of 044 * several branches. While there are no problems in runtime, there can be a confusion 045 * due to having different values for the duplicated properties. 046 * </p> 047 * 048 * @since 5.7 049 */ 050@StatelessCheck 051public class UniquePropertiesCheck extends AbstractFileSetCheck { 052 053 /** 054 * Localization key for check violation. 055 */ 056 public static final String MSG_KEY = "properties.duplicate.property"; 057 /** 058 * Localization key for IO exception occurred on file open. 059 */ 060 public static final String MSG_IO_EXCEPTION_KEY = "unable.open.cause"; 061 062 /** 063 * Pattern matching single space. 064 */ 065 private static final Pattern SPACE_PATTERN = Pattern.compile(" "); 066 067 /** 068 * Compiled pattern for all system newlines. 069 */ 070 private static final Pattern ALL_NEW_LINES = Pattern.compile("\\R"); 071 072 /** 073 * Construct the check with default values. 074 */ 075 public UniquePropertiesCheck() { 076 setFileExtensions("properties"); 077 } 078 079 /** 080 * Setter to specify the file extensions of the files to process. 081 * 082 * @param extensions the set of file extensions. A missing 083 * initial '.' character of an extension is automatically added. 084 * @throws IllegalArgumentException is argument is null 085 */ 086 @Override 087 public final void setFileExtensions(String... extensions) { 088 super.setFileExtensions(extensions); 089 } 090 091 @Override 092 protected void processFiltered(File file, FileText fileText) { 093 final UniqueProperties properties = new UniqueProperties(); 094 try (InputStream inputStream = Files.newInputStream(file.toPath())) { 095 properties.load(inputStream); 096 } 097 // -@cs[IllegalCatch] Properties.load can throw unchecked exceptions such as 098 // IllegalArgumentException on a malformed unicode escape, so any exception 099 // from reading the file is reported rather than aborting the audit. 100 catch (final Exception exc) { 101 log(1, MSG_IO_EXCEPTION_KEY, file.getPath(), 102 exc.getLocalizedMessage()); 103 } 104 105 for (Entry<String, Integer> duplication : properties 106 .getDuplicatedKeys().entrySet()) { 107 final String keyName = duplication.getKey(); 108 final int lineNumber = getLineNumber(fileText, keyName); 109 // Number of occurrences is number of duplications + 1 110 log(lineNumber, MSG_KEY, 111 ALL_NEW_LINES.matcher(keyName).replaceAll("\\\\n"), 112 duplication.getValue() + 1); 113 } 114 } 115 116 /** 117 * Method returns line number the key is detected in the checked properties files first. 118 * 119 * @param fileText 120 * {@link FileText} object contains the lines to process 121 * @param keyName 122 * key name to look for 123 * @return line number of first occurrence. If no key found in properties file, 1 is returned 124 */ 125 private static int getLineNumber(FileText fileText, String keyName) { 126 final Pattern keyPattern = getKeyPattern(keyName); 127 int lineNumber = 1; 128 final Matcher matcher = keyPattern.matcher(""); 129 for (int index = 0; index < fileText.size(); index++) { 130 final String line = fileText.get(index); 131 matcher.reset(line); 132 if (matcher.matches()) { 133 break; 134 } 135 ++lineNumber; 136 } 137 // -1 as check seeks for the first duplicate occurrence in file, 138 // so it cannot be the last line. 139 if (lineNumber > fileText.size() - 1) { 140 lineNumber = 1; 141 } 142 return lineNumber; 143 } 144 145 /** 146 * Method returns regular expression pattern given key name. 147 * 148 * @param keyName 149 * key name to look for 150 * @return regular expression pattern given key name 151 */ 152 private static Pattern getKeyPattern(String keyName) { 153 final String keyNameWithEscapedSpaces = SPACE_PATTERN.matcher(keyName) 154 .replaceAll(Matcher.quoteReplacement("\\ ")); 155 final String keyPatternString = "^" 156 + Pattern.quote(keyNameWithEscapedSpaces) 157 + "[\\s:=].*$"; 158 return Pattern.compile(keyPatternString); 159 } 160 161 /** 162 * Properties subclass to store duplicated property keys in a separate map. 163 * 164 * @noinspection ClassExtendsConcreteCollection 165 * @noinspectionreason ClassExtendsConcreteCollection - we require custom 166 * {@code put} method to find duplicate keys 167 */ 168 private static final class UniqueProperties extends Properties { 169 170 /** A unique serial version identifier. */ 171 @Serial 172 private static final long serialVersionUID = 1L; 173 /** 174 * Map, holding duplicated keys and their count. Keys are added here only if they 175 * already exist in Properties' inner map. 176 */ 177 private final Map<String, Integer> duplicatedKeys = new HashMap<>(); 178 179 /** 180 * Creates a new {@code UniqueProperties} instance. 181 */ 182 private UniqueProperties() { 183 // no code by default 184 } 185 186 /** 187 * Puts the value into properties by the key specified. 188 * 189 * @noinspection UseOfPropertiesAsHashtable 190 * @noinspectionreason We must override put to intercept properties loading. 191 */ 192 @Override 193 public synchronized Object put(Object key, Object value) { 194 final Object oldValue = super.put(key, value); 195 if (oldValue != null && key instanceof final String keyString) { 196 197 duplicatedKeys.put(keyString, 198 duplicatedKeys.getOrDefault(keyString, 0) + 1); 199 } 200 return oldValue; 201 } 202 203 /** 204 * Retrieves a collections of duplicated properties keys. 205 * 206 * @return A collection of duplicated keys. 207 */ 208 /* package */ Map<String, Integer> getDuplicatedKeys() { 209 return new HashMap<>(duplicatedKeys); 210 } 211 212 } 213 214}