001/* 002 * Copyright (C) 2022 - 2024, the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package io.github.ascopes.jct.assertions; 017 018import static io.github.ascopes.jct.utils.IterableUtils.requireAtLeastOne; 019import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues; 020import static java.util.function.Predicate.not; 021import static java.util.stream.Collectors.collectingAndThen; 022import static java.util.stream.Collectors.toUnmodifiableList; 023 024import io.github.ascopes.jct.diagnostics.TraceDiagnostic; 025import io.github.ascopes.jct.repr.TraceDiagnosticListRepresentation; 026import io.github.ascopes.jct.utils.StringUtils; 027import java.util.ArrayList; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Locale; 031import java.util.Objects; 032import java.util.function.Predicate; 033import java.util.stream.StreamSupport; 034import javax.tools.Diagnostic.Kind; 035import javax.tools.JavaFileObject; 036import org.assertj.core.api.AbstractListAssert; 037import org.jspecify.annotations.Nullable; 038 039//@formatter:off 040/** 041 * Assertions for a list of diagnostics. 042 * 043 * @author Ashley Scopes 044 * @since 0.0.1 045 */ 046public final class TraceDiagnosticListAssert 047 extends AbstractListAssert<TraceDiagnosticListAssert, List<? extends TraceDiagnostic<? extends JavaFileObject>>, TraceDiagnostic<? extends JavaFileObject>, TraceDiagnosticAssert> { 048 //@formatter:on 049 050 /** 051 * Initialize this assertion. 052 * 053 * @param traceDiagnostics the diagnostics to perform assertions on. 054 */ 055 @SuppressWarnings("DataFlowIssue") 056 public TraceDiagnosticListAssert( 057 @Nullable List<? extends TraceDiagnostic<? extends JavaFileObject>> traceDiagnostics 058 ) { 059 super(traceDiagnostics, TraceDiagnosticListAssert.class); 060 info.useRepresentation(TraceDiagnosticListRepresentation.getInstance()); 061 } 062 063 /** 064 * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#ERROR} 065 * kind. 066 * 067 * @return the assertion object for {@link Kind#ERROR} diagnostics. 068 * @throws AssertionError if the list is null. 069 */ 070 public TraceDiagnosticListAssert errors() { 071 return filteringByKinds(DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS); 072 } 073 074 /** 075 * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the 076 * {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING} kind. 077 * 078 * @return the assertion object for {@link Kind#WARNING} and {@link Kind#MANDATORY_WARNING} 079 * diagnostics. 080 * @throws AssertionError if the list is null. 081 */ 082 public TraceDiagnosticListAssert warnings() { 083 return filteringByKinds(DiagnosticKindAssert.WARNING_DIAGNOSTIC_KINDS); 084 } 085 086 /** 087 * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the 088 * {@link Kind#WARNING} kind. 089 * 090 * @return the assertion object for {@link Kind#WARNING} diagnostics. 091 * @throws AssertionError if the list is null. 092 */ 093 public TraceDiagnosticListAssert customWarnings() { 094 return filteringByKinds(Kind.WARNING); 095 } 096 097 /** 098 * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the 099 * {@link Kind#MANDATORY_WARNING} kind. 100 * 101 * @return the assertion object for {@link Kind#MANDATORY_WARNING} diagnostics. 102 * @throws AssertionError if the list is null. 103 */ 104 public TraceDiagnosticListAssert mandatoryWarnings() { 105 return filteringByKinds(Kind.MANDATORY_WARNING); 106 } 107 108 /** 109 * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#NOTE} 110 * kind. 111 * 112 * @return the assertion object for {@link Kind#NOTE} diagnostics. 113 * @throws AssertionError if the list is null. 114 */ 115 public TraceDiagnosticListAssert notes() { 116 return filteringByKinds(Kind.NOTE); 117 } 118 119 /** 120 * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#OTHER} 121 * kind. 122 * 123 * @return the assertion object for {@link Kind#OTHER} diagnostics. 124 * @throws AssertionError if the list is null. 125 */ 126 public TraceDiagnosticListAssert others() { 127 return filteringByKinds(Kind.OTHER); 128 } 129 130 /** 131 * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to any of the 132 * given {@link Kind kinds}. 133 * 134 * @param kinds the kinds to match. 135 * @return the assertion object for the filtered diagnostics. 136 * @throws AssertionError if this list is null. 137 * @throws NullPointerException if any of the kinds are null. 138 * @throws IllegalArgumentException if no kinds are provided. 139 */ 140 public TraceDiagnosticListAssert filteringByKinds(Kind... kinds) { 141 requireNonNullValues(kinds, "kinds"); 142 requireAtLeastOne(kinds, "kinds"); 143 return filteringByKinds(List.of(kinds)); 144 } 145 146 /** 147 * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to any of the 148 * given {@link Kind kinds}. 149 * 150 * @param kinds the kinds to match. 151 * @return the assertion object for the filtered diagnostics. 152 * @throws AssertionError if this list is null. 153 * @throws NullPointerException if any of the kinds are null. 154 */ 155 public TraceDiagnosticListAssert filteringByKinds(Iterable<Kind> kinds) { 156 requireNonNullValues(kinds, "kinds"); 157 return filteringBy(kindIsOneOf(kinds)); 158 } 159 160 /** 161 * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to none of the 162 * given {@link Kind kinds}. 163 * 164 * @param kinds kinds to ensure are not matched. 165 * @return the assertion object for the filtered diagnostics. 166 * @throws AssertionError if this list is null. 167 * @throws NullPointerException if any of the kinds are null. 168 * @throws IllegalArgumentException if no kinds are provided. 169 */ 170 public TraceDiagnosticListAssert excludingKinds(Kind... kinds) { 171 requireNonNullValues(kinds, "kinds"); 172 requireAtLeastOne(kinds, "kinds"); 173 return excludingKinds(List.of(kinds)); 174 } 175 176 /** 177 * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to none of the 178 * given {@link Kind kinds}. 179 * 180 * @param kinds the kinds to filter out. 181 * @return the assertion object for the filtered diagnostics. 182 * @throws AssertionError if this list is null. 183 * @throws NullPointerException if any of the kinds are null. 184 */ 185 @SuppressWarnings("ConstantValue") // actual CAN be null, IntelliJ just doesn't realise this. 186 public TraceDiagnosticListAssert excludingKinds(Iterable<Kind> kinds) { 187 requireNonNullValues(kinds, "kinds"); 188 isNotNull(); 189 return actual 190 .stream() 191 .filter(Objects::nonNull) 192 .filter(not(kindIsOneOf(kinds))) 193 .collect(collectingAndThen(toUnmodifiableList(), this::newAbstractIterableAssert)); 194 } 195 196 /** 197 * Assert that this list has no {@link Kind#ERROR} diagnostics. 198 * 199 * @return this assertion object for further call chaining. 200 * @throws AssertionError if this list is null. 201 */ 202 public TraceDiagnosticListAssert hasNoErrors() { 203 return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS); 204 } 205 206 /** 207 * Assert that this list has no {@link Kind#ERROR}, {@link Kind#WARNING}, or 208 * {@link Kind#MANDATORY_WARNING} diagnostics. 209 * 210 * @return this assertion object for further call chaining. 211 * @throws AssertionError if this list is null. 212 */ 213 public TraceDiagnosticListAssert hasNoErrorsOrWarnings() { 214 return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.WARNING_AND_ERROR_DIAGNOSTIC_KINDS); 215 } 216 217 /** 218 * Assert that this list has no {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING} 219 * diagnostics. 220 * 221 * @return this assertion object for further call chaining. 222 * @throws AssertionError if this list is null. 223 */ 224 public TraceDiagnosticListAssert hasNoWarnings() { 225 return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.WARNING_DIAGNOSTIC_KINDS); 226 } 227 228 /** 229 * Assert that this list has no {@link Kind#WARNING} diagnostics. 230 * 231 * @return this assertion object for further call chaining. 232 * @throws AssertionError if this list is null. 233 */ 234 public TraceDiagnosticListAssert hasNoCustomWarnings() { 235 return hasNoDiagnosticsOfKinds(Kind.WARNING); 236 } 237 238 /** 239 * Assert that this list has no {@link Kind#MANDATORY_WARNING} diagnostics. 240 * 241 * @return this assertion object for further call chaining. 242 * @throws AssertionError if this list is null. 243 */ 244 public TraceDiagnosticListAssert hasNoMandatoryWarnings() { 245 return hasNoDiagnosticsOfKinds(Kind.MANDATORY_WARNING); 246 } 247 248 /** 249 * Assert that this list has no {@link Kind#NOTE} diagnostics. 250 * 251 * @return this assertion object for further call chaining. 252 * @throws AssertionError if this list is null. 253 */ 254 public TraceDiagnosticListAssert hasNoNotes() { 255 return hasNoDiagnosticsOfKinds(Kind.NOTE); 256 } 257 258 /** 259 * Assert that this list has no {@link Kind#OTHER} diagnostics. 260 * 261 * @return this assertion object for further call chaining. 262 * @throws AssertionError if this list is null. 263 */ 264 public TraceDiagnosticListAssert hasNoOtherDiagnostics() { 265 return hasNoDiagnosticsOfKinds(Kind.OTHER); 266 } 267 268 /** 269 * Assert that this list has no diagnostics matching any of the given kinds. 270 * 271 * @param kinds kinds to check for. 272 * @return this assertion object for further call chaining. 273 * @throws AssertionError if the diagnostic list is null. 274 * @throws NullPointerException if the kind or more kinds are null. 275 */ 276 public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Kind... kinds) { 277 requireNonNullValues(kinds, "kinds"); 278 requireAtLeastOne(kinds, "kinds"); 279 return hasNoDiagnosticsOfKinds(List.of(kinds)); 280 } 281 282 /** 283 * Assert that this list has no diagnostics matching any of the given kinds. 284 * 285 * @param kinds the kinds to check for. 286 * @return this assertion object for further call chaining. 287 * @throws AssertionError if the diagnostic list is null. 288 * @throws NullPointerException if any of the kinds are null. 289 */ 290 public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Iterable<Kind> kinds) { 291 requireNonNullValues(kinds, "kinds"); 292 isNotNull(); 293 294 var actualDiagnostics = actual 295 .stream() 296 .filter(kindIsOneOf(kinds)) 297 .collect(toUnmodifiableList()); 298 299 if (actualDiagnostics.isEmpty()) { 300 return myself; 301 } 302 303 var allKindsString = StreamSupport 304 .stream(kinds.spliterator(), false) 305 .map(next -> next.name().toLowerCase(Locale.ROOT).replace('_', ' ')) 306 .distinct() 307 .sorted() 308 .collect(collectingAndThen( 309 toUnmodifiableList(), 310 names -> StringUtils.toWordedList(names, ", ", ", or ") 311 )); 312 313 throw failureWithActualExpected( 314 actualDiagnostics.size(), 315 0, 316 "Expected no %s diagnostics.\n\nDiagnostics:\n%s", 317 allKindsString, 318 TraceDiagnosticListRepresentation.getInstance().toStringOf(actualDiagnostics) 319 ); 320 } 321 322 /** 323 * Filter diagnostics by a given predicate and return an assertion object that applies to all 324 * diagnostics that match that predicate. 325 * 326 * @param predicate the predicate to match. 327 * @return the assertion object for the diagnostics that match. 328 * @throws NullPointerException if the predicate is null. 329 * @throws AssertionError if the diagnostic list is null. 330 */ 331 public TraceDiagnosticListAssert filteringBy( 332 Predicate<TraceDiagnostic<? extends JavaFileObject>> predicate 333 ) { 334 isNotNull(); 335 return filteredOn(predicate); 336 } 337 338 @Override 339 protected TraceDiagnosticAssert toAssert( 340 TraceDiagnostic<? extends JavaFileObject> value, 341 String description 342 ) { 343 return new TraceDiagnosticAssert(value).describedAs(description); 344 } 345 346 @Override 347 protected TraceDiagnosticListAssert newAbstractIterableAssert( 348 Iterable<? extends TraceDiagnostic<? extends JavaFileObject>> iterable 349 ) { 350 var list = new ArrayList<TraceDiagnostic<? extends JavaFileObject>>(); 351 iterable.forEach(list::add); 352 return new TraceDiagnosticListAssert(list); 353 } 354 355 private Predicate<@Nullable TraceDiagnostic<? extends JavaFileObject>> kindIsOneOf( 356 Iterable<Kind> kinds) { 357 var kindsSet = new LinkedHashSet<Kind>(); 358 kinds.forEach(kindsSet::add); 359 360 return diagnostic -> diagnostic != null && kindsSet.contains(diagnostic.getKind()); 361 } 362}