/******************************************************************************* * Copyright (c) 2005, 2016 IBM Corporation 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: * IBM Corporation - initial API and implementation * Sergey Prigogin (Google) * James Blackburn (Broadcom) - Bug 247838 * Andrew Gvozdev (Quoin Inc) * Dmitry Kozlov (CodeSourcery) - Build error highlighting and navigation * Alex Ruiz (Google) * Serge Beauchamp (Freescale Semiconductor) - Bug 417926 *******************************************************************************/ package org.eclipse.cdt.core; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Vector; import org.eclipse.cdt.core.errorparsers.ErrorParserNamedWrapper; import org.eclipse.cdt.core.language.settings.providers.IWorkingDirectoryTracker; import org.eclipse.cdt.core.resources.ACBuilder; import org.eclipse.cdt.internal.core.Cygwin; import org.eclipse.cdt.internal.core.IErrorMarkeredOutputStream; import org.eclipse.cdt.internal.core.ProblemMarkerFilterManager; import org.eclipse.cdt.internal.core.resources.ResourceLookup; import org.eclipse.cdt.internal.errorparsers.ErrorParserExtensionManager; import org.eclipse.cdt.utils.EFSExtensionManager; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceProxy; import org.eclipse.core.resources.IResourceProxyVisitor; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.URIUtil; import org.osgi.service.prefs.BackingStoreException; /** * The purpose of ErrorParserManager is to delegate the work of error parsing * build output to {@link IErrorParser}s, assist in finding {@link IResource}s, and * help create appropriate error/warning/info markers to be displayed * by the Problems view. * * @noextend This class is not intended to be subclassed by clients. */ public class ErrorParserManager extends OutputStream implements IConsoleParser, IWorkingDirectoryTracker { /** * The list of error parsers stored in .project for 3.X projects * as key/value pair with key="org.eclipse.cdt.core.errorOutputParser" * @deprecated since CDT 4.0. */ @Deprecated public final static String PREF_ERROR_PARSER = CCorePlugin.PLUGIN_ID + ".errorOutputParser"; //$NON-NLS-1$ /** * Delimiter for error parsers presented in one string. * @since 5.2 */ public final static char ERROR_PARSER_DELIMITER = ';'; /** * @since 5.4 */ public static final String BUILD_CONTEXT = "build"; //$NON-NLS-1$ private int nOpens; private int lineCounter=0; private final IProject fProject; private final IMarkerGenerator fMarkerGenerator; private Map<String, IErrorParser[]> fErrorParsers; private final List<ProblemMarkerInfo> fErrors; private final Vector<URI> fDirectoryStack; private final URI fBaseDirectoryURI; private String previousLine; private OutputStream outputStream; private final StringBuilder currentLine = new StringBuilder(); private final StringBuilder scratchBuffer = new StringBuilder(); private boolean hasErrors = false; private String cachedFileName = null; private URI cachedWorkingDirectory = null; private IFile cachedFile = null; private static boolean isCygwin = true; /** * Constructor. * * @param builder - project builder. */ public ErrorParserManager(ACBuilder builder) { this(builder.getProject(), builder); } /** * Constructor. * * @param project - project being built. * @param markerGenerator - marker generator able to create markers. */ public ErrorParserManager(IProject project, IMarkerGenerator markerGenerator) { this(project, markerGenerator, null); } /** * Constructor. * * @param project - project being built. * @param markerGenerator - marker generator able to create markers. * @param parsersIDs - array of error parsers' IDs. */ public ErrorParserManager(IProject project, IMarkerGenerator markerGenerator, String[] parsersIDs) { this(project, (URI)null, markerGenerator, parsersIDs); } /** * Constructor. * * @param project - project being built. * @param workingDirectory - IPath location of the working directory of where the build is performed. * @param markerGenerator - marker generator able to create markers. * @param parsersIDs - array of error parsers' IDs. * @deprecated use {@link #ErrorParserManager(IProject, URI, IMarkerGenerator, String[])} instead */ @Deprecated public ErrorParserManager(IProject project, IPath workingDirectory, IMarkerGenerator markerGenerator, String[] parsersIDs) { this(project, (workingDirectory == null || workingDirectory.isEmpty()) ? null : org.eclipse.core.filesystem.URIUtil.toURI(workingDirectory), markerGenerator, parsersIDs); } /** * URI based constructor. * * @param project - project being built. * @param baseDirectoryURI - absolute location URI of working directory of where the build is performed. * @param markerGenerator - marker generator able to create markers. * @param parsersIDs - array of error parsers' IDs. * @since 5.1 */ public ErrorParserManager(IProject project, URI baseDirectoryURI, IMarkerGenerator markerGenerator, String[] parsersIDs) { fProject = project; fMarkerGenerator = markerGenerator; fDirectoryStack = new Vector<URI>(); fErrors = new ArrayList<ProblemMarkerInfo>(); enableErrorParsers(parsersIDs); if (baseDirectoryURI != null) { fBaseDirectoryURI = baseDirectoryURI; } else if (project != null) { fBaseDirectoryURI = project.getLocationURI(); } else { fBaseDirectoryURI = org.eclipse.core.filesystem.URIUtil.toURI(System.getProperty("user.dir")); // CWD //$NON-NLS-1$ } } private void enableErrorParsers(String[] parserIDs) { if (parserIDs == null) { parserIDs = ErrorParserExtensionManager.getDefaultErrorParserIds(); } fErrorParsers = new LinkedHashMap<String, IErrorParser[]>(parserIDs.length); for (String parsersID : parserIDs) { IErrorParser errorParser = getErrorParserCopy(parsersID); if (errorParser!=null) { fErrorParsers.put(parsersID, new IErrorParser[] {errorParser} ); } } } /** * @return current project. */ public IProject getProject() { return fProject; } /** * @return current working directory location where build is being performed. * @deprecated use {@link #getWorkingDirectoryURI()} instead */ @Deprecated public IPath getWorkingDirectory() { return org.eclipse.core.filesystem.URIUtil.toPath(getWorkingDirectoryURI()); } /** * @return the current URI location where the build is being performed * @since 5.1 */ @Override public URI getWorkingDirectoryURI() { if (!fDirectoryStack.isEmpty()) { return fDirectoryStack.lastElement(); } // Fall back to the Project Location / Build directory return fBaseDirectoryURI; } /** * {@link #pushDirectory} and {@link #popDirectory} are used to change working directory * from where file name is searched (see {@link #findFileInWorkspace}). * The intention is to handle make output of commands "pushd dir" and "popd". * * @param dir - another directory level to keep in stack -- corresponding to 'pushd'. */ public void pushDirectory(IPath dir) { if (dir != null) { URI uri = toURI(dir); pushDirectoryURI(uri); } } /** * {@link #pushDirectoryURI} and {@link #popDirectoryURI} are used to change working directory * from where file name is searched (see {@link #findFileInWorkspace}). * The intention is to handle make output of commands "pushd dir" and "popd". * * @param dir - another directory level to keep in stack -- corresponding to 'pushd'. * @since 5.1 */ public void pushDirectoryURI(URI dir) { if (dir != null) { if (dir.isAbsolute()) { fDirectoryStack.addElement(dir); } else { fDirectoryStack.addElement(URIUtil.makeAbsolute(dir, getWorkingDirectoryURI())); } } } /** * {@link #pushDirectory} and {@link #popDirectory} are used to change working directory * from where file name is searched (see {@link #findFileInWorkspace}). * The intention is to handle make output of commands "pushd" and "popd". * * @return previous build directory location corresponding 'popd' command. * @deprecated use {@link #popDirectoryURI()} instead */ @Deprecated public IPath popDirectory() { return org.eclipse.core.filesystem.URIUtil.toPath(popDirectoryURI()); } /** * {@link #pushDirectoryURI(URI)} and {@link #popDirectoryURI()} are used to change working directory * from where file name is searched (see {@link #findFileInWorkspace(IPath)}). * The intention is to handle make output of commands "pushd" and "popd". * * @return previous build directory location corresponding 'popd' command. * @since 5.1 */ public URI popDirectoryURI() { int i = fDirectoryStack.size(); if (i != 0) { URI dir = fDirectoryStack.lastElement(); fDirectoryStack.removeElementAt(i - 1); return dir; } return fBaseDirectoryURI; } /** * @return number of directories in the stack. */ public int getDirectoryLevel() { return fDirectoryStack.size(); } /** * This function used to populate member fFilesInProject which is not necessary * anymore. Now {@link ResourceLookup} is used for search and not collection of files * kept by {@code ErrorParserManager}. * @param parent - project. * @param result - resulting collection of files. * * @deprecated Use {@link #findFileName} for searches. */ @Deprecated protected void collectFiles(IProject parent, final List<IResource> result) { try { parent.accept(new IResourceProxyVisitor() { @Override public boolean visit(IResourceProxy proxy) { if (proxy.getType() == IResource.FILE) { result.add(proxy.requestResource()); return false; } return true; } }, IResource.NONE); } catch (CoreException e) { CCorePlugin.log(e.getStatus()); } } /** * Parses one line of output and generates error or warning markers. * @since 5.4 */ @Override public boolean processLine(String line) { String lineTrimmed = line.trim(); lineCounter++; ProblemMarkerInfo marker=null; outer: for (IErrorParser[] parsers : fErrorParsers.values()) { for (IErrorParser parser : parsers) { IErrorParser curr = parser; if (parser instanceof ErrorParserNamedWrapper) { curr = ((ErrorParserNamedWrapper)parser).getErrorParser(); } int types = IErrorParser2.NONE; if (curr instanceof IErrorParser2) { types = ((IErrorParser2) curr).getProcessLineBehaviour(); } if ((types & IErrorParser2.KEEP_LONGLINES) == 0) { // long lines are not given to parsers, unless it wants it if (lineTrimmed.length() > 1000) { continue; } } // standard behavior (pre 5.1) is to trim the line String lineToParse = lineTrimmed; if ((types & IErrorParser2.KEEP_UNTRIMMED) !=0 ) { // untrimmed lines lineToParse = line; } boolean consume = false; // Protect against rough parsers who may accidentally // throw an exception on a line they can't handle. // It should not stop parsing of the rest of output. try { consume = curr.processLine(lineToParse, this); } catch (Exception e){ String id = ""; //$NON-NLS-1$ if (parser instanceof IErrorParserNamed) { id = ((IErrorParserNamed) parser).getId(); } @SuppressWarnings("nls") String message = "Errorparser " + id + " failed parsing line [" + lineToParse + "]"; CCorePlugin.log(message, e); } finally { if (fErrors.size() > 0) { if (marker==null) { marker = fErrors.get(0); } fErrors.clear(); } } if (consume) { break outer; } } } outputLine(line, marker); return false; } /** * Conditionally output line to outputStream. If stream * supports error markers, use it, otherwise use conventional stream */ private void outputLine(String line, ProblemMarkerInfo marker) { String l = line + "\n"; //$NON-NLS-1$ if ( outputStream == null ) { return; } try { if ( marker != null && outputStream instanceof IErrorMarkeredOutputStream ) { IErrorMarkeredOutputStream s = (IErrorMarkeredOutputStream) outputStream; s.write(l, marker); } else { byte[] b = l.getBytes(); outputStream.write(b, 0, b.length); } } catch (IOException e) { CCorePlugin.log(e); } } /** * @return counter counting processed lines of output * @since 5.2 */ public int getLineCounter() { return lineCounter; } /** * Returns the file with the given (partial) location if that file can be uniquely identified. * Otherwise returns {@code null}. * <br><br> * The passed in String 'partialLoc' is treated as a partial filesystem location for the * resource. Resolution is attempted with the following precedence:<br> * If partialLoc is an absolute fs location:<br> * - Resolve it to an IFile in the Project<br> * - Resolve it to an IFile in the Workspace<br> * If partialLoc is a relative path:<br> * - Resolve it relative to the Current Working Directory<br> * - Resolve just the segments provided<br> * * @param partialLoc - file name could be plain file name, absolute path or partial path * @return - file in the workspace or {@code null}. */ public IFile findFileName(String partialLoc) { if (partialLoc.equals(cachedFileName) && cachedWorkingDirectory != null && org.eclipse.core.filesystem.URIUtil.equals(getWorkingDirectoryURI(), cachedWorkingDirectory)) { return cachedFile; } // To be able to parse Windows paths on Linux systems, see bug 263977 IPath path = new Path(partialLoc.replace('\\', IPath.SEPARATOR)); // Try to find exact match. If path is not absolute - searching in working directory. IFile file = findFileInWorkspace(path); // Try to find best match considering known partial path if (file==null) { path = path.setDevice(null); IFile[] files = null; if (fProject != null) { IProject[] prjs = new IProject[] { fProject }; files = ResourceLookup.findFilesByName(path, prjs, false); if (files.length == 0) { files = ResourceLookup.findFilesByName(path, prjs, /* ignoreCase */ true); } } if (files == null || files.length == 0) { IProject[] prjs = ResourcesPlugin.getWorkspace().getRoot().getProjects(); files = ResourceLookup.findFilesByName(path, prjs, false); if (files.length == 0) { files = ResourceLookup.findFilesByName(path, prjs, /* ignoreCase */ true); } } if (files.length == 1) { file = files[0]; } } // Could be cygwin path if (file==null && isCygwin && path.isAbsolute()) { file = findCygwinFile(partialLoc); } cachedFileName = partialLoc; cachedWorkingDirectory = getWorkingDirectoryURI(); cachedFile = file; return file; } /** * Find exact match in the workspace. If path is not absolute search is done in working directory. * * @param path - file path. * @return - file in the workspace or {@code null} if such a file doesn't exist */ protected IFile findFileInWorkspace(IPath path) { URI uri = toURI(path); IFile file = findFileInWorkspace(uri); return file; } /** * Find exact match in the workspace. If path is not absolute search is done in the current working directory. * * @param uri - absolute or relative URI to resolve. * @return - file in the workspace or {@code null} if such a file doesn't exist * @since 5.1 */ protected IFile findFileInWorkspace(URI uri) { if (!uri.isAbsolute()) { uri = URIUtil.makeAbsolute(uri, getWorkingDirectoryURI()); } IFile f = ResourceLookup.selectFileForLocationURI(uri, fProject); if (f != null && f.isAccessible()) { return f; } return null; } /** * @param fileName - file name. * @return {@code true} if the project contains more than one file with the given name. * * @deprecated Use {@link #findFileName} for searches. */ @Deprecated public boolean isConflictingName(String fileName) { return ResourceLookup.findFilesByName(new Path(fileName), new IProject[] {fProject}, false).length > 1; } /** * Called by the error parsers to find an IFile for a given * external filesystem 'location' * * @param filePath - file path. * @return IFile representing the external location, or null if one * couldn't be found. * * @deprecated Use {@link #findFileName} for searches. */ @Deprecated public IFile findFilePath(String filePath) { IPath path = new Path(filePath); IFile file = findFileInWorkspace(path); // That didn't work, see if it is a cygpath if (file == null && isCygwin) { file = findCygwinFile(filePath); } return (file != null && file.exists()) ? file : null; } private IFile findCygwinFile(String filePath) { IFile file=null; try { IPath path = new Path(Cygwin.cygwinToWindowsPath(filePath)); file = findFileInWorkspace(path); } catch (UnsupportedOperationException e) { isCygwin = false; } catch (Exception e) { } return file; } /** * Add marker to the list of error markers. * * @param file - resource to add the new marker. * @param lineNumber - line number of the error. * @param desc - description of the error. * @param severity - severity of the error. * @param varName - variable name. */ public void generateMarker(IResource file, int lineNumber, String desc, int severity, String varName) { generateExternalMarker(file, lineNumber, desc, severity, varName, null); } /** * Add marker to the list of error markers. * * @param file - resource to add the new marker. * @param lineNumber - line number of the error. * @param desc - description of the error. * @param severity - severity of the error, one of * <br>{@link IMarkerGenerator#SEVERITY_INFO}, * <br>{@link IMarkerGenerator#SEVERITY_WARNING}, * <br>{@link IMarkerGenerator#SEVERITY_ERROR_RESOURCE}, * <br>{@link IMarkerGenerator#SEVERITY_ERROR_BUILD} * @param varName - variable name. * @param externalPath - external path pointing to a file outside the workspace. */ public void generateExternalMarker(IResource file, int lineNumber, String desc, int severity, String varName, IPath externalPath) { if (file == null) file = fProject; ProblemMarkerInfo problemMarkerInfo = new ProblemMarkerInfo(file, lineNumber, desc, severity, varName, externalPath); this.addProblemMarker(problemMarkerInfo); } /** * Add the given marker to the list of error markers. * * @param problemMarkerInfo - The marker to be added. * @since 5.4 */ public void addProblemMarker(ProblemMarkerInfo problemMarkerInfo){ if ( ! ProblemMarkerFilterManager.getInstance().acceptMarker(problemMarkerInfo) ) return; fErrors.add(problemMarkerInfo); fMarkerGenerator.addMarker(problemMarkerInfo); if (problemMarkerInfo.severity == IMarkerGenerator.SEVERITY_ERROR_RESOURCE) { hasErrors = true; } } /** * Called by the error parsers. * @return the previous line, save in the working buffer. */ public String getPreviousLine() { return previousLine == null ? "" : previousLine; //$NON-NLS-1$ } /** * Method setOutputStream. * Note: you have to close this stream explicitly * don't rely on ErrorParserManager.close(). * @param os - output stream */ public void setOutputStream(OutputStream os) { outputStream = os; } /** * Method getOutputStream. * Note: you have to close this stream explicitly * don't rely on ErrorParserManager.close(). * @return OutputStream */ public OutputStream getOutputStream() { nOpens++; return this; } /** * @see java.io.OutputStream#close() * Note: don't rely on this method to close underlying OutputStream, * close it explicitly */ @Override public synchronized void close() throws IOException { if (nOpens > 0 && --nOpens == 0) { checkLine(true); fDirectoryStack.removeAllElements(); } } /** * @see java.io.OutputStream#flush() */ @Override public void flush() throws IOException { if (outputStream != null) { outputStream.flush(); } } /** * @see java.io.OutputStream#write(int) */ @Override public synchronized void write(int b) throws IOException { currentLine.append((char) b); checkLine(false); } @Override public synchronized void write(byte[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if (off != 0 || (len < 0) || (len > b.length)) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return; } currentLine.append(new String(b, 0, len)); checkLine(false); } // This method examines contents of currentLine buffer // if it contains whole line this line is checked by error // parsers (processLine method). // If flush is true rest of line is checked by error parsers. private void checkLine(boolean flush) { String buffer = currentLine.toString(); int i = 0; while ((i = buffer.indexOf('\n')) != -1) { String line = buffer.substring(0, i); // get rid of any trailing '\r' if (line.endsWith("\r")) { //$NON-NLS-1$ line=line.substring(0,line.length()-1); } processLine(line); previousLine = line; buffer = buffer.substring(i + 1); // skip the \n and advance } currentLine.setLength(0); if (flush) { if (buffer.length() > 0) { processLine(buffer); previousLine = buffer; } } else { currentLine.append(buffer); } } /** * @deprecated as of 5.2. This method is no longer reporting problems. * The problem markers are generated after processing each line. * * @return {@code true} if detected a problem indicating that build failed. * The semantics of the return code is inconsistent. As far as build is concerned * there is no difference between errors * {@link IMarkerGenerator#SEVERITY_ERROR_RESOURCE} and * {@link IMarkerGenerator#SEVERITY_ERROR_BUILD} */ @Deprecated public boolean reportProblems() { return false; } /** * Converts a location {@link IPath} to an {@link URI}. * The returned URI uses the scheme and authority of the current working directory * as returned by {@link #getWorkingDirectoryURI()} * * @param path - the path to convert to URI. * @return URI * @since 5.1 */ private URI toURI(IPath path) { URI uri = null; URI workingDirectoryURI = getWorkingDirectoryURI(); if (path.isAbsolute()) { uri = EFSExtensionManager.getDefault().createNewURIFromPath(workingDirectoryURI, path.toString()); } else { uri = EFSExtensionManager.getDefault().append(workingDirectoryURI, path.toString()); } return uri; } /** * @return scratch buffer. * @deprecated Use IErrorParser2 interface to handle multiline messages rather than scratch buffer. */ @Deprecated public String getScratchBuffer() { return scratchBuffer.toString(); } /** * @param line - input line. * @deprecated Use IErrorParser2 interface to handle multiline messages rather than scratch buffer. */ @Deprecated public void appendToScratchBuffer(String line) { scratchBuffer.append(line); } /** * @deprecated Use IErrorParser2 interface to handle multiline messages rather than scratch buffer. */ @Deprecated public void clearScratchBuffer() { scratchBuffer.setLength(0); } /** * @return {@code true} if errors attributed to resources detected * * @deprecated The semantics of this function is inconsistent. As far as build is concerned * there is no difference between errors * {@link IMarkerGenerator#SEVERITY_ERROR_RESOURCE} and * {@link IMarkerGenerator#SEVERITY_ERROR_BUILD} */ @Deprecated public boolean hasErrors() { return hasErrors; } /** * @return default error parsers IDs to be used if error parser list is empty. * @since 5.3 */ public static String[] getUserDefinedErrorParserIds() { return ErrorParserExtensionManager.getUserDefinedErrorParserIds(); } /** * Set and store in workspace area user defined error parsers. * * @param errorParsers - array of user defined error parsers * @throws CoreException in case of problems * @since 5.2 */ public static void setUserDefinedErrorParsers(IErrorParserNamed[] errorParsers) throws CoreException { ErrorParserExtensionManager.setUserDefinedErrorParsers(errorParsers); } /** * @return available error parsers IDs which include contributed through extension + user defined ones * from workspace * @since 5.2 */ public static String[] getErrorParserAvailableIds() { return ErrorParserExtensionManager.getErrorParserAvailableIds(); } /** * @param context - indicates the context in which an error parser can be used. * @return available error parsers ID, which include contributed through extension and user- * defined ones from workspace, that can be used in the given context. * @since 5.4 */ public static String[] getErrorParserAvailableIdsInContext(String context) { return ErrorParserExtensionManager.getErrorParserAvailableIdsInContext(context); } /** * @return IDs of error parsers contributed through error parser extension point. * @since 5.2 */ public static String[] getErrorParserExtensionIds() { return ErrorParserExtensionManager.getErrorParserExtensionIds(); } /** * Set and store default error parsers IDs to be used if error parser list is empty. * * @param ids - default error parsers IDs * @throws BackingStoreException in case of problem with storing * @since 5.2 */ public static void setDefaultErrorParserIds(String[] ids) throws BackingStoreException { ErrorParserExtensionManager.setDefaultErrorParserIds(ids); } /** * @return default error parsers IDs to be used if error parser list is empty. * @since 5.2 */ public static String[] getDefaultErrorParserIds() { return ErrorParserExtensionManager.getDefaultErrorParserIds(); } /** * @param id - ID of error parser * @return cloned copy of error parser or {@code null}. * Note that {@link ErrorParserNamedWrapper} returns shallow copy with the same instance * of underlying error parser. * @since 5.2 */ public static IErrorParserNamed getErrorParserCopy(String id) { return ErrorParserExtensionManager.getErrorParserCopy(id, false); } /** * @param id - ID of error parser * @return cloned copy of error parser as defined by its extension point or {@code null}. * @since 5.3 */ public static IErrorParserNamed getErrorParserExtensionCopy(String id) { return ErrorParserExtensionManager.getErrorParserCopy(id, true); } /** * @param ids - array of error parser IDs * @return error parser IDs delimited with error parser delimiter ";" * @since 5.2 */ public static String toDelimitedString(String[] ids) { String result=""; //$NON-NLS-1$ for (String id : ids) { if (result.length()==0) { result = id; } else { result += ERROR_PARSER_DELIMITER + id; } } return result; } /** * @since 5.4 */ @Override public void shutdown() { for (IErrorParser[] parsers : fErrorParsers.values()) { for (IErrorParser parser : parsers) { if (parser instanceof IErrorParser3) { ((IErrorParser3) parser).shutdown(); } } } } }