001/*
002 * Copyright (C) 2022 - 2023, 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  public TraceDiagnosticListAssert(
058      @Nullable List<@Nullable ? extends TraceDiagnostic<? extends JavaFileObject>> traceDiagnostics
059  ) {
060    super(traceDiagnostics, TraceDiagnosticListAssert.class);
061    info.useRepresentation(TraceDiagnosticListRepresentation.getInstance());
062  }
063
064  /**
065   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#ERROR}
066   * kind.
067   *
068   * @return the assertion object for {@link Kind#ERROR} diagnostics.
069   * @throws AssertionError if the list is null.
070   */
071  public TraceDiagnosticListAssert errors() {
072    return filteringByKinds(DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS);
073  }
074
075  /**
076   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
077   * {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING} kind.
078   *
079   * @return the assertion object for {@link Kind#WARNING} and {@link Kind#MANDATORY_WARNING}
080   *     diagnostics.
081   * @throws AssertionError if the list is null.
082   */
083  public TraceDiagnosticListAssert warnings() {
084    return filteringByKinds(DiagnosticKindAssert.WARNING_DIAGNOSTIC_KINDS);
085  }
086
087  /**
088   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
089   * {@link Kind#WARNING} kind.
090   *
091   * @return the assertion object for {@link Kind#WARNING} diagnostics.
092   * @throws AssertionError if the list is null.
093   */
094  public TraceDiagnosticListAssert customWarnings() {
095    return filteringByKinds(Kind.WARNING);
096  }
097
098  /**
099   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
100   * {@link Kind#MANDATORY_WARNING} kind.
101   *
102   * @return the assertion object for {@link Kind#MANDATORY_WARNING} diagnostics.
103   * @throws AssertionError if the list is null.
104   */
105  public TraceDiagnosticListAssert mandatoryWarnings() {
106    return filteringByKinds(Kind.MANDATORY_WARNING);
107  }
108
109  /**
110   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#NOTE}
111   * kind.
112   *
113   * @return the assertion object for {@link Kind#NOTE} diagnostics.
114   * @throws AssertionError if the list is null.
115   */
116  public TraceDiagnosticListAssert notes() {
117    return filteringByKinds(Kind.NOTE);
118  }
119
120  /**
121   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#OTHER}
122   * kind.
123   *
124   * @return the assertion object for {@link Kind#OTHER} diagnostics.
125   * @throws AssertionError if the list is null.
126   */
127  public TraceDiagnosticListAssert others() {
128    return filteringByKinds(Kind.OTHER);
129  }
130
131  /**
132   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to any of the
133   * given {@link Kind kinds}.
134   *
135   * @param kind      the first kind to match.
136   * @param moreKinds additional 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   */
141  public TraceDiagnosticListAssert filteringByKinds(Kind kind, Kind... moreKinds) {
142    requireNonNull(kind, "kind");
143    requireNonNullValues(moreKinds, "moreKinds");
144    return filteringByKinds(combineOneOrMore(kind, moreKinds));
145  }
146
147  /**
148   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to any of the
149   * given {@link Kind kinds}.
150   *
151   * @param kinds the kinds to match.
152   * @return the assertion object for the filtered diagnostics.
153   * @throws AssertionError       if this list is null.
154   * @throws NullPointerException if any of the kinds are null.
155   */
156  public TraceDiagnosticListAssert filteringByKinds(Iterable<Kind> kinds) {
157    requireNonNullValues(kinds, "kinds");
158    return filteringBy(kindIsOneOf(kinds));
159  }
160
161  /**
162   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to none of the
163   * given {@link Kind kinds}.
164   *
165   * @param kind      the first kind to ensure are not matched.
166   * @param moreKinds additional 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   */
171  public TraceDiagnosticListAssert excludingKinds(Kind kind, Kind... moreKinds) {
172    requireNonNull(kind, "kind");
173    requireNonNullValues(moreKinds, "moreKinds");
174    return excludingKinds(combineOneOrMore(kind, moreKinds));
175  }
176
177  /**
178   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to none of the
179   * given {@link Kind kinds}.
180   *
181   * @param kinds the kinds to filter out.
182   * @return the assertion object for the filtered diagnostics.
183   * @throws AssertionError       if this list is null.
184   * @throws NullPointerException if any of the kinds are null.
185   */
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 kind      the first kind to check for.
272   * @param moreKinds any additional kinds to check for.
273   * @return this assertion object for further call chaining.
274   * @throws AssertionError       if the diagnostic list is null.
275   * @throws NullPointerException if the kind or more kinds are null.
276   */
277  public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Kind kind, Kind... moreKinds) {
278    requireNonNull(kind, "kind");
279    requireNonNullValues(moreKinds, "moreKinds");
280    return hasNoDiagnosticsOfKinds(combineOneOrMore(kind, moreKinds));
281  }
282
283  /**
284   * Assert that this list has no diagnostics matching any of the given kinds.
285   *
286   * @param kinds the kinds to check for.
287   * @return this assertion object for further call chaining.
288   * @throws AssertionError       if the diagnostic list is null.
289   * @throws NullPointerException if any of the kinds are null.
290   */
291  public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Iterable<Kind> kinds) {
292    requireNonNullValues(kinds, "kinds");
293    isNotNull();
294
295    var actualDiagnostics = actual
296        .stream()
297        .filter(kindIsOneOf(kinds))
298        .collect(toUnmodifiableList());
299
300    if (actualDiagnostics.isEmpty()) {
301      return myself;
302    }
303
304    var allKindsString = StreamSupport
305        .stream(kinds.spliterator(), false)
306        .map(next -> next.name().toLowerCase(Locale.ROOT).replace('_', ' '))
307        .distinct()
308        .sorted()
309        .collect(collectingAndThen(
310            toUnmodifiableList(),
311            names -> StringUtils.toWordedList(names, ", ", ", or ")
312        ));
313
314    throw failureWithActualExpected(
315        actualDiagnostics.size(),
316        0,
317        "Expected no %s diagnostics.\n\nDiagnostics:\n%s",
318        allKindsString,
319        TraceDiagnosticListRepresentation.getInstance().toStringOf(actualDiagnostics)
320    );
321  }
322
323  /**
324   * Filter diagnostics by a given predicate and return an assertion object that applies to all
325   * diagnostics that match that predicate.
326   *
327   * @param predicate the predicate to match.
328   * @return the assertion object for the diagnostics that match.
329   * @throws NullPointerException if the predicate is null.
330   * @throws AssertionError       if the diagnostic list is null.
331   */
332  public TraceDiagnosticListAssert filteringBy(
333      Predicate<TraceDiagnostic<? extends JavaFileObject>> predicate
334  ) {
335    isNotNull();
336    return filteredOn(predicate);
337  }
338
339  @Override
340  protected TraceDiagnosticAssert toAssert(
341      TraceDiagnostic<? extends JavaFileObject> value,
342      String description
343  ) {
344    return new TraceDiagnosticAssert(value).describedAs(description);
345  }
346
347  @Override
348  protected TraceDiagnosticListAssert newAbstractIterableAssert(
349      Iterable<? extends TraceDiagnostic<? extends JavaFileObject>> iterable
350  ) {
351    var list = new ArrayList<TraceDiagnostic<? extends JavaFileObject>>();
352    iterable.forEach(list::add);
353    return new TraceDiagnosticListAssert(list);
354  }
355
356  private Predicate<TraceDiagnostic<? extends JavaFileObject>> kindIsOneOf(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}