blob: 1445ff1485381e0243d433d44b48f6178464162d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016-2020 Martin Weber.
*
* Content is provided to you under the terms and conditions of the Eclipse Public License Version 2.0 "EPL".
* A copy of the EPL is available at http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.cdt.cmake.is.core;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.FilenameUtils;
import org.eclipse.cdt.cmake.is.core.internal.ParserDetection;
import org.eclipse.cdt.cmake.is.core.internal.ParserDetection.DetectorWithMethod;
import org.eclipse.cdt.cmake.is.core.internal.ParserDetection.ParserDetectionResult;
import org.eclipse.cdt.cmake.is.core.internal.Plugin;
import org.eclipse.cdt.cmake.is.core.internal.StringUtil;
import org.eclipse.cdt.cmake.is.core.internal.builtins.CompilerBuiltinsDetector;
import org.eclipse.cdt.cmake.is.core.participant.DefaultToolDetectionParticipant;
import org.eclipse.cdt.cmake.is.core.participant.IRawIndexerInfo;
import org.eclipse.cdt.cmake.is.core.participant.IToolCommandlineParser;
import org.eclipse.cdt.cmake.is.core.participant.IToolCommandlineParser.IResult;
import org.eclipse.cdt.cmake.is.core.participant.IToolDetectionParticipant;
import org.eclipse.cdt.cmake.is.core.participant.builtins.IBuiltinsDetectionBehavior;
import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry;
import org.eclipse.core.resources.IContainer;
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.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.osgi.framework.FrameworkUtil;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
/**
* Parses the file 'compile_commands.json' produced by cmake when option
* {@code -DCMAKE_EXPORT_COMPILE_COMMANDS=ON} is given and generates information
* about preprocessor symbols and include paths of the files being compiled for
* the CDT indexer.
*
* @author Martin Weber
*/
public class CompileCommandsJsonParser {
private static final boolean DEBUG_TIME = Boolean
.parseBoolean(Platform.getDebugOption(Plugin.PLUGIN_ID + "/debug/performance")); //$NON-NLS-1$
private static final boolean DEBUG_ENTRIES = Boolean
.parseBoolean(Platform.getDebugOption(Plugin.PLUGIN_ID + "/debug/detected.entries")); //$NON-NLS-1$
/**
* property to store the file modification time of the "compile_commands.json"
* file
*/
private static final QualifiedName TIMESTAMP_COMPILE_COMMANDS_PROPERTY = new QualifiedName(null,
"timestamp:compile_commands.json"); //$NON-NLS-1$
private static final String WORKBENCH_WILL_NOT_KNOW_ALL_MSG = Messages.CompileCommandsJsonParser_MSG_WORKBENCH_WILL_NOT_KNOW;
private static final String MARKER_ID = Plugin.PLUGIN_ID + ".CompileCommandsJsonParserMarker"; //$NON-NLS-1$
private ParseRequest parseRequest;
private final IParserPreferencesAccess prefsAccess;
/**
* last known working tool detector and its tool option parsers or {@code null},
* if unknown (to speed up parsing)
*/
private DetectorWithMethod lastDetector;
/**
* markers for commands without a cmdline parser for the tool. just for error
* reporting, no technical effect
*/
private Set<String> knownUnsupportedTools = new HashSet<>();
/**
* the raw scanner info results for each source file (source file name ->
* IResult)
*/
private Map<String, IRawIndexerInfo> fileResults;
/**
* minimized set of CompilerBuiltinsDetector to run. (detector key ->
* CompilerBuiltinsDetector). Key is created by
* {@link #makeBuiltinsDetectorKey(String, List, String)}.
*/
private Map<String, CompilerBuiltinsDetector> builtinDetectorsToRun;
/**
* the built-ins detectors for each source file (source file name -> detector
* key)
*/
private Map<String, String> fileToBuiltinDetectorLinks;
/**
* Creates a new object that will try to parse the {@code compile_commands.json}
* file in the build directory specified in the {@code ParseRequest#getBuildConfiguration()}.
*
* @param parseRequest the parser configuration
*/
// TODO interface ICBuildConfiguration should be sufficient here
public CompileCommandsJsonParser(ParseRequest parseRequest) {
this.parseRequest = Objects.requireNonNull(parseRequest, "parseRequest"); //$NON-NLS-1$
prefsAccess = EclipseContextFactory.getServiceContext(FrameworkUtil.getBundle(getClass()).getBundleContext())
.get(IParserPreferencesAccess.class);
}
/**
* Parses the content of the 'compile_commands.json' file corresponding to the
* specified configuration, if time-stamps differ.
* @param monitor the job's progress monitor
*
* @return {@code true} if the json file did change since the last invocation of
* this method (new setting entries were discovered), otherwise
* {@code false}
* @throws CoreException
*/
private boolean processJsonFile(IProgressMonitor monitor) throws CoreException {
final IFile jsonFile = parseRequest.getFile();
final IProject project = jsonFile.getProject();
project.deleteMarkers(MARKER_ID, false, IResource.DEPTH_INFINITE);
final java.nio.file.Path jsonDiskFile = java.nio.file.Path.of(jsonFile.getLocationURI());
if (!Files.exists(jsonDiskFile)) {
// no json file was produced in the build
final String msg = String.format(Messages.CompileCommandsJsonParser_errmsg_file_not_found, jsonDiskFile,
WORKBENCH_WILL_NOT_KNOW_ALL_MSG);
createMarker(project, msg);
return false;
}
// file exists on disk...
long tsJsonModified = 0;
try {
tsJsonModified = Files.getLastModifiedTime(jsonDiskFile).toMillis();
} catch (IOException e) {
// treat as 'file is not modified'
return false;
}
IContainer buildRootFolder = jsonFile.getParent();
Long sessionLastModified = (Long) buildRootFolder.getSessionProperty(TIMESTAMP_COMPILE_COMMANDS_PROPERTY);
if (sessionLastModified == null || sessionLastModified.longValue() < tsJsonModified) {
// must parse json file...
monitor.setTaskName(Messages.CompileCommandsJsonParser_msg_processing);
try (Reader in = new FileReader(jsonDiskFile.toFile())) {
// parse file...
Gson gson = new Gson();
CommandEntry[] sourceFileInfos = gson.fromJson(in, CommandEntry[].class);
for (CommandEntry sourceFileInfo : sourceFileInfos) {
processCommandEntry(sourceFileInfo, jsonFile);
}
} catch (JsonSyntaxException | JsonIOException ex) {
// file format error
final String msg = String.format(Messages.CompileCommandsJsonParser_errmsg_not_json, jsonDiskFile,
WORKBENCH_WILL_NOT_KNOW_ALL_MSG);
createMarker(jsonFile, msg);
return false;
} catch (IOException ex) {
final String msg = String.format(Messages.CompileCommandsJsonParser_errmsg_read_error, jsonDiskFile,
WORKBENCH_WILL_NOT_KNOW_ALL_MSG);
createMarker(jsonFile, msg);
return false;
}
detectBuiltins(monitor);
// store time-stamp
buildRootFolder.setSessionProperty(TIMESTAMP_COMPILE_COMMANDS_PROPERTY, tsJsonModified);
return true;
}
return false;
}
/**
* Processes an entry from a {@code compile_commands.json} file and stores a
* {@link ICLanguageSettingEntry} for the file given the specified map.
*
* @param sourceFileInfo parsed command entry of a compile_commands.json file
* @param jsonFile the JSON file being parsed (for marker creation only)
*
* @throws CoreException if marker creation failed
*/
private void processCommandEntry(CommandEntry sourceFileInfo, IFile jsonFile) throws CoreException {
// NOTE that this is the absolute file system path of the source file in
// CMake-notation (directory separator are forward slashes, even on windows)
final String file = sourceFileInfo.getFile();
final String cmdLine = sourceFileInfo.getCommand();
if (file != null && !file.isEmpty() && cmdLine != null && !cmdLine.isEmpty()) {
ParserDetection.ParserDetectionResult pdr = fastDetermineDetector(cmdLine);
if (pdr != null) {
// found a matching command-line parser
final IToolCommandlineParser parser = pdr.getDetectorWithMethod().getToolDetectionParticipant()
.getParser();
// cwdStr is the absolute working directory of the compiler in
// CMake-notation (fileSep are forward slashes)
final String cwdStr = sourceFileInfo.getDirectory();
IPath cwd = cwdStr != null ? Path.fromOSString(cwdStr) : new Path(""); //$NON-NLS-1$
IResult result = parser.processArgs(cwd, StringUtil.trimLeadingWS(pdr.getReducedCommandLine()));
// remember result together with file name
rememberFileResult(file, result);
final Optional<IBuiltinsDetectionBehavior> builtinDetection = parser.getIBuiltinsDetectionBehavior();
if (builtinDetection.isPresent()) {
rememberBuiltinsDetection(file, builtinDetection.get(), pdr.getCommandLine().getCommand(),
result.getBuiltinDetectionArgs());
}
} else {
// no matching parser found
// complain only once if no cmdline parser for the tool is known (fortran,
// assembler, etc)
int idx = cmdLine.indexOf(' ');
String unkownMarker = (idx != -1 ? cmdLine.substring(0, idx) : cmdLine)
+ FilenameUtils.getExtension(file);
if (knownUnsupportedTools.contains(unkownMarker)) {
return;
}
knownUnsupportedTools.add(unkownMarker);
final String msg = String.format(Messages.CompileCommandsJsonParser_errmsg_no_parser_for_commandline,
cmdLine, WORKBENCH_WILL_NOT_KNOW_ALL_MSG);
createMarker(jsonFile, msg);
}
return;
}
// unrecognized entry, skipping
final String msg = String.format(Messages.CompileCommandsJsonParser_errmsg_unexpected_json, jsonFile,
WORKBENCH_WILL_NOT_KNOW_ALL_MSG);
createMarker(jsonFile, msg);
}
/**
* @param monitor
* @throws CoreException
*/
private void detectBuiltins(IProgressMonitor monitor) throws CoreException {
if (builtinDetectorsToRun.isEmpty())
return;
monitor.setTaskName(Messages.CompileCommandsJsonParser_msg_detecting_builtins);
final IFile jsonFile = parseRequest.getFile();
final IContainer buildRootFolder = jsonFile.getParent();
java.nio.file.Path buildDir = java.nio.file.Path.of(buildRootFolder.getLocationURI());
// run each built-in detector and collect the results..
Map<String, IRawIndexerInfo> builtinDetectorsResults = new HashMap<>();
for (Entry<String, CompilerBuiltinsDetector> entry : builtinDetectorsToRun.entrySet()) {
IRawIndexerInfo result = entry.getValue().detectBuiltins(jsonFile.getProject(), buildDir,
parseRequest.getLauncher(), parseRequest.getConsole(), monitor);
// store detector key with result
builtinDetectorsResults.put(entry.getKey(), result);
}
// all built-in detectors have been run at this point, reduce memory footprint
builtinDetectorsToRun.clear();
// most of the time we get different String objects for different source files
// that have the same sequence of characters. So reduce the number of String
// objects by pooling them..
Map<String, String> strPool = new HashMap<>();
Function<String, String> stringPooler = v -> {
String old = strPool.putIfAbsent(v, v);
return old == null ? v : old;
};
// merge built-in results with source file results
for (Entry<String, String> link : fileToBuiltinDetectorLinks.entrySet()) {
String sourceFileName = link.getKey();
IRawIndexerInfo fileResult = fileResults.get(sourceFileName);
IRawIndexerInfo builtinDetectorsResult = builtinDetectorsResults.get(link.getValue());
mergeResultsForFile(stringPooler, sourceFileName, fileResult, builtinDetectorsResult);
}
}
/**
* Merges preprocessor symbols and macros for a source file with compiler
* built-in preprocessor symbols and macros and passes the to the
* {@code IIndexerInfoConsumer} that was specified in the constructor.
*
* @param fileResult source file preprocessor symbols and macros
* @param builtinDetectorsResult compiler built-in preprocessor symbols and
* macros
* @param stringPooler a function that returns a String from a pool
* for a given String
* @param sourceFileName the name of the source file
*/
private void mergeResultsForFile(Function<String, String> stringPooler, String sourceFileName,
IRawIndexerInfo fileResult, IRawIndexerInfo builtinDetectorsResult) {
/*
* Handling of -U and -D is ambivalent here. - The GCC man page states: '-U name
* Cancel any previous definition of name, either built in or provided with a -D
* option.' - The POSIX c99 man page states: '-U name Remove any initial
* definition of name.' We implement handling of defines according to GCC here.
*/
Map<String, String> builtinDefines = new HashMap<>(builtinDetectorsResult.getDefines());
for (String name : fileResult.getUndefines()) {
String value;
if ((value = builtinDefines.remove(name)) != null) {
if (DEBUG_ENTRIES)
System.out.printf(" Removed define: %s=%s%n", name, value); //$NON-NLS-1$
}
}
Map<String, String> effectiveDefines = Stream
.concat(builtinDefines.entrySet().stream(), fileResult.getDefines().entrySet().stream())
.collect(Collectors.toMap(stringPooler.compose(Map.Entry::getKey),
stringPooler.compose(Map.Entry::getValue)));
List<String> includePaths = Stream
.concat(fileResult.getIncludePaths().stream(), builtinDetectorsResult.getIncludePaths().stream())
.map(stringPooler).collect(Collectors.toList());
List<String> systemIncludePaths = Stream
.concat(fileResult.getSystemIncludePaths().stream(),
builtinDetectorsResult.getSystemIncludePaths().stream())
.map(stringPooler).collect(Collectors.toList());
List<String> macroFiles = fileResult.getMacroFiles();
List<String> includeFiles = fileResult.getIncludeFiles();
// feed the paths and defines with the file name to the indexer..
parseRequest.getIndexerInfoConsumer().acceptSourceFileInfo(sourceFileName, systemIncludePaths, effectiveDefines,
includePaths, macroFiles, includeFiles);
}
private static void createMarker(IResource rc, String message) throws CoreException {
IMarker marker = rc.createMarker(MARKER_ID);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_WARNING);
marker.setAttribute(IMarker.MESSAGE, message);
marker.setAttribute(IMarker.LOCATION, CompileCommandsJsonParser.class.getName());
}
/**
* Determines the parser detector that can parse the specified command-line.<br>
* Tries to be fast: That is, it tries the last known working detector first and
* will perform expensive detection required under windows only if needed.
*
* @param line the command line to process
*
* @return {@code null} if none of the detectors matched the tool name in the
* specified command-line string. Otherwise, if the tool name matches, a
* {@code ParserDetectionResult} holding the de-composed command-line is
* returned.
*/
private ParserDetectionResult fastDetermineDetector(String line) {
final IParserPreferences prefs = prefsAccess.getWorkspacePreferences();
// try last known matching detector first...
if (lastDetector != null) {
Optional<DefaultToolDetectionParticipant.MatchResult> matchResult = Optional.empty();
final IToolDetectionParticipant detector = lastDetector.getToolDetectionParticipant();
switch (lastDetector.getHow()) {
case BASENAME:
matchResult = detector.basenameMatches(line, lastDetector.isMatchBackslash());
break;
case WITH_EXTENSION:
matchResult = detector.basenameWithExtensionMatches(line, lastDetector.isMatchBackslash());
break;
case WITH_VERSION:
if (prefs.getTryVersionSuffix()) {
matchResult = detector.basenameWithVersionMatches(line, lastDetector.isMatchBackslash(),
prefs.getVersionSuffixPattern());
}
break;
case WITH_VERSION_EXTENSION:
if (prefs.getTryVersionSuffix()) {
matchResult = detector.basenameWithVersionAndExtensionMatches(line, lastDetector.isMatchBackslash(),
prefs.getVersionSuffixPattern());
}
break;
default:
break;
}
if (matchResult.isPresent()) {
return new ParserDetection.ParserDetectionResult(lastDetector, matchResult.get());
} else {
lastDetector = null; // invalidate last working detector
}
}
// no working detector found, determine a new one...
String versionPattern = prefs.getTryVersionSuffix() ? prefs.getVersionSuffixPattern() : null;
ParserDetection.ParserDetectionResult result = ParserDetection.determineDetector(line, versionPattern,
File.separatorChar == '\\');
if (result != null) {
// cache last working detector
lastDetector = result.getDetectorWithMethod();
}
return result;
}
/**
* Parses the {@code compile_commands.json} file in the build directory of the
* build configuration if necessary and generates indexer information. If the JSON file did not change since the last invocation
* of this method on the same build configuration, parsing of the file will be skipped; that is:
* Method {@link IIndexerInfoConsumer#accept()} is not invoked.
* @param monitor the job's progress monitor
*
* @return {@code true} if the {@code compile_commands.json} file did change
* since the last invocation of this method, otherwise {@code false}. If
* {@code true}, new scanner information was detected and the CDT
* indexer should be notified.
* @throws CoreException
*/
public boolean parse(IProgressMonitor monitor) throws CoreException {
long start = 0;
fileResults = new HashMap<>();
builtinDetectorsToRun = new HashMap<>();
fileToBuiltinDetectorLinks = new HashMap<>();
try {
if (DEBUG_TIME) {
System.out.printf("Parsing file '%s' ...%n", //$NON-NLS-1$
parseRequest.getFile().getLocationURI().getPath());
start = System.currentTimeMillis();
}
return processJsonFile(monitor);
} finally {
parseRequest.getIndexerInfoConsumer().shutdown();
if (DEBUG_TIME) {
long end = System.currentTimeMillis();
System.out.printf("Parsed file '%s' in %dms%n", //$NON-NLS-1$
parseRequest.getFile().getLocationURI().getPath(), end - start);
}
// clean up
builtinDetectorsToRun = null;
fileResults = null;
fileToBuiltinDetectorLinks = null;
}
}
/**
* Creates a Map-key suitable to minimize the set of CompilerBuiltinsDetector to
* run.
*
* @param compilerCommand the command name of the compiler
* @param builtinDetectionArgs the compiler arguments from the command-line that
* affect built-in detection. For the GNU compilers,
* these are options like {@code --sysroot} and
* options that specify the language's standard
* ({@code -std=c++17}.
* @param sourceFileExtension the extension of the source file name
* @return a Map-key suitable to minimize the set of CompilerBuiltinsDetector to
* run
*/
@SuppressWarnings("nls")
private static String makeBuiltinsDetectorKey(String compilerCommand, List<String> builtinDetectionArgs,
String sourceFileExtension) {
switch (sourceFileExtension) {
case "C":
case "cc":
case "cpp":
case "CPP":
case "cp":
case "cxx":
case "c++":
// make sure we run built-ins detection only once for C++ files..
sourceFileExtension = "cpp";
break;
}
return compilerCommand + "#" + sourceFileExtension + "#"
// make sure we run the compiler for built-ins detection for each set of args
// that affect built-ins detection..
+ String.join(" ", builtinDetectionArgs);
}
/**
* @param file
* @param result
*/
private void rememberFileResult(String sourceFileName, IRawIndexerInfo result) {
fileResults.put(sourceFileName, result);
}
/**
* @param sourceFileName the name of the source file
* @param compilerCommand the command name of the compiler
* @param builtinDetectionArgs the compiler arguments from the command-line that
* affect built-in detection. For the GNU compilers,
* these are options like {@code --sysroot} and
* options that specify the language's standard
* ({@code -std=c++17}.
*/
private void rememberBuiltinsDetection(String sourceFileName, IBuiltinsDetectionBehavior builtinsDetectionBehavior,
String compilerCommand, List<String> builtinDetectionArgs) {
String extension = FilenameUtils.getExtension(sourceFileName);
String key = makeBuiltinsDetectorKey(compilerCommand, builtinDetectionArgs, extension);
if (!builtinDetectorsToRun.containsKey(key)) {
CompilerBuiltinsDetector detector = new CompilerBuiltinsDetector(builtinsDetectionBehavior, compilerCommand,
builtinDetectionArgs, extension);
builtinDetectorsToRun.put(key, detector);
}
// remember the built-ins detector for the source file
fileToBuiltinDetectorLinks.put(sourceFileName, key);
}
}