|
18 | 18 |
|
19 | 19 | import java.io.File;
|
20 | 20 | import java.io.IOException;
|
21 |
| -import java.net.URLDecoder; |
22 |
| -import java.net.URLEncoder; |
23 | 21 | import java.nio.file.Files;
|
| 22 | +import java.nio.file.Path; |
24 | 23 | import java.nio.file.StandardOpenOption;
|
25 | 24 | import java.util.Collections;
|
26 | 25 | import java.util.List;
|
27 |
| -import java.util.Map; |
28 |
| -import java.util.Objects; |
29 | 26 | import java.util.function.Supplier;
|
30 |
| -import java.util.stream.Collectors; |
| 27 | +import java.util.stream.Stream; |
31 | 28 |
|
32 |
| -import com.tngtech.archunit.base.DescribedPredicate; |
33 |
| -import com.tngtech.archunit.core.domain.JavaAnnotation; |
34 |
| -import com.tngtech.archunit.core.domain.JavaCall; |
35 |
| -import com.tngtech.archunit.core.domain.JavaClass; |
36 |
| -import com.tngtech.archunit.core.domain.JavaClass.Predicates; |
37 | 29 | import com.tngtech.archunit.core.domain.JavaClasses;
|
38 |
| -import com.tngtech.archunit.core.domain.JavaMethod; |
39 |
| -import com.tngtech.archunit.core.domain.JavaParameter; |
40 |
| -import com.tngtech.archunit.core.domain.JavaType; |
41 |
| -import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; |
42 |
| -import com.tngtech.archunit.core.domain.properties.HasName; |
43 |
| -import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With; |
44 |
| -import com.tngtech.archunit.core.domain.properties.HasParameterTypes; |
45 | 30 | import com.tngtech.archunit.core.importer.ClassFileImporter;
|
46 |
| -import com.tngtech.archunit.lang.ArchCondition; |
47 | 31 | import com.tngtech.archunit.lang.ArchRule;
|
48 |
| -import com.tngtech.archunit.lang.ConditionEvents; |
49 | 32 | import com.tngtech.archunit.lang.EvaluationResult;
|
50 |
| -import com.tngtech.archunit.lang.SimpleConditionEvent; |
51 |
| -import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; |
52 |
| -import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition; |
53 | 33 | import org.gradle.api.DefaultTask;
|
54 | 34 | import org.gradle.api.GradleException;
|
55 | 35 | import org.gradle.api.Task;
|
| 36 | +import org.gradle.api.Transformer; |
56 | 37 | import org.gradle.api.file.DirectoryProperty;
|
57 | 38 | import org.gradle.api.file.FileCollection;
|
58 | 39 | import org.gradle.api.file.FileTree;
|
|
69 | 50 | import org.gradle.api.tasks.SkipWhenEmpty;
|
70 | 51 | import org.gradle.api.tasks.TaskAction;
|
71 | 52 |
|
72 |
| -import org.springframework.util.ResourceUtils; |
73 |
| - |
74 | 53 | /**
|
75 | 54 | * {@link Task} that checks for architecture problems.
|
76 | 55 | *
|
77 | 56 | * @author Andy Wilkinson
|
78 | 57 | * @author Yanming Zhou
|
79 | 58 | * @author Scott Frederick
|
80 | 59 | * @author Ivan Malutin
|
| 60 | + * @author Phillip Webb |
81 | 61 | */
|
82 | 62 | public abstract class ArchitectureCheck extends DefaultTask {
|
83 | 63 |
|
84 | 64 | private FileCollection classes;
|
85 | 65 |
|
86 | 66 | public ArchitectureCheck() {
|
87 | 67 | getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
|
88 |
| - getProhibitObjectsRequireNonNull().convention(true); |
89 |
| - getRules().addAll(allPackagesShouldBeFreeOfTangles(), |
90 |
| - allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization(), |
91 |
| - allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(), |
92 |
| - noClassesShouldCallStepVerifierStepVerifyComplete(), |
93 |
| - noClassesShouldConfigureDefaultStepVerifierTimeout(), noClassesShouldCallCollectorsToList(), |
94 |
| - noClassesShouldCallURLEncoderWithStringEncoding(), noClassesShouldCallURLDecoderWithStringEncoding(), |
95 |
| - noClassesShouldLoadResourcesUsingResourceUtils(), noClassesShouldCallStringToUpperCaseWithoutLocale(), |
96 |
| - noClassesShouldCallStringToLowerCaseWithoutLocale(), |
97 |
| - conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType(), |
98 |
| - enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType()); |
99 |
| - getRules().addAll(getProhibitObjectsRequireNonNull() |
100 |
| - .map((prohibit) -> prohibit ? noClassesShouldCallObjectsRequireNonNull() : Collections.emptyList())); |
101 |
| - getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList())); |
| 68 | + getRules().addAll(getProhibitObjectsRequireNonNull().convention(true) |
| 69 | + .map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull))); |
| 70 | + getRules().addAll(ArchitectureRules.standard()); |
| 71 | + getRuleDescriptions().set(getRules().map(this::asDescriptions)); |
| 72 | + } |
| 73 | + |
| 74 | + private Transformer<List<ArchRule>, Boolean> whenTrue(Supplier<List<ArchRule>> rules) { |
| 75 | + return (in) -> (!in) ? Collections.emptyList() : rules.get(); |
| 76 | + } |
| 77 | + |
| 78 | + private List<String> asDescriptions(List<ArchRule> rules) { |
| 79 | + return rules.stream().map(ArchRule::getDescription).toList(); |
102 | 80 | }
|
103 | 81 |
|
104 | 82 | @TaskAction
|
105 | 83 | void checkArchitecture() throws IOException {
|
106 |
| - JavaClasses javaClasses = new ClassFileImporter() |
107 |
| - .importPaths(this.classes.getFiles().stream().map(File::toPath).toList()); |
108 |
| - List<EvaluationResult> violations = getRules().get() |
109 |
| - .stream() |
110 |
| - .map((rule) -> rule.evaluate(javaClasses)) |
111 |
| - .filter(EvaluationResult::hasViolation) |
112 |
| - .toList(); |
| 84 | + JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths()); |
| 85 | + List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList(); |
113 | 86 | File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
|
114 |
| - outputFile.getParentFile().mkdirs(); |
| 87 | + writeViolationReport(violations, outputFile); |
115 | 88 | if (!violations.isEmpty()) {
|
116 |
| - StringBuilder report = new StringBuilder(); |
117 |
| - for (EvaluationResult violation : violations) { |
118 |
| - report.append(violation.getFailureReport()); |
119 |
| - report.append(String.format("%n")); |
120 |
| - } |
121 |
| - Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE, |
122 |
| - StandardOpenOption.TRUNCATE_EXISTING); |
123 | 89 | throw new GradleException("Architecture check failed. See '" + outputFile + "' for details.");
|
124 | 90 | }
|
125 |
| - else { |
126 |
| - outputFile.createNewFile(); |
127 |
| - } |
128 |
| - } |
129 |
| - |
130 |
| - private ArchRule allPackagesShouldBeFreeOfTangles() { |
131 |
| - return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles(); |
132 |
| - } |
133 |
| - |
134 |
| - private ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization() { |
135 |
| - return ArchRuleDefinition.methods() |
136 |
| - .that() |
137 |
| - .areAnnotatedWith("org.springframework.context.annotation.Bean") |
138 |
| - .and() |
139 |
| - .haveRawReturnType(Predicates.assignableTo("org.springframework.beans.factory.config.BeanPostProcessor")) |
140 |
| - .should(onlyHaveParametersThatWillNotCauseEagerInitialization()) |
141 |
| - .andShould() |
142 |
| - .beStatic() |
143 |
| - .allowEmptyShould(true); |
144 |
| - } |
145 |
| - |
146 |
| - private ArchCondition<JavaMethod> onlyHaveParametersThatWillNotCauseEagerInitialization() { |
147 |
| - DescribedPredicate<CanBeAnnotated> notAnnotatedWithLazy = DescribedPredicate |
148 |
| - .not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy")); |
149 |
| - DescribedPredicate<JavaClass> notOfASafeType = DescribedPredicate |
150 |
| - .not(Predicates.assignableTo("org.springframework.beans.factory.ObjectProvider") |
151 |
| - .or(Predicates.assignableTo("org.springframework.context.ApplicationContext")) |
152 |
| - .or(Predicates.assignableTo("org.springframework.core.env.Environment"))); |
153 |
| - return new ArchCondition<>("not have parameters that will cause eager initialization") { |
154 |
| - |
155 |
| - @Override |
156 |
| - public void check(JavaMethod item, ConditionEvents events) { |
157 |
| - item.getParameters() |
158 |
| - .stream() |
159 |
| - .filter(notAnnotatedWithLazy) |
160 |
| - .filter((parameter) -> notOfASafeType.test(parameter.getRawType())) |
161 |
| - .forEach((parameter) -> events.add(SimpleConditionEvent.violated(parameter, |
162 |
| - parameter.getDescription() + " will cause eager initialization as it is " |
163 |
| - + notAnnotatedWithLazy.getDescription() + " and is " |
164 |
| - + notOfASafeType.getDescription()))); |
165 |
| - } |
166 |
| - |
167 |
| - }; |
168 |
| - } |
169 |
| - |
170 |
| - private ArchRule allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters() { |
171 |
| - return ArchRuleDefinition.methods() |
172 |
| - .that() |
173 |
| - .areAnnotatedWith("org.springframework.context.annotation.Bean") |
174 |
| - .and() |
175 |
| - .haveRawReturnType( |
176 |
| - Predicates.assignableTo("org.springframework.beans.factory.config.BeanFactoryPostProcessor")) |
177 |
| - .should(onlyInjectEnvironment()) |
178 |
| - .andShould() |
179 |
| - .beStatic() |
180 |
| - .allowEmptyShould(true); |
181 |
| - } |
182 |
| - |
183 |
| - private ArchCondition<JavaMethod> onlyInjectEnvironment() { |
184 |
| - return new ArchCondition<>("only inject Environment") { |
185 |
| - |
186 |
| - @Override |
187 |
| - public void check(JavaMethod item, ConditionEvents events) { |
188 |
| - List<JavaParameter> parameters = item.getParameters(); |
189 |
| - for (JavaParameter parameter : parameters) { |
190 |
| - if (!"org.springframework.core.env.Environment".equals(parameter.getType().getName())) { |
191 |
| - events.add(SimpleConditionEvent.violated(item, |
192 |
| - item.getDescription() + " should only inject Environment")); |
193 |
| - } |
194 |
| - } |
195 |
| - } |
196 |
| - |
197 |
| - }; |
198 | 91 | }
|
199 | 92 |
|
200 |
| - private ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() { |
201 |
| - return ArchRuleDefinition.noClasses() |
202 |
| - .should() |
203 |
| - .callMethod(String.class, "toLowerCase") |
204 |
| - .because("String.toLowerCase(Locale.ROOT) should be used instead"); |
| 93 | + private List<Path> classFilesPaths() { |
| 94 | + return this.classes.getFiles().stream().map(File::toPath).toList(); |
205 | 95 | }
|
206 | 96 |
|
207 |
| - private ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() { |
208 |
| - return ArchRuleDefinition.noClasses() |
209 |
| - .should() |
210 |
| - .callMethod(String.class, "toUpperCase") |
211 |
| - .because("String.toUpperCase(Locale.ROOT) should be used instead"); |
| 97 | + private Stream<EvaluationResult> evaluate(JavaClasses javaClasses) { |
| 98 | + return getRules().get().stream().map((rule) -> rule.evaluate(javaClasses)); |
212 | 99 | }
|
213 | 100 |
|
214 |
| - private ArchRule noClassesShouldCallStepVerifierStepVerifyComplete() { |
215 |
| - return ArchRuleDefinition.noClasses() |
216 |
| - .should() |
217 |
| - .callMethod("reactor.test.StepVerifier$Step", "verifyComplete") |
218 |
| - .because("it can block indefinitely and expectComplete().verify(Duration) should be used instead"); |
219 |
| - } |
220 |
| - |
221 |
| - private ArchRule noClassesShouldConfigureDefaultStepVerifierTimeout() { |
222 |
| - return ArchRuleDefinition.noClasses() |
223 |
| - .should() |
224 |
| - .callMethod("reactor.test.StepVerifier", "setDefaultTimeout", "java.time.Duration") |
225 |
| - .because("expectComplete().verify(Duration) should be used instead"); |
226 |
| - } |
227 |
| - |
228 |
| - private ArchRule noClassesShouldCallCollectorsToList() { |
229 |
| - return ArchRuleDefinition.noClasses() |
230 |
| - .should() |
231 |
| - .callMethod(Collectors.class, "toList") |
232 |
| - .because("java.util.stream.Stream.toList() should be used instead"); |
233 |
| - } |
234 |
| - |
235 |
| - private ArchRule noClassesShouldCallURLEncoderWithStringEncoding() { |
236 |
| - return ArchRuleDefinition.noClasses() |
237 |
| - .should() |
238 |
| - .callMethod(URLEncoder.class, "encode", String.class, String.class) |
239 |
| - .because("java.net.URLEncoder.encode(String s, Charset charset) should be used instead"); |
240 |
| - } |
241 |
| - |
242 |
| - private ArchRule noClassesShouldCallURLDecoderWithStringEncoding() { |
243 |
| - return ArchRuleDefinition.noClasses() |
244 |
| - .should() |
245 |
| - .callMethod(URLDecoder.class, "decode", String.class, String.class) |
246 |
| - .because("java.net.URLDecoder.decode(String s, Charset charset) should be used instead"); |
247 |
| - } |
248 |
| - |
249 |
| - private ArchRule noClassesShouldLoadResourcesUsingResourceUtils() { |
250 |
| - return ArchRuleDefinition.noClasses() |
251 |
| - .should() |
252 |
| - .callMethodWhere(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class))) |
253 |
| - .and(JavaCall.Predicates.target(HasName.Predicates.name("getURL"))) |
254 |
| - .and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class))) |
255 |
| - .or(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class))) |
256 |
| - .and(JavaCall.Predicates.target(HasName.Predicates.name("getFile"))) |
257 |
| - .and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class))))) |
258 |
| - .because("org.springframework.boot.io.ApplicationResourceLoader should be used instead"); |
259 |
| - } |
260 |
| - |
261 |
| - private List<ArchRule> noClassesShouldCallObjectsRequireNonNull() { |
262 |
| - return List.of( |
263 |
| - ArchRuleDefinition.noClasses() |
264 |
| - .should() |
265 |
| - .callMethod(Objects.class, "requireNonNull", Object.class, String.class) |
266 |
| - .because("org.springframework.utils.Assert.notNull(Object, String) should be used instead"), |
267 |
| - ArchRuleDefinition.noClasses() |
268 |
| - .should() |
269 |
| - .callMethod(Objects.class, "requireNonNull", Object.class, Supplier.class) |
270 |
| - .because("org.springframework.utils.Assert.notNull(Object, Supplier) should be used instead")); |
271 |
| - } |
272 |
| - |
273 |
| - private ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType() { |
274 |
| - return ArchRuleDefinition.methods() |
275 |
| - .that() |
276 |
| - .areAnnotatedWith("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean") |
277 |
| - .should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType()) |
278 |
| - .allowEmptyShould(true); |
279 |
| - } |
280 |
| - |
281 |
| - private ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() { |
282 |
| - return new ArchCondition<>("not specify only a type that is the same as the method's return type") { |
283 |
| - |
284 |
| - @Override |
285 |
| - public void check(JavaMethod item, ConditionEvents events) { |
286 |
| - JavaAnnotation<JavaMethod> conditional = item |
287 |
| - .getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean"); |
288 |
| - Map<String, Object> properties = conditional.getProperties(); |
289 |
| - if (!properties.containsKey("type") && !properties.containsKey("name")) { |
290 |
| - conditional.get("value").ifPresent((value) -> { |
291 |
| - JavaType[] types = (JavaType[]) value; |
292 |
| - if (types.length == 1 && item.getReturnType().equals(types[0])) { |
293 |
| - events.add(SimpleConditionEvent.violated(item, conditional.getDescription() |
294 |
| - + " should not specify only a value that is the same as the method's return type")); |
295 |
| - } |
296 |
| - }); |
297 |
| - } |
298 |
| - } |
299 |
| - |
300 |
| - }; |
301 |
| - } |
302 |
| - |
303 |
| - private ArchRule enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType() { |
304 |
| - return ArchRuleDefinition.methods() |
305 |
| - .that() |
306 |
| - .areAnnotatedWith("org.junit.jupiter.params.provider.EnumSource") |
307 |
| - .should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType()) |
308 |
| - .allowEmptyShould(true); |
309 |
| - } |
310 |
| - |
311 |
| - private ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType() { |
312 |
| - return new ArchCondition<>("not specify only a type that is the same as the method's parameter type") { |
313 |
| - |
314 |
| - @Override |
315 |
| - public void check(JavaMethod item, ConditionEvents events) { |
316 |
| - JavaAnnotation<JavaMethod> conditional = item |
317 |
| - .getAnnotationOfType("org.junit.jupiter.params.provider.EnumSource"); |
318 |
| - Map<String, Object> properties = conditional.getProperties(); |
319 |
| - if (properties.size() == 1 && item.getParameterTypes().size() == 1) { |
320 |
| - conditional.get("value").ifPresent((value) -> { |
321 |
| - if (value.equals(item.getParameterTypes().get(0))) { |
322 |
| - events.add(SimpleConditionEvent.violated(item, conditional.getDescription() |
323 |
| - + " should not specify only a value that is the same as the method's parameter type")); |
324 |
| - } |
325 |
| - }); |
326 |
| - } |
327 |
| - } |
328 |
| - |
329 |
| - }; |
| 101 | + private void writeViolationReport(List<EvaluationResult> violations, File outputFile) throws IOException { |
| 102 | + outputFile.getParentFile().mkdirs(); |
| 103 | + StringBuilder report = new StringBuilder(); |
| 104 | + for (EvaluationResult violation : violations) { |
| 105 | + report.append(violation.getFailureReport()); |
| 106 | + report.append(String.format("%n")); |
| 107 | + } |
| 108 | + Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE, |
| 109 | + StandardOpenOption.TRUNCATE_EXISTING); |
330 | 110 | }
|
331 | 111 |
|
332 | 112 | public void setClasses(FileCollection classes) {
|
@@ -360,9 +140,7 @@ final FileTree getInputClasses() {
|
360 | 140 | @Internal
|
361 | 141 | public abstract Property<Boolean> getProhibitObjectsRequireNonNull();
|
362 | 142 |
|
363 |
| - @Input |
364 |
| - // The rules themselves can't be an input as they aren't serializable so we use |
365 |
| - // their descriptions instead |
| 143 | + @Input // Use descriptions as input since rules aren't serializable |
366 | 144 | abstract ListProperty<String> getRuleDescriptions();
|
367 | 145 |
|
368 | 146 | }
|
0 commit comments