1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.buildtools.checkstyle; 17 18 import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 19 import com.puppycrawl.tools.checkstyle.api.DetailAST; 20 import com.puppycrawl.tools.checkstyle.api.FullIdent; 21 import com.puppycrawl.tools.checkstyle.api.TokenTypes; 22 import java.util.Arrays; 23 import java.util.HashMap; 24 import java.util.HashSet; 25 import java.util.List; 26 import java.util.Set; 27 import java.util.regex.Pattern; 28 29 /** 30 * Verify that we're not using classes in rt.jar that aren't exported via the java.base module. 31 * 32 * If anything fails this check, it increases our module dependencies. If you absolutely must use one of these 33 * (e.g. java.beans) because it's fundamental to your functionality, you can suppress this checkstyle rule via 34 * {@link #setLegalPackages(String...)}, but know that it is not free - you're essentially adding a dependency 35 * for customers that use the module path. 36 */ 37 public class NonJavaBaseModuleCheck extends AbstractCheck { 38 private static final List<String> ILLEGAL_PACKAGES = Arrays.asList( 39 "java", 40 "javax", 41 "sun", 42 "apple", 43 "com.apple", 44 "com.oracle"); 45 46 private static final Set<String> LEGAL_PACKAGES = new HashSet<>(Arrays.asList( 47 "java.io", 48 "java.lang", 49 "java.lang.annotation", 50 "java.lang.invoke", 51 "java.lang.module", 52 "java.lang.ref", 53 "java.lang.reflect", 54 "java.math", 55 "java.net", 56 "java.net.spi", 57 "java.nio", 58 "java.nio.channels", 59 "java.nio.channels.spi", 60 "java.nio.charset", 61 "java.nio.charset.spi", 62 "java.nio.file", 63 "java.nio.file.attribute", 64 "java.nio.file.spi", 65 "java.security", 66 "java.security.acl", 67 "java.security.cert", 68 "java.security.interfaces", 69 "java.security.spec", 70 "java.text", 71 "java.text.spi", 72 "java.time", 73 "java.time.chrono", 74 "java.time.format", 75 "java.time.temporal", 76 "java.time.zone", 77 "java.util", 78 "java.util.concurrent", 79 "java.util.concurrent.atomic", 80 "java.util.concurrent.locks", 81 "java.util.function", 82 "java.util.jar", 83 "java.util.regex", 84 "java.util.spi", 85 "java.util.stream", 86 "java.util.jar", 87 "java.util.zip", 88 "javax.crypto", 89 "javax.crypto.interfaces", 90 "javax.crypto.spec", 91 "javax.net", 92 "javax.net.ssl", 93 "javax.security.auth", 94 "javax.security.auth.callback", 95 "javax.security.auth.login", 96 "javax.security.auth.spi", 97 "javax.security.auth.x500", 98 "javax.security.cert")); 99 100 private static final Pattern CLASSNAME_START_PATTERN = Pattern.compile("[A-Z]"); 101 102 private String currentSdkPackage; 103 104 private HashMap<String, Set<String>> additionalLegalPackagesBySdkPackage = new HashMap<>(); 105 106 /** 107 * Additional legal packages are formatted as "sdk.package.name:jdk.package.name,sdk.package.name2:jdk.package.name2". 108 * Multiple SDK packages can be repeated. 109 */ setLegalPackages(String... legalPackages)110 public void setLegalPackages(String... legalPackages) { 111 for (String additionalLegalPackage : legalPackages) { 112 String[] splitPackage = additionalLegalPackage.split(":", 2); 113 if (splitPackage.length != 2) { 114 throw new IllegalArgumentException("Invalid legal package definition '" + additionalLegalPackage + "'. Expected" 115 + " format is sdk.package.name:jdk.package.name"); 116 } 117 118 this.additionalLegalPackagesBySdkPackage.computeIfAbsent(splitPackage[0], k -> new HashSet<>()) 119 .add(splitPackage[1]); 120 } 121 } 122 123 @Override getDefaultTokens()124 public int[] getDefaultTokens() { 125 return getRequiredTokens(); 126 } 127 128 @Override getAcceptableTokens()129 public int[] getAcceptableTokens() { 130 return getRequiredTokens(); 131 } 132 133 @Override getRequiredTokens()134 public int[] getRequiredTokens() { 135 return new int[] { TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT, TokenTypes.PACKAGE_DEF }; 136 } 137 138 @Override visitToken(DetailAST ast)139 public void visitToken(DetailAST ast) { 140 if (ast.getType() == TokenTypes.PACKAGE_DEF) { 141 handlePackageDefToken(ast); 142 return; 143 } 144 145 handleImportToken(ast); 146 } 147 handlePackageDefToken(DetailAST ast)148 private void handlePackageDefToken(DetailAST ast) { 149 this.currentSdkPackage = FullIdent.createFullIdent(ast.getLastChild().getPreviousSibling()).getText(); 150 } 151 handleImportToken(DetailAST ast)152 private void handleImportToken(DetailAST ast) { 153 FullIdent importIdentifier; 154 if (ast.getType() == TokenTypes.IMPORT) { 155 importIdentifier = FullIdent.createFullIdentBelow(ast); 156 } else { 157 importIdentifier = FullIdent.createFullIdent(ast.getFirstChild().getNextSibling()); 158 } 159 160 String importText = importIdentifier.getText(); 161 if (isIllegalImport(importText) && !isLegalImport(importText)) { 162 log(ast, "Import '" + importText + "' uses a JDK class that is not in java.base. This essentially adds an " 163 + "additional module dependency. Don't suppress this rule unless it's absolutely required, and only " 164 + "suppress the specific package you need via checkstyle.xml instead of suppressing the entire rule."); 165 } 166 } 167 isIllegalImport(String importText)168 private boolean isIllegalImport(String importText) { 169 for (String illegalPackage : ILLEGAL_PACKAGES) { 170 if (importText.startsWith(illegalPackage + ".")) { 171 return true; 172 } 173 } 174 175 return false; 176 } 177 isLegalImport(String importText)178 private boolean isLegalImport(String importText) { 179 String importPackageWithTrailingDot = CLASSNAME_START_PATTERN.split(importText, 2)[0]; 180 String importPackage = importText.substring(0, importPackageWithTrailingDot.length() - 1); 181 182 if (LEGAL_PACKAGES.contains(importPackage)) { 183 return true; 184 } 185 186 if (additionalLegalPackagesBySdkPackage.entrySet() 187 .stream() 188 .anyMatch(e -> currentSdkPackage.startsWith(e.getKey()) && 189 e.getValue().contains(importPackage))) { 190 return true; 191 } 192 193 return false; 194 } 195 } 196