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.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 }