Skip to content

Commit 90cefea

Browse files
author
Max Schaefer
authored
Merge pull request #1988 from erik-krogh/unreacableOverloads
JS: Unreachable overloads
2 parents 016c95a + 9eda120 commit 90cefea

File tree

13 files changed

+295
-0
lines changed

13 files changed

+295
-0
lines changed

change-notes/1.23/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
| Suspicious method name (`js/suspicious-method-name-declaration`) | correctness, typescript, methods | Highlights suspiciously named methods where the developer likely meant to write a constructor or function. Results are shown on LGTM by default. |
2222
| Use of returnless function (`js/use-of-returnless-function`) | maintainability, correctness | Highlights calls where the return value is used, but the callee never returns a value. Results are shown on LGTM by default. |
2323
| Useless regular expression character escape (`js/useless-regexp-character-escape`) | correctness, security, external/cwe/cwe-20 | Highlights regular expression strings with useless character escapes, indicating a possible violation of [CWE-20](https://cwe.mitre.org/data/definitions/20.html). Results are shown on LGTM by default. |
24+
| Unreachable method overloads (`js/unreachable-method-overloads`) | correctness, typescript | Highlights method overloads that are impossible to use from client code. Results are shown on LGTM by default. |
2425

2526
## Changes to existing queries
2627

javascript/config/suites/javascript/maintainability-more

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
+ semmlecode-javascript-queries/Declarations/UnusedParameter.ql: /Maintainability/Declarations
88
+ semmlecode-javascript-queries/Declarations/UnusedProperty.ql: /Maintainability/Declarations
99
+ semmlecode-javascript-queries/Declarations/UnusedVariable.ql: /Maintainability/Declarations
10+
+ semmlecode-javascript-queries/Declarations/UnreachableMethodOverloads.ql: /Maintainability/Declarations
1011
+ semmlecode-javascript-queries/Expressions/UnneededDefensiveProgramming.ql: /Maintainability/Expressions
1112
+ semmlecode-javascript-queries/LanguageFeatures/ArgumentsCallerCallee.ql: /Maintainability/Language Features
1213
+ semmlecode-javascript-queries/LanguageFeatures/ConditionalComments.ql: /Maintainability/Language Features
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
The TypeScript compiler has to choose which specific overload is called
8+
when a method with multiple overloads is called.
9+
The compiler will always choose the textually first overload that does
10+
not give rise to any type errors with the arguments provided at the
11+
function call.
12+
</p>
13+
<p>
14+
This behavior can be unintuitive for programmers unfamiliar with the
15+
type system in TypeScript, and can in some instances lead to situations
16+
where a programmer writes an overloaded method where only the first
17+
overload can ever be used.
18+
</p>
19+
</overview>
20+
<recommendation>
21+
<p>
22+
Either reorder the method overloads if an overload with more type
23+
parameters is placed before a similar overload with fewer parameters.
24+
Alternatively, collapse multiple overloads with identical parameter types by
25+
creating a single overload that returns a union of the return types
26+
from the multiple overloads.
27+
</p>
28+
</recommendation>
29+
<example>
30+
<p>
31+
In the example below, a programmer has tried to express that a method
32+
can return multiple possible values by creating multiple overloads
33+
with identical parameter types. However, only the first overload
34+
will ever be selected by the TypeScript compiler.
35+
</p>
36+
<sample src="examples/UnreachableMethodOverloads.ts" />
37+
<p>
38+
The error can be fixed by merging the overloads into a single method
39+
signature that returns a union of the previous return types.
40+
</p>
41+
<sample src="examples/UnreachableMethodOverloadsGood.ts" />
42+
43+
<p>
44+
In the example below, an interface <code>Foo</code> declares a method
45+
<code>create()</code> with two overloads. The only difference between
46+
the two overloads is the type parameter <code>T</code> in the first
47+
overload. The TypeScript compiler will always use the first overload
48+
when <code>create()</code> is called, as a default type will be used
49+
for the type parameter <code>T</code> if none is provided.
50+
This default type is <code>unknown</code> in TypeScript 3.5+, and
51+
<code>{}</code> in earlier versions.
52+
</p>
53+
<sample src="examples/UnreachableMethodOverloadsTypeParameters.ts" />
54+
<p>
55+
In this example, the error has been fixed by switching the order of the two
56+
overloads. In this fixed version, if the <code>create()</code> method
57+
is called with an explicit type argument the second overload will be
58+
used, as the first overload would give rise to a type error.
59+
</p>
60+
<sample src="examples/UnreachableMethodOverloadsTypeParametersGood.ts" />
61+
</example>
62+
<references>
63+
64+
<li>TypeScript specification: <a href="https://github.com/microsoft/TypeScript/blob/7be7cba050799bc11c9411babd31f44c9ec087f0/doc/spec.md#4.15.1">Overload Resolution</a></li>
65+
66+
</references>
67+
</qhelp>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface Foo {
2+
getParsedThing(id: string): string[];
3+
getParsedThing(id: string): number[];
4+
getParsedThing(id: string): object[];
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
interface Foo {
2+
getParsedThing(id: string): object[] | number[] | string[];
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface Foo {
2+
create<T>(a: string): MyObject<T>;
3+
create(a: string): MyObject<any>;
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface Foo {
2+
create(a: string): Array<any>;
3+
create<T>(a: string): Array<T>;
4+
}

javascript/ql/src/semmle/javascript/Classes.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ class ClassOrInterface extends @classorinterface, TypeParameterized {
123123
* Anonymous classes and interfaces do not have a canonical name.
124124
*/
125125
TypeName getTypeName() { result.getADefinition() = this }
126+
127+
/**
128+
* Gets the ClassOrInterface corresponding to either a super type or an implemented interface.
129+
*/
130+
ClassOrInterface getASuperTypeDeclaration() {
131+
this.getSuperClass().(VarAccess).getVariable().getADeclaration() = result.getIdentifier() or
132+
this.getASuperInterface().(LocalTypeAccess).getLocalTypeName().getADeclaration() = result.getIdentifier()
133+
}
126134
}
127135

128136
/**
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
| tst.ts:3:3:3:30 | method( ... number; | This overload of method() is unreachable, the $@ overload will always be selected. | tst.ts:2:3:2:30 | method( ... string; | previous |
2+
| tst.ts:6:3:6:17 | types1(): any[] | This overload of types1() is unreachable, the $@ overload will always be selected. | tst.ts:5:3:5:18 | types1<T>(): T[] | previous |
3+
| tst.ts:15:3:15:74 | on(even ... nction; | This overload of on() is unreachable, the $@ overload will always be selected. | tst.ts:14:3:14:74 | on(even ... nction; | previous |
4+
| tst.ts:21:3:21:30 | method( ... number; | This overload of method() is unreachable, the $@ overload will always be selected. | tst.ts:20:3:20:30 | method( ... string; | previous |

0 commit comments

Comments
 (0)