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;
022
023import io.github.ascopes.jct.diagnostics.TraceDiagnostic;
024import io.github.ascopes.jct.repr.TraceDiagnosticListRepresentation;
025import io.github.ascopes.jct.utils.StringUtils;
026import java.util.ArrayList;
027import java.util.LinkedHashSet;
028import java.util.List;
029import java.util.Locale;
030import java.util.function.Predicate;
031import java.util.stream.Collectors;
032import java.util.stream.StreamSupport;
033import javax.tools.Diagnostic.Kind;
034import javax.tools.JavaFileObject;
035import org.apiguardian.api.API;
036import org.apiguardian.api.API.Status;
037import org.assertj.core.api.AbstractListAssert;
038
039/**
040 * Assertions for a list of diagnostics.
041 *
042 * @author Ashley Scopes
043 * @since 0.0.1
044 */
045@API(since = "0.0.1", status = Status.STABLE)
046public final class TraceDiagnosticListAssert
047    extends AbstractListAssert<TraceDiagnosticListAssert, List<? extends TraceDiagnostic<? extends JavaFileObject>>, TraceDiagnostic<? extends JavaFileObject>, TraceDiagnosticAssert> {
048
049  /**
050   * Initialize this assertion.
051   *
052   * @param traceDiagnostics the diagnostics to perform assertions on.
053   */
054  public TraceDiagnosticListAssert(
055      List<? extends TraceDiagnostic<? extends JavaFileObject>> traceDiagnostics
056  ) {
057    super(traceDiagnostics, TraceDiagnosticListAssert.class);
058    info.useRepresentation(TraceDiagnosticListRepresentation.getInstance());
059  }
060
061  /**
062   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#ERROR}
063   * kind.
064   *
065   * @return the assertion object for {@link Kind#ERROR} diagnostics.
066   * @throws AssertionError if the list is null.
067   */
068  public TraceDiagnosticListAssert errors() {
069    return filteringByKinds(DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS);
070  }
071
072  /**
073   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
074   * {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING} kind.
075   *
076   * @return the assertion object for {@link Kind#WARNING} and {@link Kind#MANDATORY_WARNING}
077   *     diagnostics.
078   * @throws AssertionError if the list is null.
079   */
080  public TraceDiagnosticListAssert warnings() {
081    return filteringByKinds(DiagnosticKindAssert.WARNING_DIAGNOSTIC_KINDS);
082  }
083
084  /**
085   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
086   * {@link Kind#WARNING} kind.
087   *
088   * @return the assertion object for {@link Kind#WARNING} diagnostics.
089   * @throws AssertionError if the list is null.
090   */
091  public TraceDiagnosticListAssert customWarnings() {
092    return filteringByKinds(Kind.WARNING);
093  }
094
095  /**
096   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
097   * {@link Kind#MANDATORY_WARNING} kind.
098   *
099   * @return the assertion object for {@link Kind#MANDATORY_WARNING} diagnostics.
100   * @throws AssertionError if the list is null.
101   */
102  public TraceDiagnosticListAssert mandatoryWarnings() {
103    return filteringByKinds(Kind.MANDATORY_WARNING);
104  }
105
106  /**
107   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#NOTE}
108   * kind.
109   *
110   * @return the assertion object for {@link Kind#NOTE} diagnostics.
111   * @throws AssertionError if the list is null.
112   */
113  public TraceDiagnosticListAssert notes() {
114    return filteringByKinds(Kind.NOTE);
115  }
116
117  /**
118   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#OTHER}
119   * kind.
120   *
121   * @return the assertion object for {@link Kind#OTHER} diagnostics.
122   * @throws AssertionError if the list is null.
123   */
124  public TraceDiagnosticListAssert others() {
125    return filteringByKinds(Kind.OTHER);
126  }
127
128  /**
129   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to any of the
130   * given {@link Kind kinds}.
131   *
132   * @param kind      the first kind to match.
133   * @param moreKinds additional kinds to match.
134   * @return the assertion object for the filtered diagnostics.
135   * @throws AssertionError       if this list is null.
136   * @throws NullPointerException if any of the kinds are null.
137   */
138  public TraceDiagnosticListAssert filteringByKinds(Kind kind, Kind... moreKinds) {
139    requireNonNull(kind, "kind must not be null");
140    requireNonNullValues(moreKinds, "moreKinds");
141    return filteringByKinds(combineOneOrMore(kind, moreKinds));
142  }
143
144
145  /**
146   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to any of the
147   * given {@link Kind kinds}.
148   *
149   * @param kinds the kinds to match.
150   * @return the assertion object for the filtered diagnostics.
151   * @throws AssertionError       if this list is null.
152   * @throws NullPointerException if any of the kinds are null.
153   */
154  public TraceDiagnosticListAssert filteringByKinds(Iterable<Kind> kinds) {
155    requireNonNullValues(kinds, "kinds");
156    return filteringBy(kind(kinds));
157  }
158
159  /**
160   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to none of the
161   * given {@link Kind kinds}.
162   *
163   * @param kind      the first kind to ensure are not matched.
164   * @param moreKinds additional 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   */
169  public TraceDiagnosticListAssert excludingKinds(Kind kind, Kind... moreKinds) {
170    requireNonNull(kind, "kind must not be null");
171    requireNonNullValues(moreKinds, "moreKinds");
172    return excludingKinds(combineOneOrMore(kind, moreKinds));
173  }
174
175  /**
176   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to none of the
177   * given {@link Kind kinds}.
178   *
179   * @param kinds the kinds to filter out.
180   * @return the assertion object for the filtered diagnostics.
181   * @throws AssertionError       if this list is null.
182   * @throws NullPointerException if any of the kinds are null.
183   */
184  public TraceDiagnosticListAssert excludingKinds(Iterable<Kind> kinds) {
185    requireNonNullValues(kinds, "kinds");
186    return filteringBy(not(kind(kinds)));
187  }
188
189  /**
190   * Assert that this list has no {@link Kind#ERROR} diagnostics.
191   *
192   * @return this assertion object for further call chaining.
193   * @throws AssertionError if this list is null.
194   */
195  public TraceDiagnosticListAssert hasNoErrors() {
196    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS);
197  }
198
199  /**
200   * Assert that this list has no {@link Kind#ERROR}, {@link Kind#WARNING}, or
201   * {@link Kind#MANDATORY_WARNING} diagnostics.
202   *
203   * @return this assertion object for further call chaining.
204   * @throws AssertionError if this list is null.
205   */
206  public TraceDiagnosticListAssert hasNoErrorsOrWarnings() {
207    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.WARNING_AND_ERROR_DIAGNOSTIC_KINDS);
208  }
209
210  /**
211   * Assert that this list has no {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING}
212   * diagnostics.
213   *
214   * @return this assertion object for further call chaining.
215   * @throws AssertionError if this list is null.
216   */
217  public TraceDiagnosticListAssert hasNoWarnings() {
218    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.WARNING_DIAGNOSTIC_KINDS);
219  }
220
221  /**
222   * Assert that this list has no {@link Kind#WARNING} diagnostics.
223   *
224   * @return this assertion object for further call chaining.
225   * @throws AssertionError if this list is null.
226   */
227  public TraceDiagnosticListAssert hasNoCustomWarnings() {
228    return hasNoDiagnosticsOfKinds(Kind.WARNING);
229  }
230
231  /**
232   * Assert that this list has no {@link Kind#MANDATORY_WARNING} diagnostics.
233   *
234   * @return this assertion object for further call chaining.
235   * @throws AssertionError if this list is null.
236   */
237  public TraceDiagnosticListAssert hasNoMandatoryWarnings() {
238    return hasNoDiagnosticsOfKinds(Kind.MANDATORY_WARNING);
239  }
240
241  /**
242   * Assert that this list has no {@link Kind#NOTE} diagnostics.
243   *
244   * @return this assertion object for further call chaining.
245   * @throws AssertionError if this list is null.
246   */
247  public TraceDiagnosticListAssert hasNoNotes() {
248    return hasNoDiagnosticsOfKinds(Kind.NOTE);
249  }
250
251  /**
252   * Assert that this list has no {@link Kind#OTHER} diagnostics.
253   *
254   * @return this assertion object for further call chaining.
255   * @throws AssertionError if this list is null.
256   */
257  public TraceDiagnosticListAssert hasNoOtherDiagnostics() {
258    return hasNoDiagnosticsOfKinds(Kind.OTHER);
259  }
260
261  /**
262   * Assert that this list has no diagnostics matching any of the given kinds.
263   *
264   * @param kind      the first kind to check for.
265   * @param moreKinds any additional kinds to check for.
266   * @return this assertion object for further call chaining.
267   * @throws AssertionError       if the diagnostic list is null.
268   * @throws NullPointerException if the kind or more kinds are null.
269   */
270  public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Kind kind, Kind... moreKinds) {
271    requireNonNull(kind, "kind must not be null");
272    requireNonNullValues(moreKinds, "moreKinds");
273    return hasNoDiagnosticsOfKinds(combineOneOrMore(kind, moreKinds));
274  }
275
276  /**
277   * Assert that this list has no diagnostics matching any of the given kinds.
278   *
279   * @param kinds the kinds to check for.
280   * @return this assertion object for further call chaining.
281   * @throws AssertionError       if the diagnostic list is null.
282   * @throws NullPointerException if any of the kinds are null.
283   */
284  public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Iterable<Kind> kinds) {
285    requireNonNullValues(kinds, "kinds");
286
287    var actualDiagnostics = actual
288        .stream()
289        .filter(kind(kinds))
290        .collect(Collectors.toList());
291
292    if (!actualDiagnostics.isEmpty()) {
293      var allKindsString = StreamSupport.stream(kinds.spliterator(), false)
294          .map(next -> next.name().toLowerCase(Locale.ROOT).replace('_', ' '))
295          .sorted()
296          .collect(Collectors.collectingAndThen(
297              Collectors.toUnmodifiableList(),
298              names -> StringUtils.toWordedList(names, ", ", ", or ")
299          ));
300
301      failWithActualExpectedAndMessage(
302          actualDiagnostics.size(),
303          0,
304          "Expected no %s diagnostics.\n\nDiagnostics:\n%s",
305          allKindsString,
306          TraceDiagnosticListRepresentation.getInstance().toStringOf(actualDiagnostics)
307      );
308    }
309
310    return myself;
311  }
312
313  /**
314   * Filter diagnostics by a given predicate and return an assertion object that applies to all
315   * diagnostics that match that predicate.
316   *
317   * @param predicate the predicate to match.
318   * @return the assertion object for the diagnostics that match.
319   * @throws NullPointerException if the predicate is null.
320   * @throws AssertionError       if the diagnostic list is null.
321   */
322  public TraceDiagnosticListAssert filteringBy(
323      Predicate<TraceDiagnostic<? extends JavaFileObject>> predicate
324  ) {
325    requireNonNull(predicate, "predicate must not be null");
326
327    isNotNull();
328
329    return actual
330        .stream()
331        .filter(predicate)
332        .collect(Collectors.collectingAndThen(
333            Collectors.toUnmodifiableList(),
334            TraceDiagnosticListAssert::new
335        ));
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>> kind(Iterable<Kind> kinds) {
356    var kindsSet = new LinkedHashSet<Kind>();
357    kinds.forEach(kindsSet::add);
358
359    return diagnostic -> kindsSet.contains(diagnostic.getKind());
360  }
361}