diff --git a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysis.java b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysis.java new file mode 100644 index 0000000000..c5c027cca2 --- /dev/null +++ b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysis.java @@ -0,0 +1,274 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.apitools; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.Callable; + +import org.eclipse.core.resources.ICommand; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.IWorkspaceDescription; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.osgi.service.resolver.ResolverError; +import org.eclipse.pde.api.tools.internal.BundleListTargetLocation; +import org.eclipse.pde.api.tools.internal.FilterStore; +import org.eclipse.pde.api.tools.internal.builder.BaseApiAnalyzer; +import org.eclipse.pde.api.tools.internal.builder.BuildContext; +import org.eclipse.pde.api.tools.internal.model.ApiModelFactory; +import org.eclipse.pde.api.tools.internal.model.BundleComponent; +import org.eclipse.pde.api.tools.internal.model.ProjectComponent; +import org.eclipse.pde.api.tools.internal.model.SystemLibraryApiComponent; +import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin; +import org.eclipse.pde.api.tools.internal.provisional.IApiFilterStore; +import org.eclipse.pde.api.tools.internal.provisional.model.IApiBaseline; +import org.eclipse.pde.api.tools.internal.provisional.model.IApiComponent; +import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem; +import org.eclipse.pde.core.target.ITargetDefinition; +import org.eclipse.pde.core.target.ITargetLocation; +import org.eclipse.pde.core.target.ITargetPlatformService; +import org.eclipse.pde.core.target.LoadTargetDefinitionJob; +import org.eclipse.pde.core.target.TargetBundle; +import org.eclipse.pde.internal.core.target.TargetPlatformService; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +public class ApiAnalysis implements Serializable, Callable { + + private Collection baselineBundles; + private Collection targetBundles; + private String baselineName; + private String apiFilterFile; + private String projectDir; + private boolean debug; + private String apiPreferences; + private String binaryArtifact; + + public ApiAnalysis(Collection baselineBundles, Collection dependencyBundles, String baselineName, + Path apiFilterFile, Path apiPreferences, Path projectDir, boolean debug, Path binaryArtifact) { + this.targetBundles = dependencyBundles.stream().map(ApiAnalysis::pathAsString).toList(); + this.baselineBundles = baselineBundles.stream().map(ApiAnalysis::pathAsString).toList(); + this.baselineName = baselineName; + this.apiFilterFile = pathAsString(apiFilterFile); + this.apiPreferences = pathAsString(apiPreferences); + this.projectDir = pathAsString(projectDir); + this.binaryArtifact = pathAsString(binaryArtifact); + this.debug = debug; + } + + @Override + public ApiAnalysisResult call() throws Exception { + ApiAnalysisResult result = new ApiAnalysisResult(); + printVersion(); + disableAutoBuild(); + setTargetPlatform(); + deleteAllProjects(); + BundleComponent projectComponent = importProject(); + IApiBaseline baseline = createBaseline(baselineBundles, baselineName + " - baseline"); + ResolverError[] resolverErrors = projectComponent.getErrors(); + if (resolverErrors != null && resolverErrors.length > 0) { + for (ResolverError error : resolverErrors) { + result.addResolverError(error); + } + } + IApiFilterStore filterStore = getApiFilterStore(projectComponent); + Properties preferences = getPreferences(); + BaseApiAnalyzer analyzer = new BaseApiAnalyzer(); + try { + analyzer.setContinueOnResolverError(true); + analyzer.analyzeComponent(null, filterStore, preferences, baseline, projectComponent, new BuildContext(), + new NullProgressMonitor()); + IApiProblem[] problems = analyzer.getProblems(); + for (IApiProblem problem : problems) { + result.addProblem(problem); + debug(String.valueOf(problem)); + } + } finally { + analyzer.dispose(); + ResourcesPlugin.getWorkspace().save(true, new NullProgressMonitor()); + } + return result; + } + + private BundleComponent importProject() throws CoreException, IOException { + IPath projectPath = IPath.fromOSString(projectDir); + IPath projectDescriptionFile = projectPath.append(IProjectDescription.DESCRIPTION_FILE_NAME); + IProjectDescription projectDescription = ResourcesPlugin.getWorkspace() + .loadProjectDescription(projectDescriptionFile); + projectDescription.setLocation(projectPath); + projectDescription.setBuildSpec(new ICommand[0]); + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectDescription.getName()); + project.create(projectDescription, new NullProgressMonitor()); + project.open(new NullProgressMonitor()); + IApiBaseline workspaceBaseline = ApiPlugin.getDefault().getApiBaselineManager().getWorkspaceBaseline(); + IApiComponent component = workspaceBaseline.getApiComponent(project); + if (component instanceof ProjectComponent projectComponent) { + debug("Project component was found"); + return projectComponent; + } + IApiComponent[] components = workspaceBaseline.getApiComponents(); + for (IApiComponent c : components) { + String location = c.getLocation(); + if (location != null && IPath.fromOSString(location).equals(projectPath) + && c instanceof BundleComponent bundle) { + debug("Fallback to binary bundle component"); + return bundle; + } + } + if (binaryArtifact != null) { + debug("Fallback to binary artifact"); + // TODO we would like to pass the imported project then see + // https://github.com/eclipse-pde/eclipse.pde/pull/786 + IApiComponent binaryComponent = ApiModelFactory.newApiComponent(workspaceBaseline, binaryArtifact); + if (binaryComponent instanceof BundleComponent bundle) { + workspaceBaseline.addApiComponents(new IApiComponent[] { bundle }); + return bundle; + } + + } + throw new RuntimeException("Can't import project"); + } + + private void deleteAllProjects() throws CoreException { + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + project.delete(IResource.NEVER_DELETE_PROJECT_CONTENT | IResource.FORCE, new NullProgressMonitor()); + } + } + + private void disableAutoBuild() throws CoreException { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IWorkspaceDescription desc = workspace.getDescription(); + desc.setAutoBuilding(false); + workspace.setDescription(desc); + } + + private Properties getPreferences() throws IOException { + Properties properties = new Properties(); + if (apiPreferences != null) { + Path path = Path.of(apiPreferences); + if (Files.isRegularFile(path)) { + try (InputStream stream = Files.newInputStream(path)) { + properties.load(stream); + } + } + } + return properties; + } + + private void printVersion() { + Bundle apiToolsBundle = FrameworkUtil.getBundle(ApiModelFactory.class); + if (apiToolsBundle != null) { + debug("API Tools version: " + apiToolsBundle.getVersion()); + } + } + + private IApiBaseline createBaseline(Collection bundles, String name) throws CoreException { + debug("==== " + name + " ===="); + IApiBaseline baseline = ApiModelFactory.newApiBaseline(name); + List baselineComponents = new ArrayList(); + for (String baselineBundle : bundles) { + IApiComponent component = ApiModelFactory.newApiComponent(baseline, baselineBundle); + if (component != null) { + debug(component.getSymbolicName() + " " + component.getVersion() + " -- " + + new File(Objects.requireNonNullElse(component.getLocation(), "Unknown")).getName()); + baselineComponents.add(component); + } + } + baseline.addApiComponents(baselineComponents.toArray(IApiComponent[]::new)); + for (IApiComponent component : baseline.getApiComponents()) { + if (component instanceof SystemLibraryApiComponent systemLibrary) { + debug("System Component:"); + debug("\tVersion: " + systemLibrary.getVersion()); + debug("\tLocation: " + systemLibrary.getLocation()); + for (String ee : systemLibrary.getExecutionEnvironments()) { + debug("\tExecution Environment: " + ee); + } + } + + } + return baseline; + } + + private IApiFilterStore getApiFilterStore(BundleComponent bundle) { + return new FilterStore(bundle) { + @Override + protected synchronized void initializeApiFilters() { + if (fFilterMap == null) { + fFilterMap = new HashMap<>(5); + if (apiFilterFile != null) { + Path path = Path.of(apiFilterFile); + if (Files.isRegularFile(path)) { + try (InputStream stream = Files.newInputStream(path)) { + readFilterFile(stream); + } catch (IOException e) { + debug(e.toString()); + } + } + } + } + } + }; + } + + private void debug(String string) { + if (debug) { + System.out.println(string); + } + } + + private void setTargetPlatform() throws IOException, CoreException, InterruptedException { + ITargetPlatformService service = TargetPlatformService.getDefault(); + ITargetDefinition target = service.newTarget(); + target.setName("buildpath"); + TargetBundle[] bundles = targetBundles.stream()// + .map(absoluteFile -> { + try { + return new TargetBundle(new File(absoluteFile)); + } catch (CoreException e) { + debug(e.toString()); + return null; + } + }).filter(Objects::nonNull)// + .toArray(TargetBundle[]::new); + target.setTargetLocations(new ITargetLocation[] { new BundleListTargetLocation(bundles) }); + service.saveTargetDefinition(target); + Job job = new LoadTargetDefinitionJob(target); + job.schedule(); + job.join(); + } + + private static String pathAsString(Path path) { + if (path != null) { + return path.toAbsolutePath().toString(); + } + return null; + } + +} diff --git a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisMojo.java b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisMojo.java index 48ca2904d3..f2c64a8834 100644 --- a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisMojo.java +++ b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisMojo.java @@ -12,14 +12,9 @@ *******************************************************************************/ package org.eclipse.tycho.apitools; -import java.io.BufferedWriter; import java.io.File; -import java.io.IOException; import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -27,6 +22,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.maven.artifact.Artifact; @@ -35,12 +31,16 @@ import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; +import org.eclipse.pde.api.tools.internal.IApiCoreConstants; +import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin; +import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem; import org.eclipse.tycho.ArtifactDescriptor; import org.eclipse.tycho.ArtifactKey; import org.eclipse.tycho.ClasspathEntry; @@ -89,6 +89,9 @@ public class ApiAnalysisMojo extends AbstractMojo { @Parameter(defaultValue = "false", property = "tycho.apitools.verify.skip") private boolean skip; + @Parameter(defaultValue = "false", property = "tycho.apitools.debug") + private boolean debug; + @Parameter(defaultValue = "true", property = "tycho.apitools.verify.skipIfReplaced") private boolean skipIfReplaced; @@ -104,6 +107,12 @@ public class ApiAnalysisMojo extends AbstractMojo { @Parameter private Map properties; + @Parameter(defaultValue = "${project.basedir}/.settings/" + IApiCoreConstants.API_FILTERS_XML_NAME) + private File apiFilter; + + @Parameter(defaultValue = "${project.basedir}/.settings/org.eclipse.pde.api.tools.prefs") + private File apiPreferences; + @Component private EclipseWorkspaceManager workspaceManager; @@ -134,39 +143,88 @@ public void execute() throws MojoExecutionException, MojoFailureException { } if (supportedPackagingTypes.contains(project.getPackaging())) { + Log log = getLog(); if (skipIfReplaced && wasReplaced()) { - getLog().info("Skipped because main artifact was replaced with baseline!"); + log.info("Skipped because main artifact was replaced with baseline!"); return; } long start = System.currentTimeMillis(); - Path targetFile; + Collection baselineBundles; try { - targetFile = createTargetFile(); + baselineBundles = getBaselineBundles(); } catch (DependencyResolutionException e) { - getLog().warn("Can't resolve API baseline, API baseline check is skipped!"); + log.warn("Can't resolve API baseline, API baseline check is skipped!"); return; - + } + Collection dependencyBundles; + try { + dependencyBundles = getProjectDependencies(); + } catch (Exception e) { + throw new MojoFailureException("Can't fetch dependencies!", e); } EclipseWorkspace workspace = getWorkspace(); - List configuration = setupArguments(targetFile); EclipseApplication apiApplication = applicationResolver.getApiApplication(workspace.getKey().repository); EclipseFramework eclipseFramework; try { - eclipseFramework = apiApplication.startFramework(workspace, configuration); + eclipseFramework = apiApplication.startFramework(workspace, List.of()); } catch (BundleException e) { throw new MojoFailureException("Start Framework failed!", e); } + ApiAnalysisResult analysisResult; try { - eclipseFramework.start(); + analysisResult = eclipseFramework.execute(new ApiAnalysis(baselineBundles, dependencyBundles, + project.getName(), fileToPath(apiFilter), fileToPath(apiPreferences), + fileToPath(project.getBasedir()), debug, fileToPath(project.getArtifact().getFile()))); } catch (Exception e) { throw new MojoExecutionException("Execute ApiApplication failed", e); } finally { eclipseFramework.close(); } - getLog().info("API Analysis finished in " + time(start) + "."); + log.info("API Analysis finished in " + time(start) + "."); + analysisResult.resolveErrors() + .forEach(resolveError -> log.warn(resolveError + " analysis might be inaccurate!")); + Map> problems = analysisResult.problems() + .collect(Collectors.groupingBy(IApiProblem::getSeverity)); + List errors = problems.getOrDefault(ApiPlugin.SEVERITY_ERROR, List.of()); + List warnings = problems.getOrDefault(ApiPlugin.SEVERITY_WARNING, List.of()); + log.info(errors.size() + " API ERRORS"); + log.info(warnings.size() + " API warnings"); + for (IApiProblem problem : errors) { + printProblem(problem, "API ERROR", log::error); + } + for (IApiProblem problem : warnings) { + printProblem(problem, "API WARNING", log::warn); + } + if (errors.size() > 0) { + String msg = errors.stream().map(problem -> { + if (problem.getResourcePath() == null) { + return problem.getMessage(); + } + return problem.getResourcePath() + ":" + problem.getLineNumber() + " " + problem.getMessage(); + }).collect(Collectors.joining(System.lineSeparator())); + throw new MojoFailureException("There are API errors:" + System.lineSeparator() + msg); + } } } + private void printProblem(IApiProblem problem, String type, Consumer consumer) { + Path path = getFullPath(problem); + String file = path.getFileName().toString(); + int lineNumber = problem.getLineNumber(); + String message = problem.getMessage().trim(); + consumer.accept( + String.format("[%s] File %s at line %d: %s (location: %s)", type, file, lineNumber, message, path)); + + } + + private Path getFullPath(IApiProblem problem) { + String path = problem.getResourcePath(); + if (path == null) { + return Path.of("unkown"); + } + return project.getBasedir().toPath().resolve(path); + } + private EclipseWorkspace getWorkspace() { return workspaceManager.getWorkspace(new ApiAppKey(getRepository())); } @@ -186,49 +244,22 @@ private MavenRepositoryLocation getRepository() { return new MavenRepositoryLocation(apiToolsRepository.getId(), URI.create(apiToolsRepository.getUrl())); } - - private List setupArguments(Path targetFile) - throws MojoFailureException { - List args = new ArrayList<>(); - args.add("-application"); - args.add("org.eclipse.pde.api.tools.apiAnalyzer"); - args.add("-project"); - args.add(project.getBasedir().getAbsolutePath()); - args.add("-baseline"); - args.add(targetFile.toAbsolutePath().toString()); - args.add("-dependencyList"); - try { - args.add(writeProjectDependencies().toAbsolutePath().toString()); - } catch (Exception e) { - throw new MojoFailureException("Can't write dependencies!", e); - } - args.add("-failOnError"); - return args; - } - - private Path createTargetFile() throws MojoExecutionException, MojoFailureException { + private Collection getBaselineBundles() throws MojoFailureException { long start = System.currentTimeMillis(); Collection baselineBundles; try { Optional artifactKey = projectManager.getArtifactKey(project); getLog().info("Resolve API baseline for " + project.getId()); - baselineBundles = resolver.getApiBaselineBundles(baselines.stream().filter(repo -> repo.getUrl() != null) - .map(repo -> new MavenRepositoryLocation(repo.getId(), URI.create(repo.getUrl()))).toList(), + baselineBundles = resolver.getApiBaselineBundles( + baselines.stream().filter(repo -> repo.getUrl() != null) + .map(repo -> new MavenRepositoryLocation(repo.getId(), URI.create(repo.getUrl()))).toList(), artifactKey.get()); getLog().debug("API baseline contains " + baselineBundles.size() + " bundles (resolve takes " + time(start) + ")."); } catch (IllegalArtifactReferenceException e) { throw new MojoFailureException("Project specify an invalid artifact key", e); } - String list = baselineBundles.stream().map(p -> p.toAbsolutePath().toString()) - .collect(Collectors.joining(System.lineSeparator())); - Path targetFile = Path.of(project.getBuild().getDirectory(), project.getArtifactId() + "-apiBaseline.txt"); - try { - Files.writeString(targetFile, list, StandardCharsets.UTF_8); - } catch (IOException e) { - throw new MojoExecutionException("Writing target file failed!", e); - } - return targetFile; + return baselineBundles; } private String time(long start) { @@ -240,69 +271,54 @@ private String time(long start) { return sec + " s"; } - private Path writeProjectDependencies() throws Exception { - File outputFile = new File(project.getBuild().getDirectory(), "dependencies-list.txt"); - outputFile.getParentFile().mkdirs(); - Set written = new HashSet<>(); + private Collection getProjectDependencies() throws Exception { + Set dependencySet = new HashSet<>(); TychoProject tychoProject = projectManager.getTychoProject(project).get(); - try (BufferedWriter writer = Files.newBufferedWriter(outputFile.toPath())) { - List dependencies = TychoProjectUtils - .getDependencyArtifacts(DefaultReactorProject.adapt(project)).getArtifacts(); - for (ArtifactDescriptor descriptor : dependencies) { - File location = descriptor.fetchArtifact().get(); - if (location.equals(project.getBasedir())) { - continue; - } - ReactorProject reactorProject = descriptor.getMavenProject(); - if (reactorProject == null) { - writeLocation(writer, location, written); - } else { - ReactorProject otherProject = reactorProject; - writeLocation(writer, otherProject.getArtifact(descriptor.getClassifier()), written); - } + List dependencies = TychoProjectUtils + .getDependencyArtifacts(DefaultReactorProject.adapt(project)).getArtifacts(); + for (ArtifactDescriptor descriptor : dependencies) { + File location = descriptor.fetchArtifact().get(); + if (location.equals(project.getBasedir())) { + continue; + } + ReactorProject reactorProject = descriptor.getMavenProject(); + if (reactorProject == null) { + writeLocation(location, dependencySet); + } else { + ReactorProject otherProject = reactorProject; + writeLocation(otherProject.getArtifact(descriptor.getClassifier()), dependencySet); } - if (tychoProject instanceof OsgiBundleProject bundleProject) { - pluginRealmHelper.visitPluginExtensions(project, session, ClasspathContributor.class, cpc -> { - List list = cpc.getAdditionalClasspathEntries(project, Artifact.SCOPE_COMPILE); - if (list != null && !list.isEmpty()) { - for (ClasspathEntry entry : list) { - for (File locations : entry.getLocations()) { - try { - writeLocation(writer, locations, written); - } catch (IOException e) { - // ignore... - } - } + } + if (tychoProject instanceof OsgiBundleProject bundleProject) { + pluginRealmHelper.visitPluginExtensions(project, session, ClasspathContributor.class, cpc -> { + List list = cpc.getAdditionalClasspathEntries(project, Artifact.SCOPE_COMPILE); + if (list != null && !list.isEmpty()) { + for (ClasspathEntry entry : list) { + for (File locations : entry.getLocations()) { + writeLocation(locations, dependencySet); } } - }); - // This is a hack because "org.eclipse.osgi.services" exports the annotation - // package and might then be resolved by Tycho as a dependency, but then PDE - // can't find the annotations here, so we always add this as a dependency - // manually here, once "org.eclipse.osgi.services" is gone we can remove this - // again! - Optional bundle = mavenBundleResolver.resolveMavenBundle(project, session, - "org.osgi", "org.osgi.service.component.annotations", "1.3.0"); - bundle.ifPresent(key -> { - try { - writeLocation(writer, key.getLocation(), written); - } catch (IOException e) { - } - }); - } + } + }); + // This is a hack because "org.eclipse.osgi.services" exports the annotation + // package and might then be resolved by Tycho as a dependency, but then PDE + // can't find the annotations here, so we always add this as a dependency + // manually here, once "org.eclipse.osgi.services" is gone we can remove this + // again! + Optional bundle = mavenBundleResolver.resolveMavenBundle(project, session, "org.osgi", + "org.osgi.service.component.annotations", "1.3.0"); + bundle.ifPresent(key -> { + writeLocation(key.getLocation(), dependencySet); + }); } - return outputFile.toPath(); + return dependencySet; } - private void writeLocation(BufferedWriter writer, File location, Set written) throws IOException { + private void writeLocation(File location, Collection consumer) { if (location == null) { return; } - String path = location.getAbsolutePath(); - if (written.add(path)) { - writer.write(path); - writer.write(System.lineSeparator()); - } + consumer.add(location.getAbsoluteFile().toPath()); } private static final class ApiAppKey { @@ -333,4 +349,11 @@ public boolean equals(Object obj) { } } + + private static Path fileToPath(File file) { + if (file != null) { + return file.toPath(); + } + return null; + } } diff --git a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisResult.java b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisResult.java new file mode 100644 index 0000000000..ebdf11a7f1 --- /dev/null +++ b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiAnalysisResult.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.apitools; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.osgi.service.resolver.ResolverError; +import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem; + +public class ApiAnalysisResult implements Serializable { + + private List problems = new ArrayList<>(); + private List resolveError = new ArrayList<>(); + + public Stream problems() { + return problems.stream(); + } + + public Stream resolveErrors() { + return resolveError.stream(); + } + + public void addProblem(IApiProblem problem) { + problems.add(new ApiProblemDTO(problem)); + } + + public void addResolverError(ResolverError error) { + resolveError.add(new ResolverErrorDTO(error)); + } +} diff --git a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiProblemDTO.java b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiProblemDTO.java new file mode 100644 index 0000000000..21394ea0f1 --- /dev/null +++ b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ApiProblemDTO.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.apitools; + +import java.io.Serializable; + +import org.eclipse.pde.api.tools.internal.provisional.problems.IApiProblem; + +public class ApiProblemDTO implements IApiProblem, Serializable { + + private final int severity; + private final int elementKind; + private final int messageid; + private final String resourcePath; + private final String typeName; + private final String[] messageArguments; + private final int charStart; + private final int charEnd; + private final int lineNumber; + private final int category; + private final int id; + private final String message; + private final int kind; + private final int flags; + private final String toString; + + public ApiProblemDTO(IApiProblem problem) { + severity = problem.getSeverity(); + elementKind = problem.getElementKind(); + messageid = problem.getMessageid(); + resourcePath = problem.getResourcePath(); + typeName = problem.getTypeName(); + messageArguments = problem.getMessageArguments(); + charStart = problem.getCharStart(); + charEnd = problem.getCharEnd(); + lineNumber = problem.getLineNumber(); + category = problem.getCategory(); + id = problem.getId(); + message = problem.getMessage(); + kind = problem.getKind(); + flags = problem.getFlags(); + toString = problem.toString(); + } + + @Override + public int getSeverity() { + return severity; + } + + @Override + public int getElementKind() { + return elementKind; + } + + @Override + public int getMessageid() { + return messageid; + } + + @Override + public String getResourcePath() { + return resourcePath; + } + + @Override + public String getTypeName() { + return typeName; + } + + @Override + public String[] getMessageArguments() { + return messageArguments; + } + + @Override + public int getCharStart() { + return charStart; + } + + @Override + public int getCharEnd() { + return charEnd; + } + + @Override + public int getLineNumber() { + return lineNumber; + } + + @Override + public int getCategory() { + return category; + } + + @Override + public int getId() { + return id; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public int getKind() { + return kind; + } + + @Override + public int getFlags() { + return flags; + } + + @Override + public String[] getExtraMarkerAttributeIds() { + return new String[0]; + } + + @Override + public Object[] getExtraMarkerAttributeValues() { + return new Object[0]; + } + + @Override + public String toString() { + return toString; + } + +} diff --git a/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ResolverErrorDTO.java b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ResolverErrorDTO.java new file mode 100644 index 0000000000..267dddf87e --- /dev/null +++ b/tycho-apitools-plugin/src/main/java/org/eclipse/tycho/apitools/ResolverErrorDTO.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2023 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.tycho.apitools; + +import java.io.Serializable; + +import org.eclipse.osgi.service.resolver.BundleDescription; +import org.eclipse.osgi.service.resolver.ResolverError; +import org.eclipse.osgi.service.resolver.VersionConstraint; + +public class ResolverErrorDTO implements ResolverError, Serializable { + + private final String data; + private final int type; + private final String toString; + + public ResolverErrorDTO(ResolverError error) { + data = error.getData(); + type = error.getType(); + toString = error.toString(); + } + + @Override + public BundleDescription getBundle() { + return null; + } + + @Override + public int getType() { + return type; + } + + @Override + public String getData() { + return data; + } + + @Override + public VersionConstraint getUnsatisfiedConstraint() { + return null; + } + + @Override + public String toString() { + return toString; + } + +} diff --git a/tycho-its/src/test/java/org/eclipse/tycho/test/apitools/ApiToolsTest.java b/tycho-its/src/test/java/org/eclipse/tycho/test/apitools/ApiToolsTest.java index 62d0ca6cab..d142dca7f2 100644 --- a/tycho-its/src/test/java/org/eclipse/tycho/test/apitools/ApiToolsTest.java +++ b/tycho-its/src/test/java/org/eclipse/tycho/test/apitools/ApiToolsTest.java @@ -51,10 +51,13 @@ public void testVerify() throws Exception { assertThrows("No API errors where detected!", VerificationException.class, () -> verifier.executeGoals(List.of("clean", "verify"))); - verifier.verifyTextInLog("1 API ERRORS"); + verifier.verifyTextInLog("4 API ERRORS"); verifier.verifyTextInLog("0 API warnings"); + verifier.verifyTextInLog("The type bundle.ApiInterface has been removed from api-bundle"); verifier.verifyTextInLog("The type bundle.InterfaceA has been removed from api-bundle"); verifier.verifyTextInLog("The type bundle.ClassA has been removed from api-bundle"); + verifier.verifyTextInLog( + "The major version should be incremented in version 0.0.1, since API breakage occurred since version 0.0.1"); // TODO: check with api-filter // TODO: check with second plugin with BREE?