diff --git a/python/src/com/jetbrains/python/PyParameterInfoHandler.java b/python/src/com/jetbrains/python/PyParameterInfoHandler.java index 82e594231e25..bb9373a71c6e 100644 --- a/python/src/com/jetbrains/python/PyParameterInfoHandler.java +++ b/python/src/com/jetbrains/python/PyParameterInfoHandler.java @@ -3,13 +3,19 @@ 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.*; +import static com.jetbrains.python.psi.PyCallExpression.Flag; +import static com.jetbrains.python.psi.PyCallExpression.PyMarkedFunction; import org.jetbrains.annotations.NotNull; +import java.lang.reflect.Array; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + /** * @author yole */ -public class PyParameterInfoHandler implements ParameterInfoHandler { +public class PyParameterInfoHandler implements ParameterInfoHandler { public boolean couldShowInLookup() { return true; @@ -18,31 +24,17 @@ public class PyParameterInfoHandler implements ParameterInfoHandler= 0 ? context.getCurrentParameterIndex():params.length; + final PyParameter[] params = py_function.getParameterList().getParameters(); + final PyArgumentList arglist = result.getArgumentList(); + int arg_index = context.getCurrentParameterIndex() >= 0 ? context.getCurrentParameterIndex():params.length; - int highlightStartOffset = -1; - int highlightEndOffset = -1; - - StringBuilder signatureBuilder = new StringBuilder(); - - for(int i=0; i param_indexes = new HashMap(); + for (int i=0; i < params.length; i += 1) param_indexes.put(params[i], i); + EnumSet[] flags = (EnumSet[])Array.newInstance(EnumSet.class, params.length); + // ^^ 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.STRIKEOUT); // show but mark as absent + } + int cur_arg_index = 0; + for (PyExpression arg : arglist.getArguments()) { + if (cur_arg_index == arg_index) { + PyParameter param = result.getPlainMappedParams().get(arg); + if (param != null) { + final Integer param_index = param_indexes.get(param); + if (param_index < flags.length) { + flags[param_index].add(ParameterInfoUIContextEx.Flag.HIGHLIGHT); + } + } + else if (arg == result.getTupleArg()) { + // mark all params that map to *arg + for (PyParameter tpar : result.getTupleMappedParams()) { + final Integer param_index = param_indexes.get(tpar); + if (param_index != null && param_index.intValue() < flags.length) flags[param_index.intValue()].add(ParameterInfoUIContextEx.Flag.HIGHLIGHT); + } + } + else if (arg == result.getKwdArg()) { + // mark all params that map to **arg + for (PyParameter tpar : result.getKwdMappedParams()) { + final Integer param_index = param_indexes.get(tpar); + if (param_index != null && param_index < flags.length) flags[param_index].add(ParameterInfoUIContextEx.Flag.HIGHLIGHT); + } + } + } + // else: stay unhilited + cur_arg_index += 1; + } + + + if (context instanceof ParameterInfoUIContextEx) { + final ParameterInfoUIContextEx pic = (ParameterInfoUIContextEx)context; + pic.setupUIComponentPresentation(param_texts, flags, false, context.getDefaultParameterColor()); + } + /* context.setupUIComponentPresentation(signatureBuilder.toString(), highlightStartOffset, highlightEndOffset, false, false, false, context.getDefaultParameterColor()); + */ } } diff --git a/python/src/com/jetbrains/python/psi/PyArgumentList.java b/python/src/com/jetbrains/python/psi/PyArgumentList.java index 65586d95a4f8..8294607f12e8 100644 --- a/python/src/com/jetbrains/python/psi/PyArgumentList.java +++ b/python/src/com/jetbrains/python/psi/PyArgumentList.java @@ -19,12 +19,12 @@ package com.jetbrains.python.psi; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.*; + /** - * Created by IntelliJ IDEA. + * Represents an argument list of a function call. * User: yole * Date: 29.05.2005 - * Time: 13:44:24 - * To change this template use File | Settings | File Templates. */ public interface PyArgumentList extends PyElement { @@ -39,4 +39,79 @@ public interface PyArgumentList extends PyElement { @Nullable PyCallExpression getCallExpression(); + /** + * Tries to map the argument list to callee's idea of parameters. + * @return a result object with mappings and diagnostic flags. + */ + AnalysisResult analyzeCall(); + + /** + * Flags to mark analysis results for an argument. + * Theoretically can be used together, but currently only make sense as a single value per argument. + */ + enum ArgFlag { + /** duplicate plain */ IS_DUP, + /** unexpected */ IS_UNMAPPED, + /** duplicate **arg */ IS_DUP_KWD, + /** duplicate *arg */ IS_DUP_TUPLE, + /** positional past keyword */ IS_POS_PAST_KWD + } + + + /** + * Result of analysis of argument list application to the callee. + * Contains neatly arranged lists and mappinga between arguments and parameters, + * including error diagnostics. + */ + interface AnalysisResult { + + /** + * @return A mapping parameter->argument for non-starred parameters (but includes starred argument). + */ + @NotNull Map getPlainMappedParams(); + + /** + * @return First *arg, or null. + */ + @Nullable + PyStarArgument getTupleArg(); + + /** + * @return A list of parameters mapped to a *arg. + */ + @NotNull List getTupleMappedParams(); + + /** + * @return First **arg, or null. + */ + @Nullable + PyStarArgument getKwdArg(); + + /** + * @return A list of parameters mapped to an **arg. + */ + @NotNull List getKwdMappedParams(); + + /** + * @return A list of parameters for which no arguments were found ('missing'). + */ + @NotNull + List getUnmappedParams(); + + + /** + * @return Lists all args with their flags. + * @see ArgFlag + */ + Map> getArgumentFlags(); + + /** + * @return result of a resolveCallee() against the function call to which the paramater list belongs. + */ + @Nullable + PyCallExpression.PyMarkedFunction getMarkedFunction(); + + PyArgumentList getArgumentList(); + } + } diff --git a/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java b/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java index 83eeffb00c46..747e94acffec 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyArgumentListImpl.java @@ -18,9 +18,9 @@ package com.jetbrains.python.psi.impl; import com.intellij.lang.ASTNode; import com.intellij.psi.PsiElement; -import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.TokenSet; +import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PyTokenTypes; @@ -28,7 +28,7 @@ import com.jetbrains.python.psi.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Arrays; +import java.util.*; public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList { public PyArgumentListImpl(ASTNode astNode) { @@ -246,4 +246,326 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList } } + public AnalysisResult analyzeCall() { + AnalysisResultImpl ret = new AnalysisResultImpl(); + PyExpression[] arguments = getArguments(); + // declaration-based checks + // proper arglist is: [positional,...][name=value,...][*tuple,][**dict] + // following the spec: http://docs.python.org/ref/calls.html + PyCallExpression call = getCallExpression(); + if (call != null) { + PyCallExpression.PyMarkedFunction resolved_callee = call.resolveCallee(); + 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 unmatched_args = new LinkedList(); + Collections.addAll(unmatched_args, arguments); + Map param_slots = new HashMap(); + PyParameter kwd_slot = null; // the *tuple. might be just boolean, but this way debugging is easier + PyParameter tuple_slot = null; // the **kwd + PyStarArgument kwd_arg = null; + PyStarArgument tuple_arg = null; + // all slots are initially empty, *x and **x are not among slots + for (PyParameter a_param : params) { + if (a_param.isPositionalContainer()) tuple_slot = a_param; + else if (a_param.isKeywordContainer()) kwd_slot = a_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' + int param_index = 0; + if (implicit_self && (params.length > 0)) { + param_slots.remove(params[0].getName()); // the self param + param_index = 1; + } + boolean seen_tuple_arg = false; + boolean seen_kwd_arg = false; + ListIterator unmatched_arg_iter = unmatched_args.listIterator(); + // check positional args + while (unmatched_arg_iter.hasNext() && (param_index < params.length)) { + PyExpression arg = unmatched_arg_iter.next(); // current arg + PyParameter param = params[param_index]; // its matching param + if ( + arg instanceof PyKeywordArgument || arg instanceof PyStarArgument || + param.isKeywordContainer() || param.isPositionalContainer() + ) { + seen_tuple_arg |= (arg == tuple_arg); + seen_kwd_arg |= (arg == kwd_arg); + unmatched_arg_iter.previous(); // step back + break; + } + param_slots.put(param.getName(), arg); // it cannot yet contain this name unless function definition is broken + ret.my_plain_mapped_params.put(arg, param); + 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 param : params) { + final String param_name = param.getName(); + if ( + (param.getDefaultValue() == null) && // 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 param : params) { + final String param_name = param.getName(); + if ( + (param.getDefaultValue() == null) && // 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 param : params) { + final String param_name = param.getName(); + if ( + (param.getDefaultValue() == null) && // 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, 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); + } + // any args left? + for (PyExpression arg : unmatched_args) { + //getHolder().createErrorAnnotation(arg, "unexpected arg"); + ret.markArgument(arg, ArgFlag.IS_UNMAPPED); + } + // any params still unfilled? + for (final PyParameter 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 param : params) { + PyExpression arg = param_slots.get(param.getName()); + if (arg != null) { + if (arg instanceof PyStarArgument) { + PyStarArgument star_arg = (PyStarArgument)arg; + if (star_arg.isKeyword()) ret.my_kwd_mapped_params.add(param); + else ret.my_tuple_mapped_params.add(param); + } + else ret.my_plain_mapped_params.put(arg, param); + } + } + // copy starred args + ret.my_kwd_arg = kwd_arg; + ret.my_tuple_arg = tuple_arg; + } + } + return ret; + } + + + protected /*static*/ class AnalysisResultImpl implements AnalysisResult { + + protected Map my_plain_mapped_params; + protected PyStarArgument my_tuple_arg; + protected PyStarArgument my_kwd_arg; + protected List my_tuple_mapped_params; + protected List my_kwd_mapped_params; + protected List my_unmapped_params; + protected Map> my_arg_flags; + protected PyCallExpression.PyMarkedFunction my_marked_func; + + public AnalysisResultImpl() { + // full of empty containers + my_plain_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; + } + + /** + * @return A mapping argument->parameter for non-starred arguments (but includes starred parameters). + */ + public @NotNull Map getPlainMappedParams() { + return my_plain_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.PyMarkedFunction getMarkedFunction() { + 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/testData/resolve/LookAheadCapped.py b/python/testData/resolve/LookAheadCapped.py new file mode 100644 index 000000000000..fbb1e28043ed --- /dev/null +++ b/python/testData/resolve/LookAheadCapped.py @@ -0,0 +1,4 @@ +def f(): + def q(): + return target + target = 1 \ No newline at end of file