001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.imports; 021 022import java.util.ArrayList; 023import java.util.List; 024import java.util.regex.Pattern; 025 026/** 027 * Represents a tree of import rules for a specific package. 028 * Each instance may have zero or more children. A child may 029 * be a sub-package, a class, or an allow/disallow rule. 030 */ 031class PkgImportControl extends AbstractImportControl { 032 /** The package separator: ".". */ 033 private static final String DOT = "."; 034 035 /** The regex for the package separator: "\\.". */ 036 private static final String DOT_REGEX = "\\."; 037 038 /** A pattern matching the package separator: "\.". */ 039 private static final Pattern DOT_REGEX_PATTERN = Pattern.compile(DOT_REGEX); 040 041 /** The regex for the escaped package separator: "\\\\.". */ 042 private static final String DOT_ESCAPED_REGEX = "\\\\."; 043 044 /** List of children {@link AbstractImportControl} objects. */ 045 private final List<AbstractImportControl> children = new ArrayList<>(); 046 047 /** The full name for the package. */ 048 private final String fullPackageName; 049 /** 050 * The regex pattern for partial match (exact and for subpackages) - only not 051 * null if regex is true. 052 */ 053 private final Pattern patternForPartialMatch; 054 /** The regex pattern for exact matches - only not null if regex is true. */ 055 private final Pattern patternForExactMatch; 056 /** If this package represents a regular expression. */ 057 private final boolean regex; 058 059 /** 060 * Construct a root, package node. 061 * 062 * @param packageName the name of the package. 063 * @param regex flags interpretation of name as regex pattern. 064 * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found. 065 */ 066 /* package */ PkgImportControl(String packageName, boolean regex, 067 MismatchStrategy strategyOnMismatch) { 068 super(null, strategyOnMismatch); 069 070 this.regex = regex; 071 if (regex) { 072 // ensure that fullName is a self-contained regular expression 073 fullPackageName = encloseInGroup(packageName); 074 patternForPartialMatch = createPatternForPartialMatch(fullPackageName); 075 patternForExactMatch = createPatternForExactMatch(fullPackageName); 076 } 077 else { 078 fullPackageName = packageName; 079 patternForPartialMatch = null; 080 patternForExactMatch = null; 081 } 082 } 083 084 /** 085 * Construct a sub-package node. The concatenation of regular expressions needs special care: 086 * see {@link #ensureSelfContainedRegex(String, boolean)} for more details. 087 * 088 * @param parent the parent package. 089 * @param subPackageName the name of the current sub-package. 090 * @param regex flags interpretation of name as regex pattern. 091 * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found. 092 */ 093 /* package */ PkgImportControl(PkgImportControl parent, String subPackageName, boolean regex, 094 MismatchStrategy strategyOnMismatch) { 095 super(parent, strategyOnMismatch); 096 if (regex || parent.regex) { 097 // regex gets inherited 098 final String parentRegex = ensureSelfContainedRegex(parent.fullPackageName, 099 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}