blob: 18e8541a583ff2aaf7bfd04995ab2a2511d6bb26 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2014 Google, Inc and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Sergey Prigogin (Google) - initial API and implementation
*******************************************************************************/
package org.eclipse.cdt.internal.ui.refactoring.includes;
import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.index.IIndexFile;
import org.eclipse.cdt.core.index.IIndexInclude;
import org.eclipse.cdt.core.index.IndexLocationFactory;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.PreferenceConstants;
import org.eclipse.cdt.internal.core.resources.ResourceLookup;
import org.eclipse.cdt.internal.corext.codemanipulation.IncludeInfo;
public class HeaderSubstitutor {
private final IncludeCreationContext fContext;
private IncludeMap[] fIncludeMaps;
private SymbolExportMap fSymbolExportMap;
public HeaderSubstitutor(IncludeCreationContext context) {
fContext = context;
fIncludeMaps = new IncludeMap[] { new IncludeMap(true), new IncludeMap(false) };
IPreferencesService preferences = Platform.getPreferencesService();
IScopeContext[] scopes = PreferenceConstants.getPreferenceScopes(context.getProject());
String str = preferences.getString(CUIPlugin.PLUGIN_ID,
PreferenceConstants.INCLUDES_HEADER_SUBSTITUTION, null, scopes);
if (str != null) {
List<HeaderSubstitutionMap> maps = HeaderSubstitutionMap.deserializeMaps(str);
for (HeaderSubstitutionMap map : maps) {
if (!map.isCppOnly() || fContext.isCXXLanguage()) {
fIncludeMaps[0].addAllMappings(map.getUnconditionalSubstitutionMap());
fIncludeMaps[1].addAllMappings(map.getOptionalSubstitutionMap());
}
}
}
addHeaderDerivedMappings();
fIncludeMaps[0].transitivelyClose();
fIncludeMaps[1].transitivelyClose();
fSymbolExportMap = new SymbolExportMap();
str = preferences.getString(CUIPlugin.PLUGIN_ID,
PreferenceConstants.INCLUDES_SYMBOL_EXPORTING_HEADERS, null, scopes);
if (str != null) {
List<SymbolExportMap> maps = SymbolExportMap.deserializeMaps(str);
for (SymbolExportMap map : maps) {
fSymbolExportMap.addAllMappings(map);
}
}
}
private void addHeaderDerivedMappings() {
try {
for (IIndexFile file : fContext.getIndex().getAllFiles()) {
String replacement = file.getReplacementHeader();
if (replacement != null) {
IPath path = IndexLocationFactory.getAbsolutePath(file.getLocation());
if (fContext.getCurrentDirectory().isPrefixOf(path)) {
// "IWYU pragma: private" does not affect inclusion from files under
// the directory where the header is located.
continue;
}
IncludeInfo includeInfo = fContext.getIncludeForHeaderFile(path);
if (includeInfo == null)
continue;
if (replacement.isEmpty()) {
IIndexInclude[] includedBy = fContext.getIndex().findIncludedBy(file, IIndex.DEPTH_ZERO);
for (IIndexInclude include : includedBy) {
IPath includer = IndexLocationFactory.getAbsolutePath(include.getIncludedByLocation());
IncludeInfo replacementInfo = fContext.getIncludeForHeaderFile(includer);
if (replacementInfo != null) {
fIncludeMaps[0].addMapping(includeInfo, replacementInfo);
}
}
} else {
String[] headers = replacement.split(","); //$NON-NLS-1$
for (String header : headers) {
if (!header.isEmpty()) {
char firstChar = header.charAt(0);
IncludeInfo replacementInfo;
if (firstChar == '"' || firstChar == '<') {
replacementInfo = new IncludeInfo(header);
} else {
replacementInfo = new IncludeInfo(header, includeInfo.isSystem());
}
fIncludeMaps[0].addMapping(includeInfo, replacementInfo);
}
}
}
}
}
} catch (CoreException e) {
CUIPlugin.log(e);
}
}
/**
* Selects the header file to be used in an {@code #include} statement given the header file
* that needs to be included. Returns absolute path of the header if it can be uniquely
* determined, or {@code null} otherwise. The header is determined uniquely if there are no
* optional replacement headers for it, and there is no more that one unconditional replacement.
*
* @param path absolute path of the header to be included directly or indirectly
* @return absolute path of the header to be included directly
*/
public IPath getUniqueRepresentativeHeader(IPath path) {
IncludeInfo includeInfo = fContext.getIncludeForHeaderFile(path);
if (includeInfo == null)
return null;
IncludeMap[] maps = fIncludeMaps;
for (IncludeMap map : maps) {
if (map.isUnconditionalSubstitution()) {
List<IncludeInfo> replacements = map.getMapping(includeInfo);
if (replacements.size() == 1) {
includeInfo = replacements.get(0);
} else if (replacements.size() > 1) {
return null;
}
}
}
for (IncludeMap map : maps) {
if (!map.isUnconditionalSubstitution()) {
if (!map.getMapping(includeInfo).isEmpty()) {
return null;
}
}
}
return fContext.resolveInclude(includeInfo);
}
public IPath getPreferredRepresentativeHeader(IPath path) {
IncludeInfo includeInfo = fContext.getIncludeForHeaderFile(path);
if (includeInfo == null)
return path;
List<IncludeInfo> candidates = new ArrayList<>();
candidates.add(includeInfo);
IncludeMap[] maps = fIncludeMaps;
for (IncludeMap map : maps) {
for (int i = 0; i < candidates.size();) {
IncludeInfo candidate = candidates.get(i);
List<IncludeInfo> replacements = map.getMapping(candidate);
int increment = 1;
if (!replacements.isEmpty()) {
if (map.isUnconditionalSubstitution()) {
candidates.remove(i);
increment = 0;
}
candidates.addAll(i, replacements);
increment += replacements.size();
}
i += increment;
}
}
IPath firstResolved = null;
IPath firstIncludedPreviously = null;
for (IncludeInfo candidate : candidates) {
IPath header = fContext.resolveInclude(candidate);
if (header != null) {
if (fContext.isIncluded(header))
return header;
if (firstResolved == null)
firstResolved = header;
if (firstIncludedPreviously == null && fContext.wasIncludedPreviously(header))
firstIncludedPreviously = header;
}
}
return firstIncludedPreviously != null ?
firstIncludedPreviously : firstResolved != null ? firstResolved : path;
}
/**
* Performs heuristic header substitution.
*/
public IPath getPreferredRepresentativeHeaderByHeuristic(InclusionRequest request) {
Set<IIndexFile> indexFiles = request.getDeclaringFiles().keySet();
String symbolName = request.getBinding().getName();
ArrayDeque<IIndexFile> front = new ArrayDeque<>();
HashSet<IIndexFile> processed = new HashSet<>();
IIndexFile bestCandidate = null;
IIndexFile candidateWithoutExtension = null;
IIndexFile candidateWithMatchingName = null;
try {
// Look for headers matching by name and headers without an extension.
if (fContext.isCXXLanguage()) {
front.addAll(indexFiles);
processed.addAll(indexFiles);
while (!front.isEmpty()) {
IIndexFile file = front.remove();
String path = IncludeUtil.getPath(file);
if (getFilename(path).equalsIgnoreCase(symbolName)) {
if (!hasExtension(path)) {
// A C++ header without an extension and with a name which matches the name
// of the symbol that should be declared is a perfect candidate for inclusion.
bestCandidate = file;
break;
}
if (candidateWithMatchingName == null)
candidateWithMatchingName = file;
} else if (!hasExtension(path)) {
if (candidateWithoutExtension == null)
candidateWithoutExtension = file;
}
// Process the next level of the include hierarchy.
IIndexInclude[] includes = fContext.getIndex().findIncludedBy(file, 0);
for (IIndexInclude include : includes) {
IIndexFile includer = include.getIncludedBy();
if (!processed.contains(includer)) {
front.add(includer);
processed.add(includer);
}
}
}
}
if (bestCandidate == null) {
bestCandidate = candidateWithoutExtension;
}
if (bestCandidate == null) {
bestCandidate = candidateWithMatchingName;
}
if (bestCandidate == null) {
// Repeat inclusion tree search, this time looking for any header included by a source file.
front.clear();
front.addAll(indexFiles);
processed.clear();
processed.addAll(indexFiles);
while (!front.isEmpty()) {
IIndexFile file = front.remove();
// Process the next level of the include hierarchy.
IIndexInclude[] includes = fContext.getIndex().findIncludedBy(file, 0);
for (IIndexInclude include : includes) {
IIndexFile includer = include.getIncludedBy();
if (!processed.contains(includer)) {
URI uri = includer.getLocation().getURI();
if (IncludeUtil.isSource(includer, fContext.getProject()) || isWorkspaceFile(uri)) {
bestCandidate = file;
break;
}
front.add(includer);
processed.add(includer);
}
}
}
}
if (bestCandidate != null)
return IndexLocationFactory.getAbsolutePath(bestCandidate.getLocation());
} catch (CoreException e) {
CUIPlugin.log(e);
}
return request.getCandidatePaths().iterator().next();
}
/**
* Returns the set of headers exporting the given symbol.
*/
public Set<IncludeInfo> getExportingHeaders(String symbol) {
Set<IncludeInfo> headers = fSymbolExportMap.getMapping(symbol);
if (headers == null)
return Collections.emptySet();
return headers;
}
/**
* Returns whether the given URI points within the workspace.
*/
private static boolean isWorkspaceFile(URI uri) {
for (IFile file : ResourceLookup.findFilesForLocationURI(uri)) {
if (file.exists()) {
return true;
}
}
return false;
}
/**
* Returns whether the given path has a file suffix, or not.
*/
private static boolean hasExtension(String path) {
return path.indexOf('.', path.lastIndexOf('/') + 1) >= 0;
}
/**
* Returns the filename of the given path, without extension.
*/
private static String getFilename(String path) {
int startPos = path.lastIndexOf('/') + 1;
int endPos = path.lastIndexOf('.');
if (endPos > startPos) {
return path.substring(startPos, endPos);
} else {
return path.substring(startPos);
}
}
}