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.regexp;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.util.regex.Pattern;
25
26 import com.puppycrawl.tools.checkstyle.StatelessCheck;
27 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
28 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
29 import com.puppycrawl.tools.checkstyle.api.FileText;
30 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31
32 /**
33 * <div>
34 * Checks that a specified pattern matches based on file and/or folder path.
35 * It can also be used to verify files
36 * match specific naming patterns not covered by other checks (Ex: properties,
37 * xml, etc.).
38 * </div>
39 *
40 * <p>
41 * When customizing the check, the properties are applied in a specific order.
42 * The fileExtensions property first picks only files that match any of the
43 * specific extensions supplied. Once files are matched against the
44 * fileExtensions, the match property is then used in conjunction with the
45 * patterns to determine if the check is looking for a match or mismatch on
46 * those files. If the fileNamePattern is supplied, the matching is only applied
47 * to the fileNamePattern and not the folderPattern. If no fileNamePattern is
48 * supplied, then matching is applied to the folderPattern only and will result
49 * in all files in a folder to be reported on violations. If no folderPattern is
50 * supplied, then all folders that checkstyle finds are examined for violations.
51 * The ignoreFileNameExtensions property drops the file extension and applies
52 * the fileNamePattern only to the rest of file name. For example, if the file
53 * is named 'test.java' and this property is turned on, the pattern is only
54 * applied to 'test'.
55 * </p>
56 *
57 * <p>
58 * If this check is configured with no properties, then the default behavior of
59 * this check is to report file names with spaces in them. When at least one
60 * pattern property is supplied, the entire check is under the user's control to
61 * allow them to fully customize the behavior.
62 * </p>
63 *
64 * <p>
65 * It is recommended that if you create your own pattern, to also specify a
66 * custom violation message. This allows the violation message printed to be clear what
67 * the violation is, especially if multiple RegexpOnFilename checks are used.
68 * Argument 0 for the message populates the check's folderPattern. Argument 1
69 * for the message populates the check's fileNamePattern. The file name is not
70 * passed as an argument since it is part of CheckStyle's default violation
71 * messages.
72 * </p>
73 *
74 * @since 6.15
75 */
76 @StatelessCheck
77 public class RegexpOnFilenameCheck extends AbstractFileSetCheck {
78
79 /**
80 * A key is pointing to the warning message text in "messages.properties"
81 * file.
82 */
83 public static final String MSG_MATCH = "regexp.filename.match";
84 /**
85 * A key is pointing to the warning message text in "messages.properties"
86 * file.
87 */
88 public static final String MSG_MISMATCH = "regexp.filename.mismatch";
89
90 /** Specify the regular expression to match the folder path against. */
91 private Pattern folderPattern;
92 /** Specify the regular expression to match the file name against. */
93 private Pattern fileNamePattern;
94 /**
95 * Control whether to look for a match or mismatch on the file name,
96 * if the fileNamePattern is supplied, otherwise it is applied on the folderPattern.
97 */
98 private boolean match = true;
99 /** Control whether to ignore the file extension for the file name match. */
100 private boolean ignoreFileNameExtensions;
101
102 /**
103 * Creates a new {@code RegexpOnFilenameCheck} instance.
104 */
105 public RegexpOnFilenameCheck() {
106 // no code by default
107 }
108
109 /**
110 * Setter to specify the regular expression to match the folder path against.
111 *
112 * @param folderPattern format of folder.
113 * @since 6.15
114 */
115 public void setFolderPattern(Pattern folderPattern) {
116 this.folderPattern = folderPattern;
117 }
118
119 /**
120 * Setter to specify the regular expression to match the file name against.
121 *
122 * @param fileNamePattern format of file.
123 * @since 6.15
124 */
125 public void setFileNamePattern(Pattern fileNamePattern) {
126 this.fileNamePattern = fileNamePattern;
127 }
128
129 /**
130 * Setter to control whether to look for a match or mismatch on the file name,
131 * if the fileNamePattern is supplied, otherwise it is applied on the folderPattern.
132 *
133 * @param match check's option for matching file names.
134 * @since 6.15
135 */
136 public void setMatch(boolean match) {
137 this.match = match;
138 }
139
140 /**
141 * Setter to control whether to ignore the file extension for the file name match.
142 *
143 * @param ignoreFileNameExtensions check's option for ignoring file extension.
144 * @since 6.15
145 */
146 public void setIgnoreFileNameExtensions(boolean ignoreFileNameExtensions) {
147 this.ignoreFileNameExtensions = ignoreFileNameExtensions;
148 }
149
150 @Override
151 public void init() {
152 if (fileNamePattern == null && folderPattern == null) {
153 fileNamePattern = CommonUtil.createPattern("\\s");
154 }
155 }
156
157 @Override
158 protected void processFiltered(File file, FileText fileText) throws CheckstyleException {
159 final String fileName = getFileName(file);
160 final String folderPath = getFolderPath(file);
161
162 if (isMatchFolder(folderPath) && isMatchFile(fileName)) {
163 log();
164 }
165 }
166
167 /**
168 * Retrieves the file name from the given {@code file}.
169 *
170 * @param file Input file to examine.
171 * @return The file name.
172 */
173 private String getFileName(File file) {
174 String fileName = file.getName();
175
176 if (ignoreFileNameExtensions) {
177 fileName = CommonUtil.getFileNameWithoutExtension(fileName);
178 }
179
180 return fileName;
181 }
182
183 /**
184 * Retrieves the folder path from the given {@code file}.
185 *
186 * @param file Input file to examine.
187 * @return The folder path.
188 * @throws CheckstyleException if there is an error getting the canonical
189 * path of the {@code file}.
190 */
191 private static String getFolderPath(File file) throws CheckstyleException {
192 try {
193 return file.getCanonicalFile().getParent();
194 }
195 catch (IOException exc) {
196 throw new CheckstyleException("unable to create canonical path names for "
197 + file.getAbsolutePath(), exc);
198 }
199 }
200
201 /**
202 * Checks if the given {@code folderPath} matches the specified
203 * {@link #folderPattern}.
204 *
205 * @param folderPath Input folder path to examine.
206 * @return true if they do match.
207 */
208 private boolean isMatchFolder(String folderPath) {
209 final boolean result;
210
211 // null pattern always matches, regardless of value of 'match'
212 if (folderPattern == null) {
213 result = true;
214 }
215 else {
216 // null pattern means 'match' applies to the folderPattern matching
217 final boolean useMatch = fileNamePattern != null || match;
218 result = folderPattern.matcher(folderPath).find() == useMatch;
219 }
220
221 return result;
222 }
223
224 /**
225 * Checks if the given {@code fileName} matches the specified
226 * {@link #fileNamePattern}.
227 *
228 * @param fileName Input file name to examine.
229 * @return true if they do match.
230 */
231 private boolean isMatchFile(String fileName) {
232 // null pattern always matches, regardless of value of 'match'
233 return fileNamePattern == null || fileNamePattern.matcher(fileName).find() == match;
234 }
235
236 /** Logs the violations for the check. */
237 private void log() {
238 final String folder = getStringOrDefault(folderPattern, "");
239 final String fileName = getStringOrDefault(fileNamePattern, "");
240
241 if (match) {
242 log(1, MSG_MATCH, folder, fileName);
243 }
244 else {
245 log(1, MSG_MISMATCH, folder, fileName);
246 }
247 }
248
249 /**
250 * Retrieves the String form of the {@code pattern} or {@code defaultString}
251 * if null.
252 *
253 * @param pattern The pattern to convert.
254 * @param defaultString The result to use if {@code pattern} is null.
255 * @return The String form of the {@code pattern}.
256 */
257 private static String getStringOrDefault(Pattern pattern, String defaultString) {
258 final String result;
259
260 if (pattern == null) {
261 result = defaultString;
262 }
263 else {
264 result = pattern.toString();
265 }
266
267 return result;
268 }
269
270 }