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;
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 AbstractListAssert<TraceDiagnosticListAssert, List<? extends TraceDiagnostic<? extends JavaFileObject>>, TraceDiagnostic<? extends JavaFileObject>, TraceDiagnosticAssert> {
050
051  /**
052   * Initialize this assertion.
053   *
054   * @param traceDiagnostics the diagnostics to perform assertions on.
055   */
056  public TraceDiagnosticListAssert(
057      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 kind      the first kind to match.
135   * @param moreKinds additional kinds to match.
136   * @return the assertion object for the filtered diagnostics.
137   * @throws AssertionError       if this list is null.
138   * @throws NullPointerException if any of the kinds are null.
139   */
140  public TraceDiagnosticListAssert filteringByKinds(Kind kind, Kind... moreKinds) {
141    requireNonNull(kind, "kind");
142    requireNonNullValues(moreKinds, "moreKinds");
143    return filteringByKinds(combineOneOrMore(kind, moreKinds));
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 kind      the first kind to ensure are not matched.
165   * @param moreKinds additional kinds to ensure are not matched.
166   * @return the assertion object for the filtered diagnostics.
167   * @throws AssertionError       if this list is null.
168   * @throws NullPointerException if any of the kinds are null.
169   */
170  public TraceDiagnosticListAssert excludingKinds(Kind kind, Kind... moreKinds) {
171    requireNonNull(kind, "kind");
172    requireNonNullValues(moreKinds, "moreKinds");
173    return excludingKinds(combineOneOrMore(kind, moreKinds));
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  public TraceDiagnosticListAssert excludingKinds(Iterable<Kind> kinds) {
186    requireNonNullValues(kinds, "kinds");
187    isNotNull();
188    return actual
189        .stream()
190        .filter(Objects::nonNull)
191        .filter(not(kindIsOneOf(kinds)))
192        .collect(collectingAndThen(toUnmodifiableList(), this::newAbstractIterableAssert));
193  }
194
195  /**
196   * Assert that this list has no {@link Kind#ERROR} diagnostics.
197   *
198   * @return this assertion object for further call chaining.
199   * @throws AssertionError if this list is null.
200   */
201  public TraceDiagnosticListAssert hasNoErrors() {
202    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS);
203  }
204
205  /**
206   * Assert that this list has no {@link Kind#ERROR}, {@link Kind#WARNING}, or
207   * {@link Kind#MANDATORY_WARNING} diagnostics.
208   *
209   * @return this assertion object for further call chaining.
210   * @throws AssertionError if this list is null.
211   */
212  public TraceDiagnosticListAssert hasNoErrorsOrWarnings() {
213    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.WARNING_AND_ERROR_DIAGNOSTIC_KINDS);
214  }
215
216  /**
217   * Assert that this list has no {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING}
218   * diagnostics.
219   *
220   * @return this assertion object for further call chaining.
221   * @throws AssertionError if this list is null.
222   */
223  public TraceDiagnosticListAssert hasNoWarnings() {
224    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.WARNING_DIAGNOSTIC_KINDS);
225  }
226
227  /**
228   * Assert that this list has no {@link Kind#WARNING} diagnostics.
229   *
230   * @return this assertion object for further call chaining.
231   * @throws AssertionError if this list is null.
232   */
233  public TraceDiagnosticListAssert hasNoCustomWarnings() {
234    return hasNoDiagnosticsOfKinds(Kind.WARNING);
235  }
236
237  /**
238   * Assert that this list has no {@link Kind#MANDATORY_WARNING} diagnostics.
239   *
240   * @return this assertion object for further call chaining.
241   * @throws AssertionError if this list is null.
242   */
243  public TraceDiagnosticListAssert hasNoMandatoryWarnings() {
244    return hasNoDiagnosticsOfKinds(Kind.MANDATORY_WARNING);
245  }
246
247  /**
248   * Assert that this list has no {@link Kind#NOTE} diagnostics.
249   *
250   * @return this assertion object for further call chaining.
251   * @throws AssertionError if this list is null.
252   */
253  public TraceDiagnosticListAssert hasNoNotes() {
254    return hasNoDiagnosticsOfKinds(Kind.NOTE);
255  }
256
257  /**
258   * Assert that this list has no {@link Kind#OTHER} diagnostics.
259   *
260   * @return this assertion object for further call chaining.
261   * @throws AssertionError if this list is null.
262   */
263  public TraceDiagnosticListAssert hasNoOtherDiagnostics() {
264    return hasNoDiagnosticsOfKinds(Kind.OTHER);
265  }
266
267  /**
268   * Assert that this list has no diagnostics matching any of the given kinds.
269   *
270   * @param kind      the first kind to check for.
271   * @param moreKinds any additional 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 kind, Kind... moreKinds) {
277    requireNonNull(kind, "kind");
278    requireNonNullValues(moreKinds, "moreKinds");
279    return hasNoDiagnosticsOfKinds(combineOneOrMore(kind, moreKinds));
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<TraceDiagnostic<? extends JavaFileObject>> kindIsOneOf(Iterable<Kind> kinds) {
356    var kindsSet = new LinkedHashSet<Kind>();
357    kinds.forEach(kindsSet::add);
358
359    return diagnostic -> diagnostic != null && kindsSet.contains(diagnostic.getKind());
360  }
361}