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.combineOneOrMore; 019import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues; 020import static java.util.Objects.requireNonNull; 021import static java.util.function.Predicate.not; 022import static java.util.stream.Collectors.collectingAndThen; 023import static java.util.stream.Collectors.toUnmodifiableList; 024 025import io.github.ascopes.jct.diagnostics.TraceDiagnostic; 026import io.github.ascopes.jct.repr.TraceDiagnosticListRepresentation; 027import io.github.ascopes.jct.utils.StringUtils; 028import java.util.ArrayList; 029import java.util.LinkedHashSet; 030import java.util.List; 031import java.util.Locale; 032import java.util.Objects; 033import java.util.function.Predicate; 034import java.util.stream.StreamSupport; 035import javax.tools.Diagnostic.Kind; 036import javax.tools.JavaFileObject; 037import org.apiguardian.api.API; 038import org.apiguardian.api.API.Status; 039import org.assertj.core.api.AbstractListAssert; 040import org.jspecify.annotations.Nullable; 041 042/** 043 * Assertions for a list of diagnostics. 044 * 045 * @author Ashley Scopes 046 * @since 0.0.1 047 */ 048@API(since = "0.0.1", status = Status.STABLE) 049public final class TraceDiagnosticListAssert 050 extends 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 kind the first kind to match. 137 * @param moreKinds additional kinds to match. 138 * @return the assertion object for the filtered diagnostics. 139 * @throws AssertionError if this list is null. 140 * @throws NullPointerException if any of the kinds are null. 141 */ 142 public TraceDiagnosticListAssert filteringByKinds(Kind kind, Kind... moreKinds) { 143 requireNonNull(kind, "kind"); 144 requireNonNullValues(moreKinds, "moreKinds"); 145 return filteringByKinds(combineOneOrMore(kind, moreKinds)); 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 kind the first kind to ensure are not matched. 167 * @param moreKinds additional kinds to ensure are not matched. 168 * @return the assertion object for the filtered diagnostics. 169 * @throws AssertionError if this list is null. 170 * @throws NullPointerException if any of the kinds are null. 171 */ 172 public TraceDiagnosticListAssert excludingKinds(Kind kind, Kind... moreKinds) { 173 requireNonNull(kind, "kind"); 174 requireNonNullValues(moreKinds, "moreKinds"); 175 return excludingKinds(combineOneOrMore(kind, moreKinds)); 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 public TraceDiagnosticListAssert excludingKinds(Iterable<Kind> kinds) { 188 requireNonNullValues(kinds, "kinds"); 189 isNotNull(); 190 //noinspection ConstantValue -- actual CAN be null, IntelliJ just doesn't realise this. 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 kind the first kind to check for. 274 * @param moreKinds any additional kinds to check for. 275 * @return this assertion object for further call chaining. 276 * @throws AssertionError if the diagnostic list is null. 277 * @throws NullPointerException if the kind or more kinds are null. 278 */ 279 public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Kind kind, Kind... moreKinds) { 280 requireNonNull(kind, "kind"); 281 requireNonNullValues(moreKinds, "moreKinds"); 282 return hasNoDiagnosticsOfKinds(combineOneOrMore(kind, moreKinds)); 283 } 284 285 /** 286 * Assert that this list has no diagnostics matching any of the given kinds. 287 * 288 * @param kinds the kinds to check for. 289 * @return this assertion object for further call chaining. 290 * @throws AssertionError if the diagnostic list is null. 291 * @throws NullPointerException if any of the kinds are null. 292 */ 293 public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Iterable<Kind> kinds) { 294 requireNonNullValues(kinds, "kinds"); 295 isNotNull(); 296 297 var actualDiagnostics = actual 298 .stream() 299 .filter(kindIsOneOf(kinds)) 300 .collect(toUnmodifiableList()); 301 302 if (actualDiagnostics.isEmpty()) { 303 return myself; 304 } 305 306 var allKindsString = StreamSupport 307 .stream(kinds.spliterator(), false) 308 .map(next -> next.name().toLowerCase(Locale.ROOT).replace('_', ' ')) 309 .distinct() 310 .sorted() 311 .collect(collectingAndThen( 312 toUnmodifiableList(), 313 names -> StringUtils.toWordedList(names, ", ", ", or ") 314 )); 315 316 throw failureWithActualExpected( 317 actualDiagnostics.size(), 318 0, 319 "Expected no %s diagnostics.\n\nDiagnostics:\n%s", 320 allKindsString, 321 TraceDiagnosticListRepresentation.getInstance().toStringOf(actualDiagnostics) 322 ); 323 } 324 325 /** 326 * Filter diagnostics by a given predicate and return an assertion object that applies to all 327 * diagnostics that match that predicate. 328 * 329 * @param predicate the predicate to match. 330 * @return the assertion object for the diagnostics that match. 331 * @throws NullPointerException if the predicate is null. 332 * @throws AssertionError if the diagnostic list is null. 333 */ 334 public TraceDiagnosticListAssert filteringBy( 335 Predicate<TraceDiagnostic<? extends JavaFileObject>> predicate 336 ) { 337 isNotNull(); 338 return filteredOn(predicate); 339 } 340 341 @Override 342 protected TraceDiagnosticAssert toAssert( 343 TraceDiagnostic<? extends JavaFileObject> value, 344 String description 345 ) { 346 return new TraceDiagnosticAssert(value).describedAs(description); 347 } 348 349 @Override 350 protected TraceDiagnosticListAssert newAbstractIterableAssert( 351 Iterable<? extends TraceDiagnostic<? extends JavaFileObject>> iterable 352 ) { 353 var list = new ArrayList<TraceDiagnostic<? extends JavaFileObject>>(); 354 iterable.forEach(list::add); 355 return new TraceDiagnosticListAssert(list); 356 } 357 358 private Predicate<@Nullable TraceDiagnostic<? extends JavaFileObject>> kindIsOneOf(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}