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.imports;
21
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.regex.Pattern;
25
26 /**
27 * Represents a tree of import rules for a specific package.
28 * Each instance may have zero or more children. A child may
29 * be a sub-package, a class, or an allow/disallow rule.
30 */
31 public class PkgImportControl extends AbstractImportControl {
32 /** The package separator: ".". */
33 private static final String DOT = ".";
34
35 /** The regex for the package separator: "\\.". */
36 private static final String DOT_REGEX = "\\.";
37
38 /** A pattern matching the package separator: "\.". */
39 private static final Pattern DOT_REGEX_PATTERN = Pattern.compile(DOT_REGEX);
40
41 /** The regex for the escaped package separator: "\\\\.". */
42 private static final String DOT_ESCAPED_REGEX = "\\\\.";
43
44 /** List of children {@link AbstractImportControl} objects. */
45 private final List<AbstractImportControl> children = new ArrayList<>();
46
47 /** The full name for the package. */
48 private final String fullPackageName;
49 /**
50 * The regex pattern for partial match (exact and for subpackages) - only not
51 * null if regex is true.
52 */
53 private final Pattern patternForPartialMatch;
54 /** The regex pattern for exact matches - only not null if regex is true. */
55 private final Pattern patternForExactMatch;
56 /** If this package represents a regular expression. */
57 private final boolean regex;
58
59 /**
60 * Construct a root, package node.
61 *
62 * @param packageName the name of the package.
63 * @param regex flags interpretation of name as regex pattern.
64 * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found.
65 */
66 /* package */ PkgImportControl(String packageName, boolean regex,
67 MismatchStrategy strategyOnMismatch) {
68 super(null, strategyOnMismatch);
69
70 this.regex = regex;
71 if (regex) {
72 // ensure that fullName is a self-contained regular expression
73 fullPackageName = encloseInGroup(packageName);
74 patternForPartialMatch = createPatternForPartialMatch(fullPackageName);
75 patternForExactMatch = createPatternForExactMatch(fullPackageName);
76 }
77 else {
78 fullPackageName = packageName;
79 patternForPartialMatch = null;
80 patternForExactMatch = null;
81 }
82 }
83
84 /**
85 * Construct a sub-package node. The concatenation of regular expressions needs special care:
86 * see {@link #ensureSelfContainedRegex(String, boolean)} for more details.
87 *
88 * @param parent the parent package.
89 * @param subPackageName the name of the current sub-package.
90 * @param regex flags interpretation of name as regex pattern.
91 * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found.
92 */
93 /* package */ PkgImportControl(PkgImportControl parent, String subPackageName, boolean regex,
94 MismatchStrategy strategyOnMismatch) {
95 super(parent, strategyOnMismatch);
96 this.regex = regex || parent.regex;
97 if (regex || parent.regex) {
98 // regex gets inherited
99 final String parentRegex = ensureSelfContainedRegex(parent.fullPackageName,
100 parent.regex);
101 final String thisRegex = ensureSelfContainedRegex(subPackageName, regex);
102 fullPackageName = parentRegex + DOT_REGEX + thisRegex;
103 patternForPartialMatch = createPatternForPartialMatch(fullPackageName);
104 patternForExactMatch = createPatternForExactMatch(fullPackageName);
105 }
106 else {
107 fullPackageName = parent.fullPackageName + DOT + subPackageName;
108 patternForPartialMatch = null;
109 patternForExactMatch = null;
110 }
111 }
112
113 /**
114 * Returns a regex that is suitable for concatenation by 1) either converting a plain string
115 * into a regular expression (handling special characters) or 2) by enclosing {@code input} in
116 * a (non-capturing) group if {@code input} already is a regular expression.
117 *
118 * <p>1) When concatenating a non-regex package component (like "org.google") with a regex
119 * component (like "[^.]+") the other component has to be converted into a regex too, see
120 * {@link #toRegex(String)}.
121 *
122 * <p>2) The grouping is strictly necessary if a) {@code input} is a regular expression that b)
123 * contains the alteration character ('|') and if c) the pattern is not already enclosed in a
124 * group - as you see in this example: {@code parent="com|org", child="common|uncommon"} will
125 * result in the pattern {@code "(?:org|com)\.(?common|uncommon)"} what will match
126 * {@code "com.common"}, {@code "com.uncommon"}, {@code "org.common"}, and {@code
127 * "org.uncommon"}. Without the grouping it would be {@code "com|org.common|uncommon"} which
128 * would match {@code "com"}, {@code "org.common"}, and {@code "uncommon"}, which clearly is
129 * undesirable. Adding the group fixes this.
130 *
131 * <p>For simplicity the grouping is added to regular expressions unconditionally.
132 *
133 * @param input the input string.
134 * @param alreadyRegex signals if input already is a regular expression.
135 * @return a regex string.
136 */
137 private static String ensureSelfContainedRegex(String input, boolean alreadyRegex) {
138 final String result;
139 if (alreadyRegex) {
140 result = encloseInGroup(input);
141 }
142 else {
143 result = toRegex(input);
144 }
145 return result;
146 }
147
148 /**
149 * Enclose {@code expression} in a (non-capturing) group.
150 *
151 * @param expression the input regular expression
152 * @return a grouped pattern.
153 */
154 private static String encloseInGroup(String expression) {
155 return "(?:" + expression + ")";
156 }
157
158 /**
159 * Converts a normal package name into a regex pattern by escaping all
160 * special characters that may occur in a java package name.
161 *
162 * @param input the input string.
163 * @return a regex string.
164 */
165 private static String toRegex(String input) {
166 return DOT_REGEX_PATTERN.matcher(input).replaceAll(DOT_ESCAPED_REGEX);
167 }
168
169 /**
170 * Creates a Pattern from {@code expression} that matches exactly and child packages.
171 *
172 * @param expression a self-contained regular expression matching the full package exactly.
173 * @return a Pattern.
174 */
175 private static Pattern createPatternForPartialMatch(String expression) {
176 // javadoc of encloseInGroup() explains how to concatenate regular expressions
177 // no grouping needs to be added to fullPackage since this already have been done.
178 return Pattern.compile(expression + "(?:\\..*)?");
179 }
180
181 /**
182 * Creates a Pattern from {@code expression}.
183 *
184 * @param expression a self-contained regular expression matching the full package exactly.
185 * @return a Pattern.
186 */
187 private static Pattern createPatternForExactMatch(String expression) {
188 return Pattern.compile(expression);
189 }
190
191 @Override
192 public AbstractImportControl locateFinest(String forPkg, String forFileName) {
193 AbstractImportControl finestMatch = null;
194 // Check if we are a match.
195 if (matchesAtFront(forPkg)) {
196 // If there won't be match, so I am the best there is.
197 finestMatch = this;
198 // Check if any of the children match.
199 for (AbstractImportControl child : children) {
200 final AbstractImportControl match = child.locateFinest(forPkg, forFileName);
201 if (match != null) {
202 finestMatch = match;
203 break;
204 }
205 }
206 }
207 return finestMatch;
208 }
209
210 /**
211 * Adds new child import control.
212 *
213 * @param importControl child import control
214 */
215 public void addChild(AbstractImportControl importControl) {
216 children.add(importControl);
217 }
218
219 /**
220 * Matches other package name exactly or partially at front.
221 *
222 * @param pkg the package to compare with.
223 * @return if it matches.
224 */
225 private boolean matchesAtFront(String pkg) {
226 final boolean result;
227 if (regex) {
228 result = patternForPartialMatch.matcher(pkg).matches();
229 }
230 else {
231 result = matchesAtFrontNoRegex(pkg);
232 }
233 return result;
234 }
235
236 /**
237 * Non-regex case. Ensure a trailing dot for subpackages, i.e. "com.puppy"
238 * will match "com.puppy.crawl" but not "com.puppycrawl.tools".
239 *
240 * @param pkg the package to compare with.
241 * @return if it matches.
242 */
243 private boolean matchesAtFrontNoRegex(String pkg) {
244 final int length = fullPackageName.length();
245 return pkg.startsWith(fullPackageName)
246 && (pkg.length() == length || pkg.charAt(length) == '.');
247 }
248
249 @Override
250 protected boolean matchesExactly(String pkg, String fileName) {
251 final boolean result;
252 if (regex) {
253 result = patternForExactMatch.matcher(pkg).matches();
254 }
255 else {
256 result = fullPackageName.equals(pkg);
257 }
258 return result;
259 }
260 }