PY-1767: auto-add a correct first parameter to a method + tests.

This commit is contained in:
Dmitry Cheryasov
2010-11-30 18:37:10 +02:00
parent ef6026f185
commit b04729ef36
21 changed files with 249 additions and 43 deletions
@@ -43,6 +43,7 @@
<typedHandler implementation="com.jetbrains.python.codeInsight.KeywordTypedHandler" id="pyCommaAfterKwd"/>
<typedHandler implementation="com.jetbrains.django.lang.template.editor.DjangoBracesInterpolationTypedHandler" id="pyDjangoBraceInterpolation"/>
<typedHandler implementation="com.jetbrains.python.editor.ColonTypedHandler" id="pyColonUnindent"/>
<typedHandler implementation="com.jetbrains.python.codeInsight.PyMethodNameTypedHandler" id="pyMethodNameTypedHandler"/>
<stubIndex implementation="com.jetbrains.python.psi.stubs.PyClassNameIndex"/>
<stubIndex implementation="com.jetbrains.python.psi.stubs.PyClassNameIndexInsensitive"/>
@@ -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.
* <br/>
* 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
}
}
@@ -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<PyFunction.Flag> 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"));
}
}
@@ -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 <T> 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<String> {
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<PyFunction.Flag> 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;
}
}
}
@@ -0,0 +1,3 @@
class A(object):
@classmethod
def foo(cls):
@@ -0,0 +1,3 @@
class A(object):
@classmethod
def foo<caret>
@@ -0,0 +1,3 @@
class A(type):
@classmethod
def f(cls):
@@ -0,0 +1,3 @@
class A(type):
@classmethod
def f<caret>
@@ -0,0 +1,2 @@
class A(type):
def __new__(typ):
@@ -0,0 +1,2 @@
class A(type):
def __new__<caret>
@@ -0,0 +1,2 @@
class A(type):
def f(cls):
@@ -0,0 +1,2 @@
class A(type):
def f<caret>
@@ -0,0 +1,2 @@
class A(object):
def foo(self):
@@ -0,0 +1,2 @@
class A(object):
def foo<caret>
@@ -0,0 +1,2 @@
class A(object):
def __init__(self):
@@ -0,0 +1,2 @@
class A(object):
def __init__<caret>
@@ -0,0 +1,2 @@
class A(object):
def __new__(cls):
@@ -0,0 +1,2 @@
class A(object):
def __new__<caret>
@@ -0,0 +1,3 @@
class A(object):
@staticmethod
def foo():
@@ -0,0 +1,3 @@
class A(object):
@staticmethod
def foo<caret>
@@ -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<PsiFile>() {
@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('(');
}
}