Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[java] PMD 7.x throws stack overflow in TypeOps$ProjectionVisitor while parsing a Java class #5029

Closed
uhafner opened this issue May 22, 2024 · 1 comment · Fixed by #5035
Closed
Assignees
Labels
a:bug PMD crashes or fails to analyse a file. in:type-resolution Affects the type resolution code
Milestone

Comments

@uhafner
Copy link

uhafner commented May 22, 2024

Affects PMD Version:

7.1.0

Description:

When PMD 7.x parses the file CoverageMetricColumn in my project, it produces a stack overflow. This caused no problems in PMD 6.x.

Exception Stacktrace:

[INFO] Aggregator for Coverage Plugin UNVERSIONED ......... SKIPPED
	at net.sourceforge.pmd.lang.java.symbols.internal.asm.ParseLock.ensureParsed(ParseLock.java:22)
	at net.sourceforge.pmd.lang.java.symbols.internal.asm.ClassStub.getTypeParameters(ClassStub.java:300)
	at net.sourceforge.pmd.lang.java.types.ClassTypeImpl.getFormalTypeParams(ClassTypeImpl.java:148)
	at net.sourceforge.pmd.lang.java.types.TypeOps$ProjectionVisitor.visitClass(TypeOps.java:1141)
	at net.sourceforge.pmd.lang.java.types.TypeOps$ProjectionVisitor.visitClass(TypeOps.java:1108)
	at net.sourceforge.pmd.lang.java.types.JClassType.acceptVisitor(JClassType.java:312)
	at net.sourceforge.pmd.lang.java.types.TypeOps$1.visitTypeVar(TypeOps.java:996)
	at net.sourceforge.pmd.lang.java.types.TypeOps$1.visitTypeVar(TypeOps.java:991)
	at net.sourceforge.pmd.lang.java.types.JTypeVar.acceptVisitor(JTypeVar.java:103)
	at net.sourceforge.pmd.lang.java.types.TypeOps$ProjectionVisitor.lambda$visitClass$0(TypeOps.java:1146)
	at net.sourceforge.pmd.lang.java.types.TypeOps$RecursionStop.recurseIfNotDone(TypeOps.java:1077)
	at net.sourceforge.pmd.lang.java.types.TypeOps$ProjectionVisitor.visitClass(TypeOps.java:1146)
	at net.sourceforge.pmd.lang.java.types.TypeOps$ProjectionVisitor.visitClass(TypeOps.java:1108

Code Sample demonstrating the issue:

package io.jenkins.plugins.coverage.metrics.steps;

import java.util.List;
import java.util.Optional;

import edu.hm.hafner.coverage.Metric;
import edu.hm.hafner.coverage.Value;
import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.NonNull;

import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.verb.POST;
import org.jenkinsci.Symbol;
import hudson.Extension;
import hudson.Functions;
import hudson.model.Job;
import hudson.model.Run;
import hudson.util.ListBoxModel;
import hudson.views.ListViewColumn;
import hudson.views.ListViewColumnDescriptor;
import jenkins.model.Jenkins;

import io.jenkins.plugins.coverage.metrics.color.ColorProvider;
import io.jenkins.plugins.coverage.metrics.color.ColorProvider.DisplayColors;
import io.jenkins.plugins.coverage.metrics.model.Baseline;
import io.jenkins.plugins.coverage.metrics.model.ElementFormatter;
import io.jenkins.plugins.util.JenkinsFacade;

/**
 * Dashboard column model which represents coverage metrics of different coverage types.
 *
 * @author Florian Orendi
 */
public class CoverageMetricColumn extends ListViewColumn {
    private static final ElementFormatter FORMATTER = new ElementFormatter();

    private String columnName = Messages.Coverage_Column();
    private Metric metric = Metric.LINE;
    private Baseline baseline = Baseline.PROJECT;

    /**
     * Creates a new column.
     */
    @DataBoundConstructor
    public CoverageMetricColumn() {
        super();
    }

    public ElementFormatter getFormatter() {
        return FORMATTER;
    }

    /**
     * Sets the display name of the column.
     *
     * @param columnName
     *         the human-readable name of the column
     */
    @DataBoundSetter
    public void setColumnName(final String columnName) {
        this.columnName = columnName;
    }

    public String getColumnName() {
        return columnName;
    }

    /**
     * Sets the baseline of the values that will be shown.
     *
     * @param baseline
     *         the baseline to use
     */
    @DataBoundSetter
    public void setBaseline(final Baseline baseline) {
        this.baseline = baseline;
    }

    public Baseline getBaseline() {
        return baseline;
    }

    /**
     * Sets the metric of the values that will be shown.
     *
     * @param metric
     *         the metric to use
     */
    @DataBoundSetter
    public void setMetric(final Metric metric) {
        this.metric = metric;
    }

    public Metric getMetric() {
        return metric;
    }

    /**
     * Returns all available values for the specified baseline.
     *
     * @param job
     *         the job in the current row
     *
     * @return the available values
     */
    // Called by jelly view
    public List<Value> getAllValues(final Job<?, ?> job) {
        return findAction(job).map(a -> a.getAllValues(baseline)).orElse(List.of());
    }

    /**
     * Returns a formatted and localized String representation of the specified value (without metric).
     *
     * @param value
     *         the value to format
     *
     * @return the value formatted as a string
     */
    @SuppressWarnings("unused") // Called by jelly view
    public String formatMetric(final Value value) {
        return FORMATTER.getDisplayName(value.getMetric());
    }

    /**
     * Returns a formatted and localized String representation of the specified value (without metric).
     *
     * @param value
     *         the value to format
     *
     * @return the value formatted as a string
     */
    @SuppressWarnings("unused") // Called by jelly view
    public String formatValue(final Value value) {
        return FORMATTER.formatDetails(value, Functions.getCurrentLocale());
    }

    /**
     * Provides a text which represents the coverage percentage of the selected coverage type and metric.
     *
     * @param job
     *         the job in the current row
     *
     * @return the coverage text
     */
    public String getCoverageText(final Job<?, ?> job) {
        Optional<? extends Value> coverageValue = getCoverageValue(job);
        if (coverageValue.isPresent()) {
            return FORMATTER.format(coverageValue.get(), Functions.getCurrentLocale());
        }
        return Messages.Coverage_Not_Available();
    }

    /**
     * Provides the coverage value of the selected coverage type and metric.
     *
     * @param job
     *         the job in the current row
     *
     * @return the coverage percentage
     */
    public Optional<? extends Value> getCoverageValue(final Job<?, ?> job) {
        return findAction(job).flatMap(action -> action.getStatistics().getValue(getBaseline(), metric));
    }

    private static Optional<CoverageBuildAction> findAction(final Job<?, ?> job) {
        var lastCompletedBuild = job.getLastCompletedBuild();
        if (lastCompletedBuild == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(lastCompletedBuild.getAction(CoverageBuildAction.class));
    }

    /**
     * Provides the line color for representing the passed coverage value.
     *
     * @param job
     *         the job in the current row
     * @param coverage
     *         The coverage value as percentage
     *
     * @return the line color as hex string
     */
    @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
    public DisplayColors getDisplayColors(final Job<?, ?> job, final Optional<? extends Value> coverage) {
        if (coverage.isPresent() && hasCoverageAction(job)) {
            return FORMATTER.getDisplayColors(baseline, coverage.get());
        }
        return ColorProvider.DEFAULT_COLOR;
    }

    /**
     * Provides the relative URL which can be used for accessing the coverage report.
     *
     * @param job
     *         the job in the current row
     *
     * @return the relative URL or an empty string when there is no matching URL
     */
    public String getRelativeCoverageUrl(final Job<?, ?> job) {
        if (hasCoverageAction(job)) {
            CoverageBuildAction action = job.getLastCompletedBuild().getAction(CoverageBuildAction.class);
            return action.getUrlName() + "/" + baseline.getUrl();
        }
        return "";
    }

    /**
     * Transforms percentages with a ',' decimal separator to a representation using a '.' in order to use the
     * percentage for styling HTML tags.
     *
     * @param percentage
     *         The text representation of a percentage
     *
     * @return the formatted percentage string
     */
    public String getBackgroundColorFillPercentage(final String percentage) {
        return FORMATTER.getBackgroundColorFillPercentage(percentage);
    }

    /**
     * Checks whether a {@link CoverageBuildAction} exists within the completed build.
     *
     * @param job
     *         the job in the current row
     *
     * @return {@code true} whether the action exists, else {@code false}
     */
    private boolean hasCoverageAction(final Job<?, ?> job) {
        Run<?, ?> lastCompletedBuild = job.getLastCompletedBuild();
        return lastCompletedBuild != null && !lastCompletedBuild.getActions(CoverageBuildAction.class).isEmpty();
    }

    /**
     * Descriptor of the column.
     */
    @Extension(optional = true)
    @Symbol("coverageTotalsColumn")
    public static class CoverageMetricColumnDescriptor extends ListViewColumnDescriptor {
        /**
         * Creates a new descriptor.
         */
        @SuppressWarnings("unused") // Required for Jenkins Extensions
        public CoverageMetricColumnDescriptor() {
            this(new JenkinsFacade());
        }

        @VisibleForTesting
        CoverageMetricColumnDescriptor(final JenkinsFacade jenkins) {
            super();

            this.jenkins = jenkins;
        }

        private final JenkinsFacade jenkins;

        @NonNull
        @Override
        public String getDisplayName() {
            return Messages.Coverage_Column();
        }

        /**
         * Returns a model with all {@link Metric metrics} that can be used in quality gates.
         *
         * @return a model with all {@link Metric metrics}.
         */
        @POST
        @SuppressWarnings("unused") // used by Stapler view data binding
        public ListBoxModel doFillMetricItems() {
            if (jenkins.hasPermission(Jenkins.READ)) {
                return FORMATTER.getMetricItems();
            }
            return new ListBoxModel();
        }

        /**
         * Returns a model with all {@link Metric metrics} that can be used in quality gates.
         *
         * @return a model with all {@link Metric metrics}.
         */
        @POST
        @SuppressWarnings("unused") // used by Stapler view data binding
        public ListBoxModel doFillBaselineItems() {
            if (jenkins.hasPermission(Jenkins.READ)) {
                return FORMATTER.getBaselineItems();
            }
            return new ListBoxModel();
        }
    }
}

Steps to reproduce:

Run my project (with the PMD 7.x migration) with mvn verify.

I am using a custom PMD ruleset and a custom PMD Maven configuration.

I also created a PR in my project that exposes the bug (see console log of maven).

Running PMD through:

Maven

@uhafner uhafner added the a:bug PMD crashes or fails to analyse a file. label May 22, 2024
@uhafner
Copy link
Author

uhafner commented May 22, 2024

Here is the diff: jenkinsci/coverage-plugin@084ca79

When I exclude the problematic file, then PMD runs sucessfully.

@oowekyala oowekyala self-assigned this May 22, 2024
@oowekyala oowekyala added the in:type-resolution Affects the type resolution code label May 22, 2024
@oowekyala oowekyala added this to the 7.2.0 milestone May 23, 2024
@jsotuyod jsotuyod changed the title [java] Regression: PMD 7.x throws stack overflow in TypeOps$ProjectionVisitor while parsing a Java class [java] PMD 7.x throws stack overflow in TypeOps$ProjectionVisitor while parsing a Java class May 23, 2024
jsotuyod added a commit that referenced this issue May 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a:bug PMD crashes or fails to analyse a file. in:type-resolution Affects the type resolution code
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants