blob: 0576eb01a0d9eb068f5a5247fa67ae004478b4b8 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2014 Andrew Gvozdev 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:
* Andrew Gvozdev - initial API and implementation
*******************************************************************************/
package org.eclipse.cdt.managedbuilder.language.settings.providers;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.EFSExtensionProvider;
import org.eclipse.cdt.core.ErrorParserManager;
import org.eclipse.cdt.core.cdtvariables.CdtVariableException;
import org.eclipse.cdt.core.cdtvariables.ICdtVariableManager;
import org.eclipse.cdt.core.language.settings.providers.ICBuildOutputParser;
import org.eclipse.cdt.core.language.settings.providers.IWorkingDirectoryTracker;
import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsManager;
import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsSerializableProvider;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry;
import org.eclipse.cdt.core.settings.model.ICSettingEntry;
import org.eclipse.cdt.core.settings.model.util.CDataUtil;
import org.eclipse.cdt.internal.core.LRUCache;
import org.eclipse.cdt.internal.core.XmlUtil;
import org.eclipse.cdt.managedbuilder.core.ManagedBuilderCorePlugin;
import org.eclipse.cdt.utils.EFSExtensionManager;
import org.eclipse.cdt.utils.cdtvariables.CdtVariableResolver;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
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.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.content.IContentTypeManager;
import org.w3c.dom.Element;
/**
* Abstract class for language settings providers capable to parse build output.
* <p>
* <strong>EXPERIMENTAL</strong>. This class interface is not stable yet as
* it is not currently (CDT 8.1, Juno) clear how it may need to be used in future.
* There is no guarantee that this API will work or that it will remain the same.
* Please do not use this API without consulting with the CDT team.
* </p>
* @noextend This class is not intended to be subclassed by clients.
*
* @since 8.1
*/
public abstract class AbstractLanguageSettingsOutputScanner extends LanguageSettingsSerializableProvider
implements ICBuildOutputParser {
protected static final String ATTR_KEEP_RELATIVE_PATHS = "keep-relative-paths"; //$NON-NLS-1$
// evaluates to "/${ProjName)/"
private static final String PROJ_NAME_PREFIX = '/'
+ CdtVariableResolver.createVariableReference(CdtVariableResolver.VAR_PROJ_NAME) + '/';
protected ICConfigurationDescription currentCfgDescription = null;
protected IWorkingDirectoryTracker cwdTracker = null;
protected IProject currentProject = null;
protected IResource currentResource = null;
protected String currentLanguageId = null;
protected String parsedResourceName = null;
protected boolean isResolvingPaths = true;
private static final int FIND_RESOURCES_CACHE_SIZE = 100;
private LRUCache<URI, IResource[]> workspaceRootFindContainersForLocationURICache = new LRUCache<>(
FIND_RESOURCES_CACHE_SIZE);
private LRUCache<URI, IResource[]> workspaceRootFindFilesForLocationURICache = new LRUCache<>(
FIND_RESOURCES_CACHE_SIZE);
private HashMap<IProject, LRUCache<IPath, List<IResource>>> findPathInProjectCache = new HashMap<>();
/** @since 8.2 */
protected EFSExtensionProvider efsProvider = null;
private static final EFSExtensionProvider efsProviderDefault = new EFSExtensionProvider() {
final EFSExtensionManager efsManager = EFSExtensionManager.getDefault();
@Override
public String getPathFromURI(URI locationURI) {
return efsManager.getPathFromURI(locationURI);
}
@Override
public URI getLinkedURI(URI locationURI) {
return efsManager.getLinkedURI(locationURI);
}
@Override
public URI createNewURIFromPath(URI locationOnSameFilesystem, String path) {
return efsManager.createNewURIFromPath(locationOnSameFilesystem, path);
}
@Override
public String getMappedPath(URI locationURI) {
return efsManager.getMappedPath(locationURI);
}
@Override
public boolean isVirtual(URI locationURI) {
return efsManager.isVirtual(locationURI);
}
@Override
public URI append(URI baseURI, String extension) {
return efsManager.append(baseURI, extension);
}
};
/**
* Abstract class defining common functionality for option parsers.
* The purpose of this parser is to parse a portion of string representing
* a single option and create a language settings entry out of it.
*
* See {@link GCCBuildCommandParser} for an example how to define the parsers.
*/
protected static abstract class AbstractOptionParser {
private final int kind;
private final Pattern pattern;
private final String nameExpression;
private final String valueExpression;
private final int extraFlag;
private String parsedName;
private String parsedValue;
private final Pattern removeExtraFileNamePattern;
/**
* Constructor.
*
* @param kind - kind of language settings entries being parsed by the parser.
* @param pattern - regular expression pattern being parsed by the parser.
* The pattern may be embedded into another pattern for intermediate
* parsing so it is best to avoid using numbered group back-reference e.g. \1
* @param nameExpression - capturing group expression (numbered or named) defining name of an entry.
* @param valueExpression - capturing group expression (numbered or named) defining value of an entry.
* @param extraFlag - extra-flag to add while creating language settings entry.
*/
public AbstractOptionParser(int kind, String pattern, String nameExpression, String valueExpression,
int extraFlag) {
this.kind = kind;
this.nameExpression = nameExpression;
this.valueExpression = valueExpression;
this.extraFlag = extraFlag;
this.pattern = Pattern.compile(pattern);
this.removeExtraFileNamePattern = Pattern.compile("(" + pattern + ").*"); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Create language settings entry of appropriate kind and considering extra-flag passed in constructor.
*
* @param name - name of language settings entry.
* @param value - value of language settings entry.
* @param flag - flag to set. Note that the flag will be amended with the extra-flag defined in constructor.
* @return new language settings entry.
*/
public ICLanguageSettingEntry createEntry(String name, String value, int flag) {
return (ICLanguageSettingEntry) CDataUtil.createEntry(kind, name, value, null, flag | extraFlag);
}
/**
* Check if the king of option parsed by parser is "file".
*
* @return {@code true} if the kind is file, {@code false} otherwise.
*/
public boolean isForFile() {
return kind == ICSettingEntry.INCLUDE_FILE || kind == ICSettingEntry.MACRO_FILE;
}
/**
* Check if the king of option parsed by parser is "folder".
*
* @return {@code true} if the kind is folder, {@code false} otherwise.
*/
public boolean isForFolder() {
return kind == ICSettingEntry.INCLUDE_PATH || kind == ICSettingEntry.LIBRARY_PATH;
}
/**
* Return value represented by the capturing group expression.
*/
private String parseStr(Matcher matcher, String str) {
if (str != null)
return matcher.replaceAll(str);
return null;
}
/**
* Test for a match and parse a portion of input string representing a single option
* to retrieve name and value.
*
* @param optionString - an option to test and parse, possibly with an argument.
* @return {@code true} if the option is a match to parser's regular expression
* or {@code false} otherwise.
*/
public boolean parseOption(String optionString) {
// get rid of extra text at the end (for example file name could be confused for an argument)
Matcher matcherRemoveExtra = removeExtraFileNamePattern.matcher(optionString);
String option = optionString;
if (!matcherRemoveExtra.matches()) {
return false;
}
option = matcherRemoveExtra.group(1);
Matcher matcher = pattern.matcher(option);
boolean isMatch = matcher.matches();
if (isMatch) {
parsedName = parseStr(matcher, nameExpression);
parsedValue = parseStr(matcher, valueExpression);
}
return isMatch;
}
}
/**
* Implementation of {@link AbstractOptionParser} for include path options parsing.
*/
protected static class IncludePathOptionParser extends AbstractOptionParser {
public IncludePathOptionParser(String pattern, String nameExpression) {
super(ICLanguageSettingEntry.INCLUDE_PATH, pattern, nameExpression, nameExpression, 0);
}
public IncludePathOptionParser(String pattern, String nameExpression, int extraFlag) {
super(ICLanguageSettingEntry.INCLUDE_PATH, pattern, nameExpression, nameExpression, extraFlag);
}
}
/**
* Implementation of {@link AbstractOptionParser} for include file options parsing.
*/
protected static class IncludeFileOptionParser extends AbstractOptionParser {
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
*/
public IncludeFileOptionParser(String pattern, String nameExpression) {
super(ICLanguageSettingEntry.INCLUDE_FILE, pattern, nameExpression, nameExpression, 0);
}
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
* @param extraFlag - extra-flag to add while creating language settings entry.
*/
public IncludeFileOptionParser(String pattern, String nameExpression, int extraFlag) {
super(ICLanguageSettingEntry.INCLUDE_FILE, pattern, nameExpression, nameExpression, extraFlag);
}
}
/**
* Implementation of {@link AbstractOptionParser} for macro options parsing.
*/
protected static class MacroOptionParser extends AbstractOptionParser {
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
* @param valueExpression - capturing group expression defining value of an entry.
*/
public MacroOptionParser(String pattern, String nameExpression, String valueExpression) {
super(ICLanguageSettingEntry.MACRO, pattern, nameExpression, valueExpression, 0);
}
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
* @param valueExpression - capturing group expression defining value of an entry.
* @param extraFlag - extra-flag to add while creating language settings entry.
*/
public MacroOptionParser(String pattern, String nameExpression, String valueExpression, int extraFlag) {
super(ICLanguageSettingEntry.MACRO, pattern, nameExpression, valueExpression, extraFlag);
}
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
* @param extraFlag - extra-flag to add while creating language settings entry.
*/
public MacroOptionParser(String pattern, String nameExpression, int extraFlag) {
super(ICLanguageSettingEntry.MACRO, pattern, nameExpression, null, extraFlag);
}
}
/**
* Implementation of {@link AbstractOptionParser} for macro file options parsing.
*/
protected static class MacroFileOptionParser extends AbstractOptionParser {
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
*/
public MacroFileOptionParser(String pattern, String nameExpression) {
super(ICLanguageSettingEntry.MACRO_FILE, pattern, nameExpression, nameExpression, 0);
}
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
* @param extraFlag - extra-flag to add while creating language settings entry.
*/
public MacroFileOptionParser(String pattern, String nameExpression, int extraFlag) {
super(ICLanguageSettingEntry.MACRO_FILE, pattern, nameExpression, nameExpression, extraFlag);
}
}
/**
* Implementation of {@link AbstractOptionParser} for library path options parsing.
*/
protected static class LibraryPathOptionParser extends AbstractOptionParser {
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
*/
public LibraryPathOptionParser(String pattern, String nameExpression) {
super(ICLanguageSettingEntry.LIBRARY_PATH, pattern, nameExpression, nameExpression, 0);
}
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
* @param extraFlag - extra-flag to add while creating language settings entry.
*/
public LibraryPathOptionParser(String pattern, String nameExpression, int extraFlag) {
super(ICLanguageSettingEntry.LIBRARY_PATH, pattern, nameExpression, nameExpression, extraFlag);
}
}
/**
* Implementation of {@link AbstractOptionParser} for library file options parsing.
*/
protected static class LibraryFileOptionParser extends AbstractOptionParser {
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
*/
public LibraryFileOptionParser(String pattern, String nameExpression) {
super(ICLanguageSettingEntry.LIBRARY_FILE, pattern, nameExpression, nameExpression, 0);
}
/**
* Constructor.
* @param pattern - regular expression pattern being parsed by the parser.
* @param nameExpression - capturing group expression defining name of an entry.
* @param extraFlag - extra-flag to add while creating language settings entry.
*/
public LibraryFileOptionParser(String pattern, String nameExpression, int extraFlag) {
super(ICLanguageSettingEntry.LIBRARY_FILE, pattern, nameExpression, nameExpression, extraFlag);
}
}
/**
* Parse the line returning the resource name as appears in the output.
* This is the resource where {@link ICLanguageSettingEntry} list is being added.
*
* @param line - one input line from the output stripped from end of line characters.
* @return the resource name as appears in the output or {@code null}.
* Note that {@code null} can have different semantics and can mean "no resource found"
* or "applicable to any resource". By default "no resource found" is used in this
* abstract class but extenders can handle otherwise.
*/
protected abstract String parseResourceName(String line);
/**
* Parse the line returning the list of substrings to be treated each as input to
* the option parsers. It is assumed that each substring presents one
* {@link ICLanguageSettingEntry} (for example compiler options {@code -I/path} or
* {@code -DMACRO=1}).
*
* @param line - one input line from the output stripped from end of line characters.
* @return list of substrings representing language settings entries.
*/
protected abstract List<String> parseOptions(String line);
/**
* @return array of option parsers defining how to parse a string to
* {@link ICLanguageSettingEntry}.
* See {@link AbstractOptionParser} and its specific extenders.
*/
protected abstract AbstractOptionParser[] getOptionParsers();
/**
* @return array of option parsers defining how to parse a string to
* {@link ICLanguageSettingEntry}.
* See {@link AbstractOptionParser} and its specific extenders.
*
* @param optionToParse the option string to be parsed.
* This can be used as a hint in order to return a subset of parsers, for better performance.
*
* @since 9.1
*/
protected AbstractOptionParser[] getOptionParsers(String optionToParse) {
return getOptionParsers();
}
/**
* @return {@code true} when the provider tries to resolve relative or remote paths
* to the existing paths in the workspace or local file-system using certain heuristics.
*/
public boolean isResolvingPaths() {
return isResolvingPaths;
}
/**
* Enable or disable resolving relative or remote paths to the existing paths
* in the workspace or local file-system.
*
* @param resolvePaths - set {@code true} to enable or {@code false} to disable
* resolving paths. When this parameter is set to {@code false} the paths will
* be kept as they appear in the build output.
*/
public void setResolvingPaths(boolean resolvePaths) {
this.isResolvingPaths = resolvePaths;
}
@Override
public void startup(ICConfigurationDescription cfgDescription, IWorkingDirectoryTracker cwdTracker)
throws CoreException {
this.currentCfgDescription = cfgDescription;
this.currentProject = cfgDescription != null ? cfgDescription.getProjectDescription().getProject() : null;
this.cwdTracker = cwdTracker;
this.efsProvider = getEFSProvider();
}
@Override
public void shutdown() {
// release resources for garbage collector
// but keep currentCfgDescription for AbstractBuiltinSpecsDetector flow
parsedResourceName = null;
currentLanguageId = null;
currentResource = null;
cwdTracker = null;
clearCaches();
}
private void clearCaches() {
workspaceRootFindContainersForLocationURICache.clear();
workspaceRootFindFilesForLocationURICache.clear();
findPathInProjectCache.clear();
}
@Override
public boolean processLine(String line) {
parsedResourceName = parseResourceName(line);
currentResource = findResource(parsedResourceName);
currentLanguageId = determineLanguage();
if (!isLanguageInScope(currentLanguageId)) {
return false;
}
/**
* URI of directory where the build is happening. This URI could point to a remote file-system
* for remote builds. Most often it is the same file-system as for currentResource but
* it can be different file-system (and different URI schema).
*/
URI buildDirURI = null;
/**
* Where source tree starts if mapped. This kind of mapping is useful for example in cases when
* the absolute path to the source file on the remote system is simulated inside a project in the
* workspace.
* This URI is rooted on the same file-system where currentResource resides. In general this file-system
* (or even URI schema) does not have to match that of buildDirURI.
*/
URI mappedRootURI = null;
if (isResolvingPaths) {
mappedRootURI = getMappedRootURI(currentResource, parsedResourceName);
buildDirURI = getBuildDirURI(mappedRootURI);
}
List<ICLanguageSettingEntry> entries = new ArrayList<>();
List<String> options = parseOptions(line);
if (options != null) {
for (String option : options) {
AbstractOptionParser[] optionParsers = getOptionParsers(option);
for (AbstractOptionParser optionParser : optionParsers) {
try {
if (optionParser.parseOption(option)) {
ICLanguageSettingEntry entry = null;
if (isResolvingPaths && (optionParser.isForFile() || optionParser.isForFolder())) {
URI baseURI = mappedRootURI;
if (buildDirURI != null && !new Path(optionParser.parsedName).isAbsolute()) {
if (mappedRootURI != null) {
baseURI = efsProvider.append(mappedRootURI, buildDirURI.getPath());
} else {
baseURI = buildDirURI;
}
}
entry = createResolvedPathEntry(optionParser, optionParser.parsedName, 0, baseURI);
} else {
entry = optionParser.createEntry(optionParser.parsedName, optionParser.parsedValue, 0);
}
if (entry != null && !entries.contains(entry)) {
entries.add(entry);
break;
}
}
} catch (Throwable e) {
@SuppressWarnings("nls")
String msg = "Exception trying to parse option [" + option + "], class "
+ getClass().getSimpleName();
ManagedBuilderCorePlugin
.log(new Status(IStatus.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID, msg, e));
}
}
}
if (entries.size() > 0) {
setSettingEntries(entries);
} else {
setSettingEntries(null);
}
}
return false;
}
/**
* In case when absolute path is mapped to the source tree in a project
* this function will try to figure mapping and return "mapped root",
* i.e URI where the root path would be mapped. The mapped root will be
* used to prepend to other "absolute" paths where appropriate.
*
* @param resource - a resource referred by parsed path
* @param parsedResourceName - path as appears in the output
* @return mapped path as URI
*/
protected URI getMappedRootURI(IResource resource, String parsedResourceName) {
if (resource == null) {
return null;
}
URI resourceURI = resource.getLocationURI();
String mappedRoot = "/"; //$NON-NLS-1$
if (parsedResourceName != null) {
IPath parsedSrcPath = new Path(parsedResourceName);
if (parsedSrcPath.isAbsolute()) {
IPath absResourcePath = resource.getLocation();
int absSegmentsCount = absResourcePath.segmentCount();
int relSegmentsCount = parsedSrcPath.segmentCount();
if (absSegmentsCount >= relSegmentsCount) {
IPath ending = absResourcePath.removeFirstSegments(absSegmentsCount - relSegmentsCount);
ending = ending.setDevice(parsedSrcPath.getDevice()).makeAbsolute();
if (ending.equals(parsedSrcPath.makeAbsolute())) {
// mappedRoot here is parsedSrcPath with removed parsedResourceName trailing segments,
// i.e. if absResourcePath="/path/workspace/project/file.c" and parsedResourceName="project/file.c"
// then mappedRoot="/path/workspace/"
mappedRoot = absResourcePath.removeLastSegments(relSegmentsCount).toString();
}
}
}
}
// this creates URI with schema and other components from resourceURI but path as mappedRoot
URI uri = efsProvider.createNewURIFromPath(resourceURI, mappedRoot);
return uri;
}
/**
* Determine current build directory considering currentResource (resource being compiled),
* parsedResourceName and mappedRootURI.
*
* @param mappedRootURI - root of the source tree when mapped to remote file-system.
* @return {@link URI} of current build directory
*/
protected URI getBuildDirURI(URI mappedRootURI) {
URI buildDirURI = null;
// try to deduce build directory from full path of currentResource and partial path of parsedResourceName
URI cwdURI = null;
if (currentResource != null && parsedResourceName != null && !new Path(parsedResourceName).isAbsolute()) {
cwdURI = findBaseLocationURI(currentResource.getLocationURI(), parsedResourceName);
}
String cwdPath = cwdURI != null ? efsProvider.getPathFromURI(cwdURI) : null;
if (cwdPath != null && mappedRootURI != null) {
buildDirURI = efsProvider.append(mappedRootURI, cwdPath);
} else {
buildDirURI = cwdURI;
}
// try IWorkingDirectoryTracker
if (buildDirURI == null && cwdTracker != null) {
buildDirURI = cwdTracker.getWorkingDirectoryURI();
}
// try builder working directory
if (buildDirURI == null && currentCfgDescription != null) {
IPath pathBuilderCWD = currentCfgDescription.getBuildSetting().getBuilderCWD();
if (pathBuilderCWD != null) {
String builderCWD = pathBuilderCWD.toString();
try {
// here is a hack to overcome ${workspace_loc:/prj-name} returned by builder
// where "/" is treated as path separator by pathBuilderCWD
ICdtVariableManager vmanager = CCorePlugin.getDefault().getCdtVariableManager();
builderCWD = vmanager.resolveValue(builderCWD, "", null, currentCfgDescription); //$NON-NLS-1$
} catch (CdtVariableException e) {
ManagedBuilderCorePlugin.log(e);
}
if (builderCWD != null && !builderCWD.isEmpty()) {
buildDirURI = org.eclipse.core.filesystem.URIUtil.toURI(builderCWD);
}
}
}
// try directory of the current project
if (buildDirURI == null && currentProject != null) {
buildDirURI = currentProject.getLocationURI();
}
// try parent folder of the resource
if (buildDirURI == null && currentResource != null) {
IContainer container;
if (currentResource instanceof IContainer) {
container = (IContainer) currentResource;
} else {
container = currentResource.getParent();
}
buildDirURI = container.getLocationURI();
}
return buildDirURI;
}
/**
* Sets language settings entries for current configuration description, current resource
* and current language ID.
*
* @param entries - language settings entries to set.
*/
protected void setSettingEntries(List<? extends ICLanguageSettingEntry> entries) {
setSettingEntries(currentCfgDescription, currentResource, currentLanguageId, entries);
}
/**
* Determine a language associated with the resource.
*
* @return language ID for the resource.
*/
protected String determineLanguage() {
IResource rc = currentResource;
if (rc == null && currentProject != null && parsedResourceName != null) {
String fileName = new Path(parsedResourceName).lastSegment().toString();
// use handle; resource does not need to exist
rc = currentProject.getFile("__" + fileName); //$NON-NLS-1$
}
if (rc == null)
return null;
List<String> languageIds = LanguageSettingsManager.getLanguages(rc, currentCfgDescription);
if (languageIds.isEmpty())
return null;
return languageIds.get(0);
}
/**
* Determine if the language is in scope of the provider.
*
* @param languageId - language ID.
* @return {@code true} if the language is in scope, {@code false } otherwise.
*/
protected boolean isLanguageInScope(String languageId) {
List<String> languageIds = getLanguageScope();
return languageIds == null || languageIds.contains(languageId);
}
/**
* Find file resource in the workspace for a given URI with a preference for the resource
* to reside in the given project.
*/
private IResource findFileForLocationURI(URI uri, IProject preferredProject, boolean checkExistence) {
if (!uri.isAbsolute()) {
// IWorkspaceRoot.findFilesForLocationURI(URI) below requires an absolute URI
// therefore we haven't/aren't going to find the file based on this URI.
return null;
}
IResource sourceFile = null;
IResource[] resources = workspaceRootFindFilesForLocationURICache.computeIfAbsent(uri,
key -> ResourcesPlugin.getWorkspace().getRoot().findFilesForLocationURI(key));
for (IResource rc : resources) {
if (!checkExistence || rc.isAccessible()) {
if (rc.getProject().equals(preferredProject)) {
sourceFile = rc;
break;
}
if (sourceFile == null) {
sourceFile = rc;
}
}
}
return sourceFile;
}
/**
* Return a resource in workspace corresponding the given folder {@link URI} preferable residing in
* the provided project.
*/
private IResource findContainerForLocationURI(URI uri, IProject preferredProject, boolean checkExistence) {
IResource resource = null;
IResource[] resources = workspaceRootFindContainersForLocationURICache.computeIfAbsent(uri,
key -> ResourcesPlugin.getWorkspace().getRoot().findContainersForLocationURI(key));
for (IResource rc : resources) {
if ((rc instanceof IProject || rc instanceof IFolder) && (!checkExistence || rc.isAccessible())) { // treat IWorkspaceRoot as non-workspace path
if (rc.getProject().equals(preferredProject)) {
resource = rc;
break;
}
if (resource == null) {
resource = rc; // to be deterministic the first qualified resource has preference
}
}
}
return resource;
}
/**
* Determine resource in the workspace corresponding to the parsed resource name.
*/
private IResource findResource(String parsedResourceName) {
if (parsedResourceName == null || parsedResourceName.isEmpty()) {
return null;
}
IResource sourceFile = null;
// try ErrorParserManager
if (cwdTracker instanceof ErrorParserManager) {
sourceFile = ((ErrorParserManager) cwdTracker).findFileName(parsedResourceName);
}
// try to find absolute path in the workspace
Path parsedPath = new Path(parsedResourceName);
if (sourceFile == null && parsedPath.isAbsolute()) {
// It will often happen that the file will be under the project and in the local file system, so check there first.
IPath projectLocation = currentProject != null ? currentProject.getLocation() : null;
if (projectLocation != null) {
IPath relativePath = parsedPath.makeRelativeTo(projectLocation);
if (!relativePath.equals(parsedPath)) {
IFile file = currentProject.getFile(relativePath);
if (file.isAccessible()) {
return file;
}
}
}
URI uri = org.eclipse.core.filesystem.URIUtil.toURI(parsedResourceName);
sourceFile = findFileForLocationURI(uri, currentProject, /*checkExistence*/ true);
}
// try last known current working directory from build output
if (sourceFile == null && cwdTracker != null) {
URI cwdURI = cwdTracker.getWorkingDirectoryURI();
if (cwdURI != null) {
URI uri = efsProvider.append(cwdURI, parsedResourceName);
sourceFile = findFileForLocationURI(uri, currentProject, /*checkExistence*/ true);
}
}
// try path relative to build dir from configuration
if (sourceFile == null && currentCfgDescription != null) {
IPath builderCWD = currentCfgDescription.getBuildSetting().getBuilderCWD();
if (builderCWD != null) {
String strBuilderCWD = builderCWD.toString();
try {
ICdtVariableManager varManager = CCorePlugin.getDefault().getCdtVariableManager();
strBuilderCWD = varManager.resolveValue(strBuilderCWD, "", null, currentCfgDescription); //$NON-NLS-1$
} catch (Exception e) {
@SuppressWarnings("nls")
String msg = "Exception trying to resolve value [" + strBuilderCWD + "]";
ManagedBuilderCorePlugin.log(new Status(IStatus.ERROR, ManagedBuilderCorePlugin.PLUGIN_ID, msg, e));
}
builderCWD = new Path(strBuilderCWD);
IPath path = builderCWD.append(parsedResourceName);
URI uri = org.eclipse.core.filesystem.URIUtil.toURI(path);
sourceFile = findFileForLocationURI(uri, currentProject, /*checkExistence*/ true);
}
}
// try path relative to the project
if (sourceFile == null && currentProject != null) {
sourceFile = currentProject.findMember(parsedResourceName);
}
return sourceFile;
}
/**
* Find base location of the file, i.e. location of the directory which
* results from removing trailing relativeFileName from fileURI or
* {@code null} if fileURI doesn't represent relativeFileName.
*/
private static URI findBaseLocationURI(URI fileURI, String relativeFileName) {
URI cwdURI = null;
String path = fileURI.getPath();
String[] segments = relativeFileName.split("[/\\\\]"); //$NON-NLS-1$
// start removing segments from the end of the path
for (int i = segments.length - 1; i >= 0; i--) {
String lastSegment = segments[i];
if (lastSegment.length() > 0 && !lastSegment.equals(".")) { //$NON-NLS-1$
if (lastSegment.equals("..")) { //$NON-NLS-1$
// navigating ".." in the other direction is ambiguous, bailing out
return null;
} else {
if (path.endsWith("/" + lastSegment)) { //$NON-NLS-1$
int pos = path.lastIndexOf("/" + lastSegment); //$NON-NLS-1$
path = path.substring(0, pos);
continue;
} else {
// ouch, relativeFileName does not match fileURI, bailing out
return null;
}
}
}
}
try {
cwdURI = new URI(fileURI.getScheme(), fileURI.getUserInfo(), fileURI.getHost(), fileURI.getPort(),
path + '/', fileURI.getQuery(), fileURI.getFragment());
} catch (URISyntaxException e) {
// It should be valid URI here or something is really wrong
ManagedBuilderCorePlugin.log(e);
}
return cwdURI;
}
/**
* The manipulations here are done to resolve problems such as "../" navigation for symbolic links where
* "link/.." cannot be collapsed as it must follow the real file-system path. {@link java.io.File#getCanonicalPath()}
* deals with that correctly but {@link Path} or {@link URI} try to normalize the path which would be incorrect here.
* Another issue being resolved here is fixing drive letters in URI syntax.
*/
private static URI resolvePathFromBaseLocation(String pathStr0, IPath baseLocation) {
String pathStr = pathStr0;
if (baseLocation != null && !baseLocation.isEmpty()) {
pathStr = pathStr.replace(File.separatorChar, '/');
String device = new Path(pathStr).getDevice();
if (device == null || device.equals(baseLocation.getDevice())) {
if (device != null && device.length() > 0) {
pathStr = pathStr.substring(device.length());
}
baseLocation = baseLocation.addTrailingSeparator();
if (pathStr.startsWith("/")) { //$NON-NLS-1$
pathStr = pathStr.substring(1);
}
pathStr = baseLocation.toString() + pathStr;
}
}
try {
File file = new File(pathStr);
file = file.getCanonicalFile();
URI uri = file.toURI();
if (file.exists()) {
return uri;
}
IPath path0 = new Path(pathStr0);
if (!path0.isAbsolute()) {
return uri;
}
String device = path0.getDevice();
if (device == null || device.isEmpty()) {
// Avoid spurious adding of drive letters on Windows
pathStr = path0.setDevice(null).toString();
} else {
// On Windows "C:/folder/" -> "/C:/folder/"
if (pathStr.charAt(0) != IPath.SEPARATOR) {
pathStr = IPath.SEPARATOR + pathStr;
}
}
return new URI(uri.getScheme(), uri.getAuthority(), pathStr, uri.getQuery(), uri.getFragment());
} catch (Exception e) {
// if error will leave it as is
ManagedBuilderCorePlugin.log(e);
}
return org.eclipse.core.filesystem.URIUtil.toURI(pathStr);
}
/**
* Determine URI on the local file-system considering possible mapping.
*
* @param pathStr - path to the resource, can be absolute or relative
* @param baseURI - base {@link URI} where path to the resource is rooted
* @return {@link URI} of the resource
*/
private URI determineMappedURI(String pathStr, URI baseURI) {
URI uri = null;
if (baseURI == null) {
if (new Path(pathStr).isAbsolute()) {
uri = resolvePathFromBaseLocation(pathStr, Path.ROOT);
}
} else if (baseURI.getScheme().equals(EFS.SCHEME_FILE)) {
// location on the local file-system
IPath baseLocation = org.eclipse.core.filesystem.URIUtil.toPath(baseURI);
// careful not to use Path here but 'pathStr' as String as we want to properly navigate symlinks
uri = resolvePathFromBaseLocation(pathStr, baseLocation);
} else {
// location on a remote file-system
IPath path = new Path(pathStr); // use canonicalized path here, in particular replace all '\' with '/' for Windows paths
URI remoteUri = efsProvider.append(baseURI, path.toString());
if (remoteUri != null) {
String localPath = efsProvider.getMappedPath(remoteUri);
if (localPath != null) {
uri = org.eclipse.core.filesystem.URIUtil.toURI(localPath);
}
}
}
if (uri == null) {
// if everything fails just wrap string to URI
uri = org.eclipse.core.filesystem.URIUtil.toURI(pathStr);
}
return uri;
}
/**
* Find all resources in the project which might be represented by relative path passed.
*/
private List<IResource> findPathInProject(IPath path, IProject project) {
LRUCache<IPath, List<IResource>> cache = findPathInProjectCache.computeIfAbsent(project,
key -> new LRUCache<>(FIND_RESOURCES_CACHE_SIZE));
return cache.computeIfAbsent(path, key -> findPathInFolder(path, project));
}
/**
* Find all resources in the folder which might be represented by relative path passed.
*/
private static List<IResource> findPathInFolder(IPath path, IContainer folder) {
List<IResource> paths = new ArrayList<>();
IResource resource = folder.findMember(path);
if (resource != null) {
paths.add(resource);
}
try {
for (IResource res : folder.members()) {
if (res instanceof IContainer) {
paths.addAll(findPathInFolder(path, (IContainer) res));
}
}
} catch (CoreException e) {
// ignore
}
return paths;
}
/**
* Determine which resource in workspace is the best fit to parsedName passed.
*/
private IResource findBestFitInWorkspace(String parsedName) {
Set<String> referencedProjectsNames = new LinkedHashSet<>();
if (currentCfgDescription != null) {
Map<String, String> refs = currentCfgDescription.getReferenceInfo();
referencedProjectsNames.addAll(refs.keySet());
}
IPath path = new Path(parsedName);
if (path.equals(new Path(".")) || path.equals(new Path(".."))) { //$NON-NLS-1$ //$NON-NLS-2$
return null;
}
// prefer current project
if (currentProject != null) {
List<IResource> result = findPathInProject(path, currentProject);
int size = result.size();
if (size == 1) { // found the one
return result.get(0);
} else if (size > 1) { // ambiguous
return null;
}
}
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
// then prefer referenced projects
if (referencedProjectsNames.size() > 0) {
IResource rc = null;
for (String prjName : referencedProjectsNames) {
IProject prj = root.getProject(prjName);
if (prj.isOpen()) {
List<IResource> result = findPathInProject(path, prj);
int size = result.size();
if (size == 1 && rc == null) {
rc = result.get(0);
} else if (size > 0) {
// ambiguous
rc = null;
break;
}
}
}
if (rc != null) {
return rc;
}
}
// then check all other projects in workspace
IProject[] projects = root.getProjects();
if (projects.length > 0) {
IResource rc = null;
for (IProject prj : projects) {
if (!prj.equals(currentProject) && !referencedProjectsNames.contains(prj.getName()) && prj.isOpen()) {
List<IResource> result = findPathInProject(path, prj);
int size = result.size();
if (size == 1 && rc == null) {
rc = result.get(0);
} else if (size > 0) {
// ambiguous
rc = null;
break;
}
}
}
if (rc != null) {
return rc;
}
}
// not found or ambiguous
return null;
}
/**
* Get location on the local file-system considering possible mapping by EFS provider. See {@link EFSExtensionManager}.
*/
private IPath getFilesystemLocation(URI uri) {
if (uri == null)
return null;
String pathStr = efsProvider.getMappedPath(uri);
uri = org.eclipse.core.filesystem.URIUtil.toURI(pathStr);
if (uri != null && uri.isAbsolute()) {
try {
File file = new java.io.File(uri);
String canonicalPathStr = file.getCanonicalPath();
if (new Path(pathStr).getDevice() == null) {
return new Path(canonicalPathStr).setDevice(null);
}
return new Path(canonicalPathStr);
} catch (Exception e) {
ManagedBuilderCorePlugin.log(e);
}
}
return null;
}
/**
* Resolve and create language settings path entry.
*/
private ICLanguageSettingEntry createResolvedPathEntry(AbstractOptionParser optionParser, String parsedPath,
int flag, URI baseURI) {
URI uri = determineMappedURI(parsedPath, baseURI);
boolean isRelative = !new Path(parsedPath).isAbsolute();
// is mapped something that is not a project root
boolean isRemapped = baseURI != null && currentProject != null
&& !baseURI.equals(currentProject.getLocationURI());
boolean presentAsRelative = isRelative || isRemapped;
ICLanguageSettingEntry entry = resolvePathEntryInWorkspace(optionParser, uri, flag, presentAsRelative);
if (entry != null) {
return entry;
}
entry = resolvePathEntryInFilesystem(optionParser, uri, flag);
if (entry != null) {
return entry;
}
entry = resolvePathEntryInWorkspaceAsBestFit(optionParser, parsedPath, flag, presentAsRelative);
if (entry != null) {
return entry;
}
entry = resolvePathEntryInWorkspaceToNonexistingResource(optionParser, uri, flag, presentAsRelative);
if (entry != null) {
return entry;
}
entry = resolvePathEntryInFilesystemToNonExistingResource(optionParser, uri, flag);
if (entry != null) {
return entry;
}
return optionParser.createEntry(parsedPath, parsedPath, flag);
}
/**
* Create a language settings entry for a given resource.
* This will represent relative path using CDT variable ${ProjName}.
*/
private ICLanguageSettingEntry createPathEntry(AbstractOptionParser optionParser, IResource rc, boolean isRelative,
int flag) {
String path;
if (isRelative && rc.getProject().equals(currentProject)) {
path = PROJ_NAME_PREFIX + rc.getFullPath().removeFirstSegments(1);
flag = flag | ICSettingEntry.VALUE_WORKSPACE_PATH;
} else {
path = rc.getFullPath().toString();
flag = flag | ICSettingEntry.VALUE_WORKSPACE_PATH | ICSettingEntry.RESOLVED;
}
return optionParser.createEntry(path, path, flag);
}
/**
* Find an existing resource in the workspace and create a language settings entry for it.
*/
private ICLanguageSettingEntry resolvePathEntryInWorkspace(AbstractOptionParser optionParser, URI uri, int flag,
boolean isRelative) {
if (uri != null && uri.isAbsolute()) {
IResource rc = null;
if (optionParser.isForFolder()) {
rc = findContainerForLocationURI(uri, currentProject, /*checkExistence*/ true);
} else if (optionParser.isForFile()) {
rc = findFileForLocationURI(uri, currentProject, /*checkExistence*/ true);
}
if (rc != null) {
return createPathEntry(optionParser, rc, isRelative, flag);
}
}
return null;
}
/**
* Find a resource on the file-system and create a language settings entry for it.
*/
private ICLanguageSettingEntry resolvePathEntryInFilesystem(AbstractOptionParser optionParser, URI uri, int flag) {
IPath location = getFilesystemLocation(uri);
if (location != null) {
String loc = location.toString();
if (new File(loc).exists()) {
return optionParser.createEntry(loc, loc, flag);
}
}
return null;
}
/**
* Find a best fit for the resource in the workspace and create a language settings entry for it.
*/
private ICLanguageSettingEntry resolvePathEntryInWorkspaceAsBestFit(AbstractOptionParser optionParser,
String parsedPath, int flag, boolean isRelative) {
IResource rc = findBestFitInWorkspace(parsedPath);
if (rc != null) {
return createPathEntry(optionParser, rc, isRelative, flag);
}
return null;
}
/**
* Try to map a resource in the workspace even if it does not exist and create a language settings entry for it.
*/
private ICLanguageSettingEntry resolvePathEntryInWorkspaceToNonexistingResource(AbstractOptionParser optionParser,
URI uri, int flag, boolean isRelative) {
if (uri != null && uri.isAbsolute()) {
IResource rc = null;
if (optionParser.isForFolder()) {
rc = findContainerForLocationURI(uri, currentProject, /*checkExistence*/ false);
} else if (optionParser.isForFile()) {
rc = findFileForLocationURI(uri, currentProject, /*checkExistence*/ false);
}
if (rc != null) {
return createPathEntry(optionParser, rc, isRelative, flag);
}
}
return null;
}
/**
* Try to map a resource on the file-system even if it does not exist and create a language settings entry for it.
*/
private ICLanguageSettingEntry resolvePathEntryInFilesystemToNonExistingResource(AbstractOptionParser optionParser,
URI uri, int flag) {
IPath location = getFilesystemLocation(uri);
if (location != null) {
return optionParser.createEntry(location.toString(), location.toString(), flag);
}
return null;
}
/**
* Count how many groups are present in regular expression.
* The implementation is simplistic but should be sufficient for the cause.
*
* @param str - regular expression to count the groups.
* @return number of the groups (groups are enclosed in round brackets) present.
*/
protected static int countGroups(String str) {
@SuppressWarnings("nls")
int count = str.replaceAll("[^\\(]", "").length();
return count;
}
/**
* Helper method to construct logical "or" to be used inside regular expressions.
*/
@SuppressWarnings("nls")
private static String expressionLogicalOr(Set<String> fileExts) {
String pattern = "(";
for (String ext : fileExts) {
if (pattern.length() != 1)
pattern += "|";
pattern += "(" + Pattern.quote(ext) + ")";
ext = ext.toUpperCase();
if (!fileExts.contains(ext)) {
pattern += "|(" + Pattern.quote(ext) + ")";
}
}
pattern += ")";
return pattern;
}
/**
* Construct regular expression to find any file extension for C or C++.
* Returns expression shaped in form of "((cpp)|(c++)|(c))".
*
* @return regular expression for searching C/C++ file extensions.
*/
protected String getPatternFileExtensions() {
IContentTypeManager manager = Platform.getContentTypeManager();
Set<String> fileExts = new HashSet<>();
IContentType contentTypeCpp = manager.getContentType(CCorePlugin.CONTENT_TYPE_CXXSOURCE);
fileExts.addAll(Arrays.asList(contentTypeCpp.getFileSpecs(IContentType.FILE_EXTENSION_SPEC)));
IContentType contentTypeC = manager.getContentType(CCorePlugin.CONTENT_TYPE_CSOURCE);
fileExts.addAll(Arrays.asList(contentTypeC.getFileSpecs(IContentType.FILE_EXTENSION_SPEC)));
String pattern = expressionLogicalOr(fileExts);
return pattern;
}
/**
* This {@link EFSExtensionProvider} is capable to translate EFS paths to and from local
* file-system. Added mostly for Cygwin translations.
*
* This usage of {@link EFSExtensionProvider} is somewhat a misnomer. This provider is not
* an "extension" provider but rather a wrapper on {@link EFSExtensionManager} which in fact
* will use genuine {@link EFSExtensionProvider}s defined as extensions.
*
* @since 8.2
*/
protected EFSExtensionProvider getEFSProvider() {
return efsProviderDefault;
}
@Override
public Element serializeAttributes(Element parentElement) {
Element elementProvider = super.serializeAttributes(parentElement);
elementProvider.setAttribute(ATTR_KEEP_RELATIVE_PATHS, Boolean.toString(!isResolvingPaths));
return elementProvider;
}
@Override
public void loadAttributes(Element providerNode) {
super.loadAttributes(providerNode);
String expandRelativePathsValue = XmlUtil.determineAttributeValue(providerNode, ATTR_KEEP_RELATIVE_PATHS);
if (expandRelativePathsValue != null)
isResolvingPaths = !Boolean.parseBoolean(expandRelativePathsValue);
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + (isResolvingPaths ? 1231 : 1237);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
AbstractLanguageSettingsOutputScanner other = (AbstractLanguageSettingsOutputScanner) obj;
if (isResolvingPaths != other.isResolvingPaths)
return false;
return true;
}
}