mirror of
https://github.com/github/codeql.git
synced 2026-04-30 03:05:15 +02:00
Merge pull request #3833 from asger-semmle/js/vue-class-component
Approved by erik-krogh
This commit is contained in:
@@ -726,6 +726,18 @@ DataFlow::SourceNode moduleMember(string path, string m) {
|
||||
*/
|
||||
class MemberKind extends string {
|
||||
MemberKind() { this = "method" or this = "getter" or this = "setter" }
|
||||
|
||||
/** Holds if this is the `method` kind. */
|
||||
predicate isMethod() { this = MemberKind::method() }
|
||||
|
||||
/** Holds if this is the `getter` kind. */
|
||||
predicate isGetter() { this = MemberKind::getter() }
|
||||
|
||||
/** Holds if this is the `setter` kind. */
|
||||
predicate isSetter() { this = MemberKind::setter() }
|
||||
|
||||
/** Holds if this is the `getter` or `setter` kind. */
|
||||
predicate isAccessor() { this = MemberKind::accessor() }
|
||||
}
|
||||
|
||||
module MemberKind {
|
||||
|
||||
@@ -30,6 +30,54 @@ module Vue {
|
||||
MkComponent(DataFlow::CallNode def) { def = vue().getAMemberCall("component") } or
|
||||
MkSingleFileComponent(VueFile file)
|
||||
|
||||
/** Gets the name of a lifecycle hook method. */
|
||||
private string lifecycleHookName() {
|
||||
result =
|
||||
["beforeCreate", "created", "beforeMount", "mounted", "beforeUpdate", "updated", "activated",
|
||||
"deactivated", "beforeDestroy", "destroyed", "errorCaptured"]
|
||||
}
|
||||
|
||||
/** Gets a value that can be used as a `@Component` decorator. */
|
||||
private DataFlow::SourceNode componentDecorator() {
|
||||
result = DataFlow::moduleImport("vue-class-component")
|
||||
or
|
||||
result = DataFlow::moduleMember("vue-property-decorator", "Component")
|
||||
}
|
||||
|
||||
/**
|
||||
* A class with a `@Component` decorator, making it usable as an "options" object in Vue.
|
||||
*/
|
||||
private class ClassComponent extends DataFlow::ClassNode {
|
||||
DataFlow::Node decorator;
|
||||
|
||||
ClassComponent() {
|
||||
exists(ClassDefinition cls |
|
||||
this = cls.flow() and
|
||||
cls.getADecorator().getExpression() = decorator.asExpr() and
|
||||
(
|
||||
componentDecorator().flowsTo(decorator)
|
||||
or
|
||||
componentDecorator().getACall() = decorator
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an option passed to the `@Component` decorator.
|
||||
*
|
||||
* These options correspond to the options one would pass to `new Vue({...})` or similar.
|
||||
*/
|
||||
DataFlow::Node getDecoratorOption(string name) {
|
||||
result = decorator.(DataFlow::CallNode).getOptionArgument(0, name)
|
||||
}
|
||||
}
|
||||
|
||||
private string memberKindVerb(DataFlow::MemberKind kind) {
|
||||
kind = DataFlow::MemberKind::getter() and result = "get"
|
||||
or
|
||||
kind = DataFlow::MemberKind::setter() and result = "set"
|
||||
}
|
||||
|
||||
/**
|
||||
* A Vue instance definition.
|
||||
*
|
||||
@@ -65,11 +113,27 @@ module Vue {
|
||||
endcolumn = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the options passed to the Vue object, such as the object literal `{...}` in `new Vue{{...})`
|
||||
* or the default export of a single-file component.
|
||||
*/
|
||||
abstract DataFlow::Node getOwnOptionsObject();
|
||||
|
||||
/**
|
||||
* Gets the class component implementing this Vue instance, if any.
|
||||
*
|
||||
* Specifically, this is a class annotated with `@Component` which flows to the options
|
||||
* object of this Vue instance.
|
||||
*/
|
||||
ClassComponent getAsClassComponent() { result.flowsTo(getOwnOptionsObject()) }
|
||||
|
||||
/**
|
||||
* Gets the node for option `name` for this instance, this does not include
|
||||
* those from extended objects and mixins.
|
||||
*/
|
||||
abstract DataFlow::Node getOwnOption(string name);
|
||||
DataFlow::Node getOwnOption(string name) {
|
||||
result = getOwnOptionsObject().getALocalSource().getAPropertyWrite(name).getRhs()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the node for option `name` for this instance, including those from
|
||||
@@ -92,6 +156,8 @@ module Vue {
|
||||
mixin.flowsTo(mixins.getAnElement()) and
|
||||
result = mixin.getAPropertyWrite(name).getRhs()
|
||||
)
|
||||
or
|
||||
result = getAsClassComponent().getDecoratorOption(name)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,6 +178,10 @@ module Vue {
|
||||
result = f.getAReturn()
|
||||
)
|
||||
)
|
||||
or
|
||||
result = getAsClassComponent().getAReceiverNode()
|
||||
or
|
||||
result = getAsClassComponent().getInstanceMethod("data").getAReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +192,11 @@ module Vue {
|
||||
/**
|
||||
* Gets the node for the `render` option of this instance.
|
||||
*/
|
||||
DataFlow::Node getRender() { result = getOption("render") }
|
||||
DataFlow::Node getRender() {
|
||||
result = getOption("render")
|
||||
or
|
||||
result = getAsClassComponent().getInstanceMethod("render")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the node for the `methods` option of this instance.
|
||||
@@ -143,41 +217,50 @@ module Vue {
|
||||
methods.flowsTo(getMethods()) and
|
||||
result = methods.getAPropertyWrite().getRhs()
|
||||
)
|
||||
or
|
||||
result = getAsClassComponent().getAnInstanceMethod() and
|
||||
not result = getAsClassComponent().getInstanceMethod([lifecycleHookName(), "render", "data"])
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node for a member of the `computed` option of this instance that matches `kind` ("get" or "set").
|
||||
* Gets a node for a member of the `computed` option of this instance that matches `kind`.
|
||||
*/
|
||||
pragma[noinline]
|
||||
private DataFlow::Node getAnAccessor(string kind) {
|
||||
private DataFlow::Node getAnAccessor(DataFlow::MemberKind kind) {
|
||||
exists(DataFlow::ObjectLiteralNode computedObj, DataFlow::Node accessorObjOrGetter |
|
||||
computedObj.flowsTo(getComputed()) and
|
||||
computedObj.getAPropertyWrite().getRhs() = accessorObjOrGetter
|
||||
|
|
||||
result = accessorObjOrGetter and kind = "get"
|
||||
result = accessorObjOrGetter and kind = DataFlow::MemberKind::getter()
|
||||
or
|
||||
exists(DataFlow::ObjectLiteralNode accessorObj |
|
||||
accessorObj.flowsTo(accessorObjOrGetter) and
|
||||
result = accessorObj.getAPropertyWrite(kind).getRhs()
|
||||
result = accessorObj.getAPropertyWrite(memberKindVerb(kind)).getRhs()
|
||||
)
|
||||
)
|
||||
or
|
||||
result = getAsClassComponent().getAnInstanceMember(kind) and
|
||||
kind.isAccessor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node for a member `name` of the `computed` option of this instance that matches `kind` ("get" or "set").
|
||||
* Gets a node for a member `name` of the `computed` option of this instance that matches `kind`.
|
||||
*/
|
||||
private DataFlow::Node getAccessor(string name, string kind) {
|
||||
private DataFlow::Node getAccessor(string name, DataFlow::MemberKind kind) {
|
||||
exists(DataFlow::ObjectLiteralNode computedObj, DataFlow::SourceNode accessorObjOrGetter |
|
||||
computedObj.flowsTo(getComputed()) and
|
||||
accessorObjOrGetter.flowsTo(computedObj.getAPropertyWrite(name).getRhs())
|
||||
|
|
||||
result = accessorObjOrGetter and kind = "get"
|
||||
result = accessorObjOrGetter and kind = DataFlow::MemberKind::getter()
|
||||
or
|
||||
exists(DataFlow::ObjectLiteralNode accessorObj |
|
||||
accessorObj.flowsTo(accessorObjOrGetter) and
|
||||
result = accessorObj.getAPropertyWrite(kind).getRhs()
|
||||
result = accessorObj.getAPropertyWrite(memberKindVerb(kind)).getRhs()
|
||||
)
|
||||
)
|
||||
or
|
||||
result = getAsClassComponent().getInstanceMember(name, kind) and
|
||||
kind.isAccessor()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,20 +268,12 @@ module Vue {
|
||||
*/
|
||||
pragma[noinline]
|
||||
private DataFlow::Node getALifecycleHook(string hookName) {
|
||||
hookName = lifecycleHookName() and
|
||||
(
|
||||
hookName = "beforeCreate" or
|
||||
hookName = "created" or
|
||||
hookName = "beforeMount" or
|
||||
hookName = "mounted" or
|
||||
hookName = "beforeUpdate" or
|
||||
hookName = "updated" or
|
||||
hookName = "activated" or
|
||||
hookName = "deactivated" or
|
||||
hookName = "beforeDestroy" or
|
||||
hookName = "destroyed" or
|
||||
hookName = "errorCaptured"
|
||||
) and
|
||||
result = getOption(hookName)
|
||||
result = getOption(hookName)
|
||||
or
|
||||
result = getAsClassComponent().getInstanceMethod(hookName)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +302,7 @@ module Vue {
|
||||
)
|
||||
or
|
||||
exists(DataFlow::FunctionNode getter |
|
||||
getter.flowsTo(getAccessor(name, "get")) and
|
||||
getter.flowsTo(getAccessor(name, DataFlow::MemberKind::getter())) and
|
||||
result = getter.getAReturn()
|
||||
)
|
||||
}
|
||||
@@ -249,7 +324,7 @@ module Vue {
|
||||
def.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOption(string name) { result = def.getOptionArgument(0, name) }
|
||||
override DataFlow::Node getOwnOptionsObject() { result = def.getArgument(0) }
|
||||
|
||||
override Template::Element getTemplateElement() { none() }
|
||||
}
|
||||
@@ -270,7 +345,7 @@ module Vue {
|
||||
extend.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOption(string name) { result = extend.getOptionArgument(0, name) }
|
||||
override DataFlow::Node getOwnOptionsObject() { result = extend.getArgument(0) }
|
||||
|
||||
override Template::Element getTemplateElement() { none() }
|
||||
}
|
||||
@@ -292,7 +367,7 @@ module Vue {
|
||||
sub.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOption(string name) { result = sub.getOptionArgument(0, name) }
|
||||
override DataFlow::Node getOwnOptionsObject() { result = sub.getArgument(0) }
|
||||
|
||||
override DataFlow::Node getOption(string name) {
|
||||
result = Instance.super.getOption(name)
|
||||
@@ -319,7 +394,7 @@ module Vue {
|
||||
def.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOption(string name) { result = def.getOptionArgument(1, name) }
|
||||
override DataFlow::Node getOwnOptionsObject() { result = def.getArgument(1) }
|
||||
|
||||
override Template::Element getTemplateElement() { none() }
|
||||
}
|
||||
@@ -357,6 +432,13 @@ module Vue {
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOptionsObject() {
|
||||
exists(ExportDefaultDeclaration decl |
|
||||
decl.getTopLevel() = getModule() and
|
||||
result = DataFlow::valueNode(decl.getOperand())
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getOwnOption(string name) {
|
||||
// The options of a single file component are defined by the exported object of the script element.
|
||||
// Our current module model does not support reads on this object very well, so we use custom steps for the common cases for now.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
| single-component-file-1.vue:0:0:0:0 | single-component-file-1.vue |
|
||||
| single-file-component-2.vue:0:0:0:0 | single-file-component-2.vue |
|
||||
| single-file-component-3.vue:0:0:0:0 | single-file-component-3.vue |
|
||||
| single-file-component-4.vue:0:0:0:0 | single-file-component-4.vue |
|
||||
| single-file-component-5.vue:0:0:0:0 | single-file-component-5.vue |
|
||||
| tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) |
|
||||
| tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) |
|
||||
| tst.js:18:1:27:2 | Vue.com ... }\\n\\t}\\n}) |
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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!' |
|
||||
| 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 |
|
||||
| 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!' |
|
||||
| tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | dataA | tst.js:8:10:8:11 | 42 |
|
||||
| tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) | dataA | tst.js:14:10:14:11 | 42 |
|
||||
| tst.js:18:1:27:2 | Vue.com ... }\\n\\t}\\n}) | dataA | tst.js:20:10:20:11 | 42 |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
| 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 } } |
|
||||
| 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 } } |
|
||||
| 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) => { } |
|
||||
| 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} |
|
||||
| tst.js:3:1:10:2 | new Vue ... 2\\n\\t}\\n}) | render | tst.js:4:10:6:2 | functio ... c);\\n\\t} |
|
||||
| tst.js:12:1:16:2 | new Vue ... \\t}),\\n}) | data | tst.js:13:8:15:3 | () => ( ... 42\\n\\t}) |
|
||||
|
||||
@@ -10,3 +10,11 @@
|
||||
| single-file-component-3.vue:2:5:7:8 | <p>...</> |
|
||||
| single-file-component-3.vue:4:1:5:9 | <script>...</> |
|
||||
| single-file-component-3.vue:6:1:7:8 | <style>...</> |
|
||||
| single-file-component-4.vue:1:1:3:11 | <template>...</> |
|
||||
| single-file-component-4.vue:2:5:20:9 | <p>...</> |
|
||||
| single-file-component-4.vue:4:1:18:9 | <script>...</> |
|
||||
| single-file-component-4.vue:19:1:20:8 | <style>...</> |
|
||||
| single-file-component-5.vue:1:1:3:11 | <template>...</> |
|
||||
| single-file-component-5.vue:2:5:18:9 | <p>...</> |
|
||||
| single-file-component-5.vue:4:1:16:9 | <script>...</> |
|
||||
| single-file-component-5.vue:17:1:18:8 | <style>...</> |
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
| single-component-file-1.vue:2:8:2:21 | v-html=dataA |
|
||||
| single-file-component-2.vue:2:8:2:21 | v-html=dataA |
|
||||
| single-file-component-3.vue:2:8:2:21 | v-html=dataA |
|
||||
| single-file-component-4.vue:2:8:2:21 | v-html=dataA |
|
||||
| single-file-component-5.vue:2:8:2:21 | v-html=dataA |
|
||||
| tst.js:5:13:5:13 | a |
|
||||
| tst.js:38:12:38:17 | danger |
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<p v-html="dataA"/>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import Component from 'vue-class-component'
|
||||
|
||||
@Component({
|
||||
render: (h) => { }
|
||||
})
|
||||
export default class MyComponent extends Vue {
|
||||
message: string = 'Hello!'
|
||||
|
||||
get dataA() {
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<p v-html="dataA"/>
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import Component from 'vue-class-component'
|
||||
|
||||
@Component
|
||||
export default class MyComponent extends Vue {
|
||||
message: string = 'Hello!'
|
||||
|
||||
get dataA() {
|
||||
return 42;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user