Skip to content

JS: Support value access paths in MaD type columns #16037

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ predicate parseTypeString(string rawType, string package, string qualifiedName)
)
}

/**
* Holds if `rawType` is of form `(package).accessPath`.
*/
bindingset[rawType]
predicate parseValueAccessPath(string rawType, string package, string accessPath) {
exists(string regexp |
regexp = "\\(([^)]+)\\)(.*)" and
package = rawType.regexpCapture(regexp, 1) and
accessPath = rawType.regexpCapture(regexp, 2).regexpReplaceAll("^\\.", "")
)
}

/**
* Holds if models describing `package` may be relevant for the analysis of this database.
*/
Expand All @@ -58,7 +70,7 @@ predicate isPackageUsed(string package) {
bindingset[type]
predicate isTypeUsed(string type) {
exists(string package |
parseTypeString(type, package, _) and
(parseTypeString(type, package, _) or parseValueAccessPath(type, package, _)) and
isPackageUsed(package)
)
}
Expand All @@ -75,14 +87,57 @@ private predicate parseRelevantTypeString(string rawType, string package, string
parseTypeString(rawType, package, qualifiedName)
}

/**
* A string of form `(package).accessPath` appearing in a relevant `type` column.
*
* The `accessPath` is a dot-separated list of property names to be evaluated relative to the
* package's root export object.
*
* Value-access paths are thus interpreted as values and property names. This is in contrast to
* a type string of form `package.type` which refers to the TypeScript type `type` exported from `package`.
* These paths are favored when generating a model from source code, as opposed to generating the
* model from `.d.ts` files.
*
* In common cases `foo.C` will refer to instances of the class `C` from `foo`, whereas `(foo).C` will refer
* to the class `C` itself. However, in general there is no way to know the relationship between values and types
* without having access to the `.d.ts` file. We therefore support both formats when consuming models, and the
* model generator is free to use either (or both) formats, depending on what the model is built from.
*/
private class ValueAccessPathString extends string {
private string package;
private string accessPath;

ValueAccessPathString() {
isRelevantType(this) and
parseValueAccessPath(this, package, accessPath)
}

string getPackageName() { result = package }

string getAccessPath() { result = accessPath }

pragma[nomagic]
string getAccessPathComponent(int n) {
result = accessPath.splitAt(".", n) and not accessPath = ""
}

pragma[nomagic]
int getNumAccessPathComponent() { result = count(int n | exists(this.getAccessPathComponent(n))) }
}

/** Holds if `global` is a global variable referenced via a the `global` package in a CSV row. */
private predicate isRelevantGlobal(string global) {
exists(AccessPath path, AccessPathToken token |
isRelevantFullPath("global", path) and
isRelevantFullPath(["global", "(global)"], path) and
token = path.getToken(0) and
token.getName() = "Member" and
global = token.getAnArgument()
)
or
exists(ValueAccessPathString ap |
ap.getPackageName() = "global" and
global = ap.getAccessPathComponent(0)
)
}

/** An API graph entry point for global variables mentioned in a model. */
Expand Down Expand Up @@ -112,25 +167,48 @@ bindingset[type, path]
API::Node getExtraNodeFromPath(string type, AccessPath path, int n) {
// Global variable accesses is via the 'global' package
exists(AccessPathToken token |
type = "global" and
type = ["global", "(global)"] and
token = path.getToken(0) and
token.getName() = "Member" and
result = getGlobalNode(token.getAnArgument()) and
n = 1
)
}

private API::Node packageNode(string packageName) {
result = API::Internal::getAModuleImportRaw(packageName)
or
result = API::moduleExport(packageName)
}

private API::Node getNodeFromValueAccessPath(ValueAccessPathString ap, int n) {
n = 0 and
result = packageNode(ap.getPackageName())
or
ap.getPackageName() = "global" and
n = 1 and
result = getGlobalNode(ap.getAccessPathComponent(0))
or
result = getNodeFromValueAccessPath(ap, n - 1).getMember(ap.getAccessPathComponent(n - 1))
}

private API::Node getNodeFromValueAccessPath(ValueAccessPathString ap) {
result = getNodeFromValueAccessPath(ap, ap.getNumAccessPathComponent())
}

/** Gets a JavaScript-specific interpretation of the `(package, type)` tuple. */
API::Node getExtraNodeFromType(string type) {
exists(string package, string qualifiedName |
parseRelevantTypeString(type, package, qualifiedName)
|
qualifiedName = "" and
result = [API::moduleImport(package), API::moduleExport(package)]
result = packageNode(package)
or
// Access instance of a type based on type annotations
result = API::Internal::getANodeOfTypeRaw(package, qualifiedName)
)
or
result = getNodeFromValueAccessPath(type)
}

/**
Expand Down
13 changes: 13 additions & 0 deletions javascript/ql/test/library-tests/frameworks/data/test.expected
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ taintFlow
| test.js:249:28:249:35 | source() | test.js:249:28:249:35 | source() |
| test.js:252:15:252:22 | source() | test.js:252:15:252:22 | source() |
| test.js:254:32:254:39 | source() | test.js:254:32:254:39 | source() |
| test.js:261:19:261:26 | source() | test.js:261:19:261:26 | source() |
| test.js:266:22:266:29 | source() | test.js:266:22:266:29 | source() |
| test.js:271:25:271:32 | source() | test.js:271:25:271:32 | source() |
| test.js:273:21:273:28 | source() | test.js:273:21:273:28 | source() |
| test.js:274:21:274:28 | source() | test.js:274:21:274:28 | source() |
| test.js:276:30:276:37 | source() | test.js:276:30:276:37 | source() |
| test.js:278:8:278:23 | window.fqnSource | test.js:278:8:278:23 | window.fqnSource |
isSink
| test.js:54:18:54:25 | source() | test-sink |
| test.js:55:22:55:29 | source() | test-sink |
Expand Down Expand Up @@ -152,6 +159,12 @@ isSink
| test.js:249:28:249:35 | source() | test-sink |
| test.js:252:15:252:22 | source() | test-sink |
| test.js:254:32:254:39 | source() | test-sink |
| test.js:261:19:261:26 | source() | test-sink |
| test.js:266:22:266:29 | source() | test-sink |
| test.js:271:25:271:32 | source() | test-sink |
| test.js:273:21:273:28 | source() | test-sink |
| test.js:274:21:274:28 | source() | test-sink |
| test.js:276:30:276:37 | source() | test-sink |
syntaxErrors
| Member[foo |
| Member[foo] .Member[bar] |
Expand Down
21 changes: 21 additions & 0 deletions javascript/ql/test/library-tests/frameworks/data/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,24 @@ function fuzzy() {
fuzzyCall(source()); // OK - does not come from 'testlib'
require('blah').fuzzyCall(source()); // OK - does not come from 'testlib'
}

function fqn() {
require('fqn1')(source()); // NOT OK
require('fqn1').p1(source()); // OK
require('fqn1').p1.p2(source()); // OK

require('fqn2')(source()); // OK
require('fqn2').p1(source()); // NOT OK
require('fqn2').p1.p2(source()); // OK

require('fqn3')(source()); // OK
require('fqn3').p1(source()); // OK
require('fqn3').p1.p2(source()); // NOT OK

window.fqnGlobal1(source()); // NOT OK
window.fqnGlobal2(source()); // NOT OK
window.fqnGlobal3(source()); // OK
window.fqnGlobal3.p1.p2.p3(source()); // NOT OK

sink(window.fqnSource); // NOT OK
}
23 changes: 23 additions & 0 deletions javascript/ql/test/library-tests/frameworks/data/test.ql
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,26 @@
query predicate syntaxErrors(ApiGraphModels::AccessPath path) { path.hasSyntaxError() }

query predicate warning = ModelOutput::getAWarning/0;

class FqnSinks extends ModelInput::SinkModelCsv {
override predicate row(string row) {
row =
[
"(fqn1);Argument[0];test-sink", //
"(fqn2).p1;Argument[0];test-sink", //
"(fqn3).p1.p2;Argument[0];test-sink", //
"(global);Member[fqnGlobal1].Argument[0];test-sink", //
"(global).fqnGlobal2;Argument[0];test-sink", //
"(global).fqnGlobal3.p1.p2.p3;Argument[0];test-sink", //
]
}
}

class FqnSource extends ModelInput::SourceModelCsv {
override predicate row(string row) {
row =
[
"(global).fqnSource;;test-source", //
]
Comment on lines +150 to +152

Check warning

Code scanning / CodeQL

Singleton set literal Warning test

Singleton set literal can be replaced by its member.
}
}
Loading