mirror of
https://github.com/github/codeql.git
synced 2025-12-20 02:44:30 +01:00
600 lines
19 KiB
Plaintext
600 lines
19 KiB
Plaintext
/** Provides classes for working with functions. */
|
|
|
|
import javascript
|
|
|
|
/**
|
|
* A function as defined either by a function declaration or a function expression.
|
|
*
|
|
* Examples:
|
|
*
|
|
* ```
|
|
* function greet() { // function declaration
|
|
* console.log("Hi");
|
|
* }
|
|
*
|
|
* var greet =
|
|
* function() { // function expression
|
|
* console.log("Hi");
|
|
* };
|
|
*
|
|
* var greet2 =
|
|
* () => console.log("Hi") // arrow function expression
|
|
*
|
|
* var o = {
|
|
* m() { // function expression in a method definition in an object literal
|
|
* return 0;
|
|
* },
|
|
* get x() { // function expression in a getter method definition in an object literal
|
|
* return 1
|
|
* }
|
|
* };
|
|
*
|
|
* class C {
|
|
* m() { // function expression in a method definition in a class
|
|
* return 0;
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
class Function extends @function, Parameterized, TypeParameterized, StmtContainer, Documentable,
|
|
AST::ValueNode
|
|
{
|
|
/** Gets the `i`th parameter of this function. */
|
|
Parameter getParameter(int i) { result = this.getChildExpr(i) }
|
|
|
|
/** Gets the `...rest` parameter, if any. */
|
|
Parameter getRestParameter() {
|
|
result = this.getParameter(this.getNumParameter() - 1) and result.isRestParameter()
|
|
}
|
|
|
|
/** Gets a parameter of this function. */
|
|
override Parameter getAParameter() { result = this.getParameter(_) }
|
|
|
|
/** Gets the parameter named `name` of this function, if any. */
|
|
SimpleParameter getParameterByName(string name) {
|
|
result = this.getAParameter() and
|
|
result.getName() = name
|
|
}
|
|
|
|
/** Gets the `n`th type parameter declared on this function. */
|
|
override TypeParameter getTypeParameter(int i) {
|
|
// Type parameters are at indices -7, -10, -13, ...
|
|
exists(int astIndex | typeexprs(result, _, this, astIndex, _) |
|
|
astIndex <= -7 and astIndex % 4 = -3 and i = -(astIndex + 7) / 4
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the type annotation for the special `this` parameter, if it is present.
|
|
*
|
|
* For example, this would be `void` for the following function:
|
|
* ```
|
|
* function f(this: void, x: number, y: string) {}
|
|
* ```
|
|
*
|
|
* The `this` parameter is not counted as an ordinary parameter.
|
|
* The `x` parameter above is thus considered the first parameter of the function `f`.
|
|
*
|
|
* `this` parameter types are specific to TypeScript.
|
|
*/
|
|
TypeAnnotation getThisTypeAnnotation() {
|
|
result = this.getChildTypeExpr(-4)
|
|
or
|
|
result = this.getDocumentation().getATagByTitle("this").getType()
|
|
}
|
|
|
|
/** Gets the identifier specifying the name of this function, if any. */
|
|
VarDecl getIdentifier() { result = this.getChildExpr(-1) }
|
|
|
|
/**
|
|
* Gets the name of this function if it has one, or a name inferred from its context.
|
|
*
|
|
* For named functions such as `function f() { ... }`, this is just the declared
|
|
* name. For functions assigned to variables or properties (including class
|
|
* members), this is the name of the variable or property. If no meaningful name
|
|
* can be inferred, there is no result.
|
|
*/
|
|
string getName() {
|
|
result = this.getIdentifier().getName()
|
|
or
|
|
not exists(this.getIdentifier()) and
|
|
(
|
|
exists(VarDef vd | this = vd.getSource() | result = vd.getTarget().(VarRef).getName())
|
|
or
|
|
exists(Property p |
|
|
this = p.getInit() and
|
|
result = p.getName()
|
|
)
|
|
or
|
|
exists(AssignExpr assign, PropAccess prop |
|
|
this = assign.getRhs().getUnderlyingValue() and
|
|
prop = assign.getLhs() and
|
|
result = prop.getPropertyName()
|
|
)
|
|
or
|
|
exists(ClassOrInterface c | this = c.getMember(result).getInit())
|
|
)
|
|
}
|
|
|
|
/** Gets the variable holding this function. */
|
|
Variable getVariable() { result = this.getIdentifier().getVariable() }
|
|
|
|
/** Gets the `arguments` variable of this function, if any. */
|
|
ArgumentsVariable getArgumentsVariable() { result.getFunction() = this }
|
|
|
|
/** Holds if the body of this function refers to the function's `arguments` variable. */
|
|
predicate usesArgumentsObject() {
|
|
exists(this.getArgumentsVariable().getAnAccess())
|
|
or
|
|
exists(PropAccess read |
|
|
read.getBase() = this.getVariable().getAnAccess() and
|
|
read.getPropertyName() = "arguments"
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Holds if this function declares a parameter or local variable named `arguments`.
|
|
*/
|
|
predicate declaresArguments() {
|
|
exists(this.getScope().getVariable("arguments").getADeclaration())
|
|
}
|
|
|
|
/** Gets the statement enclosing this function, if any. */
|
|
Stmt getEnclosingStmt() { none() }
|
|
|
|
/** Gets the body of this function. */
|
|
override ExprOrStmt getBody() { result = this.getChild(-2) }
|
|
|
|
/** Gets the `i`th statement in the body of this function. */
|
|
Stmt getBodyStmt(int i) { result = this.getBody().(BlockStmt).getStmt(i) }
|
|
|
|
/** Gets a statement in the body of this function. */
|
|
Stmt getABodyStmt() { result = this.getBodyStmt(_) }
|
|
|
|
/** Gets the number of statements in the body of this function. */
|
|
int getNumBodyStmt() { result = count(this.getABodyStmt()) }
|
|
|
|
/** Gets the return type annotation on this function, if any. */
|
|
TypeAnnotation getReturnTypeAnnotation() {
|
|
typeexprs(result, _, this, -3, _)
|
|
or
|
|
exists(string title | title = "return" or title = "returns" |
|
|
result = this.getDocumentation().getATagByTitle(title).getType()
|
|
)
|
|
}
|
|
|
|
/** Holds if this function is a generator function. */
|
|
predicate isGenerator() {
|
|
is_generator(this)
|
|
or
|
|
// we also support `yield` in non-generator functions
|
|
exists(YieldExpr yield | this = yield.getEnclosingFunction())
|
|
}
|
|
|
|
/** Holds if the last parameter of this function is a rest parameter. */
|
|
predicate hasRestParameter() { has_rest_parameter(this) }
|
|
|
|
/**
|
|
* Gets the last token of this function's parameter list, not including
|
|
* the closing parenthesis, if any.
|
|
*/
|
|
private Token lastTokenOfParameterList() {
|
|
// if there are any parameters, it's the last token of the last parameter
|
|
exists(Parameter lastParam | lastParam = this.getParameter(this.getNumParameter() - 1) |
|
|
result = lastParam.getDefault().getLastToken()
|
|
or
|
|
not exists(lastParam.getDefault()) and result = lastParam.getLastToken()
|
|
)
|
|
or
|
|
// otherwise we have an empty parameter list `()`, and
|
|
// we want to return the opening parenthesis
|
|
not exists(this.getAParameter()) and
|
|
(
|
|
// if the function has a name, the opening parenthesis comes right after it
|
|
result = this.getIdentifier().getLastToken().getNextToken()
|
|
or
|
|
// otherwise this must be an arrow function with no parameters, so the opening
|
|
// parenthesis is the very first token of the function
|
|
not exists(this.getIdentifier()) and result = this.getFirstToken()
|
|
)
|
|
}
|
|
|
|
/** Holds if the parameter list of this function has a trailing comma. */
|
|
predicate hasTrailingComma() { this.lastTokenOfParameterList().getNextToken().getValue() = "," }
|
|
|
|
/** Holds if this function is an asynchronous function. */
|
|
predicate isAsync() { is_async(this) }
|
|
|
|
/** Holds if this function is asynchronous or a generator. */
|
|
predicate isAsyncOrGenerator() { this.isAsync() or this.isGenerator() }
|
|
|
|
/** Gets the enclosing function or toplevel of this function. */
|
|
override StmtContainer getEnclosingContainer() { result = this.getEnclosingStmt().getContainer() }
|
|
|
|
/** Gets the number of lines in this function. */
|
|
int getNumberOfLines() {
|
|
exists(int sl, int el | this.getLocation().hasLocationInfo(_, sl, _, el, _) |
|
|
result = el - sl + 1
|
|
)
|
|
}
|
|
|
|
/** Gets the number of lines containing code in this function. */
|
|
int getNumberOfLinesOfCode() { result = LinesOfCode::getNumCodeLines(this) }
|
|
|
|
/** Gets the number of lines containing comments in this function. */
|
|
int getNumberOfLinesOfComments() { result = LinesOfComments::getNumCommentLines(this) }
|
|
|
|
/** Gets the cyclomatic complexity of this function. */
|
|
int getCyclomaticComplexity() {
|
|
result =
|
|
2 +
|
|
sum(Expr nd |
|
|
nd.getContainer() = this and nd.isBranch()
|
|
|
|
|
strictcount(nd.getASuccessor()) - 1
|
|
)
|
|
}
|
|
|
|
override predicate isStrict() {
|
|
// check for explicit strict mode directive
|
|
exists(Directive::StrictModeDecl smd | this = smd.getContainer()) or
|
|
// check for enclosing strict function
|
|
StmtContainer.super.isStrict() or
|
|
// all parts of a class definition are strict code
|
|
this.getParent*() = any(ClassDefinition cd).getSuperClass() or
|
|
this = any(MethodDeclaration md).getBody()
|
|
}
|
|
|
|
/** Gets a return statement in the body of this function, if any. */
|
|
ReturnStmt getAReturnStmt() { result.getContainer() = this }
|
|
|
|
/** Gets an expression that could be returned by this function, if any. */
|
|
Expr getAReturnedExpr() {
|
|
result = this.getBody() or
|
|
result = this.getAReturnStmt().getExpr()
|
|
}
|
|
|
|
/**
|
|
* Gets the function whose `this` binding a `this` expression in this function refers to,
|
|
* which is the nearest enclosing non-arrow function.
|
|
*/
|
|
Function getThisBinder() { result = this }
|
|
|
|
/**
|
|
* Gets the function or top-level whose `this` binding a `this` expression in this function refers to,
|
|
* which is the nearest enclosing non-arrow function or top-level.
|
|
*/
|
|
StmtContainer getThisBindingContainer() {
|
|
result = this.getThisBinder()
|
|
or
|
|
not exists(this.getThisBinder()) and
|
|
result = this.getTopLevel()
|
|
}
|
|
|
|
/**
|
|
* Holds if this function has a mapped `arguments` variable whose indices are aliased
|
|
* with the function's parameters.
|
|
*
|
|
* A function has a mapped `arguments` variable if it is non-strict, and has no rest
|
|
* parameter, no parameter default values, and no destructuring parameters.
|
|
*/
|
|
predicate hasMappedArgumentsVariable() {
|
|
exists(this.getArgumentsVariable()) and
|
|
not this.isStrict() and
|
|
forall(Parameter p | p = this.getAParameter() |
|
|
p instanceof SimpleParameter and not exists(p.getDefault())
|
|
) and
|
|
not this.hasRestParameter()
|
|
}
|
|
|
|
/**
|
|
* Gets a description of this function.
|
|
*
|
|
* For named functions such as `function f() { ... }`, this is just the declared
|
|
* name. For functions assigned to variables or properties (including class
|
|
* members), this is the name of the variable or property. If no meaningful name
|
|
* can be inferred, the result is "anonymous function".
|
|
*/
|
|
override string describe() {
|
|
if exists(this.inferNameFromVarDef())
|
|
then result = this.inferNameFromVarDef()
|
|
else
|
|
if exists(this.inferNameFromProp())
|
|
then result = this.inferNameFromProp()
|
|
else
|
|
if exists(this.inferNameFromMemberDef())
|
|
then result = this.inferNameFromMemberDef()
|
|
else
|
|
if exists(this.inferNameFromCallSig())
|
|
then result = this.inferNameFromCallSig()
|
|
else
|
|
if exists(this.inferNameFromIndexSig())
|
|
then result = this.inferNameFromIndexSig()
|
|
else
|
|
if exists(this.inferNameFromFunctionType())
|
|
then result = this.inferNameFromFunctionType()
|
|
else result = "anonymous function"
|
|
}
|
|
|
|
/**
|
|
* Gets a description of this function, based on its declared name or the name
|
|
* of the variable it is assigned to, if any.
|
|
*/
|
|
private string inferNameFromVarDef() {
|
|
// in ambiguous cases like `var f = function g() {}`, prefer `g` to `f`
|
|
if exists(this.getIdentifier())
|
|
then result = "function " + this.getIdentifier().getName()
|
|
else
|
|
exists(VarDef vd | this = vd.getSource() |
|
|
result = "function " + vd.getTarget().(VarRef).getName()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a description of this function, based on the name of the property
|
|
* it is assigned to, if any.
|
|
*/
|
|
private string inferNameFromProp() {
|
|
exists(Property p, string n | this = p.getInit() and n = p.getName() |
|
|
p instanceof PropertyGetter and result = "getter for property " + n
|
|
or
|
|
p instanceof PropertySetter and result = "setter for property " + n
|
|
or
|
|
p instanceof ValueProperty and result = "method " + n
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a description of this function, based on the name of the class
|
|
* member it is assigned to, if any.
|
|
*/
|
|
private string inferNameFromMemberDef() {
|
|
exists(ClassOrInterface c, string n, MemberDeclaration m, string classpp |
|
|
m = c.getMember(n) and this = m.getInit() and classpp = c.describe()
|
|
|
|
|
if m instanceof ConstructorDeclaration
|
|
then
|
|
if m.(ConstructorDeclaration).isSynthetic()
|
|
then result = "default constructor of " + classpp
|
|
else result = "constructor of " + classpp
|
|
else
|
|
if m instanceof GetterMethodDeclaration
|
|
then result = "getter method for property " + n + " of " + classpp
|
|
else
|
|
if m instanceof SetterMethodDeclaration
|
|
then result = "setter method for property " + n + " of " + classpp
|
|
else result = "method " + n + " of " + classpp
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a description of this function if it is part of a call signature.
|
|
*/
|
|
private string inferNameFromCallSig() {
|
|
exists(InterfaceDefinition c, CallSignature sig |
|
|
sig = c.getACallSignature() and
|
|
sig.getBody() = this and
|
|
if sig instanceof FunctionCallSignature
|
|
then result = "call signature of " + c.describe()
|
|
else result = "construct signature of " + c.describe()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a description of this function if it is part of a call signature.
|
|
*/
|
|
private string inferNameFromIndexSig() {
|
|
exists(InterfaceDefinition c | c.getAnIndexSignature().getBody() = this |
|
|
result = "index signature of " + c.describe()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a description of this function if it is part of a function type.
|
|
*/
|
|
private string inferNameFromFunctionType() {
|
|
exists(FunctionTypeExpr type | type.getFunction() = this | result = "anonymous function type")
|
|
}
|
|
|
|
/**
|
|
* Holds if this function has a body.
|
|
*
|
|
* A TypeScript function has no body if it is ambient, abstract, or an overload signature.
|
|
*
|
|
* A JavaScript function always has a body.
|
|
*/
|
|
predicate hasBody() { exists(this.getBody()) }
|
|
|
|
/**
|
|
* Holds if this function is part of an abstract class member.
|
|
*/
|
|
predicate isAbstract() { exists(MethodDeclaration md | this = md.getBody() | md.isAbstract()) }
|
|
|
|
/**
|
|
* Holds if this function cannot be invoked using `new` because it
|
|
* is of the given `kind`.
|
|
*/
|
|
predicate isNonConstructible(string kind) {
|
|
this instanceof Method and
|
|
not this instanceof Constructor and
|
|
kind = "a method"
|
|
or
|
|
this instanceof ArrowFunctionExpr and
|
|
kind = "an arrow function"
|
|
or
|
|
this.isGenerator() and
|
|
kind = "a generator function"
|
|
or
|
|
this.isAsync() and
|
|
kind = "an async function"
|
|
}
|
|
|
|
/**
|
|
* Gets the canonical name for this function, as determined by the TypeScript compiler.
|
|
*
|
|
* This predicate is only populated for files extracted with full TypeScript extraction.
|
|
*/
|
|
CanonicalFunctionName getCanonicalName() { ast_node_symbol(this, result) }
|
|
|
|
/**
|
|
* Gets the call signature of this function, as determined by the TypeScript compiler, if any.
|
|
*/
|
|
CallSignatureType getCallSignature() { declared_function_signature(this, result) }
|
|
}
|
|
|
|
/**
|
|
* Provides predicates for computing lines-of-code information for functions.
|
|
*/
|
|
private module LinesOfCode {
|
|
/**
|
|
* Holds if `tk` is interesting for the purposes of counting lines of code, that is, it might
|
|
* contribute a line of code that isn't covered by any other token.
|
|
*
|
|
* A token is interesting if it is the first token on its line, or if it spans multiple lines.
|
|
*/
|
|
private predicate isInteresting(Token tk) {
|
|
exists(int sl, int el | tk.getLocation().hasLocationInfo(_, sl, _, el, _) |
|
|
not tk.getPreviousToken().getLocation().getEndLine() = sl
|
|
or
|
|
sl != el
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the `i`th token in toplevel `tl`, but only if it is interesting.
|
|
*/
|
|
pragma[noinline]
|
|
private Token getInterestingToken(TopLevel tl, int i) {
|
|
result.getTopLevel() = tl and
|
|
result.getIndex() = i and
|
|
isInteresting(result)
|
|
}
|
|
|
|
/**
|
|
* Holds if `f` covers tokens between indices `start` and `end` (inclusive) in toplevel `tl`.
|
|
*/
|
|
predicate tokenRange(Function f, TopLevel tl, int start, int end) {
|
|
tl = f.getTopLevel() and
|
|
start = f.getFirstToken().getIndex() and
|
|
end = f.getLastToken().getIndex()
|
|
}
|
|
|
|
/**
|
|
* Gets an interesting token belonging to `f`.
|
|
*/
|
|
private Token getAnInterestingToken(Function f) {
|
|
result = f.getFirstToken()
|
|
or
|
|
exists(TopLevel tl, int start, int end | tokenRange(f, tl, start, end) |
|
|
result = getInterestingToken(tl, [start .. end])
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the line number of a line covered by `f` that contains at least one token.
|
|
*/
|
|
private int getACodeLine(Function f) {
|
|
exists(Location loc | loc = getAnInterestingToken(f).getLocation() |
|
|
result in [loc.getStartLine() .. loc.getEndLine()]
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the number of lines of code of `f`.
|
|
*
|
|
* Note the special handling of empty locations; this is needed to correctly deal with
|
|
* synthetic constructors.
|
|
*/
|
|
int getNumCodeLines(Function f) {
|
|
if f.getLocation().isEmpty() then result = 0 else result = count(getACodeLine(f))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides predicates for computing lines-of-comments information for functions.
|
|
*/
|
|
private module LinesOfComments {
|
|
/**
|
|
* Holds if `tk` is interesting for the purposes of counting comments, that is,
|
|
* if it is preceded by a comment.
|
|
*/
|
|
private predicate isInteresting(Token tk) { exists(Comment c | tk = c.getNextToken()) }
|
|
|
|
/**
|
|
* Gets the `i`th token in `tl`, if it is interesting.
|
|
*/
|
|
pragma[noinline]
|
|
private Token getToken(TopLevel tl, int i) {
|
|
result.getTopLevel() = tl and
|
|
result.getIndex() = i and
|
|
isInteresting(result)
|
|
}
|
|
|
|
/**
|
|
* Gets a comment inside function `f`.
|
|
*/
|
|
private Comment getAComment(Function f) {
|
|
exists(TopLevel tl, int start, int end | LinesOfCode::tokenRange(f, tl, start, end) |
|
|
result.getNextToken() = getToken(tl, [start + 1 .. end])
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a line covered by `f` on which at least one comment appears.
|
|
*/
|
|
private int getACommentLine(Function f) {
|
|
exists(Location loc | loc = getAComment(f).getLocation() |
|
|
result in [loc.getStartLine() .. loc.getEndLine()]
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the number of lines with at least one comment in `f`.
|
|
*/
|
|
int getNumCommentLines(Function f) { result = count(getACommentLine(f)) }
|
|
}
|
|
|
|
/**
|
|
* A method defined in a class or object expression.
|
|
*
|
|
* Examples:
|
|
*
|
|
* ```
|
|
* var o = {
|
|
* m() { // method defined in an object expression
|
|
* return 0;
|
|
* }
|
|
* };
|
|
*
|
|
* class C {
|
|
* m() { // method defined in a class
|
|
* return 0;
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
class Method extends FunctionExpr {
|
|
Method() {
|
|
exists(MethodDeclaration md | this = md.getBody())
|
|
or
|
|
exists(ValueProperty p | p.isMethod() | this = p.getInit())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A constructor defined in a class.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```
|
|
* class Point {
|
|
* constructor(x, y) { // constructor
|
|
* this.x = x;
|
|
* this.y = y;
|
|
* }
|
|
* }
|
|
*/
|
|
class Constructor extends FunctionExpr {
|
|
Constructor() { exists(ConstructorDeclaration cd | this = cd.getBody()) }
|
|
}
|