
edu.hm.hafner.analysis.IssueBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of analysis-model Show documentation
Show all versions of analysis-model Show documentation
This library provides a Java object model to read, aggregate, filter, and query static analysis reports.
It is used by Jenkins' warnings next generation plug-in to visualize the warnings of individual builds.
Additionally, this library is used by a GitHub action to autograde student software projects based on a given set of
metrics (unit tests, code and mutation coverage, static analysis warnings).
package edu.hm.hafner.analysis;
import java.io.Serializable;
import java.util.Optional;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import edu.hm.hafner.util.LineRange;
import edu.hm.hafner.util.LineRangeList;
import edu.hm.hafner.util.PathUtil;
import edu.hm.hafner.util.TreeString;
import edu.hm.hafner.util.TreeStringBuilder;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import static edu.hm.hafner.analysis.util.IntegerParser.*;
/**
* Creates new {@link Issue issues} using the builder pattern. All properties that have not been set in the builder will
* be set to their default value.
* Example:
*
* Issue issue = new IssueBuilder()
* .setFileName("affected.file")
* .setLineStart(0)
* .setCategory("JavaDoc")
* .setMessage("Missing JavaDoc")
* .setSeverity(Severity.WARNING_LOW);
*
*
* @author Ullrich Hafner
*/
@SuppressWarnings({"InstanceVariableMayNotBeInitialized", "JavaDocMethod", "PMD.TooManyFields", "PMD.GodClass"})
public class IssueBuilder implements AutoCloseable {
private static final String EMPTY = StringUtils.EMPTY;
private static final String UNDEFINED = "-";
private static final TreeString UNDEFINED_TREE_STRING = TreeString.valueOf(UNDEFINED);
private static final TreeString EMPTY_TREE_STRING = TreeString.valueOf(StringUtils.EMPTY);
private static final PathUtil PATH_UTIL = new PathUtil();
private final TreeStringBuilder fileNameBuilder = new TreeStringBuilder();
private final TreeStringBuilder packageNameBuilder = new TreeStringBuilder();
private final TreeStringBuilder messageBuilder = new TreeStringBuilder();
private int lineStart = 0;
private int lineEnd = 0;
private int columnStart = 0;
private int columnEnd = 0;
@CheckForNull
private LineRangeList lineRanges;
@CheckForNull
private String pathName;
private TreeString fileName = UNDEFINED_TREE_STRING;
private TreeString packageName = UNDEFINED_TREE_STRING;
@CheckForNull
private String directory;
@CheckForNull
private String category;
@CheckForNull
private String type;
@CheckForNull
private Severity severity;
private TreeString message = EMPTY_TREE_STRING;
private String description = EMPTY;
@CheckForNull
private String moduleName;
@CheckForNull
private String origin;
@CheckForNull
private String originName;
@CheckForNull
private String reference;
@CheckForNull
private String fingerprint;
@CheckForNull
private Serializable additionalProperties;
private UUID id = UUID.randomUUID();
/**
* Sets the unique ID of the issue. If not set, then an ID will be generated.
*
* @param id
* the ID
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setId(final UUID id) {
this.id = id;
return this;
}
/**
* Sets additional properties from the statical analysis tool. This object could be used to store tool-specific
* information.
*
* @param additionalProperties
* the instance
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setAdditionalProperties(@CheckForNull final Serializable additionalProperties) {
this.additionalProperties = additionalProperties;
return this;
}
/**
* Sets the fingerprint for this issue. Used to decide if two issues are equal even if the equals method returns
* {@code false} since some properties differ due to code refactorings. The fingerprint is created by
* analyzing the content of the affected file.
*
* @param fingerprint
* the fingerprint to set
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setFingerprint(@CheckForNull final String fingerprint) {
this.fingerprint = fingerprint;
return this;
}
/**
* Sets the name of the affected file. This file name is a path relative to the path of the affected files (see
* {@link #setPathName(String)}).
*
* @param fileName
* the file name
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setFileName(@CheckForNull final String fileName) {
this.fileName = internFileName(fileName);
return this;
}
TreeString internFileName(@CheckForNull final String unsafeFileName) {
if (unsafeFileName == null || StringUtils.isEmpty(unsafeFileName)) {
return UNDEFINED_TREE_STRING;
}
else {
if (directory != null && PATH_UTIL.isAbsolute(normalizeFileName(unsafeFileName))) {
return fileNameBuilder.intern(normalizeFileName(unsafeFileName));
}
return fileNameBuilder.intern(normalizeFileName(
PATH_UTIL.createAbsolutePath(directory, unsafeFileName)));
}
}
/**
* Sets the current work directory. This directory is used as a prefix for all subsequent issue file names. If the
* path is set as well, then the final path of an issue is the concatenation of {@code path}, {@code directory}, and
* {@code fileName}. Note that this directory is not visible later on, the issue does only store the path in the
* {@code path} and {@code fileName} properties. I.e., the created issue will get a new file name that is composed
* of {@code directory} and {@code fileName}.
*
* @param directory
* the directory that contains all affected files
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setDirectory(@CheckForNull final String directory) {
this.directory = directory;
return this;
}
/**
* Sets the path of the affected file. Note that this path is not the parent folder of the affected file. This path
* is the folder that contains all the affected files of a {@link Report}. The path of an affected file is stored
* in the {@code path} and {@code fileName} properties so that issues can be tracked even if the root folder changes
* (due to different build environments).
*
* @param pathName
* the path that contains all affected files
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setPathName(@CheckForNull final String pathName) {
this.pathName = pathName;
return this;
}
/**
* Sets the first line of this issue (lines start at 1; 0 indicates the whole file).
*
* @param lineStart
* the first line
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setLineStart(final int lineStart) {
this.lineStart = lineStart;
return this;
}
/**
* Sets the first line of this issue (lines start at 1; 0 indicates the whole file).
*
* @param lineStart
* the first line
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setLineStart(@CheckForNull final String lineStart) {
this.lineStart = parseInt(lineStart);
return this;
}
/**
* Sets the last line of this issue (lines start at 1).
*
* @param lineEnd
* the last line
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setLineEnd(final int lineEnd) {
this.lineEnd = lineEnd;
return this;
}
/**
* Sets the last line of this issue (lines start at 1).
*
* @param lineEnd
* the last line
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setLineEnd(@CheckForNull final String lineEnd) {
this.lineEnd = parseInt(lineEnd);
return this;
}
/**
* Sets the first column of this issue (columns start at 1, 0 indicates the whole line).
*
* @param columnStart
* the first column
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setColumnStart(final int columnStart) {
this.columnStart = columnStart;
return this;
}
/**
* Sets the first column of this issue (columns start at 1, 0 indicates the whole line).
*
* @param columnStart
* the first column
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setColumnStart(@CheckForNull final String columnStart) {
this.columnStart = parseInt(columnStart);
return this;
}
/**
* Sets the last column of this issue (columns start at 1).
*
* @param columnEnd
* the last column
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setColumnEnd(final int columnEnd) {
this.columnEnd = columnEnd;
return this;
}
/**
* Sets the last column of this issue (columns start at 1).
*
* @param columnEnd
* the last column
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setColumnEnd(@CheckForNull final String columnEnd) {
this.columnEnd = parseInt(columnEnd);
return this;
}
/**
* Sets the category of this issue (depends on the available categories of the static analysis tool). Examples for
* categories are "Deprecation", "Design", or "JavaDoc".
*
* @param category
* the category
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setCategory(@CheckForNull final String category) {
this.category = category;
return this;
}
/**
* Sets the type of this issue (depends on the available types of the static analysis tool). The type typically is
* the associated rule of the static analysis tool that reported this issue.
*
* @param type
* the type
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setType(@CheckForNull final String type) {
this.type = type;
return this;
}
/**
* Sets the name of the package or name space (or similar concept) that contains this issue.
*
* @param packageName
* the package or namespace name
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setPackageName(@CheckForNull final String packageName) {
this.packageName = internPackageName(packageName);
return this;
}
TreeString internPackageName(@CheckForNull final String unsafePackageName) {
if (unsafePackageName == null || StringUtils.isBlank(unsafePackageName)) {
return UNDEFINED_TREE_STRING;
}
else {
return packageNameBuilder.intern(unsafePackageName);
}
}
/**
* Sets the name of the module or project (or similar concept) that contains this issue.
*
* @param moduleName
* the module name
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setModuleName(@CheckForNull final String moduleName) {
this.moduleName = moduleName;
return this;
}
/**
* Sets the ID of the tool that did report this issue.
*
* @param origin
* the ID of the originating tool
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setOrigin(@CheckForNull final String origin) {
this.origin = origin;
return this;
}
/**
* Sets the name of the tool that did report this issue.
*
* @param originName
* the ID of the originating tool
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setOriginName(@CheckForNull final String originName) {
this.originName = originName;
return this;
}
/**
* Sets a reference to the execution of the static analysis tool (build ID, timestamp, etc.).
*
* @param reference
* the reference
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setReference(@CheckForNull final String reference) {
this.reference = reference;
return this;
}
/**
* Sets the severity of this issue.
*
* @param severity
* the severity
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setSeverity(@CheckForNull final Severity severity) {
this.severity = severity;
return this;
}
/**
* Guesses a severity for the issues: converts a String severity to one of the predefined severities. If the
* provided String does not match (even partly) then the default severity will be returned.
*
* @param severityString
* the severity given as a string representation
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder guessSeverity(@CheckForNull final String severityString) {
severity = Severity.guessFromString(severityString);
return this;
}
/**
* Sets the detailed message for this issue.
*
* @param message
* the message
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setMessage(@CheckForNull final String message) {
if (StringUtils.isBlank(message)) {
this.message = EMPTY_TREE_STRING;
}
else {
this.message = messageBuilder.intern(StringUtils.stripToEmpty(message));
}
return this;
}
/**
* Sets an additional description for this issue. Static analysis tools might provide some additional information
* about this issue. This description may contain valid HTML.
*
* @param description
* the description (as HTML content)
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setDescription(@CheckForNull final String description) {
this.description = StringUtils.stripToEmpty(description);
return this;
}
/**
* Sets additional line ranges for this issue. Not that the primary range given by {@code lineStart} and {@code *
* lineEnd} is not included.
*
* @param lineRanges
* the additional line ranges
*
* @return this
*/
@CanIgnoreReturnValue
public IssueBuilder setLineRanges(final LineRangeList lineRanges) {
this.lineRanges = new LineRangeList(lineRanges);
return this;
}
/**
* Initializes this builder with an exact copy of all properties of the specified issue.
*
* @param copy
* the issue to copy the properties from
*
* @return the initialized builder
*/
public IssueBuilder copy(final Issue copy) {
fileName = copy.getFileNameTreeString();
lineStart = copy.getLineStart();
lineEnd = copy.getLineEnd();
columnStart = copy.getColumnStart();
columnEnd = copy.getColumnEnd();
lineRanges = new LineRangeList();
lineRanges.addAll(copy.getLineRanges());
category = copy.getCategory();
type = copy.getType();
severity = copy.getSeverity();
message = copy.getMessageTreeString();
description = copy.getDescription();
packageName = copy.getPackageNameTreeString();
moduleName = copy.getModuleName();
origin = copy.getOrigin();
originName = copy.getOriginName();
reference = copy.getReference();
fingerprint = copy.getFingerprint();
additionalProperties = copy.getAdditionalProperties();
return this;
}
/**
* Creates a new {@link Issue} based on the specified properties. After building the issue, this
* {@link IssueBuilder} creates a new ID, but all other {@link Issue} properties will remain unchanged.
*
* @return the created issue
*/
public Issue build() {
Issue issue = buildWithConstructor();
id = UUID.randomUUID(); // make sure that multiple invocations will create different IDs
return issue;
}
/**
* Creates a new {@link Issue} based on the specified properties. After building the issue, this
* {@link IssueBuilder} will be reset to its defaults.
*
* @return the created issue
*/
public Issue buildAndClean() {
Issue issue = buildWithConstructor();
clean();
return issue;
}
private Issue buildWithConstructor() {
cleanupLineRanges();
return new Issue(pathName, fileName, lineStart, lineEnd, columnStart, columnEnd, lineRanges,
category, type, packageName, moduleName, severity, message, description,
origin, originName, reference, fingerprint, additionalProperties, id);
}
/**
* Sets lineStart and lineEnd if they are not set, and then removes the first element of
* lineRanges if its start and end are the same as lineStart and lineEnd.
*/
@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH", justification = "False positive, `lineRanges != null` avoids a NullPointerException")
private void cleanupLineRanges() {
if (lineRanges != null && !lineRanges.isEmpty()) {
LineRange firstRange = lineRanges.get(0);
if (lineStart == 0) {
this.lineStart = firstRange.getStart();
this.lineEnd = firstRange.getEnd();
}
if (firstRange.getStart() == lineStart
&& firstRange.getEnd() == lineEnd) {
lineRanges.remove(0);
}
}
}
/**
* Creates a new {@link Issue} based on the specified properties. The returned issue is wrapped in an {@link
* Optional}. After building the issue, this {@link IssueBuilder} will be reset to its defaults.
*
* @return the created issue
* @see #buildAndClean()
*/
public Optional buildOptional() {
return Optional.of(buildAndClean());
}
@SuppressWarnings("PMD.NullAssignment")
private void clean() {
id = UUID.randomUUID(); // make sure that multiple invocations will create different IDs
lineStart = 0;
lineEnd = 0;
columnStart = 0;
columnEnd = 0;
lineRanges = new LineRangeList();
fileName = UNDEFINED_TREE_STRING;
packageName = UNDEFINED_TREE_STRING;
category = null;
type = null;
severity = null;
message = EMPTY_TREE_STRING;
description = EMPTY;
moduleName = null;
additionalProperties = null;
}
private static String normalizeFileName(@CheckForNull final String platformFileName) {
return defaultString(StringUtils.replace(
StringUtils.strip(platformFileName), "\\", "/"));
}
/**
* Creates a default String representation for undefined input parameters.
*
* @param string
* the string to check
*
* @return the valid string or a default string if the specified string is not valid
*/
private static String defaultString(@CheckForNull final String string) {
return StringUtils.defaultIfEmpty(string, UNDEFINED).intern();
}
/**
* Reduce the memory print of internal string instances.
*/
@Override
public void close() {
fileNameBuilder.dedup();
packageNameBuilder.dedup();
messageBuilder.dedup();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy