Coverage Report - com.puppycrawl.tools.checkstyle.checks.imports.ImportControl
 
Classes in this File Line Coverage Branch Coverage Complexity
ImportControl
100%
90/90
100%
42/42
2.176
 
 1  
 ////////////////////////////////////////////////////////////////////////////////
 2  
 // checkstyle: Checks Java source code for adherence to a set of rules.
 3  
 // Copyright (C) 2001-2017 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.Deque;
 24  
 import java.util.LinkedList;
 25  
 import java.util.List;
 26  
 import java.util.regex.Pattern;
 27  
 
 28  
 /**
 29  
  * Represents a tree of import rules for controlling whether packages or
 30  
  * classes are allowed to be used. Each instance must have a single parent or
 31  
  * be the root node. Each instance may have zero or more children.
 32  
  *
 33  
  * @author Oliver Burn
 34  
  */
 35  
 class ImportControl {
 36  
     /** The package separator: "." */
 37  
     private static final String DOT = ".";
 38  
     /** A pattern matching the package separator: "." */
 39  2
     private static final Pattern DOT_PATTERN = Pattern.compile(DOT, Pattern.LITERAL);
 40  
     /** The regex for the package separator: "\\.". */
 41  
     private static final String DOT_REGEX = "\\.";
 42  
     /** List of {@link AbstractImportRule} objects to check. */
 43  161
     private final Deque<AbstractImportRule> rules = new LinkedList<>();
 44  
     /** List of children {@link ImportControl} objects. */
 45  161
     private final List<ImportControl> children = new ArrayList<>();
 46  
     /** The parent. Null indicates we are the root node. */
 47  
     private final ImportControl parent;
 48  
     /** The full package name for the node. */
 49  
     private final String fullPackage;
 50  
     /**
 51  
      * The regex pattern for partial match (exact and for subpackages) - only not
 52  
      * null if regex is true.
 53  
      */
 54  
     private final Pattern patternForPartialMatch;
 55  
     /** The regex pattern for exact matches - only not null if regex is true. */
 56  
     private final Pattern patternForExactMatch;
 57  
     /** If this package represents a regular expression. */
 58  
     private final boolean regex;
 59  
     /** Strategy in a case if matching allow/disallow rule was not found. */
 60  
     private final MismatchStrategy strategyOnMismatch;
 61  
 
 62  
     /**
 63  
      * Construct a root node.
 64  
      * @param pkgName the name of the package.
 65  
      * @param regex flags interpretation of pkgName as regex pattern.
 66  
      * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found.
 67  
      */
 68  
     ImportControl(String pkgName, boolean regex,
 69  71
                   MismatchStrategy strategyOnMismatch) {
 70  71
         parent = null;
 71  71
         this.regex = regex;
 72  71
         this.strategyOnMismatch = strategyOnMismatch;
 73  71
         if (regex) {
 74  
             // ensure that fullPackage is a self-contained regular expression
 75  18
             fullPackage = encloseInGroup(pkgName);
 76  18
             patternForPartialMatch = createPatternForPartialMatch(fullPackage);
 77  18
             patternForExactMatch = createPatternForExactMatch(fullPackage);
 78  
         }
 79  
         else {
 80  53
             fullPackage = pkgName;
 81  53
             patternForPartialMatch = null;
 82  53
             patternForExactMatch = null;
 83  
         }
 84  71
     }
 85  
 
 86  
     /**
 87  
      * Construct a root node.
 88  
      * @param pkgName the name of the package.
 89  
      * @param regex flags interpretation of pkgName as regex pattern.
 90  
      */
 91  
     ImportControl(String pkgName, boolean regex) {
 92  45
         this(pkgName, regex, MismatchStrategy.DISALLOWED);
 93  45
     }
 94  
 
 95  
     /**
 96  
      * Construct a child node. The concatenation of regular expressions needs special care:
 97  
      * see {@link #ensureSelfContainedRegex(String, boolean)} for more details.
 98  
      * @param parent the parent node.
 99  
      * @param subPkg the sub package name.
 100  
      * @param regex flags interpretation of subPkg as regex pattern.
 101  
      * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found.
 102  
      */
 103  
     ImportControl(ImportControl parent, String subPkg, boolean regex,
 104  90
                   MismatchStrategy strategyOnMismatch) {
 105  90
         this.parent = parent;
 106  90
         this.strategyOnMismatch = strategyOnMismatch;
 107  90
         if (regex || parent.regex) {
 108  
             // regex gets inherited
 109  20
             final String parentRegex = ensureSelfContainedRegex(parent.fullPackage, parent.regex);
 110  20
             final String thisRegex = ensureSelfContainedRegex(subPkg, regex);
 111  20
             fullPackage = parentRegex + DOT_REGEX + thisRegex;
 112  20
             patternForPartialMatch = createPatternForPartialMatch(fullPackage);
 113  20
             patternForExactMatch = createPatternForExactMatch(fullPackage);
 114  20
             this.regex = true;
 115  20
         }
 116  
         else {
 117  70
             fullPackage = parent.fullPackage + DOT + subPkg;
 118  70
             patternForPartialMatch = null;
 119  70
             patternForExactMatch = null;
 120  70
             this.regex = false;
 121  
         }
 122  90
     }
 123  
 
 124  
     /**
 125  
      * Construct a child node. The concatenation of regular expressions needs special care:
 126  
      * see {@link #ensureSelfContainedRegex(String, boolean)} for more details.
 127  
      * @param parent the parent node.
 128  
      * @param subPkg the sub package name.
 129  
      * @param regex flags interpretation of subPkg as regex pattern.
 130  
      */
 131  
     ImportControl(ImportControl parent, String subPkg, boolean regex) {
 132  45
         this(parent, subPkg, regex, MismatchStrategy.DELEGATE_TO_PARENT);
 133  45
     }
 134  
 
 135  
     /**
 136  
      * Returns a regex that is suitable for concatenation by 1) either converting a plain string
 137  
      * into a regular expression (handling special characters) or 2) by enclosing {@code input} in
 138  
      * a (non-capturing) group if {@code input} already is a regular expression.
 139  
      *
 140  
      * <p>1) When concatenating a non-regex package component (like "org.google") with a regex
 141  
      * component (like "[^.]+") the other component has to be converted into a regex too, see
 142  
      * {@link #toRegex(String)}.
 143  
      *
 144  
      * <p>2) The grouping is strictly necessary if a) {@code input} is a regular expression that b)
 145  
      * contains the alteration character ('|') and if c) the pattern is not already enclosed in a
 146  
      * group - as you see in this example: {@code parent="com|org", child="common|uncommon"} will
 147  
      * result in the pattern {@code "(?:org|com)\.(?common|uncommon)"} what will match
 148  
      * {@code "com.common"}, {@code "com.uncommon"}, {@code "org.common"}, and {@code
 149  
      * "org.uncommon"}. Without the grouping it would be {@code "com|org.common|uncommon"} which
 150  
      * would match {@code "com"}, {@code "org.common"}, and {@code "uncommon"}, which clearly is
 151  
      * undesirable. Adding the group fixes this.
 152  
      *
 153  
      * <p>For simplicity the grouping is added to regular expressions unconditionally.
 154  
      *
 155  
      * @param input the input string.
 156  
      * @param alreadyRegex signals if input already is a regular expression.
 157  
      * @return a regex string.
 158  
      */
 159  
     private static String ensureSelfContainedRegex(String input, boolean alreadyRegex) {
 160  
         final String result;
 161  40
         if (alreadyRegex) {
 162  35
             result = encloseInGroup(input);
 163  
         }
 164  
         else {
 165  5
             result = toRegex(input);
 166  
         }
 167  40
         return result;
 168  
     }
 169  
 
 170  
     /**
 171  
      * Enclose {@code expression} in a (non-capturing) group.
 172  
      * @param expression the input regular expression
 173  
      * @return a grouped pattern.
 174  
      */
 175  
     private static String encloseInGroup(String expression) {
 176  53
         return "(?:" + expression + ")";
 177  
     }
 178  
 
 179  
     /**
 180  
      * Converts a normal package name into a regex pattern by escaping all
 181  
      * special characters that may occur in a java package name.
 182  
      * @param input the input string.
 183  
      * @return a regex string.
 184  
      */
 185  
     private static String toRegex(String input) {
 186  5
         return DOT_PATTERN.matcher(input).replaceAll(DOT_REGEX);
 187  
     }
 188  
 
 189  
     /**
 190  
      * Creates a Pattern from {@code expression} that matches exactly and child packages.
 191  
      * @param expression a self-contained regular expression matching the full package exactly.
 192  
      * @return a Pattern.
 193  
      */
 194  
     private static Pattern createPatternForPartialMatch(String expression) {
 195  
         // javadoc of encloseInGroup() explains how to concatenate regular expressions
 196  
         // no grouping needs to be added to fullPackage since this already have been done.
 197  38
         return Pattern.compile(expression + "(?:\\..*)?");
 198  
     }
 199  
 
 200  
     /**
 201  
      * Creates a Pattern from {@code expression}.
 202  
      * @param expression a self-contained regular expression matching the full package exactly.
 203  
      * @return a Pattern.
 204  
      */
 205  
     private static Pattern createPatternForExactMatch(String expression) {
 206  38
         return Pattern.compile(expression);
 207  
     }
 208  
 
 209  
     /**
 210  
      * Adds an {@link AbstractImportRule} to the node.
 211  
      * @param rule the rule to be added.
 212  
      */
 213  
     protected void addImportRule(AbstractImportRule rule) {
 214  449
         rules.addFirst(rule);
 215  449
     }
 216  
 
 217  
     /**
 218  
      * Adds new child import control.
 219  
      * @param importControl child import control
 220  
      */
 221  
     public void addChild(ImportControl importControl) {
 222  90
         children.add(importControl);
 223  90
     }
 224  
 
 225  
     /**
 226  
      * Search down the tree to locate the finest match for a supplied package.
 227  
      * @param forPkg the package to search for.
 228  
      * @return the finest match, or null if no match at all.
 229  
      */
 230  
     public ImportControl locateFinest(String forPkg) {
 231  85
         ImportControl finestMatch = null;
 232  
         // Check if we are a match.
 233  85
         if (matchesAtFront(forPkg)) {
 234  
             // If there won't be match so I am the best there is.
 235  63
             finestMatch = this;
 236  
             // Check if any of the children match.
 237  63
             for (ImportControl child : children) {
 238  36
                 final ImportControl match = child.locateFinest(forPkg);
 239  36
                 if (match != null) {
 240  27
                     finestMatch = match;
 241  27
                     break;
 242  
                 }
 243  9
             }
 244  
         }
 245  85
         return finestMatch;
 246  
     }
 247  
 
 248  
     /**
 249  
      * Matches other package name exactly or partially at front.
 250  
      * @param pkg the package to compare with.
 251  
      * @return if it matches.
 252  
      */
 253  
     private boolean matchesAtFront(String pkg) {
 254  
         final boolean result;
 255  85
         if (regex) {
 256  38
             result = patternForPartialMatch.matcher(pkg).matches();
 257  
         }
 258  
         else {
 259  47
             result = matchesAtFrontNoRegex(pkg);
 260  
         }
 261  85
         return result;
 262  
     }
 263  
 
 264  
     /**
 265  
      * Non-regex case. Ensure a trailing dot for subpackages, i.e. "com.puppy"
 266  
      * will match "com.puppy.crawl" but not "com.puppycrawl.tools".
 267  
      * @param pkg the package to compare with.
 268  
      * @return if it matches.
 269  
      */
 270  
     private boolean matchesAtFrontNoRegex(String pkg) {
 271  94
         return pkg.startsWith(fullPackage)
 272  40
                 && (pkg.length() == fullPackage.length()
 273  26
                     || pkg.charAt(fullPackage.length()) == '.');
 274  
     }
 275  
 
 276  
     /**
 277  
      * Returns whether a package or class is allowed to be imported.
 278  
      * The algorithm checks with the current node for a result, and if none is
 279  
      * found then calls its parent looking for a match. This will recurse
 280  
      * looking for match. If there is no clear result then
 281  
      * {@link AccessResult#UNKNOWN} is returned.
 282  
      * @param forImport the import to check on.
 283  
      * @param inPkg the package doing the import.
 284  
      * @return an {@link AccessResult}.
 285  
      */
 286  
     public AccessResult checkAccess(String inPkg, String forImport) {
 287  
         final AccessResult result;
 288  132
         final AccessResult returnValue = localCheckAccess(inPkg, forImport);
 289  132
         if (returnValue != AccessResult.UNKNOWN) {
 290  56
             result = returnValue;
 291  
         }
 292  76
         else if (parent == null) {
 293  23
             if (strategyOnMismatch == MismatchStrategy.ALLOWED) {
 294  3
                 result = AccessResult.ALLOWED;
 295  
             }
 296  
             else {
 297  20
                 result = AccessResult.DISALLOWED;
 298  
             }
 299  
         }
 300  
         else {
 301  53
             if (strategyOnMismatch == MismatchStrategy.ALLOWED) {
 302  2
                 result = AccessResult.ALLOWED;
 303  
             }
 304  51
             else if (strategyOnMismatch == MismatchStrategy.DISALLOWED) {
 305  1
                 result = AccessResult.DISALLOWED;
 306  
             }
 307  
             else {
 308  50
                 result = parent.checkAccess(inPkg, forImport);
 309  
             }
 310  
         }
 311  132
         return result;
 312  
     }
 313  
 
 314  
     /**
 315  
      * Checks whether any of the rules for this node control access to
 316  
      * a specified package or class.
 317  
      * @param forImport the import to check.
 318  
      * @param inPkg the package doing the import.
 319  
      * @return an {@link AccessResult}.
 320  
      */
 321  
     private AccessResult localCheckAccess(String inPkg, String forImport) {
 322  132
         AccessResult localCheckAccessResult = AccessResult.UNKNOWN;
 323  132
         for (AbstractImportRule importRule : rules) {
 324  
             // Check if an import rule is only meant to be applied locally.
 325  230
             if (!importRule.isLocalOnly() || matchesExactly(inPkg)) {
 326  197
                 final AccessResult result = importRule.verifyImport(forImport);
 327  197
                 if (result != AccessResult.UNKNOWN) {
 328  56
                     localCheckAccessResult = result;
 329  56
                     break;
 330  
                 }
 331  
             }
 332  174
         }
 333  132
         return localCheckAccessResult;
 334  
     }
 335  
 
 336  
     /**
 337  
      * Check for equality of this with pkg.
 338  
      * @param pkg the package to compare with.
 339  
      * @return if it matches.
 340  
      */
 341  
     private boolean matchesExactly(String pkg) {
 342  
         final boolean result;
 343  67
         if (regex) {
 344  6
             result = patternForExactMatch.matcher(pkg).matches();
 345  
         }
 346  
         else {
 347  61
             result = fullPackage.equals(pkg);
 348  
         }
 349  67
         return result;
 350  
     }
 351  
 }