Skip to content

Commit de16927

Browse files
authored
Merge pull request #8576 from asgerf/js/decorated-method-or-class
JS: Add decorator edges in API graphs and corresponding MaD tokens
2 parents 82b1cd6 + 979fa23 commit de16927

File tree

7 files changed

+413
-4
lines changed

7 files changed

+413
-4
lines changed

javascript/ql/lib/semmle/javascript/ApiGraphs.qll

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,72 @@ module API {
230230
result = this.getASuccessor(Label::promisedError())
231231
}
232232

233+
/**
234+
* Gets any class that has this value as a decorator.
235+
*
236+
* For example:
237+
* ```js
238+
* import { D } from "foo";
239+
*
240+
* // moduleImport("foo").getMember("D").getADecoratedClass()
241+
* @D
242+
* class C1 {}
243+
*
244+
* // moduleImport("foo").getMember("D").getReturn().getADecoratedClass()
245+
* @D()
246+
* class C2 {}
247+
* ```
248+
*/
249+
cached
250+
Node getADecoratedClass() { result = this.getASuccessor(Label::decoratedClass()) }
251+
252+
/**
253+
* Gets any method, field, or accessor that has this value as a decorator.
254+
*
255+
* In the case of an accessor, this gets the return value of a getter, or argument to a setter.
256+
*
257+
* For example:
258+
* ```js
259+
* import { D } from "foo";
260+
*
261+
* class C {
262+
* // moduleImport("foo").getMember("D").getADecoratedMember()
263+
* @D m1() {}
264+
* @D f;
265+
* @D get g() { return this.x; }
266+
*
267+
* // moduleImport("foo").getMember("D").getReturn().getADecoratedMember()
268+
* @D() m2() {}
269+
* @D() f2;
270+
* @D() get g2() { return this.x; }
271+
* }
272+
* ```
273+
*/
274+
cached
275+
Node getADecoratedMember() { result = this.getASuccessor(Label::decoratedMember()) }
276+
277+
/**
278+
* Gets any parameter that has this value as a decorator.
279+
*
280+
* For example:
281+
* ```js
282+
* import { D } from "foo";
283+
*
284+
* class C {
285+
* method(
286+
* // moduleImport("foo").getMember("D").getADecoratedParameter()
287+
* @D
288+
* param1,
289+
* // moduleImport("foo").getMember("D").getReturn().getADecoratedParameter()
290+
* @D()
291+
* param2
292+
* ) {}
293+
* }
294+
* ```
295+
*/
296+
cached
297+
Node getADecoratedParameter() { result = this.getASuccessor(Label::decoratedParameter()) }
298+
233299
/**
234300
* Gets a string representation of the lexicographically least among all shortest access paths
235301
* from the root to this node.
@@ -570,6 +636,15 @@ module API {
570636
lbl = Label::memberFromRef(pw)
571637
)
572638
)
639+
or
640+
decoratorDualEdge(base, lbl, rhs)
641+
or
642+
decoratorRhsEdge(base, lbl, rhs)
643+
or
644+
exists(DataFlow::PropWrite write |
645+
decoratorPropEdge(base, lbl, write) and
646+
rhs = write.getRhs()
647+
)
573648
}
574649

575650
/**
@@ -699,6 +774,98 @@ module API {
699774
lbl = Label::parameter(1) and
700775
ref = awaited(call)
701776
)
777+
or
778+
decoratorDualEdge(base, lbl, ref)
779+
or
780+
decoratorUseEdge(base, lbl, ref)
781+
or
782+
// for fields and accessors, mark the reads as use-nodes
783+
decoratorPropEdge(base, lbl, ref.(DataFlow::PropRead))
784+
)
785+
}
786+
787+
/** Holds if `base` is a use-node that flows to the decorator expression of the given decorator. */
788+
pragma[nomagic]
789+
private predicate useNodeFlowsToDecorator(TApiNode base, Decorator decorator) {
790+
exists(DataFlow::SourceNode decoratorSrc |
791+
use(base, decoratorSrc) and
792+
trackUseNode(decoratorSrc).flowsToExpr(decorator.getExpression())
793+
)
794+
}
795+
796+
/**
797+
* Holds if `ref` corresponds to both a use and def-node that should have an incoming edge from `base` labelled `lbl`.
798+
*
799+
* This happens because the decorated value escapes into the decorator function, and is then replaced
800+
* by the function's return value. In the JS analysis we generally assume decorators return their input,
801+
* but library models may want to find the return value.
802+
*/
803+
private predicate decoratorDualEdge(TApiNode base, Label::ApiLabel lbl, DataFlow::Node ref) {
804+
exists(ClassDefinition cls |
805+
useNodeFlowsToDecorator(base, cls.getADecorator()) and
806+
lbl = Label::decoratedClass() and
807+
ref = DataFlow::valueNode(cls)
808+
)
809+
or
810+
exists(MethodDefinition method |
811+
useNodeFlowsToDecorator(base, method.getADecorator()) and
812+
not method instanceof AccessorMethodDefinition and
813+
lbl = Label::decoratedMember() and
814+
ref = DataFlow::valueNode(method.getBody())
815+
)
816+
}
817+
818+
/** Holds if `ref` is a use that should have an incoming edge from `base` labelled `lbl`, induced by a decorator. */
819+
private predicate decoratorUseEdge(TApiNode base, Label::ApiLabel lbl, DataFlow::Node ref) {
820+
exists(SetterMethodDefinition accessor |
821+
useNodeFlowsToDecorator(base,
822+
[accessor.getADecorator(), accessor.getCorrespondingGetter().getADecorator()]) and
823+
lbl = Label::decoratedMember() and
824+
ref = DataFlow::parameterNode(accessor.getBody().getParameter(0))
825+
)
826+
or
827+
exists(Parameter param |
828+
useNodeFlowsToDecorator(base, param.getADecorator()) and
829+
lbl = Label::decoratedParameter() and
830+
ref = DataFlow::parameterNode(param)
831+
)
832+
}
833+
834+
/** Holds if `rhs` is a def node that should have an incoming edge from `base` labelled `lbl`, induced by a decorator. */
835+
private predicate decoratorRhsEdge(TApiNode base, Label::ApiLabel lbl, DataFlow::Node rhs) {
836+
exists(GetterMethodDefinition accessor |
837+
useNodeFlowsToDecorator(base,
838+
[accessor.getADecorator(), accessor.getCorrespondingSetter().getADecorator()]) and
839+
lbl = Label::decoratedMember() and
840+
rhs = DataFlow::valueNode(accessor.getBody().getAReturnedExpr())
841+
)
842+
}
843+
844+
/**
845+
* Holds if `ref` is a reference to a field/accessor that should have en incoming edge from base labelled `lbl`.
846+
*
847+
* Since fields do not have their own data-flow nodes, we generate a node for each read or write.
848+
* For property writes, the right-hand side becomes a def-node and property reads become use-nodes.
849+
*
850+
* For accessors this predicate computes each use of the accessor.
851+
* The return value inside the accessor is computed by the `decoratorRhsEdge` predicate.
852+
*/
853+
private predicate decoratorPropEdge(TApiNode base, Label::ApiLabel lbl, DataFlow::PropRef ref) {
854+
exists(MemberDefinition fieldLike, DataFlow::ClassNode cls |
855+
fieldLike instanceof FieldDefinition
856+
or
857+
fieldLike instanceof AccessorMethodDefinition
858+
|
859+
useNodeFlowsToDecorator(base, fieldLike.getADecorator()) and
860+
lbl = Label::decoratedMember() and
861+
cls = fieldLike.getDeclaringClass().flow() and
862+
(
863+
fieldLike.isStatic() and
864+
ref = cls.getAClassReference().getAPropertyReference(fieldLike.getName())
865+
or
866+
not fieldLike.isStatic() and
867+
ref = cls.getAnInstanceReference().getAPropertyReference(fieldLike.getName())
868+
)
702869
)
703870
}
704871

@@ -1106,6 +1273,15 @@ module API {
11061273
/** Gets the `promisedError` edge label connecting a promise to its rejected value. */
11071274
LabelPromisedError promisedError() { any() }
11081275

1276+
/** Gets the label for an edge leading from a value `D` to any class that has `D` as a decorator. */
1277+
LabelDecoratedClass decoratedClass() { any() }
1278+
1279+
/** Gets the label for an edge leading from a value `D` to any method, field, or accessor that has `D` as a decorator. */
1280+
LabelDecoratedMethod decoratedMember() { any() }
1281+
1282+
/** Gets the label for an edge leading from a value `D` to any parameter that has `D` as a decorator. */
1283+
LabelDecoratedParameter decoratedParameter() { any() }
1284+
11091285
/** Gets an entry-point label for the entry-point `e`. */
11101286
LabelEntryPoint entryPoint(API::EntryPoint e) { result.getEntryPoint() = e }
11111287

@@ -1140,6 +1316,9 @@ module API {
11401316
MkLabelReturn() or
11411317
MkLabelPromised() or
11421318
MkLabelPromisedError() or
1319+
MkLabelDecoratedClass() or
1320+
MkLabelDecoratedMember() or
1321+
MkLabelDecoratedParameter() or
11431322
MkLabelEntryPoint(API::EntryPoint e)
11441323

11451324
/** A label for an entry-point. */
@@ -1229,6 +1408,21 @@ module API {
12291408
class LabelReceiver extends ApiLabel, MkLabelReceiver {
12301409
override string toString() { result = "receiver" }
12311410
}
1411+
1412+
/** A label for a class decorated by the current value. */
1413+
class LabelDecoratedClass extends ApiLabel, MkLabelDecoratedClass {
1414+
override string toString() { result = "decorated-class" }
1415+
}
1416+
1417+
/** A label for a method, field, or accessor decorated by the current value. */
1418+
class LabelDecoratedMethod extends ApiLabel, MkLabelDecoratedMember {
1419+
override string toString() { result = "decorated-member" }
1420+
}
1421+
1422+
/** A label for a parameter decorated by the current value. */
1423+
class LabelDecoratedParameter extends ApiLabel, MkLabelDecoratedParameter {
1424+
override string toString() { result = "decorated-parameter" }
1425+
}
12321426
}
12331427
}
12341428
}

javascript/ql/lib/semmle/javascript/Classes.qll

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -895,7 +895,14 @@ class SyntheticConstructor extends Function {
895895
* }
896896
* ```
897897
*/
898-
abstract class AccessorMethodDeclaration extends MethodDeclaration { }
898+
abstract class AccessorMethodDeclaration extends MethodDeclaration {
899+
/** Get the corresponding getter (if this is a setter) or setter (if this is a getter). */
900+
AccessorMethodDeclaration getOtherAccessor() {
901+
getterSetterPair(this, result)
902+
or
903+
getterSetterPair(result, this)
904+
}
905+
}
899906

900907
/**
901908
* A concrete accessor method definition in a class, that is, an accessor method with a function body.
@@ -958,6 +965,9 @@ abstract class AccessorMethodSignature extends MethodSignature, AccessorMethodDe
958965
*/
959966
class GetterMethodDeclaration extends AccessorMethodDeclaration, @property_getter {
960967
override string getAPrimaryQlClass() { result = "GetterMethodDeclaration" }
968+
969+
/** Gets the correspinding setter declaration, if any. */
970+
SetterMethodDeclaration getCorrespondingSetter() { getterSetterPair(this, result) }
961971
}
962972

963973
/**
@@ -1020,6 +1030,9 @@ class GetterMethodSignature extends GetterMethodDeclaration, AccessorMethodSigna
10201030
*/
10211031
class SetterMethodDeclaration extends AccessorMethodDeclaration, @property_setter {
10221032
override string getAPrimaryQlClass() { result = "SetterMethodDeclaration" }
1033+
1034+
/** Gets the correspinding getter declaration, if any. */
1035+
GetterMethodDeclaration getCorrespondingGetter() { getterSetterPair(result, this) }
10231036
}
10241037

10251038
/**
@@ -1263,3 +1276,25 @@ class IndexSignature extends @index_signature, MemberSignature {
12631276

12641277
override string getAPrimaryQlClass() { result = "IndexSignature" }
12651278
}
1279+
1280+
private boolean getStaticness(AccessorMethodDefinition member) {
1281+
member.isStatic() and result = true
1282+
or
1283+
not member.isStatic() and result = false
1284+
}
1285+
1286+
pragma[nomagic]
1287+
private AccessorMethodDefinition getAnAccessorFromClass(
1288+
ClassDefinition cls, string name, boolean static
1289+
) {
1290+
result = cls.getMember(name) and
1291+
static = getStaticness(result)
1292+
}
1293+
1294+
pragma[nomagic]
1295+
private predicate getterSetterPair(GetterMethodDeclaration getter, SetterMethodDeclaration setter) {
1296+
exists(ClassDefinition cls, string name, boolean static |
1297+
getter = getAnAccessorFromClass(cls, name, static) and
1298+
setter = getAnAccessorFromClass(cls, name, static)
1299+
)
1300+
}

javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModelsSpecific.qll

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathToken token) {
124124
token.getName() = "Parameter" and
125125
token.getAnArgument() = "this" and
126126
result = node.getReceiver()
127+
or
128+
token.getName() = "DecoratedClass" and
129+
result = node.getADecoratedClass()
130+
or
131+
token.getName() = "DecoratedMember" and
132+
result = node.getADecoratedMember()
133+
or
134+
token.getName() = "DecoratedParameter" and
135+
result = node.getADecoratedParameter()
127136
}
128137

129138
/**
@@ -210,15 +219,23 @@ InvokeNode getAnInvocationOf(API::Node node) { result = node.getAnInvocation() }
210219
*/
211220
bindingset[name]
212221
predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
213-
name = ["Member", "Instance", "Awaited", "ArrayElement", "Element", "MapValue", "NewCall", "Call"]
222+
name =
223+
[
224+
"Member", "Instance", "Awaited", "ArrayElement", "Element", "MapValue", "NewCall", "Call",
225+
"DecoratedClass", "DecoratedMember", "DecoratedParameter"
226+
]
214227
}
215228

216229
/**
217230
* Holds if `name` is a valid name for an access path token with no arguments, occuring
218231
* in an identifying access path.
219232
*/
220233
predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
221-
name = ["Instance", "Awaited", "ArrayElement", "Element", "MapValue", "NewCall", "Call"]
234+
name =
235+
[
236+
"Instance", "Awaited", "ArrayElement", "Element", "MapValue", "NewCall", "Call",
237+
"DecoratedClass", "DecoratedMember", "DecoratedParameter"
238+
]
222239
}
223240

224241
/**
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as testlib from 'testlib';
2+
3+
// parameter decorators are only valid in TypeScript so this test is in a .ts file
4+
5+
class C {
6+
decoratedParamSource(@testlib.ParamDecoratorSource x) {
7+
sink(x) // NOT OK
8+
}
9+
decoratedParamSink(@testlib.ParamDecoratorSink x) { // OK
10+
}
11+
decoratedParamSink2(@testlib.ParamDecoratorSink x) { // OK
12+
x.push(source()); // OK
13+
}
14+
}
15+
16+
new C().decoratedParamSink(source()); // OK - parameter decorators can't be used to mark the parameter as a sink
17+
new C().decoratedParamSink2([]); // OK

0 commit comments

Comments
 (0)