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.assertj.core.api.AbstractListAssert;
037import org.jspecify.annotations.Nullable;
038
039//@formatter:off
040/**
041 * Assertions for a list of diagnostics.
042 *
043 * @author Ashley Scopes
044 * @since 0.0.1
045 */
046public final class TraceDiagnosticListAssert
047    extends AbstractListAssert<TraceDiagnosticListAssert, List<? extends TraceDiagnostic<? extends JavaFileObject>>, TraceDiagnostic<? extends JavaFileObject>, TraceDiagnosticAssert> {
048  //@formatter:on
049
050  /**
051   * Initialize this assertion.
052   *
053   * @param traceDiagnostics the diagnostics to perform assertions on.
054   */
055  @SuppressWarnings("DataFlowIssue")
056  public TraceDiagnosticListAssert(
057      @Nullable 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 kinds the kinds to match.
135   * @return the assertion object for the filtered diagnostics.
136   * @throws AssertionError           if this list is null.
137   * @throws NullPointerException     if any of the kinds are null.
138   * @throws IllegalArgumentException if no kinds are provided.
139   */
140  public TraceDiagnosticListAssert filteringByKinds(Kind... kinds) {
141    requireNonNullValues(kinds, "kinds");
142    requireAtLeastOne(kinds, "kinds");
143    return filteringByKinds(List.of(kinds));
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 kinds kinds to ensure are not matched.
165   * @return the assertion object for the filtered diagnostics.
166   * @throws AssertionError           if this list is null.
167   * @throws NullPointerException     if any of the kinds are null.
168   * @throws IllegalArgumentException if no kinds are provided.
169   */
170  public TraceDiagnosticListAssert excludingKinds(Kind... kinds) {
171    requireNonNullValues(kinds, "kinds");
172    requireAtLeastOne(kinds, "kinds");
173    return excludingKinds(List.of(kinds));
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  @SuppressWarnings("ConstantValue")  // actual CAN be null, IntelliJ just doesn't realise this.
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 kinds 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... kinds) {
277    requireNonNullValues(kinds, "kinds");
278    requireAtLeastOne(kinds, "kinds");
279    return hasNoDiagnosticsOfKinds(List.of(kinds));
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<@Nullable TraceDiagnostic<? extends JavaFileObject>> kindIsOneOf(
356      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}