mirror of
https://github.com/github/codeql.git
synced 2026-04-25 08:45:14 +02:00
Merge pull request #15380 from asgerf/js/endpoint-naming
JS: Add library for naming endpoints
This commit is contained in:
@@ -594,6 +594,9 @@ module API {
|
||||
exportedName = "" and
|
||||
result = getAModuleImportRaw(moduleName)
|
||||
}
|
||||
|
||||
/** Gets a sink node that represents instances of `cls`. */
|
||||
Node getClassInstance(DataFlow::ClassNode cls) { result = Impl::MkClassInstance(cls) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1621,6 +1624,7 @@ private predicate exports(string m, DataFlow::Node rhs) {
|
||||
exists(Module mod | mod = importableModule(m) |
|
||||
rhs = mod.(AmdModule).getDefine().getModuleExpr().flow()
|
||||
or
|
||||
not mod.(ES2015Module).hasBothNamedAndDefaultExports() and
|
||||
exports(m, "default", rhs)
|
||||
or
|
||||
exists(ExportAssignDeclaration assgn | assgn.getTopLevel() = mod |
|
||||
@@ -1634,6 +1638,7 @@ private predicate exports(string m, DataFlow::Node rhs) {
|
||||
/** Holds if module `m` exports `rhs` under the name `prop`. */
|
||||
private predicate exports(string m, string prop, DataFlow::Node rhs) {
|
||||
exists(ExportDeclaration exp | exp.getEnclosingModule() = importableModule(m) |
|
||||
not exp.isTypeOnly() and
|
||||
rhs = exp.getSourceNode(prop)
|
||||
or
|
||||
exists(Variable v |
|
||||
|
||||
@@ -516,16 +516,37 @@ class MemberDeclaration extends @property, Documentable {
|
||||
*/
|
||||
predicate hasPublicKeyword() { has_public_keyword(this) }
|
||||
|
||||
/**
|
||||
* Holds if this member is considered private.
|
||||
*
|
||||
* This may occur in two cases:
|
||||
* - it is a TypeScript member annotated with the `private` keyword, or
|
||||
* - the member has a private name, such as `#foo`, referring to a private field in the class
|
||||
*/
|
||||
predicate isPrivate() { this.hasPrivateKeyword() or this.hasPrivateFieldName() }
|
||||
|
||||
/**
|
||||
* Holds if this is a TypeScript member annotated with the `private` keyword.
|
||||
*/
|
||||
predicate isPrivate() { has_private_keyword(this) }
|
||||
predicate hasPrivateKeyword() { has_private_keyword(this) }
|
||||
|
||||
/**
|
||||
* Holds if this is a TypeScript member annotated with the `protected` keyword.
|
||||
*/
|
||||
predicate isProtected() { has_protected_keyword(this) }
|
||||
|
||||
/**
|
||||
* Holds if the member has a private name, such as `#foo`, referring to a private field in the class.
|
||||
*
|
||||
* For example:
|
||||
* ```js
|
||||
* class Foo {
|
||||
* #method() {}
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
predicate hasPrivateFieldName() { this.getNameExpr().(Label).getName().charAt(0) = "#" }
|
||||
|
||||
/**
|
||||
* Gets the expression specifying the name of this member,
|
||||
* or nothing if this is a call signature.
|
||||
|
||||
@@ -39,6 +39,20 @@ class ES2015Module extends Module {
|
||||
// modules are implicitly strict
|
||||
any()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this module contains both named and `default` exports.
|
||||
*
|
||||
* This is used to determine whether a default-import of the module should be reinterpreted
|
||||
* as a namespace-import, to accommodate the non-standard behavior implemented by some compilers.
|
||||
*
|
||||
* When a module has both named and `default` exports, the non-standard interpretation can lead to
|
||||
* ambiguities, so we only allow the standard interpretation in that case.
|
||||
*/
|
||||
predicate hasBothNamedAndDefaultExports() {
|
||||
hasNamedExports(this) and
|
||||
hasDefaultExport(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,17 +78,6 @@ private predicate hasDefaultExport(ES2015Module mod) {
|
||||
mod.getAnExport().(ExportNamedDeclaration).getASpecifier().getExportedName() = "default"
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `mod` contains both named and `default` exports.
|
||||
*
|
||||
* This is used to determine whether a default-import of the module should be reinterpreted
|
||||
* as a namespace-import, to accommodate the non-standard behavior implemented by some compilers.
|
||||
*/
|
||||
private predicate hasBothNamedAndDefaultExports(ES2015Module mod) {
|
||||
hasNamedExports(mod) and
|
||||
hasDefaultExport(mod)
|
||||
}
|
||||
|
||||
/**
|
||||
* An import declaration.
|
||||
*
|
||||
@@ -131,7 +134,7 @@ class ImportDeclaration extends Stmt, Import, @import_declaration {
|
||||
// For compatibility with the non-standard implementation of default imports,
|
||||
// treat default imports as namespace imports in cases where it can't cause ambiguity
|
||||
// between named exports and the properties of a default-exported object.
|
||||
not hasBothNamedAndDefaultExports(this.getImportedModule()) and
|
||||
not this.getImportedModule().(ES2015Module).hasBothNamedAndDefaultExports() and
|
||||
is.getImportedName() = "default"
|
||||
)
|
||||
or
|
||||
|
||||
@@ -29,7 +29,8 @@ class PackageJson extends JsonObject {
|
||||
parentDir.getAChildContainer+() = currentDir and
|
||||
pkgNameDiff = currentDir.getAbsolutePath().suffix(parentDir.getAbsolutePath().length()) and
|
||||
not exists(pkgNameDiff.indexOf("/node_modules/")) and
|
||||
result = parentPkgName + pkgNameDiff
|
||||
result = parentPkgName + pkgNameDiff and
|
||||
not parentPkg.isPrivate()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
497
javascript/ql/lib/semmle/javascript/endpoints/EndpointNaming.qll
Normal file
497
javascript/ql/lib/semmle/javascript/endpoints/EndpointNaming.qll
Normal file
@@ -0,0 +1,497 @@
|
||||
/**
|
||||
* Provides predicates for generating names for classes and functions that are part
|
||||
* of the public API of a library.
|
||||
*
|
||||
* When possible, we try to use the qualified name by which a class/function can be accessed
|
||||
* from client code.
|
||||
*
|
||||
* However, there are cases where classes and functions can be exposed to client
|
||||
* code without being accessible as a qualified name. For example;
|
||||
* ```js
|
||||
* // 'Foo' is internal, but clients can call its methods, e.g. `getFoo().m()`
|
||||
* class Foo {
|
||||
* m() {}
|
||||
* }
|
||||
* export function getFoo() {
|
||||
* return new Foo();
|
||||
* }
|
||||
*
|
||||
* // Clients can call m() via getObj().m()
|
||||
* export function getObj() {
|
||||
* return {
|
||||
* m() {}
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* In these cases, we try to make up human-readable names for the endpoints.
|
||||
* We make an effort to make these unambiguous in practice, though this is not always guaranteed.
|
||||
*/
|
||||
|
||||
private import javascript
|
||||
|
||||
/** Concatenates two access paths. */
|
||||
bindingset[x, y]
|
||||
private string join(string x, string y) {
|
||||
if x = "" or y = "" then result = x + y else result = x + "." + y
|
||||
}
|
||||
|
||||
private predicate isPackageExport(API::Node node) { node = API::moduleExport(_) }
|
||||
|
||||
private predicate memberEdge(API::Node pred, API::Node succ) { succ = pred.getAMember() }
|
||||
|
||||
/** Gets the shortest distance from a packaeg export to `nd` in the API graph. */
|
||||
private int distanceFromPackageExport(API::Node nd) =
|
||||
shortestDistances(isPackageExport/1, memberEdge/2)(_, nd, result)
|
||||
|
||||
private predicate isExported(API::Node node) {
|
||||
isPackageExport(node)
|
||||
or
|
||||
exists(API::Node pred |
|
||||
isExported(pred) and
|
||||
memberEdge(pred, node) and
|
||||
not isPrivateLike(node)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `node` is a default export that can be reinterpreted as a namespace export,
|
||||
* because the enclosing module has no named exports.
|
||||
*/
|
||||
private predicate defaultExportCanBeInterpretedAsNamespaceExport(API::Node node) {
|
||||
exists(ES2015Module mod |
|
||||
node.asSink() = mod.getAnExportedValue("default") and
|
||||
not mod.hasBothNamedAndDefaultExports()
|
||||
)
|
||||
}
|
||||
|
||||
private predicate isPrivateAssignment(DataFlow::Node node) {
|
||||
exists(MemberDeclaration decl |
|
||||
node = decl.getInit().flow() and
|
||||
decl.isPrivate()
|
||||
)
|
||||
or
|
||||
exists(DataFlow::PropWrite write |
|
||||
write.isPrivateField() and
|
||||
node = write.getRhs()
|
||||
)
|
||||
}
|
||||
|
||||
private predicate isPrivateLike(API::Node node) { isPrivateAssignment(node.asSink()) }
|
||||
|
||||
private API::Node getASuccessor(API::Node node, string name, int badness) {
|
||||
isExported(node) and
|
||||
exists(string member |
|
||||
result = node.getMember(member) and
|
||||
if member = "default"
|
||||
then
|
||||
if defaultExportCanBeInterpretedAsNamespaceExport(node)
|
||||
then (
|
||||
badness = 5 and name = ""
|
||||
) else (
|
||||
badness = 10 and name = "default"
|
||||
)
|
||||
else (
|
||||
name = member and badness = 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private API::Node getAPredecessor(API::Node node, string name, int badness) {
|
||||
node = getASuccessor(result, name, badness)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the predecessor of `node` to use when constructing a qualified name for it,
|
||||
* and binds `name` and `badness` corresponding to the label on that edge.
|
||||
*/
|
||||
private API::Node getPreferredPredecessor(API::Node node, string name, int badness) {
|
||||
// For root nodes, we prefer not having a predecessor, as we use the package name.
|
||||
not isPackageExport(node) and
|
||||
// Rank predecessors by name-badness, export-distance, and name.
|
||||
// Since min() can only return a single value, we need a separate min() call per column.
|
||||
badness =
|
||||
min(API::Node pred, int b |
|
||||
pred = getAPredecessor(node, _, b) and
|
||||
// ensure the preferred predecessor is strictly closer to a root export, even if it means accepting more badness
|
||||
distanceFromPackageExport(pred) < distanceFromPackageExport(node)
|
||||
|
|
||||
b
|
||||
) and
|
||||
result =
|
||||
min(API::Node pred, string name1 |
|
||||
pred = getAPredecessor(node, name1, badness)
|
||||
|
|
||||
pred order by distanceFromPackageExport(pred), name1
|
||||
) and
|
||||
name = min(string n | result = getAPredecessor(node, n, badness) | n)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(package, name)` is a potential name to associate with `sink`.
|
||||
*
|
||||
* `badness` is bound to the associated badness of the name.
|
||||
*/
|
||||
private predicate sinkHasNameCandidate(API::Node sink, string package, string name, int badness) {
|
||||
sink = API::moduleExport(package) and
|
||||
name = "" and
|
||||
badness = 0
|
||||
or
|
||||
exists(API::Node baseNode, string baseName, int baseBadness, string step, int stepBadness |
|
||||
sinkHasNameCandidate(baseNode, package, baseName, baseBadness) and
|
||||
baseNode = getPreferredPredecessor(sink, step, stepBadness) and
|
||||
badness = baseBadness + stepBadness and
|
||||
name = join(baseName, step)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(package, name)` is the primary name to associate with `sink`.
|
||||
*
|
||||
* `badness` is bound to the associated badness of the name.
|
||||
*/
|
||||
private predicate sinkHasPrimaryName(API::Node sink, string package, string name, int badness) {
|
||||
badness = min(int b | sinkHasNameCandidate(sink, _, _, b) | b) and
|
||||
package = min(string p | sinkHasNameCandidate(sink, p, _, badness) | p) and
|
||||
name = min(string n | sinkHasNameCandidate(sink, package, n, badness) | n order by n.length(), n)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(package, name)` is the primary name to associate with `node`.
|
||||
*/
|
||||
predicate sinkHasPrimaryName(API::Node sink, string package, string name) {
|
||||
sinkHasPrimaryName(sink, package, name, _)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(package, name)` is an alias for `node`.
|
||||
*
|
||||
* This means it is a valid name for it, but was not chosen as the primary name.
|
||||
*/
|
||||
private predicate sinkHasAlias(API::Node sink, string package, string name) {
|
||||
not sinkHasPrimaryName(sink, package, name) and
|
||||
(
|
||||
exists(string baseName, string step |
|
||||
sinkHasPrimaryName(getAPredecessor(sink, step, _), package, baseName) and
|
||||
name = join(baseName, step)
|
||||
)
|
||||
or
|
||||
sink = API::moduleExport(package) and
|
||||
name = ""
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets a sink node reachable from `node`. */
|
||||
bindingset[node]
|
||||
private API::Node getASinkNode(DataFlow::SourceNode node) { result.getAValueReachingSink() = node }
|
||||
|
||||
/**
|
||||
* Holds if `node` is a declaration in an externs file.
|
||||
*
|
||||
* This is to ensure that functions/classes in externs are not named after a re-export in a package.
|
||||
*/
|
||||
private predicate nameFromExterns(DataFlow::Node node, string package, string name, int badness) {
|
||||
node.getTopLevel().isExterns() and
|
||||
package = "global" and
|
||||
node = AccessPath::getAnAssignmentTo(name) and
|
||||
badness = -10
|
||||
}
|
||||
|
||||
bindingset[qualifiedName]
|
||||
private int getBadnessOfClassName(string qualifiedName) {
|
||||
if qualifiedName.matches("%.constructor")
|
||||
then result = 10
|
||||
else
|
||||
if qualifiedName = ""
|
||||
then result = 5
|
||||
else result = 0
|
||||
}
|
||||
|
||||
/** Holds if `(package, name)` is a potential name for `cls`, with the given `badness`. */
|
||||
private predicate classObjectHasNameCandidate(
|
||||
DataFlow::ClassNode cls, string package, string name, int badness
|
||||
) {
|
||||
// There can be multiple API nodes associated with `cls`.
|
||||
// For example:
|
||||
///
|
||||
// class C {}
|
||||
// module.exports.A = C; // first sink
|
||||
// module.exports.B = C; // second sink
|
||||
//
|
||||
exists(int baseBadness |
|
||||
sinkHasPrimaryName(getASinkNode(cls), package, name, baseBadness) and
|
||||
badness = baseBadness + getBadnessOfClassName(name)
|
||||
)
|
||||
or
|
||||
nameFromExterns(cls, package, name, badness)
|
||||
}
|
||||
|
||||
private predicate classObjectHasPrimaryName(
|
||||
DataFlow::ClassNode cls, string package, string name, int badness
|
||||
) {
|
||||
badness = min(int b | classObjectHasNameCandidate(cls, _, _, b) | b) and
|
||||
package = min(string p | classObjectHasNameCandidate(cls, p, _, badness) | p) and
|
||||
name = min(string n | classObjectHasNameCandidate(cls, package, n, badness) | n)
|
||||
}
|
||||
|
||||
/** Holds if `(package, name)` is the primary name for the class object of `cls`. */
|
||||
predicate classObjectHasPrimaryName(DataFlow::ClassNode cls, string package, string name) {
|
||||
classObjectHasPrimaryName(cls, package, name, _)
|
||||
}
|
||||
|
||||
/** Holds if an instance of `cls` can be exposed to client code. */
|
||||
private predicate hasEscapingInstance(DataFlow::ClassNode cls) {
|
||||
cls.getAnInstanceReference().flowsTo(any(API::Node n).asSink())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(package, name)` is a potential name to use for instances of `cls`, with the given `badness`.
|
||||
*/
|
||||
private predicate classInstanceHasNameCandidate(
|
||||
DataFlow::ClassNode cls, string package, string name, int badness
|
||||
) {
|
||||
exists(string baseName |
|
||||
classObjectHasPrimaryName(cls, package, baseName, badness) and
|
||||
name = join(baseName, "prototype")
|
||||
)
|
||||
or
|
||||
// In case the class itself is unaccessible, but an instance is exposed via an access path,
|
||||
// consider using that access path. For example:
|
||||
//
|
||||
// class InternalClass {}
|
||||
// module.exports.foo = new InternalClass();
|
||||
//
|
||||
exists(int baseBadness |
|
||||
sinkHasPrimaryName(getASinkNode(cls.getAnInstanceReference()), package, name, baseBadness) and
|
||||
badness = baseBadness + 30 // add penalty, as we prefer to base this on the class name
|
||||
)
|
||||
or
|
||||
// If neither the class nor its instances are accessible via an access path, but instances of the
|
||||
// class can still escape via more complex access patterns, resort to a synthesized name.
|
||||
// For example:
|
||||
//
|
||||
// class InternalClass {}
|
||||
// function foo() {
|
||||
// return new InternalClass();
|
||||
// }
|
||||
//
|
||||
hasEscapingInstance(cls) and
|
||||
exists(string baseName |
|
||||
InternalModuleNaming::fallbackModuleName(cls.getTopLevel(), package, baseName, badness - 100) and
|
||||
name = join(baseName, cls.getName()) + ".prototype"
|
||||
)
|
||||
}
|
||||
|
||||
private predicate classInstanceHasPrimaryName(
|
||||
DataFlow::ClassNode cls, string package, string name, int badness
|
||||
) {
|
||||
badness = min(int b | classInstanceHasNameCandidate(cls, _, _, b) | b) and
|
||||
package = min(string p | classInstanceHasNameCandidate(cls, p, _, badness) | p) and
|
||||
name =
|
||||
min(string n |
|
||||
classInstanceHasNameCandidate(cls, package, n, badness)
|
||||
|
|
||||
n order by n.length(), n
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if `(package, name)` is the primary name to use for instances of `cls`. */
|
||||
predicate classInstanceHasPrimaryName(DataFlow::ClassNode cls, string package, string name) {
|
||||
classInstanceHasPrimaryName(cls, package, name, _)
|
||||
}
|
||||
|
||||
/** Holds if `(package, name)` is an alias referring to some instance of `cls`. */
|
||||
predicate classInstanceHasAlias(DataFlow::ClassNode cls, string package, string name) {
|
||||
not classInstanceHasPrimaryName(cls, package, name) and
|
||||
exists(int badness |
|
||||
classInstanceHasNameCandidate(cls, package, name, badness) and
|
||||
badness < 100 // Badness 100 is when we start to synthesize names. Do not suggest these as aliases.
|
||||
)
|
||||
}
|
||||
|
||||
private predicate functionHasNameCandidate(
|
||||
DataFlow::FunctionNode function, string package, string name, int badness
|
||||
) {
|
||||
sinkHasPrimaryName(getASinkNode(function), package, name, badness)
|
||||
or
|
||||
exists(DataFlow::ClassNode cls |
|
||||
function = cls.getConstructor() and
|
||||
classObjectHasPrimaryName(cls, package, name, badness)
|
||||
or
|
||||
exists(string baseName, string memberName |
|
||||
function = cls.getInstanceMethod(memberName) and
|
||||
classInstanceHasPrimaryName(cls, package, baseName, badness) and
|
||||
name = join(baseName, memberName)
|
||||
or
|
||||
function = cls.getStaticMethod(memberName) and
|
||||
classObjectHasPrimaryName(cls, package, baseName, badness) and
|
||||
name = join(baseName, memberName)
|
||||
)
|
||||
)
|
||||
or
|
||||
nameFromExterns(function, package, name, badness)
|
||||
}
|
||||
|
||||
private predicate functionHasPrimaryName(
|
||||
DataFlow::FunctionNode function, string package, string name, int badness
|
||||
) {
|
||||
badness = min(int b | functionHasNameCandidate(function, _, _, b) | b) and
|
||||
package = min(string p | functionHasNameCandidate(function, p, _, badness) | p) and
|
||||
name =
|
||||
min(string n |
|
||||
functionHasNameCandidate(function, package, n, badness)
|
||||
|
|
||||
n order by n.length(), n
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(package, name)` is the primary name for the given `function`.
|
||||
*/
|
||||
predicate functionHasPrimaryName(DataFlow::FunctionNode function, string package, string name) {
|
||||
functionHasPrimaryName(function, package, name, _)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `(aliasPackage, aliasName)` is an alias for `(primaryPackage, primaryName)`,
|
||||
* defined at `aliasDef`.
|
||||
*
|
||||
* Only the last component of an access path is reported as an alias, the prefix always
|
||||
* uses the primary name for that access path. The aliases for the prefix are reported
|
||||
* as separate tuples.
|
||||
*
|
||||
* For example, we might report that `a.b.C` is an alias for `a.b.c`, and that `a.B` is an alias for `a.b`.
|
||||
* By combining the two aliasing facts, we may conclude that `a.B.C` is an alias for `a.b.c`, but this fact is not
|
||||
* reported separately.
|
||||
*/
|
||||
predicate aliasDefinition(
|
||||
string primaryPackage, string primaryName, string aliasPackage, string aliasName,
|
||||
API::Node aliasDef
|
||||
) {
|
||||
exists(DataFlow::SourceNode source |
|
||||
classObjectHasPrimaryName(source, primaryPackage, primaryName)
|
||||
or
|
||||
functionHasPrimaryName(source, primaryPackage, primaryName)
|
||||
|
|
||||
aliasDef.getAValueReachingSink() = source and
|
||||
sinkHasPrimaryName(aliasDef, aliasPackage, aliasName, _) and
|
||||
not (
|
||||
primaryPackage = aliasPackage and
|
||||
primaryName = aliasName
|
||||
)
|
||||
)
|
||||
or
|
||||
sinkHasPrimaryName(aliasDef, primaryPackage, primaryName) and
|
||||
sinkHasAlias(aliasDef, aliasPackage, aliasName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a `(package, name)` pair to a string of form `(package).name`.
|
||||
*/
|
||||
bindingset[package, name]
|
||||
string renderName(string package, string name) { result = join("(" + package + ")", name) }
|
||||
|
||||
/**
|
||||
* Contains predicates for naming individual modules (i.e. files) inside of a package.
|
||||
*
|
||||
* These names are not necessarily part of a package's public API, and so we only used them
|
||||
* as a fallback when a publicly-accessible access path cannot be found.
|
||||
*/
|
||||
private module InternalModuleNaming {
|
||||
/** Gets the path to `folder` relative to its enclosing non-private `package.json` file. */
|
||||
private string getPackageRelativePathFromFolder(Folder folder) {
|
||||
exists(PackageJson json |
|
||||
json.getFile() = folder.getFile("package.json") and
|
||||
not json.isPrivate() and
|
||||
result = json.getPackageName()
|
||||
)
|
||||
or
|
||||
not exists(folder.getFile("package.json")) and
|
||||
result =
|
||||
getPackageRelativePathFromFolder(folder.getParentContainer()) + "/" + folder.getBaseName()
|
||||
}
|
||||
|
||||
private string getPackageRelativePath(Module mod) {
|
||||
exists(PackageJson json, string relativePath |
|
||||
not json.isPrivate() and
|
||||
json.getExportedModule(relativePath) = mod and
|
||||
if relativePath = "."
|
||||
then result = json.getPackageName()
|
||||
else result = json.getPackageName() + "/" + relativePath.regexpReplaceAll("^\\./", "")
|
||||
)
|
||||
or
|
||||
not mod = any(PackageJson json | not json.isPrivate()).getExportedModule(_) and
|
||||
not mod.isAmbient() and
|
||||
exists(string folderPath |
|
||||
folderPath = getPackageRelativePathFromFolder(mod.getFile().getParentContainer()) and
|
||||
if mod.getName() = "index"
|
||||
then result = folderPath
|
||||
else result = folderPath + "/" + mod.getName()
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if `(package, name)` should be used to refer to code inside `mod`. */
|
||||
predicate fallbackModuleName(Module mod, string package, string name, int badness) {
|
||||
sinkHasPrimaryName(getASinkNode(mod.getDefaultOrBulkExport()), package, name, badness)
|
||||
or
|
||||
badness = 50 and
|
||||
package = getPackageRelativePath(mod) and
|
||||
name = ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains query predicates for emitting debugging information about endpoint naming.
|
||||
*/
|
||||
module Debug {
|
||||
/** Holds if `node` has multiple preferred predecessors. */
|
||||
query predicate ambiguousPreferredPredecessor(API::Node node) {
|
||||
strictcount(API::Node pred, string name, int badness |
|
||||
pred = getPreferredPredecessor(node, name, badness)
|
||||
) > 1
|
||||
}
|
||||
|
||||
/** Holds if the given `node` has multiple primary names. */
|
||||
query string ambiguousSinkName(API::Node node) {
|
||||
strictcount(string package, string name | sinkHasPrimaryName(node, package, name)) > 1 and
|
||||
result =
|
||||
concat(string package, string name |
|
||||
sinkHasPrimaryName(node, package, name)
|
||||
|
|
||||
renderName(package, name), ", "
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if the given `node` has multiple primary names. */
|
||||
query string ambiguousClassObjectName(DataFlow::ClassNode node) {
|
||||
strictcount(string package, string name | classObjectHasPrimaryName(node, package, name)) > 1 and
|
||||
result =
|
||||
concat(string package, string name |
|
||||
classObjectHasPrimaryName(node, package, name)
|
||||
|
|
||||
renderName(package, name), ", "
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if the given `node` has multiple primary names. */
|
||||
query string ambiguousClassInstanceName(DataFlow::ClassNode node) {
|
||||
strictcount(string package, string name | classInstanceHasPrimaryName(node, package, name)) > 1 and
|
||||
result =
|
||||
concat(string package, string name |
|
||||
classInstanceHasPrimaryName(node, package, name)
|
||||
|
|
||||
renderName(package, name), ", "
|
||||
)
|
||||
}
|
||||
|
||||
/** Holds if the given `node` has multiple primary names. */
|
||||
query string ambiguousFunctionName(DataFlow::FunctionNode node) {
|
||||
strictcount(string package, string name | functionHasPrimaryName(node, package, name)) > 1 and
|
||||
result =
|
||||
concat(string package, string name |
|
||||
functionHasPrimaryName(node, package, name)
|
||||
|
|
||||
renderName(package, name), ", "
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
testFailures
|
||||
ambiguousPreferredPredecessor
|
||||
ambiguousSinkName
|
||||
ambiguousClassObjectName
|
||||
ambiguousClassInstanceName
|
||||
ambiguousFunctionName
|
||||
failures
|
||||
@@ -0,0 +1,47 @@
|
||||
import javascript
|
||||
import semmle.javascript.RestrictedLocations
|
||||
import semmle.javascript.Lines
|
||||
import semmle.javascript.endpoints.EndpointNaming as EndpointNaming
|
||||
import testUtilities.InlineExpectationsTest
|
||||
import EndpointNaming::Debug
|
||||
|
||||
module TestConfig implements TestSig {
|
||||
string getARelevantTag() { result = ["instance", "class", "method", "alias"] }
|
||||
|
||||
predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(string package, string name |
|
||||
element = "" and
|
||||
value = EndpointNaming::renderName(package, name)
|
||||
|
|
||||
exists(DataFlow::ClassNode cls | location = cls.getAstNode().getLocation() |
|
||||
tag = "class" and
|
||||
EndpointNaming::classObjectHasPrimaryName(cls, package, name)
|
||||
or
|
||||
tag = "instance" and
|
||||
EndpointNaming::classInstanceHasPrimaryName(cls, package, name)
|
||||
)
|
||||
or
|
||||
exists(DataFlow::FunctionNode function |
|
||||
not function.getFunction() = any(ConstructorDeclaration decl | decl.isSynthetic()).getBody() and
|
||||
location = function.getFunction().getLocation() and
|
||||
tag = "method" and
|
||||
EndpointNaming::functionHasPrimaryName(function, package, name)
|
||||
)
|
||||
)
|
||||
or
|
||||
element = "" and
|
||||
tag = "alias" and
|
||||
exists(
|
||||
API::Node aliasDef, string primaryPackage, string primaryName, string aliasPackage,
|
||||
string aliasName
|
||||
|
|
||||
EndpointNaming::aliasDefinition(primaryPackage, primaryName, aliasPackage, aliasName, aliasDef) and
|
||||
value =
|
||||
EndpointNaming::renderName(aliasPackage, aliasName) + "==" +
|
||||
EndpointNaming::renderName(primaryPackage, primaryName) and
|
||||
location = aliasDef.asSink().asExpr().getLocation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
import MakeTest<TestConfig>
|
||||
@@ -0,0 +1,13 @@
|
||||
export class PublicClass {} // $ class=(pack1).PublicClass instance=(pack1).PublicClass.prototype
|
||||
|
||||
class PrivateClass {}
|
||||
|
||||
export const ExportedConst = class ExportedConstClass {} // $ class=(pack1).ExportedConst instance=(pack1).ExportedConst.prototype
|
||||
|
||||
class ClassWithEscapingInstance {} // $ instance=(pack1).ClassWithEscapingInstance.prototype
|
||||
|
||||
export function getEscapingInstance() {
|
||||
return new ClassWithEscapingInstance();
|
||||
} // $ method=(pack1).getEscapingInstance
|
||||
|
||||
export function publicFunction() {} // $ method=(pack1).publicFunction
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack1",
|
||||
"main": "./main.js"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default class FooClass {} // $ class=(pack10).Foo instance=(pack10).Foo.prototype
|
||||
@@ -0,0 +1,3 @@
|
||||
import { default as Foo } from "./foo";
|
||||
|
||||
export { Foo }
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack10",
|
||||
"main": "./index.js"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
class AmbiguousClass {
|
||||
instanceMethod(foo) {} // $ method=(pack2).lib.LibClass.prototype.instanceMethod
|
||||
} // $ class=(pack2).lib.LibClass instance=(pack2).lib.LibClass.prototype
|
||||
|
||||
export default AmbiguousClass; // $ alias=(pack2).lib.default==(pack2).lib.LibClass
|
||||
export { AmbiguousClass as LibClass }
|
||||
@@ -0,0 +1,9 @@
|
||||
class AmbiguousClass {
|
||||
instanceMethod() {} // $ method=(pack2).MainClass.prototype.instanceMethod
|
||||
} // $ class=(pack2).MainClass instance=(pack2).MainClass.prototype
|
||||
|
||||
export default AmbiguousClass; // $ alias=(pack2).default==(pack2).MainClass
|
||||
export { AmbiguousClass as MainClass }
|
||||
|
||||
import * as lib from "./lib";
|
||||
export { lib }
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack2",
|
||||
"main": "./main.js"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default function(x,y,z) {} // $ method=(pack3).libFunction alias=(pack3).libFunction.default==(pack3).libFunction
|
||||
@@ -0,0 +1,7 @@
|
||||
function ambiguousFunction(x, y, z) {} // $ method=(pack3).namedFunction
|
||||
|
||||
export default ambiguousFunction; // $ alias=(pack3).default==(pack3).namedFunction
|
||||
export { ambiguousFunction as namedFunction };
|
||||
|
||||
import libFunction from "./lib";
|
||||
export { libFunction };
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack3",
|
||||
"main": "./main.js"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default class C {} // $ class=(pack4) instance=(pack4).prototype
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack4",
|
||||
"main": "./index.js"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack5",
|
||||
"main": "./dist/index.js"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default class C {} // $ class=(pack5) instance=(pack5).prototype
|
||||
@@ -0,0 +1,6 @@
|
||||
class C {
|
||||
instanceMethod() {} // $ method=(pack6).instanceMethod
|
||||
static staticMethod() {} // not accessible
|
||||
} // $ instance=(pack6)
|
||||
|
||||
export default new C();
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack6",
|
||||
"main": "./index.js"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class D {} // $ class=(pack7).D instance=(pack7).D.prototype
|
||||
|
||||
// In this case we are forced to include ".default" to avoid ambiguity with class D above.
|
||||
export default {
|
||||
D: class {} // $ class=(pack7).default.D instance=(pack7).default.D.prototype
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack7",
|
||||
"main": "./index.js"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
class Foo {} // $ class=(pack8).Foo instance=(pack8).Foo.prototype
|
||||
|
||||
module.exports = Foo;
|
||||
module.exports.default = Foo; // $ alias=(pack8).Foo.default==(pack8).Foo
|
||||
module.exports.Foo = Foo; // $ alias=(pack8).Foo.Foo==(pack8).Foo
|
||||
@@ -0,0 +1,5 @@
|
||||
class Main {} // $ class=(pack8) instance=(pack8).prototype
|
||||
|
||||
Main.Foo = require('./foo');
|
||||
|
||||
module.exports = Main;
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack8",
|
||||
"main": "./index.js"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export class Foo {} // $ instance=(pack9/foo).Foo.prototype
|
||||
@@ -0,0 +1,9 @@
|
||||
// Only the type is exposed. For the time being we do not consider type-only declarations or .d.ts files
|
||||
// when naming classes.
|
||||
export type { Foo } from "./foo";
|
||||
|
||||
import * as foo from "./foo";
|
||||
|
||||
export function expose() {
|
||||
return new foo.Foo(); // expose an instance of Foo but not the class
|
||||
} // $ method=(pack9).expose
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "pack9",
|
||||
"main": "./index.js"
|
||||
}
|
||||
@@ -3,6 +3,7 @@ groups: [javascript, test]
|
||||
dependencies:
|
||||
codeql/javascript-all: ${workspace}
|
||||
codeql/javascript-queries: ${workspace}
|
||||
codeql/util: ${workspace}
|
||||
extractor: javascript
|
||||
tests: .
|
||||
warnOnImplicitThis: true
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Inline expectation tests for JS.
|
||||
* See `shared/util/codeql/util/test/InlineExpectationsTest.qll`
|
||||
*/
|
||||
|
||||
private import codeql.util.test.InlineExpectationsTest
|
||||
private import internal.InlineExpectationsTestImpl
|
||||
import Make<Impl>
|
||||
@@ -0,0 +1,12 @@
|
||||
private import javascript as JS
|
||||
private import codeql.util.test.InlineExpectationsTest
|
||||
|
||||
module Impl implements InlineExpectationsTestSig {
|
||||
private import javascript
|
||||
|
||||
class ExpectationComment extends LineComment {
|
||||
string getContents() { result = this.getText() }
|
||||
}
|
||||
|
||||
class Location = JS::Location;
|
||||
}
|
||||
Reference in New Issue
Block a user