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