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