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}