diff --git a/python/resources/postfixTemplates/PyLenPostfixTemplate/after.py.template b/python/resources/postfixTemplates/PyLenPostfixTemplate/after.py.template
new file mode 100644
index 000000000000..6ad7f4e5b8d6
--- /dev/null
+++ b/python/resources/postfixTemplates/PyLenPostfixTemplate/after.py.template
@@ -0,0 +1,2 @@
+def f(a):
+ len(a)
\ No newline at end of file
diff --git a/python/resources/postfixTemplates/PyLenPostfixTemplate/before.py.template b/python/resources/postfixTemplates/PyLenPostfixTemplate/before.py.template
new file mode 100644
index 000000000000..7ad4281c8206
--- /dev/null
+++ b/python/resources/postfixTemplates/PyLenPostfixTemplate/before.py.template
@@ -0,0 +1,2 @@
+def f(a):
+ a.len
\ No newline at end of file
diff --git a/python/resources/postfixTemplates/PyLenPostfixTemplate/description.html b/python/resources/postfixTemplates/PyLenPostfixTemplate/description.html
new file mode 100644
index 000000000000..6565cb0c6be2
--- /dev/null
+++ b/python/resources/postfixTemplates/PyLenPostfixTemplate/description.html
@@ -0,0 +1,5 @@
+
+
+Converts postfix call to '.len' to 'len' function invocation.
+
+
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/codeInsight/postfix/PyLenPostfixTemplate.kt b/python/src/com/jetbrains/python/codeInsight/postfix/PyLenPostfixTemplate.kt
new file mode 100644
index 000000000000..9efca64daec5
--- /dev/null
+++ b/python/src/com/jetbrains/python/codeInsight/postfix/PyLenPostfixTemplate.kt
@@ -0,0 +1,38 @@
+// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.python.codeInsight.postfix
+
+import com.intellij.codeInsight.template.postfix.templates.SurroundPostfixTemplateBase
+import com.intellij.lang.surroundWith.Surrounder
+import com.intellij.openapi.util.Condition
+import com.intellij.psi.PsiElement
+import com.jetbrains.python.PyNames
+import com.jetbrains.python.psi.AccessDirection
+import com.jetbrains.python.psi.PyClass
+import com.jetbrains.python.psi.PyExpression
+import com.jetbrains.python.psi.resolve.PyResolveContext
+import com.jetbrains.python.psi.types.TypeEvalContext
+import com.jetbrains.python.refactoring.surround.surrounders.expressions.PyLenExpressionStatementSurrounder
+
+class PyLenPostfixTemplate : SurroundPostfixTemplateBase("len", DESCR, PyPostfixUtils.PY_PSI_INFO,
+ PyPostfixUtils.selectorAllExpressionsWithCurrentOffset(sizedFilter)) {
+
+ override fun getSurrounder(): Surrounder = PyLenExpressionStatementSurrounder()
+
+ companion object {
+ const val DESCR = "len(expr)"
+
+ val sizedFilter: Condition = Condition { element ->
+ val ref = element.reference
+ if (ref?.resolve() is PyClass)
+ return@Condition false
+
+ val expression = element as PyExpression
+ val context = TypeEvalContext.codeCompletion(expression.project, expression.containingFile)
+ val type = context.getType(expression) ?: return@Condition false
+
+ val resolveContext = PyResolveContext.defaultContext().withTypeEvalContext(context)
+ val results = type.resolveMember(PyNames.LEN, null, AccessDirection.READ, resolveContext)
+ return@Condition results?.isNotEmpty() ?: false
+ }
+ }
+}
diff --git a/python/src/com/jetbrains/python/codeInsight/postfix/PyPostfixTemplateProvider.java b/python/src/com/jetbrains/python/codeInsight/postfix/PyPostfixTemplateProvider.java
index f76088afcbb7..6a53a100c86e 100644
--- a/python/src/com/jetbrains/python/codeInsight/postfix/PyPostfixTemplateProvider.java
+++ b/python/src/com/jetbrains/python/codeInsight/postfix/PyPostfixTemplateProvider.java
@@ -28,7 +28,8 @@ public class PyPostfixTemplateProvider implements PostfixTemplateProvider {
new PyIsNonePostfixTemplate(),
new PyIsNotNonePostfixTemplate(),
new PyPrintPostfixTemplate(),
- new PyMainPostfixTemplate());
+ new PyMainPostfixTemplate(),
+ new PyLenPostfixTemplate());
}
@Override
diff --git a/python/src/com/jetbrains/python/refactoring/surround/PyExpressionSurroundDescriptor.java b/python/src/com/jetbrains/python/refactoring/surround/PyExpressionSurroundDescriptor.java
index 00632f38ba21..4cf3e2984702 100644
--- a/python/src/com/jetbrains/python/refactoring/surround/PyExpressionSurroundDescriptor.java
+++ b/python/src/com/jetbrains/python/refactoring/surround/PyExpressionSurroundDescriptor.java
@@ -8,11 +8,12 @@ import com.intellij.psi.PsiFile;
import com.jetbrains.python.psi.PyExpression;
import com.jetbrains.python.refactoring.PyRefactoringUtil;
import com.jetbrains.python.refactoring.surround.surrounders.expressions.*;
+import com.jetbrains.python.refactoring.surround.surrounders.expressions.PyLenExpressionStatementSurrounder;
import org.jetbrains.annotations.NotNull;
public class PyExpressionSurroundDescriptor implements SurroundDescriptor {
private static final Surrounder[] SURROUNDERS = {new PyWithParenthesesSurrounder(), new PyIfExpressionSurrounder(),
- new PyWhileExpressionSurrounder(), new PyIsNoneSurrounder(), new PyIsNotNoneSurrounder()};
+ new PyWhileExpressionSurrounder(), new PyIsNoneSurrounder(), new PyIsNotNoneSurrounder(), new PyLenExpressionStatementSurrounder()};
@Override
@NotNull
diff --git a/python/src/com/jetbrains/python/refactoring/surround/surrounders/expressions/PyLenExpressionStatementSurrounder.kt b/python/src/com/jetbrains/python/refactoring/surround/surrounders/expressions/PyLenExpressionStatementSurrounder.kt
new file mode 100644
index 000000000000..13c16a691c23
--- /dev/null
+++ b/python/src/com/jetbrains/python/refactoring/surround/surrounders/expressions/PyLenExpressionStatementSurrounder.kt
@@ -0,0 +1,31 @@
+// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.python.refactoring.surround.surrounders.expressions
+
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.TextRange
+import com.intellij.util.IncorrectOperationException
+import com.jetbrains.python.psi.*
+
+
+class PyLenExpressionStatementSurrounder : PyExpressionSurrounder() {
+
+ @Throws(IncorrectOperationException::class)
+ override fun surroundExpression(project: Project, editor: Editor, element: PyExpression): TextRange {
+ val range = element.textRange
+ val statement = PyElementGenerator.getInstance(project).createFromText(LanguageLevel.getDefault(), PyExpressionStatement::class.java, "len(a)")
+ val callExpression = statement.expression as PyCallExpression
+ val arg = callExpression.arguments[0]
+ arg.replace(element)
+ element.replace(callExpression)
+ return TextRange.from(range.endOffset + FUNCTION_LENGTH, 0)
+ }
+
+ override fun isApplicable(expr: PyExpression) = true
+
+ override fun getTemplateDescription()= "len(expr)"
+
+ companion object {
+ const val FUNCTION_LENGTH = "len()".length
+ }
+}
diff --git a/python/testData/postfix/len/asExpr.py b/python/testData/postfix/len/asExpr.py
new file mode 100644
index 000000000000..af8589306742
--- /dev/null
+++ b/python/testData/postfix/len/asExpr.py
@@ -0,0 +1 @@
+foo = "something".len
\ No newline at end of file
diff --git a/python/testData/postfix/len/asExpr_after.py b/python/testData/postfix/len/asExpr_after.py
new file mode 100644
index 000000000000..06be9a9af121
--- /dev/null
+++ b/python/testData/postfix/len/asExpr_after.py
@@ -0,0 +1 @@
+foo = len("something")
\ No newline at end of file
diff --git a/python/testData/postfix/len/dict.py b/python/testData/postfix/len/dict.py
new file mode 100644
index 000000000000..1b29cfbeea73
--- /dev/null
+++ b/python/testData/postfix/len/dict.py
@@ -0,0 +1 @@
+{"first": 1, "second": 2}.len
\ No newline at end of file
diff --git a/python/testData/postfix/len/dict_after.py b/python/testData/postfix/len/dict_after.py
new file mode 100644
index 000000000000..9edff2023988
--- /dev/null
+++ b/python/testData/postfix/len/dict_after.py
@@ -0,0 +1 @@
+len({"first": 1, "second": 2})
\ No newline at end of file
diff --git a/python/testData/postfix/len/list.py b/python/testData/postfix/len/list.py
new file mode 100644
index 000000000000..296d8fd7944a
--- /dev/null
+++ b/python/testData/postfix/len/list.py
@@ -0,0 +1 @@
+[1, 2, 3, 4].len
\ No newline at end of file
diff --git a/python/testData/postfix/len/list_after.py b/python/testData/postfix/len/list_after.py
new file mode 100644
index 000000000000..fbdb4f03d5b8
--- /dev/null
+++ b/python/testData/postfix/len/list_after.py
@@ -0,0 +1 @@
+len([1, 2, 3, 4])
\ No newline at end of file
diff --git a/python/testData/postfix/len/notSized.py b/python/testData/postfix/len/notSized.py
new file mode 100644
index 000000000000..252033ffa0e6
--- /dev/null
+++ b/python/testData/postfix/len/notSized.py
@@ -0,0 +1,5 @@
+class MyNotSizedClass:
+ pass
+
+
+MyNotSizedClass().len
\ No newline at end of file
diff --git a/python/testData/postfix/len/notSized_after.py b/python/testData/postfix/len/notSized_after.py
new file mode 100644
index 000000000000..d24b65982292
--- /dev/null
+++ b/python/testData/postfix/len/notSized_after.py
@@ -0,0 +1,5 @@
+class MyNotSizedClass:
+ pass
+
+
+MyNotSizedClass().len
\ No newline at end of file
diff --git a/python/testData/postfix/len/sized.py b/python/testData/postfix/len/sized.py
new file mode 100644
index 000000000000..4cdbd7d0227c
--- /dev/null
+++ b/python/testData/postfix/len/sized.py
@@ -0,0 +1,6 @@
+class MySizedClass:
+ def __len__(self):
+ pass
+
+
+MySizedClass().len
\ No newline at end of file
diff --git a/python/testData/postfix/len/sizedClassObj.py b/python/testData/postfix/len/sizedClassObj.py
new file mode 100644
index 000000000000..cd06e9509d10
--- /dev/null
+++ b/python/testData/postfix/len/sizedClassObj.py
@@ -0,0 +1,6 @@
+class MySizedClass:
+ def __len__(self):
+ pass
+
+
+MySizedClass.len
\ No newline at end of file
diff --git a/python/testData/postfix/len/sizedClassObj_after.py b/python/testData/postfix/len/sizedClassObj_after.py
new file mode 100644
index 000000000000..6f4fb66b3656
--- /dev/null
+++ b/python/testData/postfix/len/sizedClassObj_after.py
@@ -0,0 +1,6 @@
+class MySizedClass:
+ def __len__(self):
+ pass
+
+
+MySizedClass.len
\ No newline at end of file
diff --git a/python/testData/postfix/len/sized_after.py b/python/testData/postfix/len/sized_after.py
new file mode 100644
index 000000000000..5442242ca7f4
--- /dev/null
+++ b/python/testData/postfix/len/sized_after.py
@@ -0,0 +1,6 @@
+class MySizedClass:
+ def __len__(self):
+ pass
+
+
+len(MySizedClass())
\ No newline at end of file
diff --git a/python/testData/postfix/len/string.py b/python/testData/postfix/len/string.py
new file mode 100644
index 000000000000..f2c3295699a7
--- /dev/null
+++ b/python/testData/postfix/len/string.py
@@ -0,0 +1 @@
+"something".len
\ No newline at end of file
diff --git a/python/testData/postfix/len/string_after.py b/python/testData/postfix/len/string_after.py
new file mode 100644
index 000000000000..91e17003228f
--- /dev/null
+++ b/python/testData/postfix/len/string_after.py
@@ -0,0 +1 @@
+len("something")
\ No newline at end of file
diff --git a/python/testData/postfix/len/tuple.py b/python/testData/postfix/len/tuple.py
new file mode 100644
index 000000000000..9d244024fa0e
--- /dev/null
+++ b/python/testData/postfix/len/tuple.py
@@ -0,0 +1 @@
+(1, 2, 3, 4).len
\ No newline at end of file
diff --git a/python/testData/postfix/len/tuple_after.py b/python/testData/postfix/len/tuple_after.py
new file mode 100644
index 000000000000..69340e93c409
--- /dev/null
+++ b/python/testData/postfix/len/tuple_after.py
@@ -0,0 +1 @@
+len((1, 2, 3, 4))
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/postfix/PyLenPostfixTemplateTest.kt b/python/testSrc/com/jetbrains/python/postfix/PyLenPostfixTemplateTest.kt
new file mode 100644
index 000000000000..25d81893e458
--- /dev/null
+++ b/python/testSrc/com/jetbrains/python/postfix/PyLenPostfixTemplateTest.kt
@@ -0,0 +1,23 @@
+// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.jetbrains.python.postfix
+
+class PyLenPostfixTemplateTest : PyPostfixTemplateTestCase() {
+
+ fun testString() = doTest()
+
+ fun testList() = doTest()
+
+ fun testDict() = doTest()
+
+ fun testTuple() = doTest()
+
+ fun testSized() = doTest()
+
+ fun testSizedClassObj() = doTest()
+
+ fun testAsExpr() = doTest()
+
+ fun testNotSized() = doTest()
+
+ override fun getTestDataDir() = "len/"
+}