From efc2abc07453ec907389d6bacd05f8b7cfc4e3af Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Wed, 10 Aug 2011 15:47:44 +0200 Subject: [PATCH] rename keyword arguments in calls when a named parameter is renamed (PY-3890) --- python/src/META-INF/python-plugin-common.xml | 3 + .../python/psi/PyElementGenerator.java | 4 +- .../psi/impl/PyElementGeneratorImpl.java | 6 +- .../psi/impl/PyKeywordArgumentImpl.java | 10 +++ .../impl/PyKeywordArgumentManipulator.java | 29 ++++++++ .../psi/impl/PyKeywordArgumentReference.java | 68 +++++++++++++++++++ .../PyKeywordArgumentSearchExecutor.java | 44 ++++++++++++ .../refactoring/RefactoringProvider.java | 4 -- .../rename/renameKeywordArgument.py | 4 ++ .../rename/renameKeywordArgument_after.py | 4 ++ python/testData/resolve/KeywordArgument.py | 5 ++ .../com/jetbrains/python/PyResolveTest.java | 4 ++ .../python/refactoring/PyRenameTest.java | 4 ++ 13 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentManipulator.java create mode 100644 python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentReference.java create mode 100644 python/src/com/jetbrains/python/psi/search/PyKeywordArgumentSearchExecutor.java create mode 100644 python/testData/refactoring/rename/renameKeywordArgument.py create mode 100644 python/testData/refactoring/rename/renameKeywordArgument_after.py create mode 100644 python/testData/resolve/KeywordArgument.py diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index 4d0e8d9a30c7..8f51fa96adb4 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -253,6 +253,7 @@ order="before completionAutoPopup"/> + @@ -276,6 +277,8 @@ + diff --git a/python/src/com/jetbrains/python/psi/PyElementGenerator.java b/python/src/com/jetbrains/python/psi/PyElementGenerator.java index 54c420b919d0..69a21d476235 100644 --- a/python/src/com/jetbrains/python/psi/PyElementGenerator.java +++ b/python/src/com/jetbrains/python/psi/PyElementGenerator.java @@ -41,7 +41,9 @@ public abstract class PyElementGenerator { public abstract PyBinaryExpression createBinaryExpression(String s, PyExpression expr, PyExpression listLiteral); - public abstract PyExpression createExpressionFromText(String text); + public abstract PyExpression createExpressionFromText(String text); + + public abstract PyExpression createExpressionFromText(final LanguageLevel languageLevel, String text); public abstract PsiElement insertItemIntoList(PyElement list, @Nullable PyExpression afterThis, PyExpression toInsert) throws IncorrectOperationException; diff --git a/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java b/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java index 3f6cbfd96fb4..49b38a1badf6 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java @@ -191,7 +191,11 @@ public class PyElementGeneratorImpl extends PyElementGenerator { } public PyExpression createExpressionFromText(final String text) { - final PsiFile dummyFile = createDummyFile(LanguageLevel.getDefault(), text); + return createExpressionFromText(LanguageLevel.getDefault(), text); + } + + public PyExpression createExpressionFromText(final LanguageLevel languageLevel, final String text) { + final PsiFile dummyFile = createDummyFile(languageLevel, text); final PyExpressionStatement expressionStatement = (PyExpressionStatement)dummyFile.getFirstChild(); return expressionStatement.getExpression(); } diff --git a/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentImpl.java b/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentImpl.java index 27d0ddf3f9be..91c14a15db2c 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentImpl.java @@ -1,6 +1,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiReference; import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.psi.PyExpression; @@ -43,4 +44,13 @@ public class PyKeywordArgumentImpl extends PyElementImpl implements PyKeywordArg final PyExpression e = getValueExpression(); return e != null ? e.getType(context) : null; } + + @Override + public PsiReference getReference() { + final ASTNode keywordNode = getKeywordNode(); + if (keywordNode != null) { + return new PyKeywordArgumentReference(this, keywordNode.getTextRange().shiftRight(-getTextRange().getStartOffset())); + } + return null; + } } diff --git a/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentManipulator.java b/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentManipulator.java new file mode 100644 index 000000000000..9dbc90cb0883 --- /dev/null +++ b/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentManipulator.java @@ -0,0 +1,29 @@ +package com.jetbrains.python.psi.impl; + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.AbstractElementManipulator; +import com.intellij.util.IncorrectOperationException; +import com.jetbrains.python.psi.LanguageLevel; +import com.jetbrains.python.psi.PyCallExpression; +import com.jetbrains.python.psi.PyElementGenerator; +import com.jetbrains.python.psi.PyKeywordArgument; + +/** + * @author yole + */ +public class PyKeywordArgumentManipulator extends AbstractElementManipulator { + @Override + public PyKeywordArgument handleContentChange(PyKeywordArgument element, TextRange range, String newContent) throws IncorrectOperationException { + final ASTNode keywordNode = element.getKeywordNode(); + if (keywordNode != null && keywordNode.getTextRange().shiftRight(-element.getTextRange().getStartOffset()).equals(range)) { + final LanguageLevel langLevel = LanguageLevel.forElement(element); + final PyElementGenerator generator = PyElementGenerator.getInstance(element.getProject()); + final PyCallExpression callExpression = (PyCallExpression) generator.createExpressionFromText(langLevel, "foo(" + newContent + "=None)"); + final PyKeywordArgument kwArg = callExpression.getArgumentList().getKeywordArgument(newContent); + element.getKeywordNode().getPsi().replace(kwArg.getKeywordNode().getPsi()); + return element; + } + throw new IncorrectOperationException("unsupported manipulation on keyword argument"); + } +} diff --git a/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentReference.java b/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentReference.java new file mode 100644 index 000000000000..8da109b8affc --- /dev/null +++ b/python/src/com/jetbrains/python/psi/impl/PyKeywordArgumentReference.java @@ -0,0 +1,68 @@ +package com.jetbrains.python.psi.impl; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.python.psi.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author yole + */ +public class PyKeywordArgumentReference extends PsiReferenceBase.Poly { + public PyKeywordArgumentReference(@NotNull PyKeywordArgument element, TextRange textRange) { + super(element, textRange, true); + } + + @NotNull + @Override + public ResolveResult[] multiResolve(boolean incompleteCode) { + PyCallExpression call = PsiTreeUtil.getParentOfType(myElement, PyCallExpression.class); + assert call != null; + final PyExpression callee = call.getCallee(); + if (callee == null) return ResolveResult.EMPTY_ARRAY; + final PsiPolyVariantReference calleeReference = (PsiPolyVariantReference) callee.getReference(); + if (calleeReference == null) return ResolveResult.EMPTY_ARRAY; + final ResolveResult[] calleeCandidates = calleeReference.multiResolve(incompleteCode); + List resultList = new ArrayList(); + for (ResolveResult calleeCandidate : calleeCandidates) { + if (!calleeCandidate.isValidResult()) continue; + final PsiElement element = calleeCandidate.getElement(); + if (element == null) continue; + final PyFunction calleeFunction = resolveToFunction(element); + if (calleeFunction != null) { + final PsiElement result = calleeFunction.getParameterList().getElementNamed(myElement.getKeyword()); + if (result != null) { + resultList.add(new PsiElementResolveResult(result)); + } + } + } + return resultList.toArray(new ResolveResult[resultList.size()]); + } + + @Nullable + private static PyFunction resolveToFunction(PsiElement element) { + if (element instanceof PyFunction) { + return (PyFunction) element; + } + if (element instanceof PyTargetExpression) { + final PyExpression assignedValue = ((PyTargetExpression)element).findAssignedValue(); + return resolveToFunction(assignedValue); + } + if (element instanceof PyReferenceExpression) { + final PsiElement resolveResult = ((PyReferenceExpression)element).getReference().resolve(); + return resolveToFunction(resolveResult); + } + return null; + } + + @NotNull + @Override + public Object[] getVariants() { + return new Object[0]; //To change body of implemented methods use File | Settings | File Templates. + } +} diff --git a/python/src/com/jetbrains/python/psi/search/PyKeywordArgumentSearchExecutor.java b/python/src/com/jetbrains/python/psi/search/PyKeywordArgumentSearchExecutor.java new file mode 100644 index 000000000000..fba0d68c573b --- /dev/null +++ b/python/src/com/jetbrains/python/psi/search/PyKeywordArgumentSearchExecutor.java @@ -0,0 +1,44 @@ +package com.jetbrains.python.psi.search; + +import com.intellij.openapi.application.QueryExecutorBase; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.search.searches.ReferencesSearch; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.Processor; +import com.jetbrains.python.psi.*; +import org.jetbrains.annotations.NotNull; + +/** + * @author yole + */ +public class PyKeywordArgumentSearchExecutor extends QueryExecutorBase { + @Override + public void processQuery(@NotNull ReferencesSearch.SearchParameters queryParameters, @NotNull final Processor consumer) { + final PsiElement element = queryParameters.getElementToSearch(); + if (!(element instanceof PyNamedParameter)) { + return; + } + PyFunction owner = PsiTreeUtil.getParentOfType(element, PyFunction.class); + if (owner == null) { + return; + } + ReferencesSearch.search(owner, queryParameters.getScope()).forEach(new Processor() { + @Override + public boolean process(PsiReference reference) { + final PsiElement refElement = reference.getElement(); + final PyCallExpression call = PsiTreeUtil.getParentOfType(refElement, PyCallExpression.class); + if (call != null && PsiTreeUtil.isAncestor(call.getCallee(), refElement, false)) { + final PyArgumentList argumentList = call.getArgumentList(); + if (argumentList != null) { + final PyKeywordArgument keywordArgument = argumentList.getKeywordArgument(((PyNamedParameter)element).getName()); + if (keywordArgument != null) { + return consumer.process(keywordArgument.getReference()); + } + } + } + return true; + } + }); + } +} diff --git a/python/src/com/jetbrains/python/refactoring/RefactoringProvider.java b/python/src/com/jetbrains/python/refactoring/RefactoringProvider.java index 47bf49641deb..1d96a3126d58 100644 --- a/python/src/com/jetbrains/python/refactoring/RefactoringProvider.java +++ b/python/src/com/jetbrains/python/refactoring/RefactoringProvider.java @@ -6,7 +6,6 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.refactoring.RefactoringActionHandler; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyFunction; -import com.jetbrains.python.psi.PyNamedParameter; import com.jetbrains.python.psi.PyTargetExpression; import com.jetbrains.python.refactoring.classes.extractSuperclass.PyExtractSuperclassHandler; import com.jetbrains.python.refactoring.classes.pullUp.PyPullUpHandler; @@ -62,9 +61,6 @@ public class RefactoringProvider extends RefactoringSupportProvider { if (element instanceof PyTargetExpression || element instanceof PyFunction || element instanceof PyClass) { return true; } - if (element instanceof PyNamedParameter) { - return containingFunction.getContainingClass() == null; - } } return false; } diff --git a/python/testData/refactoring/rename/renameKeywordArgument.py b/python/testData/refactoring/rename/renameKeywordArgument.py new file mode 100644 index 000000000000..e063e60981b6 --- /dev/null +++ b/python/testData/refactoring/rename/renameKeywordArgument.py @@ -0,0 +1,4 @@ +def foo(bar=True): + pass + +foo(bar=False) \ No newline at end of file diff --git a/python/testData/refactoring/rename/renameKeywordArgument_after.py b/python/testData/refactoring/rename/renameKeywordArgument_after.py new file mode 100644 index 000000000000..8cc1cc40a686 --- /dev/null +++ b/python/testData/refactoring/rename/renameKeywordArgument_after.py @@ -0,0 +1,4 @@ +def foo(baz=True): + pass + +foo(baz=False) \ No newline at end of file diff --git a/python/testData/resolve/KeywordArgument.py b/python/testData/resolve/KeywordArgument.py new file mode 100644 index 000000000000..ee370714b131 --- /dev/null +++ b/python/testData/resolve/KeywordArgument.py @@ -0,0 +1,5 @@ +def foo(bar=True): + pass + +foo(bar=False) +# diff --git a/python/testSrc/com/jetbrains/python/PyResolveTest.java b/python/testSrc/com/jetbrains/python/PyResolveTest.java index e4a933e29aa4..5d5d0df7b060 100644 --- a/python/testSrc/com/jetbrains/python/PyResolveTest.java +++ b/python/testSrc/com/jetbrains/python/PyResolveTest.java @@ -417,4 +417,8 @@ public class PyResolveTest extends PyResolveTestCase { public void testDontResolveTargetToBuiltins() { // PY-4256 assertResolvesTo(PyTargetExpression.class, "str"); } + + public void testKeywordArgument() { + assertResolvesTo(PyNamedParameter.class, "bar"); + } } \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java index 06abfec34864..3c92a6a60612 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java @@ -77,6 +77,10 @@ public class PyRenameTest extends PyLightFixtureTestCase { public void testRenameSlots() { // PY-4195 doTest("bacon"); } + + public void testRenameKeywordArgument() { // PY-3890 + doTest("baz"); + } public void testClassNameConflict() { // PY-2390 doRenameConflictTest("Foo", "A class named 'Foo' is already defined in classNameConflict.py");