diff --git a/python/pluginSrc/com/jetbrains/python/module/PythonSdkChooserPanel.java b/python/pluginSrc/com/jetbrains/python/module/PythonSdkChooserPanel.java index 9fdf6b981997..c70461b9605f 100644 --- a/python/pluginSrc/com/jetbrains/python/module/PythonSdkChooserPanel.java +++ b/python/pluginSrc/com/jetbrains/python/module/PythonSdkChooserPanel.java @@ -8,6 +8,7 @@ import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.MultiLineLabelUI; import com.intellij.util.ui.UIUtil; import com.jetbrains.python.sdk.PythonSdkType; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; @@ -26,7 +27,7 @@ public class PythonSdkChooserPanel extends JComponent { * * @param project Current project */ - public PythonSdkChooserPanel(final Project project) { + public PythonSdkChooserPanel(@NotNull final Project project) { myJdkChooser = new JdkChooserPanel(project); setLayout(new GridBagLayout()); diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index eebb031861f6..08ad91ac7121 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -6,6 +6,7 @@ + @@ -383,6 +384,8 @@ + + diff --git a/python/src/com/jetbrains/python/PyFileTypeDetector.java b/python/src/com/jetbrains/python/PyFileTypeDetector.java new file mode 100644 index 000000000000..eba27091bd29 --- /dev/null +++ b/python/src/com/jetbrains/python/PyFileTypeDetector.java @@ -0,0 +1,33 @@ +package com.jetbrains.python; + +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.FileTypeRegistry; +import com.intellij.openapi.util.io.ByteSequence; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author yole + */ +public class PyFileTypeDetector implements FileTypeRegistry.FileTypeDetector { + @Override + public FileType detect(@NotNull VirtualFile file, @NotNull ByteSequence firstBytes, @Nullable CharSequence firstCharsIfText) { + if (firstCharsIfText == null) { + return null; + } + final int lineBreak = StringUtil.indexOf(firstCharsIfText, '\n'); + if (lineBreak < 0) { + return null; + } + String firstLine = firstCharsIfText.subSequence(0, lineBreak).toString(); + if (!firstLine.startsWith("#!")) { + return null; + } + if (firstLine.contains("python")) { + return PythonFileType.INSTANCE; + } + return null; + } +} diff --git a/python/src/com/jetbrains/python/actions/SimplifyBooleanCheckQuickFix.java b/python/src/com/jetbrains/python/actions/SimplifyBooleanCheckQuickFix.java index f3f749e9f5c1..be58526b123d 100644 --- a/python/src/com/jetbrains/python/actions/SimplifyBooleanCheckQuickFix.java +++ b/python/src/com/jetbrains/python/actions/SimplifyBooleanCheckQuickFix.java @@ -3,9 +3,11 @@ package com.jetbrains.python.actions; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; import com.intellij.psi.tree.TokenSet; import com.jetbrains.python.PyBundle; import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.psi.PyBinaryExpression; import com.jetbrains.python.psi.PyElementGenerator; import com.jetbrains.python.psi.PyExpression; @@ -18,12 +20,10 @@ import org.jetbrains.annotations.NotNull; * Time: 19:41:07 */ public class SimplifyBooleanCheckQuickFix implements LocalQuickFix { - private PyBinaryExpression myExpression; private String myReplacementText; public SimplifyBooleanCheckQuickFix(PyBinaryExpression binaryExpression) { - myExpression = binaryExpression; - myReplacementText = createReplacementText(); + myReplacementText = createReplacementText(binaryExpression); } private static boolean isTrue(PyExpression expression) { @@ -53,15 +53,19 @@ public class SimplifyBooleanCheckQuickFix implements LocalQuickFix { } public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + final PsiElement element = descriptor.getPsiElement(); + if (!element.isValid() || !(element instanceof PyBinaryExpression)) { + return; + } PyElementGenerator elementGenerator = PyElementGenerator.getInstance(project); - myExpression.replace(elementGenerator.createExpressionFromText(myReplacementText)); + element.replace(elementGenerator.createExpressionFromText(LanguageLevel.forElement(element), myReplacementText)); } - private String createReplacementText() { + private static String createReplacementText(PyBinaryExpression expression) { PyExpression resultExpression; - final PyExpression leftExpression = myExpression.getLeftExpression(); - final PyExpression rightExpression = myExpression.getRightExpression(); - boolean positiveCondition = !TokenSet.create(PyTokenTypes.NE, PyTokenTypes.NE_OLD).contains(myExpression.getOperator()); + final PyExpression leftExpression = expression.getLeftExpression(); + final PyExpression rightExpression = expression.getRightExpression(); + boolean positiveCondition = !TokenSet.create(PyTokenTypes.NE, PyTokenTypes.NE_OLD).contains(expression.getOperator()); positiveCondition ^= isFalse(leftExpression) || isFalse(rightExpression) || isNull(rightExpression) || isNull(leftExpression) || isEmpty(rightExpression) || isEmpty(leftExpression); if (isTrue(leftExpression) || isFalse(leftExpression) || isNull(leftExpression) || isEmpty(leftExpression)) { diff --git a/python/src/com/jetbrains/python/codeInsight/PyCodeBlockProvider.java b/python/src/com/jetbrains/python/codeInsight/PyCodeBlockProvider.java new file mode 100644 index 000000000000..2a1b71d3134a --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/PyCodeBlockProvider.java @@ -0,0 +1,55 @@ +package com.jetbrains.python.codeInsight; + +import com.intellij.codeInsight.editorActions.CodeBlockProvider; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.python.psi.PyStatement; +import com.jetbrains.python.psi.PyStatementList; +import org.jetbrains.annotations.Nullable; + +/** + * @author yole + */ +public class PyCodeBlockProvider implements CodeBlockProvider { + @Nullable + @Override + public TextRange getCodeBlockRange(Editor editor, PsiFile psiFile) { + int caretOffset = editor.getCaretModel().getOffset(); + PsiElement element = psiFile.findElementAt(caretOffset); + if (element == null) { + return null; + } + while (caretOffset > 0 && element instanceof PsiWhiteSpace) { + caretOffset--; + element = psiFile.findElementAt(caretOffset); + } + PyStatement statement = PsiTreeUtil.getParentOfType(element, PyStatement.class); + if (statement != null) { + PyStatementList statementList = PsiTreeUtil.findChildOfType(statement, PyStatementList.class); + + // if the statement above caret is not a block statement, look above for a statement list and then find the statement above + // that statement list + if (statementList == null) { + statementList = PsiTreeUtil.getParentOfType(statement, PyStatementList.class); + if (statementList != null) { + statement = PsiTreeUtil.getParentOfType(statementList, PyStatement.class); + } + } + if (statement != null) { + // if we're in the beginning of the statement already, pressing Ctrl-[ again should move the caret one statement higher + if (caretOffset == statement.getTextRange().getStartOffset()) { + final PyStatement statementAbove = PsiTreeUtil.getParentOfType(statement, PyStatement.class); + if (statementAbove != null) { + return new TextRange(statementAbove.getTextRange().getStartOffset(), statement.getTextRange().getEndOffset()); + } + } + return statement.getTextRange(); + } + } + return null; + } +} diff --git a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java index 760b331d0e3f..5d6efc4aa418 100644 --- a/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java +++ b/python/src/com/jetbrains/python/documentation/PythonDocumentationProvider.java @@ -362,9 +362,9 @@ public class PythonDocumentationProvider extends AbstractDocumentationProvider i } @Nullable - public static String pyVersion(String versionString) { + public static String pyVersion(@Nullable String versionString) { String prefix = "Python "; - if (versionString.startsWith(prefix)) { + if (versionString != null && versionString.startsWith(prefix)) { String version = versionString.substring(prefix.length()); int dot = version.indexOf('.'); if (dot > 0) { diff --git a/python/src/com/jetbrains/python/inspections/PyArgumentEqualDefaultInspection.java b/python/src/com/jetbrains/python/inspections/PyArgumentEqualDefaultInspection.java index 1756b1031eac..588d5966cb22 100644 --- a/python/src/com/jetbrains/python/inspections/PyArgumentEqualDefaultInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyArgumentEqualDefaultInspection.java @@ -9,6 +9,7 @@ import com.jetbrains.python.PyBundle; import com.jetbrains.python.actions.RemoveArgumentEqualDefaultQuickFix; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; +import com.jetbrains.python.psi.types.PyClassType; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -52,19 +53,36 @@ public class PyArgumentEqualDefaultInspection extends PyInspection { } @Override - public void visitPyCallExpression(final PyCallExpression node){ + public void visitPyCallExpression(final PyCallExpression node) { PyArgumentList list = node.getArgumentList(); PyCallExpression.PyMarkedCallee func = node.resolveCallee(resolveWithoutImplicits()); if ((func != null && func.isImplicitlyResolved()) || (list == null)) return; if (func != null) { - // getattr's default attribute is a special case, see PY-3440 final Callable callable = func.getCallable(); - if ("getattr".equals(callable.getName()) && PyBuiltinCache.getInstance(node).hasInBuiltins(callable)) return; + if (hasSpecialCasedDefaults(callable, node)) { + return; + } } CallArgumentsMapping result = list.analyzeCall(resolveWithoutImplicits()); checkArguments(result, node.getArguments()); } + private static boolean hasSpecialCasedDefaults(Callable callable, PsiElement anchor) { + final String name = callable.getName(); + final PyBuiltinCache cache = PyBuiltinCache.getInstance(anchor); + if ("getattr".equals(name) && cache.hasInBuiltins(callable)) { + return true; + } + else if ("get".equals(name) || "pop".equals(name)) { + final PyFunction method = callable.asMethod(); + final PyClassType dictType = cache.getDictType(); + if (method != null && dictType != null && method.getContainingClass() == dictType.getPyClass()) { + return true; + } + } + return false; + } + private void checkArguments(CallArgumentsMapping result, PyExpression[] arguments) { Map mapping = result.getPlainMappedParams(); Set problemElements = new HashSet(); diff --git a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java index fe0fe9fbbe79..676015fff35f 100644 --- a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java @@ -400,7 +400,8 @@ public class PyUnresolvedReferencesInspection extends PyInspection { PyType qtype = myTypeEvalContext.getType(qualifier); if (qtype != null) { if (qtype instanceof PyNoneType || qtype instanceof PyTypeReference || - (qtype instanceof PyUnionType && ((PyUnionType) qtype).isWeak())) { + (qtype instanceof PyUnionType && ((PyUnionType) qtype).isWeak()) || + (qtype instanceof PyImportedModuleType)) { // this almost always means that we don't know the type, so don't show an error in this case return; } diff --git a/python/src/com/jetbrains/python/lexer/PythonIndentingProcessor.java b/python/src/com/jetbrains/python/lexer/PythonIndentingProcessor.java index be7468506a73..4138d02f8856 100644 --- a/python/src/com/jetbrains/python/lexer/PythonIndentingProcessor.java +++ b/python/src/com/jetbrains/python/lexer/PythonIndentingProcessor.java @@ -174,6 +174,9 @@ public class PythonIndentingProcessor extends MergingLexerAdapter { adjustBraceLevel(); myLineHasSignificantTokens = false; checkSignificantTokens(); + if (isBaseAt(PyTokenTypes.SPACE)) { + processIndent(0, PyTokenTypes.SPACE); + } } private void adjustBraceLevel() { @@ -254,7 +257,7 @@ public class PythonIndentingProcessor extends MergingLexerAdapter { } myLineHasSignificantTokens = false; advanceBase(); - processIndent(startPos); + processIndent(startPos, PyTokenTypes.LINE_BREAK); } else { processInsignificantLineBreak(startPos, false); @@ -281,7 +284,7 @@ public class PythonIndentingProcessor extends MergingLexerAdapter { myTokenQueue.add(new PendingToken(PyTokenTypes.LINE_BREAK, startPos, end)); } - protected void processIndent(int whiteSpaceStart) { + protected void processIndent(int whiteSpaceStart, IElementType whitespaceTokenType) { int lastIndent = myIndentStack.peek(); int indent = getNextLineIndent(); myLastNewLineIndent = indent; @@ -292,7 +295,7 @@ public class PythonIndentingProcessor extends MergingLexerAdapter { int whiteSpaceEnd = (getBaseTokenType() == null) ? super.getBufferEnd() : getBaseTokenStart(); if (indent > lastIndent) { myIndentStack.push(indent); - myTokenQueue.add(new PendingToken(PyTokenTypes.LINE_BREAK, whiteSpaceStart, whiteSpaceEnd)); + myTokenQueue.add(new PendingToken(whitespaceTokenType, whiteSpaceStart, whiteSpaceEnd)); int insertIndex = skipPrecedingCommentsWithIndent(indent, myTokenQueue.size() - 1); int indentOffset = insertIndex == myTokenQueue.size() ? whiteSpaceEnd : myTokenQueue.get(insertIndex).getStart(); myTokenQueue.add(insertIndex, new PendingToken(PyTokenTypes.INDENT, indentOffset, indentOffset)); @@ -315,10 +318,10 @@ public class PythonIndentingProcessor extends MergingLexerAdapter { } myTokenQueue.add(insertIndex, new PendingToken(PyTokenTypes.DEDENT, dedentOffset, dedentOffset)); } - myTokenQueue.add(new PendingToken(PyTokenTypes.LINE_BREAK, whiteSpaceStart, whiteSpaceEnd)); + myTokenQueue.add(new PendingToken(whitespaceTokenType, whiteSpaceStart, whiteSpaceEnd)); } else { - myTokenQueue.add(new PendingToken(PyTokenTypes.LINE_BREAK, whiteSpaceStart, whiteSpaceEnd)); + myTokenQueue.add(new PendingToken(whitespaceTokenType, whiteSpaceStart, whiteSpaceEnd)); } } diff --git a/python/src/com/jetbrains/python/parsing/ExpressionParsing.java b/python/src/com/jetbrains/python/parsing/ExpressionParsing.java index 83298fcf4257..c2d6adffff1c 100644 --- a/python/src/com/jetbrains/python/parsing/ExpressionParsing.java +++ b/python/src/com/jetbrains/python/parsing/ExpressionParsing.java @@ -307,7 +307,7 @@ public class ExpressionParsing extends Parsing { else recast_first_identifier = false; myBuilder.advanceLexer(); checkMatches(PyTokenTypes.IDENTIFIER, message("PARSE.expected.name")); - if (isTargetExpression && ! recast_qualifier && myBuilder.getTokenType() != PyTokenTypes.DOT) { + if (isTargetExpression && ! recast_qualifier && !atAnyOfTokens(PyTokenTypes.DOT, PyTokenTypes.LPAR, PyTokenTypes.LBRACKET)) { expr.done(PyElementTypes.TARGET_EXPRESSION); } else { diff --git a/python/src/com/jetbrains/python/parsing/StatementParsing.java b/python/src/com/jetbrains/python/parsing/StatementParsing.java index f39f098014d3..d6bf62606e93 100644 --- a/python/src/com/jetbrains/python/parsing/StatementParsing.java +++ b/python/src/com/jetbrains/python/parsing/StatementParsing.java @@ -551,17 +551,13 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { final PsiBuilder.Marker ifPart = myBuilder.mark(); myBuilder.advanceLexer(); getExpressionParser().parseExpression(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); ifPart.done(PyElementTypes.IF_PART_IF); PsiBuilder.Marker elifPart = myBuilder.mark(); while (myBuilder.getTokenType() == elifKeyword) { myBuilder.advanceLexer(); getExpressionParser().parseExpression(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); elifPart.done(PyElementTypes.IF_PART_ELIF); elifPart = myBuilder.mark(); } @@ -569,9 +565,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { final PsiBuilder.Marker elsePart = myBuilder.mark(); if (myBuilder.getTokenType() == elseKeyword) { myBuilder.advanceLexer(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); elsePart.done(PyElementTypes.ELSE_PART); } else elsePart.drop(); @@ -598,9 +592,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { final PsiBuilder.Marker elsePart = myBuilder.mark(); if (myBuilder.getTokenType() == PyTokenTypes.ELSE_KEYWORD) { myBuilder.advanceLexer(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); elsePart.done(PyElementTypes.ELSE_PART); } else elsePart.drop(); @@ -613,9 +605,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { getExpressionParser().parseExpression(true, true); checkMatches(PyTokenTypes.IN_KEYWORD, "'in' expected"); getExpressionParser().parseExpression(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); forPart.done(PyElementTypes.FOR_PART); } @@ -627,16 +617,12 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { if (!getExpressionParser().parseSingleExpression(false)) { myBuilder.error("expression expected"); } - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); whilePart.done(PyElementTypes.WHILE_PART); final PsiBuilder.Marker elsePart = myBuilder.mark(); if (myBuilder.getTokenType() == PyTokenTypes.ELSE_KEYWORD) { myBuilder.advanceLexer(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); elsePart.done(PyElementTypes.ELSE_PART); } else elsePart.drop(); @@ -648,9 +634,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { final PsiBuilder.Marker statement = myBuilder.mark(); final PsiBuilder.Marker tryPart = myBuilder.mark(); myBuilder.advanceLexer(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); tryPart.done(PyElementTypes.TRY_PART); boolean haveExceptClause = false; if (myBuilder.getTokenType() == PyTokenTypes.EXCEPT_KEYWORD) { @@ -670,17 +654,13 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { } } } - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); exceptBlock.done(PyElementTypes.EXCEPT_PART); } final PsiBuilder.Marker elsePart = myBuilder.mark(); if (myBuilder.getTokenType() == PyTokenTypes.ELSE_KEYWORD) { myBuilder.advanceLexer(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); elsePart.done(PyElementTypes.ELSE_PART); } else elsePart.drop(); @@ -688,9 +668,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { final PsiBuilder.Marker finallyPart = myBuilder.mark(); if (myBuilder.getTokenType() == PyTokenTypes.FINALLY_KEYWORD) { myBuilder.advanceLexer(); - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); finallyPart.done(PyElementTypes.FINALLY_PART); } else { @@ -704,6 +682,16 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { statement.done(PyElementTypes.TRY_EXCEPT_STATEMENT); } + private void parseColonAndSuite(ParsingScope scope) { + if (expectColon()) { + parseSuite(scope); + } + else { + final PsiBuilder.Marker mark = myBuilder.mark(); + mark.done(PyElementTypes.STATEMENT_LIST); + } + } + private void parseWithStatement(ParsingScope scope) { assertCurrentToken(PyTokenTypes.WITH_KEYWORD); final PsiBuilder.Marker statement = myBuilder.mark(); @@ -721,9 +709,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { break; } } - if (expectColon()) { - parseSuite(scope); - } + parseColonAndSuite(scope); statement.done(PyElementTypes.WITH_STATEMENT); } @@ -743,13 +729,7 @@ public class StatementParsing extends Parsing implements ITokenTypeRemapper { final PsiBuilder.Marker inheritMarker = myBuilder.mark(); inheritMarker.done(PyElementTypes.ARGUMENT_LIST); } - if (expectColon()) { - parseSuite(scope.withClass(true)); - } - else { - final PsiBuilder.Marker mark = myBuilder.mark(); - mark.done(PyElementTypes.STATEMENT_LIST); - } + parseColonAndSuite(scope.withClass(true)); classMarker.done(PyElementTypes.CLASS_DECLARATION); } diff --git a/python/src/com/jetbrains/python/psi/PyElementGenerator.java b/python/src/com/jetbrains/python/psi/PyElementGenerator.java index 440f407f0dac..7032c248e3d2 100644 --- a/python/src/com/jetbrains/python/psi/PyElementGenerator.java +++ b/python/src/com/jetbrains/python/psi/PyElementGenerator.java @@ -41,6 +41,11 @@ public abstract class PyElementGenerator { public abstract PyBinaryExpression createBinaryExpression(String s, PyExpression expr, PyExpression listLiteral); + /** + * @deprecated use the overload with language level specified + * @param text the text to create an expression from + * @return the expression + */ @Nullable public abstract PyExpression createExpressionFromText(String text); diff --git a/python/src/com/jetbrains/python/psi/PyFileElementType.java b/python/src/com/jetbrains/python/psi/PyFileElementType.java index ba9ebdfb660e..3db631d2ebc1 100644 --- a/python/src/com/jetbrains/python/psi/PyFileElementType.java +++ b/python/src/com/jetbrains/python/psi/PyFileElementType.java @@ -44,7 +44,7 @@ public class PyFileElementType extends IStubFileElementType { @Override public int getStubVersion() { - return 36; + return 37; } @Override diff --git a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionImpl.java index 9417d8f9f87d..0a00700ea36e 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionImpl.java @@ -1,5 +1,6 @@ package com.jetbrains.python.psi.impl; +import com.intellij.codeInsight.completion.CompletionUtil; import com.intellij.lang.ASTNode; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReference; @@ -179,7 +180,7 @@ public class PyCallExpressionImpl extends PyElementImpl implements PyCallExpress if (second_type instanceof PyClassType) { // imitate isinstance(second_arg, possible_class) PyClass second_class = ((PyClassType)second_type).getPyClass(); - if (first_class == second_class) { + if (CompletionUtil.getOriginalOrSelf(first_class) == second_class) { return getSuperClassUnionType(first_class); } if (second_class != null && second_class.isSubclass(first_class)) { diff --git a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java index a87e7820ce43..d6f0f69b750e 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java @@ -1,6 +1,8 @@ package com.jetbrains.python.psi.impl; +import com.intellij.codeInsight.completion.CompletionUtil; import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.Key; import com.intellij.openapi.util.NotNullLazyValue; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; @@ -832,12 +834,12 @@ public class PyClassImpl extends PyPresentableElementImpl implement // __init__ takes priority over all other methods PyFunctionImpl initMethod = (PyFunctionImpl)findMethodByName(PyNames.INIT, false); if (initMethod != null) { - collectInstanceAttributes(initMethod, result); + collectInstanceAttributes(initMethod, result, null); } final PyFunction[] methods = getMethods(); for (PyFunction method : methods) { if (!PyNames.INIT.equals(method.getName())) { - collectInstanceAttributes(method, result); + collectInstanceAttributes(method, result, null); } } @@ -845,7 +847,9 @@ public class PyClassImpl extends PyPresentableElementImpl implement return new ArrayList(expressions); } - private static void collectInstanceAttributes(PyFunction method, final Map result) { + private static void collectInstanceAttributes(@NotNull PyFunction method, + @NotNull final Map result, + @Nullable PsiElement anchor) { final PyParameter[] params = method.getParameterList().getParameters(); if (params.length == 0) { return; @@ -859,25 +863,49 @@ public class PyClassImpl extends PyPresentableElementImpl implement } } } + else if (anchor != null) { + PyResolveUtil.treeCrawlUp(new PsiScopeProcessor() { + @Override + public boolean execute(PsiElement element, ResolveState state) { + if (element instanceof PyAssignmentStatement) { + collectNewTargets(result, (PyAssignmentStatement)element); + } + return true; + } + + @Nullable + @Override + public T getHint(Key hintKey) { + return null; + } + + @Override + public void handleEvent(Event event, @Nullable Object associated) { + } + }, false, anchor, method); + } else { - // NOTE: maybe treeCrawlUp would be more precise, but currently it works well enough; don't care. final PyStatementList statementList = method.getStatementList(); if (statementList != null) { statementList.accept(new PyRecursiveElementVisitor() { public void visitPyAssignmentStatement(final PyAssignmentStatement node) { super.visitPyAssignmentStatement(node); - final PyExpression[] targets = node.getTargets(); - for (PyExpression target : targets) { - if (PyUtil.isInstanceAttribute(target) && !result.containsKey(target.getName())) { - result.put(target.getName(), (PyTargetExpression) target); - } - } + collectNewTargets(result, node); } }); } } } + private static void collectNewTargets(Map collected, PyAssignmentStatement node) { + final PyExpression[] targets = node.getTargets(); + for (PyExpression target : targets) { + if (target instanceof PyTargetExpression && PyUtil.isInstanceAttribute(target) && !collected.containsKey(target.getName())) { + collected.put(target.getName(), (PyTargetExpression)target); + } + } + } + public boolean isNewStyleClass() { return myNewStyle.getValue().getValue(); } @@ -947,8 +975,9 @@ public class PyClassImpl extends PyPresentableElementImpl implement public boolean processInstanceLevelDeclarations(PsiScopeProcessor processor, @Nullable PyExpression location) { Map declarationsInMethod = new HashMap(); PyFunction instanceMethod = PsiTreeUtil.getParentOfType(location, PyFunction.class); - if (instanceMethod != null && instanceMethod.getContainingClass() == this) { - collectInstanceAttributes(instanceMethod, declarationsInMethod); + final PyClass containingClass = instanceMethod != null ? instanceMethod.getContainingClass() : null; + if (instanceMethod != null && containingClass != null && CompletionUtil.getOriginalElement(containingClass) == this) { + collectInstanceAttributes(instanceMethod, declarationsInMethod, location); for (PyTargetExpression targetExpression : declarationsInMethod.values()) { if (!processor.execute(targetExpression, ResolveState.initial())) { return false; diff --git a/python/src/com/jetbrains/python/psi/types/PyClassType.java b/python/src/com/jetbrains/python/psi/types/PyClassType.java index 09020e4d0b14..a1ffe2489b24 100644 --- a/python/src/com/jetbrains/python/psi/types/PyClassType.java +++ b/python/src/com/jetbrains/python/psi/types/PyClassType.java @@ -13,6 +13,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.ProcessingContext; import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.python.PyNames; import com.jetbrains.python.codeInsight.PyDynamicMember; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.impl.PyBuiltinCache; @@ -270,7 +271,7 @@ public class PyClassType extends UserDataHolderBase implements PyCallableType { if (containingClass != null) { containingClass = CompletionUtil.getOriginalElement(containingClass); } - boolean withinOurClass = containingClass == getPyClass(); + boolean withinOurClass = containingClass == getPyClass() || isInSuperCall(expressionHook); final CompletionVariantsProcessor processor = new CompletionVariantsProcessor( expressionHook, new PyResolveUtil.FilterNotInstance(myClass), null @@ -303,6 +304,14 @@ public class PyClassType extends UserDataHolderBase implements PyCallableType { } } + private static boolean isInSuperCall(PyExpression hook) { + if (hook instanceof PyReferenceExpression) { + final PyExpression qualifier = ((PyReferenceExpression)hook).getQualifier(); + return qualifier instanceof PyCallExpression && ((PyCallExpression) qualifier).isCalleeText(PyNames.SUPER); + } + return false; + } + private void addInheritedMembers(String name, PyExpression expressionHook, ProcessingContext context, List ret) { for (PyClass ancestor : myClass.getSuperClasses()) { Object[] ancestry = (new PyClassType(ancestor, true)).getCompletionVariants(name, expressionHook, context); diff --git a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java index dd988eca90d3..132eca2f5865 100644 --- a/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java +++ b/python/src/com/jetbrains/python/refactoring/introduce/IntroduceHandler.java @@ -361,6 +361,9 @@ abstract public class IntroduceHandler implements RefactoringActionHandler { final PyAssignmentStatement statement = performRefactoring(operation); PyTargetExpression target = (PyTargetExpression) statement.getTargets() [0]; final List occurrences = operation.getOccurrences(); + if (occurrences.size() == 0) { + operation.getEditor().getCaretModel().moveToOffset(target.getTextRange().getStartOffset()); + } final InplaceVariableIntroducer introducer = new PyInplaceVariableIntroducer(target, operation, occurrences); introducer.performInplaceRename(false, new LinkedHashSet(operation.getSuggestedNames())); diff --git a/python/src/com/jetbrains/python/sdk/CPythonSdkFlavor.java b/python/src/com/jetbrains/python/sdk/CPythonSdkFlavor.java new file mode 100644 index 000000000000..f318ce9e2100 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/CPythonSdkFlavor.java @@ -0,0 +1,7 @@ +package com.jetbrains.python.sdk; + +/** + * @author yole + */ +public abstract class CPythonSdkFlavor extends PythonSdkFlavor { +} diff --git a/python/src/com/jetbrains/python/sdk/MacPythonSdkFlavor.java b/python/src/com/jetbrains/python/sdk/MacPythonSdkFlavor.java index 182f63dc0689..fece725e8c6f 100644 --- a/python/src/com/jetbrains/python/sdk/MacPythonSdkFlavor.java +++ b/python/src/com/jetbrains/python/sdk/MacPythonSdkFlavor.java @@ -10,7 +10,7 @@ import java.util.List; /** * @author yole */ -public class MacPythonSdkFlavor extends PythonSdkFlavor { +public class MacPythonSdkFlavor extends CPythonSdkFlavor { private MacPythonSdkFlavor() { } diff --git a/python/src/com/jetbrains/python/sdk/PythonSdkType.java b/python/src/com/jetbrains/python/sdk/PythonSdkType.java index 6a7064461d7f..064531c11958 100644 --- a/python/src/com/jetbrains/python/sdk/PythonSdkType.java +++ b/python/src/com/jetbrains/python/sdk/PythonSdkType.java @@ -349,7 +349,6 @@ public class PythonSdkType extends SdkType { } private boolean switchPathToInterpreter(Sdk currentSdk, String... variants) { - final Project project = PlatformDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext()); File sdk_file = new File(currentSdk.getHomePath()); final String sdk_name = currentSdk.getName(); boolean success = false; @@ -369,7 +368,7 @@ public class PythonSdkType extends SdkType { if (!success) { ApplicationManager.getApplication().invokeLater(new Runnable() { public void run() { - Messages.showWarningDialog(project, + Messages.showWarningDialog( "Failed to convert Python SDK '" + sdk_name + "'\nplease delete and re-create it", "Converting Python SDK" ); diff --git a/python/src/com/jetbrains/python/sdk/UnixPythonSdkFlavor.java b/python/src/com/jetbrains/python/sdk/UnixPythonSdkFlavor.java index 863a012fbd38..34d33a92064d 100644 --- a/python/src/com/jetbrains/python/sdk/UnixPythonSdkFlavor.java +++ b/python/src/com/jetbrains/python/sdk/UnixPythonSdkFlavor.java @@ -10,7 +10,7 @@ import java.util.List; /** * @author yole */ -public class UnixPythonSdkFlavor extends PythonSdkFlavor { +public class UnixPythonSdkFlavor extends CPythonSdkFlavor { private UnixPythonSdkFlavor() { } diff --git a/python/src/com/jetbrains/python/sdk/WinPythonSdkFlavor.java b/python/src/com/jetbrains/python/sdk/WinPythonSdkFlavor.java index f270e4826ae7..47ca6cbec3b3 100644 --- a/python/src/com/jetbrains/python/sdk/WinPythonSdkFlavor.java +++ b/python/src/com/jetbrains/python/sdk/WinPythonSdkFlavor.java @@ -13,7 +13,7 @@ import java.util.TreeSet; /** * @author yole */ -public class WinPythonSdkFlavor extends PythonSdkFlavor { +public class WinPythonSdkFlavor extends CPythonSdkFlavor { public static WinPythonSdkFlavor INSTANCE = new WinPythonSdkFlavor(); private WinPythonSdkFlavor() { diff --git a/python/testData/completion/fieldReassignment.after.py b/python/testData/completion/fieldReassignment.after.py new file mode 100644 index 000000000000..0f4a09fa6f5d --- /dev/null +++ b/python/testData/completion/fieldReassignment.after.py @@ -0,0 +1,9 @@ +class C1(object): + def method1(self): + pass + +class Test(object): + def __init__(self, x): + self.x = x + self.x = C1() + self.x.method1() \ No newline at end of file diff --git a/python/testData/completion/fieldReassignment.py b/python/testData/completion/fieldReassignment.py new file mode 100644 index 000000000000..86a724683dce --- /dev/null +++ b/python/testData/completion/fieldReassignment.py @@ -0,0 +1,9 @@ +class C1(object): + def method1(self): + pass + +class Test(object): + def __init__(self, x): + self.x = x + self.x = C1() + self.x.meth \ No newline at end of file diff --git a/python/testData/completion/superInit.after.py b/python/testData/completion/superInit.after.py new file mode 100644 index 000000000000..b31494d3761f --- /dev/null +++ b/python/testData/completion/superInit.after.py @@ -0,0 +1,8 @@ +class Matrix(object): + def __init__(self): + pass + + +class Vector3(Matrix): + def __init__(self): + super(Vector3, self).__init__() diff --git a/python/testData/completion/superInit.py b/python/testData/completion/superInit.py new file mode 100644 index 000000000000..8cabeb93c247 --- /dev/null +++ b/python/testData/completion/superInit.py @@ -0,0 +1,8 @@ +class Matrix(object): + def __init__(self): + pass + + +class Vector3(Matrix): + def __init__(self): + super(Vector3, self).__in diff --git a/python/testData/inspections/PyArgumentEqualDefaultInspection/test.py b/python/testData/inspections/PyArgumentEqualDefaultInspection/test.py index f95271a0abba..6103a437e13c 100644 --- a/python/testData/inspections/PyArgumentEqualDefaultInspection/test.py +++ b/python/testData/inspections/PyArgumentEqualDefaultInspection/test.py @@ -3,9 +3,6 @@ def foo(a, b = 345, c = 1): foo(1, 345, 22) -a = dict() -a.get(1, None) - def foo(a = None): pass foo(a = None) @@ -67,3 +64,6 @@ kw = a() kw['customerPaymentProfileId'] = kw.get("customerPaymentProfileId", None) + +{1: 2}.get('foo', None) #pass +{1: 2}.pop('foo', None) #pass diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/unresolvedImportedModule.py b/python/testData/inspections/PyUnresolvedReferencesInspection/unresolvedImportedModule.py new file mode 100644 index 000000000000..9a9bd1864596 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/unresolvedImportedModule.py @@ -0,0 +1,2 @@ +import deliverance.proxycommand +deliverance.proxycommand.main() diff --git a/python/testData/psi/CallInAssignment.py b/python/testData/psi/CallInAssignment.py new file mode 100644 index 000000000000..4c117400735e --- /dev/null +++ b/python/testData/psi/CallInAssignment.py @@ -0,0 +1 @@ +etree.SubElement(dictionary, u'Name').text = dict_name \ No newline at end of file diff --git a/python/testData/psi/CallInAssignment.txt b/python/testData/psi/CallInAssignment.txt new file mode 100644 index 000000000000..69fb37582fbe --- /dev/null +++ b/python/testData/psi/CallInAssignment.txt @@ -0,0 +1,25 @@ +PyFile:CallInAssignment.py + PyAssignmentStatement + PyTargetExpression: text + PyCallExpression: etree.SubElement + PyReferenceExpression: SubElement + PyReferenceExpression: etree + PsiElement(Py:IDENTIFIER)('etree') + PsiElement(Py:DOT)('.') + PsiElement(Py:IDENTIFIER)('SubElement') + PyArgumentList + PsiElement(Py:LPAR)('(') + PyReferenceExpression: dictionary + PsiElement(Py:IDENTIFIER)('dictionary') + PsiElement(Py:COMMA)(',') + PsiWhiteSpace(' ') + PyStringLiteralExpression: Name + PsiElement(Py:SINGLE_QUOTED_STRING)('u'Name'') + PsiElement(Py:RPAR)(')') + PsiElement(Py:DOT)('.') + PsiElement(Py:IDENTIFIER)('text') + PsiWhiteSpace(' ') + PsiElement(Py:EQ)('=') + PsiWhiteSpace(' ') + PyReferenceExpression: dict_name + PsiElement(Py:IDENTIFIER)('dict_name') \ No newline at end of file diff --git a/python/testData/psi/IncompleteFor.txt b/python/testData/psi/IncompleteFor.txt index 8f7cfead6965..f785bb5d0cc0 100644 --- a/python/testData/psi/IncompleteFor.txt +++ b/python/testData/psi/IncompleteFor.txt @@ -22,6 +22,8 @@ PyFile:IncompleteFor.py PsiElement(Py:IDENTIFIER)('ba') PsiErrorElement:colon expected + PyStatementList + PsiWhiteSpace('\n\n') PyFunction('bar') PsiElement(Py:DEF_KEYWORD)('def') diff --git a/python/testData/psi/IncompleteStatementList.txt b/python/testData/psi/IncompleteStatementList.txt index b0dd35773fe8..89aa12e134d4 100644 --- a/python/testData/psi/IncompleteStatementList.txt +++ b/python/testData/psi/IncompleteStatementList.txt @@ -17,6 +17,8 @@ PyFile:IncompleteStatementList.py PsiElement(Py:IDENTIFIER)('ba') PsiErrorElement:colon expected + PyStatementList + PsiWhiteSpace('\n\n') PyFunction('bar') PsiElement(Py:DEF_KEYWORD)('def') diff --git a/python/testData/psi/MissingParenInCall.txt b/python/testData/psi/MissingParenInCall.txt index 097c88c84de4..01190ffc3dc4 100644 --- a/python/testData/psi/MissingParenInCall.txt +++ b/python/testData/psi/MissingParenInCall.txt @@ -28,7 +28,9 @@ PyFile:MissingParenInCall.py PsiErrorElement:colon expected - PsiWhiteSpace('\n\n') + PsiWhiteSpace('\n\n') + PyStatementList + PyFunction('clean') PsiElement(Py:DEF_KEYWORD)('def') PsiWhiteSpace(' ') diff --git a/python/testData/psi/WithStatement2.txt b/python/testData/psi/WithStatement2.txt index baed0b3d3bb4..e40654dfa532 100644 --- a/python/testData/psi/WithStatement2.txt +++ b/python/testData/psi/WithStatement2.txt @@ -74,6 +74,8 @@ PyFile:WithStatement2.py PsiErrorElement:colon expected + PyStatementList + PsiElement(Py:EQ)('=') PsiErrorElement:statement expected, found Py:EQ diff --git a/python/testData/refactoring/introduceVariable/functionOccurrences.after.py b/python/testData/refactoring/introduceVariable/functionOccurrences.after.py new file mode 100644 index 000000000000..6f00076ae252 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/functionOccurrences.after.py @@ -0,0 +1,13 @@ +import xml.etree.ElementTree as etree +def entries_to_xml(entries, dict_id, dict_name, closed): + dictionary = etree.Element(u'Dictionary', IDName=dict_id) + a = etree.SubElement + a(dictionary, u'Name').text = dict_name + a(dictionary, u'Closed').text = repr(closed).lower() + a(dictionary, u'Action').text = u'false' + terms = a(dictionary, u'Terms') + for i, entry in enumerate(entries): + term = a(terms, u'Term') + a(term, u'Category') + words = a(term, u'Words') + return dictionary \ No newline at end of file diff --git a/python/testData/refactoring/introduceVariable/functionOccurrences.py b/python/testData/refactoring/introduceVariable/functionOccurrences.py new file mode 100644 index 000000000000..2e9835427fb1 --- /dev/null +++ b/python/testData/refactoring/introduceVariable/functionOccurrences.py @@ -0,0 +1,12 @@ +import xml.etree.ElementTree as etree +def entries_to_xml(entries, dict_id, dict_name, closed): + dictionary = etree.Element(u'Dictionary', IDName=dict_id) + etree.SubElement(dictionary, u'Name').text = dict_name + etree.SubElement(dictionary, u'Closed').text = repr(closed).lower() + etree.SubElement(dictionary, u'Action').text = u'false' + terms = etree.SubElement(dictionary, u'Terms') + for i, entry in enumerate(entries): + term = etree.SubElement(terms, u'Term') + etree.SubElement(term, u'Category') + words = etree.SubElement(term, u'Words') + return dictionary \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/PyTypeTest.java b/python/testSrc/com/jetbrains/python/PyTypeTest.java index b41a7c1d59b5..cc1e0c2510f3 100644 --- a/python/testSrc/com/jetbrains/python/PyTypeTest.java +++ b/python/testSrc/com/jetbrains/python/PyTypeTest.java @@ -274,7 +274,25 @@ public class PyTypeTest extends PyTestCase { " self.assertIsInstance(x, int)\n" + " expr = x\n"); } - + + // PY-4279 + public void testFieldReassignment() { + doTest("C1", + "class C1(object):\n" + + " def m1(self):\n" + + " pass\n" + + "\n" + + "class C2(object):\n" + + " def m2(self):\n" + + " pass\n" + + "\n" + + "class Test(object):\n" + + " def __init__(self, param1):\n" + + " self.x = param1\n" + + " self.x = C1()\n" + + " expr = self.x\n"); + } + public void testSOEOnRecursiveCall() { PyExpression expr = parseExpr("def foo(x): return foo(x)\n" + "expr = foo(1)"); diff --git a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java index a009105fed41..d228010b3317 100644 --- a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java +++ b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java @@ -469,4 +469,13 @@ public class PythonCompletionTest extends PyTestCase { public void testCompleteBeforeSyntaxError() { // PY-3792 doTest(); } + + // PY-4279 + public void testFieldReassignment() { + doTest(); + } + + public void testSuperInit() { // PY-5066 + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/PythonLexerTest.java b/python/testSrc/com/jetbrains/python/PythonLexerTest.java index 27ad33ed7add..98424cdc3a53 100644 --- a/python/testSrc/com/jetbrains/python/PythonLexerTest.java +++ b/python/testSrc/com/jetbrains/python/PythonLexerTest.java @@ -196,6 +196,10 @@ public class PythonLexerTest extends PyLexerTestCase { "Py:INDENT", "Py:PASS_KEYWORD", "Py:STATEMENT_BREAK", "Py:LINE_BREAK", "Py:END_OF_LINE_COMMENT", "Py:DEDENT", "Py:LINE_BREAK"); } + + public void testIndentAtStartOfFile() { // PY-4941 + doTest(" a", "Py:SPACE", "Py:INDENT", "Py:IDENTIFIER"); + } private static void doTest(String text, String... expectedTokens) { doLexerTest(text, new PythonIndentingLexer(), expectedTokens); diff --git a/python/testSrc/com/jetbrains/python/PythonParsingTest.java b/python/testSrc/com/jetbrains/python/PythonParsingTest.java index 11b91950e32b..8db3eb00460d 100644 --- a/python/testSrc/com/jetbrains/python/PythonParsingTest.java +++ b/python/testSrc/com/jetbrains/python/PythonParsingTest.java @@ -305,6 +305,10 @@ public class PythonParsingTest extends ParsingTestCase { public void testIncompleteFor() { // PY-3792 doTest(); } + + public void testCallInAssignment() { // PY-5062 + doTest(); + } public void doTest() { doTest(LanguageLevel.PYTHON25); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java index c826935e3476..0b158fc1c5ab 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java @@ -64,6 +64,10 @@ public class PyUnresolvedReferencesInspectionTest extends PyTestCase { public void testTypeAssertions() { doTest(); } + + public void testUnresolvedImportedModule() { // PY-2075 + doTest(); + } public void testImportToContainingFile() { // PY-4372 myFixture.copyFileToProject("inspections/PyUnresolvedReferencesInspection/__init__.py", "PyUnresolvedReferencesInspection/__init__.py"); diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java index d0cfab971f90..2afb82e01462 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyIntroduceVariableTest.java @@ -69,6 +69,10 @@ public class PyIntroduceVariableTest extends PyIntroduceTestCase { public void testOneSidedSelection() { // PY-4456 doTestCannotPerform(); } + + public void testFunctionOccurrences() { // PY-5062 + doTest(); + } private void doTestCannotPerform() { boolean thrownExpectedException = false;