diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 9762ef040994..8e3315020156 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -395,6 +395,8 @@ INSP.too.few.args.for.fmt.string=Too few arguments for format string INSP.incompatible.options=The format options in chunk "{0}" are incompatible INSP.unused.mapping = Mapping key "{0}" is unused INSP.unsupported.format.character=Unsupported format character ''{0}'' +INSP.manual.to.auto.field.numbering=Cannot switch from manual field specification to automatic field numbering +INSP.auto.to.manual.field.numbering=Cannot switch from automatic field numbering to manual field specification # PyMethodOverridingInspection INSP.NAME.method.over=Method signature does not match signature of overridden method diff --git a/python/src/com/jetbrains/python/codeInsight/PySubstitutionChunkReference.java b/python/src/com/jetbrains/python/codeInsight/PySubstitutionChunkReference.java index 5e70d5a8d6b3..ace5140ca54d 100644 --- a/python/src/com/jetbrains/python/codeInsight/PySubstitutionChunkReference.java +++ b/python/src/com/jetbrains/python/codeInsight/PySubstitutionChunkReference.java @@ -74,7 +74,6 @@ public class PySubstitutionChunkReference extends PsiReferenceBase chunks = filterSubstitutions(PyStringFormatParser.parseNewStyleFormat(value)); + PyStringFormatParser parser = new PyStringFormatParser(value); + final List chunks = filterSubstitutions(parser.parseNewStyle()); + + switch (parser.getAutoNumberStateError()) { + case NONE: + break; + case MANUAL_TO_AUTO: + registerProblem(myFormatExpression, PyBundle.message("INSP.manual.to.auto.field.numbering")); + return; + case AUTO_TO_MANUAL: + registerProblem(myFormatExpression, PyBundle.message("INSP.auto.to.manual.field.numbering")); + return; + } for (int i = 0; i < chunks.size(); i++) { final PyStringFormatParser.NewStyleSubstitutionChunk chunk = as(chunks.get(i), PyStringFormatParser.NewStyleSubstitutionChunk.class); if (chunk != null) { - if (chunk.getPosition() == null) { - chunk.setPosition(i); - } String mappingKey = inspectNewStyleChunkAndGetMappingKey(chunk); if (!isProblem()) { inspectArguments(chunk, mappingKey); @@ -502,7 +510,7 @@ public class PyStringFormatInspection extends PyInspection { final HashSet supportedTypes = new HashSet<>(); boolean hasTypeOptions = false; - final String mappingKey = chunk.getMappingKey() != null ? chunk.getMappingKey() : String.valueOf(chunk.getPosition()); + final String mappingKey = chunk.getMappingKey() != null ? chunk.getMappingKey() : String.valueOf(chunk.getPositionalArgumentIndex()); // inspect options available only for numeric types if (chunk.hasSignOption() || chunk.useAlternateForm() || chunk.hasZeroPadding() || chunk.hasThousandsSeparator()) { @@ -539,9 +547,7 @@ public class PyStringFormatInspection extends PyInspection { } private void inspectArguments(@NotNull PyStringFormatParser.NewStyleSubstitutionChunk chunk, @NotNull String mappingKey) { - // it's true because we set position manually in inspect() - assert chunk.getPosition() != null; - final PsiElement target = new PySubstitutionChunkReference(myFormatExpression, chunk, chunk.getPosition()).resolve(); + final PsiElement target = new PySubstitutionChunkReference(myFormatExpression, chunk, chunk.getPositionalArgumentIndex()).resolve(); boolean hasElementIndex = chunk.getMappingKeyElementIndex() != null; if (target == null) { final String chunkMapping = chunk.getMappingKey(); diff --git a/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java b/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java index f0f2b4375b6d..aac74f71d9ab 100644 --- a/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java +++ b/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java @@ -18,7 +18,6 @@ package com.jetbrains.python.inspections; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; -import java.util.HashMap; import com.jetbrains.python.PyNames; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl; @@ -26,6 +25,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; @@ -144,6 +144,10 @@ public class PyStringFormatParser { protected void setMappingKey(@Nullable String mappingKey) { myMappingKey = mappingKey; } + + public int getPositionalArgumentIndex() { + return myPosition == null ? myAutoPosition == null ? 0 : myAutoPosition : myPosition; + } } public static class PercentSubstitutionChunk extends SubstitutionChunk { @@ -254,10 +258,43 @@ public class PyStringFormatParser { } } + private enum AutoNumberState {INIT, AUTO, MANUAL} + public enum AutoNumberStateError {NONE, MANUAL_TO_AUTO, AUTO_TO_MANUAL} + private static class AutoNumber { + private AutoNumberState myState = AutoNumberState.INIT; + private AutoNumberStateError myStateError = AutoNumberStateError.NONE; + + private void checkState(AutoNumberState nextState) { + if (myStateError != AutoNumberStateError.NONE) { + return; + } + switch (myState) { + case INIT: + myState = nextState; + break; + case AUTO: + if (nextState == AutoNumberState.MANUAL) { + myStateError = AutoNumberStateError.AUTO_TO_MANUAL; + } + break; + case MANUAL: + if (nextState == AutoNumberState.AUTO) { + myStateError = AutoNumberStateError.MANUAL_TO_AUTO; + } + break; + } + } + } + + public AutoNumberStateError getAutoNumberStateError() { + return myAutoNumber.myStateError; + } + @NotNull private final String myLiteral; @NotNull private final List myResult = new ArrayList<>(); private int myPos; private int mySubstitutionsCount = 0; + @NotNull private AutoNumber myAutoNumber = new AutoNumber(); // % strings private static final String CONVERSION_FLAGS = "#0- +"; @@ -273,7 +310,7 @@ public class PyStringFormatParser { private static final char ZERO_PADDING_SYMBOL = '0'; - private PyStringFormatParser(@NotNull String literal) { + public PyStringFormatParser(@NotNull String literal) { myLiteral = literal; } @@ -308,7 +345,7 @@ public class PyStringFormatParser { return myResult; } - private List parseNewStyle() { + public List parseNewStyle() { final List results = new ArrayList<>(); final Matcher matcher = NEW_STYLE_FORMAT_TOKENS.matcher(myLiteral); int autoPositionedFieldsCount = 0; @@ -350,6 +387,7 @@ public class PyStringFormatParser { try { final int number = Integer.parseInt(name); chunk.setPosition(number); + myAutoNumber.checkState(AutoNumberState.MANUAL); } catch (NumberFormatException e) { chunk.setMappingKey(name); @@ -359,6 +397,7 @@ public class PyStringFormatParser { else { chunk.setAutoPosition(autoPositionedFieldsCount); autoPositionedFieldsCount++; + myAutoNumber.checkState(AutoNumberState.AUTO); } // parse field name attribute name @@ -453,7 +492,7 @@ public class PyStringFormatParser { if (myPos < end - 1) { chunk.setConversionType(myLiteral.charAt(myPos)); } - } + } results.add(chunk); diff --git a/python/testData/inspections/PyStringFormatInspection/NewStyleAutomaticAfterManualNumbering.py b/python/testData/inspections/PyStringFormatInspection/NewStyleAutomaticAfterManualNumbering.py new file mode 100644 index 000000000000..c2b52c370aa2 --- /dev/null +++ b/python/testData/inspections/PyStringFormatInspection/NewStyleAutomaticAfterManualNumbering.py @@ -0,0 +1 @@ +'{} {1}'.format(6, 7) diff --git a/python/testData/inspections/PyStringFormatInspection/NewStyleManualAfterAutomaticNumbering.py b/python/testData/inspections/PyStringFormatInspection/NewStyleManualAfterAutomaticNumbering.py new file mode 100644 index 000000000000..16813bd19edc --- /dev/null +++ b/python/testData/inspections/PyStringFormatInspection/NewStyleManualAfterAutomaticNumbering.py @@ -0,0 +1 @@ +'{1} {}'.format(6, 7) diff --git a/python/testData/inspections/PyStringFormatInspection/NewStylePositionalSubstitutionAfterKeywordSubstitution.py b/python/testData/inspections/PyStringFormatInspection/NewStylePositionalSubstitutionAfterKeywordSubstitution.py index 7138ded8e069..6fa1684ac550 100644 --- a/python/testData/inspections/PyStringFormatInspection/NewStylePositionalSubstitutionAfterKeywordSubstitution.py +++ b/python/testData/inspections/PyStringFormatInspection/NewStylePositionalSubstitutionAfterKeywordSubstitution.py @@ -1 +1,2 @@ -'{a} {}'.format(6, a=2) \ No newline at end of file +x = '{a} {}'.format(6, a=2) +print("{} {other} {}".format("one", "two", other="OTHER")) diff --git a/python/testSrc/com/jetbrains/python/inspections/PyStringFormatInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyStringFormatInspectionTest.java index 9a220c0e9883..7b49980e1c9c 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyStringFormatInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyStringFormatInspectionTest.java @@ -125,10 +125,18 @@ public class PyStringFormatInspectionTest extends PyInspectionTestCase { doTest(); } - // PY-27710 + // PY-27601 public void testNewStylePositionalSubstitutionAfterKeywordSubstitution() { doTest(); } + + public void testNewStyleAutomaticAfterManualNumbering() { + doTest(); + } + + public void testNewStyleManualAfterAutomaticNumbering() { + doTest(); + } public void testPercentStringWithFormatStringReplacementSymbols() { doTest();