|
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 |
| - getRules().addAll(getProhibitObjectsRequireNonNull() |
99 |
| - .map((prohibit) -> prohibit ? noClassesShouldCallObjectsRequireNonNull() : Collections.emptyList())); |
100 |
| - 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(); |
101 | 80 | }
|
102 | 81 |
|
103 | 82 | @TaskAction
|
104 | 83 | void checkArchitecture() throws IOException {
|
105 |
| - JavaClasses javaClasses = new ClassFileImporter() |
106 |
| - .importPaths(this.classes.getFiles().stream().map(File::toPath).toList()); |
107 |
| - List<EvaluationResult> violations = getRules().get() |
108 |
| - .stream() |
109 |
| - .map((rule) -> rule.evaluate(javaClasses)) |
110 |
| - .filter(EvaluationResult::hasViolation) |
111 |
| - .toList(); |
| 84 | + JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths()); |
| 85 | + List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList(); |
112 | 86 | File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
|
113 |
| - outputFile.getParentFile().mkdirs(); |
| 87 | + writeViolationReport(violations, outputFile); |
114 | 88 | if (!violations.isEmpty()) {
|
115 |
| - StringBuilder report = new StringBuilder(); |
116 |
| - for (EvaluationResult violation : violations) { |
117 |
| - report.append(violation.getFailureReport()); |
118 |
| - report.append(String.format("%n")); |
119 |
| - } |
120 |
| - Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE, |
121 |
| - StandardOpenOption.TRUNCATE_EXISTING); |
122 | 89 | throw new GradleException("Architecture check failed. See '" + outputFile + "' for details.");
|
123 | 90 | }
|
124 |
| - else { |
125 |
| - outputFile.createNewFile(); |
126 |
| - } |
127 |
| - } |
128 |
| - |
129 |
| - private ArchRule allPackagesShouldBeFreeOfTangles() { |
130 |
| - return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles(); |
131 |
| - } |
132 |
| - |
133 |
| - private ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization() { |
134 |
| - return ArchRuleDefinition.methods() |
135 |
| - .that() |
136 |
| - .areAnnotatedWith("org.springframework.context.annotation.Bean") |
137 |
| - .and() |
138 |
| - .haveRawReturnType(Predicates.assignableTo("org.springframework.beans.factory.config.BeanPostProcessor")) |
139 |
| - .should(onlyHaveParametersThatWillNotCauseEagerInitialization()) |
140 |
| - .andShould() |
141 |
| - .beStatic() |
142 |
| - .allowEmptyShould(true); |
143 |
| - } |
144 |
| - |
145 |
| - private ArchCondition<JavaMethod> onlyHaveParametersThatWillNotCauseEagerInitialization() { |
146 |
| - DescribedPredicate<CanBeAnnotated> notAnnotatedWithLazy = DescribedPredicate |
147 |
| - .not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy")); |
148 |
| - DescribedPredicate<JavaClass> notOfASafeType = DescribedPredicate |
149 |
| - .not(Predicates.assignableTo("org.springframework.beans.factory.ObjectProvider") |
150 |
| - .or(Predicates.assignableTo("org.springframework.context.ApplicationContext")) |
151 |
| - .or(Predicates.assignableTo("org.springframework.core.env.Environment"))); |
152 |
| - return new ArchCondition<>("not have parameters that will cause eager initialization") { |
153 |
| - |
154 |
| - @Override |
155 |
| - public void check(JavaMethod item, ConditionEvents events) { |
156 |
| - item.getParameters() |
157 |
| - .stream() |
158 |
| - .filter(notAnnotatedWithLazy) |
159 |
| - .filter((parameter) -> notOfASafeType.test(parameter.getRawType())) |
160 |
| - .forEach((parameter) -> events.add(SimpleConditionEvent.violated(parameter, |
161 |
| - parameter.getDescription() + " will cause eager initialization as it is " |
162 |
| - + notAnnotatedWithLazy.getDescription() + " and is " |
163 |
| - + notOfASafeType.getDescription()))); |
164 |
| - } |
165 |
| - |
166 |
| - }; |
167 |
| - } |
168 |
| - |
169 |
| - private ArchRule allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters() { |
170 |
| - return ArchRuleDefinition.methods() |
171 |
| - .that() |
172 |
| - .areAnnotatedWith("org.springframework.context.annotation.Bean") |
173 |
| - .and() |
174 |
| - .haveRawReturnType( |
175 |
| - Predicates.assignableTo("org.springframework.beans.factory.config.BeanFactoryPostProcessor")) |
176 |
| - .should(haveNoParameters()) |
177 |
| - .andShould() |
178 |
| - .beStatic() |
179 |
| - .allowEmptyShould(true); |
180 |
| - } |
181 |
| - |
182 |
| - private ArchCondition<JavaMethod> haveNoParameters() { |
183 |
| - return new ArchCondition<>("have no parameters") { |
184 |
| - |
185 |
| - @Override |
186 |
| - public void check(JavaMethod item, ConditionEvents events) { |
187 |
| - List<JavaParameter> parameters = item.getParameters(); |
188 |
| - if (!parameters.isEmpty()) { |
189 |
| - events |
190 |
| - .add(SimpleConditionEvent.violated(item, item.getDescription() + " should have no parameters")); |
191 |
| - } |
192 |
| - } |
193 |
| - |
194 |
| - }; |
195 | 91 | }
|
196 | 92 |
|
197 |
| - private ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() { |
198 |
| - return ArchRuleDefinition.noClasses() |
199 |
| - .should() |
200 |
| - .callMethod(String.class, "toLowerCase") |
201 |
| - .because("String.toLowerCase(Locale.ROOT) should be used instead"); |
| 93 | + private List<Path> classFilesPaths() { |
| 94 | + return this.classes.getFiles().stream().map(File::toPath).toList(); |
202 | 95 | }
|
203 | 96 |
|
204 |
| - private ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() { |
205 |
| - return ArchRuleDefinition.noClasses() |
206 |
| - .should() |
207 |
| - .callMethod(String.class, "toUpperCase") |
208 |
| - .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)); |
209 | 99 | }
|
210 | 100 |
|
211 |
| - private ArchRule noClassesShouldCallStepVerifierStepVerifyComplete() { |
212 |
| - return ArchRuleDefinition.noClasses() |
213 |
| - .should() |
214 |
| - .callMethod("reactor.test.StepVerifier$Step", "verifyComplete") |
215 |
| - .because("it can block indefinitely and expectComplete().verify(Duration) should be used instead"); |
216 |
| - } |
217 |
| - |
218 |
| - private ArchRule noClassesShouldConfigureDefaultStepVerifierTimeout() { |
219 |
| - return ArchRuleDefinition.noClasses() |
220 |
| - .should() |
221 |
| - .callMethod("reactor.test.StepVerifier", "setDefaultTimeout", "java.time.Duration") |
222 |
| - .because("expectComplete().verify(Duration) should be used instead"); |
223 |
| - } |
224 |
| - |
225 |
| - private ArchRule noClassesShouldCallCollectorsToList() { |
226 |
| - return ArchRuleDefinition.noClasses() |
227 |
| - .should() |
228 |
| - .callMethod(Collectors.class, "toList") |
229 |
| - .because("java.util.stream.Stream.toList() should be used instead"); |
230 |
| - } |
231 |
| - |
232 |
| - private ArchRule noClassesShouldCallURLEncoderWithStringEncoding() { |
233 |
| - return ArchRuleDefinition.noClasses() |
234 |
| - .should() |
235 |
| - .callMethod(URLEncoder.class, "encode", String.class, String.class) |
236 |
| - .because("java.net.URLEncoder.encode(String s, Charset charset) should be used instead"); |
237 |
| - } |
238 |
| - |
239 |
| - private ArchRule noClassesShouldCallURLDecoderWithStringEncoding() { |
240 |
| - return ArchRuleDefinition.noClasses() |
241 |
| - .should() |
242 |
| - .callMethod(URLDecoder.class, "decode", String.class, String.class) |
243 |
| - .because("java.net.URLDecoder.decode(String s, Charset charset) should be used instead"); |
244 |
| - } |
245 |
| - |
246 |
| - private ArchRule noClassesShouldLoadResourcesUsingResourceUtils() { |
247 |
| - return ArchRuleDefinition.noClasses() |
248 |
| - .should() |
249 |
| - .callMethodWhere(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class))) |
250 |
| - .and(JavaCall.Predicates.target(HasName.Predicates.name("getURL"))) |
251 |
| - .and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class))) |
252 |
| - .or(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class))) |
253 |
| - .and(JavaCall.Predicates.target(HasName.Predicates.name("getFile"))) |
254 |
| - .and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class))))) |
255 |
| - .because("org.springframework.boot.io.ApplicationResourceLoader should be used instead"); |
256 |
| - } |
257 |
| - |
258 |
| - private List<ArchRule> noClassesShouldCallObjectsRequireNonNull() { |
259 |
| - return List.of( |
260 |
| - ArchRuleDefinition.noClasses() |
261 |
| - .should() |
262 |
| - .callMethod(Objects.class, "requireNonNull", Object.class, String.class) |
263 |
| - .because("org.springframework.utils.Assert.notNull(Object, String) should be used instead"), |
264 |
| - ArchRuleDefinition.noClasses() |
265 |
| - .should() |
266 |
| - .callMethod(Objects.class, "requireNonNull", Object.class, Supplier.class) |
267 |
| - .because("org.springframework.utils.Assert.notNull(Object, Supplier) should be used instead")); |
268 |
| - } |
269 |
| - |
270 |
| - private ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType() { |
271 |
| - return ArchRuleDefinition.methods() |
272 |
| - .that() |
273 |
| - .areAnnotatedWith("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean") |
274 |
| - .should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType()) |
275 |
| - .allowEmptyShould(true); |
276 |
| - } |
277 |
| - |
278 |
| - private ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() { |
279 |
| - return new ArchCondition<>("not specify only a type that is the same as the method's return type") { |
280 |
| - |
281 |
| - @Override |
282 |
| - public void check(JavaMethod item, ConditionEvents events) { |
283 |
| - JavaAnnotation<JavaMethod> conditional = item |
284 |
| - .getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean"); |
285 |
| - Map<String, Object> properties = conditional.getProperties(); |
286 |
| - if (!properties.containsKey("type") && !properties.containsKey("name")) { |
287 |
| - conditional.get("value").ifPresent((value) -> { |
288 |
| - JavaType[] types = (JavaType[]) value; |
289 |
| - if (types.length == 1 && item.getReturnType().equals(types[0])) { |
290 |
| - events.add(SimpleConditionEvent.violated(item, conditional.getDescription() |
291 |
| - + " should not specify only a value that is the same as the method's return type")); |
292 |
| - } |
293 |
| - }); |
294 |
| - } |
295 |
| - } |
296 |
| - |
297 |
| - }; |
| 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); |
298 | 110 | }
|
299 | 111 |
|
300 | 112 | public void setClasses(FileCollection classes) {
|
@@ -328,9 +140,7 @@ final FileTree getInputClasses() {
|
328 | 140 | @Internal
|
329 | 141 | public abstract Property<Boolean> getProhibitObjectsRequireNonNull();
|
330 | 142 |
|
331 |
| - @Input |
332 |
| - // The rules themselves can't be an input as they aren't serializable so we use |
333 |
| - // their descriptions instead |
| 143 | + @Input // Use descriptions as input since rules aren't serializable |
334 | 144 | abstract ListProperty<String> getRuleDescriptions();
|
335 | 145 |
|
336 | 146 | }
|
0 commit comments