View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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  * <div class="wrapper"><pre class="prettyprint"><code class="language-xml">
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  * </code></pre></div>
111  *
112  * @since 4.0
113  */
114 @FileStatefulCheck
115 public class ImportControlCheck extends AbstractCheck implements ExternalResourceHolder {
116 
117     /**
118      * A key is pointing to the warning message text in "messages.properties"
119      * file.
120      */
121     public static final String MSG_MISSING_FILE = "import.control.missing.file";
122 
123     /**
124      * A key is pointing to the warning message text in "messages.properties"
125      * file.
126      */
127     public static final String MSG_UNKNOWN_PKG = "import.control.unknown.pkg";
128 
129     /**
130      * A key is pointing to the warning message text in "messages.properties"
131      * file.
132      */
133     public static final String MSG_DISALLOWED = "import.control.disallowed";
134 
135     /**
136      * A part of message for exception.
137      */
138     private static final String UNABLE_TO_LOAD = "Unable to load ";
139 
140     /**
141      * Specify the location of the file containing the import control configuration.
142      * It can be a regular file, URL or resource path. It will try loading the path
143      * as a URL first, then as a file, and finally as a resource.
144      */
145     private URI file;
146 
147     /**
148      * Specify the regular expression of file paths to which this check should apply.
149      * Files that don't match the pattern will not be checked. The pattern will
150      * be matched against the full absolute file path.
151      */
152     private Pattern path = Pattern.compile(".*");
153     /** Whether to process the current file. */
154     private boolean processCurrentFile;
155 
156     /** The root package controller. */
157     private PkgImportControl root;
158     /** The package doing the import. */
159     private String packageName;
160     /** The file name doing the import. */
161     private String fileName;
162 
163     /**
164      * The package controller for the current file. Used for performance
165      * optimisation.
166      */
167     private AbstractImportControl currentImportControl;
168 
169     @Override
170     public int[] getDefaultTokens() {
171         return getRequiredTokens();
172     }
173 
174     @Override
175     public int[] getAcceptableTokens() {
176         return getRequiredTokens();
177     }
178 
179     @Override
180     public int[] getRequiredTokens() {
181         return new int[] {TokenTypes.PACKAGE_DEF, TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT, };
182     }
183 
184     @Override
185     public void beginTree(DetailAST rootAST) {
186         currentImportControl = null;
187         final String fullFileName = getFilePath();
188         processCurrentFile = path.matcher(fullFileName).find();
189         fileName = CommonUtil.getFileNameWithoutExtension(fullFileName);
190     }
191 
192     @Override
193     public void visitToken(DetailAST ast) {
194         if (processCurrentFile) {
195             if (ast.getType() == TokenTypes.PACKAGE_DEF) {
196                 if (root == null) {
197                     log(ast, MSG_MISSING_FILE);
198                 }
199                 else {
200                     packageName = getPackageText(ast);
201                     currentImportControl = root.locateFinest(packageName, fileName);
202                     if (currentImportControl == null) {
203                         log(ast, MSG_UNKNOWN_PKG);
204                     }
205                 }
206             }
207             else if (currentImportControl != null) {
208                 final String importText = getImportText(ast);
209                 final AccessResult access = currentImportControl.checkAccess(packageName, fileName,
210                         importText);
211                 if (access != AccessResult.ALLOWED) {
212                     log(ast, MSG_DISALLOWED, importText);
213                 }
214             }
215         }
216     }
217 
218     @Override
219     public Set<String> getExternalResourceLocations() {
220         return Set.of(file.toASCIIString());
221     }
222 
223     /**
224      * Returns package text.
225      *
226      * @param ast PACKAGE_DEF ast node
227      * @return String that represents full package name
228      */
229     private static String getPackageText(DetailAST ast) {
230         final DetailAST nameAST = ast.getLastChild().getPreviousSibling();
231         return FullIdent.createFullIdent(nameAST).getText();
232     }
233 
234     /**
235      * Returns import text.
236      *
237      * @param ast ast node that represents import
238      * @return String that represents importing class
239      */
240     private static String getImportText(DetailAST ast) {
241         final FullIdent imp;
242         if (ast.getType() == TokenTypes.IMPORT) {
243             imp = FullIdent.createFullIdentBelow(ast);
244         }
245         else {
246             // know it is a static import
247             imp = FullIdent.createFullIdent(ast
248                     .getFirstChild().getNextSibling());
249         }
250         return imp.getText();
251     }
252 
253     /**
254      * Setter to specify the location of the file containing the import control configuration.
255      * It can be a regular file, URL or resource path. It will try loading the path
256      * as a URL first, then as a file, and finally as a resource.
257      *
258      * @param uri the uri of the file to load.
259      * @throws IllegalArgumentException on error loading the file.
260      * @since 4.0
261      */
262     public void setFile(URI uri) {
263         // Handle empty param
264         if (uri != null) {
265             try {
266                 root = ImportControlLoader.load(uri);
267                 file = uri;
268             }
269             catch (CheckstyleException exc) {
270                 throw new IllegalArgumentException(UNABLE_TO_LOAD + uri, exc);
271             }
272         }
273     }
274 
275     /**
276      * Setter to specify the regular expression of file paths to which this check should apply.
277      * Files that don't match the pattern will not be checked. The pattern will be matched
278      * against the full absolute file path.
279      *
280      * @param pattern the file path regex this check should apply to.
281      * @since 7.5
282      */
283     public void setPath(Pattern pattern) {
284         path = pattern;
285     }
286 }