blob: 89c6c662847c7202fe50f4b1a0967f3ff6fde198 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2013 Richard Hoefter 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:
* Richard Hoefter (richard.hoefter@web.de) - initial API and implementation, bug 313386
* IBM Corporation - incorporating into Eclipse
*******************************************************************************/
package org.eclipse.ant.internal.ui.datatransfer;
import java.io.File;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.eclipse.core.resources.IFile;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.util.IClassFileReader;
import org.eclipse.jdt.core.util.IConstantPool;
import org.eclipse.jdt.core.util.IConstantPoolConstant;
import org.eclipse.jdt.core.util.IConstantPoolEntry;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Shell;
/**
* Provides a method to analyze sources if it is possible to export projects to an Ant buildfile which compiles correctly.
*/
public class SourceAnalyzer {
/**
* Utility class.
*/
private SourceAnalyzer() {
}
/**
* Check if source directories of project has cycles or if there are dependencies between them that are not conform with classpath order.
*
* <p>
* NOTE: Unused references in classes are not considered if they cause cycles or classpath order problems. This is because this class analyzes the
* bytecode and indeed the compiler throws unused references away.
*/
public static void checkCycles(IJavaProject currentProject, EclipseClasspath classpath, Shell shell) {
StringBuffer message = new StringBuffer();
Map<String, String> src2dir = new TreeMap<>(); // map string to string
Map<String, Set<String>> srcdir2classes = new TreeMap<>(); // map string to Set of strings
determineSources(currentProject, classpath, src2dir, srcdir2classes);
Map<String, Set<String>> srcdir2sourcedirs = determineRequiredSrcDirs(src2dir, srcdir2classes);
String projectName = currentProject.getProject().getName();
List<String> cycle = new ArrayList<>();
if (isCyclic(srcdir2sourcedirs, cycle)) {
showCycleWarning(projectName, shell, cycle, message);
return;
}
checkBuildOrder(classpath, projectName, shell, srcdir2sourcedirs);
}
/**
* Determine all sources belonging to a source directory.
*/
private static void determineSources(IJavaProject currentProject, EclipseClasspath classpath, Map<String, String> src2dir, Map<String, Set<String>> srcdir2classes) {
for (int i = 0; i < classpath.srcDirs.size(); i++) {
String srcDir = classpath.srcDirs.get(i);
String classDir = classpath.classDirs.get(i);
if (EclipseClasspath.isReference(srcDir)) {
continue;
}
File dir;
if (srcDir.equals(".")) { //$NON-NLS-1$
dir = currentProject.getResource().getLocation().toFile();
} else {
IFile file = currentProject.getProject().getFile(srcDir);
dir = file.getLocation().toFile();
}
if (EclipseClasspath.isLinkedResource(srcDir)) {
String link = classpath.resolveLinkedResource(srcDir);
dir = new File(link);
if (!dir.isAbsolute()) {
// make absolute
dir = new File(ExportUtil.getProjectRoot(currentProject), link);
}
}
Set<String> sources = findFiles(dir, ".java"); //$NON-NLS-1$
// find all required classfiles for each source directory
for (Iterator<String> iter = sources.iterator(); iter.hasNext();) {
String srcFile = iter.next();
src2dir.put(srcFile, srcDir);
IFile classFile = currentProject.getProject().getFile(classDir + '/' + srcFile + ".class"); //$NON-NLS-1$
if (!classFile.exists()) {
// project was not compiled, check not possible
continue;
}
Set<String> classes = srcdir2classes.get(srcDir);
if (classes == null) {
classes = new TreeSet<>();
}
classes.addAll(getRequiredClasses(classFile));
srcdir2classes.put(srcDir, classes);
}
}
}
/**
* Determine for each source directory which other source directories it requires.
*
* @return Map string to Set of strings. (Maps source dir to Set of required source dirs.)
*/
private static Map<String, Set<String>> determineRequiredSrcDirs(Map<String, String> src2dir, Map<String, Set<String>> srcdir2classes) {
Map<String, Set<String>> srcdir2sourcedirs = new TreeMap<>(); // map string to Set of strings
for (Iterator<String> iter = srcdir2classes.keySet().iterator(); iter.hasNext();) {
String srcDir = iter.next();
Set<String> classes = srcdir2classes.get(srcDir);
for (Iterator<String> iterator = classes.iterator(); iterator.hasNext();) {
String classname = iterator.next();
String classsrc = src2dir.get(classname);
// don't add reference to itself
if (classsrc != null && !classsrc.equals(srcDir)) {
Set<String> sourcedirs = srcdir2sourcedirs.get(srcDir);
if (sourcedirs == null) {
sourcedirs = new TreeSet<>();
}
sourcedirs.add(classsrc);
srcdir2sourcedirs.put(srcDir, sourcedirs);
}
}
}
return srcdir2sourcedirs;
}
private static void showCycleWarning(String projectName, Shell shell, List<String> cycle, StringBuffer message) {
String m = MessageFormat.format(DataTransferMessages.SourceAnalyzer_0, new Object[] { projectName });
message.append(m);
message.append(ExportUtil.NEWLINE);
// print cycle path
for (String s : cycle) {
s = EclipseClasspath.getLinkedResourceName(s);
message.append(s);
message.append(" -> "); //$NON-NLS-1$
}
message.append(EclipseClasspath.getLinkedResourceName(cycle.get(0)));
MessageDialog.openWarning(shell, DataTransferMessages.SourceAnalyzer_1, message.toString());
}
/**
* Check if build order is correct.
*/
private static void checkBuildOrder(EclipseClasspath classpath, String projectName, Shell shell, Map<String, Set<String>> srcdir2sourcedirs) {
for (Iterator<String> iter = srcdir2sourcedirs.keySet().iterator(); iter.hasNext();) {
String srcdir = iter.next();
Set<String> sourcedirs = srcdir2sourcedirs.get(srcdir);
int classpathIndex = classpath.srcDirs.indexOf(srcdir);
for (Iterator<String> iterator = sourcedirs.iterator(); iterator.hasNext();) {
String requiredSrc = iterator.next();
int i = classpath.srcDirs.indexOf(requiredSrc);
if (i > classpathIndex) {
String s = MessageFormat.format(DataTransferMessages.SourceAnalyzer_3, new Object[] { projectName });
MessageDialog.openWarning(shell, DataTransferMessages.SourceAnalyzer_2, s + ExportUtil.NEWLINE + requiredSrc + " <-> " + srcdir //$NON-NLS-1$
+ ExportUtil.NEWLINE);
break;
}
}
}
}
/**
* Find all classes that are required by given class file.
*
* @param file
* a ".class" file
* @return set of strings, each contains a full qualified class name (forward slash as package separator)
*/
public static Set<String> getRequiredClasses(IFile file) {
Set<String> classes = new TreeSet<>();
IClassFile classFile = JavaCore.createClassFileFrom(file);
IClassFileReader reader = ToolFactory.createDefaultClassFileReader(classFile, IClassFileReader.CONSTANT_POOL);
if (reader == null) {
// class not compiled
return classes;
}
IConstantPool pool = reader.getConstantPool();
for (int i = 0; i < pool.getConstantPoolCount(); i++) {
if (pool.getEntryKind(i) == IConstantPoolConstant.CONSTANT_Class) {
IConstantPoolEntry entry = pool.decodeEntry(i);
String classname = new String(entry.getClassInfoName());
// don't return inner classes
int index = classname.indexOf('$');
if (index != -1) {
classname = classname.substring(0, index);
}
classes.add(classname);
}
}
return classes;
}
/**
* Find all files with particular extension under given directory.
*
* @param dir
* directory to start search
* @param extension
* extension to search
* @return filenames relative to directory (without extension and with forward slashes)
*/
public static Set<String> findFiles(File dir, String extension) {
Set<String> visited = new TreeSet<>();
findFiles(dir, dir, extension, visited);
return visited;
}
private static void findFiles(File base, File dir, String extension, Set<String> visited) {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
for (int i = 0; i < children.length; i++) {
findFiles(base, children[i], extension, visited);
}
} else if (dir.getAbsolutePath().endsWith(extension)) {
// remove base directory
String filename = ExportUtil.removePrefixAndSuffix(dir.getAbsolutePath(), base.getAbsolutePath() + File.separator, extension);
visited.add(filename.replace('\\', '/'));
}
}
/**
* Check if given graph that is described through a map is cyclic.
*
* @param srcdir2sourcedirs
* Maps string to set of strings. The keys are the graph nodes which are mapped to its neighbors.
* @param cycle
* filled with name of nodes which cause cycle
*/
private static boolean isCyclic(Map<String, Set<String>> srcdir2sourcedirs, List<String> cycle) {
return !isAcyclic(srcdir2sourcedirs, cycle);
}
private static boolean isAcyclic(Map<String, Set<String>> srcdir2sourcedirs, List<String> cycle) {
// standard graph theory
List<String> visited = new ArrayList<>();
List<String> exited = new ArrayList<>();
for (Iterator<String> iter = srcdir2sourcedirs.keySet().iterator(); iter.hasNext();) {
String srcdir = iter.next();
if (!visited.contains(srcdir)) {
if (circleSearch(srcdir, srcdir2sourcedirs, visited, exited, cycle)) {
return false;
}
}
}
return true;
}
private static boolean circleSearch(String srcdir, Map<String, Set<String>> srcdir2sourcedirs, List<String> visited, List<String> exited, List<String> cycle) {
boolean res = false;
visited.add(srcdir);
cycle.add(srcdir);
Set<String> sourcedirs = srcdir2sourcedirs.get(srcdir);
if (sourcedirs != null) {
for (Iterator<String> iter = sourcedirs.iterator(); iter.hasNext();) {
String src = iter.next();
if (!visited.contains(src)) {
res = circleSearch(src, srcdir2sourcedirs, visited, exited, cycle);
} else if (!exited.contains(src)) {
res = true;
}
if (res) {
break;
}
}
}
if (!res) {
cycle.clear();
}
exited.add(srcdir);
return res;
}
}