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
048    AbstractListAssert<TraceDiagnosticListAssert, List<? extends TraceDiagnostic<? extends JavaFileObject>>, TraceDiagnostic<? extends JavaFileObject>, TraceDiagnosticAssert> {
049
050  /**
051   * Initialize this assertion.
052   *
053   * @param traceDiagnostics the diagnostics to perform assertions on.
054   */
055  public TraceDiagnosticListAssert(
056      List<? extends TraceDiagnostic<? extends JavaFileObject>> traceDiagnostics
057  ) {
058    super(traceDiagnostics, TraceDiagnosticListAssert.class);
059    info.useRepresentation(TraceDiagnosticListRepresentation.getInstance());
060  }
061
062  /**
063   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#ERROR}
064   * kind.
065   *
066   * @return the assertion object for {@link Kind#ERROR} diagnostics.
067   * @throws AssertionError if the list is null.
068   */
069  public TraceDiagnosticListAssert errors() {
070    return filteringByKinds(DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS);
071  }
072
073  /**
074   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
075   * {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING} kind.
076   *
077   * @return the assertion object for {@link Kind#WARNING} and {@link Kind#MANDATORY_WARNING}
078   *     diagnostics.
079   * @throws AssertionError if the list is null.
080   */
081  public TraceDiagnosticListAssert warnings() {
082    return filteringByKinds(DiagnosticKindAssert.WARNING_DIAGNOSTIC_KINDS);
083  }
084
085  /**
086   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
087   * {@link Kind#WARNING} kind.
088   *
089   * @return the assertion object for {@link Kind#WARNING} diagnostics.
090   * @throws AssertionError if the list is null.
091   */
092  public TraceDiagnosticListAssert customWarnings() {
093    return filteringByKinds(Kind.WARNING);
094  }
095
096  /**
097   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the
098   * {@link Kind#MANDATORY_WARNING} kind.
099   *
100   * @return the assertion object for {@link Kind#MANDATORY_WARNING} diagnostics.
101   * @throws AssertionError if the list is null.
102   */
103  public TraceDiagnosticListAssert mandatoryWarnings() {
104    return filteringByKinds(Kind.MANDATORY_WARNING);
105  }
106
107  /**
108   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#NOTE}
109   * kind.
110   *
111   * @return the assertion object for {@link Kind#NOTE} diagnostics.
112   * @throws AssertionError if the list is null.
113   */
114  public TraceDiagnosticListAssert notes() {
115    return filteringByKinds(Kind.NOTE);
116  }
117
118  /**
119   * Get a {@link TraceDiagnosticListAssert} across all diagnostics that have the {@link Kind#OTHER}
120   * kind.
121   *
122   * @return the assertion object for {@link Kind#OTHER} diagnostics.
123   * @throws AssertionError if the list is null.
124   */
125  public TraceDiagnosticListAssert others() {
126    return filteringByKinds(Kind.OTHER);
127  }
128
129  /**
130   * Get a {@link TraceDiagnosticListAssert} that contains diagnostics corresponding to any of the
131   * given {@link Kind kinds}.
132   *
133   * @param kind      the first kind to match.
134   * @param moreKinds additional 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   */
139  public TraceDiagnosticListAssert filteringByKinds(Kind kind, Kind... moreKinds) {
140    requireNonNull(kind, "kind must not be null");
141    requireNonNullValues(moreKinds, "moreKinds");
142    return filteringByKinds(combineOneOrMore(kind, moreKinds));
143  }
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(kind(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 must not be null");
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    return filteringBy(not(kind(kinds)));
188  }
189
190  /**
191   * Assert that this list has no {@link Kind#ERROR} diagnostics.
192   *
193   * @return this assertion object for further call chaining.
194   * @throws AssertionError if this list is null.
195   */
196  public TraceDiagnosticListAssert hasNoErrors() {
197    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.ERROR_DIAGNOSTIC_KINDS);
198  }
199
200  /**
201   * Assert that this list has no {@link Kind#ERROR}, {@link Kind#WARNING}, or
202   * {@link Kind#MANDATORY_WARNING} diagnostics.
203   *
204   * @return this assertion object for further call chaining.
205   * @throws AssertionError if this list is null.
206   */
207  public TraceDiagnosticListAssert hasNoErrorsOrWarnings() {
208    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.WARNING_AND_ERROR_DIAGNOSTIC_KINDS);
209  }
210
211  /**
212   * Assert that this list has no {@link Kind#WARNING} or {@link Kind#MANDATORY_WARNING}
213   * diagnostics.
214   *
215   * @return this assertion object for further call chaining.
216   * @throws AssertionError if this list is null.
217   */
218  public TraceDiagnosticListAssert hasNoWarnings() {
219    return hasNoDiagnosticsOfKinds(DiagnosticKindAssert.WARNING_DIAGNOSTIC_KINDS);
220  }
221
222  /**
223   * Assert that this list has no {@link Kind#WARNING} diagnostics.
224   *
225   * @return this assertion object for further call chaining.
226   * @throws AssertionError if this list is null.
227   */
228  public TraceDiagnosticListAssert hasNoCustomWarnings() {
229    return hasNoDiagnosticsOfKinds(Kind.WARNING);
230  }
231
232  /**
233   * Assert that this list has no {@link Kind#MANDATORY_WARNING} diagnostics.
234   *
235   * @return this assertion object for further call chaining.
236   * @throws AssertionError if this list is null.
237   */
238  public TraceDiagnosticListAssert hasNoMandatoryWarnings() {
239    return hasNoDiagnosticsOfKinds(Kind.MANDATORY_WARNING);
240  }
241
242  /**
243   * Assert that this list has no {@link Kind#NOTE} diagnostics.
244   *
245   * @return this assertion object for further call chaining.
246   * @throws AssertionError if this list is null.
247   */
248  public TraceDiagnosticListAssert hasNoNotes() {
249    return hasNoDiagnosticsOfKinds(Kind.NOTE);
250  }
251
252  /**
253   * Assert that this list has no {@link Kind#OTHER} diagnostics.
254   *
255   * @return this assertion object for further call chaining.
256   * @throws AssertionError if this list is null.
257   */
258  public TraceDiagnosticListAssert hasNoOtherDiagnostics() {
259    return hasNoDiagnosticsOfKinds(Kind.OTHER);
260  }
261
262  /**
263   * Assert that this list has no diagnostics matching any of the given kinds.
264   *
265   * @param kind      the first kind to check for.
266   * @param moreKinds any additional kinds to check for.
267   * @return this assertion object for further call chaining.
268   * @throws AssertionError       if the diagnostic list is null.
269   * @throws NullPointerException if the kind or more kinds are null.
270   */
271  public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Kind kind, Kind... moreKinds) {
272    requireNonNull(kind, "kind must not be null");
273    requireNonNullValues(moreKinds, "moreKinds");
274    return hasNoDiagnosticsOfKinds(combineOneOrMore(kind, moreKinds));
275  }
276
277  /**
278   * Assert that this list has no diagnostics matching any of the given kinds.
279   *
280   * @param kinds the kinds to check for.
281   * @return this assertion object for further call chaining.
282   * @throws AssertionError       if the diagnostic list is null.
283   * @throws NullPointerException if any of the kinds are null.
284   */
285  public TraceDiagnosticListAssert hasNoDiagnosticsOfKinds(Iterable<Kind> kinds) {
286    requireNonNullValues(kinds, "kinds");
287
288    var actualDiagnostics = actual
289        .stream()
290        .filter(kind(kinds))
291        .collect(Collectors.toList());
292
293    if (!actualDiagnostics.isEmpty()) {
294      var allKindsString = StreamSupport.stream(kinds.spliterator(), false)
295          .map(next -> next.name().toLowerCase(Locale.ROOT).replace('_', ' '))
296          .sorted()
297          .collect(Collectors.collectingAndThen(
298              Collectors.toUnmodifiableList(),
299              names -> StringUtils.toWordedList(names, ", ", ", or ")
300          ));
301
302      failWithActualExpectedAndMessage(
303          actualDiagnostics.size(),
304          0,
305          "Expected no %s diagnostics.\n\nDiagnostics:\n%s",
306          allKindsString,
307          TraceDiagnosticListRepresentation.getInstance().toStringOf(actualDiagnostics)
308      );
309    }
310
311    return myself;
312  }
313
314  /**
315   * Filter diagnostics by a given predicate and return an assertion object that applies to all
316   * diagnostics that match that predicate.
317   *
318   * @param predicate the predicate to match.
319   * @return the assertion object for the diagnostics that match.
320   * @throws NullPointerException if the predicate is null.
321   * @throws AssertionError       if the diagnostic list is null.
322   */
323  public TraceDiagnosticListAssert filteringBy(
324      Predicate<TraceDiagnostic<? extends JavaFileObject>> predicate
325  ) {
326    requireNonNull(predicate, "predicate must not be null");
327
328    isNotNull();
329
330    return actual
331        .stream()
332        .filter(predicate)
333        .collect(Collectors.collectingAndThen(
334            Collectors.toUnmodifiableList(),
335            TraceDiagnosticListAssert::new
336        ));
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>> kind(Iterable<Kind> kinds) {
357    var kindsSet = new LinkedHashSet<Kind>();
358    kinds.forEach(kindsSet::add);
359
360    return diagnostic -> kindsSet.contains(diagnostic.getKind());
361  }
362}