/**
* Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
/*
* Created on Apr 6, 2006
*/
package com.python.pydev.analysis.builder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jface.text.IDocument;
import org.python.pydev.builder.PyDevBuilderPrefPage;
import org.python.pydev.builder.PyDevBuilderVisitor;
import org.python.pydev.builder.pylint.IPyLintVisitor;
import org.python.pydev.builder.pylint.PyLintVisitorFactory;
import org.python.pydev.core.IMiscConstants;
import org.python.pydev.core.IModule;
import org.python.pydev.core.IPythonNature;
import org.python.pydev.core.MisconfigurationException;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.PyEdit;
import org.python.pydev.editor.autoedit.DefaultIndentPrefs;
import org.python.pydev.editor.codecompletion.revisited.modules.SourceModule;
import org.python.pydev.editor.correctionassist.CheckAnalysisErrors;
import org.python.pydev.logging.DebugSettings;
import org.python.pydev.shared_core.callbacks.ICallback;
import org.python.pydev.shared_ui.utils.PyMarkerUtils;
import org.python.pydev.shared_ui.utils.PyMarkerUtils.MarkerInfo;
import com.python.pydev.analysis.AnalysisPreferences;
import com.python.pydev.analysis.IAnalysisPreferences;
import com.python.pydev.analysis.OccurrencesAnalyzer;
import com.python.pydev.analysis.additionalinfo.AbstractAdditionalTokensInfo;
import com.python.pydev.analysis.additionalinfo.AdditionalProjectInterpreterInfo;
import com.python.pydev.analysis.messages.IMessage;
/**
* This class is used to do analysis on a thread, so that if an analysis is asked for some analysis that
* is already in progress, that analysis will be stopped and this one will begin.
*
* @author Fabio
*/
public class AnalysisBuilderRunnable extends AbstractAnalysisBuilderRunnable {
/**
* These are the callbacks called whenever there's a run to be done in this class.
*/
public static final List<ICallback<Object, IResource>> analysisBuilderListeners = new ArrayList<ICallback<Object, IResource>>();
// -------------------------------------------------------------------------------------------- ATTRIBUTES
private IDocument document;
private IResource resource;
private ICallback<IModule, Integer> module;
private int moduleRequest;
private IPyLintVisitor pyLintVisitor;
private boolean onlyRecreateCtxInsensitiveInfo;
// ---------------------------------------------------------------------------------------- END ATTRIBUTES
/**
* Versions before eclipse 3.4 don't have an isDerived(IResource.CHECK_ANCESTORS)
*/
private static boolean useEclipse32DerivedVersion = false;
/**
* Checks if some resource is hierarchically derived (if any parent is derived, it's also derived).
*/
private static boolean isHierarchicallyDerived(IResource curr) {
if (useEclipse32DerivedVersion) {
do {
if (curr.isDerived()) {
return true;
}
curr = curr.getParent();
} while (curr != null);
return false;
} else {
try {
return curr.isDerived(IResource.CHECK_ANCESTORS);
} catch (Throwable e) {
useEclipse32DerivedVersion = true;
return isHierarchicallyDerived(curr);
}
}
}
/**
* @param oldAnalysisBuilderThread This is an existing runnable that was already analyzing things... we must wait for it
* to finish to start it again.
*
* @param module: this is a callback that'll be called with a boolean that should return the IModule to be used in the
* analysis.
* The parameter is FULL_MODULE or DEFINITIONS_MODULE
*/
/*Default*/ AnalysisBuilderRunnable(IDocument document, IResource resource, ICallback<IModule, Integer> module,
boolean isFullBuild, String moduleName, boolean forceAnalysis, int analysisCause,
IAnalysisBuilderRunnable oldAnalysisBuilderThread, IPythonNature nature, long documentTime,
KeyForAnalysisRunnable key, long resourceModificationStamp) {
super(isFullBuild, moduleName, forceAnalysis, analysisCause, oldAnalysisBuilderThread, nature, documentTime,
key, resourceModificationStamp);
if (resource == null) {
Log.toLogFile(this, "Unexpected null resource for: " + moduleName);
return;
}
this.document = document;
this.resource = resource;
this.module = module;
this.pyLintVisitor = PyLintVisitorFactory.create(resource, document, module, internalCancelMonitor);
// Important: we can only update the index if it was a builder... if it was the parser,
// we can't update it otherwise we could end up with data that's not saved in the index.
boolean updateIndex = analysisCause == ANALYSIS_CAUSE_BUILDER;
// Previously we did this in a thread, but updating the indexes in a thread made things too
// unreliable for the index (it was not uncommon for it to become unsynchronized as we can't
// guarantee the order of operations).
// So, this process is now synchronous (just the code-analysis is done in a thread now).
try {
onlyRecreateCtxInsensitiveInfo = !forceAnalysis && analysisCause == ANALYSIS_CAUSE_BUILDER
&& PyDevBuilderPrefPage.getAnalyzeOnlyActiveEditor();
if (!onlyRecreateCtxInsensitiveInfo) {
//if not a source folder, we'll just want to recreate the context insensitive information
if (!nature.isResourceInPythonpathProjectSources(resource, false)) {
onlyRecreateCtxInsensitiveInfo = true;
}
}
AbstractAdditionalTokensInfo info = AdditionalProjectInterpreterInfo.getAdditionalInfoForProject(nature);
if (info == null) {
Log.log("Unable to get additional info for: " + resource + " -- " + moduleName);
return;
}
//remove dependency information (and anything else that was already generated)
if (!isFullBuild && updateIndex) {
//if it is a full build, that info is already removed
AnalysisBuilderRunnableForRemove.removeInfoForModule(moduleName, nature, isFullBuild);
}
if (onlyRecreateCtxInsensitiveInfo) {
moduleRequest = DEFINITIONS_MODULE;
} else {
moduleRequest = FULL_MODULE;
}
//recreate the ctx insensitive info
if (updateIndex) {
recreateCtxInsensitiveInfo(info, (SourceModule) this.module.call(moduleRequest), nature, resource);
}
} catch (MisconfigurationException | CoreException e) {
Log.log(e);
}
}
@Override
protected void dispose() {
super.dispose();
this.document = null;
this.resource = null;
this.module = null;
}
@Override
protected void doAnalysis() {
if (!nature.startRequests()) {
return;
}
try {
if (DebugSettings.DEBUG_ANALYSIS_REQUESTS) {
Log.toLogFile(this, "doAnalysis() - " + moduleName + " " + this.getAnalysisCauseStr());
}
//if the resource is not open, there's not much we can do...
final IResource r = resource;
if (r == null) {
Log.toLogFile(this, "Finished analysis -- resource null -- " + moduleName);
return;
}
if (!r.getProject().isOpen()) {
Log.toLogFile(this, "Finished analysis -- project closed -- " + moduleName);
return;
}
AnalysisRunner runner = new AnalysisRunner();
checkStop();
IAnalysisPreferences analysisPreferences = new AnalysisPreferences(r);
boolean makeAnalysis = runner.canDoAnalysis(document) && PyDevBuilderVisitor.isInPythonPath(r) && //just get problems in resources that are in the pythonpath
analysisPreferences.makeCodeAnalysis();
if (!makeAnalysis) {
//let's see if we should do code analysis
AnalysisRunner.deleteMarkers(r);
if (DebugSettings.DEBUG_ANALYSIS_REQUESTS) {
Log.toLogFile(this, "Skipping: !makeAnalysis -- " + moduleName);
}
return;
}
if (onlyRecreateCtxInsensitiveInfo) {
if (DebugSettings.DEBUG_ANALYSIS_REQUESTS) {
Log.toLogFile(this, "Skipping: !forceAnalysis && analysisCause == ANALYSIS_CAUSE_BUILDER && "
+ "PyDevBuilderPrefPage.getAnalyzeOnlyActiveEditor() -- " + moduleName);
}
return;
}
if (nature == null) {
Log.log("Finished analysis: null nature -- " + moduleName);
return;
}
AbstractAdditionalTokensInfo info = AdditionalProjectInterpreterInfo.getAdditionalInfoForProject(nature);
if (info == null) {
Log.log("Unable to get additional info for: " + r + " -- " + moduleName);
return;
}
if (DebugSettings.DEBUG_ANALYSIS_REQUESTS) {
Log.toLogFile(this, "makeAnalysis:" + makeAnalysis + " " + "analysisCause: " + getAnalysisCauseStr()
+ " -- " + moduleName);
}
checkStop();
if (isHierarchicallyDerived(r)) {
if (DebugSettings.DEBUG_ANALYSIS_REQUESTS) {
Log.toLogFile(this, "Resource marked as derived not analyzed: " + r + " -- " + moduleName);
}
//We don't want to check derived resources (but we want to remove any analysis messages that
//might be already there)
if (r != null) {
runner.setMarkers(r, document, new IMessage[0], this.internalCancelMonitor);
pyLintVisitor.deleteMarkers();
}
return;
}
// Currently, the PyLint visitor can only analyze the contents saved, so, if the contents on the doc
// changed in the meanwhile, skip doing this visit for PyLint.
// Maybe we can improve that when https://github.com/PyCQA/pylint/pull/1189 is done.
if (!MarkEditorOnSave.hasDocumentChanged(resource, document)) {
pyLintVisitor.startVisit();
} else {
pyLintVisitor.deleteMarkers();
}
OccurrencesAnalyzer analyzer = new OccurrencesAnalyzer();
checkStop();
SourceModule module = (SourceModule) this.module.call(moduleRequest);
IMessage[] messages = analyzer.analyzeDocument(nature, module, analysisPreferences, document,
this.internalCancelMonitor, DefaultIndentPrefs.get(this.resource));
checkStop();
if (DebugSettings.DEBUG_ANALYSIS_REQUESTS) {
Log.toLogFile(this, "Adding markers for module: " + moduleName);
//for (IMessage message : messages) {
// Log.toLogFile(this, message.toString());
//}
}
//last chance to stop...
checkStop();
List<MarkerInfo> markersFromCodeAnalysis = null;
//don't stop after setting to add / remove the markers
if (r != null) {
boolean analyzeOnlyActiveEditor = PyDevBuilderPrefPage.getAnalyzeOnlyActiveEditor();
if (forceAnalysis
|| !analyzeOnlyActiveEditor
|| (analyzeOnlyActiveEditor
&& (!PyDevBuilderPrefPage.getRemoveErrorsWhenEditorIsClosed() || PyEdit
.isEditorOpenForResource(r)))) {
markersFromCodeAnalysis = runner.setMarkers(r, document, messages, this.internalCancelMonitor);
} else {
if (DebugSettings.DEBUG_ANALYSIS_REQUESTS) {
Log.toLogFile(this, "Skipped adding markers for module: " + moduleName
+ " (editor not opened).");
}
}
}
//if there are callbacks registered, call them if we still didn't return (mostly for tests)
for (ICallback<Object, IResource> callback : analysisBuilderListeners) {
try {
callback.call(r);
} catch (Exception e) {
Log.log(e);
}
}
checkStop();
pyLintVisitor.join();
checkStop();
if (r != null) {
List<MarkerInfo> markersFromPyLint = pyLintVisitor.getMarkers();
if (markersFromPyLint != null && markersFromPyLint.size() > 0) {
Map<Integer, List<MarkerInfo>> lineToMarkerInfo = new HashMap<>();
if (markersFromCodeAnalysis != null) {
for (MarkerInfo codeAnalysisMarkerInfo : markersFromCodeAnalysis) {
List<MarkerInfo> list = lineToMarkerInfo.get(codeAnalysisMarkerInfo.lineStart);
if (list == null) {
list = new ArrayList<>(2);
lineToMarkerInfo.put(codeAnalysisMarkerInfo.lineStart, list);
}
list.add(codeAnalysisMarkerInfo);
}
}
// I.e.: if the error is already generated in the PyDev code-analysis, skip the same error on PyLint
// (there's no real point in putting an error twice).
for (Iterator<MarkerInfo> pyLintMarkerInfoIterator = markersFromPyLint
.iterator(); pyLintMarkerInfoIterator
.hasNext();) {
MarkerInfo pyLintMarkerInfo = pyLintMarkerInfoIterator.next();
List<MarkerInfo> codeAnalysisMarkers = lineToMarkerInfo.get(pyLintMarkerInfo.lineStart);
if (codeAnalysisMarkers != null && codeAnalysisMarkers.size() > 0) {
for (MarkerInfo codeAnalysisMarker : codeAnalysisMarkers) {
if (codeAnalysisMarker.severity < IMarker.SEVERITY_INFO) {
// Don't consider if it shouldn't be shown.
continue;
}
Map<String, Object> additionalInfo = codeAnalysisMarker.additionalInfo;
if (additionalInfo != null) {
Object analysisType = additionalInfo.get(AnalysisRunner.PYDEV_ANALYSIS_TYPE);
if (analysisType != null && analysisType instanceof Integer) {
String pyLintMessageId = CheckAnalysisErrors
.getPyLintMessageIdForPyDevAnalysisType((int) analysisType);
if (pyLintMessageId != null
&& pyLintMessageId.equals(pyLintMarkerInfo.additionalInfo
.get(IMiscConstants.PYLINT_MESSAGE_ID))) {
pyLintMarkerInfoIterator.remove();
break; // Stop the for (we've already removed it).
}
}
}
}
}
}
PyMarkerUtils.replaceMarkers(markersFromPyLint, resource, IMiscConstants.PYLINT_PROBLEM_MARKER,
true, this.internalCancelMonitor);
} else {
pyLintVisitor.deleteMarkers();
}
}
} catch (OperationCanceledException e) {
//ok, ignore it
logOperationCancelled();
} catch (Exception e) {
Log.log(e);
} finally {
try {
nature.endRequests();
} catch (Throwable e) {
Log.log("Error when analyzing: " + moduleName, e);
}
try {
AnalysisBuilderRunnableFactory.removeFromThreads(key, this);
} catch (Throwable e) {
Log.log(e);
}
dispose();
}
}
/**
* @return false if there's no modification among the current version of the file and the last version analyzed.
*/
private void recreateCtxInsensitiveInfo(AbstractAdditionalTokensInfo info, SourceModule sourceModule,
IPythonNature nature, IResource r) {
//info.removeInfoFromModule(sourceModule.getName()); -- does not remove info from the module because this
//should be already done once it gets here (the AnalysisBuilder, that also makes dependency info
//should take care of this).
boolean generateDelta;
if (isFullBuild) {
generateDelta = false;
} else {
generateDelta = true;
}
info.addAstInfo(sourceModule.getAst(), sourceModule.getModulesKey(), generateDelta);
}
}