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 }