|
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