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  
34  /**
35   * <p>
36   * Controls what can be imported in each package and file. Useful for ensuring
37   * that application layering rules are not violated, especially on large projects.
38   * </p>
39   * <p>
40   * You can control imports based on the package name or based on the file name.
41   * When controlling packages, all files and sub-packages in the declared package
42   * will be controlled by this check. To specify differences between a main package
43   * and a sub-package, you must define the sub-package inside the main package.
44   * When controlling file, only the file name is considered and only files processed by
45   * <a href="https://checkstyle.org/config.html#TreeWalker">TreeWalker</a>.
46   * The file's extension is ignored.
47   * </p>
48   * <p>
49   * Short description of the behaviour:
50   * </p>
51   * <ul>
52   * <li>
53   * Check starts checking from the longest matching subpackage (later 'current subpackage') or
54   * the first file name match described inside import control file to package defined in class file.
55   * <ul>
56   * <li>
57   * The longest matching subpackage is found by starting with the root package and
58   * examining if any of the sub-packages or file definitions match the current
59   * class' package or file name.
60   * </li>
61   * <li>
62   * If a file name is matched first, that is considered the longest match and becomes
63   * the current file/subpackage.
64   * </li>
65   * <li>
66   * If another subpackage is matched, then it's subpackages and file names are examined
67   * for the next longest match and the process repeats recursively.
68   * </li>
69   * <li>
70   * If no subpackages or file names are matched, the current subpackage is then used.
71   * </li>
72   * </ul>
73   * </li>
74   * <li>
75   * Order of rules in the same subpackage/root are defined by the order of declaration
76   * in the XML file, which is from top (first) to bottom (last).
77   * </li>
78   * <li>
79   * If there is matching allow/disallow rule inside the current file/subpackage
80   * then the Check returns the first "allowed" or "disallowed" message.
81   * </li>
82   * <li>
83   * If there is no matching allow/disallow rule inside the current file/subpackage
84   * then it continues checking in the parent subpackage.
85   * </li>
86   * <li>
87   * If there is no matching allow/disallow rule in any of the files/subpackages,
88   * including the root level (import-control), then the import is disallowed by default.
89   * </li>
90   * </ul>
91   * <p>
92   * The DTD for an import control XML document is at
93   * <a href="https://checkstyle.org/dtds/import_control_1_4.dtd">
94   * https://checkstyle.org/dtds/import_control_1_4.dtd</a>.
95   * It contains documentation on each of the elements and attributes.
96   * </p>
97   * <p>
98   * The check validates a XML document when it loads the document. To validate against
99   * the above DTD, include the following document type declaration in your XML document:
100  * </p>
101  * <pre>
102  * &lt;!DOCTYPE import-control PUBLIC
103  *     "-//Checkstyle//DTD ImportControl Configuration 1.4//EN"
104  *     "https://checkstyle.org/dtds/import_control_1_4.dtd"&gt;
105  * </pre>
106  * <ul>
107  * <li>
108  * Property {@code file} - Specify the location of the file containing the
109  * import control configuration. It can be a regular file, URL or resource path.
110  * It will try loading the path as a URL first, then as a file, and finally as a resource.
111  * Type is {@code java.net.URI}.
112  * Default value is {@code null}.
113  * </li>
114  * <li>
115  * Property {@code path} - Specify the regular expression of file paths to which
116  * this check should apply. Files that don't match the pattern will not be checked.
117  * The pattern will be matched against the full absolute file path.
118  * Type is {@code java.util.regex.Pattern}.
119  * Default value is {@code ".*"}.
120  * </li>
121  * </ul>
122  * <p>
123  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
124  * </p>
125  * <p>
126  * Violation Message Keys:
127  * </p>
128  * <ul>
129  * <li>
130  * {@code import.control.disallowed}
131  * </li>
132  * <li>
133  * {@code import.control.missing.file}
134  * </li>
135  * <li>
136  * {@code import.control.unknown.pkg}
137  * </li>
138  * </ul>
139  *
140  * @since 4.0
141  */
142 @FileStatefulCheck
143 public class ImportControlCheck extends AbstractCheck implements ExternalResourceHolder {
144 
145     /**
146      * A key is pointing to the warning message text in "messages.properties"
147      * file.
148      */
149     public static final String MSG_MISSING_FILE = "import.control.missing.file";
150 
151     /**
152      * A key is pointing to the warning message text in "messages.properties"
153      * file.
154      */
155     public static final String MSG_UNKNOWN_PKG = "import.control.unknown.pkg";
156 
157     /**
158      * A key is pointing to the warning message text in "messages.properties"
159      * file.
160      */
161     public static final String MSG_DISALLOWED = "import.control.disallowed";
162 
163     /**
164      * A part of message for exception.
165      */
166     private static final String UNABLE_TO_LOAD = "Unable to load ";
167 
168     /**
169      * Specify the location of the file containing the import control configuration.
170      * It can be a regular file, URL or resource path. It will try loading the path
171      * as a URL first, then as a file, and finally as a resource.
172      */
173     private URI file;
174 
175     /**
176      * Specify the regular expression of file paths to which this check should apply.
177      * Files that don't match the pattern will not be checked. The pattern will
178      * be matched against the full absolute file path.
179      */
180     private Pattern path = Pattern.compile(".*");
181     /** Whether to process the current file. */
182     private boolean processCurrentFile;
183 
184     /** The root package controller. */
185     private PkgImportControl root;
186     /** The package doing the import. */
187     private String packageName;
188     /** The file name doing the import. */
189     private String fileName;
190 
191     /**
192      * The package controller for the current file. Used for performance
193      * optimisation.
194      */
195     private AbstractImportControl currentImportControl;
196 
197     @Override
198     public int[] getDefaultTokens() {
199         return getRequiredTokens();
200     }
201 
202     @Override
203     public int[] getAcceptableTokens() {
204         return getRequiredTokens();
205     }
206 
207     @Override
208     public int[] getRequiredTokens() {
209         return new int[] {TokenTypes.PACKAGE_DEF, TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT, };
210     }
211 
212     // suppress deprecation until https://github.com/checkstyle/checkstyle/issues/11166
213     @SuppressWarnings("deprecation")
214     @Override
215     public void beginTree(DetailAST rootAST) {
216         currentImportControl = null;
217         processCurrentFile = path.matcher(getFilePath()).find();
218         fileName = getFileContents().getText().getFile().getName();
219 
220         final int period = fileName.lastIndexOf('.');
221 
222         if (period != -1) {
223             fileName = fileName.substring(0, period);
224         }
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 
322 }