Files
codeql/javascript/ql/lib/semmle/javascript/Functions.qll

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()) }
}