PY-20159 Add an option to make optimize imports case-insensitive

Both packages/module names and names of individual imported symbols in
"from" imports are sorted case-insensitively if it's enabled.
To keep things simple I decided not to replicate the exact scheme used
by isort to order names in "from" imports where they have different priority
depending on whether they are ALL_CAPS or CamelCased (check the issue for
related discussion).
This commit is contained in:
Mikhail Golubev
2018-10-01 12:07:13 +03:00
parent dc67ca84f1
commit 841fcbc914
11 changed files with 87 additions and 18 deletions
@@ -1019,6 +1019,7 @@ formatter.imports.panel.sort.imports=Sort import statements
formatter.imports.panel.sort.names.in.from.imports=Sort imported names in "from" imports
formatter.imports.panel.join.from.imports.with.same.source=Join "from" imports with the same source
formatter.imports.panel.sort.by.type=Sort plain and "from" imports separately within a group
formatter.imports.panel.sort.case.insensitively=Sort case-insensitively
formatter.panel.dict.alignment.do.not.align=Do not align
formatter.panel.dict.alignment.align.on.colon=Align on colon
@@ -53,8 +53,14 @@ public class AddImportHelper {
return firstIsFromImport - secondIsFromImport;
};
private static final Comparator<PyImportStatementBase> IMPORT_NAMES_COMPARATOR =
(import1, import2) -> ContainerUtil.compareLexicographically(getSortNames(import1), getSortNames(import2));
@NotNull
private static Comparator<PyImportStatementBase> getImportNamesComparator(@NotNull PyCodeStyleSettings settings) {
return (import1, import2) -> {
final Comparator<String> stringComparator =
settings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER ? String.CASE_INSENSITIVE_ORDER : Comparator.naturalOrder();
return ContainerUtil.compareLexicographically(getSortNames(import1), getSortNames(import2), Comparator.nullsFirst(stringComparator));
};
}
@NotNull
private static List<String> getSortNames(@NotNull PyImportStatementBase importStatement) {
@@ -93,10 +99,10 @@ public class AddImportHelper {
public static Comparator<PyImportStatementBase> getSameGroupImportsComparator(@NotNull PsiFile settingsAnchor) {
final PyCodeStyleSettings settings = CodeStyle.getCustomSettings(settingsAnchor, PyCodeStyleSettings.class);
if (settings.OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST) {
return IMPORT_TYPE_COMPARATOR.thenComparing(IMPORT_NAMES_COMPARATOR);
return IMPORT_TYPE_COMPARATOR.thenComparing(getImportNamesComparator(settings));
}
else {
return IMPORT_NAMES_COMPARATOR.thenComparing(IMPORT_TYPE_COMPARATOR);
return getImportNamesComparator(settings).thenComparing(IMPORT_TYPE_COMPARATOR);
}
}
@@ -30,6 +30,7 @@ import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
@@ -90,14 +91,6 @@ public class PyImportOptimizer implements ImportOptimizer {
}
private static class ImportSorter {
private static final Comparator<PyImportElement> IMPORT_ELEMENT_COMPARATOR = (o1, o2) -> {
final int byImportedName = Comparing.compare(o1.getImportedQName(), o2.getImportedQName());
if (byImportedName != 0) {
return byImportedName;
}
return Comparing.compare(o1.getAsName(), o2.getAsName());
};
private final PyFile myFile;
private final PyCodeStyleSettings myPySettings;
private final List<PyImportStatementBase> myImportBlock;
@@ -122,6 +115,16 @@ public class PyImportOptimizer implements ImportOptimizer {
}
}
@NotNull
private Comparator<PyImportElement> getFromNamesComparator() {
final Comparator<String> stringComparator =
myPySettings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER ? String.CASE_INSENSITIVE_ORDER : Comparator.naturalOrder();
final Comparator<QualifiedName> qNamesComparator = Comparator.comparing(QualifiedName::toString, stringComparator);
return Comparator
.comparing(PyImportElement::getImportedQName, Comparator.nullsFirst(qNamesComparator))
.thenComparing(PyImportElement::getAsName, Comparator.nullsFirst(stringComparator));
}
public void run() {
if (myImportBlock.isEmpty()) {
return;
@@ -201,6 +204,7 @@ public class PyImportOptimizer implements ImportOptimizer {
final List<PyImportElement> newStatementElements = new ArrayList<>();
boolean forceParentheses = false;
final Comparator<PyImportElement> fromNamesComparator = getFromNamesComparator();
// We can neither sort, nor combine star imports
if (!fromImport.isStarImport()) {
final Collection<PyFromImportStatement> sameSourceImports = myOldFromImportBySources.get(source);
@@ -220,7 +224,7 @@ public class PyImportOptimizer implements ImportOptimizer {
}
else if (myPySettings.OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS) {
final List<PyImportElement> originalElements = Arrays.asList(fromImport.getImportElements());
if (!Ordering.from(IMPORT_ELEMENT_COMPARATOR).isOrdered(originalElements)) {
if (!Ordering.from(fromNamesComparator).isOrdered(originalElements)) {
ContainerUtil.addAll(newStatementElements, originalElements);
}
}
@@ -228,7 +232,7 @@ public class PyImportOptimizer implements ImportOptimizer {
if (!newStatementElements.isEmpty()) {
if (myPySettings.OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS) {
Collections.sort(newStatementElements, IMPORT_ELEMENT_COMPARATOR);
Collections.sort(newStatementElements, fromNamesComparator);
}
String importedNames = StringUtil.join(newStatementElements, ImportSorter::getNormalizedImportElementText, ", ");
if (forceParentheses) {
@@ -101,6 +101,7 @@ public class PyCodeStyleSettings extends CustomCodeStyleSettings {
public boolean OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS = false;
public boolean OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST = true;
public boolean OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE = false;
public boolean OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER = false;
/**
* Affects wrapping of multiple imported names in a single "from" import.
@@ -3,7 +3,7 @@
<grid id="27dc6" binding="myRootPanel" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="10" bottom="10" right="10"/>
<constraints>
<xy x="20" y="20" width="675" height="400"/>
<xy x="20" y="20" width="852" height="400"/>
</constraints>
<properties/>
<border type="none"/>
@@ -13,7 +13,7 @@
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<grid id="ee1fb" layout-manager="GridLayoutManager" row-count="4" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="ee1fb" layout-manager="GridLayoutManager" row-count="5" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
@@ -42,7 +42,7 @@
</component>
<component id="a7a3a" class="com.intellij.ui.components.JBCheckBox" binding="myJoinFromImportsWithSameSource" default-binding="true">
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
<grid row="4" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text resource-bundle="com/jetbrains/python/PyBundle" key="formatter.imports.panel.join.from.imports.with.same.source"/>
@@ -56,6 +56,15 @@
<text resource-bundle="com/jetbrains/python/PyBundle" key="formatter.imports.panel.sort.by.type"/>
</properties>
</component>
<component id="48046" class="com.intellij.ui.components.JBCheckBox" binding="mySortCaseInsensitively">
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="2" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="false"/>
<text resource-bundle="com/jetbrains/python/PyBundle" key="formatter.imports.panel.sort.case.insensitively"/>
</properties>
</component>
</children>
</grid>
<hspacer id="25073">
@@ -39,6 +39,7 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel {
private JBCheckBox mySortImportsByTypeFirst;
private JBCheckBox myJoinFromImportsWithSameSource;
private JPanel myRootPanel;
private JBCheckBox mySortCaseInsensitively;
public PyImportsCodeStylePanel(@NotNull CodeStyleSettings settings) {
super(PythonLanguage.getInstance(), null, settings);
@@ -48,6 +49,7 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel {
final boolean sortingEnabled = mySortImports.isSelected();
mySortNamesInFromImports.setEnabled(sortingEnabled);
mySortImportsByTypeFirst.setEnabled(sortingEnabled);
mySortCaseInsensitively.setEnabled(sortingEnabled);
});
}
@@ -87,6 +89,7 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel {
pySettings.OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS = mySortNamesInFromImports.isSelected();
pySettings.OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST = mySortImportsByTypeFirst.isSelected();
pySettings.OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE = myJoinFromImportsWithSameSource.isSelected();
pySettings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER = mySortCaseInsensitively.isSelected();
}
@Override
@@ -96,7 +99,8 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel {
return mySortImports.isSelected() != pySettings.OPTIMIZE_IMPORTS_SORT_IMPORTS ||
mySortNamesInFromImports.isSelected() != pySettings.OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS ||
mySortImportsByTypeFirst.isSelected() != pySettings.OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST ||
myJoinFromImportsWithSameSource.isSelected() != pySettings.OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE;
myJoinFromImportsWithSameSource.isSelected() != pySettings.OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE ||
mySortCaseInsensitively.isSelected() != pySettings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER;
}
@Nullable
@@ -115,5 +119,6 @@ public class PyImportsCodeStylePanel extends CodeStyleAbstractPanel {
mySortImportsByTypeFirst.setSelected(pySettings.OPTIMIZE_IMPORTS_SORT_BY_TYPE_FIRST);
mySortImportsByTypeFirst.setEnabled(mySortImports.isSelected());
myJoinFromImportsWithSameSource.setSelected(pySettings.OPTIMIZE_IMPORTS_JOIN_FROM_IMPORTS_WITH_SAME_SOURCE);
mySortCaseInsensitively.setSelected(pySettings.OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER);
}
}
@@ -0,0 +1,12 @@
import mod_a
import mod_Z
# noinspection PyUnresolvedReferences
import pkg.pkg_a
# noinspection PyUnresolvedReferences
import pkg.pkg_Z
from mod import var_a
from mod import var_Z
from pkg.pkg_a import mod1
from pkg.pkg_Z import mod2
print(mod_a, mod_Z, pkg.pkg_a, pkg.pkg_Z, mod1, mod2, var_a, var_Z)
@@ -0,0 +1,12 @@
import mod_a
import mod_Z
# noinspection PyUnresolvedReferences
import pkg.pkg_a
# noinspection PyUnresolvedReferences
import pkg.pkg_Z
from mod import var_a
from mod import var_Z
from pkg.pkg_a import mod1
from pkg.pkg_Z import mod2
print(mod_a, mod_Z, pkg.pkg_a, pkg.pkg_Z, mod1, mod2, var_a, var_Z)
@@ -0,0 +1,3 @@
from mod import var_a, var_Z
print(var_a, var_Z)
@@ -0,0 +1,3 @@
from mod import var_a, var_Z
print(var_a, var_Z)
@@ -173,6 +173,19 @@ public class PyOptimizeImportsTest extends PyTestCase {
doTest();
}
// PY-20159
public void testCaseInsensitiveOrderOfImports() {
getPythonCodeStyleSettings().OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER = true;
doTest();
}
// PY-20159
public void testCaseInsensitiveOrderOfNamesInsideFromImports() {
getPythonCodeStyleSettings().OPTIMIZE_IMPORTS_SORT_NAMES_IN_FROM_IMPORTS = true;
getPythonCodeStyleSettings().OPTIMIZE_IMPORTS_CASE_INSENSITIVE_ORDER = true;
doTest();
}
// PY-19674
public void testUnresolvedRelativeImportsShouldBeInProjectGroup() {
final String testName = getTestName(true);