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