org.conqat.engine.commons.findings.DetachedFinding Maven / Gradle / Ivy
Show all versions of teamscale-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.conqat.engine.commons.findings;
import java.io.Serializable;
import java.io.UTFDataFormatException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.engine.commons.findings.location.ElementLocation;
import org.conqat.engine.commons.findings.location.ManualTestCaseTextRegionLocation;
import org.conqat.engine.commons.findings.location.QualifiedNameLocation;
import org.conqat.engine.commons.findings.location.TeamscaleIssueFieldLocation;
import org.conqat.engine.commons.findings.location.TeamscaleIssueLocation;
import org.conqat.engine.commons.findings.location.TextRegionLocation;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.assessment.ETrafficLightColor;
import org.conqat.lib.commons.collections.CollectionUtils;
import org.conqat.lib.commons.collections.SetMap;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.test.IndexValueClass;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* This class describes orphaned finding that is attached to a node or a findings report. This is
* useful if e.g. findings have been filtered but certain operations should still be carried out on
* the findings.
*
* This class is used as DTO during communication with IDE clients via
* com.teamscale.ide.commons.client.IIdeServiceClient, special care has to be taken when changing
* its signature!
*/
@IndexValueClass(containedInBackup = true)
public class DetachedFinding implements Serializable {
// Class argument necessary due to
// .
private static final Logger LOGGER = LogManager.getLogger(DetachedFinding.class);
/** Serial version UID. */
private static final long serialVersionUID = 1;
/** The maximum length of the {@link #message}. */
public static final int MAX_MESSAGE_LENGTH = 1000;
/** The name of the JSON property name for {@link #location}. */
protected static final String LOCATION_PROPERTY = "location";
/** The name of the JSON property name for {@link #groupName}. */
protected static final String GROUP_NAME_PROPERTY = "groupName";
/** The name of the JSON property name for {@link #categoryName}. */
protected static final String CATEGORY_NAME_PROPERTY = "categoryName";
/** The name of the JSON property name for {@link #message}. */
protected static final String MESSAGE_PROPERTY = "message";
/** The name of the JSON property name for {@link #assessment}. */
private static final String ASSESSMENT_PROPERTY = "assessment";
/** The location. */
@JsonProperty(LOCATION_PROPERTY)
@Schema(implementation = ElementLocationSubtype.class)
private ElementLocation location;
/** The group name. */
@JsonProperty(GROUP_NAME_PROPERTY)
private String groupName;
/** The category name. */
@JsonProperty(CATEGORY_NAME_PROPERTY)
private String categoryName;
/**
* The finding message (the title line in the findings detail page).
*
* This text is in markdown format (e.g., __bold__
will be displayed in bold font). To
* avoid that markdown symbols are interpreted, use
* {@link org.conqat.lib.commons.markup.MarkupUtils#escapeMarkdownRelevantSymbols(String)} before
* creating the {@link DetachedFinding}.
*/
@JsonProperty(MESSAGE_PROPERTY)
private String message;
/** The assessment color of the finding (may be null). */
@JsonProperty(ASSESSMENT_PROPERTY)
@Nullable
private ETrafficLightColor assessment;
/**
* The locations of other findings that are considered siblings. This is, e.g., used to find other
* clone instances in the same clone class. As this is commonly empty, we keep this attribute null
* in this case to save space for serialization. The access methods, however, handle this
* transparently.
*/
@JsonProperty("siblingLocations")
@Nullable
@Schema(implementation = ElementLocations.class)
private List siblingLocations;
/**
* Caches the sibling locations as strings. Used to avoid adding duplicate sibling locations.
* Initialized lazily.
*/
private transient Set siblingLocationCache = null;
/**
* Next to the primary location {@link #location}, a finding may optionally have secondary
* locations. For instance, an architecture finding may contain the individual source code locations
* of the violating identifiers.
*/
@JsonProperty("secondaryLocations")
@Nullable
@Schema(implementation = ElementLocations.class)
private List secondaryLocations;
/**
* Properties for this finding. Each finding can be associated with one or more properties
* describing details of the finding (e.g. length of long method, etc.). These properties are also
* displayed in the UI and can be used for sorting findings (in which case the value class must be
* comparable).
*/
@JsonProperty("properties")
@Schema(additionalProperties = Schema.AdditionalPropertiesValue.USE_ADDITIONAL_PROPERTIES_ANNOTATION, oneOf = {
String.class, Number.class })
private final Map properties = new HashMap<>();
/**
* @see #getStatementPath()
*/
@JsonProperty("statementPath")
@Nullable
private List statementPath = null;
/**
* The guidelines (e.g. AUTOSAR C++14) the check for this finding is part of as a mapping from
* guideline name to rules.
*/
@JsonProperty("guidelineMapping")
@Nullable
private SetMap guidelineMapping = null;
/** Constructor. */
public DetachedFinding(String groupName, String categoryName, String message, ElementLocation location) {
this(groupName, categoryName, message, location, null);
}
/**
* Constructor. Adds the given finding properties to the finding. The assessment may be
* null
.
*/
public DetachedFinding(String groupName, String categoryName, String message, ElementLocation location,
ETrafficLightColor assessment, Map findingProperties) {
this(groupName, categoryName, message, location, assessment);
properties.putAll(findingProperties);
}
/** Constructor. The assessment may be null
*/
@JsonCreator
public DetachedFinding(@JsonProperty(GROUP_NAME_PROPERTY) String groupName,
@JsonProperty(CATEGORY_NAME_PROPERTY) String categoryName, @JsonProperty(MESSAGE_PROPERTY) String message,
@JsonProperty(LOCATION_PROPERTY) ElementLocation location,
@JsonProperty(ASSESSMENT_PROPERTY) ETrafficLightColor assessment) {
CCSMAssert.isNotNull(location);
this.groupName = groupName;
this.categoryName = categoryName;
this.location = location;
this.assessment = assessment;
setMessage(message);
}
/** Copy constructor. */
protected DetachedFinding(DetachedFinding other) {
this(other.groupName, other.categoryName, other.message, other.location, other.assessment);
if (other.hasSiblings()) {
this.siblingLocations = new ArrayList<>(other.siblingLocations);
}
if (other.secondaryLocations != null) {
this.secondaryLocations = new ArrayList<>(other.secondaryLocations);
}
properties.putAll(other.properties);
statementPath = other.statementPath;
guidelineMapping = other.guidelineMapping;
}
/** Get group name. */
public String getGroupName() {
return groupName;
}
/** Sets group name. */
public void setGroupName(String groupName) {
this.groupName = groupName;
}
/** Get category name. */
public String getCategoryName() {
return categoryName;
}
/** Sets category name. */
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
/** Get location. */
public ElementLocation getLocation() {
return location;
}
/** Sets location. */
public void setLocation(ElementLocation location) {
CCSMAssert.isNotNull(location);
this.location = location;
}
/**
* Get Location Path string. (e.g src/main/java/org/foo/bar/Foo.java:189-204)
*/
public String getLocationPathString() {
return location.toString();
}
/** Get location string. (e.g src/main/java/org/foo/bar/Foo.java) */
public String getLocationString() {
return location.toLocationString();
}
/** Get message. */
public String getMessage() {
return message;
}
/**
* Setter for the {@link #message} that ensures findings messages are not too long for serialization
* causing a {@link UTFDataFormatException} in the FindingSerializationUtils.
*/
public void setMessage(String message) {
if (message.length() > MAX_MESSAGE_LENGTH) {
this.message = StringUtils.truncateWithEllipsis(message, MAX_MESSAGE_LENGTH);
LOGGER.error("Shortening overly long finding message of length {} for finding {}", message::length,
this::toString);
return;
}
this.message = message;
}
/** Returns whether this findings has siblings. */
public boolean hasSiblings() {
return siblingLocations != null && !siblingLocations.isEmpty();
}
/** Returns the locations of sibling findings. */
public List getSiblingLocations() {
if (siblingLocations == null) {
return CollectionUtils.emptyList();
}
return CollectionUtils.asUnmodifiable(siblingLocations);
}
/** Removes all sibling locations matching the given filter. */
public void removeMatchingSiblingLocations(Predicate super ElementLocation> filter) {
siblingLocations.removeIf(filter);
}
/**
* @see #secondaryLocations
*/
public List getSecondaryLocations() {
if (secondaryLocations == null) {
return CollectionUtils.emptyList();
}
return CollectionUtils.asUnmodifiable(secondaryLocations);
}
/**
* Adds all previously unknown locations of sibling findings (already known locations are skipped).
*/
public void addSiblingLocation(ElementLocation location) {
if (siblingLocations == null) {
siblingLocations = new ArrayList<>();
}
if (siblingLocationCache == null) {
siblingLocationCache = new HashSet<>(
CollectionUtils.map(siblingLocations, ElementLocation::getLocationKey));
// This way, a finding can never be a sibling of itself
siblingLocationCache.add(this.location.getLocationKey());
}
String locationString = location.getLocationKey();
if (!siblingLocationCache.contains(locationString)) {
siblingLocations.add(location);
siblingLocationCache.add(locationString);
}
}
/**
* Adds the given list of locations as sibling findings.
*
* @param findings
* a collection of {@link DetachedFinding} that will be added as siblings.
*/
public void addSiblingFindings(Collection findings) {
findings.forEach(this::addSiblingFinding);
}
/** Add the given sibling locations */
public void addSiblingLocations(Collection siblingLocations) {
siblingLocations.forEach(this::addSiblingLocation);
}
/** Adds the given {@link DetachedFinding} to the list of sibling findings. */
public void addSiblingFinding(DetachedFinding sibling) {
addSiblingLocation(sibling.getLocation());
}
/** Adds a secondary location to this finding. */
public void addSecondaryLocation(ElementLocation location) {
if (secondaryLocations == null) {
secondaryLocations = new ArrayList<>();
}
secondaryLocations.add(location);
}
/** Adds the given locations as secondary locations. */
public void addSecondaryLocations(Collection locations) {
if (secondaryLocations == null) {
secondaryLocations = new ArrayList<>();
}
secondaryLocations.addAll(locations);
}
/** Returns the properties of this finding. */
public Map getProperties() {
return CollectionUtils.asUnmodifiable(properties);
}
/** Sets a property for this finding. */
public void setProperty(String name, Object value) {
properties.put(name, value);
}
/**
* Sets the properties of this finding, overwriting any previously existing ones
*/
public void setProperties(Map properties) {
this.properties.clear();
addProperties(properties);
}
/**
* Adds a list of properties to this finding, in addition to any previously existing ones.
*/
public void addProperties(Map properties) {
this.properties.putAll(properties);
}
/**
* Get the property value for a given key. Returns null if no such property exists.
*/
public Object getProperty(String name) {
return properties.get(name);
}
/** Returns assessment. */
public ETrafficLightColor getAssessment() {
return assessment;
}
/** Sets the assessment. */
public void setAssessment(ETrafficLightColor assessment) {
this.assessment = assessment;
}
/**
* Returns a string representation that contains the message, the group and a location hint.
*/
@Override
public String toString() {
return getMessage() + " (" + getGroupName() + ") @ " + location.toLocationString();
}
/**
* Returns the statement path if there is one attached to this finding.
*
* A statementPath is a control-flow path through the actual program that leads to this finding,
* comparable to a stacktrace, however, the path may not only be a simple chain, but could be a DAG.
*
* Each entry in the list refers to the indices of its predecessor statements. Contract for the
* setup of this structure:
*
* - An element might have more than one predecessor.
* - Multiple elements can have the same predecessor.
* - There are no cycles in the path.
*
*/
public List getStatementPath() {
if (statementPath == null) {
return CollectionUtils.emptyList();
}
return statementPath;
}
/** @see #getStatementPath() */
public void setStatementPath(List path) {
this.statementPath = path;
}
/** @see #guidelineMapping */
public void setGuidelineMapping(SetMap guidelineMapping) {
this.guidelineMapping = guidelineMapping;
}
/** @see #guidelineMapping */
public SetMap getGuidelineMapping() {
if (guidelineMapping == null) {
return new SetMap<>();
}
return guidelineMapping;
}
/*
* The following interfaces are required to correctly generate the OpenAPI spec. Otherwise, it is
* not possible to set the correct value type of the finding location.
*
* See: https://github.com/swagger-api/swagger-core/issues/3080
*
* See: https://stackoverflow.com/a/68752621
*/
@Schema(oneOf = { ElementLocation.class, TextRegionLocation.class, QualifiedNameLocation.class,
TeamscaleIssueLocation.class, ManualTestCaseTextRegionLocation.class, TeamscaleIssueFieldLocation.class })
private interface ElementLocationSubtype {
}
@ArraySchema(schema = @Schema(implementation = ElementLocationSubtype.class))
private interface ElementLocations extends List {
}
}