diff --git a/python/src/com/jetbrains/python/psi/impl/PropertyBunch.java b/python/src/com/jetbrains/python/psi/impl/PropertyBunch.java index bacb2f70d92e..fd67fbc1cd57 100644 --- a/python/src/com/jetbrains/python/psi/impl/PropertyBunch.java +++ b/python/src/com/jetbrains/python/psi/impl/PropertyBunch.java @@ -1,8 +1,11 @@ package com.jetbrains.python.psi.impl; import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ArrayUtil; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.resolve.PyResolveUtil; +import com.jetbrains.python.psi.resolve.ResolveProcessor; import com.jetbrains.python.toolbox.Maybe; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -58,9 +61,13 @@ public abstract class PropertyBunch { if (source instanceof PyCallExpression) { final PyCallExpression call = (PyCallExpression)source; PyExpression callee = call.getCallee(); - if (callee instanceof PyReferenceExpression && "property".equals(callee.getName())) { - PsiElement resolved = ((PyReferenceExpression)callee).getReference().resolve(); - if (resolved != null && PyBuiltinCache.getInstance(source).hasInBuiltins(resolved)) { + if (callee instanceof PyReferenceExpression) { + PyReferenceExpression ref = (PyReferenceExpression)callee; + if (ref.getQualifier() != null) return null; + if ("property".equals(callee.getName()) && !resolvesLocally(ref)) { + // we assume that a non-local name 'property' is a built-in name. + // ref.resolve() is not used because we run in stub building phase where resolve() is frowned upon. + // NOTE: this logic fails if (quite unusually) name 'property' is directly imported from builtins. return call; } } @@ -68,6 +75,27 @@ public abstract class PropertyBunch { return null; } + /** + * Resolve in containing file only. + * @param ref what to resolve + * @return true iff ref obviously resolves to a local name (maybe partially, e.g. via import). + */ + protected static boolean resolvesLocally(@NotNull PyReferenceExpression ref) { + final String name = ref.getName(); + if (name != null) { + PsiElement outermost_context = ref; + PsiElement seeker = ref; + do { + seeker = PsiTreeUtil.getParentOfType(seeker, PyFunction.class); + if (seeker != null) outermost_context = seeker; + } while (seeker != null); + final ResolveProcessor processor = new ResolveProcessor(name); + PyResolveUtil.treeCrawlUp(processor, true, outermost_context); + return (processor.getResult() != null || processor.getDefiners().size() > 0); + } + return false; + } + /** * Tries to form a bunch from data available at a possible property() call site. * @param source should be a PyCallExpression (if not, null is immediately returned). @@ -79,55 +107,58 @@ public abstract class PropertyBunch { if (call != null) { PyArgumentList arglist = call.getArgumentList(); if (arglist != null) { - PyArgumentList.AnalysisResult analysis = arglist.analyzeCall(); - PyCallExpression.PyMarkedCallee marked_callee = analysis.getMarkedCallee(); - if (marked_callee != null) { - PyParameter[] params = marked_callee.getCallable().getParameterList().getParameters(); - List> accessors = new ArrayList>(3); - final Maybe unknown = new Maybe(); - accessors.add(null); accessors.add(null); accessors.add(null); // 3 times - final int offset = marked_callee.getImplicitOffset(); - for (Map.Entry entry: analysis.getPlainMappedParams().entrySet()) { - PyNamedParameter param = entry.getValue(); - int n = ArrayUtil.indexOf(params, param) - offset; - if (n >= 0) { - if (n < 3) { - // accessors - accessors.set(n, unknown); // definitely filled - PyExpression expr = entry.getKey(); - if (expr instanceof PyReferenceExpression) { - PyReferenceExpression arg_ref = (PyReferenceExpression)expr; - if (arg_ref.getQualifier() == null) accessors.set(n, new Maybe(target.translate(arg_ref))); + PyArgumentList.AnalysisResult analysis = PyCallExpressionHelper.analyzeBuiltinCall(call); + if (analysis != null) { + PyCallExpression.PyMarkedCallee marked_callee = analysis.getMarkedCallee(); + if (marked_callee != null) { + PyParameter[] params = marked_callee.getCallable().getParameterList().getParameters(); + List> accessors = new ArrayList>(3); + final Maybe unknown = new Maybe(); + accessors.add(null); + accessors.add(null); + accessors.add(null); // 3 times + final int offset = marked_callee.getImplicitOffset(); + for (Map.Entry entry : analysis.getPlainMappedParams().entrySet()) { + PyNamedParameter param = entry.getValue(); + int n = ArrayUtil.indexOf(params, param) - offset; + if (n >= 0) { + if (n < 3) { + // accessors + accessors.set(n, unknown); // definitely filled + PyExpression expr = entry.getKey(); + if (expr instanceof PyReferenceExpression) { + PyReferenceExpression arg_ref = (PyReferenceExpression)expr; + if (arg_ref.getQualifier() == null) accessors.set(n, new Maybe(target.translate(arg_ref))); + } } - } - else if (n == 3) { - // doc - PyExpression expr = entry.getKey(); - if (expr instanceof PyStringLiteralExpression) { - target.myDoc = ((PyStringLiteralExpression)expr).getStringValue(); + else if (n == 3) { + // doc + PyExpression expr = entry.getKey(); + if (expr instanceof PyStringLiteralExpression) { + target.myDoc = ((PyStringLiteralExpression)expr).getStringValue(); + } } } } + for (PyNamedParameter param : analysis.getKwdMappedParams()) { + // can't extract values, but values are present + int n = ArrayUtil.indexOf(params, param) - offset; + if (n >= 0 && n < 3) accessors.set(n, unknown); + } + for (PyParameter param : analysis.getTupleMappedParams()) { + // can't extract values, but values are present + int n = ArrayUtil.indexOf(params, param) - offset; + if (n >= 0 && n < 3) accessors.set(n, unknown); + } + // something could have been not set; this means None was implicitly passed + for (int i = 0; i < 3; i += 1) if (accessors.get(i) == null) accessors.set(i, new Maybe(null)); + target.myGetter = accessors.get(0); + target.mySetter = accessors.get(1); + target.myDeleter = accessors.get(2); + return true; } - for (PyNamedParameter param : analysis.getKwdMappedParams()) { - // can't extract values, but values are present - int n = ArrayUtil.indexOf(params, param) - offset; - if (n >= 0 && n < 3) accessors.set(n, unknown); - } - for (PyParameter param : analysis.getTupleMappedParams()) { - // can't extract values, but values are present - int n = ArrayUtil.indexOf(params, param) - offset; - if (n >= 0 && n < 3) accessors.set(n, unknown); - } - // something could have been not set; this means None was implicitly passed - for (int i=0; i < 3; i+=1) if (accessors.get(i) == null) accessors.set(i, new Maybe(null)); - target.myGetter = accessors.get(0); - target.mySetter = accessors.get(1); - target.myDeleter = accessors.get(2); } - return true; - } - } + } } return false; } diff --git a/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java b/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java index b5a10269ccb1..8de9a2bcfef2 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java @@ -222,7 +222,7 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList } public AnalysisResult analyzeCall() { - final AnalysisResultImpl ret = new AnalysisResultImpl(); + final PyCallExpressionHelper.AnalysisResultImpl ret = new PyCallExpressionHelper.AnalysisResultImpl(this); PyExpression[] arguments = getArguments(); // declaration-based checks // proper arglist is: [positional,...][name=value,...][*tuple,][**dict] @@ -231,480 +231,12 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList PyCallExpression call = getCallExpression(); if (call != null) { PyCallExpression.PyMarkedCallee resolved_callee = call.resolveCallee(); - ret.my_marked_func = resolved_callee; if (resolved_callee != null) { - analyzeCall(arguments, resolved_callee, ret); + ret.mapArguments(arguments, resolved_callee); } } return ret; } - private static void analyzeCall(PyExpression[] arguments, PyCallExpression.PyMarkedCallee resolved_callee, AnalysisResultImpl ret) { - Callable callable = resolved_callee.getCallable(); - PyParameterList paramlist = callable.getParameterList(); - PyParameter[] params = paramlist.getParameters(); - // prepare args and slots - List unmatched_args = new LinkedList(); - Collections.addAll(unmatched_args, arguments); - Map param_slots = new HashMap(); - PyNamedParameter kwd_slot = null; // the *tuple. might be just boolean, but this way debugging is easier - PyNamedParameter tuple_slot = null; // the **kwd - PyStarArgument kwd_arg = null; - PyStarArgument tuple_arg = null; - final List unmatched_subargs = new LinkedList(); // unmatched nested arguments will go here - // all slots are initially empty, *x and **x are not among slots - for (PyParameter a_param : params) { - PyNamedParameter n_param = a_param.getAsNamed(); - if (n_param != null) { - if (n_param.isPositionalContainer()) tuple_slot = n_param; - else if (n_param.isKeywordContainer()) kwd_slot = n_param; - else param_slots.put(a_param.getName(), null); - } - } - // look for star args - for (PyExpression arg : arguments) { - if (arg instanceof PyStarArgument) { - final PyStarArgument star_arg = (PyStarArgument)arg; - if (star_arg.isKeyword()) { - if (kwd_arg == null) kwd_arg = star_arg; - else { - ret.markArgument(arg, ArgFlag.IS_DUP_KWD); - //getHolder().createErrorAnnotation(arg, "duplicate **arg"); - unmatched_args.remove(arg); // error. ignore later - } - } - else { - if (tuple_arg == null) { - tuple_arg = star_arg; - } - else { - ret.markArgument(arg, ArgFlag.IS_DUP_TUPLE); - //getHolder().createErrorAnnotation(arg, "duplicate *arg"); - unmatched_args.remove(arg); // error. ignore later - } - } - } - } - // rule out 'self' or other implicit params - int param_index = 0; - 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; - boolean seenSingleStar = false; - ListIterator unmatched_arg_iter = unmatched_args.listIterator(); - // check positional args - while (unmatched_arg_iter.hasNext() && (param_index < params.length)) { - PyParameter a_param = params[param_index]; // its matching param - if (a_param instanceof PySingleStarParameter) { - param_index++; - seenSingleStar = true; - continue; - } - final PyExpression arg = unmatched_arg_iter.next(); // current arg - PyNamedParameter n_param = a_param.getAsNamed(); - if (n_param != null) { // named - if ( - arg instanceof PyKeywordArgument || arg instanceof PyStarArgument || - n_param.isKeywordContainer() || n_param.isPositionalContainer() - ) { - seen_tuple_arg |= (arg == tuple_arg); - seen_kwd_arg |= (arg == kwd_arg); - unmatched_arg_iter.previous(); // step back - break; - } - if (!seenSingleStar) { - param_slots.put(n_param.getName(), arg); // it cannot yet contain this name unless function definition is broken - ret.my_plain_mapped_params.put(arg, n_param); - } - else { - param_index++; - continue; - } - } - else { // tuple: it may contain only positionals or other tuples. - PyTupleParameter tupleParameter = a_param.getAsTuple(); - if (tupleParameter != null) { - unmatched_arg_iter.previous(); // step back so that the visitor takes this arg again - MyParamVisitor visitor = new MyParamVisitor(unmatched_arg_iter, ret); - visitor.enterTuple(a_param.getAsTuple()); // will recurse as needed - unmatched_subargs.addAll(visitor.getUnmatchedSubargs()); // what it's seen - } - } - unmatched_arg_iter.remove(); // it has been matched - param_index += 1; - } - if (!seen_kwd_arg) { // **kwd arg is the last; if it's present, checking the rest would be useless - if (!seen_tuple_arg) { // any pos args can only come before *arg - // some pos args might go to a *param - if (tuple_slot != null) { - while (unmatched_arg_iter.hasNext()) { - PyExpression arg = unmatched_arg_iter.next(); - if (arg instanceof PyKeywordArgument) { - unmatched_arg_iter.previous(); // step back - break; - } - ret.my_plain_mapped_params.put(arg, tuple_slot); - unmatched_arg_iter.remove(); // consumed as nameless - } - } - } - // check named args - boolean seen_kwd = false; - while (unmatched_arg_iter.hasNext()) { - PyExpression arg = unmatched_arg_iter.next(); - if (arg instanceof PyKeywordArgument) { - if (!seen_kwd_arg && !seen_tuple_arg) { - final String argname = ((PyKeywordArgument)arg).getKeyword(); - if (param_slots.containsKey(argname)) { // slot is known - if (param_slots.get(argname) == null) { // slot is not filled - param_slots.put(argname, arg); - // we'll put() it to ret.my_plain_mapped_params later - seen_kwd = true; - } - else { - //getHolder().createErrorAnnotation(arg, "duplicate arg '" + argname + "'"); - ret.markArgument(arg, ArgFlag.IS_DUP); - } - unmatched_arg_iter.remove(); // it has been matched or flagged, forget - } - // else: ignore unknown arg, we'll deal with them later - } - else { - ret.markArgument(arg, ArgFlag.IS_UNMAPPED); - //getHolder().createErrorAnnotation(arg, "cannot appear past an *arg"); - unmatched_arg_iter.remove(); // it has been flagged, forget - } - } - else if (seen_kwd && (arg != kwd_arg)) { - ret.markArgument(arg, ArgFlag.IS_POS_PAST_KWD); - //getHolder().createErrorAnnotation(arg, "non-keyword arg after keyword arg"); - unmatched_arg_iter.remove(); // it has been flagged, forget - } - seen_tuple_arg |= (arg == tuple_arg); - seen_kwd_arg |= (arg == kwd_arg); - } - // some named args might go to a **kwd param - if (kwd_slot != null) { - unmatched_arg_iter = unmatched_args.listIterator(); // anew - while (unmatched_arg_iter.hasNext()) { - PyExpression arg = unmatched_arg_iter.next(); - if (arg instanceof PyKeywordArgument) { - ret.my_plain_mapped_params.put(arg, kwd_slot); - unmatched_arg_iter.remove(); // consumed by **kwd - } - // no else: name errors are all detected above - } - } - } - if (seen_tuple_arg) { // link remaining params to *arg if present - for (PyParameter a_param : params) { - PyNamedParameter n_param = a_param.getAsNamed(); - if (n_param != null) { - final String param_name = n_param.getName(); - if ( - (!n_param.hasDefaultValue()) && // has no default value - param_slots.containsKey(param_name) && // known as a slot - (param_slots.get(param_name) == null) // the slot yet unfilled - ) { - param_slots.put(param_name, tuple_arg); - unmatched_args.remove(tuple_arg); - } - } - } - } - if (seen_kwd_arg) { // link remaining params to **kwarg if present - for (PyParameter a_param : params) { - PyNamedParameter n_param = a_param.getAsNamed(); - if (n_param != null) { - final String param_name = n_param.getName(); - if ( - (!n_param.hasDefaultValue()) && // has no default value - param_slots.containsKey(param_name) && // known as a slot - (param_slots.get(param_name) == null) // the slot yet unfilled - ) { - param_slots.put(param_name, kwd_arg); - unmatched_args.remove(kwd_arg); - } - } - } - } - // check and collect all yet unfilled params without default values - Map unfilled_params = new HashMap(); - for (PyParameter a_param : params) { - PyNamedParameter n_param = a_param.getAsNamed(); - if (n_param != null) { - final String param_name = n_param.getName(); - if ( - (!n_param.hasDefaultValue()) && // has no default value - param_slots.containsKey(param_name) && // known as a slot - (param_slots.get(param_name) == null) // the slot yet unfilled - ) { - if (tuple_arg != null) { - // An *arg, if present, fills all positional params - param_slots.put(param_name, tuple_arg); - unmatched_args.remove(tuple_arg); - } - else { - unfilled_params.put(param_name, n_param); - } - } - } - } - // *arg and **kwarg are not in slots list; write any *param or **param off to them if present. - if (kwd_arg != null && kwd_slot != null) { - ret.my_kwd_mapped_params.add(kwd_slot); - unmatched_args.remove(kwd_arg); - } - if (tuple_arg != null && tuple_slot != null) { - ret.my_tuple_mapped_params.add(tuple_slot); - unmatched_args.remove(tuple_arg); - } - // maybe we did not map a star arg because all eligible params have defaults; time to be less picky now. - if (tuple_arg != null && ret.my_tuple_mapped_params.isEmpty()) { // link remaining params to *arg if nothing else is mapped to it - for (PyParameter a_param : params) { - PyNamedParameter n_param = a_param.getAsNamed(); - if (n_param != null) { - final String param_name = n_param.getName(); - if ( - param_slots.containsKey(param_name) && // known as a slot - (param_slots.get(param_name) == null) // the slot yet unfilled - ) { - param_slots.put(param_name, tuple_arg); - unmatched_args.remove(tuple_arg); - } - } - } - } - if (kwd_arg != null && ret.my_kwd_mapped_params.isEmpty()) { // link remaining params to **kwarg if nothing else is mapped to it - for (PyParameter a_param : params) { - PyNamedParameter n_param = a_param.getAsNamed(); - if (n_param != null) { - final String param_name = n_param.getName(); - if ( - param_slots.containsKey(param_name) && // known as a slot - (param_slots.get(param_name) == null) // the slot yet unfilled - ) { - param_slots.put(param_name, kwd_arg); - unmatched_args.remove(kwd_arg); - } - } - } - } - // any args left? - boolean tuple_arg_consumed_some = false; - for (PyExpression arg : param_slots.values()) { // any(\x: x == tuple_arg) - if (arg != null && arg == tuple_arg) { - tuple_arg_consumed_some = true; - break; - } - } - for (PyExpression arg : unmatched_args) { - //getHolder().createErrorAnnotation(arg, "unexpected arg"); - if (arg == kwd_arg && tuple_arg_consumed_some) continue; // *arg consumed anything that **arg might equally consume. - ret.markArgument(arg, ArgFlag.IS_UNMAPPED); - } - // any params still unfilled? - for (final PyNamedParameter param : unfilled_params.values()) { - // getHolder().createErrorAnnotation(close_paren, "parameter '" + param_name + "' unfilled"); - ret.my_unmapped_params.add(param); - } - // copy the mapping of args - for (PyParameter a_param : params) { - PyNamedParameter n_param = a_param.getAsNamed(); - if (n_param != null) { - PyExpression arg = param_slots.get(n_param.getName()); - if (arg != null) { - if (arg instanceof PyStarArgument) { - PyStarArgument star_arg = (PyStarArgument)arg; - if (star_arg.isKeyword()) ret.my_kwd_mapped_params.add(n_param); - else ret.my_tuple_mapped_params.add(n_param); - } - else ret.my_plain_mapped_params.put(arg, n_param); - } - } - } - // copy starred args - ret.my_kwd_arg = kwd_arg; - ret.my_tuple_arg = tuple_arg; - // add unmatched nested arguments - for (PyExpression subarg : unmatched_subargs) { - ret.my_arg_flags.put(subarg, EnumSet.of(ArgFlag.IS_UNMAPPED)); - } - } - - private static class MyParamVisitor extends PyElementVisitor { - private final Iterator myArgIterator; - private final AnalysisResultImpl myResult; - private final List myUnmatchedSubargs; - - private MyParamVisitor(Iterator arg_iterator, AnalysisResultImpl ret) { - myArgIterator = arg_iterator; - myResult = ret; - myUnmatchedSubargs = new ArrayList(5); // arbitrary 'enough' - } - - private Collection getUnmatchedSubargs() { - return myUnmatchedSubargs; - } - - @Override - public void visitPyParameter(PyParameter node) { - PyNamedParameter named = node.getAsNamed(); - if (named != null) enterNamed(named); - else enterTuple(node.getAsTuple()); - } - - public void enterTuple(PyTupleParameter param) { - PyExpression arg = null; - if (myArgIterator.hasNext()) arg = myArgIterator.next(); - // try to unpack a tuple expr in argument, if there's any - PyExpression[] elements = null; - if (arg instanceof PyParenthesizedExpression) { - PyExpression inner_expr = ((PyParenthesizedExpression)arg).getContainedExpression(); - if (inner_expr instanceof PyTupleExpression) elements = ((PyTupleExpression)inner_expr).getElements(); - } - else if (arg instanceof PyListLiteralExpression) { - elements = ((PyListLiteralExpression)arg).getElements(); - } - final PyParameter[] nested_params = param.getContents(); - if (elements != null) { // recursively map expression's tuple to parameter's. - MyParamVisitor visitor = new MyParamVisitor(Arrays.asList(elements).iterator(), myResult); - for (PyParameter nested : nested_params) nested.accept(visitor); - myUnmatchedSubargs.addAll(visitor.getUnmatchedSubargs()); - } - else { // map all what's inside to this arg - final List nested_mapped = new ArrayList(nested_params.length); - ParamHelper.walkDownParamArray( - nested_params, - new ParamHelper.ParamVisitor() { - @Override public void visitNamedParameter(PyNamedParameter param, boolean first, boolean last) { - nested_mapped.add(param); - } - } - ); - myResult.my_nested_mapped_params.put(arg, nested_mapped); - } - } - - public void enterNamed(PyNamedParameter param) { - if (myArgIterator.hasNext()) { - PyExpression subarg = myArgIterator.next(); - myResult.my_plain_mapped_params.put(subarg, param); - } - else { - myResult.my_unmapped_params.add(param); - } - // ...and *arg or **arg just won't parse inside a tuple, no need to handle it here - } - } - - private class AnalysisResultImpl implements PyArgumentList.AnalysisResult { - - private final Map my_plain_mapped_params; // one param per arg - private final Map> my_nested_mapped_params; // one arg sweeps a nested tuple of params - private PyStarArgument my_tuple_arg; // the *arg - private PyStarArgument my_kwd_arg; // the **arg - private final List my_tuple_mapped_params; // params mapped to *arg - private final List my_kwd_mapped_params; // params mapped to **arg - private final List my_unmapped_params; - private final Map> my_arg_flags; // flags of every arg - private PyCallExpression.PyMarkedCallee my_marked_func; - - public AnalysisResultImpl() { - // full of empty containers - my_plain_mapped_params = new HashMap(); - my_nested_mapped_params = new HashMap>(); - my_tuple_mapped_params = new ArrayList(); - my_kwd_mapped_params = new ArrayList(); - my_unmapped_params = new ArrayList(); - my_arg_flags = new HashMap>(); - my_marked_func = null; - } - - public boolean isImplicitlyResolved() { - return my_marked_func == null ? false : my_marked_func.isImplicitlyResolved(); - } - - /** - * @return A mapping argument->parameter for non-starred arguments (but includes starred parameters). - */ - public @NotNull Map getPlainMappedParams() { - return my_plain_mapped_params; - } - - @NotNull - public Map> getNestedMappedParams() { - return my_nested_mapped_params; - } - - /** - * @return First *arg, or null. - */ - public PyStarArgument getTupleArg(){ - return my_tuple_arg; - } - - /** - * @return A list of parameters mapped to an *arg. - */ - public @NotNull List getTupleMappedParams(){ - return my_tuple_mapped_params; - } - - /** - * @return First **arg, or null. - */ - public PyStarArgument getKwdArg(){ - return my_kwd_arg; - } - - /** - * @return A list of parameters mapped to an **arg. - */ - public @NotNull List getKwdMappedParams(){ - return my_kwd_mapped_params; - } - - /** - * @return A list of parameters for which no arguments were found ('missing'). - */ - public @NotNull - List getUnmappedParams(){ - return my_unmapped_params; - } - - /** - * @return result of a resolveCallee() against the function call to which the paramater list belongs. - */ - @Nullable - public PyCallExpression.PyMarkedCallee getMarkedCallee() { - return my_marked_func; - } - - /** - * @return Lists all args with their flags. - */ - public Map> getArgumentFlags(){ - return my_arg_flags; - } - - public PyArgumentList getArgumentList() { - return PyArgumentListImpl.this; // that is, 'outer' - } - - protected PyExpression markArgument(PyExpression arg, ArgFlag... flags) { - EnumSet argflags = my_arg_flags.get(arg); - if (argflags == null) { - argflags = EnumSet.noneOf(ArgFlag.class); - } - argflags.addAll(Arrays.asList(flags)); - my_arg_flags.put(arg, argflags); - return arg; - } - } - } diff --git a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java index 50b31c3e21e3..cd08eda452c0 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java +++ b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java @@ -9,9 +9,10 @@ import com.jetbrains.python.psi.resolve.QualifiedResolveResult; import com.jetbrains.python.psi.types.PyClassType; import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.TypeEvalContext; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.EnumSet; +import java.util.*; /** * Functions common to different implementors of PyCallExpression, with different base classes. @@ -253,4 +254,516 @@ public class PyCallExpressionHelper { return false; } + /** + * Maps arguments of a call assuming it's a call to a builtin. No resolution or assignment chasing is done inside; + * caller should have done that. + * Class to constructor mapping is made inside, though. + * @param call an unqualified call expression; name is directly searched in builtins. + * @return analysis with argument mapping. + */ + @Nullable + public static PyArgumentList.AnalysisResult analyzeBuiltinCall(@NotNull PyCallExpression call) { + final AnalysisResultImpl ret = new AnalysisResultImpl(call.getArgumentList()); + PyExpression[] arguments = call.getArguments(); + final String name = call.getCallee().getName(); + if (name != null) { + PyFile builtins = PyBuiltinCache.getInstance(call).getBuiltinsFile(); + if (builtins != null) { + int arg_offset = 0; + EnumSet flags = EnumSet.noneOf(PyFunction.Flag.class); + PyFunction func = builtins.findTopLevelFunction(name); + if (func == null) { + PyClass cls = builtins.findTopLevelClass(name); + if (cls != null) { + func = cls.findInitOrNew(true); + arg_offset = 1; + } + } + if (func != null) { + if (PyNames.NEW.equals(func.getName())) flags.add(PyFunction.Flag.CLASSMETHOD); + ret.mapArguments(arguments, new PyCallExpression.PyMarkedCallee(func, flags, arg_offset, false)); + return ret; + } + } + } + return null; + } + + + static class MyParamVisitor extends PyElementVisitor { + private final Iterator myArgIterator; + private final AnalysisResultImpl myResult; + private final List myUnmatchedSubargs; + + private MyParamVisitor(Iterator arg_iterator, AnalysisResultImpl ret) { + myArgIterator = arg_iterator; + myResult = ret; + myUnmatchedSubargs = new ArrayList(5); // arbitrary 'enough' + } + + private Collection getUnmatchedSubargs() { + return myUnmatchedSubargs; + } + + @Override + public void visitPyParameter(PyParameter node) { + PyNamedParameter named = node.getAsNamed(); + if (named != null) enterNamed(named); + else enterTuple(node.getAsTuple()); + } + + public void enterTuple(PyTupleParameter param) { + PyExpression arg = null; + if (myArgIterator.hasNext()) arg = myArgIterator.next(); + // try to unpack a tuple expr in argument, if there's any + PyExpression[] elements = null; + if (arg instanceof PyParenthesizedExpression) { + PyExpression inner_expr = ((PyParenthesizedExpression)arg).getContainedExpression(); + if (inner_expr instanceof PyTupleExpression) elements = ((PyTupleExpression)inner_expr).getElements(); + } + else if (arg instanceof PyListLiteralExpression) { + elements = ((PyListLiteralExpression)arg).getElements(); + } + final PyParameter[] nested_params = param.getContents(); + if (elements != null) { // recursively map expression's tuple to parameter's. + MyParamVisitor visitor = new MyParamVisitor(Arrays.asList(elements).iterator(), myResult); + for (PyParameter nested : nested_params) nested.accept(visitor); + myUnmatchedSubargs.addAll(visitor.getUnmatchedSubargs()); + } + else { // map all what's inside to this arg + final List nested_mapped = new ArrayList(nested_params.length); + ParamHelper.walkDownParamArray( + nested_params, + new ParamHelper.ParamVisitor() { + @Override public void visitNamedParameter(PyNamedParameter param, boolean first, boolean last) { + nested_mapped.add(param); + } + } + ); + myResult.my_nested_mapped_params.put(arg, nested_mapped); + } + } + + public void enterNamed(PyNamedParameter param) { + if (myArgIterator.hasNext()) { + PyExpression subarg = myArgIterator.next(); + myResult.my_plain_mapped_params.put(subarg, param); + } + else { + myResult.my_unmapped_params.add(param); + } + // ...and *arg or **arg just won't parse inside a tuple, no need to handle it here + } + } + + static class AnalysisResultImpl implements PyArgumentList.AnalysisResult { + + private final Map my_plain_mapped_params; // one param per arg + private final Map> my_nested_mapped_params; // one arg sweeps a nested tuple of params + private PyStarArgument my_tuple_arg; // the *arg + private PyStarArgument my_kwd_arg; // the **arg + private final List my_tuple_mapped_params; // params mapped to *arg + private final List my_kwd_mapped_params; // params mapped to **arg + private final List my_unmapped_params; + private final Map> my_arg_flags; // flags of every arg + private PyCallExpression.PyMarkedCallee my_marked_callee; + private PyArgumentList my_argument_list; + + public AnalysisResultImpl(PyArgumentList arglist) { + // full of empty containers + my_plain_mapped_params = new HashMap(); + my_nested_mapped_params = new HashMap>(); + my_tuple_mapped_params = new ArrayList(); + my_kwd_mapped_params = new ArrayList(); + my_unmapped_params = new ArrayList(); + my_arg_flags = new HashMap>(); + my_marked_callee = null; + my_argument_list = arglist; + } + + /** + * Maps arguments of a call to parameters of a callee. + * must contain already resolved callee with flags set appropriately + * @param arguments what to map, get if from call site + * @param resolved_callee + */ + void mapArguments(PyExpression[] arguments, PyCallExpression.PyMarkedCallee resolved_callee) { + my_marked_callee = resolved_callee; + Callable callable = resolved_callee.getCallable(); + PyParameterList paramlist = callable.getParameterList(); + PyParameter[] params = paramlist.getParameters(); + // prepare args and slots + List unmatched_args = new LinkedList(); + Collections.addAll(unmatched_args, arguments); + Map param_slots = new HashMap(); + PyNamedParameter kwd_slot = null; // the *tuple. might be just boolean, but this way debugging is easier + PyNamedParameter tuple_slot = null; // the **kwd + PyStarArgument kwd_arg = null; + PyStarArgument tuple_arg = null; + final List unmatched_subargs = new LinkedList(); // unmatched nested arguments will go here + // all slots are initially empty, *x and **x are not among slots + for (PyParameter a_param : params) { + PyNamedParameter n_param = a_param.getAsNamed(); + if (n_param != null) { + if (n_param.isPositionalContainer()) tuple_slot = n_param; + else if (n_param.isKeywordContainer()) kwd_slot = n_param; + else param_slots.put(a_param.getName(), null); + } + } + // look for star args + for (PyExpression arg : arguments) { + if (arg instanceof PyStarArgument) { + final PyStarArgument star_arg = (PyStarArgument)arg; + if (star_arg.isKeyword()) { + if (kwd_arg == null) kwd_arg = star_arg; + else { + markArgument(arg, PyArgumentList.ArgFlag.IS_DUP_KWD); + //getHolder().createErrorAnnotation(arg, "duplicate **arg"); + unmatched_args.remove(arg); // error. ignore later + } + } + else { + if (tuple_arg == null) { + tuple_arg = star_arg; + } + else { + markArgument(arg, PyArgumentList.ArgFlag.IS_DUP_TUPLE); + //getHolder().createErrorAnnotation(arg, "duplicate *arg"); + unmatched_args.remove(arg); // error. ignore later + } + } + } + } + // rule out 'self' or other implicit params + int param_index = 0; + 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; + boolean seenSingleStar = false; + ListIterator unmatched_arg_iter = unmatched_args.listIterator(); + // check positional args + while (unmatched_arg_iter.hasNext() && (param_index < params.length)) { + PyParameter a_param = params[param_index]; // its matching param + if (a_param instanceof PySingleStarParameter) { + param_index++; + seenSingleStar = true; + continue; + } + final PyExpression arg = unmatched_arg_iter.next(); // current arg + PyNamedParameter n_param = a_param.getAsNamed(); + if (n_param != null) { // named + if ( + arg instanceof PyKeywordArgument || arg instanceof PyStarArgument || + n_param.isKeywordContainer() || n_param.isPositionalContainer() + ) { + seen_tuple_arg |= (arg == tuple_arg); + seen_kwd_arg |= (arg == kwd_arg); + unmatched_arg_iter.previous(); // step back + break; + } + if (!seenSingleStar) { + param_slots.put(n_param.getName(), arg); // it cannot yet contain this name unless function definition is broken + my_plain_mapped_params.put(arg, n_param); + } + else { + param_index++; + continue; + } + } + else { // tuple: it may contain only positionals or other tuples. + PyTupleParameter tupleParameter = a_param.getAsTuple(); + if (tupleParameter != null) { + unmatched_arg_iter.previous(); // step back so that the visitor takes this arg again + MyParamVisitor visitor = new MyParamVisitor(unmatched_arg_iter, this); + visitor.enterTuple(a_param.getAsTuple()); // will recurse as needed + unmatched_subargs.addAll(visitor.getUnmatchedSubargs()); // what it's seen + } + } + unmatched_arg_iter.remove(); // it has been matched + param_index += 1; + } + if (!seen_kwd_arg) { // **kwd arg is the last; if it's present, checking the rest would be useless + if (!seen_tuple_arg) { // any pos args can only come before *arg + // some pos args might go to a *param + if (tuple_slot != null) { + while (unmatched_arg_iter.hasNext()) { + PyExpression arg = unmatched_arg_iter.next(); + if (arg instanceof PyKeywordArgument) { + unmatched_arg_iter.previous(); // step back + break; + } + my_plain_mapped_params.put(arg, tuple_slot); + unmatched_arg_iter.remove(); // consumed as nameless + } + } + } + // check named args + boolean seen_kwd = false; + while (unmatched_arg_iter.hasNext()) { + PyExpression arg = unmatched_arg_iter.next(); + if (arg instanceof PyKeywordArgument) { + if (!seen_kwd_arg && !seen_tuple_arg) { + final String argname = ((PyKeywordArgument)arg).getKeyword(); + if (param_slots.containsKey(argname)) { // slot is known + if (param_slots.get(argname) == null) { // slot is not filled + param_slots.put(argname, arg); + // we'll put() it to ret.my_plain_mapped_params later + seen_kwd = true; + } + else { + //getHolder().createErrorAnnotation(arg, "duplicate arg '" + argname + "'"); + markArgument(arg, PyArgumentList.ArgFlag.IS_DUP); + } + unmatched_arg_iter.remove(); // it has been matched or flagged, forget + } + // else: ignore unknown arg, we'll deal with them later + } + else { + markArgument(arg, PyArgumentList.ArgFlag.IS_UNMAPPED); + //getHolder().createErrorAnnotation(arg, "cannot appear past an *arg"); + unmatched_arg_iter.remove(); // it has been flagged, forget + } + } + else if (seen_kwd && (arg != kwd_arg)) { + markArgument(arg, PyArgumentList.ArgFlag.IS_POS_PAST_KWD); + //getHolder().createErrorAnnotation(arg, "non-keyword arg after keyword arg"); + unmatched_arg_iter.remove(); // it has been flagged, forget + } + seen_tuple_arg |= (arg == tuple_arg); + seen_kwd_arg |= (arg == kwd_arg); + } + // some named args might go to a **kwd param + if (kwd_slot != null) { + unmatched_arg_iter = unmatched_args.listIterator(); // anew + while (unmatched_arg_iter.hasNext()) { + PyExpression arg = unmatched_arg_iter.next(); + if (arg instanceof PyKeywordArgument) { + my_plain_mapped_params.put(arg, kwd_slot); + unmatched_arg_iter.remove(); // consumed by **kwd + } + // no else: name errors are all detected above + } + } + } + if (seen_tuple_arg) { // link remaining params to *arg if present + for (PyParameter a_param : params) { + PyNamedParameter n_param = a_param.getAsNamed(); + if (n_param != null) { + final String param_name = n_param.getName(); + if ( + (!n_param.hasDefaultValue()) && // has no default value + param_slots.containsKey(param_name) && // known as a slot + (param_slots.get(param_name) == null) // the slot yet unfilled + ) { + param_slots.put(param_name, tuple_arg); + unmatched_args.remove(tuple_arg); + } + } + } + } + if (seen_kwd_arg) { // link remaining params to **kwarg if present + for (PyParameter a_param : params) { + PyNamedParameter n_param = a_param.getAsNamed(); + if (n_param != null) { + final String param_name = n_param.getName(); + if ( + (!n_param.hasDefaultValue()) && // has no default value + param_slots.containsKey(param_name) && // known as a slot + (param_slots.get(param_name) == null) // the slot yet unfilled + ) { + param_slots.put(param_name, kwd_arg); + unmatched_args.remove(kwd_arg); + } + } + } + } + // check and collect all yet unfilled params without default values + Map unfilled_params = new HashMap(); + for (PyParameter a_param : params) { + PyNamedParameter n_param = a_param.getAsNamed(); + if (n_param != null) { + final String param_name = n_param.getName(); + if ( + (!n_param.hasDefaultValue()) && // has no default value + param_slots.containsKey(param_name) && // known as a slot + (param_slots.get(param_name) == null) // the slot yet unfilled + ) { + if (tuple_arg != null) { + // An *arg, if present, fills all positional params + param_slots.put(param_name, tuple_arg); + unmatched_args.remove(tuple_arg); + } + else { + unfilled_params.put(param_name, n_param); + } + } + } + } + // *arg and **kwarg are not in slots list; write any *param or **param off to them if present. + if (kwd_arg != null && kwd_slot != null) { + my_kwd_mapped_params.add(kwd_slot); + unmatched_args.remove(kwd_arg); + } + if (tuple_arg != null && tuple_slot != null) { + my_tuple_mapped_params.add(tuple_slot); + unmatched_args.remove(tuple_arg); + } + // maybe we did not map a star arg because all eligible params have defaults; time to be less picky now. + if (tuple_arg != null && my_tuple_mapped_params.isEmpty()) { // link remaining params to *arg if nothing else is mapped to it + for (PyParameter a_param : params) { + PyNamedParameter n_param = a_param.getAsNamed(); + if (n_param != null) { + final String param_name = n_param.getName(); + if ( + param_slots.containsKey(param_name) && // known as a slot + (param_slots.get(param_name) == null) // the slot yet unfilled + ) { + param_slots.put(param_name, tuple_arg); + unmatched_args.remove(tuple_arg); + } + } + } + } + if (kwd_arg != null && my_kwd_mapped_params.isEmpty()) { // link remaining params to **kwarg if nothing else is mapped to it + for (PyParameter a_param : params) { + PyNamedParameter n_param = a_param.getAsNamed(); + if (n_param != null) { + final String param_name = n_param.getName(); + if ( + param_slots.containsKey(param_name) && // known as a slot + (param_slots.get(param_name) == null) // the slot yet unfilled + ) { + param_slots.put(param_name, kwd_arg); + unmatched_args.remove(kwd_arg); + } + } + } + } + // any args left? + boolean tuple_arg_consumed_some = false; + for (PyExpression arg : param_slots.values()) { // any(\x: x == tuple_arg) + if (arg != null && arg == tuple_arg) { + tuple_arg_consumed_some = true; + break; + } + } + for (PyExpression arg : unmatched_args) { + //getHolder().createErrorAnnotation(arg, "unexpected arg"); + if (arg == kwd_arg && tuple_arg_consumed_some) continue; // *arg consumed anything that **arg might equally consume. + markArgument(arg, PyArgumentList.ArgFlag.IS_UNMAPPED); + } + // any params still unfilled? + for (final PyNamedParameter param : unfilled_params.values()) { + // getHolder().createErrorAnnotation(close_paren, "parameter '" + param_name + "' unfilled"); + my_unmapped_params.add(param); + } + // copy the mapping of args + for (PyParameter a_param : params) { + PyNamedParameter n_param = a_param.getAsNamed(); + if (n_param != null) { + PyExpression arg = param_slots.get(n_param.getName()); + if (arg != null) { + if (arg instanceof PyStarArgument) { + PyStarArgument star_arg = (PyStarArgument)arg; + if (star_arg.isKeyword()) my_kwd_mapped_params.add(n_param); + else my_tuple_mapped_params.add(n_param); + } + else my_plain_mapped_params.put(arg, n_param); + } + } + } + // copy starred args + my_kwd_arg = kwd_arg; + my_tuple_arg = tuple_arg; + // add unmatched nested arguments + for (PyExpression subarg : unmatched_subargs) { + my_arg_flags.put(subarg, EnumSet.of(PyArgumentList.ArgFlag.IS_UNMAPPED)); + } + } + + public boolean isImplicitlyResolved() { + return my_marked_callee == null ? false : my_marked_callee.isImplicitlyResolved(); + } + + /** + * @return A mapping argument->parameter for non-starred arguments (but includes starred parameters). + */ + public @NotNull + Map getPlainMappedParams() { + return my_plain_mapped_params; + } + + @NotNull + public Map> getNestedMappedParams() { + return my_nested_mapped_params; + } + + /** + * @return First *arg, or null. + */ + public PyStarArgument getTupleArg(){ + return my_tuple_arg; + } + + /** + * @return A list of parameters mapped to an *arg. + */ + public @NotNull List getTupleMappedParams(){ + return my_tuple_mapped_params; + } + + /** + * @return First **arg, or null. + */ + public PyStarArgument getKwdArg(){ + return my_kwd_arg; + } + + /** + * @return A list of parameters mapped to an **arg. + */ + public @NotNull List getKwdMappedParams(){ + return my_kwd_mapped_params; + } + + /** + * @return A list of parameters for which no arguments were found ('missing'). + */ + public @NotNull + List getUnmappedParams(){ + return my_unmapped_params; + } + + /** + * @return result of a resolveCallee() against the function call to which the paramater list belongs. + */ + @Nullable + public PyCallExpression.PyMarkedCallee getMarkedCallee() { + return my_marked_callee; + } + + /** + * @return Lists all args with their flags. + */ + public Map> getArgumentFlags(){ + return my_arg_flags; + } + + public PyArgumentList getArgumentList() { + return my_argument_list; // that is, 'outer' + } + + protected PyExpression markArgument(PyExpression arg, PyArgumentList.ArgFlag... flags) { + EnumSet argflags = my_arg_flags.get(arg); + if (argflags == null) { + argflags = EnumSet.noneOf(PyArgumentList.ArgFlag.class); + } + argflags.addAll(Arrays.asList(flags)); + my_arg_flags.put(arg, argflags); + return arg; + } + } } diff --git a/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java b/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java index b0a3e567112d..6fab44b952c5 100644 --- a/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java +++ b/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java @@ -30,8 +30,8 @@ public class PyResolveUtil { /** * Returns closest previous node of given class, as input file would have it. - * @param elt node from which to look for a previous atatement. - * @param classes which class of the previous nodes to find. + * @param elt node from which to look for a previous statement. + * @param condition determines where a node is considered found. * @return previous statement, or null. */ @Nullable