Missing type hints inspection

This commit is contained in:
Dmitry Trofimov
2016-02-24 13:11:44 +01:00
parent 90b71d797c
commit 681fb5d778
13 changed files with 312 additions and 103 deletions
@@ -0,0 +1,7 @@
<html>
<body>
<span style="font-family: verdana,serif;">
This inspection detects lack of type hints for function declaration in
one of the two formats: parameter annotations or a type comment
</body>
</html>
@@ -380,6 +380,8 @@
<localInspection language="Python" shortName="PyPep8NamingInspection" suppressId="PyPep8Naming" displayName="PEP 8 naming convention violation" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyPep8NamingInspection"/>
<localInspection language="Python" shortName="PyAssignmentToLoopOrWithParameterInspection" suppressId="PyAssignmentToLoopOrWithParameter" displayName="Assignment to 'for' loop or 'with' statement parameter" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyAssignmentToLoopOrWithParameterInspection"/>
<localInspection language="Python" shortName="PyMissingTypeHintsInspection" suppressId="PyMissingTypeHints" displayName="Type hinting is missing for function definition" groupKey="INSP.GROUP.python" enabledByDefault="false" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyMissingTypeHintsInspection"/>
<defaultLiveTemplatesProvider implementation="com.jetbrains.python.codeInsight.liveTemplates.PyDefaultLiveTemplatesProvider"/>
<liveTemplateContext implementation="com.jetbrains.python.codeInsight.liveTemplates.PythonTemplateContextType$General"/>
<liveTemplateContext implementation="com.jetbrains.python.codeInsight.liveTemplates.PythonTemplateContextType$Class"/>
@@ -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) {
@@ -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
@@ -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);
}
}
}
@@ -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));
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,2 @@
def <weak_warning descr="Type hinting is missing for function definition">foo</weak_warning>(x, y):
pass
@@ -0,0 +1,2 @@
def foo(x: int, y: int) -> str:
pass
@@ -0,0 +1,3 @@
def foo(x):
# type: (int) -> int
pass
@@ -0,0 +1,2 @@
def foo(x): # type: (int) -> int
pass
@@ -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);
}
}
}