/** * Provides classes for working with xUnit.js tests. */ import javascript /** Holds if an initialization of xUnit.js is detected. */ private predicate xUnitDetected() { // look for `Function.RegisterNamespace("xUnit.js");` exists(MethodCallExpr mc | mc.getParent() instanceof ExprStmt and mc = DataFlow::globalVarRef("Function").getAMemberCall("RegisterNamespace").asExpr() and mc.getNumArgument() = 1 and mc.getArgument(0).getStringValue() = "xUnit.js" ) } /** Holds if `e` looks like an xUnit.js attribute, possibly with arguments. */ private predicate possiblyAttribute(Expr e, string name) { exists(Identifier id | id = e or id = e.(CallExpr).getCallee() | name = id.getName() and name = ["Async", "Data", "Fact", "Fixture", "Import", "ImportJson", "Skip", "Trait"] ) } final private class FinalExpr = Expr; /** * A bracketed list of expressions. * * Depending on their syntactic position, such lists will either be parsed as * array expressions, or as a property index expression where the indexing * expression is a comma expression: for example, in `[a, b][c, d]`, the * list `[a, b]` is an array expression, whereas `[c, d]` is an indexing * expression. * * We also allow singleton lists, as in `[a][b]`. */ abstract private class BracketedListOfExpressions extends FinalExpr { /** Gets the `i`th element expression of this list. */ abstract Expr getElement(int i); /** Gets the first token in this bracketed list of expressions */ Token getFirstToken() { result = Expr.super.getFirstToken() } /** Gets the last token in this bracketed list of expressions */ Token getLastToken() { result = Expr.super.getLastToken() } } /** * An array expression viewed as a bracketed list of expressions. */ private class ArrayExprIsABracketedListOfExpressions extends BracketedListOfExpressions instanceof ArrayExpr { /** Gets the `i`th element of this array literal. */ override Expr getElement(int i) { result = ArrayExpr.super.getElement(i) } } /** * A bracketed list of expressions that appears right after another such list, * and is hence parsed as an index expression. * * Note that the index expression itself does not include the opening and * closing brackets, for which we compensate by overriding `getFirstToken()` * and `getLastToken()`. */ private class IndexExprIndexIsBracketedListOfExpressions extends BracketedListOfExpressions { IndexExprIndexIsBracketedListOfExpressions() { exists(IndexExpr idx, Expr base | base = idx.getBase() and this = idx.getIndex() and // restrict to case where previous expression is also bracketed (base instanceof IndexExpr or base instanceof ArrayExpr) ) } override Expr getElement(int i) { result = this.(SeqExpr).getChildExpr(i) or not this instanceof SeqExpr and i = 0 and result = this } override Token getFirstToken() { // include opening bracket result = BracketedListOfExpressions.super.getFirstToken().getPreviousToken() } override Token getLastToken() { // include closing bracket result = BracketedListOfExpressions.super.getLastToken().getNextToken() } } /** * Holds if `ann` annotates `trg`, possibly skipping over other intervening * annotations. * * We use a token-level definition, since depending on the number of annotations involved * the AST structure can become pretty complicated. */ private predicate annotationTarget(BracketedListOfExpressions ann, XUnitTarget trg) { // every element looks like an attribute forex(Expr e | e = ann.getElement(_) | possiblyAttribute(e, _)) and // followed directly either by a target or by another annotation exists(Token next | next = ann.getLastToken().getNextToken() | trg.getFirstToken() = next or exists(BracketedListOfExpressions ann2 | ann2.getFirstToken() = next | annotationTarget(ann2, trg) ) ) } /** * An xUnit.js annotation, such as `[Fixture]` or `[Data(23)]` annotating * a target declaration or definition. */ class XUnitAnnotation extends Expr { XUnitAnnotation() { xUnitDetected() and annotationTarget(this, _) } /** Gets the declaration or definition to which this annotation belongs. */ XUnitTarget getTarget() { annotationTarget(this, result) } /** Gets the `i`th attribute of this annotation. */ Expr getAttribute(int i) { result = this.(BracketedListOfExpressions).getElement(i) } /** Gets an attribute of this annotation. */ Expr getAnAttribute() { result = this.getAttribute(_) } /** Gets the number of attributes of this annotation. */ int getNumAttribute() { result = strictcount(this.getAnAttribute()) } /** * Holds if this element is at the specified location. * The location spans column `startcolumn` of line `startline` to * column `endcolumn` of line `endline` in file `filepath`. * For more information, see * [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/). */ predicate hasLocationInfo( string filepath, int startline, int startcolumn, int endline, int endcolumn ) { // extend location to cover brackets exists(Location l1, Location l2 | l1 = this.getFirstToken().getLocation() and l2 = this.getLastToken().getLocation() | filepath = l1.getFile().getAbsolutePath() and startline = l1.getStartLine() and startcolumn = l1.getStartColumn() and endline = l2.getEndLine() and endcolumn = l2.getEndColumn() ) } } /** * A declaration or definition that can serve as the target of an xUnit.js * annotation: a function declaration, a variable declaration, or an assignment. */ class XUnitTarget extends Stmt { XUnitTarget() { this instanceof FunctionDeclStmt or this instanceof VarDeclStmt or this.(ExprStmt).getExpr() instanceof AssignExpr } /** Gets an annotation of which this is the target, if any. */ XUnitAnnotation getAnAnnotation() { this = result.getTarget() } } /** * An xUnit.js attribute appearing in an annotation. */ class XUnitAttribute extends Expr { XUnitAttribute() { exists(XUnitAnnotation ann | this = ann.getAnAttribute()) } /** Gets the name of this attribute. */ string getName() { possiblyAttribute(this, result) } /** Gets the `i`th parameter of this attribute. */ Expr getParameter(int i) { result = this.(CallExpr).getArgument(i) } /** Gets a parameter of this attribute. */ Expr getAParameter() { result = this.getParameter(_) } /** Gets the number of parameters of this attribute. */ int getNumParameter() { result = count(this.getAParameter()) } } /** * A function with an xUnit.js annotation. */ private class XUnitAnnotatedFunction extends Function { XUnitAnnotatedFunction() { exists(XUnitTarget target | exists(target.getAnAnnotation()) | this = target or this = target.(VarDeclStmt).getADecl().getInit() or this = target.(ExprStmt).getExpr().(AssignExpr).getRhs() ) } /** Gets an xUnit.js annotation on this function. */ XUnitAnnotation getAnAnnotation() { result = this.(XUnitTarget).getAnAnnotation() or result = this.(Expr).getEnclosingStmt().(XUnitTarget).getAnAnnotation() } } /** * An xUnit.js `Fixture` annotation. */ class XUnitFixtureAnnotation extends XUnitAnnotation { XUnitFixtureAnnotation() { this.getAnAttribute().accessesGlobal("Fixture") } } /** * An xUnit.js fixture. */ class XUnitFixture extends XUnitAnnotatedFunction { XUnitFixture() { this.getAnAnnotation() instanceof XUnitFixtureAnnotation } } /** * An xUnit.js `Fact` annotation. */ class XUnitFactAnnotation extends XUnitAnnotation { XUnitFactAnnotation() { this.getAnAttribute().accessesGlobal("Fact") } } /** * An xUnit.js fact. */ class XUnitFact extends XUnitAnnotatedFunction { XUnitFact() { this.getAnAnnotation() instanceof XUnitFactAnnotation } }