View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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 }