D-Scanner/src/dscanner/analysis/mismatched_args.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.");
}