mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Adds proper inspection of decorator argument lists. Changes parsing slightly, updates call analysis.
Fixes PY-97.
This commit is contained in:
@@ -28,6 +28,7 @@ INSP.duplicate.doublestar.arg=Duplicate **arg
|
||||
INSP.cannot.appear.past.keyword.arg=Cannot appear past keyword arguments
|
||||
INSP.unexpected.arg=Unexpected argument
|
||||
INSP.parameter.$0.unfilled=Parameter ''{0}'' unfilled
|
||||
INSP.func.$0.lacks.first.arg=Function ''{0}'' lacks a positional argument
|
||||
|
||||
# PyMethodParametersInspection
|
||||
INSP.NAME.problematic.first.parameter=Methods having troubles with first parameter
|
||||
@@ -46,6 +47,10 @@ INSP.module.$0.not.found=Module ''{0}'' not found
|
||||
INSP.unresolved.ref.$0=Unresolved reference ''{0}''
|
||||
INSP.unresolved.ref.$0.for.class.$1=Unresolved attribute reference ''{0}'' for class ''{1}''
|
||||
|
||||
# ReturnValueFromInitInspection
|
||||
INSP.NAME.init.return=__init__ method that returns a value
|
||||
INSP.cant.return.value.from.init=Cannot return a value from __init__
|
||||
|
||||
### Annotators ###
|
||||
ANN.deleting.none=deleting None
|
||||
ANN.assign.to.none=assignment to None
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.jetbrains.python;
|
||||
import com.intellij.codeInsight.lookup.LookupElement;
|
||||
import com.intellij.lang.parameterInfo.*;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import static com.jetbrains.python.psi.PyCallExpression.Flag;
|
||||
import static com.jetbrains.python.psi.PyCallExpression.PyMarkedFunction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -103,9 +102,9 @@ public class PyParameterInfoHandler implements ParameterInfoHandler<PyArgumentLi
|
||||
// ^^ gotta hate the covariance issues
|
||||
for (int i =0; i < flags.length; i += 1) flags[i] = EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class);
|
||||
|
||||
if (marked.getFlags().contains(Flag.IMPLICIT_FIRST_ARG)) {
|
||||
//arg_index -= 1; // argument 0 is parameter 1, thus kipping para,eter 0 which is 'self'
|
||||
flags[0].add(ParameterInfoUIContextEx.Flag.DISABLE); // show but mark as absent
|
||||
// disable implicit params
|
||||
for (int i=0; i < marked.getImplicitOffset(); i +=1) {
|
||||
flags[i].add(ParameterInfoUIContextEx.Flag.DISABLE); // show but mark as absent
|
||||
}
|
||||
int cur_arg_index = 0;
|
||||
for (PyExpression arg : arglist.getArguments()) {
|
||||
|
||||
@@ -5,11 +5,9 @@ import com.intellij.codeInspection.LocalInspectionTool;
|
||||
import com.intellij.codeInspection.ProblemsHolder;
|
||||
import com.intellij.lang.ASTNode;
|
||||
import com.intellij.psi.PsiElementVisitor;
|
||||
import com.jetbrains.python.PyTokenTypes;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.psi.PyArgumentList;
|
||||
import com.jetbrains.python.psi.PyExpression;
|
||||
import com.jetbrains.python.psi.PyParameter;
|
||||
import com.jetbrains.python.PyTokenTypes;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -64,8 +62,7 @@ public class PyArgumentListInspection extends LocalInspectionTool {
|
||||
@Override
|
||||
public void visitPyArgumentList(final PyArgumentList node) {
|
||||
// analyze
|
||||
/*ArgumentAnalysisResult result = analyzeArgumentList(node);*/
|
||||
PyArgumentList.AnalysisResult result = node.analyzeCall();
|
||||
inspectPyArgumentList(node, getHolder());
|
||||
/*
|
||||
// X XX debug
|
||||
PyCallExpression call_ex = node.getCallExpression();
|
||||
@@ -105,36 +102,70 @@ public class PyArgumentListInspection extends LocalInspectionTool {
|
||||
System.out.println();
|
||||
// \\
|
||||
*/
|
||||
// show argument problems
|
||||
for (Map.Entry<PyExpression, EnumSet<PyArgumentList.ArgFlag>> arg_entry : result.getArgumentFlags().entrySet()) {
|
||||
EnumSet<PyArgumentList.ArgFlag> flags = arg_entry.getValue();
|
||||
if (!flags.isEmpty()) { // something's wrong
|
||||
PyExpression arg = arg_entry.getKey();
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP)) {
|
||||
registerProblem(arg, PyBundle.message("INSP.duplicate.argument"));
|
||||
}
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP_KWD)) {
|
||||
registerProblem(arg, PyBundle.message("INSP.duplicate.doublestar.arg"));
|
||||
}
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP_TUPLE)) {
|
||||
registerProblem(arg, PyBundle.message("INSP.duplicate.star.arg"));
|
||||
}
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_POS_PAST_KWD)) {
|
||||
registerProblem(arg, PyBundle.message("INSP.cannot.appear.past.keyword.arg"));
|
||||
}
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_UNMAPPED)) {
|
||||
registerProblem(arg, PyBundle.message("INSP.unexpected.arg"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPyDecoratorList(final PyDecoratorList node) {
|
||||
PyDecorator[] decos = node.getDecorators();
|
||||
for (PyDecorator deco : decos) {
|
||||
if (! deco.hasArgumentList()) {
|
||||
// empty arglist; deco function must have a non-kwarg first arg
|
||||
PyCallExpression.PyMarkedFunction mkfunc = deco.resolveCallee();
|
||||
if (mkfunc != null) {
|
||||
PyFunction decofunc = mkfunc.getFunction();
|
||||
int first_param_offset = mkfunc.getImplicitOffset();
|
||||
PyParameter[] params = decofunc.getParameterList().getParameters();
|
||||
if (params.length < first_param_offset || params[first_param_offset-1].isKeywordContainer()) {
|
||||
// no paramaters left to pass function implicitly, or wrong param type
|
||||
registerProblem(deco, PyBundle.message("INSP.func.$0.lacks.first.arg", decofunc.getName()));
|
||||
}
|
||||
else {
|
||||
// possible unfilled params
|
||||
for (int i=first_param_offset; i < params.length; i += 1) {
|
||||
PyParameter par = params[i];
|
||||
if (! par.isKeywordContainer() && ! par.isPositionalContainer() && (par.getDefaultValue() == null)) {
|
||||
registerProblem(deco, PyBundle.message("INSP.parameter.$0.unfilled", par.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// else: this case is handled by arglist visitor
|
||||
}
|
||||
// show unfilled params
|
||||
ASTNode our_node = node.getNode();
|
||||
if (our_node != null) {
|
||||
ASTNode close_paren = our_node.findChildByType(PyTokenTypes.RPAR);
|
||||
if (close_paren != null) {
|
||||
for (PyParameter param : result.getUnmappedParams()) {
|
||||
registerProblem(close_paren.getPsi(), PyBundle.message("INSP.parameter.$0.unfilled", param.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void inspectPyArgumentList(PyArgumentList node, ProblemsHolder holder) {
|
||||
PyArgumentList.AnalysisResult result = node.analyzeCall();
|
||||
for (Map.Entry<PyExpression, EnumSet<PyArgumentList.ArgFlag>> arg_entry : result.getArgumentFlags().entrySet()) {
|
||||
EnumSet<PyArgumentList.ArgFlag> flags = arg_entry.getValue();
|
||||
if (!flags.isEmpty()) { // something's wrong
|
||||
PyExpression arg = arg_entry.getKey();
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP)) {
|
||||
holder.registerProblem(arg, PyBundle.message("INSP.duplicate.argument"));
|
||||
}
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP_KWD)) {
|
||||
holder.registerProblem(arg, PyBundle.message("INSP.duplicate.doublestar.arg"));
|
||||
}
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP_TUPLE)) {
|
||||
holder.registerProblem(arg, PyBundle.message("INSP.duplicate.star.arg"));
|
||||
}
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_POS_PAST_KWD)) {
|
||||
holder.registerProblem(arg, PyBundle.message("INSP.cannot.appear.past.keyword.arg"));
|
||||
}
|
||||
if (flags.contains(PyArgumentList.ArgFlag.IS_UNMAPPED)) {
|
||||
holder.registerProblem(arg, PyBundle.message("INSP.unexpected.arg"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// show unfilled params
|
||||
ASTNode our_node = node.getNode();
|
||||
if (our_node != null) {
|
||||
ASTNode close_paren = our_node.findChildByType(PyTokenTypes.RPAR);
|
||||
if (close_paren != null) {
|
||||
for (PyParameter param : result.getUnmappedParams()) {
|
||||
holder.registerProblem(close_paren.getPsi(), PyBundle.message("INSP.parameter.$0.unfilled", param.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@ public class PyInspectionVisitor extends PyElementVisitor {
|
||||
myHolder = holder;
|
||||
}
|
||||
|
||||
public ProblemsHolder getHolder() {
|
||||
return myHolder;
|
||||
}
|
||||
|
||||
protected final void registerProblem(final PsiElement element,
|
||||
final String message){
|
||||
if (element == null || element.getTextLength() == 0){
|
||||
|
||||
@@ -65,6 +65,9 @@ public class FunctionParsing extends Parsing {
|
||||
if (myBuilder.getTokenType() == PyTokenTypes.LPAR) {
|
||||
getExpressionParser().parseArgumentList(myBuilder);
|
||||
}
|
||||
else { // empty arglist node, so we always have it
|
||||
myBuilder.mark().done(PyElementTypes.ARGUMENT_LIST);
|
||||
}
|
||||
checkMatches(PyTokenTypes.STATEMENT_BREAK, message("PARSE.expected.statement.break"));
|
||||
decoratorMarker.done(PyElementTypes.DECORATOR_CALL);
|
||||
decorated = true;
|
||||
|
||||
@@ -50,10 +50,6 @@ public interface PyCallExpression extends PyExpression {
|
||||
|
||||
|
||||
enum Flag {
|
||||
/**
|
||||
* First arg of the call is implicit, drop first parameter.
|
||||
*/
|
||||
IMPLICIT_FIRST_ARG,
|
||||
/**
|
||||
* Called function is decorated with @classmethod, first param is the class.
|
||||
*/
|
||||
@@ -70,10 +66,12 @@ public interface PyCallExpression extends PyExpression {
|
||||
class PyMarkedFunction {
|
||||
PyFunction myFunction;
|
||||
EnumSet<Flag> myFlags;
|
||||
int myImplicitOffset;
|
||||
|
||||
public PyMarkedFunction(@NotNull PyFunction function, EnumSet<Flag> flags) {
|
||||
public PyMarkedFunction(@NotNull PyFunction function, EnumSet<Flag> flags, int offset) {
|
||||
myFunction = function;
|
||||
myFlags = flags;
|
||||
myImplicitOffset = offset;
|
||||
}
|
||||
|
||||
public PyFunction getFunction() {
|
||||
@@ -84,6 +82,15 @@ public interface PyCallExpression extends PyExpression {
|
||||
return myFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return number of implicitly passed positional parameters; 0 means no parameters are passed implicitly.
|
||||
* Note that a <tt>*args</tt> is never markeg as passed implicitly.
|
||||
* E.g. for a function like <tt>foo(a, b, *args)</tt> always holds <tt>getImplicitOffset() < 2</tt>.
|
||||
*/
|
||||
public int getImplicitOffset() {
|
||||
return myImplicitOffset;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ public class PyElementVisitor extends PsiElementVisitor {
|
||||
visitPyExpression(node);
|
||||
}
|
||||
|
||||
public void visitPyDecoratorList(final PyDecoratorList node) {
|
||||
visitElement(node);
|
||||
}
|
||||
|
||||
public void visitPyGeneratorExpression(final PyGeneratorExpression node) {
|
||||
visitPyExpression(node);
|
||||
}
|
||||
|
||||
@@ -258,7 +258,6 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList
|
||||
ret.my_marked_func = resolved_callee;
|
||||
if (resolved_callee != null) {
|
||||
PyFunction func = resolved_callee.getFunction();
|
||||
boolean implicit_self = resolved_callee.getFlags().contains(PyCallExpression.Flag.IMPLICIT_FIRST_ARG);
|
||||
PyParameter[] params = func.getParameterList().getParameters();
|
||||
// prepare args and slots
|
||||
List<PyExpression> unmatched_args = new LinkedList<PyExpression>();
|
||||
@@ -298,11 +297,11 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList
|
||||
}
|
||||
}
|
||||
}
|
||||
// rule out 'self'
|
||||
// rule out 'self' or other implicit params
|
||||
int param_index = 0;
|
||||
if (implicit_self && (params.length > 0)) {
|
||||
param_slots.remove(params[0].getName()); // the self param
|
||||
param_index = 1;
|
||||
for (int i=0; i < resolved_callee.getImplicitOffset() && i < params.length; i+=1) {
|
||||
param_slots.remove(params[i].getName()); // the self param
|
||||
param_index += 1;
|
||||
}
|
||||
boolean seen_tuple_arg = false;
|
||||
boolean seen_kwd_arg = false;
|
||||
|
||||
@@ -64,11 +64,12 @@ public class PyCallExpressionHelper {
|
||||
else resolved = cref.resolve();
|
||||
if (resolved != null) {
|
||||
EnumSet<PyCallExpression.Flag> flags = EnumSet.noneOf(PyCallExpression.Flag.class);
|
||||
int implicit_offset = 0;
|
||||
//boolean is_inst = isByInstance();
|
||||
if (isByInstance(us)) flags.add(PyCallExpression.Flag.IMPLICIT_FIRST_ARG);
|
||||
if (isByInstance(us)) implicit_offset += 1;
|
||||
if (resolved instanceof PyFunction) {
|
||||
PyFunction meth = (PyFunction)resolved; // constructor call?
|
||||
if (PyNames.INIT.equals(meth.getName())) flags.add(PyCallExpression.Flag.IMPLICIT_FIRST_ARG);
|
||||
if (PyNames.INIT.equals(meth.getName())) implicit_offset += 1;
|
||||
// look for closest decorator
|
||||
PyDecoratorList decolist = meth.getDecoratorList();
|
||||
if (decolist != null) {
|
||||
@@ -80,7 +81,7 @@ public class PyCallExpressionHelper {
|
||||
if (deco.isBuiltin()) {
|
||||
if (PyNames.STATICMETHOD.equals(deconame)) {
|
||||
flags.add(PyCallExpression.Flag.STATICMETHOD);
|
||||
flags.remove(PyCallExpression.Flag.IMPLICIT_FIRST_ARG);
|
||||
if (implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self'
|
||||
}
|
||||
else if (PyNames.CLASSMETHOD.equals(deconame)) {
|
||||
flags.add(PyCallExpression.Flag.CLASSMETHOD);
|
||||
@@ -91,7 +92,7 @@ public class PyCallExpressionHelper {
|
||||
}
|
||||
}
|
||||
if (!(resolved instanceof PyFunction)) return null; // omg, bogus __init__
|
||||
return new PyCallExpression.PyMarkedFunction((PyFunction) resolved, flags);
|
||||
return new PyCallExpression.PyMarkedFunction((PyFunction)resolved, flags, implicit_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,8 @@ public class PyDecoratorImpl extends PyPresentableElementImpl<PyDecoratorStub> i
|
||||
}
|
||||
|
||||
public boolean hasArgumentList() {
|
||||
return getNode().findChildByType(PyElementTypes.ARGUMENT_LIST) != null;
|
||||
ASTNode arglist_node = getNode().findChildByType(PyElementTypes.ARGUMENT_LIST);
|
||||
return (arglist_node != null) && (arglist_node.findChildByType(PyTokenTypes.LPAR) != null);
|
||||
}
|
||||
|
||||
public PyExpression getCallee() {
|
||||
@@ -100,7 +101,7 @@ public class PyDecoratorImpl extends PyPresentableElementImpl<PyDecoratorStub> i
|
||||
PyMarkedFunction callee = PyCallExpressionHelper.resolveCallee(this);
|
||||
if (callee == null) return null;
|
||||
if (! hasArgumentList()) {
|
||||
callee.getFlags().add(Flag.IMPLICIT_FIRST_ARG); // NOTE: assumes mutability
|
||||
callee = new PyMarkedFunction(callee.getFunction(), callee.getFlags(), callee.getImplicitOffset() + 1);
|
||||
}
|
||||
return callee;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.jetbrains.python.PyElementTypes;
|
||||
import com.jetbrains.python.psi.PyDecoratorList;
|
||||
import com.jetbrains.python.psi.stubs.PyDecoratorListStub;
|
||||
import com.jetbrains.python.psi.PyDecorator;
|
||||
import com.jetbrains.python.psi.PyElementVisitor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,11 @@ public class PyDecoratorListImpl extends PyBaseElementImpl<PyDecoratorListStub>
|
||||
super(astNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void acceptPyVisitor(PyElementVisitor pyVisitor) {
|
||||
pyVisitor.visitPyDecoratorList(this);
|
||||
}
|
||||
|
||||
public PyDecoratorListImpl(final PyDecoratorListStub stub) {
|
||||
super(stub, PyElementTypes.DECORATOR_LIST);
|
||||
}
|
||||
|
||||
@@ -80,4 +80,44 @@
|
||||
<line>66</line>
|
||||
<description>Unexpected argument</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>badarglist.py</file>
|
||||
<line>80</line>
|
||||
<description>Parameter 'param' unfilled</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>badarglist.py</file>
|
||||
<line>84</line>
|
||||
<description>Unexpected argument</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>badarglist.py</file>
|
||||
<line>90</line>
|
||||
<description>Parameter 'p2' unfilled</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>badarglist.py</file>
|
||||
<line>94</line>
|
||||
<description>Parameter 'p2' unfilled</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>badarglist.py</file>
|
||||
<line>119</line>
|
||||
<description>Parameter 'param' unfilled</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>badarglist.py</file>
|
||||
<line>128</line>
|
||||
<description>Parameter 'p1' unfilled</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>badarglist.py</file>
|
||||
<line>128</line>
|
||||
<description>Parameter 'p2' unfilled</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>badarglist.py</file>
|
||||
<line>132</line>
|
||||
<description>Parameter 'p2' unfilled</description>
|
||||
</problem>
|
||||
</problems>
|
||||
|
||||
@@ -5,6 +5,8 @@ PyFile:DecoratedFunction.py
|
||||
PsiElement(Py:AT)('@')
|
||||
PyReferenceExpression: staticmethod
|
||||
PsiElement(Py:IDENTIFIER)('staticmethod')
|
||||
PyArgumentList
|
||||
<empty list>
|
||||
PsiWhiteSpace('\n')
|
||||
PyDecorator: @xmlize
|
||||
PsiElement(Py:AT)('@')
|
||||
|
||||
@@ -24,7 +24,7 @@ public class PyResolveCalleeTest extends ResolveTestCase {
|
||||
public void testInstanceCall() throws Exception {
|
||||
PyCallExpression.PyMarkedFunction resolved = resolveCallee();
|
||||
assertNotNull(resolved.getFunction());
|
||||
assertTrue(resolved.getFlags().equals(EnumSet.of(PyCallExpression.Flag.IMPLICIT_FIRST_ARG)));
|
||||
assertEquals(1, resolved.getImplicitOffset());
|
||||
}
|
||||
|
||||
public void testClassCall() throws Exception {
|
||||
@@ -36,7 +36,7 @@ public class PyResolveCalleeTest extends ResolveTestCase {
|
||||
public void testDecoCall() throws Exception {
|
||||
PyCallExpression.PyMarkedFunction resolved = resolveCallee();
|
||||
assertNotNull(resolved.getFunction());
|
||||
assertTrue(resolved.getFlags().equals(EnumSet.of(PyCallExpression.Flag.IMPLICIT_FIRST_ARG)));
|
||||
assertEquals(1, resolved.getImplicitOffset());
|
||||
}
|
||||
|
||||
public void testDecoParamCall() throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user