diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index ff3dc3ae8d56..bed99bd64d91 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -43,6 +43,7 @@ + diff --git a/python/src/com/jetbrains/python/codeInsight/PyMethodNameTypedHandler.java b/python/src/com/jetbrains/python/codeInsight/PyMethodNameTypedHandler.java new file mode 100644 index 000000000000..a16636998e1d --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/PyMethodNameTypedHandler.java @@ -0,0 +1,69 @@ +package com.jetbrains.python.codeInsight; + +import com.intellij.codeInsight.editorActions.TypedHandlerDelegate; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorModificationUtil; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.jetbrains.python.PyNames; +import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.PythonFileType; +import com.jetbrains.python.psi.PyFunction; +import com.jetbrains.python.psi.PyStringLiteralExpression; +import com.jetbrains.python.psi.PyUtil; + +/** + * Adds appropriate first parameter to a freshly-typed method declaration. + *
+ * User: dcheryasov + * Date: 11/29/10 12:44 AM + */ +public class PyMethodNameTypedHandler extends TypedHandlerDelegate { + @Override + public Result beforeCharTyped(char character, Project project, Editor editor, PsiFile file, FileType fileType) { + if (!(fileType instanceof PythonFileType)) return Result.CONTINUE; // else we'd mess up with other file types! + if (character == '(') { + final Document document = editor.getDocument(); + final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); + final int offset = editor.getCaretModel().getOffset(); + + PsiElement token = file.findElementAt(offset - 1); + if (token == null) return Result.CONTINUE; // sanity check: beyond EOL + + final ASTNode token_node = token.getNode(); + if (token_node != null && token_node.getElementType() == PyTokenTypes.IDENTIFIER) { + PsiElement maybe_def = PyUtil.getFirstNonCommentBefore(token.getPrevSibling()); + if (maybe_def != null) { + ASTNode def_node = maybe_def.getNode(); + if (def_node != null && def_node.getElementType() == PyTokenTypes.DEF_KEYWORD) { + PsiElement maybe_func = token.getParent(); + if (maybe_func instanceof PyFunction) { + PyFunction func = (PyFunction)maybe_func; + PyUtil.MethodFlags flags = PyUtil.MethodFlags.of(func); + if (flags != null) { + // we're in a method + // TODO: all string constants go to Settings + String pname = flags.isClassMethod() || flags.isMetaclassMethod() ? "cls" : "self"; + final boolean is_new = PyNames.NEW.equals(func.getName()); + if (flags.isMetaclassMethod() && is_new) pname = "typ"; + else if (flags.isClassMethod() || is_new) pname = "cls"; + else if (flags.isStaticMethod()) pname=""; + documentManager.commitDocument(document); + // TODO: only print the ")" if Settings require it + EditorModificationUtil.typeInStringAtCaretHonorBlockSelection(editor, "("+pname+"):", true); + editor.getCaretModel().moveToOffset(offset + 1 + pname.length()); // right after param name + return Result.STOP; + } + } + } + } + } + } + return Result.CONTINUE; // the default + } +} diff --git a/python/src/com/jetbrains/python/inspections/PyMethodParametersInspection.java b/python/src/com/jetbrains/python/inspections/PyMethodParametersInspection.java index 74ff3aecb5a9..6b154a475039 100644 --- a/python/src/com/jetbrains/python/inspections/PyMethodParametersInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyMethodParametersInspection.java @@ -11,16 +11,10 @@ import com.jetbrains.python.PyNames; import com.jetbrains.python.actions.AddSelfQuickFix; import com.jetbrains.python.actions.RenameParameterQuickFix; import com.jetbrains.python.psi.*; -import com.jetbrains.python.psi.impl.PyBuiltinCache; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -import java.util.Set; - -import static com.jetbrains.python.psi.PyFunction.Flag.CLASSMETHOD; -import static com.jetbrains.python.psi.PyFunction.Flag.STATICMETHOD; - /** * Looks for the 'self' or its equivalents. * @author dcheryasov @@ -50,34 +44,16 @@ public class PyMethodParametersInspection extends PyInspection { } - private static boolean among(@NotNull String what, String... variants) { - for (String s : variants) { - if (what.equals(s)) return true; - } - return false; - } - @Override public void visitPyFunction(final PyFunction node) { - PsiElement cap = PyUtil.getConcealingParent(node); - if (cap instanceof PyClass) { + PyUtil.MethodFlags flags = PyUtil.MethodFlags.of(node); + if (flags != null) { PyParameterList plist = node.getParameterList(); PyParameter[] params = plist.getParameters(); - Set flags = PyUtil.detectDecorationsAndWrappersOf(node); - boolean isMetaclassMethod = false; - PyClass type_cls = PyBuiltinCache.getInstance(node).getClass("type"); - for (PyClass ancestor_cls : ((PyClass)cap).iterateAncestors()) { - if (ancestor_cls == type_cls) { - isMetaclassMethod = true; - break; - } - } final String method_name = node.getName(); - boolean isSpecialMetaclassMethod = isMetaclassMethod && among(method_name, PyNames.INIT, "__call__"); - final boolean is_staticmethod = flags.contains(STATICMETHOD); if (params.length == 0) { // check for "staticmetod" - if (is_staticmethod) return; // no params may be fine + if (flags.isStaticMethod()) return; // no params may be fine // check actual param list ASTNode name_node = node.getNameNode(); if (name_node != null) { @@ -87,7 +63,7 @@ public class PyMethodParametersInspection extends PyInspection { open_paren != null && close_paren != null && "(".equals(open_paren.getText()) && ")".equals(close_paren.getText()) ) { - String paramName = flags.contains(CLASSMETHOD) || isMetaclassMethod ? "cls" : "self"; + String paramName = flags.isClassMethod() || flags.isMetaclassMethod() ? "cls" : "self"; registerProblem( plist, PyBundle.message("INSP.must.have.first.parameter", paramName), ProblemHighlightType.GENERIC_ERROR, null, new AddSelfQuickFix(paramName) @@ -101,20 +77,18 @@ public class PyMethodParametersInspection extends PyInspection { String pname = first_param.getText(); // every dup, swap, drop, or dup+drop of "self" @NonNls String[] mangled = {"eslf", "sself", "elf", "felf", "slef", "seelf", "slf", "sslf", "sefl", "sellf", "sef", "seef"}; - for (String typo : mangled) { - if (typo.equals(pname)) { - registerProblem( - PyUtil.sure(params[0].getNode()).getPsi(), - PyBundle.message("INSP.probably.mistyped.self"), - new RenameParameterQuickFix(PyNames.CANONICAL_SELF) - ); - return; - } + if (PyUtil.among(pname, mangled)) { + registerProblem( + PyUtil.sure(params[0].getNode()).getPsi(), + PyBundle.message("INSP.probably.mistyped.self"), + new RenameParameterQuickFix(PyNames.CANONICAL_SELF) + ); + return; } String CLS = "cls"; // TODO: move to style settings - if (isMetaclassMethod && PyNames.NEW.equals(method_name)) { + if (flags.isMetaclassMethod() && PyNames.NEW.equals(method_name)) { final String[] POSSIBLE_PARAM_NAMES = {"typ", "meta"}; // TODO: move to style settings - if (!among(pname, POSSIBLE_PARAM_NAMES)) { + if (!PyUtil.among(pname, POSSIBLE_PARAM_NAMES)) { registerProblem( PyUtil.sure(params[0].getNode()).getPsi(), PyBundle.message("INSP.usually.named.$0", POSSIBLE_PARAM_NAMES[0]), @@ -122,7 +96,7 @@ public class PyMethodParametersInspection extends PyInspection { ); } } - else if (flags.contains(CLASSMETHOD) || isSpecialMetaclassMethod || PyNames.NEW.equals(method_name)) { + else if (flags.isClassMethod() || flags.isSpecialMetaclassMethod() || PyNames.NEW.equals(method_name)) { if (!CLS.equals(pname)) { registerProblem( PyUtil.sure(params[0].getNode()).getPsi(), @@ -131,8 +105,8 @@ public class PyMethodParametersInspection extends PyInspection { ); } } - else if (!is_staticmethod && !first_param.isPositionalContainer() && !PyNames.CANONICAL_SELF.equals(pname)) { - if (isMetaclassMethod && CLS.equals(pname)) { + else if (!flags.isStaticMethod() && !first_param.isPositionalContainer() && !PyNames.CANONICAL_SELF.equals(pname)) { + if (flags.isMetaclassMethod() && CLS.equals(pname)) { return; // accept either 'self' or 'cls' for all methods in metaclass } registerProblem( @@ -143,7 +117,7 @@ public class PyMethodParametersInspection extends PyInspection { } } else { // the unusual case of a method with first tuple param - if (!is_staticmethod) { + if (!flags.isStaticMethod()) { registerProblem(plist, PyBundle.message("INSP.first.param.must.not.be.tuple")); } } diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index 9ea31462b213..e83d49997d9f 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -678,6 +678,18 @@ public class PyUtil { return null; } + /** + * @param what thing to search for + * @param variants things to search among + * @return true iff what.equals() one of the variants. + */ + public static boolean among(@NotNull T what, T... variants) { + for (T s : variants) { + if (what.equals(s)) return true; + } + return false; + } + public static class UnderscoreFilter implements Condition { private int myAllowed; // how many starting underscores is allowed: 0 is none, 1 is only one, 2 is two and more. @@ -716,5 +728,66 @@ public class PyUtil { } return false; } + + public static class MethodFlags { + + private boolean myIsStaticMethod; + private boolean myIsMetaclassMethod; + private boolean myIsSpecialMetaclassMethod; + private boolean myIsClassMethod; + + /** + * @return true iff the method belongs to a metaclass (an ancestor of 'type'). + */ + public boolean isMetaclassMethod() { + return myIsMetaclassMethod; + } + + /** + * @return iff isMetaclassMethod and the method is either __init__ or __call__. + */ + public boolean isSpecialMetaclassMethod() { + return myIsSpecialMetaclassMethod; + } + + public boolean isStaticMethod() { + return myIsStaticMethod; + } + + public boolean isClassMethod() { + return myIsClassMethod; + } + + private MethodFlags(boolean isClassMethod, boolean isStaticMethod, boolean isMetaclassMethod, boolean isSpecialMetaclassMethod) { + myIsClassMethod = isClassMethod; + myIsStaticMethod = isStaticMethod; + myIsMetaclassMethod = isMetaclassMethod; + myIsSpecialMetaclassMethod = isSpecialMetaclassMethod; + } + + /** + * @param node a function + * @return a new flags object, or null if the function is not a method + */ + @Nullable + public static MethodFlags of(@NotNull PyFunction node) { + PyClass cls = node.getContainingClass(); + if (cls != null) { + Set flags = detectDecorationsAndWrappersOf(node); + boolean isMetaclassMethod = false; + PyClass type_cls = PyBuiltinCache.getInstance(node).getClass("type"); + for (PyClass ancestor_cls : cls.iterateAncestors()) { + if (ancestor_cls == type_cls) { + isMetaclassMethod = true; + break; + } + } + final String method_name = node.getName(); + boolean isSpecialMetaclassMethod = isMetaclassMethod && method_name != null && among(method_name, PyNames.INIT, "__call__"); + return new MethodFlags(flags.contains(CLASSMETHOD), flags.contains(STATICMETHOD), isMetaclassMethod, isSpecialMetaclassMethod); + } + return null; + } + } } diff --git a/python/testData/editing/firstParamClassmethod.after.py b/python/testData/editing/firstParamClassmethod.after.py new file mode 100644 index 000000000000..d2f03b1fc3b9 --- /dev/null +++ b/python/testData/editing/firstParamClassmethod.after.py @@ -0,0 +1,3 @@ +class A(object): + @classmethod + def foo(cls): diff --git a/python/testData/editing/firstParamClassmethod.py b/python/testData/editing/firstParamClassmethod.py new file mode 100644 index 000000000000..a63856443848 --- /dev/null +++ b/python/testData/editing/firstParamClassmethod.py @@ -0,0 +1,3 @@ +class A(object): + @classmethod + def foo diff --git a/python/testData/editing/firstParamMetaClass.after.py b/python/testData/editing/firstParamMetaClass.after.py new file mode 100644 index 000000000000..6c702ea1d221 --- /dev/null +++ b/python/testData/editing/firstParamMetaClass.after.py @@ -0,0 +1,3 @@ +class A(type): + @classmethod + def f(cls): diff --git a/python/testData/editing/firstParamMetaClass.py b/python/testData/editing/firstParamMetaClass.py new file mode 100644 index 000000000000..e84217f2caf5 --- /dev/null +++ b/python/testData/editing/firstParamMetaClass.py @@ -0,0 +1,3 @@ +class A(type): + @classmethod + def f diff --git a/python/testData/editing/firstParamMetaNew.after.py b/python/testData/editing/firstParamMetaNew.after.py new file mode 100644 index 000000000000..e1b9d89fd3dc --- /dev/null +++ b/python/testData/editing/firstParamMetaNew.after.py @@ -0,0 +1,2 @@ +class A(type): + def __new__(typ): diff --git a/python/testData/editing/firstParamMetaNew.py b/python/testData/editing/firstParamMetaNew.py new file mode 100644 index 000000000000..e9af18205f9c --- /dev/null +++ b/python/testData/editing/firstParamMetaNew.py @@ -0,0 +1,2 @@ +class A(type): + def __new__ diff --git a/python/testData/editing/firstParamMetaSimple.after.py b/python/testData/editing/firstParamMetaSimple.after.py new file mode 100644 index 000000000000..0c4d3dde42f6 --- /dev/null +++ b/python/testData/editing/firstParamMetaSimple.after.py @@ -0,0 +1,2 @@ +class A(type): + def f(cls): diff --git a/python/testData/editing/firstParamMetaSimple.py b/python/testData/editing/firstParamMetaSimple.py new file mode 100644 index 000000000000..8a94f6ede2c5 --- /dev/null +++ b/python/testData/editing/firstParamMetaSimple.py @@ -0,0 +1,2 @@ +class A(type): + def f diff --git a/python/testData/editing/firstParamSimple.after.py b/python/testData/editing/firstParamSimple.after.py new file mode 100644 index 000000000000..cc336854a5d5 --- /dev/null +++ b/python/testData/editing/firstParamSimple.after.py @@ -0,0 +1,2 @@ +class A(object): + def foo(self): diff --git a/python/testData/editing/firstParamSimple.py b/python/testData/editing/firstParamSimple.py new file mode 100644 index 000000000000..46d5efacf32d --- /dev/null +++ b/python/testData/editing/firstParamSimple.py @@ -0,0 +1,2 @@ +class A(object): + def foo diff --git a/python/testData/editing/firstParamSimpleInit.after.py b/python/testData/editing/firstParamSimpleInit.after.py new file mode 100644 index 000000000000..f917824242d0 --- /dev/null +++ b/python/testData/editing/firstParamSimpleInit.after.py @@ -0,0 +1,2 @@ +class A(object): + def __init__(self): diff --git a/python/testData/editing/firstParamSimpleInit.py b/python/testData/editing/firstParamSimpleInit.py new file mode 100644 index 000000000000..e266c73c1708 --- /dev/null +++ b/python/testData/editing/firstParamSimpleInit.py @@ -0,0 +1,2 @@ +class A(object): + def __init__ diff --git a/python/testData/editing/firstParamSimpleNew.after.py b/python/testData/editing/firstParamSimpleNew.after.py new file mode 100644 index 000000000000..bd75378dd3cf --- /dev/null +++ b/python/testData/editing/firstParamSimpleNew.after.py @@ -0,0 +1,2 @@ +class A(object): + def __new__(cls): diff --git a/python/testData/editing/firstParamSimpleNew.py b/python/testData/editing/firstParamSimpleNew.py new file mode 100644 index 000000000000..fc2e9d2ac1bb --- /dev/null +++ b/python/testData/editing/firstParamSimpleNew.py @@ -0,0 +1,2 @@ +class A(object): + def __new__ diff --git a/python/testData/editing/firstParamStaticmethod.after.py b/python/testData/editing/firstParamStaticmethod.after.py new file mode 100644 index 000000000000..0d53971e8ce8 --- /dev/null +++ b/python/testData/editing/firstParamStaticmethod.after.py @@ -0,0 +1,3 @@ +class A(object): + @staticmethod + def foo(): diff --git a/python/testData/editing/firstParamStaticmethod.py b/python/testData/editing/firstParamStaticmethod.py new file mode 100644 index 000000000000..48bb875821d7 --- /dev/null +++ b/python/testData/editing/firstParamStaticmethod.py @@ -0,0 +1,3 @@ +class A(object): + @staticmethod + def foo diff --git a/python/testSrc/com/jetbrains/python/PyEditingTest.java b/python/testSrc/com/jetbrains/python/PyEditingTest.java index 04ebebe1d97f..8bef93514603 100644 --- a/python/testSrc/com/jetbrains/python/PyEditingTest.java +++ b/python/testSrc/com/jetbrains/python/PyEditingTest.java @@ -176,4 +176,55 @@ public class PyEditingTest extends PyLightFixtureTestCase { }); return myFixture.getDocument(file).getText(); } + + private void doTypingTest(final char character) { + final String testName = "editing/" + getTestName(true); + myFixture.configureByFile(testName + ".py"); + doTyping(character); + myFixture.checkResultByFile(testName + ".after.py"); + } + + private void doTyping(final char character) { + final int offset = myFixture.getEditor().getCaretModel().getOffset(); + final PsiFile file = ApplicationManager.getApplication().runWriteAction(new Computable() { + @Override + public PsiFile compute() { + myFixture.getEditor().getCaretModel().moveToOffset(offset); + myFixture.type(character); + return myFixture.getFile(); + } + }); + } + + public void testFirstParamClassmethod() { + doTypingTest('('); + } + + public void testFirstParamMetaClass() { + doTypingTest('('); + } + + public void testFirstParamMetaNew() { + doTypingTest('('); + } + + public void testFirstParamMetaSimple() { + doTypingTest('('); + } + + public void testFirstParamSimpleInit() { + doTypingTest('('); + } + + public void testFirstParamSimpleNew() { + doTypingTest('('); + } + + public void testFirstParamSimple() { + doTypingTest('('); + } + + public void testFirstParamStaticmethod() { + doTypingTest('('); + } }