Skip to content

Commit 4ca541b

Browse files
committed
Add support for checking package exports for changed API
Currently API tools only check for API changes on the bundle level, but even more important are checks on the exported packages. If the bundle version is not properly incremented this can lead to method not found or similar errors, also consumers of the package can not depend on the package version reliable to get new API. This now adds some basic checks to check for API changes on the package level, compare it with the baseline and suggest a new version based on the resulting delta being a breaking or non breaking change.
1 parent 5702213 commit 4ca541b

File tree

5 files changed

+165
-13
lines changed

5 files changed

+165
-13
lines changed

apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/builder/BaseApiAnalyzer.java

Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
import java.util.HashSet;
3030
import java.util.List;
3131
import java.util.Map;
32+
import java.util.Map.Entry;
3233
import java.util.Properties;
3334
import java.util.Set;
35+
import java.util.function.Function;
3436
import java.util.jar.JarFile;
37+
import java.util.stream.Collectors;
3538

3639
import org.eclipse.core.resources.IFile;
3740
import org.eclipse.core.resources.IMarker;
@@ -68,13 +71,15 @@
6871
import org.eclipse.jdt.internal.core.BinaryType;
6972
import org.eclipse.jface.text.BadLocationException;
7073
import org.eclipse.jface.text.IDocument;
74+
import org.eclipse.osgi.service.resolver.ExportPackageDescription;
7175
import org.eclipse.osgi.service.resolver.ResolverError;
7276
import org.eclipse.osgi.service.resolver.VersionConstraint;
7377
import org.eclipse.osgi.util.NLS;
7478
import org.eclipse.pde.api.tools.internal.ApiBaselineManager;
7579
import org.eclipse.pde.api.tools.internal.ApiFilterStore;
7680
import org.eclipse.pde.api.tools.internal.IApiCoreConstants;
7781
import org.eclipse.pde.api.tools.internal.comparator.Delta;
82+
import org.eclipse.pde.api.tools.internal.model.BundleComponent;
7883
import org.eclipse.pde.api.tools.internal.model.ProjectComponent;
7984
import org.eclipse.pde.api.tools.internal.model.WorkspaceBaseline;
8085
import org.eclipse.pde.api.tools.internal.problems.ApiProblemFactory;
@@ -2043,6 +2048,11 @@ private void checkApiComponentVersion(final IApiComponent reference, final IApiC
20432048
}
20442049
IDelta[] breakingChanges = fBuildState.getBreakingChanges();
20452050
IDelta[] compatibleChanges = fBuildState.getCompatibleChanges();
2051+
if (reference instanceof BundleComponent referenceBundle) {
2052+
if (component instanceof BundleComponent componentBundle) {
2053+
checkApiComponentPackageVersions(referenceBundle, componentBundle, breakingChanges, compatibleChanges);
2054+
}
2055+
}
20462056
if (breakingChanges.length != 0) {
20472057
// make sure that the major version has been incremented
20482058
if (compversion.getMajor() <= refversion.getMajor()) {
@@ -2287,6 +2297,85 @@ && checkIfMajorVersionIncreased(compversion, refversion)) {
22872297
}
22882298
}
22892299

2300+
private void checkApiComponentPackageVersions(BundleComponent referenceBundle, BundleComponent componentBundle,
2301+
IDelta[] breakingChanges, IDelta[] compatibleChanges) throws CoreException {
2302+
Map<String, ExportPackageDescription> referencePackages = Arrays
2303+
.stream(referenceBundle.getBundleDescription().getExportPackages())
2304+
.collect(Collectors.toMap(ExportPackageDescription::getName, Function.identity()));
2305+
Map<String, ExportPackageDescription> componentPackages = Arrays
2306+
.stream(componentBundle.getBundleDescription().getExportPackages())
2307+
.collect(Collectors.toMap(ExportPackageDescription::getName, Function.identity()));
2308+
// a mapping between a package name and a required change
2309+
Map<String, RequiredPackageVersionChange> requiredChanges = new HashMap<>();
2310+
// we must compare compatible changes first, so these where overwritten later by
2311+
// breaking changes probably
2312+
for (IDelta delta : compatibleChanges) {
2313+
// a compatible change must result in a minor package version increment
2314+
analyzePackageDelta(delta, IApiProblem.MINOR_VERSION_CHANGE_PACKAGE, referencePackages, componentPackages,
2315+
requiredChanges);
2316+
}
2317+
for (IDelta delta : breakingChanges) {
2318+
// a breaking change must result in a major package change
2319+
analyzePackageDelta(delta, IApiProblem.MAJOR_VERSION_CHANGE_PACKAGE, referencePackages, componentPackages,
2320+
requiredChanges);
2321+
}
2322+
for (String pkg : referencePackages.keySet()) {
2323+
if (!componentPackages.containsKey(pkg)) {
2324+
// TODO a package export was removed! This should be require a major version
2325+
// change in bundle!
2326+
}
2327+
}
2328+
for (Entry<String, RequiredPackageVersionChange> entry : requiredChanges.entrySet()) {
2329+
addProblem(createPackageVersionProblem(entry.getKey(), entry.getValue()));
2330+
}
2331+
}
2332+
2333+
private void analyzePackageDelta(IDelta delta, int category,
2334+
Map<String, ExportPackageDescription> referencePackages,
2335+
Map<String, ExportPackageDescription> componentPackages,
2336+
Map<String, RequiredPackageVersionChange> requiredChanges) {
2337+
String packageName = delta.getTypeName();
2338+
if (packageName != null) {
2339+
int idx = packageName.lastIndexOf('.');
2340+
if (idx > 0) {
2341+
packageName = packageName.substring(0, idx);
2342+
}
2343+
ExportPackageDescription pkgRef = referencePackages.get(packageName);
2344+
if (pkgRef == null) {
2345+
return;
2346+
}
2347+
Version baselineVersion = pkgRef.getVersion();
2348+
if (baselineVersion == null || Version.emptyVersion.equals(baselineVersion)) {
2349+
return;
2350+
}
2351+
ExportPackageDescription baselinePackage = componentPackages.get(packageName);
2352+
if (baselinePackage == null) {
2353+
return;
2354+
}
2355+
Version suggested;
2356+
if (IApiProblem.MINOR_VERSION_CHANGE_PACKAGE == category) {
2357+
suggested = new Version(baselineVersion.getMajor(), baselineVersion.getMinor() + 1, 0);
2358+
} else {
2359+
suggested = new Version(baselineVersion.getMajor() + 1, baselineVersion.getMinor(), 0);
2360+
}
2361+
Version compVersion = baselinePackage.getVersion();
2362+
if (compVersion == null || compVersion.compareTo(baselineVersion) < 0) {
2363+
requiredChanges.put(packageName,
2364+
new RequiredPackageVersionChange(category, baselineVersion, compVersion, suggested));
2365+
}
2366+
if (compVersion.getMajor() > baselineVersion.getMajor()) {
2367+
return;
2368+
}
2369+
if (IApiProblem.MINOR_VERSION_CHANGE_PACKAGE == category) {
2370+
if (compVersion.getMinor() > baselineVersion.getMinor()) {
2371+
return;
2372+
}
2373+
}
2374+
requiredChanges.put(packageName,
2375+
new RequiredPackageVersionChange(category, baselineVersion, compVersion, suggested));
2376+
}
2377+
}
2378+
22902379
private boolean reportMultipleIncreaseMinorVersion(Version compversion, Version refversion) {
22912380
if (compversion.getMajor() == refversion.getMajor()) {
22922381
if (((compversion.getMinor() - refversion.getMinor()) > 1)) {
@@ -2364,6 +2453,12 @@ private String collectDetails(final IDelta[] deltas) {
23642453
return String.valueOf(writer.getBuffer());
23652454
}
23662455

2456+
private IApiProblem createPackageVersionProblem(String packageName, RequiredPackageVersionChange versionChange) {
2457+
return createVersionProblem(versionChange.category(),
2458+
new String[] { packageName, versionChange.suggested().toString(), versionChange.baseline().toString() },
2459+
versionChange.suggested().toString(), null, Constants.EXPORT_PACKAGE, packageName);
2460+
}
2461+
23672462
/**
23682463
* Creates a marker on a manifest file for a version numbering problem and
23692464
* returns it or <code>null</code>
@@ -2372,6 +2467,11 @@ private String collectDetails(final IDelta[] deltas) {
23722467
* @return a new {@link IApiProblem} or <code>null</code>
23732468
*/
23742469
private IApiProblem createVersionProblem(int kind, final String[] messageargs, String version, String description) {
2470+
return createVersionProblem(kind, messageargs, version, description, Constants.BUNDLE_VERSION, null);
2471+
}
2472+
2473+
private IApiProblem createVersionProblem(int kind, final String[] messageargs, String version, String description,
2474+
String header, String value) {
23752475
IResource manifestFile = null;
23762476
String path = JarFile.MANIFEST_NAME;
23772477
if (fJavaProject != null) {
@@ -2394,7 +2494,7 @@ private IApiProblem createVersionProblem(int kind, final String[] messageargs, S
23942494
String line = null;
23952495
loop: while ((line = reader.readLine()) != null) {
23962496
lineCounter++;
2397-
if (line.startsWith(Constants.BUNDLE_VERSION)) {
2497+
if (line.startsWith(header)) {
23982498
lineNumber = lineCounter;
23992499
break loop;
24002500
}
@@ -2406,24 +2506,31 @@ private IApiProblem createVersionProblem(int kind, final String[] messageargs, S
24062506
}
24072507
if (lineNumber != -1 && contents != null) {
24082508
// initialize char start, char end
2409-
int index = CharOperation.indexOf(Constants.BUNDLE_VERSION.toCharArray(), contents, true);
2410-
loop: for (int i = index + Constants.BUNDLE_VERSION.length() + 1, max = contents.length; i < max; i++) {
2509+
int index = CharOperation.indexOf(header.toCharArray(), contents, true);
2510+
int headerOffset = index + header.length() + 1;
2511+
loop: for (int i = headerOffset, max = contents.length; i < max; i++) {
24112512
char currentCharacter = contents[i];
24122513
if (CharOperation.isWhitespace(currentCharacter)) {
24132514
continue;
24142515
}
24152516
charStart = i;
24162517
break loop;
24172518
}
2418-
loop: for (int i = charStart + 1, max = contents.length; i < max; i++) {
2419-
switch (contents[i]) {
2420-
case '\r':
2421-
case '\n':
2422-
charEnd = i;
2423-
break loop;
2424-
default:
2425-
continue;
2519+
if (value == null) {
2520+
loop: for (int i = charStart + 1, max = contents.length; i < max; i++) {
2521+
switch (contents[i])
2522+
{
2523+
case '\r':
2524+
case '\n':
2525+
charEnd = i;
2526+
break loop;
2527+
default:
2528+
continue;
2529+
}
24262530
}
2531+
} else {
2532+
// TODO find the matching value in the header
2533+
charEnd = charStart;
24272534
}
24282535
} else {
24292536
lineNumber = 1;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Christoph Läubrich and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Christoph Läubrich - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.pde.api.tools.internal.builder;
15+
16+
import org.osgi.framework.Version;
17+
18+
record RequiredPackageVersionChange(int category, Version baseline, Version current, Version suggested) {
19+
20+
}

apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/ApiProblemFactory.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,10 @@ public static int getProblemMessageId(int category, int element, int kind, int f
590590
return 58;
591591
case IApiProblem.MINOR_VERSION_CHANGE_UNNECESSARILY:
592592
return 59;
593-
593+
case IApiProblem.MAJOR_VERSION_CHANGE_PACKAGE:
594+
return 65;
595+
case IApiProblem.MINOR_VERSION_CHANGE_PACKAGE:
596+
return 63;
594597
default:
595598
break;
596599
}

apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/problems/problemmessages.properties

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# Contributors:
1212
# IBM Corporation - initial API and implementation
1313
###############################################################################
14-
# available message ids 63, 65, 68, 70, 71, 75, 80,
14+
# available message ids 68, 70, 71, 75, 80,
1515
# 82, 83, 88, 90, 93
1616

1717
#API baseline
@@ -37,6 +37,8 @@
3737
19 = The major version should be incremented in version {0}, because the modification of the version range for the re-exported bundle {1} requires a major version change
3838
20 = The minor version should be incremented in version {0}, because the modification of the version range for the re-exported bundle {1} requires a minor version change
3939
62 = The major version should be incremented in version {0}, because the bundle {1} is no longer re-exported
40+
63 = The minor version for the package ''{0}'' should be incremented to version {1}, since new APIs have been added since version {2}
41+
65 = The major version for the package ''{0}'' should be incremented to version {1}, since API breakage occurred since version {2}
4042

4143
#API usage problems
4244
#{0} = referenced type name

apitools/org.eclipse.pde.api.tools/src/org/eclipse/pde/api/tools/internal/provisional/problems/IApiProblem.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,26 @@ public interface IApiProblem {
242242
* @see #CATEGORY_USAGE
243243
*/
244244

245+
/**
246+
* Constant representing the value of the major version change
247+
* {@link IApiProblem} kind for a package. <br>
248+
* Value is: <code>11</code>
249+
*
250+
* @see #getKind()
251+
* @see #CATEGORY_VERSION
252+
*/
253+
public static final int MAJOR_VERSION_CHANGE_PACKAGE = 11;
254+
255+
/**
256+
* Constant representing the value of the minor version change
257+
* {@link IApiProblem} kind for a package. <br>
258+
* Value is: <code>12</code>
259+
*
260+
* @see #getKind()
261+
* @see #CATEGORY_VERSION
262+
*/
263+
public static final int MINOR_VERSION_CHANGE_PACKAGE = 12;
264+
245265
public static final int ILLEGAL_EXTEND = 1;
246266

247267
/**

0 commit comments

Comments
 (0)