286 lines
6.8 KiB
D
286 lines
6.8 KiB
D
module dscanner.analysis.mismatched_args;
|
|
|
|
import dscanner.analysis.base;
|
|
import dscanner.utils : safeAccess;
|
|
import dsymbol.scope_;
|
|
import dsymbol.symbol;
|
|
import dparse.ast;
|
|
import dparse.lexer : tok, Token;
|
|
import dsymbol.builtin.names;
|
|
|
|
/// Checks for mismatched argument and parameter names
|
|
final class MismatchedArgumentCheck : BaseAnalyzer
|
|
{
|
|
mixin AnalyzerInfo!"mismatched_args_check";
|
|
|
|
///
|
|
this(BaseAnalyzerArguments args)
|
|
{
|
|
super(args);
|
|
}
|
|
|
|
override void visit(const FunctionCallExpression fce)
|
|
{
|
|
import std.typecons : scoped;
|
|
import std.algorithm.iteration : each, map;
|
|
import std.array : array;
|
|
|
|
if (fce.arguments is null)
|
|
return;
|
|
auto argVisitor = scoped!ArgVisitor;
|
|
argVisitor.visit(fce.arguments);
|
|
const istring[] args = argVisitor.args;
|
|
|
|
auto identVisitor = scoped!IdentVisitor;
|
|
if (fce.unaryExpression !is null)
|
|
identVisitor.visit(fce.unaryExpression);
|
|
else if (fce.type !is null)
|
|
identVisitor.visit(fce.type);
|
|
|
|
const(DSymbol)*[] symbols = resolveSymbol(sc, identVisitor.names.length > 0
|
|
? identVisitor.names : [CONSTRUCTOR_SYMBOL_NAME]);
|
|
|
|
static struct ErrorMessage
|
|
{
|
|
const(Token)[] range;
|
|
string message;
|
|
}
|
|
|
|
ErrorMessage[] messages;
|
|
bool matched;
|
|
|
|
foreach (sym; symbols)
|
|
{
|
|
// The cast is a hack because .array() confuses the compiler's overload
|
|
// resolution code.
|
|
const(istring)[] params = sym is null ? [] : sym.argNames[].map!(a => cast() a).array();
|
|
const ArgMismatch[] mismatches = compareArgsToParams(params, args);
|
|
if (mismatches.length == 0)
|
|
matched = true;
|
|
else
|
|
{
|
|
foreach (ref const mm; mismatches)
|
|
{
|
|
messages ~= ErrorMessage(argVisitor.tokens[mm.argIndex], createWarningFromMismatch(mm));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!matched)
|
|
foreach (m; messages)
|
|
addErrorMessage(m.range, KEY, m.message);
|
|
}
|
|
|
|
alias visit = ASTVisitor.visit;
|
|
|
|
private:
|
|
|
|
enum string KEY = "dscanner.confusing.argument_parameter_mismatch";
|
|
}
|
|
|
|
final class IdentVisitor : ASTVisitor
|
|
{
|
|
override void visit(const IdentifierOrTemplateInstance ioti)
|
|
{
|
|
import dsymbol.string_interning : internString;
|
|
|
|
if (ioti.identifier != tok!"")
|
|
names ~= internString(ioti.identifier.text);
|
|
else
|
|
names ~= internString(ioti.templateInstance.identifier.text);
|
|
}
|
|
|
|
override void visit(const Arguments)
|
|
{
|
|
}
|
|
|
|
override void visit(const IndexExpression ie)
|
|
{
|
|
if (ie.unaryExpression !is null)
|
|
visit(ie.unaryExpression);
|
|
}
|
|
|
|
alias visit = ASTVisitor.visit;
|
|
|
|
istring[] names;
|
|
}
|
|
|
|
final class ArgVisitor : ASTVisitor
|
|
{
|
|
override void visit(const NamedArgumentList al)
|
|
{
|
|
foreach (a; al.items)
|
|
{
|
|
auto u = cast(UnaryExpression) a.assignExpression;
|
|
size_t prevArgs = args.length;
|
|
if (u !is null && !a.name.text.length)
|
|
visit(u);
|
|
|
|
if (args.length == prevArgs)
|
|
{
|
|
// if we didn't get an identifier in the unary expression,
|
|
// assume it's a good argument
|
|
args ~= istring.init;
|
|
tokens ~= a.tokens;
|
|
}
|
|
}
|
|
}
|
|
|
|
override void visit(const UnaryExpression unary)
|
|
{
|
|
import dsymbol.string_interning : internString;
|
|
|
|
if (auto iot = unary.safeAccess.primaryExpression.identifierOrTemplateInstance.unwrap)
|
|
{
|
|
if (iot.identifier == tok!"")
|
|
return;
|
|
immutable t = iot.identifier;
|
|
tokens ~= [t];
|
|
args ~= internString(t.text);
|
|
}
|
|
}
|
|
|
|
alias visit = ASTVisitor.visit;
|
|
|
|
const(Token[])[] tokens;
|
|
istring[] args;
|
|
}
|
|
|
|
const(DSymbol)*[] resolveSymbol(const Scope* sc, const istring[] symbolChain)
|
|
{
|
|
import std.array : empty;
|
|
|
|
const(DSymbol)*[] matchingSymbols = sc.getSymbolsByName(symbolChain[0]);
|
|
if (matchingSymbols.empty)
|
|
return null;
|
|
|
|
foreach (ref symbol; matchingSymbols)
|
|
{
|
|
inner: foreach (i; 1 .. symbolChain.length)
|
|
{
|
|
if (symbol.kind == CompletionKind.variableName
|
|
|| symbol.kind == CompletionKind.memberVariableName
|
|
|| symbol.kind == CompletionKind.functionName
|
|
|| symbol.kind == CompletionKind.aliasName)
|
|
symbol = symbol.type;
|
|
if (symbol is null)
|
|
{
|
|
symbol = null;
|
|
break inner;
|
|
}
|
|
auto p = symbol.getPartsByName(symbolChain[i]);
|
|
if (p.empty)
|
|
{
|
|
symbol = null;
|
|
break inner;
|
|
}
|
|
symbol = p[0];
|
|
}
|
|
}
|
|
return matchingSymbols;
|
|
}
|
|
|
|
struct ArgMismatch
|
|
{
|
|
size_t argIndex;
|
|
size_t paramIndex;
|
|
string name;
|
|
}
|
|
|
|
immutable(ArgMismatch[]) compareArgsToParams(const istring[] params, const istring[] args) pure
|
|
{
|
|
import std.exception : assumeUnique;
|
|
|
|
if (args.length != params.length)
|
|
return [];
|
|
ArgMismatch[] retVal;
|
|
foreach (i, arg; args)
|
|
{
|
|
if (arg is null || arg == params[i])
|
|
continue;
|
|
foreach (j, param; params)
|
|
if (param == arg)
|
|
retVal ~= ArgMismatch(i, j, arg);
|
|
}
|
|
return assumeUnique(retVal);
|
|
}
|
|
|
|
string createWarningFromMismatch(const ArgMismatch mismatch) pure
|
|
{
|
|
import std.format : format;
|
|
|
|
return "Argument %d is named '%s', but this is the name of parameter %d".format(
|
|
mismatch.argIndex + 1, mismatch.name, mismatch.paramIndex + 1);
|
|
}
|
|
|
|
unittest
|
|
{
|
|
import dsymbol.string_interning : internString;
|
|
import std.algorithm.iteration : map;
|
|
import std.array : array;
|
|
import std.conv : to;
|
|
|
|
{
|
|
istring[] args = ["a", "b", "c"].map!internString().array();
|
|
istring[] params = ["a", "b", "c"].map!internString().array();
|
|
immutable res = compareArgsToParams(params, args);
|
|
assert(res == []);
|
|
}
|
|
|
|
{
|
|
istring[] args = ["a", "c", "b"].map!internString().array();
|
|
istring[] params = ["a", "b", "c"].map!internString().array();
|
|
immutable res = compareArgsToParams(params, args);
|
|
assert(res == [ArgMismatch(1, 2, "c"), ArgMismatch(2, 1, "b")], to!string(res));
|
|
}
|
|
|
|
{
|
|
istring[] args = ["a", "c", "b"].map!internString().array();
|
|
istring[] params = ["alpha", "bravo", "c"].map!internString().array();
|
|
immutable res = compareArgsToParams(params, args);
|
|
assert(res == [ArgMismatch(1, 2, "c")]);
|
|
}
|
|
|
|
{
|
|
istring[] args = ["a", "b"].map!internString().array();
|
|
istring[] params = [null, "b"].map!internString().array();
|
|
immutable res = compareArgsToParams(params, args);
|
|
assert(res == []);
|
|
}
|
|
}
|
|
|
|
unittest
|
|
{
|
|
import dscanner.analysis.helpers : assertAnalyzerWarnings;
|
|
import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
|
|
import std.stdio : stderr;
|
|
|
|
StaticAnalysisConfig sac = disabledConfig();
|
|
sac.mismatched_args_check = Check.enabled;
|
|
assertAnalyzerWarnings(q{
|
|
void foo(int x, int y)
|
|
{
|
|
}
|
|
|
|
void bar()
|
|
{
|
|
int x = 1;
|
|
int y = 2;
|
|
foo(y, x); /+
|
|
^ [warn]: Argument 2 is named 'x', but this is the name of parameter 1 +/
|
|
foo(y + 1, x); /+
|
|
^ [warn]: Argument 2 is named 'x', but this is the name of parameter 1 +/
|
|
foo(y + 1, f(x));
|
|
foo(x: y, y: x);
|
|
foo(y, 0); /+
|
|
^ [warn]: Argument 1 is named 'y', but this is the name of parameter 2 +/
|
|
|
|
// foo(y: y, x); // TODO: this shouldn't error
|
|
foo(x, y: x); // TODO: this should error
|
|
foo(y, y: x); /+
|
|
^ [warn]: Argument 1 is named 'y', but this is the name of parameter 2 +/
|
|
}
|
|
}c, sac);
|
|
stderr.writeln("Unittest for MismatchedArgumentCheck passed.");
|
|
}
|