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}