1 | //===--- SuspiciousCallArgumentCheck.cpp - clang-tidy ---------------------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | |
9 | #include "SuspiciousCallArgumentCheck.h" |
10 | #include "../utils/OptionsUtils.h" |
11 | #include "clang/AST/ASTContext.h" |
12 | #include "clang/AST/Type.h" |
13 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
14 | #include <optional> |
15 | #include <sstream> |
16 | |
17 | using namespace clang::ast_matchers; |
18 | namespace optutils = clang::tidy::utils::options; |
19 | |
20 | namespace clang::tidy::readability { |
21 | |
22 | namespace { |
23 | struct DefaultHeuristicConfiguration { |
24 | /// Whether the heuristic is to be enabled by default. |
25 | const bool Enabled; |
26 | |
27 | /// The upper bound of % of similarity the two strings might have to be |
28 | /// considered dissimilar. |
29 | /// (For purposes of configuration, -1 if the heuristic is not configurable |
30 | /// with bounds.) |
31 | const int8_t DissimilarBelow; |
32 | |
33 | /// The lower bound of % of similarity the two string must have to be |
34 | /// considered similar. |
35 | /// (For purposes of configuration, -1 if the heuristic is not configurable |
36 | /// with bounds.) |
37 | const int8_t SimilarAbove; |
38 | |
39 | /// Can the heuristic be configured with bounds? |
40 | bool hasBounds() const { return DissimilarBelow > -1 && SimilarAbove > -1; } |
41 | }; |
42 | } // namespace |
43 | |
44 | static constexpr std::size_t DefaultMinimumIdentifierNameLength = 3; |
45 | |
46 | static constexpr StringRef HeuristicToString[] = { |
47 | "Equality" , "Abbreviation" , "Prefix" , "Suffix" , |
48 | "Substring" , "Levenshtein" , "JaroWinkler" , "Dice" }; |
49 | |
50 | static constexpr DefaultHeuristicConfiguration Defaults[] = { |
51 | {.Enabled: true, .DissimilarBelow: -1, .SimilarAbove: -1}, // Equality. |
52 | {.Enabled: true, .DissimilarBelow: -1, .SimilarAbove: -1}, // Abbreviation. |
53 | {.Enabled: true, .DissimilarBelow: 25, .SimilarAbove: 30}, // Prefix. |
54 | {.Enabled: true, .DissimilarBelow: 25, .SimilarAbove: 30}, // Suffix. |
55 | {.Enabled: true, .DissimilarBelow: 40, .SimilarAbove: 50}, // Substring. |
56 | {.Enabled: true, .DissimilarBelow: 50, .SimilarAbove: 66}, // Levenshtein. |
57 | {.Enabled: true, .DissimilarBelow: 75, .SimilarAbove: 85}, // Jaro-Winkler. |
58 | {.Enabled: true, .DissimilarBelow: 60, .SimilarAbove: 70}, // Dice. |
59 | }; |
60 | |
61 | static_assert( |
62 | sizeof(HeuristicToString) / sizeof(HeuristicToString[0]) == |
63 | SuspiciousCallArgumentCheck::HeuristicCount, |
64 | "Ensure that every heuristic has a corresponding stringified name" ); |
65 | static_assert(sizeof(Defaults) / sizeof(Defaults[0]) == |
66 | SuspiciousCallArgumentCheck::HeuristicCount, |
67 | "Ensure that every heuristic has a default configuration." ); |
68 | |
69 | namespace { |
70 | template <std::size_t I> struct HasWellConfiguredBounds { |
71 | static constexpr bool Value = |
72 | !((Defaults[I].DissimilarBelow == -1) ^ (Defaults[I].SimilarAbove == -1)); |
73 | static_assert(Value, "A heuristic must either have a dissimilarity and " |
74 | "similarity bound, or neither!" ); |
75 | }; |
76 | |
77 | template <std::size_t I> struct HasWellConfiguredBoundsFold { |
78 | static constexpr bool Value = HasWellConfiguredBounds<I>::Value && |
79 | HasWellConfiguredBoundsFold<I - 1>::Value; |
80 | }; |
81 | |
82 | template <> struct HasWellConfiguredBoundsFold<0> { |
83 | static constexpr bool Value = HasWellConfiguredBounds<0>::Value; |
84 | }; |
85 | |
86 | struct AllHeuristicsBoundsWellConfigured { |
87 | static constexpr bool Value = |
88 | HasWellConfiguredBoundsFold<SuspiciousCallArgumentCheck::HeuristicCount - |
89 | 1>::Value; |
90 | }; |
91 | |
92 | static_assert(AllHeuristicsBoundsWellConfigured::Value); |
93 | } // namespace |
94 | |
95 | static constexpr llvm::StringLiteral DefaultAbbreviations = "addr=address;" |
96 | "arr=array;" |
97 | "attr=attribute;" |
98 | "buf=buffer;" |
99 | "cl=client;" |
100 | "cnt=count;" |
101 | "col=column;" |
102 | "cpy=copy;" |
103 | "dest=destination;" |
104 | "dist=distance" |
105 | "dst=distance;" |
106 | "elem=element;" |
107 | "hght=height;" |
108 | "i=index;" |
109 | "idx=index;" |
110 | "len=length;" |
111 | "ln=line;" |
112 | "lst=list;" |
113 | "nr=number;" |
114 | "num=number;" |
115 | "pos=position;" |
116 | "ptr=pointer;" |
117 | "ref=reference;" |
118 | "src=source;" |
119 | "srv=server;" |
120 | "stmt=statement;" |
121 | "str=string;" |
122 | "val=value;" |
123 | "var=variable;" |
124 | "vec=vector;" |
125 | "wdth=width" ; |
126 | |
127 | static constexpr std::size_t SmallVectorSize = |
128 | SuspiciousCallArgumentCheck::SmallVectorSize; |
129 | |
130 | /// Returns how many % X is of Y. |
131 | static inline double percentage(double X, double Y) { return X / Y * 100.0; } |
132 | |
133 | static bool applyEqualityHeuristic(StringRef Arg, StringRef Param) { |
134 | return Arg.equals_insensitive(RHS: Param); |
135 | } |
136 | |
137 | static bool applyAbbreviationHeuristic( |
138 | const llvm::StringMap<std::string> &AbbreviationDictionary, StringRef Arg, |
139 | StringRef Param) { |
140 | if (AbbreviationDictionary.contains(Key: Arg) && |
141 | Param.equals(RHS: AbbreviationDictionary.lookup(Key: Arg))) |
142 | return true; |
143 | |
144 | if (AbbreviationDictionary.contains(Key: Param) && |
145 | Arg.equals(RHS: AbbreviationDictionary.lookup(Key: Param))) |
146 | return true; |
147 | |
148 | return false; |
149 | } |
150 | |
151 | /// Check whether the shorter String is a prefix of the longer String. |
152 | static bool applyPrefixHeuristic(StringRef Arg, StringRef Param, |
153 | int8_t Threshold) { |
154 | StringRef Shorter = Arg.size() < Param.size() ? Arg : Param; |
155 | StringRef Longer = Arg.size() >= Param.size() ? Arg : Param; |
156 | |
157 | if (Longer.starts_with_insensitive(Prefix: Shorter)) |
158 | return percentage(X: Shorter.size(), Y: Longer.size()) > Threshold; |
159 | |
160 | return false; |
161 | } |
162 | |
163 | /// Check whether the shorter String is a suffix of the longer String. |
164 | static bool applySuffixHeuristic(StringRef Arg, StringRef Param, |
165 | int8_t Threshold) { |
166 | StringRef Shorter = Arg.size() < Param.size() ? Arg : Param; |
167 | StringRef Longer = Arg.size() >= Param.size() ? Arg : Param; |
168 | |
169 | if (Longer.ends_with_insensitive(Suffix: Shorter)) |
170 | return percentage(X: Shorter.size(), Y: Longer.size()) > Threshold; |
171 | |
172 | return false; |
173 | } |
174 | |
175 | static bool applySubstringHeuristic(StringRef Arg, StringRef Param, |
176 | int8_t Threshold) { |
177 | |
178 | std::size_t MaxLength = 0; |
179 | SmallVector<std::size_t, SmallVectorSize> Current(Param.size()); |
180 | SmallVector<std::size_t, SmallVectorSize> Previous(Param.size()); |
181 | std::string ArgLower = Arg.lower(); |
182 | std::string ParamLower = Param.lower(); |
183 | |
184 | for (std::size_t I = 0; I < Arg.size(); ++I) { |
185 | for (std::size_t J = 0; J < Param.size(); ++J) { |
186 | if (ArgLower[I] == ParamLower[J]) { |
187 | if (I == 0 || J == 0) |
188 | Current[J] = 1; |
189 | else |
190 | Current[J] = 1 + Previous[J - 1]; |
191 | |
192 | MaxLength = std::max(a: MaxLength, b: Current[J]); |
193 | } else |
194 | Current[J] = 0; |
195 | } |
196 | |
197 | Current.swap(RHS&: Previous); |
198 | } |
199 | |
200 | size_t LongerLength = std::max(a: Arg.size(), b: Param.size()); |
201 | return percentage(X: MaxLength, Y: LongerLength) > Threshold; |
202 | } |
203 | |
204 | static bool applyLevenshteinHeuristic(StringRef Arg, StringRef Param, |
205 | int8_t Threshold) { |
206 | std::size_t LongerLength = std::max(a: Arg.size(), b: Param.size()); |
207 | double Dist = Arg.edit_distance(Other: Param); |
208 | Dist = (1.0 - Dist / LongerLength) * 100.0; |
209 | return Dist > Threshold; |
210 | } |
211 | |
212 | // Based on http://en.wikipedia.org/wiki/Jaro–Winkler_distance. |
213 | static bool applyJaroWinklerHeuristic(StringRef Arg, StringRef Param, |
214 | int8_t Threshold) { |
215 | std::size_t Match = 0, Transpos = 0; |
216 | std::ptrdiff_t ArgLen = Arg.size(); |
217 | std::ptrdiff_t ParamLen = Param.size(); |
218 | SmallVector<int, SmallVectorSize> ArgFlags(ArgLen); |
219 | SmallVector<int, SmallVectorSize> ParamFlags(ParamLen); |
220 | std::ptrdiff_t Range = |
221 | std::max(a: std::ptrdiff_t{0}, b: std::max(a: ArgLen, b: ParamLen) / 2 - 1); |
222 | |
223 | // Calculate matching characters. |
224 | for (std::ptrdiff_t I = 0; I < ParamLen; ++I) |
225 | for (std::ptrdiff_t J = std::max(a: I - Range, b: std::ptrdiff_t{0}), |
226 | L = std::min(a: I + Range + 1, b: ArgLen); |
227 | J < L; ++J) |
228 | if (tolower(c: Param[I]) == tolower(c: Arg[J]) && !ArgFlags[J]) { |
229 | ArgFlags[J] = 1; |
230 | ParamFlags[I] = 1; |
231 | ++Match; |
232 | break; |
233 | } |
234 | |
235 | if (!Match) |
236 | return false; |
237 | |
238 | // Calculate character transpositions. |
239 | std::ptrdiff_t L = 0; |
240 | for (std::ptrdiff_t I = 0; I < ParamLen; ++I) { |
241 | if (ParamFlags[I] == 1) { |
242 | std::ptrdiff_t J = 0; |
243 | for (J = L; J < ArgLen; ++J) |
244 | if (ArgFlags[J] == 1) { |
245 | L = J + 1; |
246 | break; |
247 | } |
248 | |
249 | if (tolower(c: Param[I]) != tolower(c: Arg[J])) |
250 | ++Transpos; |
251 | } |
252 | } |
253 | Transpos /= 2; |
254 | |
255 | // Jaro distance. |
256 | double MatchD = Match; |
257 | double Dist = ((MatchD / ArgLen) + (MatchD / ParamLen) + |
258 | ((MatchD - Transpos) / Match)) / |
259 | 3.0; |
260 | |
261 | // Calculate common string prefix up to 4 chars. |
262 | L = 0; |
263 | for (std::ptrdiff_t I = 0; |
264 | I < std::min(a: std::min(a: ArgLen, b: ParamLen), b: std::ptrdiff_t{4}); ++I) |
265 | if (tolower(c: Arg[I]) == tolower(c: Param[I])) |
266 | ++L; |
267 | |
268 | // Jaro-Winkler distance. |
269 | Dist = (Dist + (L * 0.1 * (1.0 - Dist))) * 100.0; |
270 | return Dist > Threshold; |
271 | } |
272 | |
273 | // Based on http://en.wikipedia.org/wiki/Sørensen–Dice_coefficient |
274 | static bool applyDiceHeuristic(StringRef Arg, StringRef Param, |
275 | int8_t Threshold) { |
276 | llvm::StringSet<> ArgBigrams; |
277 | llvm::StringSet<> ParamBigrams; |
278 | |
279 | // Extract character bigrams from Arg. |
280 | for (std::ptrdiff_t I = 0; I < static_cast<std::ptrdiff_t>(Arg.size()) - 1; |
281 | ++I) |
282 | ArgBigrams.insert(key: Arg.substr(Start: I, N: 2).lower()); |
283 | |
284 | // Extract character bigrams from Param. |
285 | for (std::ptrdiff_t I = 0; I < static_cast<std::ptrdiff_t>(Param.size()) - 1; |
286 | ++I) |
287 | ParamBigrams.insert(key: Param.substr(Start: I, N: 2).lower()); |
288 | |
289 | std::size_t Intersection = 0; |
290 | |
291 | // Find the intersection between the two sets. |
292 | for (auto IT = ParamBigrams.begin(); IT != ParamBigrams.end(); ++IT) |
293 | Intersection += ArgBigrams.count(Key: (IT->getKey())); |
294 | |
295 | // Calculate Dice coefficient. |
296 | return percentage(X: Intersection * 2.0, |
297 | Y: ArgBigrams.size() + ParamBigrams.size()) > Threshold; |
298 | } |
299 | |
300 | /// Checks if ArgType binds to ParamType regarding reference-ness and |
301 | /// cv-qualifiers. |
302 | static bool areRefAndQualCompatible(QualType ArgType, QualType ParamType) { |
303 | return !ParamType->isReferenceType() || |
304 | ParamType.getNonReferenceType().isAtLeastAsQualifiedAs( |
305 | other: ArgType.getNonReferenceType()); |
306 | } |
307 | |
308 | static bool isPointerOrArray(QualType TypeToCheck) { |
309 | return TypeToCheck->isPointerType() || TypeToCheck->isArrayType(); |
310 | } |
311 | |
312 | /// Checks whether ArgType is an array type identical to ParamType's array type. |
313 | /// Enforces array elements' qualifier compatibility as well. |
314 | static bool isCompatibleWithArrayReference(QualType ArgType, |
315 | QualType ParamType) { |
316 | if (!ArgType->isArrayType()) |
317 | return false; |
318 | // Here, qualifiers belong to the elements of the arrays. |
319 | if (!ParamType.isAtLeastAsQualifiedAs(other: ArgType)) |
320 | return false; |
321 | |
322 | return ParamType.getUnqualifiedType() == ArgType.getUnqualifiedType(); |
323 | } |
324 | |
325 | static QualType convertToPointeeOrArrayElementQualType(QualType TypeToConvert) { |
326 | unsigned CVRqualifiers = 0; |
327 | // Save array element qualifiers, since getElementType() removes qualifiers |
328 | // from array elements. |
329 | if (TypeToConvert->isArrayType()) |
330 | CVRqualifiers = TypeToConvert.getLocalQualifiers().getCVRQualifiers(); |
331 | TypeToConvert = TypeToConvert->isPointerType() |
332 | ? TypeToConvert->getPointeeType() |
333 | : TypeToConvert->getAsArrayTypeUnsafe()->getElementType(); |
334 | TypeToConvert = TypeToConvert.withCVRQualifiers(CVR: CVRqualifiers); |
335 | return TypeToConvert; |
336 | } |
337 | |
338 | /// Checks if multilevel pointers' qualifiers compatibility continues on the |
339 | /// current pointer level. For multilevel pointers, C++ permits conversion, if |
340 | /// every cv-qualifier in ArgType also appears in the corresponding position in |
341 | /// ParamType, and if PramType has a cv-qualifier that's not in ArgType, then |
342 | /// every * in ParamType to the right of that cv-qualifier, except the last |
343 | /// one, must also be const-qualified. |
344 | static bool (QualType ArgType, QualType ParamType, |
345 | bool &IsParamContinuouslyConst) { |
346 | // The types are compatible, if the parameter is at least as qualified as the |
347 | // argument, and if it is more qualified, it has to be const on upper pointer |
348 | // levels. |
349 | bool AreTypesQualCompatible = |
350 | ParamType.isAtLeastAsQualifiedAs(other: ArgType) && |
351 | (!ParamType.hasQualifiers() || IsParamContinuouslyConst); |
352 | // Check whether the parameter's constness continues at the current pointer |
353 | // level. |
354 | IsParamContinuouslyConst &= ParamType.isConstQualified(); |
355 | |
356 | return AreTypesQualCompatible; |
357 | } |
358 | |
359 | /// Checks whether multilevel pointers are compatible in terms of levels, |
360 | /// qualifiers and pointee type. |
361 | static bool arePointerTypesCompatible(QualType ArgType, QualType ParamType, |
362 | bool IsParamContinuouslyConst) { |
363 | if (!arePointersStillQualCompatible(ArgType, ParamType, |
364 | IsParamContinuouslyConst)) |
365 | return false; |
366 | |
367 | do { |
368 | // Step down one pointer level. |
369 | ArgType = convertToPointeeOrArrayElementQualType(TypeToConvert: ArgType); |
370 | ParamType = convertToPointeeOrArrayElementQualType(TypeToConvert: ParamType); |
371 | |
372 | // Check whether cv-qualifiers permit compatibility on |
373 | // current level. |
374 | if (!arePointersStillQualCompatible(ArgType, ParamType, |
375 | IsParamContinuouslyConst)) |
376 | return false; |
377 | |
378 | if (ParamType.getUnqualifiedType() == ArgType.getUnqualifiedType()) |
379 | return true; |
380 | |
381 | } while (ParamType->isPointerType() && ArgType->isPointerType()); |
382 | // The final type does not match, or pointer levels differ. |
383 | return false; |
384 | } |
385 | |
386 | /// Checks whether ArgType converts implicitly to ParamType. |
387 | static bool areTypesCompatible(QualType ArgType, QualType ParamType, |
388 | const ASTContext &Ctx) { |
389 | if (ArgType.isNull() || ParamType.isNull()) |
390 | return false; |
391 | |
392 | ArgType = ArgType.getCanonicalType(); |
393 | ParamType = ParamType.getCanonicalType(); |
394 | |
395 | if (ArgType == ParamType) |
396 | return true; |
397 | |
398 | // Check for constness and reference compatibility. |
399 | if (!areRefAndQualCompatible(ArgType, ParamType)) |
400 | return false; |
401 | |
402 | bool IsParamReference = ParamType->isReferenceType(); |
403 | |
404 | // Reference-ness has already been checked and should be removed |
405 | // before further checking. |
406 | ArgType = ArgType.getNonReferenceType(); |
407 | ParamType = ParamType.getNonReferenceType(); |
408 | |
409 | if (ParamType.getUnqualifiedType() == ArgType.getUnqualifiedType()) |
410 | return true; |
411 | |
412 | // Arithmetic types are interconvertible, except scoped enums. |
413 | if (ParamType->isArithmeticType() && ArgType->isArithmeticType()) { |
414 | if ((ParamType->isEnumeralType() && |
415 | ParamType->castAs<EnumType>()->getDecl()->isScoped()) || |
416 | (ArgType->isEnumeralType() && |
417 | ArgType->castAs<EnumType>()->getDecl()->isScoped())) |
418 | return false; |
419 | |
420 | return true; |
421 | } |
422 | |
423 | // Check if the argument and the param are both function types (the parameter |
424 | // decayed to a function pointer). |
425 | if (ArgType->isFunctionType() && ParamType->isFunctionPointerType()) { |
426 | ParamType = ParamType->getPointeeType(); |
427 | return ArgType == ParamType; |
428 | } |
429 | |
430 | // Arrays or pointer arguments convert to array or pointer parameters. |
431 | if (!(isPointerOrArray(TypeToCheck: ArgType) && isPointerOrArray(TypeToCheck: ParamType))) |
432 | return false; |
433 | |
434 | // When ParamType is an array reference, ArgType has to be of the same-sized |
435 | // array-type with cv-compatible element type. |
436 | if (IsParamReference && ParamType->isArrayType()) |
437 | return isCompatibleWithArrayReference(ArgType, ParamType); |
438 | |
439 | bool IsParamContinuouslyConst = |
440 | !IsParamReference || ParamType.getNonReferenceType().isConstQualified(); |
441 | |
442 | // Remove the first level of indirection. |
443 | ArgType = convertToPointeeOrArrayElementQualType(TypeToConvert: ArgType); |
444 | ParamType = convertToPointeeOrArrayElementQualType(TypeToConvert: ParamType); |
445 | |
446 | // Check qualifier compatibility on the next level. |
447 | if (!ParamType.isAtLeastAsQualifiedAs(other: ArgType)) |
448 | return false; |
449 | |
450 | if (ParamType.getUnqualifiedType() == ArgType.getUnqualifiedType()) |
451 | return true; |
452 | |
453 | // At this point, all possible C language implicit conversion were checked. |
454 | if (!Ctx.getLangOpts().CPlusPlus) |
455 | return false; |
456 | |
457 | // Check whether ParamType and ArgType were both pointers to a class or a |
458 | // struct, and check for inheritance. |
459 | if (ParamType->isStructureOrClassType() && |
460 | ArgType->isStructureOrClassType()) { |
461 | const auto *ArgDecl = ArgType->getAsCXXRecordDecl(); |
462 | const auto *ParamDecl = ParamType->getAsCXXRecordDecl(); |
463 | if (!ArgDecl || !ArgDecl->hasDefinition() || !ParamDecl || |
464 | !ParamDecl->hasDefinition()) |
465 | return false; |
466 | |
467 | return ArgDecl->isDerivedFrom(Base: ParamDecl); |
468 | } |
469 | |
470 | // Unless argument and param are both multilevel pointers, the types are not |
471 | // convertible. |
472 | if (!(ParamType->isAnyPointerType() && ArgType->isAnyPointerType())) |
473 | return false; |
474 | |
475 | return arePointerTypesCompatible(ArgType, ParamType, |
476 | IsParamContinuouslyConst); |
477 | } |
478 | |
479 | static bool isOverloadedUnaryOrBinarySymbolOperator(const FunctionDecl *FD) { |
480 | switch (FD->getOverloadedOperator()) { |
481 | case OO_None: |
482 | case OO_Call: |
483 | case OO_Subscript: |
484 | case OO_New: |
485 | case OO_Delete: |
486 | case OO_Array_New: |
487 | case OO_Array_Delete: |
488 | case OO_Conditional: |
489 | case OO_Coawait: |
490 | return false; |
491 | |
492 | default: |
493 | return FD->getNumParams() <= 2; |
494 | } |
495 | } |
496 | |
497 | SuspiciousCallArgumentCheck::SuspiciousCallArgumentCheck( |
498 | StringRef Name, ClangTidyContext *Context) |
499 | : ClangTidyCheck(Name, Context), |
500 | MinimumIdentifierNameLength(Options.get( |
501 | LocalName: "MinimumIdentifierNameLength" , Default: DefaultMinimumIdentifierNameLength)) { |
502 | auto GetToggleOpt = [this](Heuristic H) -> bool { |
503 | auto Idx = static_cast<std::size_t>(H); |
504 | assert(Idx < HeuristicCount); |
505 | return Options.get(LocalName: HeuristicToString[Idx], Default: Defaults[Idx].Enabled); |
506 | }; |
507 | auto GetBoundOpt = [this](Heuristic H, BoundKind BK) -> int8_t { |
508 | auto Idx = static_cast<std::size_t>(H); |
509 | assert(Idx < HeuristicCount); |
510 | |
511 | SmallString<32> Key = HeuristicToString[Idx]; |
512 | Key.append(RHS: BK == BoundKind::DissimilarBelow ? "DissimilarBelow" |
513 | : "SimilarAbove" ); |
514 | int8_t Default = BK == BoundKind::DissimilarBelow |
515 | ? Defaults[Idx].DissimilarBelow |
516 | : Defaults[Idx].SimilarAbove; |
517 | return Options.get(LocalName: Key, Default); |
518 | }; |
519 | for (std::size_t Idx = 0; Idx < HeuristicCount; ++Idx) { |
520 | auto H = static_cast<Heuristic>(Idx); |
521 | if (GetToggleOpt(H)) |
522 | AppliedHeuristics.emplace_back(Args&: H); |
523 | ConfiguredBounds.emplace_back( |
524 | Args: std::make_pair(x: GetBoundOpt(H, BoundKind::DissimilarBelow), |
525 | y: GetBoundOpt(H, BoundKind::SimilarAbove))); |
526 | } |
527 | |
528 | for (StringRef Abbreviation : optutils::parseStringList( |
529 | Option: Options.get(LocalName: "Abbreviations" , Default: DefaultAbbreviations))) { |
530 | auto KeyAndValue = Abbreviation.split(Separator: "=" ); |
531 | assert(!KeyAndValue.first.empty() && !KeyAndValue.second.empty()); |
532 | AbbreviationDictionary.insert( |
533 | KV: std::make_pair(x&: KeyAndValue.first, y: KeyAndValue.second.str())); |
534 | } |
535 | } |
536 | |
537 | void SuspiciousCallArgumentCheck::storeOptions( |
538 | ClangTidyOptions::OptionMap &Opts) { |
539 | Options.store(Options&: Opts, LocalName: "MinimumIdentifierNameLength" , |
540 | Value: MinimumIdentifierNameLength); |
541 | const auto &SetToggleOpt = [this, &Opts](Heuristic H) -> void { |
542 | auto Idx = static_cast<std::size_t>(H); |
543 | Options.store(Options&: Opts, LocalName: HeuristicToString[Idx], Value: isHeuristicEnabled(H)); |
544 | }; |
545 | const auto &SetBoundOpt = [this, &Opts](Heuristic H, BoundKind BK) -> void { |
546 | auto Idx = static_cast<std::size_t>(H); |
547 | assert(Idx < HeuristicCount); |
548 | if (!Defaults[Idx].hasBounds()) |
549 | return; |
550 | |
551 | SmallString<32> Key = HeuristicToString[Idx]; |
552 | Key.append(RHS: BK == BoundKind::DissimilarBelow ? "DissimilarBelow" |
553 | : "SimilarAbove" ); |
554 | Options.store(Options&: Opts, LocalName: Key, Value: *getBound(H, BK)); |
555 | }; |
556 | |
557 | for (std::size_t Idx = 0; Idx < HeuristicCount; ++Idx) { |
558 | auto H = static_cast<Heuristic>(Idx); |
559 | SetToggleOpt(H); |
560 | SetBoundOpt(H, BoundKind::DissimilarBelow); |
561 | SetBoundOpt(H, BoundKind::SimilarAbove); |
562 | } |
563 | |
564 | SmallVector<std::string, 32> Abbreviations; |
565 | for (const auto &Abbreviation : AbbreviationDictionary) { |
566 | SmallString<32> EqualSignJoined; |
567 | EqualSignJoined.append(RHS: Abbreviation.first()); |
568 | EqualSignJoined.append(RHS: "=" ); |
569 | EqualSignJoined.append(RHS: Abbreviation.second); |
570 | |
571 | if (!Abbreviation.second.empty()) |
572 | Abbreviations.emplace_back(Args: EqualSignJoined.str()); |
573 | } |
574 | Options.store(Options&: Opts, LocalName: "Abbreviations" , |
575 | Value: optutils::serializeStringList(Strings: std::vector<StringRef>( |
576 | Abbreviations.begin(), Abbreviations.end()))); |
577 | } |
578 | |
579 | bool SuspiciousCallArgumentCheck::isHeuristicEnabled(Heuristic H) const { |
580 | return llvm::is_contained(Range: AppliedHeuristics, Element: H); |
581 | } |
582 | |
583 | std::optional<int8_t> |
584 | SuspiciousCallArgumentCheck::getBound(Heuristic H, BoundKind BK) const { |
585 | auto Idx = static_cast<std::size_t>(H); |
586 | assert(Idx < HeuristicCount); |
587 | |
588 | if (!Defaults[Idx].hasBounds()) |
589 | return std::nullopt; |
590 | |
591 | switch (BK) { |
592 | case BoundKind::DissimilarBelow: |
593 | return ConfiguredBounds[Idx].first; |
594 | case BoundKind::SimilarAbove: |
595 | return ConfiguredBounds[Idx].second; |
596 | } |
597 | llvm_unreachable("Unhandled Bound kind." ); |
598 | } |
599 | |
600 | void SuspiciousCallArgumentCheck::registerMatchers(MatchFinder *Finder) { |
601 | // Only match calls with at least 2 arguments. |
602 | Finder->addMatcher( |
603 | NodeMatch: functionDecl(forEachDescendant(callExpr(unless(anyOf(argumentCountIs(N: 0), |
604 | argumentCountIs(N: 1)))) |
605 | .bind(ID: "functionCall" ))) |
606 | .bind(ID: "callingFunc" ), |
607 | Action: this); |
608 | } |
609 | |
610 | void SuspiciousCallArgumentCheck::check( |
611 | const MatchFinder::MatchResult &Result) { |
612 | const auto *MatchedCallExpr = |
613 | Result.Nodes.getNodeAs<CallExpr>(ID: "functionCall" ); |
614 | const auto *Caller = Result.Nodes.getNodeAs<FunctionDecl>(ID: "callingFunc" ); |
615 | assert(MatchedCallExpr && Caller); |
616 | |
617 | const Decl *CalleeDecl = MatchedCallExpr->getCalleeDecl(); |
618 | if (!CalleeDecl) |
619 | return; |
620 | |
621 | const FunctionDecl *CalleeFuncDecl = CalleeDecl->getAsFunction(); |
622 | if (!CalleeFuncDecl) |
623 | return; |
624 | if (CalleeFuncDecl == Caller) |
625 | // Ignore recursive calls. |
626 | return; |
627 | if (isOverloadedUnaryOrBinarySymbolOperator(FD: CalleeFuncDecl)) |
628 | return; |
629 | |
630 | // Get param attributes. |
631 | setParamNamesAndTypes(CalleeFuncDecl); |
632 | |
633 | if (ParamNames.empty()) |
634 | return; |
635 | |
636 | // Get Arg attributes. |
637 | std::size_t InitialArgIndex = 0; |
638 | |
639 | if (const auto *MethodDecl = dyn_cast<CXXMethodDecl>(Val: CalleeFuncDecl)) { |
640 | if (MethodDecl->getParent()->isLambda()) |
641 | // Lambda functions' first Arg are the lambda object. |
642 | InitialArgIndex = 1; |
643 | else if (MethodDecl->getOverloadedOperator() == OO_Call) |
644 | // For custom operator()s, the first Arg is the called object. |
645 | InitialArgIndex = 1; |
646 | } |
647 | |
648 | setArgNamesAndTypes(MatchedCallExpr, InitialArgIndex); |
649 | |
650 | if (ArgNames.empty()) |
651 | return; |
652 | |
653 | std::size_t ParamCount = ParamNames.size(); |
654 | |
655 | // Check similarity. |
656 | for (std::size_t I = 0; I < ParamCount; ++I) { |
657 | for (std::size_t J = I + 1; J < ParamCount; ++J) { |
658 | // Do not check if param or arg names are short, or not convertible. |
659 | if (!areParamAndArgComparable(Position1: I, Position2: J, Ctx: *Result.Context)) |
660 | continue; |
661 | if (!areArgsSwapped(Position1: I, Position2: J)) |
662 | continue; |
663 | |
664 | // Warning at the call itself. |
665 | diag(MatchedCallExpr->getExprLoc(), |
666 | "%ordinal0 argument '%1' (passed to '%2') looks like it might be " |
667 | "swapped with the %ordinal3, '%4' (passed to '%5')" ) |
668 | << static_cast<unsigned>(I + 1) << ArgNames[I] << ParamNames[I] |
669 | << static_cast<unsigned>(J + 1) << ArgNames[J] << ParamNames[J] |
670 | << MatchedCallExpr->getArg(Arg: I)->getSourceRange() |
671 | << MatchedCallExpr->getArg(Arg: J)->getSourceRange(); |
672 | |
673 | // Note at the functions declaration. |
674 | SourceLocation IParNameLoc = |
675 | CalleeFuncDecl->getParamDecl(i: I)->getLocation(); |
676 | SourceLocation JParNameLoc = |
677 | CalleeFuncDecl->getParamDecl(i: J)->getLocation(); |
678 | |
679 | diag(CalleeFuncDecl->getLocation(), "in the call to %0, declared here" , |
680 | DiagnosticIDs::Note) |
681 | << CalleeFuncDecl |
682 | << CharSourceRange::getTokenRange(B: IParNameLoc, E: IParNameLoc) |
683 | << CharSourceRange::getTokenRange(B: JParNameLoc, E: JParNameLoc); |
684 | } |
685 | } |
686 | } |
687 | |
688 | void SuspiciousCallArgumentCheck::setParamNamesAndTypes( |
689 | const FunctionDecl *CalleeFuncDecl) { |
690 | // Reset vectors, and fill them with the currently checked function's |
691 | // parameters' data. |
692 | ParamNames.clear(); |
693 | ParamTypes.clear(); |
694 | |
695 | for (const ParmVarDecl *Param : CalleeFuncDecl->parameters()) { |
696 | ParamTypes.push_back(Elt: Param->getType()); |
697 | |
698 | if (IdentifierInfo *II = Param->getIdentifier()) |
699 | ParamNames.push_back(Elt: II->getName()); |
700 | else |
701 | ParamNames.push_back(Elt: StringRef()); |
702 | } |
703 | } |
704 | |
705 | void SuspiciousCallArgumentCheck::setArgNamesAndTypes( |
706 | const CallExpr *MatchedCallExpr, std::size_t InitialArgIndex) { |
707 | // Reset vectors, and fill them with the currently checked function's |
708 | // arguments' data. |
709 | ArgNames.clear(); |
710 | ArgTypes.clear(); |
711 | |
712 | for (std::size_t I = InitialArgIndex, J = MatchedCallExpr->getNumArgs(); |
713 | I < J; ++I) { |
714 | assert(ArgTypes.size() == I - InitialArgIndex && |
715 | ArgNames.size() == ArgTypes.size() && |
716 | "Every iteration must put an element into the vectors!" ); |
717 | |
718 | if (const auto *ArgExpr = dyn_cast<DeclRefExpr>( |
719 | Val: MatchedCallExpr->getArg(Arg: I)->IgnoreUnlessSpelledInSource())) { |
720 | if (const auto *Var = dyn_cast<VarDecl>(Val: ArgExpr->getDecl())) { |
721 | ArgTypes.push_back(Elt: Var->getType()); |
722 | ArgNames.push_back(Elt: Var->getName()); |
723 | continue; |
724 | } |
725 | if (const auto *FCall = dyn_cast<FunctionDecl>(Val: ArgExpr->getDecl())) { |
726 | if (FCall->getNameInfo().getName().isIdentifier()) { |
727 | ArgTypes.push_back(Elt: FCall->getType()); |
728 | ArgNames.push_back(Elt: FCall->getName()); |
729 | continue; |
730 | } |
731 | } |
732 | } |
733 | |
734 | ArgTypes.push_back(Elt: QualType()); |
735 | ArgNames.push_back(Elt: StringRef()); |
736 | } |
737 | } |
738 | |
739 | bool SuspiciousCallArgumentCheck::areParamAndArgComparable( |
740 | std::size_t Position1, std::size_t Position2, const ASTContext &Ctx) const { |
741 | if (Position1 >= ArgNames.size() || Position2 >= ArgNames.size()) |
742 | return false; |
743 | |
744 | // Do not report for too short strings. |
745 | if (ArgNames[Position1].size() < MinimumIdentifierNameLength || |
746 | ArgNames[Position2].size() < MinimumIdentifierNameLength || |
747 | ParamNames[Position1].size() < MinimumIdentifierNameLength || |
748 | ParamNames[Position2].size() < MinimumIdentifierNameLength) |
749 | return false; |
750 | |
751 | if (!areTypesCompatible(ArgType: ArgTypes[Position1], ParamType: ParamTypes[Position2], Ctx) || |
752 | !areTypesCompatible(ArgType: ArgTypes[Position2], ParamType: ParamTypes[Position1], Ctx)) |
753 | return false; |
754 | |
755 | return true; |
756 | } |
757 | |
758 | bool SuspiciousCallArgumentCheck::areArgsSwapped(std::size_t Position1, |
759 | std::size_t Position2) const { |
760 | for (Heuristic H : AppliedHeuristics) { |
761 | bool A1ToP2Similar = areNamesSimilar( |
762 | Arg: ArgNames[Position2], Param: ParamNames[Position1], H, BK: BoundKind::SimilarAbove); |
763 | bool A2ToP1Similar = areNamesSimilar( |
764 | Arg: ArgNames[Position1], Param: ParamNames[Position2], H, BK: BoundKind::SimilarAbove); |
765 | |
766 | bool A1ToP1Dissimilar = |
767 | !areNamesSimilar(Arg: ArgNames[Position1], Param: ParamNames[Position1], H, |
768 | BK: BoundKind::DissimilarBelow); |
769 | bool A2ToP2Dissimilar = |
770 | !areNamesSimilar(Arg: ArgNames[Position2], Param: ParamNames[Position2], H, |
771 | BK: BoundKind::DissimilarBelow); |
772 | |
773 | if ((A1ToP2Similar || A2ToP1Similar) && A1ToP1Dissimilar && |
774 | A2ToP2Dissimilar) |
775 | return true; |
776 | } |
777 | return false; |
778 | } |
779 | |
780 | bool SuspiciousCallArgumentCheck::areNamesSimilar(StringRef Arg, |
781 | StringRef Param, Heuristic H, |
782 | BoundKind BK) const { |
783 | int8_t Threshold = -1; |
784 | if (std::optional<int8_t> GotBound = getBound(H, BK)) |
785 | Threshold = *GotBound; |
786 | |
787 | switch (H) { |
788 | case Heuristic::Equality: |
789 | return applyEqualityHeuristic(Arg, Param); |
790 | case Heuristic::Abbreviation: |
791 | return applyAbbreviationHeuristic(AbbreviationDictionary, Arg, Param); |
792 | case Heuristic::Prefix: |
793 | return applyPrefixHeuristic(Arg, Param, Threshold); |
794 | case Heuristic::Suffix: |
795 | return applySuffixHeuristic(Arg, Param, Threshold); |
796 | case Heuristic::Substring: |
797 | return applySubstringHeuristic(Arg, Param, Threshold); |
798 | case Heuristic::Levenshtein: |
799 | return applyLevenshteinHeuristic(Arg, Param, Threshold); |
800 | case Heuristic::JaroWinkler: |
801 | return applyJaroWinklerHeuristic(Arg, Param, Threshold); |
802 | case Heuristic::Dice: |
803 | return applyDiceHeuristic(Arg, Param, Threshold); |
804 | } |
805 | llvm_unreachable("Unhandled heuristic kind" ); |
806 | } |
807 | |
808 | } // namespace clang::tidy::readability |
809 | |