001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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 */ 031public class 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 this.regex = regex || parent.regex; 097 if (regex || parent.regex) { 098 // regex gets inherited 099 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}