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 * <!DOCTYPE import-control PUBLIC
108 * "-//Checkstyle//DTD ImportControl Configuration 1.4//EN"
109 * "https://checkstyle.org/dtds/import_control_1_4.dtd">
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 }