1 module dscanner.analysis.mismatched_args;
2 
3 import dscanner.analysis.base : BaseAnalyzer;
4 import dscanner.utils : safeAccess;
5 import dsymbol.scope_;
6 import dsymbol.symbol;
7 import dparse.ast;
8 import dparse.lexer : tok;
9 import dsymbol.builtin.names;
10 
11 /// Checks for mismatched argument and parameter names
12 final class MismatchedArgumentCheck : BaseAnalyzer
13 {
14 	///
15 	this(string fileName, const(Scope)* sc, bool skipTests = false)
16 	{
17 		super(fileName, sc, skipTests);
18 	}
19 
20 	override void visit(const FunctionCallExpression fce)
21 	{
22 		import std.typecons : scoped;
23 		import std.algorithm.iteration : each, map;
24 		import std.array : array;
25 
26 		if (fce.arguments is null)
27 			return;
28 		auto argVisitor = scoped!ArgVisitor;
29 		argVisitor.visit(fce.arguments);
30 		const istring[] args = argVisitor.args;
31 
32 		auto identVisitor = scoped!IdentVisitor;
33 		if (fce.unaryExpression !is null)
34 			identVisitor.visit(fce.unaryExpression);
35 		else if (fce.type !is null)
36 			identVisitor.visit(fce.type);
37 
38 		const(DSymbol)*[] symbols = resolveSymbol(sc, identVisitor.names.length > 0
39 				? identVisitor.names : [CONSTRUCTOR_SYMBOL_NAME]);
40 
41 		static struct ErrorMessage
42 		{
43 			size_t line;
44 			size_t column;
45 			string message;
46 		}
47 
48 		ErrorMessage[] messages;
49 		bool matched;
50 
51 		foreach (sym; symbols)
52 		{
53 			// The cast is a hack because .array() confuses the compiler's overload
54 			// resolution code.
55 			const(istring)[] params = sym is null ? [] : sym.argNames[].map!(a => cast() a).array();
56 			const ArgMismatch[] mismatches = compareArgsToParams(params, args);
57 			if (mismatches.length == 0)
58 				matched = true;
59 			else
60 			{
61 				foreach (size_t i, ref const mm; mismatches)
62 				{
63 					messages ~= ErrorMessage(argVisitor.lines[i],
64 							argVisitor.columns[i], createWarningFromMismatch(mm));
65 				}
66 			}
67 		}
68 
69 		if (!matched)
70 			foreach (m; messages)
71 				addErrorMessage(m.line, m.column, KEY, m.message);
72 	}
73 
74 	alias visit = ASTVisitor.visit;
75 
76 private:
77 
78 	enum string KEY = "dscanner.confusing.argument_parameter_mismatch";
79 }
80 
81 final class IdentVisitor : ASTVisitor
82 {
83 	override void visit(const IdentifierOrTemplateInstance ioti)
84 	{
85 		import dsymbol.string_interning : internString;
86 
87 		if (ioti.identifier != tok!"")
88 			names ~= internString(ioti.identifier.text);
89 		else
90 			names ~= internString(ioti.templateInstance.identifier.text);
91 	}
92 
93 	override void visit(const Arguments)
94 	{
95 	}
96 
97 	override void visit(const IndexExpression ie)
98 	{
99 		if (ie.unaryExpression !is null)
100 			visit(ie.unaryExpression);
101 	}
102 
103 	alias visit = ASTVisitor.visit;
104 
105 	istring[] names;
106 }
107 
108 final class ArgVisitor : ASTVisitor
109 {
110 	override void visit(const ArgumentList al)
111 	{
112 		foreach (a; al.items)
113 		{
114 			auto u = cast(UnaryExpression) a;
115 			if (u !is null)
116 				visit(u);
117 			else
118 			{
119 				args ~= istring.init;
120 				lines ~= size_t.max;
121 				columns ~= size_t.max;
122 			}
123 		}
124 	}
125 
126 	override void visit(const UnaryExpression unary)
127 	{
128 		import dsymbol.string_interning : internString;
129 
130 		if (auto iot = unary.safeAccess.primaryExpression.identifierOrTemplateInstance.unwrap)
131 		{
132 			if (iot.identifier == tok!"")
133 				return;
134 			immutable t = iot.identifier;
135 			lines ~= t.line;
136 			columns ~= t.column;
137 			args ~= internString(t.text);
138 		}
139 	}
140 
141 	alias visit = ASTVisitor.visit;
142 
143 	size_t[] lines;
144 	size_t[] columns;
145 	istring[] args;
146 }
147 
148 const(DSymbol)*[] resolveSymbol(const Scope* sc, const istring[] symbolChain)
149 {
150 	import std.array : empty;
151 
152 	const(DSymbol)*[] matchingSymbols = sc.getSymbolsByName(symbolChain[0]);
153 	if (matchingSymbols.empty)
154 		return null;
155 
156 	foreach (ref symbol; matchingSymbols)
157 	{
158 		inner: foreach (i; 1 .. symbolChain.length)
159 		{
160 			if (symbol.kind == CompletionKind.variableName
161 					|| symbol.kind == CompletionKind.memberVariableName
162 					|| symbol.kind == CompletionKind.functionName)
163 				symbol = symbol.type;
164 			if (symbol is null)
165 			{
166 				symbol = null;
167 				break inner;
168 			}
169 			auto p = symbol.getPartsByName(symbolChain[i]);
170 			if (p.empty)
171 			{
172 				symbol = null;
173 				break inner;
174 			}
175 			symbol = p[0];
176 		}
177 	}
178 	return matchingSymbols;
179 }
180 
181 struct ArgMismatch
182 {
183 	size_t argIndex;
184 	size_t paramIndex;
185 	string name;
186 }
187 
188 immutable(ArgMismatch[]) compareArgsToParams(const istring[] params, const istring[] args) pure
189 {
190 	import std.exception : assumeUnique;
191 
192 	if (args.length != params.length)
193 		return [];
194 	ArgMismatch[] retVal;
195 	foreach (i, arg; args)
196 	{
197 		if (arg is null || arg == params[i])
198 			continue;
199 		foreach (j, param; params)
200 			if (param == arg)
201 				retVal ~= ArgMismatch(i, j, arg);
202 	}
203 	return assumeUnique(retVal);
204 }
205 
206 string createWarningFromMismatch(const ArgMismatch mismatch) pure
207 {
208 	import std.format : format;
209 
210 	return "Argument %d is named '%s', but this is the name of parameter %d".format(
211 			mismatch.argIndex + 1, mismatch.name, mismatch.paramIndex + 1);
212 }
213 
214 unittest
215 {
216 	import dsymbol.string_interning : internString;
217 	import std.algorithm.iteration : map;
218 	import std.array : array;
219 	import std.conv : to;
220 
221 	{
222 		istring[] args = ["a", "b", "c"].map!internString().array();
223 		istring[] params = ["a", "b", "c"].map!internString().array();
224 		immutable res = compareArgsToParams(params, args);
225 		assert(res == []);
226 	}
227 
228 	{
229 		istring[] args = ["a", "c", "b"].map!internString().array();
230 		istring[] params = ["a", "b", "c"].map!internString().array();
231 		immutable res = compareArgsToParams(params, args);
232 		assert(res == [ArgMismatch(1, 2, "c"), ArgMismatch(2, 1, "b")], to!string(res));
233 	}
234 
235 	{
236 		istring[] args = ["a", "c", "b"].map!internString().array();
237 		istring[] params = ["alpha", "bravo", "c"].map!internString().array();
238 		immutable res = compareArgsToParams(params, args);
239 		assert(res == [ArgMismatch(1, 2, "c")]);
240 	}
241 
242 	{
243 		istring[] args = ["a", "b"].map!internString().array();
244 		istring[] params = [null, "b"].map!internString().array();
245 		immutable res = compareArgsToParams(params, args);
246 		assert(res == []);
247 	}
248 }