|
| 1 | +/** |
| 2 | + * @name Unreachable method overloads |
| 3 | + * @description Having multiple overloads with the same parameter types in TypeScript |
| 4 | + * makes all overloads except the first one unreachable, as the compiler |
| 5 | + * always resolves calls to the textually first matching overload. |
| 6 | + * @kind problem |
| 7 | + * @problem.severity warning |
| 8 | + * @id js/unreachable-method-overloads |
| 9 | + * @precision high |
| 10 | + * @tags correctness |
| 11 | + * typescript |
| 12 | + */ |
| 13 | + |
| 14 | +import javascript |
| 15 | + |
| 16 | +/** |
| 17 | + * Gets the `i`th parameter from the method signature. |
| 18 | + */ |
| 19 | +SimpleParameter getParameter(MethodSignature sig, int i) { result = sig.getBody().getParameter(i) } |
| 20 | + |
| 21 | +/** |
| 22 | + * Gets a string-representation of the type-annotation from the `i`th parameter in the method signature. |
| 23 | + */ |
| 24 | +string getParameterTypeAnnotation(MethodSignature sig, int i) { |
| 25 | + result = getParameter(sig, i).getTypeAnnotation().toString() |
| 26 | +} |
| 27 | + |
| 28 | +/** |
| 29 | + * Gets the other overloads for an overloaded method signature. |
| 30 | + */ |
| 31 | +MethodSignature getOtherMatchingSignatures(MethodSignature sig) { |
| 32 | + signaturesMatch(result, sig) and |
| 33 | + result != sig |
| 34 | +} |
| 35 | + |
| 36 | +/** |
| 37 | + * Gets the kind of the member-declaration. Either "static" or "instance". |
| 38 | + */ |
| 39 | +string getKind(MemberDeclaration m) { |
| 40 | + if m.isStatic() then result = "static" else result = "instance" |
| 41 | +} |
| 42 | + |
| 43 | +/** |
| 44 | + * A call-signature that originates from a MethodSignature in the AST. |
| 45 | + */ |
| 46 | +private class MethodCallSig extends CallSignatureType { |
| 47 | + string name; |
| 48 | + |
| 49 | + MethodCallSig() { |
| 50 | + exists(MethodSignature sig | |
| 51 | + this = sig.getBody().getCallSignature() and |
| 52 | + name = sig.getName() |
| 53 | + ) |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * Gets the name of any member that has this signature. |
| 58 | + */ |
| 59 | + string getName() { |
| 60 | + result = name |
| 61 | + } |
| 62 | +} |
| 63 | + |
| 64 | +/** |
| 65 | + * Holds if the two call signatures could be overloads of each other and have the same parameter types. |
| 66 | + */ |
| 67 | +predicate matchingCallSignature(MethodCallSig method, MethodCallSig other) { |
| 68 | + method.getName() = other.getName() and |
| 69 | + |
| 70 | + method.getNumOptionalParameter() = other.getNumOptionalParameter() and |
| 71 | + method.getNumParameter() = other.getNumParameter() and |
| 72 | + method.getNumRequiredParameter() = other.getNumRequiredParameter() and |
| 73 | + // purposely not looking at number of type arguments. |
| 74 | + |
| 75 | + method.getKind() = other.getKind() and |
| 76 | + |
| 77 | + |
| 78 | + forall(int i | i in [0 .. -1 + method.getNumParameter()] | |
| 79 | + method.getParameter(i) = other.getParameter(i) // This is sometimes imprecise, so it is still a good idea to compare type annotations. |
| 80 | + ) and |
| 81 | + |
| 82 | + // shared type parameters are equal. |
| 83 | + forall(int i | i in [0 .. -1 + min(int num | num = method.getNumTypeParameter() or num = other.getNumTypeParameter())] | |
| 84 | + method.getTypeParameterBound(i) = other.getTypeParameterBound(i) |
| 85 | + ) |
| 86 | +} |
| 87 | + |
| 88 | +/** |
| 89 | + * Gets which overload index the MethodSignature has among the overloads of the same name. |
| 90 | + */ |
| 91 | +int getOverloadIndex(MethodSignature sig) { |
| 92 | + sig.getDeclaringType().getMethodOverload(sig.getName(), result) = sig |
| 93 | +} |
| 94 | + |
| 95 | +/** |
| 96 | + * Holds if the two method signatures are overloads of each other and have the same parameter types. |
| 97 | + */ |
| 98 | +predicate signaturesMatch(MethodSignature method, MethodSignature other) { |
| 99 | + // declared in the same interface/class. |
| 100 | + method.getDeclaringType() = other.getDeclaringType() and |
| 101 | + // same static modifier. |
| 102 | + getKind(method) = getKind(other) and |
| 103 | + |
| 104 | + // same name. |
| 105 | + method.getName() = other.getName() and |
| 106 | + |
| 107 | + // same number of parameters. |
| 108 | + method.getBody().getNumParameter() = other.getBody().getNumParameter() and |
| 109 | + |
| 110 | + // The types are compared in matchingCallSignature. This is sanity-check that the textual representation of the type-annotations are somewhat similar. |
| 111 | + forall(int i | i in [0 .. -1 + method.getBody().getNumParameter()] | |
| 112 | + getParameterTypeAnnotation(method, i) = getParameterTypeAnnotation(other, i) |
| 113 | + ) and |
| 114 | + |
| 115 | + matchingCallSignature(method.getBody().getCallSignature(), other.getBody().getCallSignature()) |
| 116 | +} |
| 117 | + |
| 118 | +from ClassOrInterface decl, string name, MethodSignature previous, MethodSignature unreachable |
| 119 | +where |
| 120 | + previous = decl.getMethod(name) and |
| 121 | + unreachable = getOtherMatchingSignatures(previous) and |
| 122 | + |
| 123 | + // If the method is part of inheritance between classes/interfaces, then there can sometimes be reasons for having this pattern. |
| 124 | + not exists(decl.getASuperTypeDeclaration().getMethod(name)) and |
| 125 | + not exists(ClassOrInterface sub | |
| 126 | + decl = sub.getASuperTypeDeclaration() and |
| 127 | + exists(sub.getMethod(name)) |
| 128 | + ) and |
| 129 | + |
| 130 | + |
| 131 | + // If a later method overload has more type parameters, then that overload can be selected by explicitly declaring the type arguments at the callsite. |
| 132 | + // This comparison removes those cases. |
| 133 | + unreachable.getBody().getNumTypeParameter() <= previous.getBody().getNumTypeParameter() and |
| 134 | + |
| 135 | + // We always select the first of the overloaded methods. |
| 136 | + not exists(MethodSignature later | later = getOtherMatchingSignatures(previous) | |
| 137 | + getOverloadIndex(later) < getOverloadIndex(previous) |
| 138 | + ) |
| 139 | +select unreachable, |
| 140 | + "This overload of " + name + "() is unreachable, the $@ overload will always be selected.", previous, "previous" |
0 commit comments