PY-18525 Suggest Python compatibility inspection for compatible-looking code

This commit is contained in:
Mikhail Golubev
2016-03-03 01:07:25 +03:00
committed by Andrey Vlasovskikh
parent 6c1d3b4de5
commit 0db5ac3c11
5 changed files with 325 additions and 39 deletions
@@ -19,6 +19,7 @@ import com.google.common.collect.ImmutableList;
import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -52,7 +53,7 @@ public enum LanguageLevel {
.build();
private static final LanguageLevel DEFAULT2 = PYTHON27;
private static final LanguageLevel DEFAULT3 = PYTHON34;
private static final LanguageLevel DEFAULT3 = PYTHON35;
public static LanguageLevel FORCE_LANGUAGE_LEVEL = null;
@@ -158,6 +159,12 @@ public enum LanguageLevel {
return getDefault();
}
@NotNull
public static LanguageLevel getLatest() {
//noinspection ConstantConditions
return ArrayUtil.getLastElement(values());
}
@Override
public String toString() {
return myVersion / 10 + "." + myVersion % 10;
@@ -43,6 +43,7 @@
<highlightRangeExtension implementation="com.jetbrains.python.validation.DumbAwareHighlightingAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyAnnotatingVisitor"/>
<annotator language="Python" implementationClass="com.jetbrains.python.validation.PyDumbAwareAnnotator"/>
<annotator language="Python" implementationClass="com.jetbrains.python.inspections.PyCompatibilityInspectionAdvertiser"/>
<quoteHandler fileType="Python" className="com.jetbrains.python.editor.PythonQuoteHandler"/>
<enterHandlerDelegate implementation="com.jetbrains.python.editor.PythonEnterHandler"/>
<enterHandlerDelegate implementation="com.jetbrains.python.editor.PyEnterAtIndentHandler" order="first"/>
@@ -93,6 +94,8 @@
serviceImplementation="com.jetbrains.python.psi.types.TypeEvalContextCacheImpl"
/>
<projectService serviceImplementation="com.jetbrains.python.inspections.PyCompatibilityInspectionAdvertiserSettings"/>
<applicationService serviceInterface="com.jetbrains.python.packaging.PyPackageService"
serviceImplementation="com.jetbrains.python.packaging.PyPackageService"/>
<applicationService serviceInterface="com.jetbrains.python.packaging.PyCondaPackageService"
@@ -15,23 +15,26 @@
*/
package com.jetbrains.python.inspections;
import com.google.common.collect.ImmutableList;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.ide.util.ElementsChooser;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.JDOMExternalizableStringList;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.QualifiedName;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.psi.*;
@@ -47,8 +50,6 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.util.*;
import java.util.List;
@@ -59,15 +60,24 @@ import java.util.List;
* Inspection to detect code incompatibility with python versions
*/
public class PyCompatibilityInspection extends PyInspection {
public static final int LATEST_INSPECTION_VERSION = 1;
public static final List<LanguageLevel> DEFAULT_PYTHON_VERSIONS = ImmutableList.of(LanguageLevel.PYTHON27, LanguageLevel.getLatest());
public JDOMExternalizableStringList ourVersions = new JDOMExternalizableStringList();
// Legacy DefaultJDOMExternalizer requires public fields for proper serialization
public JDOMExternalizableStringList ourVersions = new JDOMExternalizableStringList(ContainerUtil.map(DEFAULT_PYTHON_VERSIONS, LanguageLevel::toString));
public PyCompatibilityInspection () {
super();
if (ApplicationManager.getApplication().isUnitTestMode()) {
ourVersions.addAll(UnsupportedFeaturesUtil.ALL_LANGUAGE_LEVELS);
}
}
@Nullable
public static PyCompatibilityInspection getInstance(@NotNull PsiElement element) {
final InspectionProfile inspectionProfile = InspectionProjectProfileManager.getInstance(element.getProject()).getInspectionProfile();
final String toolName = PyCompatibilityInspection.class.getSimpleName();
return (PyCompatibilityInspection)inspectionProfile.getUnwrappedTool(toolName, element);
}
@Override
public boolean isEnabledByDefault() {
@@ -93,42 +103,21 @@ public class PyCompatibilityInspection extends PyInspection {
@Override
public JComponent createOptionsPanel() {
final JPanel versionPanel = new JPanel(new BorderLayout());
final JBList list = new JBList(UnsupportedFeaturesUtil.ALL_LANGUAGE_LEVELS);
JLabel label = new JLabel("Check for compatibility with python versions:");
label.setLabelFor(list);
versionPanel.add(label, BorderLayout.PAGE_START);
list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
JBScrollPane scrollPane = new JBScrollPane(list, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
versionPanel.add(scrollPane);
int[] indices = new int[ourVersions.size()];
for (int i = 0; i != ourVersions.size(); ++i) {
String s = ourVersions.get(i);
indices[i] = UnsupportedFeaturesUtil.ALL_LANGUAGE_LEVELS.indexOf(s);
}
list.setSelectedIndices(indices);
list.setCellRenderer(new DefaultListCellRenderer() {
final ElementsChooser<String> chooser = new ElementsChooser<String>(true);
chooser.setElements(UnsupportedFeaturesUtil.ALL_LANGUAGE_LEVELS, false);
chooser.markElements(ourVersions);
chooser.addElementsMarkListener(new ElementsChooser.ElementsMarkListener<String>() {
@Override
public Component getListCellRendererComponent(JList list, Object o, int i, boolean b, boolean b2) {
return super
.getListCellRendererComponent(list, "Python " + o, i, b, b2);
}
});
list.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent event) {
public void elementMarkChanged(String element, boolean isMarked) {
ourVersions.clear();
for (Object value : list.getSelectedValues()) {
ourVersions.add((String)value);
}
ourVersions.addAll(chooser.getMarkedElements());
}
});
final JPanel versionPanel = new JPanel(new BorderLayout());
JLabel label = new JLabel("Check for compatibility with python versions:");
label.setLabelFor(chooser);
versionPanel.add(label, BorderLayout.PAGE_START);
versionPanel.add(chooser);
return versionPanel;
}
@@ -0,0 +1,247 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.inspections;
import com.intellij.codeInsight.daemon.HighlightDisplayKey;
import com.intellij.codeInspection.InspectionProfile;
import com.intellij.codeInspection.ex.EditInspectionToolsSettingsAction;
import com.intellij.codeInspection.ex.InspectionProfileImpl;
import com.intellij.codeInspection.ex.InspectionToolWrapper;
import com.intellij.codeInspection.ex.ScopeToolState;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.JDOMExternalizableStringList;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.psi.LanguageLevel;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PyFromImportStatement;
import com.jetbrains.python.psi.PyImportElement;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.jetbrains.python.psi.PyUtil.as;
/**
* @author Mikhail Golubev
*/
public class PyCompatibilityInspectionAdvertiser implements Annotator {
private static final String NOTIFICATIONS_TITLE = "Python Versions Compatibility";
private static final NotificationGroup BALLOON_NOTIFICATIONS = new NotificationGroup("Python Compatibility Inspection Advertiser",
NotificationDisplayType.STICKY_BALLOON, false);
private static final Key<Boolean> DONT_SHOW_BALLOON = Key.create("showingPyCompatibilityAdvertiserBalloon");
// Allow to show declined suggestion multiple times to ease debugging
private static final boolean SHOW_ONCE_FOR_VERSION = true;
@Language("HTML")
private static final String YES_NO_REFS = "<a href=\"#yes\">Yes</a>&nbsp;&nbsp;<a href=\"#no\">No</a>";
private static final String USING_FUTURE_IMPORTS = "Your source code contains __future__ imports";
private static final String USING_SIX = "Your source code imports the 'six' package";
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (element instanceof PyFile) {
final PyFile pyFile = (PyFile)element;
final Project project = element.getProject();
final Boolean showingFlag = project.getUserData(DONT_SHOW_BALLOON);
if (showingFlag != null && showingFlag.booleanValue()) {
return;
}
final int inspectionVersion = getSettings(project).version;
if (inspectionVersion < PyCompatibilityInspection.LATEST_INSPECTION_VERSION) {
if (isCompatibilityInspectionEnabled(element)) {
final LanguageLevel pyVersion = getLatestConfiguredCompatiblePython3Version(element);
if (pyVersion != null && pyVersion.isOlderThan(LanguageLevel.getLatest())) {
showStalePython3VersionWarning(pyFile, project, pyVersion);
}
}
else if (containsFutureImports(pyFile)) {
showInspectionAdvertisement(project, USING_FUTURE_IMPORTS);
}
else if (containsSixImport(pyFile)) {
showInspectionAdvertisement(project, USING_SIX);
}
}
}
}
@Nullable
private static LanguageLevel getLatestConfiguredCompatiblePython3Version(@NotNull PsiElement element) {
final LanguageLevel latestVersion = getLatestConfiguredCompatiblePythonVersion(element);
return latestVersion != null && latestVersion.isAtLeast(LanguageLevel.PYTHON30) ? latestVersion : null;
}
private static void showStalePython3VersionWarning(@NotNull PyFile file,
@NotNull Project project,
@NotNull LanguageLevel latestConfiguredVersion) {
final List<LanguageLevel> versionsToEnable = getVersionsNewerThan(latestConfiguredVersion);
final String versionsList = StringUtil.join(versionsToEnable, ",&nbsp;");
final String message =
String.format("Code compatibility inspection is configured for Python versions up to %s.<br/>" +
"Would you like to enable it for Python %s?<br/>" + YES_NO_REFS, latestConfiguredVersion, versionsList);
showSingletonNotification(project, NOTIFICATIONS_TITLE, message, NotificationType.INFORMATION, (notification, event) -> {
final boolean enabled = "#yes".equals(event.getDescription());
if (enabled) {
enableVersions(project, file, versionsToEnable);
}
if (enabled || SHOW_ONCE_FOR_VERSION) {
getSettings(project).version = PyCompatibilityInspection.LATEST_INSPECTION_VERSION;
}
});
}
@NotNull
private static List<LanguageLevel> getVersionsNewerThan(@NotNull LanguageLevel version) {
final List<LanguageLevel> result = new ArrayList<>();
final LanguageLevel latest = LanguageLevel.getLatest();
for (LanguageLevel level : LanguageLevel.values()) {
if (version.isOlderThan(level) && latest.isAtLeast(level)) {
result.add(level);
}
}
return result;
}
private static void enableVersions(@NotNull Project project, @NotNull PsiElement file, @NotNull List<LanguageLevel> versions) {
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
final String shortName = getCompatibilityInspectionShortName();
final InspectionToolWrapper tool = profile.getInspectionTool(shortName, project);
if (tool != null) {
profile.modifyProfile(model -> {
final PyCompatibilityInspection inspection = (PyCompatibilityInspection)model.getUnwrappedTool(shortName, file);
inspection.ourVersions.addAll(ContainerUtil.map(versions, LanguageLevel::toString));
});
EditInspectionToolsSettingsAction.editToolSettings(project, profile, true, shortName);
}
}
private static void showInspectionAdvertisement(@NotNull Project project, @NotNull String message) {
final String msg = message + ".<br/>Would you like to enable Code compatibility inspection?<br/>" + YES_NO_REFS;
showSingletonNotification(project, NOTIFICATIONS_TITLE, msg, NotificationType.INFORMATION, (notification, event) -> {
final boolean enabled = "#yes".equals(event.getDescription());
if (enabled) {
enableCompatibilityInspection(project);
}
if (enabled || SHOW_ONCE_FOR_VERSION) {
getSettings(project).version = PyCompatibilityInspection.LATEST_INSPECTION_VERSION;
}
});
}
private static void showSingletonNotification(@NotNull Project project,
@NotNull String title,
@NotNull String htmlContent,
@NotNull NotificationType type,
@NotNull NotificationListener listener) {
project.putUserData(DONT_SHOW_BALLOON, true);
BALLOON_NOTIFICATIONS.createNotification(title, htmlContent, type, (notification, event) -> {
try {
listener.hyperlinkUpdate(notification, event);
}
finally {
notification.expire();
}
}).notify(project);
}
private static boolean containsSixImport(@NotNull PyFile file) {
for (PyFromImportStatement importStatement : file.getFromImports()) {
final QualifiedName name = importStatement.getImportSourceQName();
if (name != null && "six".equals(name.toString())) {
return true;
}
}
for (PyImportElement importElement : file.getImportTargets()) {
final QualifiedName name = importElement.getImportedQName();
if (name != null && "six".equals(name.getFirstComponent())) {
return true;
}
}
return false;
}
private static boolean containsFutureImports(@NotNull PyFile file) {
for (PyFromImportStatement importStatement : file.getFromImports()) {
if (importStatement.isFromFuture()) {
return true;
}
}
return false;
}
private static boolean isCompatibilityInspectionEnabled(@NotNull PsiElement anchor) {
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(anchor.getProject()).getInspectionProfile();
final InspectionToolWrapper tool = profile.getInspectionTool(getCompatibilityInspectionShortName(), anchor.getProject());
return tool != null && profile.isToolEnabled(HighlightDisplayKey.findById(tool.getID()), anchor);
}
private static void enableCompatibilityInspection(@NotNull Project project) {
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
final InspectionToolWrapper tool = profile.getInspectionTool(getCompatibilityInspectionShortName(), project);
if (tool != null) {
// Partially copied from JSLinterInspection
final InspectionProfileImpl inspectionProfileImpl = as(profile, InspectionProfileImpl.class);
if (inspectionProfileImpl != null) {
final ScopeToolState state = inspectionProfileImpl.getToolDefaultState(tool.getShortName(), project);
state.setEnabled(true);
}
profile.modifyProfile(model -> model.enableTool(tool.getShortName(), null, project));
EditInspectionToolsSettingsAction.editToolSettings(project, profile, true, getCompatibilityInspectionShortName());
}
}
@Nullable
private static LanguageLevel getLatestConfiguredCompatiblePythonVersion(@NotNull PsiElement anchor) {
final InspectionProfile profile = InspectionProjectProfileManager.getInstance(anchor.getProject()).getInspectionProfile();
final PyCompatibilityInspection inspection = (PyCompatibilityInspection)profile.getUnwrappedTool(getCompatibilityInspectionShortName(), anchor);
final JDOMExternalizableStringList versions = inspection.ourVersions;
if (versions.isEmpty()) {
return null;
}
final String maxVersion = Collections.max(versions);
return LanguageLevel.fromPythonVersion(maxVersion);
}
@NotNull
private static String getCompatibilityInspectionShortName() {
return PyCompatibilityInspection.class.getSimpleName();
}
@NotNull
private static PyCompatibilityInspectionAdvertiserSettings getSettings(@NotNull Project project) {
return ServiceManager.getService(project, PyCompatibilityInspectionAdvertiserSettings.class);
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.inspections;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.Nullable;
/**
* @author Mikhail Golubev
*/
@State(name = "PythonCompatibilityInspectionAdvertiser")
public class PyCompatibilityInspectionAdvertiserSettings implements PersistentStateComponent<PyCompatibilityInspectionAdvertiserSettings> {
public int version = 0;
@Nullable
@Override
public PyCompatibilityInspectionAdvertiserSettings getState() {
return this;
}
@Override
public void loadState(PyCompatibilityInspectionAdvertiserSettings state) {
XmlSerializerUtil.copyBean(state, this);
}
}