blob: 753c8a89369053ceb74f46859f96793b8ba0de4b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2017 IBM Corporation 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.pde.api.tools.internal.builder;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.builder.ReferenceCollection;
import org.eclipse.jdt.internal.core.builder.State;
import org.eclipse.jdt.internal.core.builder.StringSet;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.api.tools.internal.TypeAnnotations;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.provisional.Factory;
import org.eclipse.pde.api.tools.internal.provisional.IApiAnnotations;
import org.eclipse.pde.api.tools.internal.provisional.IApiMarkerConstants;
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.util.Util;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.osgi.framework.Version;
/**
* Used to incrementally build changed Java types
*
* @since 3.5
*/
public class IncrementalApiBuilder {
private static final int UNKNOWN = 0;
private static final int CLASS_FILE = 1;
private static final int JAVA__FILE = 2;
// bit mask for kinds of changes
private static final int STRUCTURAL = 0x0001;
private static final int DESCRIPTION = 0x0002;
class Change {
int changeKind; // STUCTURAL | DESCRIPTION
int fileKind; // JAVA | CLASS
int deltaKind;
IProject project;
IFile resource;
String typeName;
/**
* Creates a change of the specified kinds.
*
* @param kind bit mask of STUCTURAL | DESCRIPTION
* @param deltaKind resource delta kind
* @param resource file that changed
* @param typeName associated qualified type name
* @param fileKind JAVA_FILE or CLASS_FILE
*/
Change(int kind, int deltaKind, IFile resource, String typeName, int fileKind) {
this.changeKind = kind;
this.deltaKind = deltaKind;
this.resource = resource;
this.project = resource.getProject();
this.typeName = typeName;
this.fileKind = fileKind;
}
/**
* IF the given {@link IProject} is the same as the project context, or
* is contained in the listing of interesting projects
*
* @param lproject
* @param others
* @return <code>true</code> if the project is the same or in the
* interesting list <code>false</code> otherwise
*/
boolean isContained(IProject lproject, HashSet<IProject> others) {
return this.project.equals(lproject) || (others != null && others.contains(this.project));
}
}
/**
* Visits a resource delta to collect changes that need to be built
*/
class ResourceDeltaVisitor implements IResourceDeltaVisitor {
List<Change> changes = new ArrayList<>();
boolean buildpathChanged = false;
/**
* Constructs a new visitor, noting whether the build path of the
* project has changed since the last build.
*
* @param pathChanged
*/
ResourceDeltaVisitor(boolean pathChanged) {
buildpathChanged = pathChanged;
}
@Override
public boolean visit(IResourceDelta delta) throws CoreException {
switch (delta.getResource().getType()) {
case IResource.ROOT:
case IResource.PROJECT:
case IResource.FOLDER: {
return true;
}
case IResource.FILE: {
IFile resource = (IFile) delta.getResource();
String fileName = resource.getName();
if (Util.isClassFile(fileName)) {
if (delta.getKind() == IResourceDelta.REMOVED) {
String typename = resolveTypeName(resource, CLASS_FILE);
if (typename != null) {
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Found removed class file " + typename); //$NON-NLS-1$
}
changes.add(new Change(STRUCTURAL, delta.getKind(), resource, typename, CLASS_FILE));
}
} else if (buildpathChanged && delta.getKind() == IResourceDelta.CHANGED) {
if ((delta.getFlags() & IResourceDelta.CONTENT) > 0) {
// re-building due to build path (class path
// changes, project add/remove, target platform
// changes)
// TODO: for now use structural change, but
// really, we just need to re-analyze API use in
// case of
// required bundle API description changes
String typename = resolveTypeName(resource, CLASS_FILE);
if (typename != null) {
changes.add(new Change(STRUCTURAL, delta.getKind(), resource, typename, CLASS_FILE));
}
}
}
} else if (Util.isJavaFileName(fileName)) {
String type = resolveTypeName(resource, JAVA__FILE);
if (type != null) {
Change change = new Change(STRUCTURAL, delta.getKind(), resource, type, JAVA__FILE);
changes.add(change);
// check if description has changed
IApiComponent component = workspaceBaseline.getApiComponent(resource.getProject());
if (component != null) {
try {
IApiAnnotations annotations = component.getApiDescription().resolveAnnotations(Factory.typeDescriptor(type.replace('/', '.')));
if (annotations instanceof TypeAnnotations) {
TypeAnnotations ta = (TypeAnnotations) annotations;
if (ta.getBuildStamp() == BuildStamps.getBuildStamp(resource.getProject())) {
// note description change in
// addition to structure
change.changeKind |= DESCRIPTION;
}
}
} catch (CoreException e) {
ApiPlugin.log(e);
}
}
}
}
break;
}
default:
break;
}
return false;
}
}
ApiAnalysisBuilder builder = null;
BuildContext context = null;
IApiBaseline workspaceBaseline = null;
/**
* Constructor
*
* @param project the current project context being built
* @param delta the {@link IResourceDelta} from the build framework
* @param buildstate the current build state from the
* {@link org.eclipse.jdt.internal.core.builder.JavaBuilder}
*/
public IncrementalApiBuilder(ApiAnalysisBuilder builder) {
this.builder = builder;
}
/**
* Incrementally builds using the
* {@link org.eclipse.pde.api.tools.internal.provisional.builder.IApiAnalyzer}
* from the given {@link ApiAnalysisBuilder}
*
* @param baseline the baseline to compare with
* @param wbaseline the workspace baseline
* @param deltas the deltas to be built
* @param state the current JDT build state
* @param buildstate the current API tools build state
* @param monitor
* @throws CoreException
*/
public void build(IApiBaseline baseline, IApiBaseline wbaseline, IResourceDelta[] deltas, State state, BuildState buildstate, IProgressMonitor monitor) throws CoreException {
IProject project = this.builder.getProject();
this.workspaceBaseline = wbaseline;
SubMonitor localmonitor = SubMonitor.convert(monitor, NLS.bind(BuilderMessages.IncrementalBuilder_builder_for_project, project.getName()), 1);
this.context = new BuildContext();
try {
String[] projectNames = buildstate.getReexportedComponents();
HashSet<IProject> depprojects = null;
if (projectNames.length != 0) {
depprojects = new HashSet<>();
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IProject pj = null;
for (String projectName : projectNames) {
pj = root.getProject(projectName);
if (pj.isAccessible()) {
// select only projects that don't exist in the
// reference baseline
if (baseline != null && baseline.getApiComponent(projectName) == null) {
depprojects.add(pj);
}
}
}
}
// check if the build path has changed
long prev = buildstate.getBuildPathCRC();
long curr = BuildState.computeBuildPathCRC(project);
ResourceDeltaVisitor visitor = new ResourceDeltaVisitor(curr != prev);
for (IResourceDelta delta : deltas) {
delta.accept(visitor);
}
buildContext(project, state, visitor.changes, depprojects);
build(project, baseline, wbaseline, state, buildstate, localmonitor.split(1));
} catch (OperationCanceledException oce) {
// do nothing, but don't forward it
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=304315
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: Trapped OperationCanceledException"); //$NON-NLS-1$
}
} finally {
this.context.dispose();
}
}
/**
* Builds an API delta using the default baseline (from the workspace
* settings and the current
*
* @param project
* @param baseline the baseline to compare to
* @param wbaseline the current workspace baseline
* @param state the current JDT build state
* @param buildstate the current API tools build state
* @param monitor
*/
void build(final IProject project, final IApiBaseline baseline, final IApiBaseline wbaseline, final State state, BuildState buildstate, IProgressMonitor monitor) {
SubMonitor localmonitor = SubMonitor.convert(monitor, BuilderMessages.api_analysis_on_0, 3);
localmonitor.subTask(NLS.bind(BuilderMessages.ApiAnalysisBuilder_finding_affected_source_files, project.getName()));
if (this.context.hasTypes()) {
IPluginModelBase currentModel = this.builder.getCurrentModel();
if (currentModel != null) {
String id = currentModel.getBundleDescription().getSymbolicName();
IApiComponent comp = wbaseline.getApiComponent(id);
Set<IApiComponent> apiComponentMultiple = wbaseline.getAllApiComponents(id);
if (!apiComponentMultiple.isEmpty()) {
// add the exact match
for (Iterator<IApiComponent> iterator = apiComponentMultiple.iterator(); iterator.hasNext();) {
IApiComponent iApiComponent = iterator.next();
Version workspaceBaselineVersion = new Version(iApiComponent.getVersion());// removes
// qualifier
Version currentProjectVersion = currentModel.getBundleDescription().getVersion();
if (new Version(currentProjectVersion.getMajor(), currentProjectVersion.getMinor(), currentProjectVersion.getMicro()).compareTo(workspaceBaselineVersion) == 0) {
comp = iApiComponent;
break;
}
}
}
if (comp == null) {
return;
}
extClean(project, buildstate, localmonitor.split(1));
this.builder.getAnalyzer().analyzeComponent(buildstate, null, null, baseline, comp, this.context, localmonitor.split(1));
localmonitor.split(1);
this.builder.createMarkers();
}
}
}
/**
* Records the type name from the given IFile as a dependent type in the
* given build context
*
* @param file
* @param kind mask of STRUCTURAL and/or DESCRIPTION
*/
private void addDependentTypeToContext(IFile file, int kind) {
String type = resolveTypeName(file, JAVA__FILE);
if (type == null) {
return;
}
if ((STRUCTURAL & kind) > 0) {
if (!this.context.containsStructuralChange(type)) {
this.builder.cleanupCompatibilityMarkers(file);
}
}
if ((DESCRIPTION & kind) > 0) {
if (!this.context.containsDescriptionChange(type) && !this.context.containsDescriptionDependent(type)) {
this.builder.cleanupUsageMarkers(file);
this.builder.cleanUnusedFilterMarkers(file);
this.context.recordDescriptionDependent(type);
}
}
addInnerTypesToDependents(file, kind);
}
/**
* Collects the inner types from the compilation unit
*
* @param file
* @param mask of STRUCTURAL and/or DESCRIPTION
*/
private void addInnerTypesToDependents(IFile file, int kind) {
ICompilationUnit unit = (ICompilationUnit) JavaCore.create(file);
IType[] types = null;
try {
types = unit.getAllTypes();
String typename = null;
for (IType type : types) {
typename = type.getFullyQualifiedName('$');
if ((DESCRIPTION & kind) > 0) {
if (!this.context.containsDescriptionChange(typename) && !this.context.containsDescriptionDependent(typename)) {
this.context.recordDescriptionDependent(typename);
}
}
}
} catch (JavaModelException jme) {
// do nothing, just don't consider types
}
}
/**
* Collects the inner types from the compilation unit
*
* @param file
* @param mask of STRUCTURAL and/or DESCRIPTION
*/
private void addInnerTypes(IFile file, int kind) {
ICompilationUnit unit = (ICompilationUnit) JavaCore.create(file);
IType[] types = null;
try {
types = unit.getAllTypes();
String typename = null;
for (IType type : types) {
typename = type.getFullyQualifiedName('$');
if ((STRUCTURAL & kind) > 0) {
if (!this.context.containsStructuralChange(typename)) {
this.context.recordStructuralChange(typename);
}
}
if ((DESCRIPTION & kind) > 0) {
if (!this.context.containsDescriptionChange(typename)) {
this.context.recordDescriptionChanged(typename);
}
}
}
} catch (JavaModelException jme) {
// do nothing, just don't consider types
}
}
/**
* Constructs a build context based on the current JDT build state and known
* changes.
*
* @param project the current project being built
* @param state the current JDT build state
* @param list of changes
*/
void buildContext(final IProject project, State state, List<Change> changes, HashSet<IProject> depprojects) {
StringSet structural = null;
StringSet description = null;
for (Change change : changes) {
boolean contained = change.isContained(project, depprojects);
if ((change.changeKind & STRUCTURAL) > 0) {
// don't analyze dependents of removed types
if (change.deltaKind != IResourceDelta.REMOVED) {
if (structural == null) {
structural = new StringSet(16);
}
structural.add(change.typeName);
}
// only add to structural types if contained in the project
// being built
if (contained) {
context.recordStructuralChange(change.typeName);
if (change.deltaKind == IResourceDelta.REMOVED) {
context.recordRemovedType(change.typeName);
}
}
}
if ((change.changeKind & DESCRIPTION) > 0) {
if (description == null) {
description = new StringSet(16);
}
description.add(change.typeName);
// only add to description changes if contained in the project
// being built
if (contained) {
context.recordDescriptionChanged(change.typeName);
}
}
if (contained) {
if (change.fileKind == JAVA__FILE) {
this.builder.cleanupMarkers(change.resource);
addInnerTypes(change.resource, change.changeKind);
} else {
// look up the source file
String path = state.typeLocators.get(change.typeName);
if (path != null) {
IResource member = this.builder.getProject().findMember(path);
if (member != null && member.getType() == IResource.FILE) {
IFile source = (IFile) member;
this.builder.cleanupMarkers(source);
addInnerTypes(source, change.changeKind);
}
}
}
}
}
// only resolve dependents once for case of 1 type changed and is both
// structural and description
if (changes.size() == 1 && structural != null && description != null) {
String[] types = structural.values;
if (types.length > 0) {
addDependents(project, state, types, STRUCTURAL | DESCRIPTION);
}
} else {
if (structural != null) {
String[] types = structural.values;
if (types.length > 0) {
addDependents(project, state, types, STRUCTURAL);
}
}
if (description != null) {
String[] types = description.values;
if (types.length > 0) {
addDependents(project, state, types, DESCRIPTION);
}
}
}
}
/**
* Adds the dependent files from the current build context based on the
* current JDT build state to either the structural or description
* dependents.
*
* @param project the current project being built
* @param state the current JDT build state
* @param types dot and $ qualified names of base types that changed
* @param kind mask of STRUCTURAL or DESCRIPTION
*/
private void addDependents(final IProject project, State state, String[] types, int kind) {
StringSet packages = new StringSet(16);
StringSet typenames = new StringSet(16);
for (String type : types) {
if (type != null) {
splitName(type, packages, typenames);
}
}
// the qualifiedStrings are of the form 'p1/p2' & the simpleStrings are
// just 'X'
char[][][] internedQualifiedNames = ReferenceCollection.internQualifiedNames(packages);
// if a well known qualified name was found then we can skip over these
if (internedQualifiedNames.length < packages.elementSize) {
internedQualifiedNames = null;
}
char[][] internedSimpleNames = ReferenceCollection.internSimpleNames(typenames, true);
// if a well known name was found then we can skip over these
if (internedSimpleNames.length < typenames.elementSize) {
internedSimpleNames = null;
}
Set<Entry<String, ReferenceCollection>> entrySet = state.getReferences().entrySet();
for (Entry<String, ReferenceCollection> entry : entrySet) {
String typeLocator = entry.getKey();
ReferenceCollection refs = entry.getValue();
if (refs.includes(internedQualifiedNames, internedSimpleNames, null)) {
IFile file = project.getFile(typeLocator);
if (file == null) {
continue;
}
if (ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: adding affected source file " + file.getName()); //$NON-NLS-1$
}
addDependentTypeToContext(file, kind);
}
}
}
/**
* Adds a type to search for dependents of in considered projects for an
* incremental build
*
* @param path
*/
void splitName(String typename, StringSet packages, StringSet simpleTypes) {
// the qualifiedStrings are of the form 'p1/p2' & the simpleStrings are
// just 'X'
int idx = typename.lastIndexOf('/');
String packageName = (idx < 0 ? Util.EMPTY_STRING : typename.substring(0, idx));
String typeName = (idx < 0 ? typename : typename.substring(idx + 1, typename.length()));
idx = typeName.indexOf('$');
if (idx > 0) {
typeName = typeName.substring(0, idx);
}
if (simpleTypes.add(typeName) && packages.add(packageName) && ApiPlugin.DEBUG_BUILDER) {
System.out.println("ApiAnalysisBuilder: will look for dependents of " + typeName + " in " + packageName); //$NON-NLS-1$ //$NON-NLS-2$
}
}
/**
* Returns an array of type names, and cleans up markers for the specified
* resource
*
* @param project the project being built
* @param state the current build state for the given project
* @param monitor
*/
void extClean(final IProject project, BuildState state, IProgressMonitor monitor) {
SubMonitor subMonitor = SubMonitor.convert(monitor, 3);
// clean up the state -
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=271110
String[] types = this.context.getRemovedTypes();
for (String type : types) {
state.cleanup(type);
}
subMonitor.split(1);
IResource resource = project.findMember(ApiAnalysisBuilder.MANIFEST_PATH);
if (resource != null) {
try {
// TODO we should find a way to cache markers to type names,
// that way to get all
// the manifest markers for a given type name is time of O(1)
IMarker[] markers = resource.findMarkers(IApiMarkerConstants.COMPATIBILITY_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
String tname = null;
for (IMarker marker : markers) {
tname = Util.getTypeNameFromMarker(marker);
if (this.context.containsStructuralChange(tname)) {
marker.delete();
}
}
subMonitor.split(1);
// TODO we should find a way to cache markers to type names,
// that way to get all
// the manifest markers for a given type name is time of O(1)
markers = resource.findMarkers(IApiMarkerConstants.UNUSED_FILTER_PROBLEM_MARKER, false, IResource.DEPTH_ZERO);
for (IMarker marker : markers) {
tname = Util.getTypeNameFromMarker(marker);
if (this.context.containsStructuralChange(tname)) {
marker.delete();
}
}
subMonitor.split(1);
} catch (CoreException e) {
ApiPlugin.log(e);
}
}
}
/**
* Resolves the java path from the given resource
*
* @param resource
* @param kind CLASS_FILE, JAVA_FILE, or UNKNOWN
* @return the resolved path or <code>null</code> if the resource is not
* part of the java model
*/
String resolveTypeName(IResource resource, int kind) {
IPath typepath = resource.getFullPath();
int type = kind;
if (kind == UNKNOWN) {
if (Util.isClassFile(resource.getName())) {
type = CLASS_FILE;
} else if (Util.isJavaFileName(resource.getName())) {
type = JAVA__FILE;
}
}
HashSet<IPath> paths = null;
switch (type) {
case JAVA__FILE:
paths = this.builder.src_locs.get(resource.getProject());
break;
case CLASS_FILE:
paths = this.builder.output_locs.get(resource.getProject());
break;
default:
break;
}
if (paths != null) {
for (IPath path : paths) {
if (path.isPrefixOf(typepath)) {
typepath = typepath.removeFirstSegments(path.segmentCount()).removeFileExtension();
return typepath.toString();
}
}
}
return null;
}
}