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.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 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 if (regex || parent.regex) {
97 // regex gets inherited
98 final String parentRegex = ensureSelfContainedRegex(parent.fullPackageName,
99 parent.regex);
100 final String thisRegex = ensureSelfContainedRegex(subPackageName, regex);
101 fullPackageName = parentRegex + DOT_REGEX + thisRegex;
102 patternForPartialMatch = createPatternForPartialMatch(fullPackageName);
103 patternForExactMatch = createPatternForExactMatch(fullPackageName);
104 this.regex = true;
105 }
106 else {
107 fullPackageName = parent.fullPackageName + DOT + subPackageName;
108 patternForPartialMatch = null;
109 patternForExactMatch = null;
110 this.regex = false;
111 }
112 }
113
114 /**
115 * Returns a regex that is suitable for concatenation by 1) either converting a plain string
116 * into a regular expression (handling special characters) or 2) by enclosing {@code input} in
117 * a (non-capturing) group if {@code input} already is a regular expression.
118 *
119 * <p>1) When concatenating a non-regex package component (like "org.google") with a regex
120 * component (like "[^.]+") the other component has to be converted into a regex too, see
121 * {@link #toRegex(String)}.
122 *
123 * <p>2) The grouping is strictly necessary if a) {@code input} is a regular expression that b)
124 * contains the alteration character ('|') and if c) the pattern is not already enclosed in a
125 * group - as you see in this example: {@code parent="com|org", child="common|uncommon"} will
126 * result in the pattern {@code "(?:org|com)\.(?common|uncommon)"} what will match
127 * {@code "com.common"}, {@code "com.uncommon"}, {@code "org.common"}, and {@code
128 * "org.uncommon"}. Without the grouping it would be {@code "com|org.common|uncommon"} which
129 * would match {@code "com"}, {@code "org.common"}, and {@code "uncommon"}, which clearly is
130 * undesirable. Adding the group fixes this.
131 *
132 * <p>For simplicity the grouping is added to regular expressions unconditionally.
133 *
134 * @param input the input string.
135 * @param alreadyRegex signals if input already is a regular expression.
136 * @return a regex string.
137 */
138 private static String ensureSelfContainedRegex(String input, boolean alreadyRegex) {
139 final String result;
140 if (alreadyRegex) {
141 result = encloseInGroup(input);
142 }
143 else {
144 result = toRegex(input);
145 }
146 return result;
147 }
148
149 /**
150 * Enclose {@code expression} in a (non-capturing) group.
151 *
152 * @param expression the input regular expression
153 * @return a grouped pattern.
154 */
155 private static String encloseInGroup(String expression) {
156 return "(?:" + expression + ")";
157 }
158
159 /**
160 * Converts a normal package name into a regex pattern by escaping all
161 * special characters that may occur in a java package name.
162 *
163 * @param input the input string.
164 * @return a regex string.
165 */
166 private static String toRegex(String input) {
167 return DOT_REGEX_PATTERN.matcher(input).replaceAll(DOT_ESCAPED_REGEX);
168 }
169
170 /**
171 * Creates a Pattern from {@code expression} that matches exactly and child packages.
172 *
173 * @param expression a self-contained regular expression matching the full package exactly.
174 * @return a Pattern.
175 */
176 private static Pattern createPatternForPartialMatch(String expression) {
177 // javadoc of encloseInGroup() explains how to concatenate regular expressions
178 // no grouping needs to be added to fullPackage since this already have been done.
179 return Pattern.compile(expression + "(?:\\..*)?");
180 }
181
182 /**
183 * Creates a Pattern from {@code expression}.
184 *
185 * @param expression a self-contained regular expression matching the full package exactly.
186 * @return a Pattern.
187 */
188 private static Pattern createPatternForExactMatch(String expression) {
189 return Pattern.compile(expression);
190 }
191
192 @Override
193 public AbstractImportControl locateFinest(String forPkg, String forFileName) {
194 AbstractImportControl finestMatch = null;
195 // Check if we are a match.
196 if (matchesAtFront(forPkg)) {
197 // If there won't be match, so I am the best there is.
198 finestMatch = this;
199 // Check if any of the children match.
200 for (AbstractImportControl child : children) {
201 final AbstractImportControl match = child.locateFinest(forPkg, forFileName);
202 if (match != null) {
203 finestMatch = match;
204 break;
205 }
206 }
207 }
208 return finestMatch;
209 }
210
211 /**
212 * Adds new child import control.
213 *
214 * @param importControl child import control
215 */
216 public void addChild(AbstractImportControl importControl) {
217 children.add(importControl);
218 }
219
220 /**
221 * Matches other package name exactly or partially at front.
222 *
223 * @param pkg the package to compare with.
224 * @return if it matches.
225 */
226 private boolean matchesAtFront(String pkg) {
227 final boolean result;
228 if (regex) {
229 result = patternForPartialMatch.matcher(pkg).matches();
230 }
231 else {
232 result = matchesAtFrontNoRegex(pkg);
233 }
234 return result;
235 }
236
237 /**
238 * Non-regex case. Ensure a trailing dot for subpackages, i.e. "com.puppy"
239 * will match "com.puppy.crawl" but not "com.puppycrawl.tools".
240 *
241 * @param pkg the package to compare with.
242 * @return if it matches.
243 */
244 private boolean matchesAtFrontNoRegex(String pkg) {
245 final int length = fullPackageName.length();
246 return pkg.startsWith(fullPackageName)
247 && (pkg.length() == length || pkg.charAt(length) == '.');
248 }
249
250 @Override
251 protected boolean matchesExactly(String pkg, String fileName) {
252 final boolean result;
253 if (regex) {
254 result = patternForExactMatch.matcher(pkg).matches();
255 }
256 else {
257 result = fullPackageName.equals(pkg);
258 }
259 return result;
260 }
261 }