View Javadoc
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.net.URI;
23  import java.util.Set;
24  import java.util.regex.Pattern;
25  
26  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
29  import com.puppycrawl.tools.checkstyle.api.DetailAST;
30  import com.puppycrawl.tools.checkstyle.api.ExternalResourceHolder;
31  import com.puppycrawl.tools.checkstyle.api.FullIdent;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
34  
35  /**
36   * <div>
37   * Controls what can be imported in each package and file. Useful for ensuring
38   * that application layering rules are not violated, especially on large projects.
39   * </div>
40   *
41   * <p>
42   * You can control imports based on the package name or based on the file name.
43   * When controlling packages, all files and sub-packages in the declared package
44   * will be controlled by this check. To specify differences between a main package
45   * and a sub-package, you must define the sub-package inside the main package.
46   * When controlling file, only the file name is considered and only files processed by
47   * <a href="https://checkstyle.org/config.html#TreeWalker">TreeWalker</a>.
48   * The file's extension is ignored.
49   * </p>
50   *
51   * <p>
52   * Short description of the behaviour:
53   * </p>
54   * <ul>
55   * <li>
56   * Check starts checking from the longest matching subpackage (later 'current subpackage') or
57   * the first file name match described inside import control file to package defined in class file.
58   * <ul>
59   * <li>
60   * The longest matching subpackage is found by starting with the root package and
61   * examining if any of the sub-packages or file definitions match the current
62   * class' package or file name.
63   * </li>
64   * <li>
65   * If a file name is matched first, that is considered the longest match and becomes
66   * the current file/subpackage.
67   * </li>
68   * <li>
69   * If another subpackage is matched, then it's subpackages and file names are examined
70   * for the next longest match and the process repeats recursively.
71   * </li>
72   * <li>
73   * If no subpackages or file names are matched, the current subpackage is then used.
74   * </li>
75   * </ul>
76   * </li>
77   * <li>
78   * Order of rules in the same subpackage/root are defined by the order of declaration
79   * in the XML file, which is from top (first) to bottom (last).
80   * </li>
81   * <li>
82   * If there is matching allow/disallow rule inside the current file/subpackage
83   * then the Check returns the first "allowed" or "disallowed" message.
84   * </li>
85   * <li>
86   * If there is no matching allow/disallow rule inside the current file/subpackage
87   * then it continues checking in the parent subpackage.
88   * </li>
89   * <li>
90   * If there is no matching allow/disallow rule in any of the files/subpackages,
91   * including the root level (import-control), then the import is disallowed by default.
92   * </li>
93   * </ul>
94   *
95   * <p>
96   * The DTD for an import control XML document is at
97   * <a href="https://checkstyle.org/dtds/import_control_1_4.dtd">
98   * https://checkstyle.org/dtds/import_control_1_4.dtd</a>.
99   * It contains documentation on each of the elements and attributes.
100  * </p>
101  *
102  * <p>
103  * The check validates a XML document when it loads the document. To validate against
104  * the above DTD, include the following document type declaration in your XML document:
105  * </p>
106  * <pre>
107  * &lt;!DOCTYPE import-control PUBLIC
108  *     "-//Checkstyle//DTD ImportControl Configuration 1.4//EN"
109  *     "https://checkstyle.org/dtds/import_control_1_4.dtd"&gt;
110  * </pre>
111  * <ul>
112  * <li>
113  * Property {@code file} - Specify the location of the file containing the
114  * import control configuration. It can be a regular file, URL or resource path.
115  * It will try loading the path as a URL first, then as a file, and finally as a resource.
116  * Type is {@code java.net.URI}.
117  * Default value is {@code null}.
118  * </li>
119  * <li>
120  * Property {@code path} - Specify the regular expression of file paths to which
121  * this check should apply. Files that don't match the pattern will not be checked.
122  * The pattern will be matched against the full absolute file path.
123  * Type is {@code java.util.regex.Pattern}.
124  * Default value is {@code ".*"}.
125  * </li>
126  * </ul>
127  *
128  * <p>
129  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
130  * </p>
131  *
132  * <p>
133  * Violation Message Keys:
134  * </p>
135  * <ul>
136  * <li>
137  * {@code import.control.disallowed}
138  * </li>
139  * <li>
140  * {@code import.control.missing.file}
141  * </li>
142  * <li>
143  * {@code import.control.unknown.pkg}
144  * </li>
145  * </ul>
146  *
147  * @since 4.0
148  */
149 @FileStatefulCheck
150 public class ImportControlCheck extends AbstractCheck implements ExternalResourceHolder {
151 
152     /**
153      * A key is pointing to the warning message text in "messages.properties"
154      * file.
155      */
156     public static final String MSG_MISSING_FILE = "import.control.missing.file";
157 
158     /**
159      * A key is pointing to the warning message text in "messages.properties"
160      * file.
161      */
162     public static final String MSG_UNKNOWN_PKG = "import.control.unknown.pkg";
163 
164     /**
165      * A key is pointing to the warning message text in "messages.properties"
166      * file.
167      */
168     public static final String MSG_DISALLOWED = "import.control.disallowed";
169 
170     /**
171      * A part of message for exception.
172      */
173     private static final String UNABLE_TO_LOAD = "Unable to load ";
174 
175     /**
176      * Specify the location of the file containing the import control configuration.
177      * It can be a regular file, URL or resource path. It will try loading the path
178      * as a URL first, then as a file, and finally as a resource.
179      */
180     private URI file;
181 
182     /**
183      * Specify the regular expression of file paths to which this check should apply.
184      * Files that don't match the pattern will not be checked. The pattern will
185      * be matched against the full absolute file path.
186      */
187     private Pattern path = Pattern.compile(".*");
188     /** Whether to process the current file. */
189     private boolean processCurrentFile;
190 
191     /** The root package controller. */
192     private PkgImportControl root;
193     /** The package doing the import. */
194     private String packageName;
195     /** The file name doing the import. */
196     private String fileName;
197 
198     /**
199      * The package controller for the current file. Used for performance
200      * optimisation.
201      */
202     private AbstractImportControl currentImportControl;
203 
204     @Override
205     public int[] getDefaultTokens() {
206         return getRequiredTokens();
207     }
208 
209     @Override
210     public int[] getAcceptableTokens() {
211         return getRequiredTokens();
212     }
213 
214     @Override
215     public int[] getRequiredTokens() {
216         return new int[] {TokenTypes.PACKAGE_DEF, TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT, };
217     }
218 
219     @Override
220     public void beginTree(DetailAST rootAST) {
221         currentImportControl = null;
222         final String fullFileName = getFilePath();
223         processCurrentFile = path.matcher(fullFileName).find();
224         fileName = CommonUtil.getFileNameWithoutExtension(fullFileName);
225     }
226 
227     @Override
228     public void visitToken(DetailAST ast) {
229         if (processCurrentFile) {
230             if (ast.getType() == TokenTypes.PACKAGE_DEF) {
231                 if (root == null) {
232                     log(ast, MSG_MISSING_FILE);
233                 }
234                 else {
235                     packageName = getPackageText(ast);
236                     currentImportControl = root.locateFinest(packageName, fileName);
237                     if (currentImportControl == null) {
238                         log(ast, MSG_UNKNOWN_PKG);
239                     }
240                 }
241             }
242             else if (currentImportControl != null) {
243                 final String importText = getImportText(ast);
244                 final AccessResult access = currentImportControl.checkAccess(packageName, fileName,
245                         importText);
246                 if (access != AccessResult.ALLOWED) {
247                     log(ast, MSG_DISALLOWED, importText);
248                 }
249             }
250         }
251     }
252 
253     @Override
254     public Set<String> getExternalResourceLocations() {
255         return Set.of(file.toASCIIString());
256     }
257 
258     /**
259      * Returns package text.
260      *
261      * @param ast PACKAGE_DEF ast node
262      * @return String that represents full package name
263      */
264     private static String getPackageText(DetailAST ast) {
265         final DetailAST nameAST = ast.getLastChild().getPreviousSibling();
266         return FullIdent.createFullIdent(nameAST).getText();
267     }
268 
269     /**
270      * Returns import text.
271      *
272      * @param ast ast node that represents import
273      * @return String that represents importing class
274      */
275     private static String getImportText(DetailAST ast) {
276         final FullIdent imp;
277         if (ast.getType() == TokenTypes.IMPORT) {
278             imp = FullIdent.createFullIdentBelow(ast);
279         }
280         else {
281             // know it is a static import
282             imp = FullIdent.createFullIdent(ast
283                     .getFirstChild().getNextSibling());
284         }
285         return imp.getText();
286     }
287 
288     /**
289      * Setter to specify the location of the file containing the import control configuration.
290      * It can be a regular file, URL or resource path. It will try loading the path
291      * as a URL first, then as a file, and finally as a resource.
292      *
293      * @param uri the uri of the file to load.
294      * @throws IllegalArgumentException on error loading the file.
295      * @since 4.0
296      */
297     public void setFile(URI uri) {
298         // Handle empty param
299         if (uri != null) {
300             try {
301                 root = ImportControlLoader.load(uri);
302                 file = uri;
303             }
304             catch (CheckstyleException ex) {
305                 throw new IllegalArgumentException(UNABLE_TO_LOAD + uri, ex);
306             }
307         }
308     }
309 
310     /**
311      * Setter to specify the regular expression of file paths to which this check should apply.
312      * Files that don't match the pattern will not be checked. The pattern will be matched
313      * against the full absolute file path.
314      *
315      * @param pattern the file path regex this check should apply to.
316      * @since 7.5
317      */
318     public void setPath(Pattern pattern) {
319         path = pattern;
320     }
321 }