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 }