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}