diff --git a/python/resources/inspectionDescriptions/PyMissingTypeHintsInspection.html b/python/resources/inspectionDescriptions/PyMissingTypeHintsInspection.html new file mode 100644 index 000000000000..7d88ef248c2c --- /dev/null +++ b/python/resources/inspectionDescriptions/PyMissingTypeHintsInspection.html @@ -0,0 +1,7 @@ + + + +This inspection detects lack of type hints for function declaration in + one of the two formats: parameter annotations or a type comment + + \ No newline at end of file diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index 911f5041744a..c8a11791611d 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -380,6 +380,8 @@ + + diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/PyAnnotateTypesIntention.java b/python/src/com/jetbrains/python/codeInsight/intentions/PyAnnotateTypesIntention.java index dd2b90d0256d..72eadd1fad2f 100644 --- a/python/src/com/jetbrains/python/codeInsight/intentions/PyAnnotateTypesIntention.java +++ b/python/src/com/jetbrains/python/codeInsight/intentions/PyAnnotateTypesIntention.java @@ -82,17 +82,21 @@ public class PyAnnotateTypesIntention implements IntentionAction { final PsiElement elementAt = PyUtil.findNonWhitespaceAtOffset(file, editor.getCaretModel().getOffset()); final PyCallable callable = getCallable(elementAt); - if (isPy3k(file)) { - generatePy3kTypeAnnotations(project, editor, elementAt, callable); + annotateTypes(editor, callable); + } + + public static void annotateTypes(Editor editor, PyCallable callable) { + if (isPy3k(callable.getContainingFile())) { + generatePy3kTypeAnnotations(callable.getProject(), editor, callable); } else { if (callable instanceof PyFunction) { - generateTypeCommentAnnotations(project, editor, elementAt, (PyFunction)callable); + generateTypeCommentAnnotations(callable.getProject(), (PyFunction)callable); } } } - private static void generateTypeCommentAnnotations(Project project, Editor editor, PsiElement at, PyFunction function) { + private static void generateTypeCommentAnnotations(Project project, PyFunction function) { StringBuilder replacementTextBuilder = new StringBuilder("# type: ("); @@ -181,13 +185,15 @@ public class PyAnnotateTypesIntention implements IntentionAction { return LanguageLevel.forElement(file).isPy3K(); } - private static void generatePy3kTypeAnnotations(@NotNull Project project, Editor editor, PsiElement elementAt, PyCallable callable) { + private static void generatePy3kTypeAnnotations(@NotNull Project project, Editor editor, PyCallable callable) { final TemplateBuilder builder = TemplateBuilderFactory.getInstance().createTemplateBuilder(callable); - PyExpression returnType = annotateReturnType(project, editor.getDocument(), elementAt, false); + if (callable instanceof PyFunction) { + PyExpression returnType = annotateReturnType(project, (PyFunction) callable, false); - if (returnType != null) { - builder.replaceElement(returnType, returnType.getText()); + if (returnType != null) { + builder.replaceElement(returnType, returnType.getText()); + } } if (callable instanceof PyFunction) { diff --git a/python/src/com/jetbrains/python/codeInsight/intentions/SpecifyTypeInPy3AnnotationsIntention.java b/python/src/com/jetbrains/python/codeInsight/intentions/SpecifyTypeInPy3AnnotationsIntention.java index 180a2165d9c4..f9b41ab1c7b7 100644 --- a/python/src/com/jetbrains/python/codeInsight/intentions/SpecifyTypeInPy3AnnotationsIntention.java +++ b/python/src/com/jetbrains/python/codeInsight/intentions/SpecifyTypeInPy3AnnotationsIntention.java @@ -76,14 +76,17 @@ public class SpecifyTypeInPy3AnnotationsIntention extends TypeIntention { annotateParameter(project, editor, parameter, true); } else { - annotateReturnType(project, editor.getDocument(), elementAt, true); + PyCallable callable = getCallable(elementAt); + if (callable instanceof PyFunction) { + annotateReturnType(project, (PyFunction)callable, true); + } } } static PyNamedParameter annotateParameter(Project project, - Editor editor, - @NotNull PyNamedParameter parameter, - boolean createTemplate) { + Editor editor, + @NotNull PyNamedParameter parameter, + boolean createTemplate) { final PyExpression defaultParamValue = parameter.getDefaultValue(); final String paramName = StringUtil.notNullize(parameter.getName()); @@ -93,7 +96,6 @@ public class SpecifyTypeInPy3AnnotationsIntention extends TypeIntention { String paramType = parameterType(parameter); - final PyNamedParameter namedParameter = elementGenerator.createParameter(paramName, defaultParamText, paramType, LanguageLevel.forElement(parameter)); @@ -119,7 +121,7 @@ public class SpecifyTypeInPy3AnnotationsIntention extends TypeIntention { static String parameterType(PyParameter parameter) { String paramType = PyNames.OBJECT; - + PyFunction function = PsiTreeUtil.getParentOfType(parameter, PyFunction.class); if (function != null) { final PySignature signature = PySignatureCacheManager.getInstance(parameter.getProject()).findSignature( @@ -143,65 +145,57 @@ public class SpecifyTypeInPy3AnnotationsIntention extends TypeIntention { return returnType; } - public static PyExpression annotateReturnType(Project project, Document document, PsiElement resolved, boolean createTemplate) { - PyCallable callable = getCallable(resolved); + public static PyExpression annotateReturnType(Project project, PyFunction function, boolean createTemplate) { + String returnType = returnType(function); - if (callable instanceof PyFunction) { - PyFunction function = (PyFunction)callable; + final String annotationText = " -> " + returnType; - String returnType = returnType(function); + final PsiElement prevElem = PyPsiUtils.getPrevNonCommentSibling(function.getStatementList(), true); + assert prevElem != null; - final String annotationText = " -> " + returnType; - - final PsiElement prevElem = PyPsiUtils.getPrevNonCommentSibling(((PyFunction)callable).getStatementList(), true); - assert prevElem != null; - - final PsiDocumentManager manager = PsiDocumentManager.getInstance(project); - Document documentWithCallable = manager.getDocument(callable.getContainingFile()); - if (documentWithCallable != null) { - try { - final TextRange range = prevElem.getTextRange(); - manager.doPostponedOperationsAndUnblockDocument(documentWithCallable); - if (prevElem.getNode().getElementType() == PyTokenTypes.COLON) { - documentWithCallable.insertString(range.getStartOffset(), annotationText); - } - else { - documentWithCallable.insertString(range.getEndOffset(), annotationText + ":"); - } + final PsiDocumentManager manager = PsiDocumentManager.getInstance(project); + Document documentWithCallable = manager.getDocument(function.getContainingFile()); + if (documentWithCallable != null) { + try { + final TextRange range = prevElem.getTextRange(); + manager.doPostponedOperationsAndUnblockDocument(documentWithCallable); + if (prevElem.getNode().getElementType() == PyTokenTypes.COLON) { + documentWithCallable.insertString(range.getStartOffset(), annotationText); } - finally { - manager.commitDocument(documentWithCallable); + else { + documentWithCallable.insertString(range.getEndOffset(), annotationText + ":"); } } - - - callable = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(callable); - final PyAnnotation annotation = ((PyFunction)callable).getAnnotation(); - assert annotation != null; - final PyExpression annotationValue = annotation.getValue(); - assert annotationValue != null : "Generated function must have annotation"; - - if (createTemplate) { - final int offset = annotationValue.getTextOffset(); - - final TemplateBuilder builder = TemplateBuilderFactory.getInstance().createTemplateBuilder(annotationValue); - builder.replaceRange(TextRange.create(0, returnType.length()), returnType); - final Template template = ((TemplateBuilderImpl)builder).buildInlineTemplate(); - final OpenFileDescriptor descriptor = new OpenFileDescriptor( - project, - callable.getContainingFile().getVirtualFile(), - offset - ); - final Editor targetEditor = FileEditorManager.getInstance(project).openTextEditor(descriptor, true); - if (targetEditor != null) { - targetEditor.getCaretModel().moveToOffset(offset); - TemplateManager.getInstance(project).startTemplate(targetEditor, template); - } + finally { + manager.commitDocument(documentWithCallable); } - return annotationValue; } - return null; + + function = CodeInsightUtilCore.forcePsiPostprocessAndRestoreElement(function); + final PyAnnotation annotation = function.getAnnotation(); + assert annotation != null; + final PyExpression annotationValue = annotation.getValue(); + assert annotationValue != null : "Generated function must have annotation"; + + if (createTemplate) { + final int offset = annotationValue.getTextOffset(); + + final TemplateBuilder builder = TemplateBuilderFactory.getInstance().createTemplateBuilder(annotationValue); + builder.replaceRange(TextRange.create(0, returnType.length()), returnType); + final Template template = ((TemplateBuilderImpl)builder).buildInlineTemplate(); + final OpenFileDescriptor descriptor = new OpenFileDescriptor( + project, + function.getContainingFile().getVirtualFile(), + offset + ); + final Editor targetEditor = FileEditorManager.getInstance(project).openTextEditor(descriptor, true); + if (targetEditor != null) { + targetEditor.getCaretModel().moveToOffset(offset); + TemplateManager.getInstance(project).startTemplate(targetEditor, template); + } + } + return annotationValue; } @Override diff --git a/python/src/com/jetbrains/python/inspections/PyMissingTypeHintsInspection.java b/python/src/com/jetbrains/python/inspections/PyMissingTypeHintsInspection.java new file mode 100644 index 000000000000..6a3ce7e60eaa --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/PyMissingTypeHintsInspection.java @@ -0,0 +1,132 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.inspections; + +import com.intellij.codeInspection.LocalInspectionToolSession; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElementVisitor; +import com.jetbrains.python.PyTokenTypes; +import com.jetbrains.python.codeInsight.intentions.PyAnnotateTypesIntention; +import com.jetbrains.python.inspections.quickfix.PyQuickFixUtil; +import com.jetbrains.python.psi.PyElementVisitor; +import com.jetbrains.python.psi.PyFunction; +import com.jetbrains.python.psi.PyNamedParameter; +import com.jetbrains.python.psi.PyParameter; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +/** + * @author traff + */ +public class PyMissingTypeHintsInspection extends PyInspection{ + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) { + return new PyElementVisitor() { + @Override + public void visitPyFunction(PyFunction function) { + if (!(typeCommentExists(function) || typeAnnotationsExist(function))) { + ASTNode nameNode = function.getNameNode(); + if (nameNode != null) { + holder.registerProblem(nameNode.getPsi(), "Type hinting is missing for function definition", new AddTypeHintsQuickFix(function)); + } + } + } + }; + } + + private static boolean typeCommentExists(PyFunction function) { + ASTNode node = function.getStatementList().getNode().getFirstChildNode(); + + while (node != null && node.getElementType() != PyTokenTypes.END_OF_LINE_COMMENT) { + node = node.getTreeNext(); + } + if (node != null) { + return isTypeComment(node); + } + + node = function.getStatementList().getPrevSibling().getNode(); + + while (node != null && node.getElementType() != PyTokenTypes.COLON && node.getElementType() != PyTokenTypes.END_OF_LINE_COMMENT) { + node = node.getTreePrev(); + } + + if (node != null && node.getElementType() == PyTokenTypes.END_OF_LINE_COMMENT) { + return isTypeComment(node); + } + + return false; + } + + private static boolean isTypeComment(ASTNode node) { + String commentText = node.getText(); + int startInd = commentText.indexOf('#'); + if (startInd != -1) { + commentText = commentText.substring(startInd+1).trim(); + if (commentText.startsWith("type:")) { + return true; + } + } + return false; + } + + private static boolean typeAnnotationsExist(PyFunction function) { + for (PyParameter param: function.getParameterList().getParameters()) { + PyNamedParameter namedParameter = param.getAsNamed(); + if (namedParameter != null) { + if (namedParameter.getAnnotation() != null) { + return true; + } + } + } + if (function.getAnnotation() != null) { + return true; + } + + return false; + } + + private static class AddTypeHintsQuickFix implements LocalQuickFix { + private PyFunction myFunction; + + public AddTypeHintsQuickFix(@NotNull PyFunction function) { + myFunction = function; + } + + @Nls + @NotNull + @Override + public String getName() { + return "Add type hinting for '" + myFunction.getName() + "'"; + } + + @Nls + @NotNull + @Override + public String getFamilyName() { + return "Add type hinting"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PyAnnotateTypesIntention.annotateTypes(PyQuickFixUtil.getEditor(myFunction), myFunction); + } + } +} diff --git a/python/src/com/jetbrains/python/inspections/quickfix/DocstringQuickFix.java b/python/src/com/jetbrains/python/inspections/quickfix/DocstringQuickFix.java index 0622727ebc1b..a42ddfdc7cca 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/DocstringQuickFix.java +++ b/python/src/com/jetbrains/python/inspections/quickfix/DocstringQuickFix.java @@ -17,12 +17,7 @@ package com.jetbrains.python.inspections.quickfix; import com.intellij.codeInspection.LocalQuickFix; import com.intellij.codeInspection.ProblemDescriptor; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiElement; import com.intellij.psi.SmartPointerManager; import com.intellij.psi.SmartPsiElementPointer; import com.intellij.psi.util.PsiTreeUtil; @@ -73,20 +68,6 @@ public class DocstringQuickFix implements LocalQuickFix { return "Fix docstring"; } - @Nullable - private static Editor getEditor(@NotNull PsiElement element) { - Document document = PsiDocumentManager.getInstance(element.getProject()).getDocument(element.getContainingFile()); - if (document != null) { - final EditorFactory instance = EditorFactory.getInstance(); - if (instance == null) return null; - Editor[] editors = instance.getEditors(document); - if (editors.length > 0) { - return editors[0]; - } - } - return null; - } - public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(descriptor.getPsiElement(), PyDocStringOwner.class); if (docStringOwner == null) return; @@ -113,7 +94,7 @@ public class DocstringQuickFix implements LocalQuickFix { private static void addEmptyDocstring(@NotNull PyDocStringOwner docStringOwner) { if (docStringOwner instanceof PyFunction || docStringOwner instanceof PyClass && ((PyClass)docStringOwner).findInitOrNew(false, null) != null) { - PyGenerateDocstringIntention.generateDocstring(docStringOwner, getEditor(docStringOwner)); + PyGenerateDocstringIntention.generateDocstring(docStringOwner, PyQuickFixUtil.getEditor(docStringOwner)); } } } diff --git a/python/src/com/jetbrains/python/inspections/quickfix/PyImplementMethodsQuickFix.java b/python/src/com/jetbrains/python/inspections/quickfix/PyImplementMethodsQuickFix.java index 282bca97dbb5..d480f4e203e3 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/PyImplementMethodsQuickFix.java +++ b/python/src/com/jetbrains/python/inspections/quickfix/PyImplementMethodsQuickFix.java @@ -17,11 +17,8 @@ package com.jetbrains.python.inspections.quickfix; import com.intellij.codeInspection.LocalQuickFixOnPsiElement; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.EditorFactory; 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.PyBundle; @@ -31,7 +28,6 @@ import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyFunction; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Set; @@ -62,7 +58,7 @@ public class PyImplementMethodsQuickFix extends LocalQuickFixOnPsiElement { @Override public void invoke(@NotNull Project project, @NotNull PsiFile file, @NotNull PsiElement startElement, @NotNull PsiElement endElement) { - final Editor editor = getEditor(project, file); + final Editor editor = PyQuickFixUtil.getEditor(file); if (editor != null && startElement instanceof PyClass) { if (ApplicationManager.getApplication().isUnitTestMode()) { @@ -81,18 +77,4 @@ public class PyImplementMethodsQuickFix extends LocalQuickFixOnPsiElement { } } } - - @Nullable - private static Editor getEditor(Project project, PsiFile file) { - Document document = PsiDocumentManager.getInstance(project).getDocument(file); - if (document != null) { - final EditorFactory instance = EditorFactory.getInstance(); - if (instance == null) return null; - Editor[] editors = instance.getEditors(document); - if (editors.length > 0) { - return editors[0]; - } - } - return null; - } } diff --git a/python/src/com/jetbrains/python/inspections/quickfix/PyQuickFixUtil.java b/python/src/com/jetbrains/python/inspections/quickfix/PyQuickFixUtil.java new file mode 100644 index 000000000000..3ad435445642 --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/quickfix/PyQuickFixUtil.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.inspections.quickfix; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author traff + */ +public class PyQuickFixUtil { + @Nullable + public static Editor getEditor(@NotNull PsiElement element) { + Document document = PsiDocumentManager.getInstance(element.getProject()).getDocument(element.getContainingFile()); + if (document != null) { + final EditorFactory instance = EditorFactory.getInstance(); + if (instance == null) return null; + Editor[] editors = instance.getEditors(document); + if (editors.length > 0) { + return editors[0]; + } + } + return null; + } +} diff --git a/python/testData/inspections/PyMissingTypeHintsInspection/noAnnotations.py b/python/testData/inspections/PyMissingTypeHintsInspection/noAnnotations.py new file mode 100644 index 000000000000..73bdd73dbc82 --- /dev/null +++ b/python/testData/inspections/PyMissingTypeHintsInspection/noAnnotations.py @@ -0,0 +1,2 @@ +def foo(x, y): + pass \ No newline at end of file diff --git a/python/testData/inspections/PyMissingTypeHintsInspection/py3kAnnotations.py b/python/testData/inspections/PyMissingTypeHintsInspection/py3kAnnotations.py new file mode 100644 index 000000000000..caefec0ac1f7 --- /dev/null +++ b/python/testData/inspections/PyMissingTypeHintsInspection/py3kAnnotations.py @@ -0,0 +1,2 @@ +def foo(x: int, y: int) -> str: + pass \ No newline at end of file diff --git a/python/testData/inspections/PyMissingTypeHintsInspection/typeComment.py b/python/testData/inspections/PyMissingTypeHintsInspection/typeComment.py new file mode 100644 index 000000000000..38efd7e111c2 --- /dev/null +++ b/python/testData/inspections/PyMissingTypeHintsInspection/typeComment.py @@ -0,0 +1,3 @@ +def foo(x): + # type: (int) -> int + pass \ No newline at end of file diff --git a/python/testData/inspections/PyMissingTypeHintsInspection/typeCommentOnTheSameLine.py b/python/testData/inspections/PyMissingTypeHintsInspection/typeCommentOnTheSameLine.py new file mode 100644 index 000000000000..2b42a1884977 --- /dev/null +++ b/python/testData/inspections/PyMissingTypeHintsInspection/typeCommentOnTheSameLine.py @@ -0,0 +1,2 @@ +def foo(x): # type: (int) -> int + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyMissingTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyMissingTypeHintsInspectionTest.java new file mode 100644 index 000000000000..fd6ca83100bc --- /dev/null +++ b/python/testSrc/com/jetbrains/python/inspections/PyMissingTypeHintsInspectionTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.inspections; + +import com.jetbrains.python.fixtures.PyTestCase; +import com.jetbrains.python.psi.LanguageLevel; +import com.jetbrains.python.psi.impl.PythonLanguageLevelPusher; + +/** + * @author traff + */ +public class PyMissingTypeHintsInspectionTest extends PyTestCase { + public void testPy3kAnnotations() { + doTest(LanguageLevel.PYTHON35); + } + + public void testNoAnnotations() { + doTest(LanguageLevel.PYTHON35); + } + + public void testTypeComment() { + doTest(LanguageLevel.PYTHON27); + } + + public void testTypeCommentOnTheSameLine() { + doTest(LanguageLevel.PYTHON27); + } + + private void doTest(LanguageLevel languageLevel) { + PythonLanguageLevelPusher.setForcedLanguageLevel(myFixture.getProject(), languageLevel); + try { + myFixture.configureByFile("inspections/PyMissingTypeHintsInspection/" + getTestName(true) + ".py"); + myFixture.enableInspections(PyMissingTypeHintsInspection.class); + myFixture.checkHighlighting(false, false, true); + } + finally { + PythonLanguageLevelPusher.setForcedLanguageLevel(myFixture.getProject(), null); + } + } +}