| /******************************************************************************* |
| * Copyright (c) 2019 Altran Netherlands B.V. and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Niko Stotz (Altran Netherlands B.V.) - initial implementation |
| *******************************************************************************/ |
| package org.eclipse.ui.internal.editors.text.codemining.annotation; |
| |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Spliterator; |
| import java.util.Spliterators; |
| import java.util.function.Function; |
| import java.util.function.Predicate; |
| import java.util.stream.Stream; |
| import java.util.stream.StreamSupport; |
| |
| import org.eclipse.jdt.annotation.NonNullByDefault; |
| import org.eclipse.jdt.annotation.Nullable; |
| |
| import org.eclipse.core.resources.IMarker; |
| |
| import org.eclipse.jface.text.quickassist.IQuickFixableAnnotation; |
| import org.eclipse.jface.text.source.Annotation; |
| import org.eclipse.jface.text.source.IAnnotationAccessExtension; |
| import org.eclipse.jface.text.source.IAnnotationPresentation; |
| |
| import org.eclipse.ui.texteditor.MarkerAnnotation; |
| |
| /** |
| * Filters and arranges Annotations that are suitable as code minings. Takes user preferences into |
| * account. |
| * |
| * @since 3.13 |
| */ |
| @NonNullByDefault |
| public class AnnotationCodeMiningFilter { |
| /** |
| * Callback to locate an Annotation inside the editor. |
| */ |
| public interface Locator { |
| public @Nullable Integer getOffset(Annotation annotation); |
| |
| public @Nullable Integer getLine(Annotation annotation); |
| } |
| |
| final private IAnnotationAccessExtension annotationAccess; |
| |
| final private AnnotationCodeMiningPreferences preferences= new AnnotationCodeMiningPreferences(); |
| |
| final private Stream<Annotation> annotations; |
| |
| public AnnotationCodeMiningFilter(IAnnotationAccessExtension annotationAccess, Annotation[]... annotations) { |
| this.annotationAccess= annotationAccess; |
| this.annotations= Arrays.stream(annotations).flatMap(Arrays::stream); |
| } |
| |
| public AnnotationCodeMiningFilter(IAnnotationAccessExtension annotationAccess, Iterator<Annotation> annotations) { |
| this.annotationAccess= annotationAccess; |
| this.annotations= StreamSupport.stream(Spliterators.spliteratorUnknownSize(annotations, Spliterator.ORDERED), false); |
| } |
| |
| /** |
| * Checks if there are any suitable annotations. |
| */ |
| public boolean isEmpty() { |
| return !filterReTrigger(annotations).findAny().isPresent(); |
| } |
| |
| /** |
| * Returns all suitable annotations. |
| */ |
| public Stream<Annotation> sortDistinctLimit(Locator locator) { |
| return limit(distinct(locator, sort(locator, filterShown(filterReTrigger(annotations))))); |
| } |
| |
| /** |
| * Filters suitable annotations to decide whether we need to re-trigger code minings. |
| */ |
| private Stream<Annotation> filterReTrigger(Stream<Annotation> anns) { |
| return anns |
| .filter(this::isTypeProcessable) |
| .filter(this::isPaintable) |
| .filter(this::isInScope); |
| } |
| |
| private boolean isTypeProcessable(Annotation a) { |
| return a instanceof MarkerAnnotation || |
| a instanceof IQuickFixableAnnotation || |
| a instanceof IAnnotationPresentation; |
| } |
| |
| private boolean isPaintable(Annotation a) { |
| return annotationAccess.isPaintable(a); |
| } |
| |
| private boolean isInScope(Annotation a) { |
| return isError(a) || isWarning(a) || isInfo(a); |
| } |
| |
| /** |
| * Filters suitable annotations to show. |
| */ |
| private Stream<Annotation> filterShown(Stream<Annotation> anns) { |
| return anns |
| .filter(a -> !a.isMarkedDeleted()) |
| .filter(this::isEnabled); |
| } |
| |
| private boolean isEnabled(Annotation a) { |
| if (isError(a)) { |
| return preferences.isErrorEnabled(); |
| } else if (isWarning(a)) { |
| return preferences.isWarningEnabled(); |
| } else if (isInfo(a)) { |
| return preferences.isInfoEnabled(); |
| } else { |
| return false; |
| } |
| } |
| |
| /** |
| * Sorts annotations based on 1) position in text, 2) layer, 3) severity, 4) text. |
| */ |
| private Stream<Annotation> sort(Locator locator, Stream<Annotation> anns) { |
| return anns.sorted((a, b) -> { |
| int resultPosition= comparePosition(locator, a, b); |
| if (resultPosition != 0) { |
| return resultPosition; |
| } |
| |
| final int resultLayer= compareLayer(a, b); |
| if (resultLayer != 0) { |
| return resultLayer; |
| } |
| |
| final int resultSeverity= compareSeverity(a, b); |
| if (resultSeverity != 0) { |
| return resultSeverity; |
| } |
| |
| return a.getText().compareTo(b.getText()); |
| }); |
| } |
| |
| private int comparePosition(Locator locator, Annotation a, Annotation b) { |
| final Integer aOffset= locator.getOffset(a); |
| final Integer bOffset= locator.getOffset(b); |
| |
| if (aOffset == null || bOffset == null) { |
| return 0; |
| } |
| |
| int resultPosition= Integer.compare(aOffset, bOffset); |
| return resultPosition; |
| } |
| |
| private int compareLayer(Annotation a, Annotation b) { |
| final int resultPriority= Integer.compare(annotationAccess.getLayer(a), annotationAccess.getLayer(b)); |
| return resultPriority; |
| } |
| |
| private int compareSeverity(Annotation a, Annotation b) { |
| final int resultSeverity= Integer.compare(getSeverity(a), getSeverity(b)); |
| return resultSeverity; |
| } |
| |
| private int getSeverity(Annotation a) { |
| if (isError(a)) { |
| return IMarker.SEVERITY_ERROR; |
| } else if (isWarning(a)) { |
| return IMarker.SEVERITY_WARNING; |
| } else if (isInfo(a)) { |
| return IMarker.SEVERITY_INFO; |
| } else { |
| return -1; |
| } |
| } |
| |
| /** |
| * Assures annotations are distinct by line and text. Required, as sometimes the same message is |
| * shown at the same place from different annotations. |
| */ |
| @SuppressWarnings("null") |
| private Stream<Annotation> distinct(Locator locator, Stream<Annotation> anns) { |
| return anns |
| .filter(distinctByKey(a -> { |
| final Integer line= locator.getLine(a); |
| if (line == null) { |
| return null; |
| } |
| String key= line + a.getText(); |
| |
| return key; |
| })) |
| .filter(Objects::nonNull); |
| } |
| |
| /** |
| * Limits annotations to user-defined amount. |
| */ |
| private Stream<Annotation> limit(Stream<Annotation> anns) { |
| return anns.limit(preferences.getMaxMinings()); |
| } |
| |
| private boolean isInfo(Annotation a) { |
| return annotationAccess.isSubtype(a.getType(), "org.eclipse.ui.workbench.texteditor.info"); //$NON-NLS-1$ |
| } |
| |
| private boolean isWarning(Annotation a) { |
| return annotationAccess.isSubtype(a.getType(), "org.eclipse.ui.workbench.texteditor.warning"); //$NON-NLS-1$ |
| } |
| |
| private boolean isError(Annotation a) { |
| return annotationAccess.isSubtype(a.getType(), "org.eclipse.ui.workbench.texteditor.error"); //$NON-NLS-1$ |
| } |
| |
| @SuppressWarnings("null") |
| public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) { |
| Map<Object, Boolean> seen= new LinkedHashMap<>(); |
| return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; |
| } |
| } |