Skip to content

Commit c850938

Browse files
authored
Merge pull request github#3833 from asger-semmle/js/vue-class-component
Approved by erik-krogh
2 parents 15a0297 + 3e616e9 commit c850938

File tree

11 files changed

+180
-28
lines changed

11 files changed

+180
-28
lines changed

change-notes/1.25/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- [sqlite](https://www.npmjs.com/package/sqlite)
2424
- [ssh2-streams](https://www.npmjs.com/package/ssh2-streams)
2525
- [ssh2](https://www.npmjs.com/package/ssh2)
26+
- [vue](https://www.npmjs.com/package/vue)
2627
- [yargs](https://www.npmjs.com/package/yargs)
2728
- [webpack-dev-server](https://www.npmjs.com/package/webpack-dev-server)
2829

javascript/ql/src/semmle/javascript/dataflow/Nodes.qll

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,18 @@ DataFlow::SourceNode moduleMember(string path, string m) {
726726
*/
727727
class MemberKind extends string {
728728
MemberKind() { this = "method" or this = "getter" or this = "setter" }
729+
730+
/** Holds if this is the `method` kind. */
731+
predicate isMethod() { this = MemberKind::method() }
732+
733+
/** Holds if this is the `getter` kind. */
734+
predicate isGetter() { this = MemberKind::getter() }
735+
736+
/** Holds if this is the `setter` kind. */
737+
predicate isSetter() { this = MemberKind::setter() }
738+
739+
/** Holds if this is the `getter` or `setter` kind. */
740+
predicate isAccessor() { this = MemberKind::accessor() }
729741
}
730742

731743
module MemberKind {

javascript/ql/src/semmle/javascript/frameworks/Vue.qll

Lines changed: 110 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,54 @@ module Vue {
3030
MkComponent(DataFlow::CallNode def) { def = vue().getAMemberCall("component") } or
3131
MkSingleFileComponent(VueFile file)
3232

33+
/** Gets the name of a lifecycle hook method. */
34+
private string lifecycleHookName() {
35+
result =
36+
["beforeCreate", "created", "beforeMount", "mounted", "beforeUpdate", "updated", "activated",
37+
"deactivated", "beforeDestroy", "destroyed", "errorCaptured"]
38+
}
39+
40+
/** Gets a value that can be used as a `@Component` decorator. */
41+
private DataFlow::SourceNode componentDecorator() {
42+
result = DataFlow::moduleImport("vue-class-component")
43+
or
44+
result = DataFlow::moduleMember("vue-property-decorator", "Component")
45+
}
46+
47+
/**
48+
* A class with a `@Component` decorator, making it usable as an "options" object in Vue.
49+
*/
50+
private class ClassComponent extends DataFlow::ClassNode {
51+
DataFlow::Node decorator;
52+
53+
ClassComponent() {
54+
exists(ClassDefinition cls |
55+
this = cls.flow() and
56+
cls.getADecorator().getExpression() = decorator.asExpr() and
57+
(
58+
componentDecorator().flowsTo(decorator)
59+
or
60+
componentDecorator().getACall() = decorator
61+
)
62+
)
63+
}
64+
65+
/**
66+
* Gets an option passed to the `@Component` decorator.
67+
*
68+
* These options correspond to the options one would pass to `new Vue({...})` or similar.
69+
*/
70+
DataFlow::Node getDecoratorOption(string name) {
71+
result = decorator.(DataFlow::CallNode).getOptionArgument(0, name)
72+
}
73+
}
74+
75+
private string memberKindVerb(DataFlow::MemberKind kind) {
76+
kind = DataFlow::MemberKind::getter() and result = "get"
77+
or
78+
kind = DataFlow::MemberKind::setter() and result = "set"
79+
}
80+
3381
/**
3482
* A Vue instance definition.
3583
*
@@ -65,11 +113,27 @@ module Vue {
65113
endcolumn = 0
66114
}
67115

116+
/**
117+
* Gets the options passed to the Vue object, such as the object literal `{...}` in `new Vue{{...})`
118+
* or the default export of a single-file component.
119+
*/
120+
abstract DataFlow::Node getOwnOptionsObject();
121+
122+
/**
123+
* Gets the class component implementing this Vue instance, if any.
124+
*
125+
* Specifically, this is a class annotated with `@Component` which flows to the options
126+
* object of this Vue instance.
127+
*/
128+
ClassComponent getAsClassComponent() { result.flowsTo(getOwnOptionsObject()) }
129+
68130
/**
69131
* Gets the node for option `name` for this instance, this does not include
70132
* those from extended objects and mixins.
71133
*/
72-
abstract DataFlow::Node getOwnOption(string name);
134+
DataFlow::Node getOwnOption(string name) {
135+
result = getOwnOptionsObject().getALocalSource().getAPropertyWrite(name).getRhs()
136+
}
73137

74138
/**
75139
* Gets the node for option `name` for this instance, including those from
@@ -92,6 +156,8 @@ module Vue {
92156
mixin.flowsTo(mixins.getAnElement()) and
93157
result = mixin.getAPropertyWrite(name).getRhs()
94158
)
159+
or
160+
result = getAsClassComponent().getDecoratorOption(name)
95161
}
96162

97163
/**
@@ -112,6 +178,10 @@ module Vue {
112178
result = f.getAReturn()
113179
)
114180
)
181+
or
182+
result = getAsClassComponent().getAReceiverNode()
183+
or
184+
result = getAsClassComponent().getInstanceMethod("data").getAReturn()
115185
}
116186

117187
/**
@@ -122,7 +192,11 @@ module Vue {
122192
/**
123193
* Gets the node for the `render` option of this instance.
124194
*/
125-
DataFlow::Node getRender() { result = getOption("render") }
195+
DataFlow::Node getRender() {
196+
result = getOption("render")
197+
or
198+
result = getAsClassComponent().getInstanceMethod("render")
199+
}
126200

127201
/**
128202
* Gets the node for the `methods` option of this instance.
@@ -143,62 +217,63 @@ module Vue {
143217
methods.flowsTo(getMethods()) and
144218
result = methods.getAPropertyWrite().getRhs()
145219
)
220+
or
221+
result = getAsClassComponent().getAnInstanceMethod() and
222+
not result = getAsClassComponent().getInstanceMethod([lifecycleHookName(), "render", "data"])
146223
}
147224

148225
/**
149-
* Gets a node for a member of the `computed` option of this instance that matches `kind` ("get" or "set").
226+
* Gets a node for a member of the `computed` option of this instance that matches `kind`.
150227
*/
151228
pragma[noinline]
152-
private DataFlow::Node getAnAccessor(string kind) {
229+
private DataFlow::Node getAnAccessor(DataFlow::MemberKind kind) {
153230
exists(DataFlow::ObjectLiteralNode computedObj, DataFlow::Node accessorObjOrGetter |
154231
computedObj.flowsTo(getComputed()) and
155232
computedObj.getAPropertyWrite().getRhs() = accessorObjOrGetter
156233
|
157-
result = accessorObjOrGetter and kind = "get"
234+
result = accessorObjOrGetter and kind = DataFlow::MemberKind::getter()
158235
or
159236
exists(DataFlow::ObjectLiteralNode accessorObj |
160237
accessorObj.flowsTo(accessorObjOrGetter) and
161-
result = accessorObj.getAPropertyWrite(kind).getRhs()
238+
result = accessorObj.getAPropertyWrite(memberKindVerb(kind)).getRhs()
162239
)
163240
)
241+
or
242+
result = getAsClassComponent().getAnInstanceMember(kind) and
243+
kind.isAccessor()
164244
}
165245

166246
/**
167-
* Gets a node for a member `name` of the `computed` option of this instance that matches `kind` ("get" or "set").
247+
* Gets a node for a member `name` of the `computed` option of this instance that matches `kind`.
168248
*/
169-
private DataFlow::Node getAccessor(string name, string kind) {
249+
private DataFlow::Node getAccessor(string name, DataFlow::MemberKind kind) {
170250
exists(DataFlow::ObjectLiteralNode computedObj, DataFlow::SourceNode accessorObjOrGetter |
171251
computedObj.flowsTo(getComputed()) and
172252
accessorObjOrGetter.flowsTo(computedObj.getAPropertyWrite(name).getRhs())
173253
|
174-
result = accessorObjOrGetter and kind = "get"
254+
result = accessorObjOrGetter and kind = DataFlow::MemberKind::getter()
175255
or
176256
exists(DataFlow::ObjectLiteralNode accessorObj |
177257
accessorObj.flowsTo(accessorObjOrGetter) and
178-
result = accessorObj.getAPropertyWrite(kind).getRhs()
258+
result = accessorObj.getAPropertyWrite(memberKindVerb(kind)).getRhs()
179259
)
180260
)
261+
or
262+
result = getAsClassComponent().getInstanceMember(name, kind) and
263+
kind.isAccessor()
181264
}
182265

183266
/**
184267
* Gets the node for the life cycle hook of the `hookName` option of this instance.
185268
*/
186269
pragma[noinline]
187270
private DataFlow::Node getALifecycleHook(string hookName) {
271+
hookName = lifecycleHookName() and
188272
(
189-
hookName = "beforeCreate" or
190-
hookName = "created" or
191-
hookName = "beforeMount" or
192-
hookName = "mounted" or
193-
hookName = "beforeUpdate" or
194-
hookName = "updated" or
195-
hookName = "activated" or
196-
hookName = "deactivated" or
197-
hookName = "beforeDestroy" or
198-
hookName = "destroyed" or
199-
hookName = "errorCaptured"
200-
) and
201-
result = getOption(hookName)
273+
result = getOption(hookName)
274+
or
275+
result = getAsClassComponent().getInstanceMethod(hookName)
276+
)
202277
}
203278

204279
/**
@@ -227,7 +302,7 @@ module Vue {
227302
)
228303
or
229304
exists(DataFlow::FunctionNode getter |
230-
getter.flowsTo(getAccessor(name, "get")) and
305+
getter.flowsTo(getAccessor(name, DataFlow::MemberKind::getter())) and
231306
result = getter.getAReturn()
232307
)
233308
}
@@ -249,7 +324,7 @@ module Vue {
249324
def.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
250325
}
251326

252-
override DataFlow::Node getOwnOption(string name) { result = def.getOptionArgument(0, name) }
327+
override DataFlow::Node getOwnOptionsObject() { result = def.getArgument(0) }
253328

254329
override Template::Element getTemplateElement() { none() }
255330
}
@@ -270,7 +345,7 @@ module Vue {
270345
extend.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
271346
}
272347

273-
override DataFlow::Node getOwnOption(string name) { result = extend.getOptionArgument(0, name) }
348+
override DataFlow::Node getOwnOptionsObject() { result = extend.getArgument(0) }
274349

275350
override Template::Element getTemplateElement() { none() }
276351
}
@@ -292,7 +367,7 @@ module Vue {
292367
sub.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
293368
}
294369

295-
override DataFlow::Node getOwnOption(string name) { result = sub.getOptionArgument(0, name) }
370+
override DataFlow::Node getOwnOptionsObject() { result = sub.getArgument(0) }
296371

297372
override DataFlow::Node getOption(string name) {
298373
result = Instance.super.getOption(name)
@@ -319,7 +394,7 @@ module Vue {
319394
def.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
320395
}
321396

322-
override DataFlow::Node getOwnOption(string name) { result = def.getOptionArgument(1, name) }
397+
override DataFlow::Node getOwnOptionsObject() { result = def.getArgument(1) }
323398

324399
override Template::Element getTemplateElement() { none() }
325400
}
@@ -357,6 +432,13 @@ module Vue {
357432
)
358433
}
359434

435+
override DataFlow::Node getOwnOptionsObject() {
436+
exists(ExportDefaultDeclaration decl |
437+
decl.getTopLevel() = getModule() and
438+
result = DataFlow::valueNode(decl.getOperand())
439+
)
440+
}
441+
360442
override DataFlow::Node getOwnOption(string name) {
361443
// The options of a single file component are defined by the exported object of the script element.
362444
// Our current module model does not support reads on this object very well, so we use custom steps for the common cases for now.

javascript/ql/test/library-tests/frameworks/Vue/Instance.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue |
22
| single-file-component-2.vue:0:0:0:0 | single-file-component-2.vue |
33
| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue |
4+
| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue |
5+
| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue |
46
| tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) |
57
| tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) |
68
| tst.js:18:1:27:2 | Vue.com ... }\\n\\t}\\n}) |

javascript/ql/test/library-tests/frameworks/Vue/Instance_getAPropertyValue.expected

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue | dataA | single-component-file-1.vue:6:40:6:41 | 42 |
22
| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue | dataA | single-file-component-3-script.js:4:37:4:38 | 42 |
3+
| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | dataA | single-file-component-4.vue:15:14:15:15 | 42 |
4+
| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | message | single-file-component-4.vue:12:23:12:30 | 'Hello!' |
5+
| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue | dataA | single-file-component-5.vue:13:14:13:15 | 42 |
6+
| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue | message | single-file-component-5.vue:10:23:10:30 | 'Hello!' |
37
| tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | dataA | tst.js:8:10:8:11 | 42 |
48
| tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) | dataA | tst.js:14:10:14:11 | 42 |
59
| tst.js:18:1:27:2 | Vue.com ... }\\n\\t}\\n}) | dataA | tst.js:20:10:20:11 | 42 |

javascript/ql/test/library-tests/frameworks/Vue/Instance_getOption.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue | data | single-component-file-1.vue:6:11:6:45 | functio ... 42 } } |
22
| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue | data | single-file-component-3-script.js:4:8:4:42 | functio ... 42 } } |
3+
| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue | render | single-file-component-4.vue:9:13:9:22 | (h) => { } |
34
| tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | data | tst.js:7:8:9:2 | {\\n\\t\\tdataA: 42\\n\\t} |
45
| tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | render | tst.js:4:10:6:2 | functio ... c);\\n\\t} |
56
| tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) | data | tst.js:13:8:15:3 | () => ( ... 42\\n\\t}) |

javascript/ql/test/library-tests/frameworks/Vue/TemplateElement.expected

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@
1010
| single-file-component-3.vue:2:5:7:8 | <p>...</> |
1111
| single-file-component-3.vue:4:1:5:9 | <script>...</> |
1212
| single-file-component-3.vue:6:1:7:8 | <style>...</> |
13+
| single-file-component-4.vue:1:1:3:11 | <template>...</> |
14+
| single-file-component-4.vue:2:5:20:9 | <p>...</> |
15+
| single-file-component-4.vue:4:1:18:9 | <script>...</> |
16+
| single-file-component-4.vue:19:1:20:8 | <style>...</> |
17+
| single-file-component-5.vue:1:1:3:11 | <template>...</> |
18+
| single-file-component-5.vue:2:5:18:9 | <p>...</> |
19+
| single-file-component-5.vue:4:1:16:9 | <script>...</> |
20+
| single-file-component-5.vue:17:1:18:8 | <style>...</> |
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
| single-component-file-1.vue:6:40:6:41 | 42 | single-component-file-1.vue:6:40:6:41 | 42 | single-component-file-1.vue:2:8:2:21 | v-html=dataA |
22
| single-file-component-3-script.js:4:37:4:38 | 42 | single-file-component-3-script.js:4:37:4:38 | 42 | single-file-component-3.vue:2:8:2:21 | v-html=dataA |
3+
| single-file-component-4.vue:15:14:15:15 | 42 | single-file-component-4.vue:15:14:15:15 | 42 | single-file-component-4.vue:2:8:2:21 | v-html=dataA |
4+
| single-file-component-5.vue:13:14:13:15 | 42 | single-file-component-5.vue:13:14:13:15 | 42 | single-file-component-5.vue:2:8:2:21 | v-html=dataA |
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
| single-component-file-1.vue:2:8:2:21 | v-html=dataA |
22
| single-file-component-2.vue:2:8:2:21 | v-html=dataA |
33
| single-file-component-3.vue:2:8:2:21 | v-html=dataA |
4+
| single-file-component-4.vue:2:8:2:21 | v-html=dataA |
5+
| single-file-component-5.vue:2:8:2:21 | v-html=dataA |
46
| tst.js:5:13:5:13 | a |
57
| tst.js:38:12:38:17 | danger |
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<p v-html="dataA"/>
3+
</template>
4+
<script>
5+
import Vue from 'vue'
6+
import Component from 'vue-class-component'
7+
8+
@Component({
9+
render: (h) => { }
10+
})
11+
export default class MyComponent extends Vue {
12+
message: string = 'Hello!'
13+
14+
get dataA() {
15+
return 42;
16+
}
17+
}
18+
</script>
19+
<style>
20+
</style>

0 commit comments

Comments
 (0)