/******************************************************************************* * Copyright (c) 2009, 2016 Andrew Gvozdev 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: * Andrew Gvozdev - initial API and implementation *******************************************************************************/ package org.eclipse.cdt.managedbuilder.language.settings.providers; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.cdt.core.IErrorParser2; import org.eclipse.cdt.core.IMarkerGenerator; import org.eclipse.cdt.core.errorparsers.RegexErrorParser; import org.eclipse.cdt.core.errorparsers.RegexErrorPattern; import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsProvider; import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsManager; import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; /** * Abstract class for providers parsing compiler option from build command when present in 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 AbstractBuildCommandParser extends AbstractLanguageSettingsOutputScanner { public static final Object JOB_FAMILY_BUILD_COMMAND_PARSER = "org.eclipse.cdt.managedbuilder.AbstractBuildCommandParser"; //$NON-NLS-1$ private static final String ATTR_PARAMETER = "parameter"; //$NON-NLS-1$ private static final String ATTR_RESOURCE_SCOPE = "resource-scope"; //$NON-NLS-1$ private static final String VALUE_FILE_SCOPE = "per-file"; //$NON-NLS-1$ private static final String VALUE_FOLDER_SCOPE = "per-folder"; //$NON-NLS-1$ private static final String VALUE_PROJECT_SCOPE = "per-project"; //$NON-NLS-1$ private static final String LEADING_PATH_PATTERN = "\\S+[/\\\\]"; //$NON-NLS-1$ /** * "foo" * Using look-ahead and look-behind to resolve ambiguity with "\" {@link #QUOTE_BSLASH_QUOTE} */ private static final String QUOTE = "(\"(?!\\\\).*?(?<!\\\\)\")"; //$NON-NLS-1$ /** \"foo\" */ private static final String BSLASH_QUOTE = "(\\\\\".*?\\\\\")"; //$NON-NLS-1$ /** 'foo' */ private static final String SINGLE_QUOTE = "('.*?')"; //$NON-NLS-1$ /** "\"foo\"" */ private static final String QUOTE_BSLASH_QUOTE = "(\"\\\\\".*?\\\\\"\")"; //$NON-NLS-1$ private static final Pattern OPTIONS_PATTERN = Pattern.compile("-[^\\s\"'\\\\]*(\\s*(" + QUOTE +"|" + QUOTE_BSLASH_QUOTE + "|" + BSLASH_QUOTE + "|" + SINGLE_QUOTE + "|([^-\\s][^\\s]+)))?"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$< private static final int OPTION_GROUP = 0; public enum ResourceScope { FILE, FOLDER, PROJECT, } /** * Note: design patterns to keep file group the same and matching {@link #FILE_GROUP} */ @SuppressWarnings("nls") private static final String[] COMPILER_COMMAND_PATTERN_TEMPLATES = { "${COMPILER_PATTERN}.*\\s" + "()([^'\"\\s]*\\.${EXTENSIONS_PATTERN})(\\s.*)?[\r\n]*", // compiling unquoted file "${COMPILER_PATTERN}.*\\s" + "(['\"])(.*\\.${EXTENSIONS_PATTERN})\\${COMPILER_GROUPS+1}(\\s.*)?[\r\n]*" // compiling quoted file }; private static final int FILE_GROUP = 2; // cached value from properties, do not need to use in equals() and hashCode() private ResourceScope resourceScope = null; // Used to handle line continuations in the build output. private String partialLine; /** * The compiler command pattern without specifying compiler options. * The options are intended to be handled with option parsers, * see {@link #getOptionParsers()}. * This is regular expression pattern. * * @return the compiler command pattern. */ public String getCompilerPattern() { return getProperty(ATTR_PARAMETER); } /** * Set compiler command pattern for the provider. See {@link #getCompilerPattern()}. * @param commandPattern - value of the command pattern to set. * This is regular expression pattern. */ public void setCompilerPattern(String commandPattern) { setProperty(ATTR_PARAMETER, commandPattern); } /** * Sub-expression for compiler command pattern accounting for spaces, quotes etc. */ @SuppressWarnings("nls") private String getCompilerPatternExtended() { String compilerPattern = getCompilerPattern(); return "\\s*\"?("+LEADING_PATH_PATTERN+")?(" + compilerPattern + ")\"?"; } /** * @return resource scope of the entries, i.e. level in resource hierarchy where language settings entries * will be applied by the provider. Resource scope can be one of the following: * <br>- {@code AbstractBuildCommandParser.ResourceScope.FILE} - apply entries to the file being parsed. * <br>- {@code AbstractBuildCommandParser.ResourceScope.FOLDER} - apply entries to the enclosing folder. * <br>- {@code AbstractBuildCommandParser.ResourceScope.PROJECT} - apply entries to the project level. */ public ResourceScope getResourceScope() { if (resourceScope == null) { String scopeStr = getProperty(ATTR_RESOURCE_SCOPE); if (scopeStr.equals(VALUE_FILE_SCOPE)) { resourceScope = ResourceScope.FILE; } else if (scopeStr.equals(VALUE_FOLDER_SCOPE)) { resourceScope = ResourceScope.FOLDER; } else if (scopeStr.equals(VALUE_PROJECT_SCOPE)) { resourceScope = ResourceScope.PROJECT; } else { resourceScope = ResourceScope.FILE; } } return resourceScope; } /** * Set resource scope of the entries, i.e. level in resource hierarchy where language settings entries * will be applied by the provider. * * @param rcScope - resource scope can be one of the following: * <br>- {@code AbstractBuildCommandParser.ResourceScope.FILE} - apply entries to the file being parsed. * <br>- {@code AbstractBuildCommandParser.ResourceScope.FOLDER} - apply entries to the enclosing folder. * <br>- {@code AbstractBuildCommandParser.ResourceScope.PROJECT} - apply entries to the project level. */ public void setResourceScope(ResourceScope rcScope) { resourceScope = rcScope; switch (rcScope) { case FILE: setProperty(ATTR_RESOURCE_SCOPE, VALUE_FILE_SCOPE); break; case FOLDER: setProperty(ATTR_RESOURCE_SCOPE, VALUE_FOLDER_SCOPE); break; case PROJECT: setProperty(ATTR_RESOURCE_SCOPE, VALUE_PROJECT_SCOPE); break; default: setProperty(ATTR_RESOURCE_SCOPE, VALUE_FILE_SCOPE); break; } } @Override protected void setSettingEntries(List<? extends ICLanguageSettingEntry> entries) { IResource rc = null; switch (getResourceScope()) { case FILE: rc = currentResource; break; case FOLDER: if (currentResource instanceof IFile) { rc = currentResource.getParent(); } break; case PROJECT: rc = currentProject; break; default: break; } setSettingEntries(currentCfgDescription, rc, currentLanguageId, entries); } /** * Adjust count for file group taking into consideration extra groups added by {@link #getCompilerPatternExtended()}. */ private int adjustFileGroup() { return countGroups(getCompilerPatternExtended()) + FILE_GROUP; } /** * Make search pattern for compiler command based on template. */ private String makePattern(String template) { @SuppressWarnings("nls") String pattern = template .replace("${COMPILER_PATTERN}", getCompilerPatternExtended()) .replace("${EXTENSIONS_PATTERN}", getPatternFileExtensions()) .replace("${COMPILER_GROUPS+1}", Integer.toString(countGroups(getCompilerPatternExtended()) + 1)); return pattern; } @Override protected String parseResourceName(String line) { if (line == null) { return null; } for (String template : COMPILER_COMMAND_PATTERN_TEMPLATES) { String pattern = makePattern(template); Matcher fileMatcher = Pattern.compile(pattern).matcher(line); if (fileMatcher.matches()) { int fileGroup = adjustFileGroup(); String sourceFileName = fileMatcher.group(fileGroup); return sourceFileName; } } return null; } @Override protected List<String> parseOptions(String line) { if (line == null || (currentResource == null && resourceScope != ResourceScope.PROJECT)) { return null; } List<String> options = new ArrayList<String>(); Matcher optionMatcher = OPTIONS_PATTERN.matcher(line); while (optionMatcher.find()) { String option = optionMatcher.group(OPTION_GROUP); if (option!=null) { options.add(option); } } return options; } private void serializeLanguageSettingsInBackground() { ILanguageSettingsProvider wspProvider = LanguageSettingsManager.getWorkspaceProvider(getId()); ILanguageSettingsProvider rawProvider = LanguageSettingsManager.getRawProvider(wspProvider); if (rawProvider == this) { // this is workspace provider serializeLanguageSettingsInBackground(null); } else { serializeLanguageSettingsInBackground(currentCfgDescription); } } @Override public void shutdown() { // If there's an unprocessed partial line (because the last line of the build output ended // in a line-continuation character), process it. if (partialLine != null) { processLine(partialLine); partialLine = null; } serializeLanguageSettingsInBackground(); super.shutdown(); } @Override public boolean processLine(String line) { line = handleLineContinuation(line); return super.processLine(line); } /** * Handle line continuations ('\' at the end of a line, indicating that the next line is a * continuation of this one). */ private String handleLineContinuation(String line) { if (line == null) return null; // If the character preceding the '\' is also '\', it's not a line continuation - // the first '\' escapes the second. if (line.length() > 0 && line.charAt(line.length() - 1) == '\\' && (line.length() == 1 || line.charAt(line.length() - 2) != '\\')) { // Line ends in line continuation - save it for later. String fragment = line.substring(0, line.length() - 1); if (partialLine == null) { partialLine = fragment; } else { partialLine += fragment; } return null; // line will not be processed now } else if (partialLine != null) { // Line doesn't end in continuation but previous lines did - use their contents. line = partialLine + line; partialLine = null; } return line; } /** * Trivial Error Parser which allows highlighting of output lines matching the patterns * of this parser. Intended for better troubleshooting experience. * Implementers are supposed to add the error parser via extension point {@code org.eclipse.cdt.core.ErrorParser}. */ protected static abstract class AbstractBuildCommandPatternHighlighter extends RegexErrorParser implements IErrorParser2 { /** * Constructor. * @param parserId - build command parser ID specified in the extension {@code org.eclipse.cdt.core.LanguageSettingsProvider}. */ public AbstractBuildCommandPatternHighlighter(String parserId) { init(parserId); } /** * Initialize the error parser. * @param parserId - language settings provider (the build command parser) ID. */ protected void init(String parserId) { AbstractBuildCommandParser buildCommandParser = (AbstractBuildCommandParser) LanguageSettingsManager.getExtensionProviderCopy(parserId, false); if (buildCommandParser != null) { for (String template : COMPILER_COMMAND_PATTERN_TEMPLATES) { String pattern = buildCommandParser.makePattern(template); String fileExpr = "$"+buildCommandParser.adjustFileGroup(); //$NON-NLS-1$ String descExpr = "$0"; //$NON-NLS-1$ addPattern(new RegexErrorPattern(pattern, fileExpr, null, descExpr, null, IMarkerGenerator.SEVERITY_WARNING, true)); } } } @Override public int getProcessLineBehaviour() { return KEEP_LONGLINES; } } }