/* * Copyright $year Lukas Krejci * * 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.revapi.basic; import static java.util.stream.Collectors.toList; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.jboss.dmr.ModelNode; import org.revapi.AnalysisContext; import org.revapi.Archive; import org.revapi.CompatibilityType; import org.revapi.Difference; import org.revapi.DifferenceSeverity; import org.revapi.DifferenceTransform; import org.revapi.Element; /** * @author Lukas Krejci * @since 0.3.7 */ public class SemverIgnoreTransform implements DifferenceTransform<Element> { private boolean enabled; private DifferenceSeverity allowedSeverity; private List<String> passThroughDifferences; @Nonnull @Override public Pattern[] getDifferenceCodePatterns() { return enabled ? new Pattern[]{Pattern.compile(".*")} : new Pattern[0]; } @Nullable @Override public Difference transform(@Nullable Element oldElement, @Nullable Element newElement, @Nonnull Difference difference) { if (!enabled) { return difference; } if (passThroughDifferences.contains(difference.code)) { return difference; } if (allowedSeverity == null) { return asBreaking(difference); } else if (allowedSeverity == DifferenceSeverity.BREAKING) { return null; } else { DifferenceSeverity diffSeverity = getMaxSeverity(difference); if (allowedSeverity.ordinal() - diffSeverity.ordinal() >= 0) { return null; } else { return asBreaking(difference); } } } private Difference asBreaking(Difference d) { return Difference.builder().withCode(d.code) .withDescription(d.description + " (breaks semantic versioning)") .withName("Incompatible with the current version: " + d.name) .addAttachments(d.attachments).addClassifications(d.classification) .addClassification(CompatibilityType.OTHER, DifferenceSeverity.BREAKING).build(); } private DifferenceSeverity getMaxSeverity(Difference diff) { return diff.classification.values().stream().max((d1, d2) -> d1.ordinal() - d2.ordinal()).get(); } @Override public void close() throws Exception { } @Nullable @Override public String[] getConfigurationRootPaths() { return new String[]{"revapi.semver.ignore"}; } @Nullable @Override public Reader getJSONSchema(@Nonnull String configurationRootPath) { if ("revapi.semver.ignore".equals(configurationRootPath)) { return new InputStreamReader(getClass().getResourceAsStream("/META-INF/semver-ignore-schema.json"), Charset.forName("UTF-8")); } else { return null; } } @Override public void initialize(@Nonnull AnalysisContext analysisContext) { ModelNode node = analysisContext.getConfiguration().get("revapi", "semver", "ignore"); if (hasMultipleElements(analysisContext.getOldApi().getArchives()) || hasMultipleElements(analysisContext.getNewApi().getArchives())) { throw new IllegalArgumentException( "The semver extension doesn't handle changes in multiple archives at once."); } enabled = node.get("enabled").isDefined() && node.get("enabled").asBoolean(); if (enabled) { Archive oldArchive = analysisContext.getOldApi().getArchives().iterator().next(); Archive newArchive = analysisContext.getNewApi().getArchives().iterator().next(); if (!(oldArchive instanceof Archive.Versioned)) { throw new IllegalArgumentException("Old archive doesn't support extracting the version."); } if (!(newArchive instanceof Archive.Versioned)) { throw new IllegalArgumentException("New archive doesn't support extracting the version."); } String oldVersionString = ((Archive.Versioned) oldArchive).getVersion(); String newVersionString = ((Archive.Versioned) newArchive).getVersion(); Version oldVersion = Version.parse(oldVersionString); Version newVersion = Version.parse(newVersionString); if (newVersion.major == 0 && oldVersion.major == 0 && !node.get("versionIncreaseAllows").isDefined()) { DifferenceSeverity minorChangeAllowed = asSeverity(node.get("versionIncreaseAllows", "minor"), DifferenceSeverity.BREAKING); DifferenceSeverity patchVersionAllowed = asSeverity(node.get("versionIncreaseAllows", "patch"), DifferenceSeverity.NON_BREAKING); if (newVersion.minor > oldVersion.minor) { allowedSeverity = minorChangeAllowed; } else if (newVersion.minor == oldVersion.minor && newVersion.patch > oldVersion.patch) { allowedSeverity = patchVersionAllowed; } else { allowedSeverity = null; } } else { DifferenceSeverity majorChangeAllowed = asSeverity(node.get("versionIncreaseAllows", "major"), DifferenceSeverity.BREAKING); DifferenceSeverity minorChangeAllowed = asSeverity(node.get("versionIncreaseAllows", "minor"), DifferenceSeverity.NON_BREAKING); DifferenceSeverity patchVersionAllowed = asSeverity(node.get("versionIncreaseAllows", "patch"), DifferenceSeverity.EQUIVALENT); if (newVersion.major > oldVersion.major) { allowedSeverity = majorChangeAllowed; } else if (newVersion.major == oldVersion.major && newVersion.minor > oldVersion.minor) { allowedSeverity = minorChangeAllowed; } else { allowedSeverity = patchVersionAllowed; } } passThroughDifferences = Collections.emptyList(); if (node.get("passThroughDifferences").isDefined()) { passThroughDifferences = node.get("passThroughDifferences").asList().stream().map(ModelNode::asString).collect(toList()); } } } private boolean hasMultipleElements(Iterable<?> it) { Iterator<?> i = it.iterator(); if (!i.hasNext()) { return false; } i.next(); return i.hasNext(); } private static final class Version { private static final Pattern SEMVER_PATTERN = Pattern.compile("(\\d+)(\\.(\\d+)(?:\\.)?(\\d*))?(\\.|-|\\+)?([0-9A-Za-z-.]*)?"); final int major; final int minor; final int patch; final String sep; final String suffix; static Version parse(String version) { Matcher m = SEMVER_PATTERN.matcher(version); if (!m.matches()) { throw new IllegalArgumentException("Could not parse the version string '" + version + "'. It does not follow the semver schema."); } int major = Integer.valueOf(m.group(1)); String minorMatch = m.group(3); int minor = minorMatch == null || minorMatch.isEmpty() ? 0 : Integer.valueOf(minorMatch); int patch = 0; String patchMatch = m.group(4); if (patchMatch != null && !patchMatch.isEmpty()) { patch = Integer.valueOf(patchMatch); } String sep = m.group(5); String suffix = m.group(6); if (sep != null && sep.isEmpty()) { sep = null; } if (suffix != null && suffix.isEmpty()) { suffix = null; } return new Version(major, minor, patch, sep, suffix); } Version(int major, int minor, int patch, String sep, String suffix) { this.major = major; this.minor = minor; this.patch = patch; this.sep = sep; this.suffix = suffix; } } private static DifferenceSeverity asSeverity(ModelNode configNode, DifferenceSeverity defaultValue) { if (configNode == null || !configNode.isDefined()) { return defaultValue; } else { switch (configNode.asString()) { case "none": return null; case "equivalent": return DifferenceSeverity.EQUIVALENT; case "nonBreaking": return DifferenceSeverity.NON_BREAKING; case "potentiallyBreaking": return DifferenceSeverity.POTENTIALLY_BREAKING; case "breaking": return DifferenceSeverity.BREAKING; default: return defaultValue; } } } }