From 7fccdecd909238d21755b47fd187f0adb331e7b6 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 6 Mar 2026 13:29:05 +0100 Subject: [PATCH 1/2] Create CallGraphForComparison.ql --- .../src/meta/alerts/CallGraphForComparison.ql | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 javascript/ql/src/meta/alerts/CallGraphForComparison.ql diff --git a/javascript/ql/src/meta/alerts/CallGraphForComparison.ql b/javascript/ql/src/meta/alerts/CallGraphForComparison.ql new file mode 100644 index 000000000000..f828e6ed3612 --- /dev/null +++ b/javascript/ql/src/meta/alerts/CallGraphForComparison.ql @@ -0,0 +1,20 @@ +/** + * @name Call graph for comparison + * @description An edge in the call graph. + * @kind problem + * @problem.severity recommendation + * @id js/meta/alerts/call-graph-for-comparison + * @tags meta + * @precision very-low + */ + +import javascript +private import semmle.javascript.dataflow.internal.DataFlowPrivate + +from InvokeExpr invoke, Function f, string kind +where + viableCallable(any(DataFlowCall call | call.asOrdinaryCall().getInvokeExpr() = invoke)) + .asSourceCallable() = f and + kind = "Call" and + not f.getTopLevel().isExterns() +select invoke, kind + " to $@", f, f.toString() From 613173a07cf3bb097525c99cabe60fe103c7444f Mon Sep 17 00:00:00 2001 From: Asger F Date: Wed, 25 Feb 2026 09:56:28 +0100 Subject: [PATCH 2/2] JS: Squash and rebase unified language lib work --- .../ql/lib/ext/module-systems.model.yml | 10 + .../ql/lib/ide-contextual-queries/printDfg.ql | 35 + javascript/ql/lib/qlpack.yml | 1 + javascript/ql/lib/semmle/javascript/Expr.qll | 9 +- .../semmle/javascript/internal/Overlay.qll | 1 - .../javascript/internal/unified/Constant.qll | 61 + .../internal/unified/DataFlowSteps.qll | 858 ++++ .../javascript/internal/unified/JSUnified.qll | 453 ++ .../internal/unified/NamespaceObjects.qll | 103 + .../internal/unified/PathResolution.qll | 256 ++ .../internal/unified/Transforms.qll | 143 + .../internal/unified/minimal/AST.qll | 468 ++ .../unified/minimal/BasicBlockInternal.qll | 263 ++ .../internal/unified/minimal/BasicBlocks.qll | 6 + .../internal/unified/minimal/CFG.qll | 423 ++ .../internal/unified/minimal/Classes.qll | 1303 ++++++ .../internal/unified/minimal/Comments.qll | 137 + .../unified/minimal/ES2015Modules.qll | 665 +++ .../internal/unified/minimal/Expr.qll | 2784 ++++++++++++ .../internal/unified/minimal/FileSystem.qll | 3 + .../internal/unified/minimal/Files.qll | 173 + .../internal/unified/minimal/Functions.qll | 585 +++ .../internal/unified/minimal/JSDoc.qll | 623 +++ .../internal/unified/minimal/JSON.qll | 164 + .../internal/unified/minimal/JSPaths.qll | 70 + .../internal/unified/minimal/JSX.qll | 258 ++ .../internal/unified/minimal/Lines.qll | 52 + .../internal/unified/minimal/Locations.qll | 160 + .../internal/unified/minimal/NPM.qll | 414 ++ .../minimal/NodeModuleResolutionImpl.qll | 42 + .../internal/unified/minimal/Overlay.qll | 1 + .../unified/minimal/PackageJsonEx.qll | 151 + .../unified/minimal/PathConcatenation.qll | 39 + .../internal/unified/minimal/PathMapping.qll | 31 + .../internal/unified/minimal/Paths.qll | 91 + .../internal/unified/minimal/Stmt.qll | 1185 +++++ .../unified/minimal/StmtContainers.qll | 49 + .../internal/unified/minimal/TSConfig.qll | 222 + .../internal/unified/minimal/Templates.qll | 106 + .../internal/unified/minimal/Tokens.qll | 142 + .../unified/minimal/TypeAnnotations.qll | 89 + .../internal/unified/minimal/TypeScript.qll | 1655 +++++++ .../internal/unified/minimal/Util.qll | 24 + .../internal/unified/minimal/Variables.qll | 998 +++++ .../internal/unified/minimal/XML.qll | 71 + .../internal/unified/minimal/YAML.qll | 56 + .../internal/unified/minimal/minimal.qll | 30 + .../lib/utils/test/InlineFlowTestUnified.qll | 25 + .../utils/test/InlineFlowTestUnifiedUtil.qll | 28 + .../UnifiedInlineExpectationsTestImpl.qll | 25 + javascript/ql/src/meta/alerts/CallGraph.ql | 2 +- .../src/meta/alerts/CallGraphForComparison.ql | 19 +- .../src/meta/alerts/FullCallGraphBaseline.ql | 20 + .../src/meta/alerts/FullCallGraphUnified.ql | 25 + .../FlowSummary/DataFlowConsistency.ql | 5 +- .../UnifiedDataFlow/accessors.js | 136 + .../UnifiedDataFlow/array-with.js | 25 + .../library-tests/UnifiedDataFlow/arrays.js | 46 + .../UnifiedDataFlow/arrow-function.js | 14 + .../UnifiedDataFlow/assumed-receiver.js | 41 + .../augmented-library-object.js | 19 + .../library-tests/UnifiedDataFlow/buffer.js | 7 + .../UnifiedDataFlow/callbacks.js | 81 + .../UnifiedDataFlow/class-flow.js | 28 + .../base-class-def.js | 5 + .../base-class-re-export.js | 1 + .../bulk-exported-base-class/subclass.js | 5 + .../cross-file/bulk-exporting.js | 7 + .../UnifiedDataFlow/cross-file/file1.js | 17 + .../UnifiedDataFlow/cross-file/file2.js | 30 + .../cross-file/function-raw-export.js | 3 + .../cross-file/hard-link-reexport/lib.js | 5 + .../cross-file/hard-link-reexport/reexport.js | 3 + .../cross-file/hard-link-reexport/use.js | 1 + .../UnifiedDataFlow/cross-file/reExported.js | 3 + .../library-tests/UnifiedDataFlow/curried.js | 16 + .../default-interop/export-default-only.js | 3 + .../default-interop/export-named-only.js | 3 + .../UnifiedDataFlow/default-interop/import.js | 17 + .../UnifiedDataFlow/exceptions.js | 81 + .../UnifiedDataFlow/field-parameter.ts | 15 + .../library-tests/UnifiedDataFlow/fields.js | 58 + .../UnifiedDataFlow/flow-through.js | 18 + .../library-tests/UnifiedDataFlow/iife.js | 82 + .../UnifiedDataFlow/inheritance.js | 106 + .../library-tests/UnifiedDataFlow/jsx.jsx | 26 + .../UnifiedDataFlow/promise-try.js | 29 + .../UnifiedDataFlow/react-use.js | 12 + .../UnifiedDataFlow/side-effect.js | 74 + .../library-tests/UnifiedDataFlow/simple.js | 97 + .../library-tests/UnifiedDataFlow/subclass.js | 34 + .../UnifiedDataFlow/test.expected | 2 + .../library-tests/UnifiedDataFlow/test.ql | 4 + .../test/library-tests/UnifiedDataFlow/tst.js | 174 + .../UnifiedDataFlow/underscore.string.js | 130 + .../UnifiedDataFlow/urlsearchparams.js | 15 + .../library-tests/UnifiedDataFlow/useuse.js | 175 + .../codeql/unified/UnifiedDataFlow.qll | 3950 +++++++++++++++++ shared/unified/qlpack.yml | 11 + shared/util/codeql/util/DualGraph.qll | 92 + 100 files changed, 21301 insertions(+), 12 deletions(-) create mode 100644 javascript/ql/lib/ext/module-systems.model.yml create mode 100644 javascript/ql/lib/ide-contextual-queries/printDfg.ql create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/Constant.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/DataFlowSteps.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/JSUnified.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/NamespaceObjects.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/PathResolution.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/Transforms.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/AST.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/BasicBlockInternal.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/BasicBlocks.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/CFG.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Classes.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Comments.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/ES2015Modules.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Expr.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/FileSystem.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Files.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Functions.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSDoc.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSON.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSPaths.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSX.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Lines.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Locations.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/NPM.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/NodeModuleResolutionImpl.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Overlay.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/PackageJsonEx.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/PathConcatenation.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/PathMapping.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Paths.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Stmt.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/StmtContainers.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/TSConfig.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Templates.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Tokens.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/TypeAnnotations.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/TypeScript.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Util.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/Variables.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/XML.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/YAML.qll create mode 100644 javascript/ql/lib/semmle/javascript/internal/unified/minimal/minimal.qll create mode 100644 javascript/ql/lib/utils/test/InlineFlowTestUnified.qll create mode 100644 javascript/ql/lib/utils/test/InlineFlowTestUnifiedUtil.qll create mode 100644 javascript/ql/lib/utils/test/internal/UnifiedInlineExpectationsTestImpl.qll create mode 100644 javascript/ql/src/meta/alerts/FullCallGraphBaseline.ql create mode 100644 javascript/ql/src/meta/alerts/FullCallGraphUnified.ql create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/accessors.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/array-with.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/arrays.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/arrow-function.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/assumed-receiver.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/augmented-library-object.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/buffer.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/callbacks.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/class-flow.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/base-class-def.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/base-class-re-export.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/subclass.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exporting.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/file1.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/file2.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/function-raw-export.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/lib.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/reexport.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/use.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/reExported.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/curried.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/export-default-only.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/export-named-only.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/import.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/exceptions.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/field-parameter.ts create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/fields.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/flow-through.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/iife.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/inheritance.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/jsx.jsx create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/promise-try.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/react-use.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/side-effect.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/simple.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/subclass.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/test.expected create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/test.ql create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/tst.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/underscore.string.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/urlsearchparams.js create mode 100644 javascript/ql/test/library-tests/UnifiedDataFlow/useuse.js create mode 100644 shared/unified/codeql/unified/UnifiedDataFlow.qll create mode 100644 shared/unified/qlpack.yml create mode 100644 shared/util/codeql/util/DualGraph.qll diff --git a/javascript/ql/lib/ext/module-systems.model.yml b/javascript/ql/lib/ext/module-systems.model.yml new file mode 100644 index 000000000000..ae9d16937240 --- /dev/null +++ b/javascript/ql/lib/ext/module-systems.model.yml @@ -0,0 +1,10 @@ +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: aliasModel + data: + - ["amd-define", "PredefinedVar[define]"] + - ["require", "PredefinedVar[require]"] + - ["module", "PredefinedVar[module]"] + - ["exports", "PredefinedVar[exports]"] + - ["exports", "PredefinedVar[module].Member[exports]"] diff --git a/javascript/ql/lib/ide-contextual-queries/printDfg.ql b/javascript/ql/lib/ide-contextual-queries/printDfg.ql new file mode 100644 index 000000000000..947d157c8048 --- /dev/null +++ b/javascript/ql/lib/ide-contextual-queries/printDfg.ql @@ -0,0 +1,35 @@ +/** + * @name Print DFG + * @description Produces a representation of a file's data flow graph. + * This query is used by the VS Code extension. + * @id javascript/print-dfg + * @kind graph + * @tags ide-contextual-queries/print-dfg + */ + +private import semmle.javascript.internal.unified.minimal.minimal +private import semmle.javascript.internal.unified.JSUnified + +external string selectedSourceFile(); + +private predicate selectedSourceFileAlias = selectedSourceFile/0; + +external int selectedSourceLine(); + +private predicate selectedSourceLineAlias = selectedSourceLine/0; + +external int selectedSourceColumn(); + +private predicate selectedSourceColumnAlias = selectedSourceColumn/0; + +module ViewDfgQueryInput implements ViewDfgQueryInputSig { + predicate selectedSourceFile = selectedSourceFileAlias/0; + + predicate selectedSourceLine = selectedSourceLineAlias/0; + + predicate selectedSourceColumn = selectedSourceColumnAlias/0; + + File getFileFromLocation(Location loc) { result = loc.getFile() } +} + +import ViewDfgQuery diff --git a/javascript/ql/lib/qlpack.yml b/javascript/ql/lib/qlpack.yml index d3ae02b327ce..1726d56db299 100644 --- a/javascript/ql/lib/qlpack.yml +++ b/javascript/ql/lib/qlpack.yml @@ -17,6 +17,7 @@ dependencies: codeql/util: ${workspace} codeql/xml: ${workspace} codeql/yaml: ${workspace} + codeql/unified: ${workspace} dataExtensions: - semmle/javascript/frameworks/**/model.yml - semmle/javascript/frameworks/**/*.model.yml diff --git a/javascript/ql/lib/semmle/javascript/Expr.qll b/javascript/ql/lib/semmle/javascript/Expr.qll index 008cc1283770..4b8796358541 100644 --- a/javascript/ql/lib/semmle/javascript/Expr.qll +++ b/javascript/ql/lib/semmle/javascript/Expr.qll @@ -574,6 +574,9 @@ class ArrayExpr extends @array_expr, Expr { override predicate isImpure() { this.getAnElement().isImpure() } override string getAPrimaryQlClass() { result = "ArrayExpr" } + + /** Gets the first index at which a `...` operator appears in this array literal. */ + int getFirstSpreadIndex() { result = min(int i | this.getElement(i) instanceof SpreadElement) } } /** @@ -1001,6 +1004,9 @@ class InvokeExpr extends @invokeexpr, Expr { */ predicate isSpreadArgument(int i) { this.getArgument(i).stripParens() instanceof SpreadElement } + /** Gets the first index of a spread argument (`...`), if any. */ + int getFirstSpreadIndex() { result = min(int i | this.isSpreadArgument(i)) } + /** * Holds if the `i`th argument of this invocation is an object literal whose property * `name` is set to `value`. @@ -2915,7 +2921,8 @@ deprecated private class LiteralDynamicImportPath extends PathExpr, ConstantStri * Examples: * * ``` - * x ?? f + * x?.f + * x?.() * ``` */ class OptionalUse extends Expr, @optionalchainable { diff --git a/javascript/ql/lib/semmle/javascript/internal/Overlay.qll b/javascript/ql/lib/semmle/javascript/internal/Overlay.qll index db3dc8ac6bf6..b483cf2b2a5c 100644 --- a/javascript/ql/lib/semmle/javascript/internal/Overlay.qll +++ b/javascript/ql/lib/semmle/javascript/internal/Overlay.qll @@ -1,4 +1,3 @@ -private import javascript private import OverlayXml /** Holds if the database is an overlay. */ diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/Constant.qll b/javascript/ql/lib/semmle/javascript/internal/unified/Constant.qll new file mode 100644 index 000000000000..760f8a5da5e9 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/Constant.qll @@ -0,0 +1,61 @@ +overlay[local] +module; + +private import minimal.minimal +private import codeql.util.Boolean + +private newtype TConstant = + TInt(int n) { + n = [0 .. 10] or + n = any(Expr e).getIntValue() or + n = any(Parameter p).getIndex() or + exists(any(InvokeExpr e).getArgument(n)) + } or + TString(string s) { s = any(Expr e).getStringValue() } or + TBoolean(Boolean b) or + TNull() or + TUndefined() + +class Constant extends TConstant { + int asInt() { this = TInt(result) } + + string asString() { this = TString(result) } + + boolean asBoolean() { this = TBoolean(result) } + + predicate isNull() { this = TNull() } + + predicate isUndefined() { this = TUndefined() } + + string toString() { + result = this.asInt().toString() + or + result = "\"" + this.asString() + "\"" + or + result = this.asBoolean().toString() + or + this.isNull() and result = "null" + or + this.isUndefined() and result = "undefined" + } + + int asArrayIndex() { result = this.asInt() and result >= 0 } + + string getAsOperand() { + result = this.asInt().toString() + or + result = "\"" + this.asString() + "\"" + } +} + +module Constant { + Constant fromInt(int n) { result = TInt(n) } + + Constant fromString(string s) { result = TString(s) } + + Constant fromBoolean(boolean b) { result = TBoolean(b) } + + Constant null() { result = TNull() } + + Constant undefined() { result = TUndefined() } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/DataFlowSteps.qll b/javascript/ql/lib/semmle/javascript/internal/unified/DataFlowSteps.qll new file mode 100644 index 000000000000..2f9a7edaf227 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/DataFlowSteps.qll @@ -0,0 +1,858 @@ +overlay[local] +module; + +private import minimal.minimal +private import JSUnified +private import DataFlowBuilder +private import Contents +private import UnifiedDataFlowInput + +pragma[nomagic] +private predicate isForLoopVariable(Variable v) { + v.getADeclarationStatement() = any(ForStmt stmt).getInit() + or + // Handle the somewhat rare case: `for (v; ...; ++v) { ... }` + v.getADeclaration() = any(ForStmt stmt).getInit() +} + +private predicate isLikelyArrayIndex(Expr e) { + // Note: This is written like this to mimick the original behavior. Consider making this more precise. + isForLoopVariable(e.(VarAccess).getVariable()) + or + e.(PropAccess).getPropertyName() = "length" +} + +private ContentSet getContentSetFromNode(AstNode node) { + result = property(node.(Label).getName()) + or + result = property(node.(Expr).getStringValue()) + or + result = property(node.(Expr).getIntValue().toString()) + or + isLikelyArrayIndex(node) and + result = ArrayContent::anyElement() +} + +private class InheritsCall extends CallExpr { + InheritsCall() { this.getCalleeName() = "inherits" and this.getNumArgument() = 2 } +} + +private class ExtendLikeCall extends CallExpr { + ExtendLikeCall() { this.getCalleeName() = ["extend", "merge", "mixin"] } +} + +predicate synthesizeNode(AstNode node, string tag, ControlFlowNode cfgNode) { + node instanceof NewExpr and + tag = ["allocation-site", "prototype"] and + cfgNode = node + or + exists(ClassDefinition cls | + node = cls and + tag = "super-prototype" and + cfgNode = cls.getSuperClass() + ) + or + ( + node = any(ImportDeclaration d).getImportedPathExpr() or + node = any(ReExportDeclaration d).getImportedPathExpr() or + node = any(DynamicImportExpr d).getImportedPathExpr() + ) and + tag = "imported-module" and + cfgNode = node + or + exists(InheritsCall call | + node = call.getAnArgument() and + cfgNode = call and + tag = "inherit-prototype" + ) + or + node instanceof JsxElement and + tag = "jsx-props" and + cfgNode = node +} + +Node getSuperPrototypeOf(ClassDefinition node) { result.isSyntheticNode(node, "super-prototype") } + +private Function toFunction(AstNode node) { + result = node + or + result = node.(ClassDefinition).getConstructor().getBody() +} + +Node getPrototypeOf(AstNode node) { + result.isNamespaceObject(NamespaceObject::prototype(toFunction(node))) +} + +private class StepJS extends Step { + pragma[nomagic] + predicate readProperty(string name) { this.read(property(name)) } + + pragma[nomagic] + predicate storeProperty(string name) { this.store(property(name)) } +} + +class DataFlowStepsImpl extends DataFlowSteps { + override predicate step(Node node1, Step step, Node node2) { stepJs(node1, step, node2) } +} + +pragma[inline] +predicate stepJs(Node node1, StepJS step, Node node2) { + exists(ParExpr e | + node1.isValueOf(e.getExpression()) and + step.value() and + node2.isValueOf(e) + // TODO: eliminate parentheses in extractos + ) + or + exists(ArrayExpr e | + exists(int i, Expr element | + element = e.getElement(i) and + node1.isValueOf(element) and + ( + if i > e.getFirstSpreadIndex() + then step.store(ArrayContent::anyElement()) + else + if i = e.getFirstSpreadIndex() + then step.transform(Transforms::shiftArrayElements(i)) + else step.store(ArrayContent::elementAt(i)) + ) and + node2.isValueOf(e) + ) + or + exists(SpreadElement spread | spread = e.getAnElement() | + node1.isValueOf(spread.getOperand()) and + (step.read(ArrayContent::anyElement()) or step.taint()) and + node2.isValueOf(spread) + ) + ) + or + exists(ObjectExpr e | + node1.isNamespaceObject(NamespaceObject::allocationSite(e)) and + step.value() and + node2.isValueOf(e) + or + exists(Property prop | prop = e.getAProperty() | + prop instanceof ValueProperty and + node1.isValueOf(prop.getInit()) and + step.storeProperty(prop.getName()) and + node2.isValueOf(e) + or + prop instanceof SpreadProperty and + node1.isValueOf(prop.getInit().(SpreadElement).getOperand()) and + (step.withContent(anyProperty()) or step.taint()) and + node2.isValueOf(e) + or + prop instanceof PropertyGetter and + node1.isValueOf(prop.getInit()) and + step.storeAsGetter(getContentSetFromNode(prop.getNameExpr())) and + node2.isValueOf(e) + or + prop instanceof PropertySetter and + node1.isValueOf(prop.getInit()) and + step.storeAsSetter(getContentSetFromNode(prop.getNameExpr())) and + node2.isValueOf(e) + ) + ) + or + exists(ObjectPattern e | + exists(PropertyPattern prop | prop = e.getAPropertyPattern() | + node1.isIncomingValue(e) and + step.readProperty(prop.getName()) and + node2.isIncomingValue(prop.getValuePattern()) + or + node1.isValueOf(prop.getDefault()) and + step.value() and + node2.isIncomingValue(prop.getValuePattern()) + ) + or + node1.isIncomingValue(e) and + step.value() and + node2.isIncomingValue(e.getRest()) + ) + or + exists(ArrayPattern e | + exists(int i | + node1.isIncomingValue(e) and + step.read(ArrayContent::elementAt(i)) and + node2.isIncomingValue(e.getElement(i)) + or + node1.isValueOf(e.getDefault(i)) and + step.value() and + node2.isIncomingValue(e.getElement(i)) + ) + or + node1.isIncomingValue(e) and + step.transform(Transforms::shiftArrayElements(-e.getSize())) and + node2.isIncomingValue(e.getRest()) + ) + or + exists(Function e | + node1.isCallable(e) and + step.value() and + ( + if e instanceof FunctionDeclStmt + then node2.isIncomingValue(e.getIdentifier()) + else node2.isValueOf(e) + ) + or + // For named function expressions (`return function blah() { ... }`), the function name is in scope + // within the function itself. + e instanceof FunctionExpr and + node1.isCallableSelfReferenceParameter(e) and + step.value() and + node2.isIncomingValue(e.getIdentifier()) + or + e instanceof FunctionDeclStmt and + node1.isIncomingValue(e.getIdentifier()) and + step.value() and + node2.isValueOf(e) + or + node1.isNamespaceObject(NamespaceObject::allocationSite(e)) and + step.value() and + node2.isCallableSelfReferenceParameter(e) // TODO: tell the framework that the namespace and callable are the same thing + ) + or + exists(SeqExpr e | + node1.isValueOf(e.getLastOperand()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(ConditionalExpr e | + node1.isValueOf(e.getABranch()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(InvokeExpr e, Call call | call.asOrdinaryCall() = e | + exists(int i | + node1.isValueOf(e.getArgument(i)) and + not i = e.getFirstSpreadIndex() and + ( + if i > e.getFirstSpreadIndex() + then step.store(ArrayContent::anyElement()) + else step.store(ArrayContent::elementAt(i)) + ) and + node2.isArgumentObject(call) + or + i = e.getFirstSpreadIndex() and + node1.isValueOf(e.getArgument(i).(SpreadElement).getOperand()) and + step.transform(Transforms::shiftArrayElements(i)) and + node2.isArgumentObject(call) + ) + or + exists(SpreadElement spread | spread = e.getAnArgument() | + node1.isValueOf(spread.getOperand()) and + (step.read(ArrayContent::anyElement()) or step.taint()) and + node2.isValueOf(spread) + ) + or + node1.isValueOf(e.getCallee()) and + step.value() and + node2.isCallTarget(call) + or + node1.isValueOf(e.(CallExpr).getReceiver()) and + step.value() and + node2.isReceiverArgument(call) + or + node1.isReturnValueFromCall(call) and + step.value() and + node2.isValueOf(e) + or + node1.isExceptionThrownFromCall(call) and // TODO: can this be moved into the framework? + step.value() and + node2.isExceptionTargetFromContext(e) + or + e instanceof NewExpr and + ( + node1.isValueOf(e.getCallee()) and + step.instantiate() and + node2.isSyntheticNode(e, "allocation-site") + or + node1.isSyntheticNode(e, "allocation-site") and + step.value() and + node2.isReceiverArgument(call) + or + (node1.isReceiverArgument(call) or node1.isPostUpdatedReceiverArgument(call)) and + step.value() and + node2.isValueOf(e) + ) + or + e instanceof SuperCall and // `super()` call in a constructor + ( + node1.isVariableRead(e.getContainer().getScope().getVariable("this")) and + step.value() and + node2.isReceiverArgument(call) + or + exists(ClassDefinition cls | + e.(SuperCall).getBinder() = cls.getConstructor().getBody() and + node1.isValueOf(cls.getSuperClass()) and + step.value() and + node2.isCallTarget(call) + ) + ) + ) + or + exists(MethodCallExpr e, Call reflectiveCall | reflectiveCall.asReflectiveCall() = e | + node1.isValueOf(e.getReceiver()) and + step.value() and + node2.isCallTarget(reflectiveCall) + or + node1.isValueOf(e.getArgument(0)) and + step.value() and + node2.isReceiverArgument(reflectiveCall) + or + e.getMethodName() = "call" and + exists(int i | + i >= 1 and + node1.isValueOf(e.getArgument(i)) and + step.store(ArrayContent::elementAt(i - 1)) and + node2.isArgumentObject(reflectiveCall) + ) + or + e.getMethodName() = "apply" and + node1.isValueOf(e.getArgument(1)) and + step.transform(Transforms::toArray()) and + node2.isArgumentObject(reflectiveCall) + or + // Forward out nodes to the outer call + exists(Call ordinaryCall | ordinaryCall.asOrdinaryCall() = e | + node1.isReturnValueFromCall(reflectiveCall) and + step.value() and + node2.isReturnValueFromCall(ordinaryCall) + or + node1.isExceptionThrownFromCall(reflectiveCall) and + step.value() and + node2.isExceptionThrownFromCall(ordinaryCall) + ) + ) + or + exists(PropAccess prop | + // TODO: also handle optional prop access `x?.f` + node1.isValueOf(prop.getBase()) and + ( + step.read(getContentSetFromNode(prop.getPropertyNameExpr())) + or + not exists(getContentSetFromNode(prop.getPropertyNameExpr())) and + step.read(ArrayContent::anyElement()) + ) and + node2.isValueOf(prop) + or + node1.isIncomingValue(prop) and + step.store(getContentSetFromNode(prop.getPropertyNameExpr())) and + node2.isPostUpdatedValue(prop.getBase()) + ) + or + exists(VarRef e | + node1.isIncomingValue(e) and + step.value() and + node2.isVariableWrite(e.getVariable()) + or + node1.isVariableRead(e.getVariable()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(ThisExpr e | + node1.isVariableRead(e.getBindingContainer().getScope().getVariable("this")) and + step.value() and + node2.isValueOf(e) + ) + or + exists(AddExpr e | + node1.isValueOf(e.getAnOperand()) and + step.taint() and + node2.isValueOf(e) + ) + or + exists(LogAndExpr e | + node1.isValueOf(e.getRightOperand()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(LogOrExpr e | + node1.isValueOf(e.getAnOperand()) and + step.value() and // TODO: add step.ifTruthy() for flow out of left operand? + node2.isValueOf(e) + ) + or + exists(NullishCoalescingExpr e | + node1.isValueOf(e.getAnOperand()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(Assignment e | + // Whatever gets assigned to the LHS is also returned by the assignment + node1.isIncomingValue(e.getLhs()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(AssignExpr e | + // plain assignment + node1.isValueOf(e.getRhs()) and + step.value() and + node2.isIncomingValue(e.getLhs()) + ) + or + exists(AssignAddExpr e | + node1.isValueOf([e.getLhs(), e.getRhs()]) and + step.taint() and + node2.isIncomingValue(e.getLhs()) + ) + or + exists(AssignLogOrExpr e | + node1.isValueOf([e.getLhs(), e.getRhs()]) and + step.value() and + node2.isIncomingValue(e.getLhs()) + ) + or + exists(AssignLogAndExpr e | + node1.isValueOf(e.getRhs()) and + step.value() and + node2.isIncomingValue(e.getLhs()) + or + // If the LHS is already truthy, then that's the return value of the expression + // (the assignment does not take place in this case) + node1.isValueOf(e.getLhs()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(AssignNullishCoalescingExpr e | + node1.isValueOf([e.getLhs(), e.getRhs()]) and + step.value() and + node2.isIncomingValue(e.getLhs()) + ) + or + exists(YieldExpr e | + node1.isValueOf(e.getOperand()) and + (if e.isDelegating() then step.transform(Transforms::readIterableContents()) else step.value()) and + node2.isInnerReturnValue(e.getContainer()) + ) + or + exists(ForOfComprehensionBlock e | + node1.isValueOf(e.getDomain()) and + step.transform(Transforms::readIterableContents()) and + node2.isIncomingValue(e.getIterator()) + ) + or + exists(ForInComprehensionBlock e | + node1.isValueOf(e.getDomain()) and + step.taint() and + node2.isIncomingValue(e.getIterator()) + ) + or + exists(ArrayComprehensionExpr e | + node1.isValueOf(e.getBody()) and + step.store(ArrayContent::anyElement()) and + node2.isValueOf(e) + ) + or + exists(GeneratorExpr e | + node1.isValueOf(e.getBody()) and + step.store(Contents::iteratorElement()) and + node2.isValueOf(e) + ) + or + exists(LegacyLetExpr e | + node1.isValueOf(e.getBody()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(ImmediatelyInvokedFunctionExpr e | + exists(e) and + // TODO: should we optimise for this case in language-specific code? + none() + ) + or + exists(AwaitExpr e | + node1.isValueOf(e.getOperand()) and + step.transform(Transforms::awaitValue()) and + node2.isValueOf(e) + or + node1.isValueOf(e.getOperand()) and + step.read(Contents::promiseError()) and + node2.isExceptionTargetFromContext(e) + ) + or + exists(FunctionSentExpr e | + // Not implemented yet (also not in baseline) + exists(e) and none() + ) + or + exists(FunctionBindExpr e | + // Partial invocation not handled here + exists(e) and none() + ) + or + exists(DynamicImportExpr e | + node1.isSyntheticNode(e.getImportedPathExpr(), "imported-module") and + step.store(Contents::promiseValue()) and + node2.isValueOf(e) + ) + or + exists(OptionalUse e | exists(e) and none()) // TODO: add filtered steps so this can also be used for nullness/type analysis? + or + exists(ImportMetaExpr e | + node1.isUnknown() and + step.value() and + node2.isValueOf(e) + ) + or + // exists(Templating::TemplatePlaceholderTag tag | + // node1.isValueOf(tag.getInnerTopLevel().getExpression()) and + // step.value() and // TODO: mark safe for jump? + // node2.isValueOf(tag.getEnclosingExpr()) + // ) + // or + exists(Function f | + node1.isValueOf(f.getAReturnedExpr()) and + step.value() and + node2.isInnerReturnValue(f) + or + f.isAsync() and + not f.isGenerator() and + ( + node1.isInnerReturnValue(f) and + step.transform(Transforms::toPromise()) and + node2.isReturnValue(f) + or + node1.isInnerExceptionalReturnValue(f) and + step.store(Contents::promiseError()) and + node2.isReturnValue(f) + ) + or + f.isGenerator() and + not f.isAsync() and + ( + node1.isInnerReturnValue(f) and + step.store(Contents::iteratorElement()) and + node2.isReturnValue(f) + or + node1.isInnerExceptionalReturnValue(f) and + step.store(Contents::iteratorError()) and + node2.isReturnValue(f) + ) + or + f.isGenerator() and + f.isAsync() and + ( + node1.isInnerReturnValue(f) and + step.transform(Transforms::toAsyncIteratorElement()) and + node2.isReturnValue(f) + or + node1.isInnerExceptionalReturnValue(f) and + step.store(Contents::iteratorError()) and + node2.isReturnValue(f) + ) + or + node1.isParameterObject(f) and + ( + exists(int i | + step.read(ArrayContent::elementAt(i)) and + node2.isIncomingValue(f.getParameter(i)) and + not f.getParameter(i).isRestParameter() + ) + or + f.usesArgumentsObject() and + step.withContent(ArrayContent::anyElement()) and + node2.isVariableWrite(f.getArgumentsVariable()) + or + exists(Parameter rest | + rest = f.getRestParameter() and + step.transform(Transforms::shiftArrayElements(-rest.getIndex())) and + node2.isIncomingValue(rest) + ) + ) + or + node1.isReceiverParameter(f) and + step.value() and + node2.isVariableWrite(f.getScope().getVariable("this")) + or + node1 = getPrototypeOf(f) and + step.storeProperty("prototype") and + node2.isNamespaceObject(NamespaceObject::allocationSite(f)) + or + // To handle assignments to .prototype, read the new value back into the prototype node + node2.isNamespaceObject(NamespaceObject::allocationSite(f)) and + step.readProperty("prototype") and + node1 = getPrototypeOf(f) + or + node1.isNamespaceObject(NamespaceObject::allocationSite(f)) and + step.value() and + ( + if f instanceof FunctionDeclStmt + then node2.isIncomingValue(f.getIdentifier()) + else node2.isValueOf(f) + ) + ) + or + exists(ThrowStmt e | + node1.isValueOf(e.getExpr()) and + step.value() and + node2.isExceptionTargetFromContext(e) + ) + or + exists(TryStmt e | + node1.isInterceptedException(e.getBody()) and + step.value() and + // Note: standard JS only allow at most one catch clause, but some old dialects support more + // Just propagate to all catch clauses + node2.isIncomingValue(e.getACatchClause().getAParameter()) + ) + or + exists(ForInStmt stmt | + node1.isValueOf(stmt.getIterationDomain()) and + step.taint() and + node2.isIncomingValue(stmt.getLValue()) + ) + or + exists(ForOfStmt stmt | + node1.isValueOf(stmt.getIterationDomain()) and + ( + if stmt.isAwait() + then step.transform(Transforms::readAsyncIterableContents()) + else step.transform(Transforms::readIterableContents()) + ) and + node2.isIncomingValue(stmt.getLValue()) + or + node1.isValueOf(stmt.getIterationDomain()) and + step.read(Contents::iteratorError()) and + node2.isExceptionTargetFromContext(stmt) + ) + or + exists(ForEachStmt stmt | + // Note: this is a `for each` statement from legacy JS dialects. Not standard JS. + node1.isValueOf(stmt.getIterationDomain()) and + step.transform(Transforms::readIterableContents()) and + node2.isIncomingValue(stmt.getLValue()) + ) + or + exists(VariableDeclarator decl | + node1.isValueOf(decl.getInit()) and + step.value() and + node2.isIncomingValue(decl.getBindingPattern()) + ) + or + exists(ImportDeclaration imprt | + node1.isSyntheticNode(imprt.getImportedPathExpr(), "imported-module") and + step.value() and + node2.isIncomingValue(imprt.getASpecifier()) + ) + or + exists(NamedImportSpecifier e | + node1.isIncomingValue(e) and + step.readProperty(e.getImportedName()) and + node2.isIncomingValue(e.getLocal()) // TODO: imported locals don't act as true local variables; their reads are syntactic sugar for property reads and can change value from read to read + ) + or + exists(ImportNamespaceSpecifier e | + node1.isIncomingValue(e) and + step.value() and + node2.isIncomingValue(e.getLocal()) + ) + or + exists(ImportDefaultSpecifier e | + node1.isIncomingValue(e) and + step.readProperty("default") and // TODO: handle non-standard 'default' semantics + node2.isIncomingValue(e.getLocal()) + ) + or + exists(FieldDeclaration e | + node1.isValueOf(e.getInit()) and + step.store(getContentSetFromNode(e.getNameExpr())) and + ( + if e.isStatic() + then node2.isVariableRead(e.getDeclaringClass().getVariable()) + else + node2 + .isVariableRead(e.getDeclaringClass() + .getConstructor() + .getBody() + .getScope() + .getVariable("this")) + ) + ) + or + exists(MethodDeclaration method, ContentSet contents | + not method instanceof ConstructorDeclaration + | + node1.isValueOf(method.getInit()) and + contents = getContentSetFromNode(method.getNameExpr()) and + ( + if method instanceof SetterMethodDeclaration + then step.storeAsSetter(contents) + else + if method instanceof GetterMethodDeclaration + then step.storeAsGetter(contents) + else step.store(contents) + ) and + if method.isStatic() + then node2.isVariableRead(method.getDeclaringClass().getVariable()) + else node2 = getPrototypeOf(method.getDeclaringClass()) + ) + or + // Note: "class declarations" in JavaScript are syntactic sugar for plain functions and properties. This just models the desugaring. + exists(ClassDefinition cls | + node1.isValueOf(cls.getConstructor().getBody()) and + step.value() and + ( + node2.isIncomingValue(cls.getIdentifier()) + or + not exists(cls.getIdentifier()) and + node2.isVariableWrite(cls.getVariable()) + ) + or + // To ensure static members are assigned before 'cls' returns, model the return as a read of the class variable + node1.isVariableRead(cls.getVariable()) and + step.value() and + node2.isValueOf(cls) + or + node1.isValueOf(cls.getSuperClass()) and + step.storeBaseObject() and + node2.isValueOf(cls.getConstructor().getBody()) + or + node1.isValueOf(cls.getSuperClass()) and + step.readProperty("prototype") and + node2 = getSuperPrototypeOf(cls) + or + node1 = getSuperPrototypeOf(cls) and + step.storeBaseObject() and + node2 = getPrototypeOf(cls) + ) + or + exists(TopLevel top | + node1.isNamespaceObject(NamespaceObject::moduleExportsObject(top)) and + step.storeProperty("exports") and + node2.isNamespaceObject(NamespaceObject::moduleObject(top)) + or + // To simply handling of `module.exports = {...}` assignments, + // make stored values flow back into the canonical exports-object node. + node1.isNamespaceObject(NamespaceObject::moduleObject(top)) and + step.readProperty("exports") and + node2.isNamespaceObject(NamespaceObject::moduleExportsObject(top)) + or + node1.isNamespaceObject(NamespaceObject::moduleObject(top)) and + step.value() and + node2.isVariableWrite(top.getScope().getVariable("module")) + or + node1.isNamespaceObject(NamespaceObject::moduleExportsObject(top)) and + step.value() and + node2.isVariableWrite(top.getScope().getVariable("exports")) + or + // Implement "default" interop for compatibility with various compilers/bundlers + // If a module does not explicitly have a "default" export, use the entire module as the default export. + not exists(ExportDefaultDeclaration decl | decl.getTopLevel() = top) and + node1.isNamespaceObject(NamespaceObject::moduleExportsObject(top)) and + step.storeProperty("default") and + node2 = node1 + or + // Conversely, a module that solely consists of a default export should also be importable + // as a namespace + exists(ExportDefaultDeclaration decl | + decl = unique(ExportDeclaration d | d.getTopLevel() = top) and + node1.isValueOf(decl.getOperand()) and + step.storeProperty("exports") and + node2.isNamespaceObject(NamespaceObject::moduleObject(top)) + ) + ) + or + exists(ExportNamedDeclaration decl, Variable v, string name | + decl.exportsDirectlyAs(v, name) and + node1.isIncomingValue(v.getAReference()) and + step.storeProperty(name) and + node2.isNamespaceObject(NamespaceObject::moduleExportsObject(decl.getContainer())) + ) + or + exists(ExportDefaultDeclaration decl | + node1.isValueOf(decl.getOperand()) and + step.storeProperty("default") and + node2.isNamespaceObject(NamespaceObject::moduleExportsObject(decl.getContainer())) + ) + or + exists(SelectiveReExportDeclaration reExport, ExportSpecifier spec | + spec = reExport.getASpecifier() + | + node1.isSyntheticNode(reExport.getImportedPathExpr(), "imported-module") and + ( + // export { x as y } from '..' + step.readProperty(spec.getLocalName()) + or + // export * as y from '..' + spec instanceof ExportNamespaceSpecifier and + step.value() + ) and + node2.isValueOf(spec) + or + node1.isValueOf(spec) and + step.storeProperty(spec.getExportedName()) and + node2.isNamespaceObject(NamespaceObject::moduleExportsObject(reExport.getContainer())) + ) + or + exists(BulkReExportDeclaration reExport, TopLevel top | top = reExport.getContainer() | + node1.isSyntheticNode(reExport.getImportedPathExpr(), "imported-module") and + step.withoutContent(Contents::shadowedFromBulkExport(top)) and + node2.isNamespaceObject(NamespaceObject::moduleExportsObject(top)) + ) + or + exists(InheritsCall call | + // inherits(Class, SuperClass) should result in the storeBase step: + // + // SuperClass.prototype --> Class.prototype + // + // We use some synthetic nodes to load both the prototypes + exists(Expr arg | arg = call.getAnArgument() | + node1.isValueOf(arg) and + step.readProperty("prototype") and + node2.isSyntheticNode(arg, "inherit-prototype") + ) + or + node1.isSyntheticNode(call.getArgument(1), "inherit-prototype") and + step.storeBaseObject() and + node2.isSyntheticNode(call.getArgument(0), "inherit-prototype") + ) + or + exists(ExtendLikeCall call | + node1.isValueOf(call.getArgument(any(int i | i >= 1))) and + step.storeBaseObject() and + node2.isValueOf(call.getArgument(0)) + or + node1.isValueOf(call.getArgument(0)) and + step.value() and + node2.isValueOf(call) + ) + or + exists(FieldParameter param, Function f | + param = f.getAParameter() and + node1.isIncomingValue(param) and + step.storeProperty(param.getName()) and + node2.isVariableRead(f.getScope().getVariable("this")) + ) + or + exists(TypeAssertion e | + node1.isValueOf(e.getExpression()) and + step.value() and + node2.isValueOf(e) + ) + or + exists(JsxElement e, Call call | call.asJsxCall() = e | + node1.isValueOf(e.getNameExpr()) and + step.value() and + node2.isCallTarget(call) + or + node1.isSyntheticNode(e, "jsx-props") and + step.store(ArrayContent::elementAt(0)) and + node2.isArgumentObject(call) + or + exists(JsxAttribute attr | + attr = e.getAnAttribute() and + node1.isValueOf(attr.getValue()) and + step.storeProperty(attr.getName()) and + node2.isSyntheticNode(e, "jsx-props") + ) + ) +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/JSUnified.qll b/javascript/ql/lib/semmle/javascript/internal/unified/JSUnified.qll new file mode 100644 index 000000000000..f6821b579773 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/JSUnified.qll @@ -0,0 +1,453 @@ +overlay[local?] +module; + +private import minimal.minimal +private import minimal.minimal as JS +private import codeql.unified.UnifiedDataFlow +private import codeql.util.Boolean +import minimal.BasicBlockInternal as BB +private import PathResolution + +module UnifiedDataFlow0 = MakeUnifiedDataFlow0; + +module UnifiedDataFlow1 = MakeUnifiedDataFlow1; + +module UnifiedDataFlow2 = MakeUnifiedDataFlow2; + +module UnifiedDataFlow3 = MakeUnifiedDataFlow3; + +module UnifiedDataFlow4 = MakeUnifiedDataFlow4; + +import UnifiedDataFlow0 +import UnifiedDataFlow1 +import UnifiedDataFlow2 +import UnifiedDataFlow3 +import UnifiedDataFlow4 + +module UnifiedDataFlowInput implements + UnifiedDataFlowSig1, UnifiedDataFlowSig2, UnifiedDataFlowSig3, UnifiedDataFlowSig4 +{ + import NamespaceObjects + import semmle.javascript.internal.unified.Constant + + class AstNode = JS::AstNode; + + class Callable = JS::StmtContainer; + + predicate lateStageStep(DataFlow2::Node node1, StepBase step, DataFlow2::Node node2) { + importStep(node1, step, node2) + } + + predicate isTrackedAllocationSite(DataFlow2::Node node) { + node.isSyntheticNode(_, + ["module-exports-object", "module-object", "prototype", "allocation-site"]) + or + node.isValueOf(any(ObjectExpr e)) + or + node.isIncomingValue(any(ClassDefinition def).getIdentifier()) + or + exists(Function f | + ( + if f instanceof FunctionDeclStmt + then node.isIncomingValue(f.getIdentifier()) + else node.isValueOf(f) + ) + ) + } + + Callable getCallableFromBasicBlock(BB::Cfg::BasicBlock bb) { result = bb.getContainer() } + + Callable getCallableFromCfgNode(ControlFlowNode node) { result = node.getContainer() } + + private newtype TCall = + TOrdinaryCall(JS::InvokeExpr invoke) or + TReflectiveCall(JS::MethodCallExpr call) { call.getMethodName() = ["call", "apply"] } or + TJsxCall(JS::JsxElement elm) { not elm.getNameExpr() instanceof Label } + + class Call extends TCall { + JS::InvokeExpr asOrdinaryCall() { this = TOrdinaryCall(result) } + + JS::MethodCallExpr asReflectiveCall() { this = TReflectiveCall(result) } + + JS::JsxElement asJsxCall() { this = TJsxCall(result) } + + string toString() { + result = this.asOrdinaryCall().toString() + or + result = "[inner] " + this.asReflectiveCall().toString() + or + result = this.asJsxCall().toString() + } + + Location getLocation() { + result = this.asOrdinaryCall().getLocation() + or + result = this.asReflectiveCall().getLocation() + or + result = this.asJsxCall().getLocation() + } + + JS::InvokeExpr getUnderlyingInvokeExpr() { + result = this.asOrdinaryCall() or result = this.asReflectiveCall() + } + } + + predicate modelEntryPoint(string head, string operand, DataFlow2::Node node) { + head = "PredefinedVar" and + exists(Variable v | + v.getName() = operand and + ( + v.getScope().getScopeElement() instanceof TopLevel and + node.isVariablePreInitializer(v) + or + v instanceof GlobalVariable and + node.isValueOf(v.getAnAccess()) + ) + ) + } + + predicate argumentParameterContent(string operand, Content content) { + operand = "" + content.asArrayIndex(_) + } + + predicate isMethod(Callable callable) { + exists(MethodDeclaration method | + not method instanceof ConstructorDeclaration and + callable = method.getBody() + ) + or + // For functions stored on an object, we don't know if it's a constructor stored on a namespace-like object, + // or a method on the target object. Make a best-effort guess based on whether the name is capitalized. + not callable instanceof ArrowFunctionExpr and + ( + exists(AssignExpr assign | + assign.getTarget().(PropAccess).getPropertyName().regexpMatch("[a-z].*") and + callable = assign.getRhs() + ) + or + exists(Property prop | + callable = prop.getInit() and + prop.getName().regexpMatch("[a-z].*") + ) + ) + } + + predicate isInstanceInitializer(Callable callable, ClassLikeObject cls) { + callable = cls.getConstructor() + } + + ControlFlowNode getCfgNodeFromCall(Call call) { + result = call.asOrdinaryCall() or result = call.asReflectiveCall() + } + + class LocalVariable = JS::LocalVariable; + + class GuardValue = Boolean; + + class Guard extends Expr { + Guard() { this = any(ConditionGuardNode g).getTest() } + + private ConditionGuardNode getAConditionGuardNode() { result.getTest() = this } + + BB::Cfg::BasicBlock getOutcomeBlock(boolean branch) { + // A ConditionGuardNode sits at the beginning of each outcome branch + exists(ConditionGuardNode guard | + guard = this.getAConditionGuardNode() and + guard.getOutcome() = branch and + result = guard.getBasicBlock() + ) + } + + predicate hasValueBranchEdge(BB::Cfg::BasicBlock bb1, BB::Cfg::BasicBlock bb2, GuardValue branch) { + exists(ConditionGuardNode guard | + branch = guard.getOutcome() and + bb1 = guard.getBasicBlock().getAPredecessor() and + bb2 = guard.getBasicBlock() + ) + } + + predicate valueControlsBranchEdge( + BB::Cfg::BasicBlock bb1, BB::Cfg::BasicBlock bb2, GuardValue val + ) { + // TODO: instantiate guards library to get a better implementation of this + this.hasValueBranchEdge(bb1, bb2, val) + } + } + + predicate guardDirectlyControlsBlock(Guard guard, BB::Cfg::BasicBlock bb, GuardValue branch) { + guard.getOutcomeBlock(branch).dominates(bb) + } + + predicate hasIncomingValue(AstNode node, ControlFlowNode when) { + node = when.(Assignment).getTarget() // note: includes both plain and compound assignments + or + node = when.(UpdateExpr).getOperand() // x++ + or + node = when.(Parameter) + or + node = when.(EnhancedForLoop).getLValue() + or + node = when.(VariableDeclarator).getBindingPattern() + or + node = when.(PropertyPattern).getValuePattern() + or + node = when.(ArrayPattern).getAnElement() + or + node = when.(ObjectPattern).getRest() + or + node = when.(ArrayPattern).getRest() + or + exists(VarDecl decl | + decl = any(Function f).getIdentifier() + or + decl = any(ClassDefinition cls).getIdentifier() + | + when = decl and + node = when + ) + or + node = any(ComprehensionBlock b).getIterator() and when = node + or + node = any(ImportDeclaration d).getASpecifier() and when = node + or + node = any(ImportSpecifier s).getLocal() and when = node + // TODO: exports + // TODO: TypeScript specific declarations + // TODO: TypeScript namespace declarations are impure, or special-case for lazy-created namespace access? + } + + private predicate hasBothIncomingAndCompletionValue(AstNode node) { + node = any(CompoundAssignExpr e).getTarget() + or + node = any(UpdateExpr e).getOperand() + } + + predicate hasCompletionValue(AstNode node, ControlFlowNode when) { + node instanceof AST::ValueNode and + when = node and + (hasIncomingValue(node, _) implies hasBothIncomingAndCompletionValue(node)) + } + + predicate hasPostUpdatedValue(AstNode node, ControlFlowNode when) { + exists(PropAccess prop | + hasIncomingValue(prop, when) and + node = prop.getBase() + ) + } + + predicate isInterceptingExceptions(AstNode tryBlock) { + exists(TryStmt try | + exists(try.getACatchClause()) and + tryBlock = try.getBody() + ) + } + + private newtype TIndexedContainerKind = + TArray() or + TMap() + + class IndexedContainerKind extends TIndexedContainerKind { + string toString() { + this = TArray() and result = "Array" + or + this = TMap() and result = "Map" + } + + predicate trackFlowIntoKeys() { this = TMap() } + + string getKeyToken() { this = TMap() and result = "MapKey" } + + string getValueToken() { + this = TArray() and result = "ArrayElement" + or + this = TMap() and result = "MapValue" + } + + predicate trackValuesAssociatedWithKey(Constant key) { + this = TMap() and exists(key) + or + this = TArray() and key.asArrayIndex() = [0 .. 20] + } + } + + additional class ArrayContainerKind extends IndexedContainerKind, TArray { } + + additional class MapContainerKind extends IndexedContainerKind, TMap { } + + private newtype TLanguageContent = + TPropertyName(string name) { + name = any(Identifier id).getName() or name = any(Expr e).getStringValue() + } or + TPromiseValue() or + TPromiseError() or + TIteratorElement() or + TIteratorError() + + class LanguageContent extends TLanguageContent { + predicate supportsStrongUpdate() { this instanceof TPropertyName } + + string asPropertyName() { this = TPropertyName(result) } + + predicate isPromiseValue() { this = TPromiseValue() } + + predicate isPromiseError() { this = TPromiseError() } + + predicate hasModelToken(string head, string operand) { + head = "Member" and + operand = this.asPropertyName() + or + this = TPromiseValue() and head = "Awaited" and operand = "value" + or + this = TPromiseError() and head = "Awaited" and operand = "error" + or + this = TIteratorElement() and head = "IteratorElement" and operand = "" + or + this = TIteratorError() and head = "IteratorError" and operand = "" + } + + string toString() { + result = this.asPropertyName() + or + this = TPromiseValue() and result = "Promise.value" + or + this = TPromiseError() and result = "Promise.error" + or + this = TIteratorElement() and result = "IteratorElement" + or + this = TIteratorError() and result = "IteratorError" + } + + Location getLocation() { none() } + } + + private newtype TLanguageContentSet = + TAnyProperty() or + TAnyPromiseContent() or + TShadowedFromBulkExport(TopLevel top) { + exists(BulkReExportDeclaration decl | decl.getContainer() = top) + } + + class LanguageContentSet extends TLanguageContentSet { + Content getAReadContent() { + this instanceof TAnyProperty and + ( + exists(result.asLanguageContent().asPropertyName()) + or + exists(result.asContainerSlot(any(ArrayContainerKind a))) + ) + or + this instanceof TAnyPromiseContent and + ( + result.asLanguageContent().isPromiseValue() + or + result.asLanguageContent().isPromiseError() + ) + or + exists(TopLevel t, ExportNamedDeclaration decl, string name | + this = TShadowedFromBulkExport(t) and + decl.getContainer() = t and + decl.exportsDirectlyAs(_, name) and + result.asLanguageContent().asPropertyName() = name + ) + or + this instanceof TShadowedFromBulkExport and + result.asLanguageContent().asPropertyName() = "default" + } + + Content getAStoreContent() { none() } + + predicate hasModelToken(string head, string operand) { + this = TAnyProperty() and head = "AnyMember" and operand = "" + } + + Location getLocation() { + exists(TopLevel top | + this = TShadowedFromBulkExport(top) and + result = top.getLocation() + ) + } + + string toString() { + this = TAnyProperty() and result = "anyProperty" + or + this = TAnyPromiseContent() and result = "anyPromiseContent" + or + this instanceof TShadowedFromBulkExport and + result = "shadowedFromBulkExport" + } + + predicate supportsStrongUpdate() { none() } + } + + /** Provides access to contents. */ + additional module Contents { + module ArrayContent = ArrayContentAccessor; + + module MapContent = MapContentAccessor; + + private class PropertyName extends string { + PropertyName() { + this = any(Identifier id).getName() + or + this = any(Expr e).getStringValue() + or + this = any(StringLiteralTypeExpr e).getValue() + } + } + + pragma[nomagic] + private ContentSet arrayProperty(string name) { + name = result.asContainerSlot(ArrayContent::kind()).asArrayIndex().toString() + } + + pragma[nomagic] + ContentSet property(string name) { + // In JavaScript, array elements are semantically just properties whose name is a string containing the index. + // Make sure numeric strings are mapped to their array element contents. + result = arrayProperty(name) + or + not exists(arrayProperty(name)) and + result.asSingleton().asLanguageContent().asPropertyName() = name + } + + bindingset[name] + pragma[inline_late] + ContentSet propertyStrict(string name) { result = property(name) } + + ContentSet anyProperty() { result.asLanguageContentSet() = TAnyProperty() } + + ContentSet promiseValue() { result.asSingleton().asLanguageContent() = TPromiseValue() } + + ContentSet promiseError() { result.asSingleton().asLanguageContent() = TPromiseError() } + + ContentSet anyPromiseContent() { result.asLanguageContentSet() = TAnyPromiseContent() } + + ContentSet iteratorElement() { result.asSingleton().asLanguageContent() = TIteratorElement() } + + ContentSet iteratorError() { result.asSingleton().asLanguageContent() = TIteratorError() } + + /** Content-set matching the set of contents that are shadowed from bulk-export statements in `top`. */ + ContentSet shadowedFromBulkExport(TopLevel top) { + result.asLanguageContentSet() = TShadowedFromBulkExport(top) + } + } + + import semmle.javascript.internal.unified.DataFlowSteps + import semmle.javascript.internal.unified.Transforms + + Content setterParameter() { result.asArrayIndex(_) = 0 } +} + +module Contents = UnifiedDataFlowInput::Contents; + +module Transforms = UnifiedDataFlowInput::Transforms; + +module DataFlow2 = DataFlowPublic; + +extensible predicate aliasModel(string aliasName, string accessPath); + +module ModelsAsData = MakeModelsAsData; + +class LocalVariable = JS::LocalVariable; + +import UnifiedDataFlowInput diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/NamespaceObjects.qll b/javascript/ql/lib/semmle/javascript/internal/unified/NamespaceObjects.qll new file mode 100644 index 000000000000..4b827bce3470 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/NamespaceObjects.qll @@ -0,0 +1,103 @@ +overlay[local?] +module; + +private import minimal.minimal +private import JSUnified + +private newtype TNamespaceObject = + MkAllocationSite(AstNode node) { + node instanceof Function + or + node instanceof ObjectExpr + } or + MkPrototype(Function f) { + not f instanceof ArrowFunctionExpr and + not f = any(MethodDeclaration m | not m instanceof ConstructorDeclaration).getBody() + } or + MkModuleObject(TopLevel top) or + MkModuleExportsObject(TopLevel top) + +private string getPrettyName1(AstNode node) { + result = node.(Function).getName() and + not node = any(ClassDefinition cls).getConstructor().getBody() + or + result = node.(ClassDefinition).getName() + or + exists(ClassDefinition cls | + node = cls.getConstructor().getBody() and + result = cls.getName() + ) +} + +private string getPrettyName(AstNode node) { + result = getPrettyName1(node) + or + not exists(getPrettyName1(node)) and + result = node.toString() +} + +class NamespaceObject extends TNamespaceObject { + predicate isAllocationSite(AstNode node) { this = MkAllocationSite(node) } + + predicate isPrototype(Function f) { this = MkPrototype(f) } + + predicate isModuleObject(TopLevel top) { this = MkModuleObject(top) } + + predicate isModuleExportsObject(TopLevel top) { this = MkModuleExportsObject(top) } + + AstNode asAstNode() { + this.isAllocationSite(result) or + this.isPrototype(result) or + this.isModuleObject(result) or + this.isModuleExportsObject(result) + } + + string toString() { + exists(AstNode node | + this.isAllocationSite(node) and + result = getPrettyName(node) + or + this.isPrototype(node) and + result = "[prototype] " + getPrettyName(node) + ) + or + exists(TopLevel top | + this.isModuleObject(top) and + result = "[module] " + top + ) + or + exists(TopLevel top | + this.isModuleExportsObject(top) and + result = "[module.exports] " + top + ) + } + + Location getLocation() { result = this.asAstNode().getLocation() } + + StmtContainer getEnclosingCallable() { + result = this.asAstNode().getContainer() or result = this.asAstNode().(TopLevel) + } +} + +module NamespaceObject { + NamespaceObject allocationSite(AstNode node) { result.isAllocationSite(node) } + + NamespaceObject prototype(Function f) { result.isPrototype(f) } + + NamespaceObject moduleObject(TopLevel top) { result.isModuleObject(top) } + + NamespaceObject moduleExportsObject(TopLevel top) { result.isModuleExportsObject(top) } +} + +class ClassLikeObject extends NamespaceObject { + private Function f; + + ClassLikeObject() { + this.isAllocationSite(f) and + any(NamespaceObject obj).isPrototype(f) + } + + NamespaceObject getInstancePrototype() { result.isPrototype(f) } + + Function getConstructor() { result = f } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/PathResolution.qll b/javascript/ql/lib/semmle/javascript/internal/unified/PathResolution.qll new file mode 100644 index 000000000000..21e73e378d78 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/PathResolution.qll @@ -0,0 +1,256 @@ +overlay[local] +module; + +private import minimal.minimal +private import JSUnified +private import DataFlowBuilder +private import Contents +private import minimal.JSPaths +private import minimal.PathMapping +private import minimal.PathConcatenation +private import semmle.javascript.internal.unified.Constant + +private predicate moduleSystemAliases(string name) { name = ["require"] } + +private module ModuleSystemAliases = ModelsAsData::EvaluatePreCallGraph; + +class RequireCall extends CallExpr { + RequireCall() { ModuleSystemAliases::getAnAliasSource("require").isValueOf(this.getCallee()) } + + string getImportedString() { result = PathStrings::getValueOfExpr(this.getArgument(0)) } +} + +predicate pathResolutionStep(DataFlow2::Node path, DataFlow2::Node importedModule) { + exists(RequireCall call | + path.isValueOf(call.getArgument(0)) and + importedModule.isValueOf(call) + ) + or + exists(AstNode tagged | + path.isValueOf(tagged) and + importedModule.isSyntheticNode(tagged, "imported-module") + ) +} + +private module PathStringsInput implements ValuePropagationInputSig { + class Value extends string { + bindingset[this] + Value() { this.length() < 200 } + } + + predicate shouldComputeValue(DataFlow2::Node node) { pathResolutionStep(node, _) } + + predicate allowMayFlow() { none() } + + private import PathStrings + + language[monotonicAggregates] + private string getValueFromConcat(PathConcatenation c) { + result = + concat(Expr e, int i | e = c.getOperand(i) | getValueOfExpr(e), c.getSeparator() order by i) + } + + private class PathStringFolding extends ValuePropagationRule { + bindingset[node] + override DataFlow2::Node getADependency(DataFlow2::Node node) { + exists(AddExpr binary | + node.isValueOf(binary) and + result.isValueOf(binary.getAnOperand()) + ) + or + exists(AssignAddExpr add | + node.isIncomingValue(add.getTarget()) and + result.isValueOf([add.getTarget(), add.getRhs()]) + ) + } + + bindingset[node] + override Value computeValue(DataFlow2::Node node) { + exists(PathConcatenation c | + node.isValueOf(c) and + result = getValueFromConcat(c) + ) + or + // Compound assignments must be handled here since the AST-based abstraction in 'PathConcatenation' cannot handle them + exists(AssignAddExpr add | + node.isIncomingValue(add.getTarget()) and + result = getValueOfExpr(add.getTarget()) + getValueOfExpr(add.getRhs()) + ) + or + exists(StringLiteral expr | + node.isValueOf(expr) and + result = expr.getStringValue() + ) + } + } +} + +module PathStrings = MakeValuePropagation; + +private class RelevantNode extends DataFlow2::Node { + string getValue() { result = PathStrings::getValue(this) } + + /** Gets a path mapping affecting this path. */ + overlay[global] + pragma[nomagic] + PathMapping getAPathMapping() { result.getAnAffectedFile() = this.getFile() } + + /** Gets the NPM package name from the beginning of this path. */ + overlay[global] + pragma[nomagic] + string getPackagePrefix() { result = this.getValue().(FilePath).getPackagePrefix() } + + File getFile() { result = this.getLocation().getFile() } +} + +/** + * Holds if `expr` matches a path mapping, and should thus be resolved as `newPath` relative to `base`. + */ +overlay[global] +pragma[nomagic] +private predicate resolveViaPathMapping(RelevantNode expr, Container base, string newPath) { + // Handle path mappings such as `{ "paths": { "@/*": "./src/*" }}` in a tsconfig.json file + exists(PathMapping mapping, string value | + mapping = expr.getAPathMapping() and + value = expr.getValue() + | + mapping.hasExactPathMapping(value, base, newPath) + or + exists(string pattern, string suffix, string mappedPath | + mapping.hasPrefixPathMapping(pattern, base, mappedPath) and + value = pattern + suffix and + newPath = mappedPath + suffix + ) + ) + or + // Handle imports referring to a package by name, where we have a package.json + // file for that package in the codebase. This is treated separately from PathMapping for performance + // reasons, as there can be a large number of packages which affect all files in the project. + // + // This part only handles the "exports" property of package.json. "main" and "modules" are + // handled further down because their semantics are easier to handle there. + exists(PackageJsonEx pkg, string packageName, string remainder | + packageName = expr.getPackagePrefix() and + pkg.getDeclaredPackageName() = packageName and + remainder = expr.getValue().suffix(packageName.length()).regexpReplaceAll("^[/\\\\]", "") + | + // "exports": { ".": "./foo.js" } + // "exports": { "./foo.js": "./foo/impl.js" } + pkg.hasExactPathMappingTo(remainder, base) and + newPath = "" + or + // "exports": { "./*": "./foo/*" } + exists(string prefix | + pkg.hasPrefixPathMappingTo(prefix, base) and + remainder = prefix + newPath + ) + ) +} + +overlay[global] +pragma[noopt] +private predicate relativePathExpr(RelevantNode expr, Container base, FilePath path) { + expr instanceof RelevantNode and + path = expr.getValue() and + path.isDotRelativePath() and + exists(File file | + file = expr.getFile() and + base = file.getParentContainer() + ) +} + +overlay[global] +pragma[nomagic] +private Container getJSDocProvidedModule(string moduleName) { + exists(JSDocTag tag | + tag.getTitle() = "providesModule" and + tag.getDescription().trim() = moduleName and + tag.getFile() = result + ) +} + +/** + * Holds if `expr` should be resolved as `path` relative to `base`. + */ +overlay[global] +pragma[nomagic] +private predicate shouldResolve(RelevantNode expr, Container base, FilePath path) { + // Relative paths are resolved from their enclosing folder + relativePathExpr(expr, base, path) + or + resolveViaPathMapping(expr, base, path) + or + // Resolve from baseUrl of relevant tsconfig.json file + path = expr.getValue() and + not path.isDotRelativePath() and + expr.getAPathMapping().hasBaseUrl(base) + or + // If the path starts with the name of a package, resolve relative to the directory of that package. + // Note that `getFileFromFolderImport` may subsequently redirect this to the package's "main", + // so we don't have to deal with that here. + exists(PackageJson pkg, string packageName | + packageName = expr.getPackagePrefix() and + pkg.getDeclaredPackageName() = packageName and + path = expr.getValue().suffix(packageName.length()) and + base = pkg.getFolder() + ) + or + base = getJSDocProvidedModule(expr.getValue()) and + path = "" +} + +overlay[global] +private module ResolverConfig implements Folder::ResolveSig { + predicate shouldResolve(Container base, string path) { shouldResolve(_, base, path) } + + predicate getAnAdditionalChild = JSPaths::getAnAdditionalChild/2; +} + +private module Resolver = Folder::Resolve; + +overlay[global] +private Container resolvePathExpr1(RelevantNode expr) { + exists(Container base, string path | + shouldResolve(expr, base, path) and + result = Resolver::resolve(base, path) + ) +} + +/** + * Gets the file to import when an imported path resolves to the given `folder`. + */ +overlay[global] +File getFileFromFolderImport(Folder folder) { + result = folder.getJavaScriptFileOrTypings("index") + or + // Note that unlike "exports" paths, "main" and "module" also take effect when the package + // is imported via a relative path, e.g. `require("..")` targeting a folder with a package.json file. + exists(PackageJsonEx pkg | + pkg.getFolder() = folder and + result = pkg.getMainFileOrBestGuess() + ) +} + +overlay[global] +File resolveExpr(RelevantNode expr) { + result = resolvePathExpr1(expr) + or + result = getFileFromFolderImport(resolvePathExpr1(expr)) +} + +overlay[global] +private predicate importStepAux( + DataFlow2::Node pathNode, DataFlow2::Node node1, DataFlow2::Node node2 +) { + exists(TopLevel target | + pathResolutionStep(pathNode, node2) and + resolveExpr(pathNode) = target.getFile() and + node1.isNamespaceObject(NamespaceObject::moduleObject(target)) + ) +} + +overlay[global] +predicate importStep(DataFlow2::Node node1, DataFlowBuilder::Step step, DataFlow2::Node node2) { + importStepAux(_, node1, node2) and + step.read(Contents::property("exports")) +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/Transforms.qll b/javascript/ql/lib/semmle/javascript/internal/unified/Transforms.qll new file mode 100644 index 000000000000..75a78bc2a3f8 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/Transforms.qll @@ -0,0 +1,143 @@ +overlay[local] +module; + +private import minimal.minimal +private import JSUnified +private import DataFlowBuilder +private import Contents + +private module Raw { + newtype TTransform = + ToPromise() or + AwaitValue() or + ReadIterableContents() or + ReadAsyncIterableContents() or + ToAsyncIteratorElement() or + ShiftArrayElements(int amount) { amount = [-10 .. 10] } or + ToArray() +} + +class TransformBase = Raw::TTransform; + +private class ToPromise extends Transform, Raw::ToPromise { + override string toString() { result = "ToPromise" } + + override predicate step(string node1, Step step, string node2) { + node1 = "input" and step.withoutContent(anyPromiseContent()) and node2 = "mid" + or + node1 = "mid" and step.store(promiseValue()) and node2 = "output" + or + node1 = "input" and step.withContent(anyPromiseContent()) and node2 = "output" + } +} + +private class AwaitValue extends Transform, Raw::AwaitValue { + override string toString() { result = "AwaitValue" } + + override predicate step(string node1, Step step, string node2) { + node1 = "input" and step.withoutContent(anyPromiseContent()) and node2 = "output" + or + node1 = "input" and step.read(promiseValue()) and node2 = "output" + } +} + +private class ReadIterableContents extends Transform, Raw::ReadIterableContents { + override string toString() { result = "ReadIterableContents" } + + override predicate step(string node1, Step step, string node2) { + node1 = "input" and step.read(ArrayContent::anyElement()) and node2 = "output" + or + node1 = "input" and step.read(Contents::iteratorElement()) and node2 = "output" + or + node1 = "input" and step.read(MapContent::key()) and node2 = "map-key" + or + node1 = "map-key" and step.store(ArrayContent::elementAt(0)) and node2 = "output" + or + node1 = "input" and step.read(MapContent::value()) and node2 = "map-value" + or + node1 = "map-value" and + step.store(ArrayContent::elementAt(1)) and + node2 = "output" + or + node1 = "input" and step.taint() and node2 = "output" + } +} + +private class ReadAsyncIterableContents extends Transform, Raw::ReadAsyncIterableContents { + override string toString() { result = "ReadAsyncIterableContents" } + + override predicate step(string node1, Step step, string node2) { + node1 = "input" and step.transform(Transforms::readIterableContents()) and node2 = "value" + or + node1 = "value" and step.transform(Transforms::awaitValue()) and node2 = "output" + } +} + +private class ToAsyncIteratorElement extends Transform, Raw::ToAsyncIteratorElement { + override string toString() { result = "ToAsyncIteratorElement" } + + override predicate step(string node1, Step step, string node2) { + node1 = "input" and step.withoutContent(anyPromiseContent()) and node2 = "raw-value" + or + node1 = "raw-value" and step.store(promiseValue()) and node2 = "promise-value" + or + node1 = "input" and step.withContent(promiseValue()) and node2 = "promise-value" + or + node1 = "promise-value" and step.store(iteratorElement()) and node2 = "output" + or + node1 = "input" and step.read(promiseError()) and node2 = "promise-error" + or + node1 = "promise-error" and step.store(iteratorError()) and node2 = "output" + } +} + +private class ShiftArrayElements extends Transform, Raw::ShiftArrayElements { + private int amount; + + ShiftArrayElements() { this = Raw::ShiftArrayElements(amount) } + + override string toString() { result = "ShiftArrayElements(" + amount + ")" } + + override predicate step(string node1, Step step, string node2) { + StandardTransforms::ShiftArrayContent::step(amount, node1, step, node2) + } +} + +private class ToArray extends Transform, Raw::ToArray { + override string toString() { result = "ToArray" } + + override predicate step(string node1, Step step, string node2) { + node1 = "input" and step.withContent(ArrayContent::anyElement()) and node2 = "output" + or + node1 = "input" and step.taint() and node2 = "tainted" + or + node1 = "tainted" and step.store(ArrayContent::anyElement()) and node2 = "output" + } +} + +module Transforms { + /** Wraps a non-promise value in a succesful promise, and preserves existing promises as they are (including failed promises). */ + Transform toPromise() { result instanceof ToPromise } + + /** Maps a resolved promise to its value, non-promise values are preserved, and failed promises are blocked. */ + Transform awaitValue() { result instanceof AwaitValue } + + /** Maps an iterable value to its contents. */ + Transform readIterableContents() { result instanceof ReadIterableContents } + + /** Maps an async iterable value to its contents. */ + Transform readAsyncIterableContents() { result instanceof ReadAsyncIterableContents } + + /** + * Coerces the incoming value to a promise and then wraps it in an iterator element. + * + * A failed promise is converted to a failed iterator. + */ + Transform toAsyncIteratorElement() { result instanceof ToAsyncIteratorElement } + + /** Shifts array elements up by the given amount, or down if `amount` is negative. */ + Transform shiftArrayElements(int amount) { result = Raw::ShiftArrayElements(amount) } + + /** Preserves array contents, and boxes taint in an unknown array element. */ + Transform toArray() { result instanceof ToArray } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/AST.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/AST.qll new file mode 100644 index 000000000000..6e28c93df573 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/AST.qll @@ -0,0 +1,468 @@ +/** + * Provides classes for working with the AST-based representation of JavaScript programs. + */ +overlay[local?] +module; + +import minimal +private import StmtContainers + +/** + * A program element corresponding to JavaScript code, such as an expression + * or a statement. + * + * This class provides generic traversal methods applicable to all AST nodes, + * such as obtaining the children of an AST node. + * + * Examples: + * + * ``` + * function abs(x) { + * return x < 0 ? -x : x; + * } + * abs(-42); + * ``` + */ +class AstNode extends @ast_node, NodeInStmtContainer { + override File getFile() { + result = this.getLocation().getFile() // Specialized for performance reasons + } + + /** Gets the first token belonging to this element. */ + Token getFirstToken() { + exists(Location l1, Location l2, string filepath, int startline, int startcolumn | + l1 = this.getLocation() and + l2 = result.getLocation() and + l1.hasLocationInfo(filepath, startline, startcolumn, _, _) and + l2.hasLocationInfo(filepath, startline, startcolumn, _, _) + ) + } + + /** Gets the last token belonging to this element. */ + Token getLastToken() { + exists(Location l1, Location l2, string filepath, int endline, int endcolumn | + l1 = this.getLocation() and + l2 = result.getLocation() and + l1.hasLocationInfo(filepath, _, _, endline, endcolumn) and + l2.hasLocationInfo(filepath, _, _, endline, endcolumn) + ) and + // exclude empty EOF token + not result instanceof EOFToken + } + + /** Gets a token belonging to this element. */ + Token getAToken() { + exists(string path, int sl, int sc, int el, int ec, int tksl, int tksc, int tkel, int tkec | + this.getLocation().hasLocationInfo(path, sl, sc, el, ec) and + result.getLocation().hasLocationInfo(path, tksl, tksc, tkel, tkec) + | + ( + sl < tksl + or + sl = tksl and sc <= tksc + ) and + ( + tkel < el + or + tkel = el and tkec <= ec + ) + ) and + // exclude empty EOF token + not result instanceof EOFToken + } + + /** Gets the toplevel syntactic unit to which this element belongs. */ + cached + TopLevel getTopLevel() { result = this.getParent().getTopLevel() } + + /** + * Gets the `i`th child node of this node. + * + * _Note_: The indices of child nodes are considered an implementation detail and may + * change between versions of the extractor. + */ + AstNode getChild(int i) { + result = this.getChildExpr(i) or + result = this.getChildStmt(i) or + properties(result, this, i, _, _) or + result = this.getChildTypeExpr(i) + } + + /** Gets the `i`th child statement of this node. */ + Stmt getChildStmt(int i) { stmts(result, _, this, i, _) } + + /** Gets the `i`th child expression of this node. */ + Expr getChildExpr(int i) { exprs(result, _, this, i, _) } + + /** Gets the `i`th child type expression of this node. */ + TypeExpr getChildTypeExpr(int i) { typeexprs(result, _, this, i, _) } + + /** Gets a child node of this node. */ + AstNode getAChild() { result = this.getChild(_) } + + /** Gets a child expression of this node. */ + Expr getAChildExpr() { result = this.getChildExpr(_) } + + /** Gets a child statement of this node. */ + Stmt getAChildStmt() { result = this.getChildStmt(_) } + + /** Gets the number of child nodes of this node. */ + int getNumChild() { result = count(this.getAChild()) } + + /** Gets the number of child expressions of this node. */ + int getNumChildExpr() { result = count(this.getAChildExpr()) } + + /** Gets the number of child statements of this node. */ + int getNumChildStmt() { result = count(this.getAChildStmt()) } + + /** Gets the parent node of this node, if any. */ + cached + AstNode getParent() { this = result.getAChild() } + + /** Gets the first control flow node belonging to this syntactic entity. */ + ControlFlowNode getFirstControlFlowNode() { result = this } + + /** Holds if this syntactic entity belongs to an externs file. */ + predicate inExternsFile() { this.getTopLevel().isExterns() } + + /** + * Holds if this is an ambient node that is not a `TypeExpr` and is not inside a `.d.ts` file + * + * Since the overwhelming majority of ambient nodes are `TypeExpr` or inside `.d.ts` files, + * we avoid caching them. + */ + cached + private predicate isAmbientInternal() { + this.getParent().isAmbientInternal() + or + not isAmbientTopLevel(this.getTopLevel()) and + ( + this instanceof ExternalModuleDeclaration + or + this instanceof GlobalAugmentationDeclaration + or + this instanceof ExportAsNamespaceDeclaration + or + this instanceof TypeAliasDeclaration + or + this instanceof InterfaceDeclaration + or + has_declare_keyword(this) + or + has_type_keyword(this) + or + // An export such as `export declare function f()` should be seen as ambient. + has_declare_keyword(this.(ExportNamedDeclaration).getOperand()) + or + exists(Function f | + this = f and + not f.hasBody() + ) + ) + } + + /** + * Holds if this is part of an ambient declaration or type annotation in a TypeScript file. + * + * A declaration is ambient if it occurs under a `declare` modifier or is + * an interface declaration, type alias, type annotation, or type-only import/export declaration. + * + * The TypeScript compiler emits no code for ambient declarations, but they + * can affect name resolution and type checking at compile-time. + */ + overlay[caller?] + pragma[inline] + predicate isAmbient() { + this.isAmbientInternal() + or + isAmbientTopLevel(this.getTopLevel()) + or + this instanceof TypeExpr + } +} + +/** + * Holds if the given file is a `.d.ts` file. + */ +cached +private predicate isAmbientTopLevel(TopLevel tl) { tl.getFile().getBaseName().matches("%.d.ts") } + +/** + * A toplevel syntactic unit; that is, a stand-alone script, an inline script + * embedded in an HTML ` + * ``` + */ +class TopLevel extends @toplevel, StmtContainer { + /** Holds if this toplevel is minified. */ + cached + predicate isMinified() { + // file name contains 'min' (not as part of a longer word) + this.getFile().getBaseName().regexpMatch(".*[^-._]*[-._]min([-._].*)?\\.\\w+") + or + exists(int numstmt | numstmt = strictcount(Stmt s | s.getTopLevel() = this) | + // there are more than two statements per line on average + numstmt.(float) / this.getNumberOfLines() > 2 and + // and there are at least ten statements overall + numstmt >= 10 + ) + or + // many variables, and they all have short names + count(VarDecl d | d.getTopLevel() = this) > 100 and + forall(VarDecl d | d.getTopLevel() = this | d.getName().length() <= 2) + } + + /** Holds if this toplevel is an externs definitions file. */ + predicate isExterns() { + // either it was explicitly extracted as an externs file... + is_externs(this) + or + // ...or it has a comment with an `@externs` tag in it + exists(JSDocTag externs | + externs.getTitle() = "externs" and + externs.getTopLevel() = this + ) + } + + /** Gets the toplevel to which this element belongs, that is, itself. */ + override TopLevel getTopLevel() { result = this } + + /** Gets the number of lines in this toplevel. */ + int getNumberOfLines() { numlines(this, result, _, _) } + + /** Gets the number of lines containing code in this toplevel. */ + int getNumberOfLinesOfCode() { numlines(this, _, result, _) } + + /** Gets the number of lines containing comments in this toplevel. */ + int getNumberOfLinesOfComments() { numlines(this, _, _, result) } + + override predicate isStrict() { this.getAStmt() instanceof Directive::StrictModeDecl } + + override ControlFlowNode getFirstControlFlowNode() { result = this.getEntry() } + + override string toString() { result = "" } +} + +/** + * A stand-alone file or script originating from an HTML ` + * ``` + */ +class Script extends TopLevel { + Script() { this instanceof @script or this instanceof @inline_script } +} + +/** + * A stand-alone file or an external script originating from an HTML ` + * ``` + */ +class InlineScript extends @inline_script, Script { } + +/** + * A code snippet originating from an HTML attribute value. + * + * Examples: + * + * ``` + *
Click me
+ * Click me + * ``` + */ +class CodeInAttribute extends TopLevel { + CodeInAttribute() { + this instanceof @event_handler or + this instanceof @javascript_url or + this instanceof @template_toplevel + } +} + +/** + * A code snippet originating from an event handler attribute. + * + * Example: + * + * ``` + *
Click me
+ * ``` + */ +class EventHandlerCode extends @event_handler, CodeInAttribute { } + +/** + * A code snippet originating from a URL with the `javascript:` URL scheme. + * + * Example: + * + * ``` + * Click me + * ``` + */ +class JavaScriptUrl extends @javascript_url, CodeInAttribute { } + +/** + * A toplevel syntactic entity containing Closure-style externs definitions. + * + * Example: + * + *
+ * /** @externs */
+ * /** @typedef {String} */
+ * var MyString;
+ * 
+ */ +class Externs extends TopLevel { + Externs() { this.isExterns() } +} + +/** + * A program element that is either an expression or a statement. + * + * Examples: + * + * ``` + * var i = 0; + * i = 9 + * ``` + */ +class ExprOrStmt extends @expr_or_stmt, ControlFlowNode, AstNode { } + +/** + * A program element that contains statements, but isn't itself + * a statement, in other words a toplevel or a function. + * + * Example: + * + * ``` + * function f() { + * g(); + * } + * ``` + */ +class StmtContainer extends @stmt_container, AstNode { + /** Gets the innermost enclosing container in which this container is nested. */ + cached + StmtContainer getEnclosingContainer() { none() } + + /** + * Gets the innermost enclosing function or top-level, + * possibly this container itself if it is a function or top-level. + * + * To get a strictly enclosing function or top-level, use + * `getEnclosingContainer().getFunctionBoundary()`. + * + * TypeScript namespace declarations are containers that are not considered + * function boundaries. In plain JavaScript, all containers are function boundaries. + */ + StmtContainer getFunctionBoundary() { + if this instanceof Function or this instanceof TopLevel + then result = this + else result = this.getEnclosingContainer().getFunctionBoundary() + } + + /** Gets a statement that belongs to this container. */ + Stmt getAStmt() { result.getContainer() = this } + + /** + * Gets the body of this container. + * + * For scripts or modules, this is the container itself; for functions, + * it is the function body. + */ + AstNode getBody() { result = this } + + /** + * Gets the (unique) entry node of the control flow graph for this toplevel or function. + * + * For most purposes, the start node should be used instead of the entry node; + * see predicate `getStart()`. + */ + ControlFlowEntryNode getEntry() { result.getContainer() = this } + + /** Gets the (unique) exit node of the control flow graph for this toplevel or function. */ + ControlFlowExitNode getExit() { result.getContainer() = this } + + /** + * Gets the (unique) CFG node at which execution of this toplevel or function begins. + * + * Unlike the entry node, which is a synthetic construct, the start node corresponds to + * an actual program element, such as the first statement of a toplevel or the first + * parameter of a function. + * + * Empty toplevels do not have a start node. + */ + ConcreteControlFlowNode getStart() { successor(this.getEntry(), result) } + + /** + * Gets the entry basic block of this function, that is, the basic block + * containing the entry node of its CFG. + */ + EntryBasicBlock getEntryBB() { result = this.getEntry() } + + /** + * Gets the start basic block of this function, that is, the basic block + * containing the start node of its CFG. + */ + BasicBlock getStartBB() { result.getANode() = this.getStart() } + + /** Gets the scope induced by this toplevel or function, if any. */ + Scope getScope() { scopenodes(this, result) } + + /** + * Holds if the code in this container is executed in ECMAScript strict mode. + * + * See Annex C of the ECMAScript language specification. + */ + predicate isStrict() { this.getEnclosingContainer().isStrict() } +} + +/** + * Provides a class `ValueNode` encompassing all program elements that evaluate to + * a value at runtime. + */ +module AST { + /** + * A program element that evaluates to a value or destructures a value at runtime. + * This includes expressions and destructuring patterns, but also function and + * class declaration statements, as well as TypeScript namespace and enum declarations. + * + * Examples: + * + * ``` + * 0 // expression + * (function id(x) { return x; }) // parenthesized function expression + * function id(x) { return x; } // function declaration + * ``` + */ + class ValueNode extends AstNode, @dataflownode { } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/BasicBlockInternal.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/BasicBlockInternal.qll new file mode 100644 index 000000000000..d98347a3a7f2 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/BasicBlockInternal.qll @@ -0,0 +1,263 @@ +/** + * Provides classes for working with basic blocks, and predicates for computing + * liveness information for local variables. + */ +overlay[local?] +module; + +import minimal +private import StmtContainers +private import codeql.controlflow.BasicBlock as BB + +/** + * Holds if `nd` starts a new basic block. + */ +private predicate startsBB(ControlFlowNode nd) { + not exists(nd.getAPredecessor()) and exists(nd.getASuccessor()) + or + nd.isJoin() + or + nd.getAPredecessor().isBranch() +} + +/** + * Holds if the first node of basic block `succ` is a control flow + * successor of the last node of basic block `bb`. + */ +private predicate succBB(BasicBlock bb, BasicBlock succ) { succ = bb.getLastNode().getASuccessor() } + +/** + * Holds if the first node of basic block `bb` is a control flow + * successor of the last node of basic block `pre`. + */ +private predicate predBB(BasicBlock bb, BasicBlock pre) { succBB(pre, bb) } + +/** Holds if `bb` is an entry basic block. */ +private predicate entryBB(BasicBlock bb) { bb.getFirstNode() instanceof ControlFlowEntryNode } + +/** Holds if `bb` is an exit basic block. */ +private predicate exitBB(BasicBlock bb) { bb.getLastNode() instanceof ControlFlowExitNode } + +cached +private module Cached { + /** + * Holds if `succ` is a control flow successor of `nd` within the same basic block. + */ + private predicate intraBBSucc(ControlFlowNode nd, ControlFlowNode succ) { + succ = nd.getASuccessor() and + not succ instanceof BasicBlock + } + + /** + * Holds if `nd` is the `i`th node in basic block `bb`. + * + * In other words, `i` is the shortest distance from a node `bb` + * that starts a basic block to `nd` along the `intraBBSucc` relation. + */ + cached + predicate bbIndex(BasicBlock bb, ControlFlowNode nd, int i) = + shortestDistances(startsBB/1, intraBBSucc/2)(bb, nd, i) + + cached + int bbLength(BasicBlock bb) { result = strictcount(ControlFlowNode nd | bbIndex(bb, nd, _)) } + + cached + predicate reachableBB(BasicBlock bb) { + entryBB(bb) + or + exists(BasicBlock predBB | succBB(predBB, bb) | reachableBB(predBB)) + } +} + +private import Cached + +/** Gets the immediate dominator of `bb`. */ +cached +BasicBlock immediateDominator(BasicBlock bb) = idominance(entryBB/1, succBB/2)(_, result, bb) + +/** Gets the immediate post-dominator of `bb`. */ +cached +BasicBlock immediatePostDominator(BasicBlock bb) = idominance(exitBB/1, predBB/2)(_, result, bb) + +import Public + +module Public { + /** + * A basic block, that is, a maximal straight-line sequence of control flow nodes + * without branches or joins. + * + * At the database level, a basic block is represented by its first control flow node. + */ + class BasicBlock extends @cfg_node, NodeInStmtContainer { + cached + BasicBlock() { startsBB(this) } + + /** Gets a basic block succeeding this one. */ + BasicBlock getASuccessor() { succBB(this, result) } + + /** Gets a basic block preceding this one. */ + BasicBlock getAPredecessor() { result.getASuccessor() = this } + + /** Gets a node in this block. */ + ControlFlowNode getANode() { result = this.getNode(_) } + + /** Gets the node at the given position in this block. */ + ControlFlowNode getNode(int pos) { bbIndex(this, result, pos) } + + /** Gets the first node in this block. */ + ControlFlowNode getFirstNode() { result = this } + + /** Gets the last node in this block. */ + ControlFlowNode getLastNode() { result = this.getNode(this.length() - 1) } + + /** Gets the length of this block. */ + int length() { result = bbLength(this) } + + /** + * Gets the basic block that immediately dominates this basic block. + */ + ReachableBasicBlock getImmediateDominator() { result = immediateDominator(this) } + + /** + * Holds if this if a basic block whose last node is an exit node. + */ + predicate isExitBlock() { exitBB(this) } + } + + /** + * An unreachable basic block, that is, a basic block + * whose first node is unreachable. + */ + class UnreachableBlock extends BasicBlock { + UnreachableBlock() { this.getFirstNode().isUnreachable() } + } + + /** + * An entry basic block, that is, a basic block + * whose first node is the entry node of a statement container. + */ + class EntryBasicBlock extends BasicBlock { + EntryBasicBlock() { entryBB(this) } + } + + /** + * A basic block that is reachable from an entry basic block. + */ + class ReachableBasicBlock extends BasicBlock { + ReachableBasicBlock() { reachableBB(this) } + + /** + * Holds if this basic block strictly dominates `bb`. + */ + overlay[caller?] + pragma[inline] + predicate strictlyDominates(ReachableBasicBlock bb) { this = immediateDominator+(bb) } + + /** + * Holds if this basic block dominates `bb`. + * + * This predicate is reflexive: each reachable basic block dominates itself. + */ + overlay[caller?] + pragma[inline] + predicate dominates(ReachableBasicBlock bb) { this = immediateDominator*(bb) } + + /** + * Holds if this basic block strictly post-dominates `bb`. + */ + overlay[caller?] + pragma[inline] + predicate strictlyPostDominates(ReachableBasicBlock bb) { this = immediatePostDominator+(bb) } + + /** + * Holds if this basic block post-dominates `bb`. + * + * This predicate is reflexive: each reachable basic block post-dominates itself. + */ + overlay[caller?] + pragma[inline] + predicate postDominates(ReachableBasicBlock bb) { this = immediatePostDominator*(bb) } + } + + /** + * A reachable basic block with more than one predecessor. + */ + class ReachableJoinBlock extends ReachableBasicBlock { + ReachableJoinBlock() { this.getFirstNode().isJoin() } + + /** + * Holds if this basic block belongs to the dominance frontier of `b`, that is + * `b` dominates a predecessor of this block, but not this block itself. + * + * Algorithm from Cooper et al., "A Simple, Fast Dominance Algorithm" (Figure 5), + * who in turn attribute it to Ferrante et al., "The program dependence graph and + * its use in optimization". + */ + predicate inDominanceFrontierOf(ReachableBasicBlock b) { + b = this.getAPredecessor() and not b = this.getImmediateDominator() + or + exists(ReachableBasicBlock prev | this.inDominanceFrontierOf(prev) | + b = prev.getImmediateDominator() and + not b = this.getImmediateDominator() + ) + } + } + + final private class FinalBasicBlock = BasicBlock; + + module Cfg implements BB::CfgSig { + private import minimal as Js + private import codeql.controlflow.SuccessorType + + class ControlFlowNode = Js::ControlFlowNode; + + private predicate conditionSucc(BasicBlock bb1, BasicBlock bb2, boolean branch) { + exists(ConditionGuardNode g | + bb1 = g.getTest().getBasicBlock() and + bb2 = g.getBasicBlock() and + branch = g.getOutcome() + ) + } + + class BasicBlock extends FinalBasicBlock { + BasicBlock getASuccessor() { result = super.getASuccessor() } + + BasicBlock getASuccessor(SuccessorType t) { + conditionSucc(this, result, t.(BooleanSuccessor).getValue()) + or + result = super.getASuccessor() and + t instanceof DirectSuccessor and + not conditionSucc(this, result, _) + } + + predicate strictlyDominates(BasicBlock bb) { + this.(ReachableBasicBlock).strictlyDominates(bb) + } + + predicate dominates(BasicBlock bb) { this.(ReachableBasicBlock).dominates(bb) } + + predicate inDominanceFrontier(BasicBlock df) { + df.(ReachableJoinBlock).inDominanceFrontierOf(this) + } + + BasicBlock getImmediateDominator() { result = super.getImmediateDominator() } + + predicate strictlyPostDominates(BasicBlock bb) { + this.(ReachableBasicBlock).strictlyPostDominates(bb) + } + + predicate postDominates(BasicBlock bb) { this.(ReachableBasicBlock).postDominates(bb) } + } + + class EntryBasicBlock extends BasicBlock { + EntryBasicBlock() { entryBB(this) } + } + + pragma[nomagic] + predicate dominatingEdge(BasicBlock bb1, BasicBlock bb2) { + bb1.getASuccessor() = bb2 and + bb1 = bb2.getImmediateDominator() and + forall(BasicBlock pred | pred = bb2.getAPredecessor() and pred != bb1 | bb2.dominates(pred)) + } + } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/BasicBlocks.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/BasicBlocks.qll new file mode 100644 index 000000000000..3706c643f881 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/BasicBlocks.qll @@ -0,0 +1,6 @@ +/** + * Provides classes for working with basic blocks, and predicates for computing + * liveness information for local variables. + */ + +import BasicBlockInternal::Public diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/CFG.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/CFG.qll new file mode 100644 index 000000000000..898e22829bc9 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/CFG.qll @@ -0,0 +1,423 @@ +/** + * Provides classes for working with a CFG-based program representation. + * + * ## Overview + * + * Each `StmtContainer` (that is, function or toplevel) has an intra-procedural + * CFG associated with it, which is composed of `ControlFlowNode`s under a successor + * relation exposed by predicates `ControlFlowNode.getASuccessor()` and + * `ControlFlowNode.getAPredecessor()`. + * + * Each CFG has designated entry and exit nodes with types + * `ControlFlowEntryNode` and `ControlFlowExitNode`, respectively, which are the only two + * subtypes of `SyntheticControlFlowNode`. All `ControlFlowNode`s that are _not_ + * `SyntheticControlFlowNode`s belong to class `ConcreteControlFlowNode`. + * + * The predicate `ASTNode.getFirstControlFlowNode()` relates AST nodes + * to the first (concrete) CFG node in the sub-graph of the CFG + * corresponding to the node. + * + * Most statement containers also have a _start node_, obtained by + * `StmtContainer.getStart()`, which is the unique CFG node at which execution + * of the toplevel or function begins. Unlike the entry node, which is a synthetic + * construct, the start node corresponds to an AST node: for instance, for + * toplevels, it is the first CFG node of the first statement, and for functions + * with parameters it is the CFG node corresponding to the first parameter. + * + * Empty toplevels do not have a start node, since all their CFG nodes are + * synthetic. + * + * ## CFG Nodes + * + * Non-synthetic CFG nodes exist for six kinds of AST nodes, representing various + * aspects of the program's runtime semantics: + * + * - `Expr`: the CFG node represents the evaluation of the expression, + * including any side effects this may have; + * - `Stmt`: the CFG node represents the execution of the statement; + * - `Property`: the CFG node represents the assignment of the property; + * - `PropertyPattern`: the CFG node represents the matching of the property; + * - `MemberDefinition`: the CFG node represents the definition of the member + * method or field; + * - `MemberSignature`: the CFG node represents the point where the signature + * is declared, although this has no effect at runtime. + * + * ## CFG Structure + * + * ### Expressions + * + * For most expressions, the successor relation visits sub-expressions first, + * and then the expression itself, representing the order of evaluation at + * runtime. For example, the CFG for the expression `23 + 19` is + * + *
+ * … → [23] → [19] → [23 + 19] → …
+ * 
+ * + * In particular, this means that `23` is the first CFG node of the expression + * `23 + 19`. + * + * Similarly, for assignments the left hand side is visited first, then + * the right hand side, then the assignment itself: + * + *
+ * … → [x] → [y] → [x = y] → …
+ * 
+ * + * For properties, the name expression is visited first, then the value, + * then the default value, if any. The same principle applies for getter + * and setter properties: in this case, the "value" is simply the accessor + * function, and there is no default value. + * + * There are only a few exceptions, generally for cases where the value of + * the whole expression is the value of one of its sub-expressions. That + * sub-expression then comes last in the CFG: + * + * - Parenthesized expression: + *
+ * … → [(x)] → [x] → …
+ * 
+ * - Conditional expressions: + *
+ * … →  [x ? y : z]  → [x] ┬→ [y] → … 
+ *                       └→ [z] → … + *
+ * - Short-circuiting operator `&&` (same for `||`): + *
+ * … → [x && y] → [x] → … 
+ *                 ↓
+ *                [y] → … + *
+ * - Sequence/comma expressions: + *
+ * … → [x, y] → [x] → [y] → …
+ * 
+ * + * Finally, array expressions and object expressions also precede their + * sub-expressions in the CFG to model the fact that the new array/object + * is created before its elements/properties are evaluated: + * + *
+ * … → [{ x: 42 }] → [x] → [42] → [x : 42] → …
+ * 
+ * + * ### Statements + * + * For most statements, the successor relation visits the statement first and then + * its sub-expressions and sub-statements. + * + * For example, the CFG of a block statement first visits the individual statements, + * then the block statement itself. + * + * Similarly, the CFG for an `if` statement first visits the statement itself, then + * the condition. The condition, in turn, has the "then" branch as one of its successors + * and the "else" branch (if it exists) or the next statement after the "if" (if it does not) + * as the other: + * + *
+ * … → [if (x) s1 else s2] → [x] ┬→ [s1] → …
+ *                               └→ [s2] → …
+ * 
+ * + * For loops, the CFG reflects the order in which the loop test and the body are + * executed. + * + * For instance, the CFG of a `while` loop starts with the statement itself, followed by + * the condition. The condition has two successors: the body, and the statement following + * the loop. The body, in turn, has the condition as its successor. This reflects the fact + * that `while` loops first test their condition before executing their body: + * + *
+ * … → [while (x) s] → [x] → …
+ *                      ⇅
+ *                     [s]
+ * 
+ * + * On the other hand, `do`-`while` loops first execute their body before testing their condition: + * + *
+ * … → [do s while (x)] → [s] ⇄ [x] → …
+ * 
+ * + * The CFG of a for loop starts with the loop itself, followed by the initializer expression + * (if any), then the test expression (if any). The test expression has two successors: the + * body, and the statement following the loop. The body, in turn, has the update expression + * (if any) as its successor, and the update expression has the test expression as its only + * successor: + * + *
+ * … → [for(i;t;u) s] → [i] → [t] → …
+ *                            ↙ ↖
+ *                         [s] → [u]
+ * 
+ * + * The CFG of a for-in loop `for(x in y) s` starts with the loop itself, followed by the + * iteration domain `y`. That node has two successors: the iterator `x`, and the statement + * following the loop (modeling early exit in case `y` is empty). After the iterator `x` + * comes the loop body `s`, which again has two successors: the iterator `x` (modeling the + * case where there are more elements to iterate over), and the statement following the loop + * (modeling the case where there are no more elements to iterate): + * + *
+ * … → [for(x in y) s] → [y] →  …
+ *                        ↓     ↑
+ *                       [x] ⇄ [s]
+ * 
+ * + * For-of loops are the same. + * + * Finally, `return` and `throw` statements are different from all other statement types in + * that for them the statement itself comes _after_ the operand, reflecting the fact that + * the operand is evaluated before the return or throw is initiated: + * + *
+ * … → [x] → [return x;] → …
+ * 
+ * + * ### Unstructured control flow + * + * Unstructured control flow is modeled in the obvious way: `break` and `continue` statements + * have as their successor the next statement that is executed after the jump; `throw` + * statements have the nearest enclosing `catch` clause as their successor, or the exit node + * of the enclosing container if there is no enclosing `catch`; `return` statements have the + * exit node of the enclosing container as their successor. + * + * In all cases, the control flow may be intercepted by an intervening `finally` block. For + * instance, consider the following code snippet: + * + *
+ * try {
+ *   if (x)
+ *     return;
+ *   s
+ * } finally {
+ *   t
+ * }
+ * u
+ * 
+ * + * Here, the successor of `return` is not the exit node of the enclosing container, but instead + * the `finally` block. The last statement of the `finally` block (here, `t`) has two successors: + * `u` to model the case where `finally` was entered from `s`, and the exit node of the enclosing + * container to model the case where the `return` is resumed after the `finally` block. + * + * Note that `finally` blocks can lead to imprecise control flow modeling since the `finally` + * block resumes the action of _all_ statements it intercepts: in the above example, the CFG + * not only models the executions `return` → `finally` → `t` → `exit` and + * `s` → `finally` → `t` → `u`, but also allows the path `return` → + * `finally` → `t` → `u`, which does not correspond to any actual execution. + * + * The CFG also models the fact that certain kinds of expressions (calls, `new` expressions, + * property accesses and `await` expressions) can throw exceptions, but _only_ if there is + * an enclosing `try`-`catch` statement. + * + * ### Function preambles + * + * The CFG of a function starts with its entry node, followed by a _preamble_, which is a part of + * the CFG that models parameter passing and function hoisting. The preamble is followed by the + * function body, which in turn is followed by the exit node. + * + * For function expressions, the preamble starts with the function name, if any, to reflect the + * fact that the function object is bound to that name inside the scope of the function. Next, + * for both function expressions and function declarations, the parameters are executed in sequence + * to represent parameter passing. If a parameter has a default value, that value is visited before + * the parameter itself. Finally, the CFG nodes corresponding to the names of all hoisted functions + * inside the outer function body are visited in lexical order. This reflects the fact that hoisted + * functions are initialized before the body starts executing, but _after_ parameters have been + * initialized. + * + * For instance, consider the following function declaration: + * + *
+ * function outer(x, y = 42) {
+ *   s
+ *   function inner() {}
+ *   t
+ * }
+ * 
+ * + * Its CFG is + * + *
+ * [entry] → [x] → [42] → [y] → [inner] → [s] → [function inner() {}] → [t] → [exit]
+ * 
+ * + * Note that the function declaration `[function inner() {}]` as a whole is part of the CFG of the + * body of `outer`, while its function identifier `inner` is part of the preamble. + * + * ### Toplevel preambles + * + * Similar to functions, toplevels (that is, modules, scripts or event handlers) also have a + * preamble. For ECMAScript 2015 modules, all import specifiers are traversed first, in lexical + * order, reflecting the fact that imports are resolved before execution of the module itself + * begins; next, for all toplevels, the names of hoisted functions are traversed in lexical order + * (as for functions). Afterwards, the CFG continues with the body of the toplevel, and ends + * with the exit node. + * + * As an example, consider the following module: + * + * ``` + * s + * import x as y from 'foo'; + * function f() {} + * t + * ``` + * + * Its CFG is + * + *
+ * [entry] → [x as y] → [f] → [s] → [import x as y from 'foo';] → [function f() {}] → [t] → [exit]
+ * 
+ * + * Note that the `import` statement as a whole is part of the CFG of the body, while its single + * import specifier `x as y` forms part of the preamble. + */ +overlay[local?] +module; + +import minimal +private import StmtContainers + +/** + * A node in the control flow graph, which is an expression, a statement, + * or a synthetic node. + */ +class ControlFlowNode extends @cfg_node, Locatable, NodeInStmtContainer { + /** Gets a node succeeding this node in the CFG. */ + ControlFlowNode getASuccessor() { successor(this, result) } + + /** Gets a node preceding this node in the CFG. */ + ControlFlowNode getAPredecessor() { this = result.getASuccessor() } + + /** Holds if this is a node with more than one successor. */ + predicate isBranch() { strictcount(this.getASuccessor()) > 1 } + + /** Holds if this is a node with more than one predecessor. */ + predicate isJoin() { strictcount(this.getAPredecessor()) > 1 } + + /** + * Holds if this is a start node, that is, the CFG node where execution of a + * toplevel or function begins. + */ + predicate isStart() { this = any(StmtContainer sc).getStart() } + + /** + * Holds if this is a final node of `container`, that is, a CFG node where execution + * of that toplevel or function terminates. + */ + predicate isAFinalNodeOfContainer(StmtContainer container) { + this.getASuccessor().(SyntheticControlFlowNode).isAFinalNodeOfContainer(container) + } + + /** + * Holds if this is a final node, that is, a CFG node where execution of a + * toplevel or function terminates. + */ + final predicate isAFinalNode() { this.isAFinalNodeOfContainer(_) } + + /** + * Holds if this node is unreachable, that is, it has no predecessors in the CFG. + * Entry nodes are always considered reachable. + * + * Note that in a block of unreachable code, only the first node is unreachable + * in this sense. For instance, in + * + * ``` + * function foo() { return; s1; s2; } + * ``` + * + * `s1` is unreachable, but `s2` is not. + */ + predicate isUnreachable() { + forall(ControlFlowNode pred | pred = this.getAPredecessor() | + pred.(SyntheticControlFlowNode).isUnreachable() + ) + // note the override in ControlFlowEntryNode below + } + + /** Gets the basic block this node belongs to. */ + BasicBlock getBasicBlock() { this = result.getANode() } + + /** + * For internal use. + * + * Gets a string representation of this control-flow node that can help + * distinguish it from other nodes with the same `toString` value. + */ + string describeControlFlowNode() { + if this = any(MethodDeclaration mem).getBody() + then result = "function in " + any(MethodDeclaration mem | mem.getBody() = this) + else + if this instanceof @decorator_list + then result = "parameter decorators of " + this.(AstNode).getParent().(Function).describe() + else result = this.toString() + } +} + +/** + * A synthetic CFG node that does not correspond to a statement or expression; + * examples include guard nodes and entry/exit nodes. + */ +class SyntheticControlFlowNode extends @synthetic_cfg_node, ControlFlowNode { } + +/** A synthetic CFG node marking the entry point of a function or toplevel script. */ +class ControlFlowEntryNode extends SyntheticControlFlowNode, @entry_node { + override predicate isUnreachable() { none() } + + override string toString() { + result = "entry node of " + pragma[only_bind_out](this.getContainer()).toString() + } +} + +/** A synthetic CFG node marking the exit of a function or toplevel script. */ +class ControlFlowExitNode extends SyntheticControlFlowNode, @exit_node { + override predicate isAFinalNodeOfContainer(StmtContainer container) { + exit_cfg_node(this, container) + } + + override string toString() { + result = "exit node of " + pragma[only_bind_out](this.getContainer()).toString() + } +} + +/** + * A synthetic CFG node recording that some condition is known to hold + * at this point in the program. + */ +class GuardControlFlowNode extends SyntheticControlFlowNode, @guard_node { + /** Gets the expression that this guard concerns. */ + Expr getTest() { guard_node(this, _, result) } + + /** + * Holds if this guard dominates basic block `bb`, that is, the guard + * is known to hold at `bb`. + */ + predicate dominates(ReachableBasicBlock bb) { + this = bb.getANode() + or + exists(ReachableBasicBlock prev | prev.strictlyDominates(bb) | this = prev.getANode()) + } +} + +/** + * A guard node recording that some condition is known to be truthy or + * falsy at this point in the program. + */ +class ConditionGuardNode extends GuardControlFlowNode, @condition_guard { + /** Gets the value recorded for the condition. */ + boolean getOutcome() { + guard_node(this, 0, _) and result = false + or + guard_node(this, 1, _) and result = true + } + + override string toString() { result = "guard: " + this.getTest() + " is " + this.getOutcome() } +} + +/** + * A CFG node corresponding to a program element, that is, a CFG node that is + * not a `SyntheticControlFlowNode`. + */ +class ConcreteControlFlowNode extends ControlFlowNode { + ConcreteControlFlowNode() { not this instanceof SyntheticControlFlowNode } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Classes.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Classes.qll new file mode 100644 index 000000000000..015573ad51fb --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Classes.qll @@ -0,0 +1,1303 @@ +/** + * Provides classes for working with ECMAScript 2015 classes. + * + * Class declarations and class expressions are modeled by (QL) classes `ClassDeclaration` + * and `ClassExpression`, respectively, which are both subclasses of `ClassDefinition`. + */ +overlay[local?] +module; + +import minimal + +/** + * An ECMAScript 2015/TypeScript class definition or a TypeScript interface definition, + * including both declarations and expressions. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { + * Rectangle(width, height) { + * this.width = width; + * this.height = height; + * } + * + * area() { return this.width * this.height; } + * } + * + * interface EventEmitter { + * addListener(listener: (x: T) => void): void; + * } + * ``` + */ +class ClassOrInterface extends @class_or_interface, TypeParameterized { + /** Gets the identifier naming the declared type, if any. */ + Identifier getIdentifier() { none() } // Overridden in subtypes. + + /** + * Gets the name of the defined class or interface, possibly inferred + * from the context if this is an anonymous class expression. + * + * Has no result if no name could be determined. + */ + string getName() { + result = this.getIdentifier().getName() // Overridden in ClassExpr + } + + /** Gets a member declared in this class or interface. */ + MemberDeclaration getAMember() { result.getDeclaringType() = this } + + /** Gets the `i`th member declared in this class or interface. */ + MemberDeclaration getMemberByIndex(int i) { properties(result, this, i, _, _) } + + /** Gets the member with the given name declared in this class or interface. */ + MemberDeclaration getMember(string name) { + result = this.getAMember() and + result.getName() = name + } + + /** Gets a method declared in this class or interface. */ + MethodDeclaration getAMethod() { result = this.getAMember() } + + /** + * Gets the method with the given name declared in this class or interface. + * + * Note that for overloaded method signatures in TypeScript files, this returns every overload. + */ + MethodDeclaration getMethod(string name) { result = this.getMember(name) } + + /** Gets an overloaded version of the method with the given name declared in this class or interface. */ + MethodDeclaration getMethodOverload(string name, int overloadIndex) { + result = this.getMethod(name) and + overloadIndex = result.getOverloadIndex() + } + + /** Gets a field declared in this class or interface. */ + FieldDeclaration getAField() { result = this.getAMember() } + + /** Gets the field with the given name declared in this class or interface. */ + FieldDeclaration getField(string name) { result = this.getMember(name) } + + /** Gets a call signature declared in this interface. */ + CallSignature getACallSignature() { result = this.getAMember() } + + /** Gets an index signature declared in this interface. */ + IndexSignature getAnIndexSignature() { result = this.getAMember() } + + /** + * Gets the expression denoting the super class of this class, + * or nothing if this is an interface or a class without an `extends` clause. + */ + Expr getSuperClass() { none() } + + /** + * Gets the `n`th type from the `implements` clause of this class or `extends` clause of this interface, + * starting at 0. + */ + TypeExpr getSuperInterface(int n) { none() } + + /** + * Gets any type from the `implements` clause of this class or `extends` clause of this interface. + */ + TypeExpr getASuperInterface() { result = this.getSuperInterface(_) } + + /** + * Holds if this is an interface or a class declared with the `abstract` modifier. + */ + predicate isAbstract() { none() } + + /** + * Gets a description of this class or interface. + * + * For named types such as `class C { ... }`, this is just the declared + * name. For classes assigned to variables, this is the name of the variable. + * If no meaningful name can be inferred, the result is "anonymous class" or + * "anonymous interface". + */ + override string describe() { none() } // Overridden in subtypes. +} + +/** + * An ECMAScript 2015 or TypeScript class definition, that is, either a class declaration statement + * or a class expression. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { // class declaration statement + * constructor(width, height) { + * this.width = width; + * this.height = height; + * } + * + * area() { return this.width * this.height; } + * } + * + * let C = + * class { // class expression + * constructor() { this.x = 0; } + * + * bump() { return this.x++; } + * }; + * ``` + */ +class ClassDefinition extends @class_definition, ClassOrInterface, AST::ValueNode { + /** Gets the variable holding this class. */ + Variable getVariable() { result = this.getIdentifier().getVariable() } + + /** Gets the identifier naming the defined class, if any. */ + override VarDecl getIdentifier() { result = this.getChildExpr(0) } + + override TypeParameter getTypeParameter(int i) { + // AST indices for type parameters: -3, -6, -9, ... + exists(int astIndex | typeexprs(result, _, this, astIndex, _) | + astIndex <= -3 and astIndex % 3 = 0 and i = -(astIndex + 3) / 3 + ) + } + + /** Gets the expression denoting the super class of the defined class, if any. */ + override Expr getSuperClass() { result = this.getChildExpr(1) } + + /** Gets the `i`th type from the `implements` clause of this class, starting at 0. */ + override TypeExpr getSuperInterface(int i) { + // AST indices for super interfaces: -1, -4, -7, ... + exists(int astIndex | typeexprs(result, _, this, astIndex, _) | + astIndex <= -1 and astIndex % 3 = -1 and i = -(astIndex + 1) / 3 + ) + } + + /** Gets any type from the `implements` clause of this class. */ + override TypeExpr getASuperInterface() { result = ClassOrInterface.super.getASuperInterface() } + + /** + * Gets the constructor of this class. + * + * Note that every class has a constructor: if no explicit constructor + * is declared, it has a synthetic default constructor. + */ + ConstructorDeclaration getConstructor() { result = this.getAMethod() } + + /** + * Gets the `i`th decorator applied to this class. + * + * For example, the class `@A @B class C {}` has + * `@A` as its 0th decorator, and `@B` as its first decorator. + */ + Decorator getDecorator(int i) { + // AST indices for decorators: -2, -5, -8, ... + exists(int astIndex | exprs(result, _, this, astIndex, _) | + astIndex <= -2 and astIndex % 3 = -2 and i = -(astIndex + 2) / 3 + ) + } + + /** + * Gets a decorator applied to this class, if any. + * + * For example, the class `@A @B class C {}` has + * decorators `@A` and `@B`. + */ + Decorator getADecorator() { result = this.getDecorator(_) } + + /** + * Holds if this class has the `abstract` modifier. + */ + override predicate isAbstract() { is_abstract_class(this) } + + override string describe() { + if exists(this.inferNameFromVarDef()) + then result = this.inferNameFromVarDef() + else result = "anonymous class" + } + + /** + * Gets a description of this class, based on its declared name or the name + * of the variable it is assigned to, if any. + */ + private string inferNameFromVarDef() { + // in ambiguous cases like `let C = class D {}`, prefer `D` to `C` + if exists(this.getIdentifier()) + then result = "class " + this.getIdentifier().getName() + else + exists(AssignExpr assign | this = assign.getRhs() | + result = "class " + assign.getTarget().(VarRef).getName() + ) + } + + /** + * Gets an instance method of this class with the given name. + * + * Note that constructors aren't considered instance methods. + */ + Function getInstanceMethod(string name) { + exists(MemberDefinition mem | mem = this.getMember(name) | + result = mem.getInit() and + not mem.isStatic() and + not mem instanceof ConstructorDefinition + ) + } + + override string getAPrimaryQlClass() { result = "ClassDefinition" } + + /** + * Gets a static initializer of this class, if any. + */ + BlockStmt getAStaticInitializerBlock() { + exists(StaticInitializer init | init.getDeclaringClass() = this | result = init.getBody()) + } +} + +/** + * A class declaration statement. + * + * Example: + * + * ``` + * class Rectangle extends Shape { + * constructor(width, height) { + * this.width = width; + * this.height = height; + * } + * + * area() { return this.width * this.height; } + * } + * ``` + */ +class ClassDeclStmt extends @class_decl_stmt, ClassDefinition, Stmt { + override ControlFlowNode getFirstControlFlowNode() { + if has_declare_keyword(this) then result = this else result = this.getIdentifier() + } +} + +/** + * A class expression. + * + * Example: + * + * ``` + * let C = + * class { // class expression + * constructor() { this.x = 0; } + * + * bump() { return this.x++; } + * }; + * ``` + */ +class ClassExpr extends @class_expr, ClassDefinition, Expr { + override string getName() { + result = ClassDefinition.super.getName() + or + not exists(this.getIdentifier()) and + ( + exists(ValueProperty p | + this = p.getInit() and + result = p.getName() + ) + or + exists(AssignExpr assign | this = assign.getRhs().getUnderlyingValue() | + result = assign.getLhs().(PropAccess).getPropertyName() + or + result = assign.getLhs().(VarRef).getName() + ) + ) + } + + override predicate isImpure() { none() } + + override ControlFlowNode getFirstControlFlowNode() { + if exists(this.getIdentifier()) + then result = this.getIdentifier() + else + if exists(this.getSuperClass()) + then result = this.getSuperClass().getFirstControlFlowNode() + else + if exists(this.getClassInitializedMember()) + then + result = + min(ClassInitializedMember m | + m = this.getClassInitializedMember() + | + m order by m.getIndex() + ) + else result = this + } + + /** Returns a member that is initialized during the creation of this class. */ + private ClassInitializedMember getClassInitializedMember() { result = this.getAMember() } +} + +/** + * A class member that is initialized at class creation time (as opposed to instance creation time). + */ +private class ClassInitializedMember extends MemberDeclaration { + ClassInitializedMember() { this instanceof MethodDefinition or this.isStatic() } + + int getIndex() { properties(this, _, result, _, _) } + + override string getAPrimaryQlClass() { result = "ClassInitializedMember" } +} + +/** + * A `super` expression. + * + * Example: + * + * ``` + * super + * ``` + */ +class SuperExpr extends @super_expr, Expr { + override predicate isImpure() { none() } + + /** + * Gets the function whose `super` binding this expression refers to, + * which is the nearest enclosing non-arrow function. + */ + Function getBinder() { result = this.getEnclosingFunction().getThisBinder() } + + override string getAPrimaryQlClass() { result = "SuperExpr" } +} + +/** + * A `super(...)` call. + * + * Example: + * + * ``` + * super(...args) + * ``` + */ +class SuperCall extends CallExpr { + SuperCall() { this.getCallee().getUnderlyingValue() instanceof SuperExpr } + + /** + * Gets the function whose `super` binding this call refers to, + * which is the nearest enclosing non-arrow function. + */ + Function getBinder() { result = this.getCallee().getUnderlyingValue().(SuperExpr).getBinder() } +} + +/** + * A property access on `super`. + * + * Example: + * + * ``` + * super.f + * ``` + */ +class SuperPropAccess extends PropAccess { + SuperPropAccess() { this.getBase().getUnderlyingValue() instanceof SuperExpr } +} + +/** + * A `new.target` expression. + * + * Example: + * + * ``` + * new.target + * ``` + * + * When a function `f` is invoked as `new f()`, then `new.target` inside + * `f` evaluates to `f` ; on the other hand, when `f` is invoked without + * `new`, it evaluates to `undefined`. + * + * See also ECMAScript 2015 Language Specification, Chapter 12.3.8. + */ +class NewTargetExpr extends @newtarget_expr, Expr { + override predicate isImpure() { none() } + + override string getAPrimaryQlClass() { result = "NewTargetExpr" } +} + +/** + * A scope induced by a named class expression or class expression with type parameters. + */ +class ClassExprScope extends @class_expr_scope, Scope { + override string toString() { result = "class expression scope" } +} + +/** + * A scope induced by a class declaration with type parameters. + */ +class ClassDeclScope extends @class_decl_scope, Scope { + override string toString() { result = "class declaration scope" } +} + +/** + * A member declaration in a class or interface, that is, either a method declaration or a field declaration. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { + * width; // field declaration + * height; // field declaration + * + * constructor(height, width) { // constructor declaration, which is also a method declaration + * this.height = height; + * this.width = width; + * } + * + * area() { // method declaration + * return this.width*this.height; + * } + * } + * + * interface EventEmitter { + * addListener(listener: (x: T) => void): void; // method declaration + * } + * ``` + * + * The subtype `MemberSignature` contains TypeScript members that are abstract, ambient, or + * overload signatures. + * + * The subtype `MemberDefinition` contains all members that are not signatures. In regular + * JavaScript, all members are definitions. + * + * There are also subtypes for working with specific kinds of members, such as `FieldDeclaration`, + * `FieldSignature`, and `FieldDefinition`, and similarly named subtypes for methods, constructors, and + * getters and setters. + */ +class MemberDeclaration extends @property, Documentable { + MemberDeclaration() { + // filter out property patterns and object properties and enum members + exists(ClassOrInterface cl | properties(this, cl, _, _, _)) + } + + /** + * Holds if this member is static. + */ + predicate isStatic() { is_static(this) } + + /** Gets a boolean indicating if this member is static. */ + boolean getStaticAsBool() { if this.isStatic() then result = true else result = false } + + /** + * Holds if this member is abstract. + * + * Abstract members occur only in TypeScript. + */ + predicate isAbstract() { is_abstract_member(this) } + + /** + * Holds if this member is public, either because it has no access modifier or + * because it is explicitly annotated as `public`. + * + * Class members not declared in a TypeScript file are always considered public. + */ + predicate isPublic() { not this.isPrivate() and not this.isProtected() } + + /** + * Holds if this is a TypeScript member explicitly annotated with the `public` keyword. + */ + 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 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. + */ + Expr getNameExpr() { result = this.getChildExpr(0) } + + /** + * Gets the expression specifying the initial value of the member; + * for methods and constructors this is always a function, for fields + * it may not be defined. + */ + Expr getInit() { result = this.getChildExpr(1) } + + /** Gets the name of this member. */ + string getName() { + result = this.getNameExpr().(Literal).getValue() + or + not this.isComputed() and result = this.getNameExpr().(Identifier).getName() + } + + /** Holds if the name of this member is computed. */ + predicate isComputed() { is_computed(this) } + + /** Gets the class or interface this member belongs to. */ + ClassOrInterface getDeclaringType() { properties(this, result, _, _, _) } + + /** Gets the class this member belongs to, if any. */ + ClassDefinition getDeclaringClass() { properties(this, result, _, _, _) } + + /** Gets the index of this member within its enclosing type. */ + int getMemberIndex() { properties(this, _, result, _, _) } + + /** Holds if the name of this member is computed by an impure expression. */ + overlay[global] + predicate hasImpureNameExpr() { this.isComputed() and this.getNameExpr().isImpure() } + + /** + * Gets the `i`th decorator applied to this member. + * + * For example, a method of the form `@A @B m() { ... }` has + * `@A` as its 0th decorator and `@B` as its first decorator. + */ + Decorator getDecorator(int i) { result = this.getChildExpr(-(i + 1)) } + + /** + * Gets a decorator applied to this member. + * + * For example, a method of the form `@A @B m() { ... }` has + * decorators `@A` and `@B`. + */ + Decorator getADecorator() { result = this.getDecorator(_) } + + override string toString() { properties(this, _, _, _, result) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getNameExpr().getFirstControlFlowNode() + } + + /** + * True if this is neither abstract, ambient, nor part of an overloaded method signature. + */ + predicate isConcrete() { + not this.isAbstract() and + not this.isAmbient() and + (this instanceof MethodDeclaration implies this.(MethodDeclaration).getBody().hasBody()) + } + + /** + * True if this is abstract, ambient, or an overload signature. + */ + predicate isSignature() { not this.isConcrete() } + + override string getAPrimaryQlClass() { result = "MemberDeclaration" } +} + +/** + * A concrete member of a class, that is, a non-abstract, non-ambient field or method with a body. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { + * width; // field declaration + * height; // field declaration + * + * constructor(height, width) { // constructor declaration, which is also a method declaration + * this.height = height; + * this.width = width; + * } + * + * area() { // method declaration + * return this.width*this.height; + * } + * } + * ``` + */ +class MemberDefinition extends MemberDeclaration { + MemberDefinition() { this.isConcrete() } +} + +/** + * A member signature declared in a class or interface, that is, an abstract or ambient field + * or method without a function body. + * + * Example: + * + * ``` + * interface EventEmitter { + * addListener(listener: (x: T) => void): void; // method signature + * } + * ``` + */ +class MemberSignature extends MemberDeclaration { + MemberSignature() { this.isSignature() } +} + +/** + * A method declaration in a class or interface, either a concrete definition or a signature without a body. + * + * Examples: + * + * ``` + * abstract class Shape { + * abstract area() : number; // method declaration + * } + * + * class Rectangle extends Shape { + * height: number; + * width: number; + * + * constructor(height: number, width: number) { // constructor declaration, which is also a method declaration + * super(); + * this.height = height; + * this.width = width; + * } + * + * area() { // method declaration + * return this.width*this.height; + * } + * } + * ``` + * + * Note that TypeScript call signatures are not considered methods. + */ +class MethodDeclaration extends MemberDeclaration { + MethodDeclaration() { is_method(this) } + + /** + * Gets the body of this method. + */ + FunctionExpr getBody() { result = this.getChildExpr(1) } + + /** + * Holds if this method is overloaded, that is, there are multiple method + * signatures with its name declared in the enclosing type. + */ + predicate isOverloaded() { + not this instanceof ConstructorDeclaration and + hasOverloadedMethod(this.getDeclaringType(), this.getName()) + or + this instanceof ConstructorDeclaration and + hasOverloadedConstructor(this.getDeclaringClass()) + } + + /** + * Gets the index of this method declaration among all the method declarations + * with this name. + * + * In the rare case of a class containing multiple concrete methods with the same name, + * the overload index is defined as if only one of them was concrete. + */ + int getOverloadIndex() { + exists(ClassOrInterface type, string name, boolean static | + this = + rank[result + 1](MethodDeclaration method, int i | + methodDeclaredInType(type, name, static, i, method) + | + method order by i + ) + ) + or + exists(ClassDefinition type | + this = + rank[result + 1](ConstructorDeclaration ctor, int i | + ctor = type.getMemberByIndex(i) + | + ctor order by i + ) + ) + } +} + +/** + * Holds if the `index`th member of `type` is `method`, which has the given `name`. + */ +private predicate methodDeclaredInType( + ClassOrInterface type, string name, boolean static, int index, MethodDeclaration method +) { + not method instanceof ConstructorDeclaration and // distinguish methods named "constructor" from the constructor + type.getMemberByIndex(index) = method and + static = method.getStaticAsBool() and + method.getName() = name +} + +/** + * Holds if `type` has an overloaded method named `name`. + */ +private predicate hasOverloadedMethod(ClassOrInterface type, string name) { + exists(MethodDeclaration method | + method = type.getMethod(name) and + not method instanceof ConstructorDeclaration and + method.getOverloadIndex() > 0 + ) +} + +/** Holds if `type` has an overloaded constructor declaration. */ +private predicate hasOverloadedConstructor(ClassDefinition type) { + type.getConstructor().getOverloadIndex() > 0 +} + +/** Holds if `type` has an overloaded function call signature. */ +private predicate hasOverloadedFunctionCallSignature(ClassOrInterface type) { + type.getACallSignature().(FunctionCallSignature).getOverloadIndex() > 0 +} + +/** Holds if `type` has an overloaded constructor call signature. */ +private predicate hasOverloadedConstructorCallSignature(ClassOrInterface type) { + type.getACallSignature().(ConstructorCallSignature).getOverloadIndex() > 0 +} + +/** + * A concrete method definition in a class. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { + * constructor(height, width) { // constructor definition, which is also a method definition + * this.height = height; + * this.width = width; + * } + * + * area() { // method definition + * return this.width*this.height; + * } + * } + * ``` + */ +class MethodDefinition extends MethodDeclaration, MemberDefinition { + override string getAPrimaryQlClass() { result = "MethodDefinition" } +} + +/** + * A method signature declared in a class or interface, that is, a method without a function body. + * + * Example: + * + * ``` + * interface EventEmitter { + * addListener(listener: (x: T) => void): void; // method signature + * } + * ``` + * + * Note that TypeScript call signatures are not considered method signatures. + */ +class MethodSignature extends MethodDeclaration, MemberSignature { + override string getAPrimaryQlClass() { result = "MethodSignature" } +} + +/** + * A constructor declaration in a class, either a concrete definition or a signature without a body. + * + * Example: + * + * ``` + * class Rectangle extends Shape { + * constructor(height, width) { // constructor declaration + * this.height = height; + * this.width = width; + * } + * + * area() { + * return this.width*this.height; + * } + * } + * ``` + */ +class ConstructorDeclaration extends MethodDeclaration { + ConstructorDeclaration() { + not this.isComputed() and + not this.isStatic() and + this.getName() = "constructor" + } + + /** Holds if this is a synthetic default constructor. */ + predicate isSynthetic() { this.getLocation().isEmpty() } + + override string getAPrimaryQlClass() { result = "ConstructorDeclaration" } +} + +/** + * The concrete constructor definition of a class, possibly a synthetic constructor if the class + * did not declare any constructors. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { + * constructor(height, width) { // constructor definition + * this.height = height; + * this.width = width; + * } + * + * area() { + * return this.width*this.height; + * } + * } + * ``` + */ +class ConstructorDefinition extends ConstructorDeclaration, MethodDefinition { + override string getAPrimaryQlClass() { result = "ConstructorDefinition" } +} + +/** + * A constructor signature declared in a class, that is, a constructor without a function body. + * + * ``` + * declare class Rectangle { + * constructor(width: number, height: number); // constructor signature + * } + * ``` + */ +class ConstructorSignature extends ConstructorDeclaration, MethodSignature { + override string getAPrimaryQlClass() { result = "ConstructorSignature" } +} + +/** + * A function generated by the extractor to implement a synthetic default constructor. + * + * Example: + * + * ``` + * class Rectangle extends Shape { + * // implicitly generated synthetic constructor: + * // constructor() { super(); } + * + * area() { + * return this.width*this.height; + * } + * } + * ``` + */ +class SyntheticConstructor extends Function { + SyntheticConstructor() { this = any(ConstructorDeclaration cd | cd.isSynthetic() | cd.getBody()) } +} + +/** + * An accessor method declaration in a class or interface, either a concrete definition or a signature without a body. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { + * constructor(height, width) { + * this.height = height; + * this.width = width; + * } + * + * get area() { // accessor method declaration + * return this.width*this.height; + * } + * } + * ``` + */ +abstract class AccessorMethodDeclaration extends MethodDeclaration { + /** Get the corresponding getter (if this is a setter) or setter (if this is a getter). */ + AccessorMethodDeclaration getOtherAccessor() { + getterSetterPair(this, result) + or + getterSetterPair(result, this) + } +} + +/** + * A concrete accessor method definition in a class, that is, an accessor method with a function body. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { + * constructor(height, width) { + * this.height = height; + * this.width = width; + * } + * + * get area() { // accessor method declaration + * return this.width*this.height; + * } + * } + * ``` + */ +abstract class AccessorMethodDefinition extends MethodDefinition, AccessorMethodDeclaration { } + +/** + * An accessor method signature declared in a class or interface, that is, an accessor method without a function body. + * + * Example: + * + * ``` + * abstract class Shape { + * abstract get area() : number; // accessor method signature + * } + * ``` + */ +abstract class AccessorMethodSignature extends MethodSignature, AccessorMethodDeclaration { } + +/** + * A getter method declaration in a class or interface, either a concrete definition or a signature without a function body. + * + * Examples: + * + * ``` + * abstract class Shape { + * abstract get area() : number; // getter method signature + * } + * + * class Rectangle extends Shape { + * height: number; + * width: number; + * + * constructor(height: number, width: number) { + * super(); + * this.height = height; + * this.width = width; + * } + * + * get area() { // getter method definition + * return this.width*this.height; + * } + * } + * ``` + */ +class GetterMethodDeclaration extends AccessorMethodDeclaration, @property_getter { + override string getAPrimaryQlClass() { result = "GetterMethodDeclaration" } + + /** Gets the correspinding setter declaration, if any. */ + SetterMethodDeclaration getCorrespondingSetter() { getterSetterPair(this, result) } +} + +/** + * A concrete getter method definition in a class, that is, a getter method with a function body. + * + * Examples: + * + * ``` + * class Rectangle extends Shape { + * constructor(height, width) { + * this.height = height; + * this.width = width; + * } + * + * get area() { // getter method definition + * return this.width*this.height; + * } + * } + * ``` + */ +class GetterMethodDefinition extends GetterMethodDeclaration, AccessorMethodDefinition { + override string getAPrimaryQlClass() { result = "GetterMethodDefinition" } +} + +/** + * A getter method signature declared in a class or interface, that is, a getter method without a function body. + * + * Example: + * + * ``` + * abstract class Shape { + * abstract get area() : number; // getter method signature + * } + * ``` + */ +class GetterMethodSignature extends GetterMethodDeclaration, AccessorMethodSignature { + override string getAPrimaryQlClass() { result = "GetterMethodSignature" } +} + +/** + * A setter method declaration in a class or interface, either a concrete definition or a signature without a body. + * + * Examples: + * + * ``` + * abstract class Cell { + * abstract set value(v: any); // setter method signature + * } + * + * class NumberCell extends Cell { + * constructor(private _value: number) { + * super(); + * } + * + * set value(v: any) { // setter method definition + * this._value = +v; + * } + * } + * ``` + */ +class SetterMethodDeclaration extends AccessorMethodDeclaration, @property_setter { + override string getAPrimaryQlClass() { result = "SetterMethodDeclaration" } + + /** Gets the correspinding getter declaration, if any. */ + GetterMethodDeclaration getCorrespondingGetter() { getterSetterPair(result, this) } +} + +/** + * A concrete setter method definition in a class, that is, a setter method with a function body + * + * Examples: + * + * ``` + * class NumberCell extends Cell { + * constructor(private _value: number) { + * super(); + * } + * + * set value(v: any) { // setter method definition + * this._value = +v; + * } + * } + * ``` + */ +class SetterMethodDefinition extends SetterMethodDeclaration, AccessorMethodDefinition { + override string getAPrimaryQlClass() { result = "SetterMethodDefinition" } +} + +/** + * A setter method signature declared in a class or interface, that is, a setter method without a function body. + * + * Example: + * + * ``` + * abstract class Cell { + * abstract set value(v: any); // setter method signature + * } + * ``` + */ +class SetterMethodSignature extends SetterMethodDeclaration, AccessorMethodSignature { + override string getAPrimaryQlClass() { result = "SetterMethodSignature" } +} + +/** + * A field declaration in a class or interface, either a concrete definition or an abstract or ambient field signature. + * + * Examples: + * + * ``` + * class Rectangle { + * height; // field declaration + * width; // field declaration + * } + * + * abstract class Counter { + * abstract value: number; // field signature + * } + * ``` + */ +class FieldDeclaration extends MemberDeclaration, @field { + /** Gets the type annotation of this field, if any, such as `T` in `{ x: T }`. */ + TypeAnnotation getTypeAnnotation() { + result = this.getChildTypeExpr(2) + or + result = this.getDocumentation().getATagByTitle("type").getType() + } + + /** Holds if this is a TypeScript field annotated with the `readonly` keyword. */ + predicate isReadonly() { has_readonly_keyword(this) } + + /** Holds if this is a TypeScript field marked as optional with the `?` operator. */ + predicate isOptional() { is_optional_member(this) } + + /** Holds if this is a TypeScript field marked as definitely assigned with the `!` operator. */ + predicate hasDefiniteAssignmentAssertion() { has_definite_assignment_assertion(this) } + + override string getAPrimaryQlClass() { result = "FieldDeclaration" } +} + +/** + * A concrete field definition in a class. + * + * Examples: + * + * ``` + * class Rectangle { + * height; // field definition + * width; // field definition + * } + * ``` + */ +class FieldDefinition extends FieldDeclaration, MemberDefinition { } + +/** + * A field signature declared in a class or interface, that is, an abstract or ambient field declaration. + * + * Example: + * + * ``` + * abstract class Counter { + * abstract value: number; // field signature + * } + * ``` + */ +class FieldSignature extends FieldDeclaration, MemberSignature { } + +/** + * A field induced by an initializing constructor parameter. + * + * Example: + * + * ``` + * class C { + * constructor(public x: number) {} // `x` is a parameter field + * } + * ``` + */ +class ParameterField extends FieldDeclaration, @parameter_field { + /** Gets the initializing constructor parameter from which this field was induced. */ + Parameter getParameter() { + exists(FunctionExpr constructor, int index | parameter_fields(this, constructor, index) | + result = constructor.getParameter(index) + ) + } + + override Expr getNameExpr() { result = this.getParameter() } + + override TypeAnnotation getTypeAnnotation() { result = this.getParameter().getTypeAnnotation() } +} + +/** + * A static initializer in a class. + */ +class StaticInitializer extends MemberDefinition, @static_initializer { + /** + * Gets the body of the static initializer. + */ + BlockStmt getBody() { result.getParent() = this } + + override Expr getNameExpr() { none() } +} + +/** + * A call signature declared in an interface. + * + * Examples: + * + * ``` + * interface I { + * (x: number): number; // function call signature + * new (x: string): Object; // constructor call signature + * } + * ``` + * + * Call signatures are either function call signatures or constructor call signatures. + */ +class CallSignature extends @call_signature, MemberSignature { + FunctionExpr getBody() { result = this.getChildExpr(1) } + + /** Gets the interface or function type that declares this call signature. */ + override InterfaceDefinition getDeclaringType() { + result = MemberSignature.super.getDeclaringType() + } +} + +/** + * A function call signature declared in an interface. + * + * Example: + * + * ``` + * interface I { + * (x: number): string; // function call signature + * } + * ``` + */ +class FunctionCallSignature extends @function_call_signature, CallSignature { + /** Gets the index of this function call signature among the function call signatures in the enclosing type. */ + int getOverloadIndex() { + exists(ClassOrInterface type | type = this.getDeclaringType() | + this = + rank[result + 1](FunctionCallSignature sig, int i | + sig = type.getMemberByIndex(i) + | + sig order by i + ) + ) + } + + /** + * Holds if this function call signature is overloaded, that is, there are multiple function call + * signatures declared in the enclosing type. + */ + predicate isOverloaded() { hasOverloadedFunctionCallSignature(this.getDeclaringType()) } +} + +/** + * A constructor call signature declared in an interface. + * + * Example: + * + * ``` + * interface I { + * new (x: string): Object; // constructor call signature + * } + * ``` + */ +class ConstructorCallSignature extends @constructor_call_signature, CallSignature { + /** Gets the index of this constructor call signature among the constructor call signatures in the enclosing type. */ + int getOverloadIndex() { + exists(ClassOrInterface type | type = this.getDeclaringType() | + this = + rank[result + 1](ConstructorCallSignature sig, int i | + sig = type.getMemberByIndex(i) + | + sig order by i + ) + ) + } + + /** + * Holds if this constructor call signature is overloaded, that is, there are multiple constructor call + * signatures declared in the enclosing type. + */ + predicate isOverloaded() { hasOverloadedConstructorCallSignature(this.getDeclaringType()) } +} + +/** + * An index signature declared in an interface. + * + * Example: + * + * ``` + * interface I { + * [x: number]: number; // index signature + * } + * ``` + */ +class IndexSignature extends @index_signature, MemberSignature { + FunctionExpr getBody() { result = this.getChildExpr(1) } + + /** Gets the interface or function type that declares this index signature. */ + override InterfaceDefinition getDeclaringType() { + result = MemberSignature.super.getDeclaringType() + } + + override string getAPrimaryQlClass() { result = "IndexSignature" } +} + +private boolean getStaticness(AccessorMethodDefinition member) { + member.isStatic() and result = true + or + not member.isStatic() and result = false +} + +pragma[nomagic] +private AccessorMethodDefinition getAnAccessorFromClass( + ClassDefinition cls, string name, boolean static +) { + result = cls.getMember(name) and + static = getStaticness(result) +} + +pragma[nomagic] +private predicate getterSetterPair(GetterMethodDeclaration getter, SetterMethodDeclaration setter) { + exists(ClassDefinition cls, string name, boolean static | + getter = getAnAccessorFromClass(cls, name, static) and + setter = getAnAccessorFromClass(cls, name, static) + ) +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Comments.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Comments.qll new file mode 100644 index 000000000000..ef1faa947898 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Comments.qll @@ -0,0 +1,137 @@ +/** Provides classes for working with JavaScript comments. */ +overlay[local?] +module; + +import minimal + +/** + * A JavaScript source-code comment. + * + * Examples: + * + *
+ * // a line comment
+ * /* a block
+ *   comment */
+ * <!-- an HTML line comment
+ * 
+ */ +class Comment extends @comment, Locatable { + /** Gets the toplevel element this comment belongs to. */ + TopLevel getTopLevel() { comments(this, _, result, _, _) } + + /** Gets the text of this comment, not including delimiters. */ + string getText() { comments(this, _, _, result, _) } + + /** Gets the `i`th line of comment text. */ + string getLine(int i) { result = this.getText().splitAt("\n", i) } + + /** Gets the next token after this comment. */ + Token getNextToken() { next_token(this, result) } + + override int getNumLines() { result = count(this.getLine(_)) } + + override string toString() { comments(this, _, _, _, result) } + + /** Holds if this comment spans lines `start` to `end` (inclusive) in file `f`. */ + predicate onLines(File f, int start, int end) { + exists(Location loc | loc = this.getLocation() | + f = loc.getFile() and + start = loc.getStartLine() and + end = loc.getEndLine() + ) + } +} + +/** + * A line comment, that is, either an HTML comment or a `//` comment. + * + * Examples: + * + *
+ * // a line comment
+ * <!-- an HTML line comment
+ * 
+ */ +class LineComment extends @line_comment, Comment { } + +/** + * An HTML comment start/end token interpreted as a line comment. + * + * Example: + * + * ``` + * <!-- an HTML line comment + * --> also an HTML line comment + * ``` + */ +class HtmlLineComment extends @html_comment, LineComment { } + +/** + * An HTML comment start token interpreted as a line comment. + * + * Example: + * + * ``` + * <!-- an HTML line comment + * ``` + */ +class HtmlCommentStart extends @html_comment_start, HtmlLineComment { } + +/** + * An HTML comment end token interpreted as a line comment. + * + * Example: + * + * ``` + * --> also an HTML line comment + * ``` + */ +class HtmlCommentEnd extends @htmlcommentend, HtmlLineComment { } + +/** + * A `//` comment. + * + * Example: + * + * ``` + * // a line comment + * ``` + */ +class SlashSlashComment extends @slashslash_comment, LineComment { } + +/** + * A block comment (which may be a JSDoc comment). + * + * Examples: + * + *
+ * /* a block comment
+ *   (but not a JSDoc comment) */
+ * /** a JSDoc comment */
+ * 
+ */ +class BlockComment extends @block_comment, Comment { } + +/** + * A C-style block comment which is not a JSDoc comment. + * + * Example: + * + *
+ * /* a block comment
+ *   (but not a JSDoc comment) */
+ * 
+ */ +class SlashStarComment extends @slashstar_comment, BlockComment { } + +/** + * A JSDoc comment. + * + * Example: + * + *
+ * /** a JSDoc comment */
+ * 
+ */ +class DocComment extends @doc_comment, BlockComment { } diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/ES2015Modules.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/ES2015Modules.qll new file mode 100644 index 000000000000..ea2f794060fb --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/ES2015Modules.qll @@ -0,0 +1,665 @@ +/** Provides classes for working with ECMAScript 2015 modules. */ +overlay[local?] +module; + +import minimal + +class Module extends TopLevel { + /** Gets the full path of the file containing this module. */ + string getPath() { result = this.getFile().getAbsolutePath() } + + /** Gets the short name of this module without file extension. */ + string getName() { result = this.getFile().getStem() } +} + +/** + * An ECMAScript 2015 module. + * + * Example: + * + * ``` + * import console from 'console'; + * + * console.log("Hello, world!"); + * ``` + */ +class ES2015Module extends Module { + ES2015Module() { is_es2015_module(this) } + + override ModuleScope getScope() { result.getScopeElement() = this } + + /** Gets an export declaration in this module. */ + pragma[nomagic] + ExportDeclaration getAnExport() { result.getTopLevel() = this } + + override predicate isStrict() { + // 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. + */ + overlay[global] + predicate hasBothNamedAndDefaultExports() { + hasNamedExports(this) and + hasDefaultExport(this) + } +} + +/** + * Holds if `mod` contains one or more named export declarations other than `default`. + */ +overlay[global] +private predicate hasNamedExports(ES2015Module mod) { + mod.getAnExport().(ExportNamedDeclaration).getASpecifier().getExportedName() != "default" + or + exists(mod.getAnExport().(ExportNamedDeclaration).getAnExportedDecl()) + or + // Bulk re-exports only export named bindings (not "default") + mod.getAnExport() instanceof BulkReExportDeclaration +} + +/** + * Holds if this module contains a default export. + */ +overlay[global] +private predicate hasDefaultExport(ES2015Module mod) { + // export default foo; + mod.getAnExport() instanceof ExportDefaultDeclaration + or + // export { foo as default }; + mod.getAnExport().(ExportNamedDeclaration).getASpecifier().getExportedName() = "default" +} + +/** + * An import declaration. + * + * Examples: + * + * ``` + * import console, { log, error as fatal } from 'console'; + * import * as console from 'console'; + * ``` + */ +class ImportDeclaration extends Stmt, @import_declaration { + /** + * INTERNAL USE ONLY. DO NOT USE. + */ + string getRawImportPath() { result = this.getChildExpr(-1).getStringValue() } + + Expr getImportedPathExpr() { result = this.getChildExpr(-1) } + + /** + * Gets the object literal passed as part of the `with` (or `assert`) clause in this import declaration. + * + * For example, this gets the `{ type: "json" }` object literal in the following: + * ```js + * import foo from "foo" with { type: "json" }; + * import foo from "foo" assert { type: "json" }; + * ``` + */ + ObjectExpr getImportAttributes() { result = this.getChildExpr(-10) } + + /** + * DEPRECATED: use `getImportAttributes` instead. + * Gets the object literal passed as part of the `with` (or `assert`) clause in this import declaration. + * + * For example, this gets the `{ type: "json" }` object literal in the following: + * ```js + * import foo from "foo" with { type: "json" }; + * import foo from "foo" assert { type: "json" }; + * ``` + */ + deprecated ObjectExpr getImportAssertion() { result = this.getImportAttributes() } + + /** Gets the `i`th import specifier of this import declaration. */ + ImportSpecifier getSpecifier(int i) { result = this.getChildExpr(i) } + + /** Gets an import specifier of this import declaration. */ + ImportSpecifier getASpecifier() { result = this.getSpecifier(_) } + + /** Holds if this is declared with the `type` keyword, so it only imports types. */ + predicate isTypeOnly() { has_type_keyword(this) } + + /** + * Holds if this is declared with the `defer` keyword, for example: + * ```ts + * import defer * as f from "somewhere"; + * ``` + */ + predicate isDeferredImport() { has_defer_keyword(this) } + + override string getAPrimaryQlClass() { result = "ImportDeclaration" } +} + +/** + * An import specifier in an import declaration. + * + * Examples: + * + * ``` + * import + * console, // default import specifier + * { + * log, // named import specifier + * error as fatal // renaming import specifier + * } from 'console'; + * + * import + * * as console // namespace import specifier + * from 'console'; + * ``` + */ +class ImportSpecifier extends Expr, @import_specifier { + /** Gets the import declaration in which this specifier appears. */ + overlay[global] + ImportDeclaration getImportDeclaration() { result.getASpecifier() = this } + + /** Gets the imported symbol; undefined for default and namespace import specifiers. */ + Identifier getImported() { result = this.getChildExpr(0) } + + /** + * Gets the name of the imported symbol. + * + * For example, consider these four imports: + * + * ```javascript + * import { x } from 'a' + * import { y as z } from 'b' + * import f from 'c' + * import * as g from 'd' + * ``` + * + * The names of the imported symbols for the first three of them are, respectively, + * `x`, `y` and `default`, while the last one does not import an individual symbol. + */ + string getImportedName() { result = this.getImported().getName() } + + /** Gets the local variable into which this specifier imports. */ + VarDecl getLocal() { result = this.getChildExpr(1) } + + override string getAPrimaryQlClass() { result = "ImportSpecifier" } + + /** Holds if this is declared with the `type` keyword, so only types are imported. */ + predicate isTypeOnly() { has_type_keyword(this) } +} + +/** + * A named import specifier. + * + * Examples: + * + * ``` + * import + * { + * log, // named import specifier + * error as fatal // renaming import specifier + * } from 'console'; + * ``` + */ +class NamedImportSpecifier extends ImportSpecifier, @named_import_specifier { } + +/** + * A default import specifier. + * + * Example: + * + * ``` + * import + * console // default import specifier + * from 'console'; + * ``` + */ +class ImportDefaultSpecifier extends ImportSpecifier, @import_default_specifier { + override string getImportedName() { result = "default" } +} + +/** + * A namespace import specifier. + * + * Example: + * + * ``` + * import + * * as console // namespace import specifier + * from 'console'; + * ``` + */ +class ImportNamespaceSpecifier extends ImportSpecifier, @import_namespace_specifier { } + +/** + * A bulk import that imports an entire module as a namespace. + * + * Example: + * + * ``` + * import * as console from 'console'; + * ``` + */ +class BulkImportDeclaration extends ImportDeclaration { + BulkImportDeclaration() { this.getASpecifier() instanceof ImportNamespaceSpecifier } + + /** Gets the local namespace variable under which the module is imported. */ + VarDecl getLocal() { result = this.getASpecifier().getLocal() } +} + +/** + * A selective import that imports zero or more declarations. + * + * Example: + * + * ``` + * import console, { log } from 'console'; + * ``` + */ +overlay[global] +class SelectiveImportDeclaration extends ImportDeclaration { + SelectiveImportDeclaration() { not this instanceof BulkImportDeclaration } + + /** Holds if `local` is the local variable into which `imported` is imported. */ + predicate importsAs(string imported, LexicalDecl local) { + exists(ImportSpecifier spec | spec = this.getASpecifier() | + imported = spec.getImported().getName() and + local = spec.getLocal() + ) + or + imported = "default" and local = this.getASpecifier().(ImportDefaultSpecifier).getLocal() + } +} + +/** + * An export declaration. + * + * Examples: + * + * ``` + * export * from 'a'; // bulk re-export declaration + * + * export default function f() {}; // default export declaration + * export default 42; // default export declaration + * + * export { x, y as z }; // named export declaration + * export var x = 42; // named export declaration + * export { x } from 'a'; // named re-export declaration + * export x from 'a'; // default re-export declaration + * ``` + */ +abstract class ExportDeclaration extends Stmt, @export_declaration { + /** Gets the module to which this export declaration belongs. */ + overlay[global] + ES2015Module getEnclosingModule() { this = result.getAnExport() } + + /** + * Holds if this export declaration exports variable `v` under the name `name`, + * not counting re-exports. + */ + predicate exportsDirectlyAs(LexicalName v, string name) { none() } + + /** Holds if is declared with the `type` keyword, so only types are exported. */ + predicate isTypeOnly() { has_type_keyword(this) } + + override string getAPrimaryQlClass() { result = "ExportDeclaration" } + + /** + * Gets the object literal passed as part of the `with` (or `assert`) clause, if this is + * a re-export declaration. + * + * For example, this gets the `{ type: "json" }` expression in each of the following: + * ```js + * export { x } from 'foo' with { type: "json" }; + * export * from 'foo' with { type: "json" }; + * export * as x from 'foo' with { type: "json" }; + * export * from 'foo' assert { type: "json" }; + * ``` + */ + ObjectExpr getImportAttributes() { result = this.getChildExpr(-10) } + + /** + * DEPRECATED: use `getImportAttributes` instead. + * Gets the object literal passed as part of the `with` (or `assert`) clause, if this is + * a re-export declaration. + * + * For example, this gets the `{ type: "json" }` expression in each of the following: + * ```js + * export { x } from 'foo' with { type: "json" }; + * export * from 'foo' with { type: "json" }; + * export * as x from 'foo' with { type: "json" }; + * export * from 'foo' assert { type: "json" }; + * ``` + */ + deprecated ObjectExpr getImportAssertion() { result = this.getImportAttributes() } +} + +/** + * A bulk re-export declaration of the form `export * from 'a'`, which re-exports + * all exports of another module. + * + * Examples: + * + * ``` + * export * from 'a'; // bulk re-export declaration + * ``` + */ +class BulkReExportDeclaration extends ReExportDeclaration, @export_all_declaration { + /** Gets the name of the module from which this declaration re-exports. */ + override Expr getImportedPathExpr() { result = this.getChildExpr(0) } +} + +/** + * Holds if bulk re-exports in `mod` should not re-export `name` because there is an explicit export + * of that name in `mod`. + * + * At compile time, shadowing works across declaration spaces. + * For instance, directly exporting an interface `X` will block a variable `X` from being re-exported: + * ``` + * export interface X {} + * export * from 'lib' // will not re-export X + * ``` + * At runtime, the interface `X` will have been removed, so `X` is actually re-exported anyway, + * but we ignore this subtlety. + */ +overlay[global] +predicate isShadowedFromBulkExport(Module mod, string name) { + exists(ExportNamedDeclaration other | other.getTopLevel() = mod | + other.getAnExportedDecl().getName() = name + or + other.getASpecifier().getExportedName() = name + ) +} + +/** + * A default export declaration. + * + * Examples: + * + * ``` + * export default function f() {}; + * export default 42; + * ``` + */ +class ExportDefaultDeclaration extends ExportDeclaration, @export_default_declaration { + /** Gets the operand statement or expression that is exported by this declaration. */ + ExprOrStmt getOperand() { result = this.getChild(0) } + + override predicate exportsDirectlyAs(LexicalName v, string name) { + name = "default" and v = this.getADecl().getVariable() + } + + /** Gets the declaration, if any, exported by this default export. */ + VarDecl getADecl() { + exists(ExprOrStmt op | op = this.getOperand() | + result = op.(FunctionDeclStmt).getIdentifier() or + result = op.(ClassDeclStmt).getIdentifier() + ) + } +} + +/** + * A named export declaration. + * * + * Examples: + * + * ``` + * export { x, y as z }; + * export var x = 42; + * export { x } from 'a'; + * ``` + */ +class ExportNamedDeclaration extends ExportDeclaration, @export_named_declaration { + /** Gets the operand statement or expression that is exported by this declaration. */ + ExprOrStmt getOperand() { result = this.getChild(-1) } + + /** + * Gets an identifier, if any, exported as part of a declaration by this named export. + * + * Does not include names of export specifiers. + * That is, it includes the `v` in `export var v` but not in `export {v}`. + */ + Identifier getAnExportedDecl() { + exists(ExprOrStmt op | op = this.getOperand() | + result = op.(DeclStmt).getADecl().getBindingPattern().getABindingVarRef() or + result = op.(FunctionDeclStmt).getIdentifier() or + result = op.(ClassDeclStmt).getIdentifier() or + result = op.(NamespaceDeclaration).getIdentifier() or + result = op.(EnumDeclaration).getIdentifier() or + result = op.(InterfaceDeclaration).getIdentifier() or + result = op.(TypeAliasDeclaration).getIdentifier() or + result = op.(ImportEqualsDeclaration).getIdentifier() + ) + } + + /** Gets the variable declaration, if any, exported by this named export. */ + VarDecl getADecl() { result = this.getAnExportedDecl() } + + override predicate exportsDirectlyAs(LexicalName v, string name) { + ( + exists(LexicalDecl vd | vd = this.getAnExportedDecl() | + name = vd.getName() and v = vd.getALexicalName() + ) + or + exists(ExportSpecifier spec | spec = this.getASpecifier() and name = spec.getExportedName() | + v = spec.getLocal().(LexicalAccess).getALexicalName() + ) + ) and + not (this.isTypeOnly() and v instanceof Variable) + } + + /** Gets the module from which the exports are taken if this is a re-export. */ + Expr getImportedPathExpr() { result = this.getChildExpr(-2) } + + /** Gets the `i`th export specifier of this declaration. */ + ExportSpecifier getSpecifier(int i) { result = this.getChildExpr(i) } + + /** Gets an export specifier of this declaration. */ + ExportSpecifier getASpecifier() { result = this.getSpecifier(_) } +} + +private import semmle.javascript.dataflow.internal.PreCallGraphStep + +/** + * An export specifier in an export declaration. + * + * Examples: + * + * ``` + * export + * * // namespace export specifier + * from 'a'; + * + * export + * default // default export specifier + * var x = 42; + * + * export { + * x, // named export specifier + * y as z // named export specifier + * }; + * + * export + * x // default re-export specifier + * from 'a'; + * ``` + */ +class ExportSpecifier extends Expr, @exportspecifier { + /** Gets the declaration to which this specifier belongs. */ + ExportDeclaration getExportDeclaration() { result = this.getParent() } + + /** Gets the local symbol that is being exported. */ + Identifier getLocal() { result = this.getChildExpr(0) } + + /** Gets the name under which the symbol is exported. */ + Identifier getExported() { result = this.getChildExpr(1) } + + /** + * Gets the local name of the exported symbol, that is, the name + * of the exported local variable, or the imported name in a + * re-export. + * + * For example, consider these six exports: + * + * ```javascript + * export { x } + * export { y as z } + * export function f() {} + * export default 42 + * export * from 'd' + * export default from 'm' + * ``` + * + * The local names for the first three of them are, respectively, + * `x`, `y` and `f`; the fourth one exports an un-named value, and + * hence has no local name; the fifth one does not export a unique + * name, and hence also does not have a local name. + * + * The sixth one (unlike the fourth one) _does_ have a local name + * (that is, `default`), since it is a re-export. + */ + string getLocalName() { result = this.getLocal().getName() } + + /** + * Gets the name under which the symbol is exported. + * + * For example, consider these five exports: + * + * ```javascript + * export { x } + * export { y as z } + * export function f() {} + * export default 42 + * export * from 'd' + * ``` + * + * The exported names for the first four of them are, respectively, + * `x`, `z`, `f` and `default`, while the last one does not have + * an exported name since it does not export a unique symbol. + */ + string getExportedName() { result = this.getExported().getName() } + + override string getAPrimaryQlClass() { result = "ExportSpecifier" } +} + +/** + * A named export specifier. + * + * Examples: + * + * ``` + * export { + * x, // named export specifier + * y as z // named export specifier + * }; + * ``` + */ +class NamedExportSpecifier extends ExportSpecifier, @named_export_specifier { } + +/** + * A default export specifier. + * + * Examples: + * + * ``` + * export + * default // default export specifier + * 42; + * export + * x // default re-export specifier + * from 'a'; + * ``` + */ +class ExportDefaultSpecifier extends ExportSpecifier, @export_default_specifier { + override string getExportedName() { result = "default" } +} + +/** + * A default export specifier in a re-export declaration. + * + * Example: + * + * ``` + * export + * x // default re-export specifier + * from 'a'; + * ``` + */ +class ReExportDefaultSpecifier extends ExportDefaultSpecifier { + ReExportDefaultSpecifier() { this.getExportDeclaration() instanceof ReExportDeclaration } + + override string getLocalName() { result = "default" } + + override string getExportedName() { result = this.getExported().getName() } +} + +/** + * A namespace export specifier, that is `*` or `* as x` occurring in an export declaration. + * + * Examples: + * + * ``` + * export + * * // namespace export specifier + * from 'a'; + * + * export + * * as x // namespace export specifier + * from 'a'; + * ``` + */ +class ExportNamespaceSpecifier extends ExportSpecifier, @export_namespace_specifier { } + +/** + * An export declaration that re-exports declarations from another module. + * + * Examples: + * + * ``` + * export * from 'a'; // bulk re-export declaration + * export * as x from 'a'; // namespace re-export declaration + * export { x } from 'a'; // named re-export declaration + * export x from 'a'; // default re-export declaration + * ``` + */ +abstract class ReExportDeclaration extends ExportDeclaration { + /** Gets the path of the module from which this declaration re-exports. */ + abstract Expr getImportedPathExpr(); +} + +/** + * A named export declaration that re-exports symbols imported from another module. + * + * Example: + * + * ``` + * export { x } from 'a'; + * ``` + */ +class SelectiveReExportDeclaration extends ReExportDeclaration, ExportNamedDeclaration { + SelectiveReExportDeclaration() { exists(ExportNamedDeclaration.super.getImportedPathExpr()) } + + /** Gets the path of the module from which this declaration re-exports. */ + override Expr getImportedPathExpr() { + result = ExportNamedDeclaration.super.getImportedPathExpr() + } +} + +/** + * An export declaration that exports zero or more declarations from the module it appears in. + * + * Examples: + * + * ``` + * export default function f() {}; + * export default 42; + * export { x, y as z }; + * export var x = 42; + * ``` + */ +class OriginalExportDeclaration extends ExportDeclaration { + OriginalExportDeclaration() { not this instanceof ReExportDeclaration } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Expr.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Expr.qll new file mode 100644 index 000000000000..1c3c45ed7af9 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Expr.qll @@ -0,0 +1,2784 @@ +/** + * Provides classes for working with expressions. + */ +overlay[local?] +module; + +import minimal + +/** + * A program element that is either an expression or a type annotation. + * + * Examples: + * + * ``` + * x + 1 + * string[] + * ``` + */ +class ExprOrType extends @expr_or_type, Documentable { + /** Gets the statement in which this expression or type appears. */ + Stmt getEnclosingStmt() { enclosing_stmt(this, result) } + + /** Gets the function in which this expression or type appears, if any. */ + Function getEnclosingFunction() { result = this.getContainer() } + + /** + * Gets the JSDoc comment associated with this expression or type or its parent statement, if any. + */ + override JSDoc getDocumentation() { + result = this.getOwnDocumentation() + or + // if there is no JSDoc for the expression itself, check the enclosing property or statement + not exists(this.getOwnDocumentation()) and + ( + exists(Property prop | prop = this.getParent() | result = prop.getDocumentation()) + or + exists(MethodDeclaration decl | decl = this.getParent() | result = decl.getDocumentation()) + or + exists(VariableDeclarator decl | decl = this.getParent() | result = decl.getDocumentation()) + or + exists(DeclStmt stmt | this = stmt.getDecl(0) | result = stmt.getDocumentation()) + or + exists(DotExpr dot | this = dot.getProperty() | result = dot.getDocumentation()) + or + exists(AssignExpr e | this = e.getRhs() | result = e.getDocumentation()) + or + exists(ParExpr p | this = p.getExpression() | result = p.getDocumentation()) + ) + } + + /** Gets a JSDoc comment that is immediately before this expression or type (ignoring parentheses). */ + private JSDoc getOwnDocumentation() { + exists(Token tk | tk = result.getComment().getNextToken() | + tk = this.getFirstToken() + or + exists(Expr p | p.getUnderlyingValue() = this | tk = p.getFirstToken()) + ) + } + + /** + * Gets this expression or type, with any surrounding parentheses removed. + * + * Also see `getUnderlyingValue` and `getUnderlyingReference`. + */ + ExprOrType stripParens() { result = this } + + /** + * Gets the innermost reference that this expression evaluates to, if any. + * + * Examples: + * + * - a variable or property access: the access itself. + * - a parenthesized expression `(e)`: the underlying reference of `e`. + * - a TypeScript type assertion `e as T`: the underlying reference of `e`. + * + * Also see `getUnderlyingValue` and `stripParens`. + */ + Expr getUnderlyingReference() { none() } + + /** + * Gets the innermost expression that this expression evaluates to. + * + * Examples: + * + * - a parenthesised expression `(e)`: the underlying value of `e`. + * - a sequence expression `e1, e2`: the underlying value of `e2`. + * - an assignment expression `v = e`: the underlying value of `e`. + * - a TypeScript type assertion `e as T`: the underlying value of `e`. + * - any other expression: the expression itself. + * + * Also see `getUnderlyingReference` and `stripParens`. + */ + cached + Expr getUnderlyingValue() { result = this } +} + +/** + * An expression. + * + * Example: + * + * ``` + * Math.sqrt(x*x + y*y) + * ``` + */ +class Expr extends @expr, ExprOrStmt, ExprOrType, AST::ValueNode { + /** Gets this expression, with any surrounding parentheses removed. */ + override Expr stripParens() { result = this } + + /** Gets the constant integer value this expression evaluates to, if any. */ + int getIntValue() { none() } + + /** Gets the constant string value this expression evaluates to, if any. */ + cached + string getStringValue() { result = getStringValue(this) } + + /** Holds if this expression is impure, that is, its evaluation could have side effects. */ + overlay[global] + predicate isImpure() { any() } + + /** + * Holds if this expression is pure, that is, its evaluation is guaranteed + * to be side-effect free. + */ + overlay[global] + predicate isPure() { not this.isImpure() } + + /** + * Gets the kind of this expression, which is an integer value representing the expression's + * node type. + * + * _Note_: The mapping from node types to integers is considered an implementation detail + * and may change between versions of the extractor. + */ + int getKind() { exprs(this, result, _, _, _) } + + override string toString() { exprs(this, _, _, _, result) } + + /** + * Gets the expression that is the parent of this expression in the AST, if any. + * + * Note that for property names and property values the associated object expression or pattern + * is returned, skipping the property node itself (which is not an expression). + */ + Expr getParentExpr() { + this = result.getAChildExpr() + or + exists(Property prop | + result = prop.getParent() and + this = prop.getAChildExpr() + ) + } + + /** + * Holds if the syntactic context that the expression appears in relies on the expression + * being non-null/non-undefined. + * + * A context relies on the subexpression being non-null/non-undefined if either... + * + * * Using null or undefined would cause a runtime error + * * Using null or undefined would cause no error due to type conversion, but the + * behavior in the broader context is sufficiently non-obvious to warrant explicitly + * converting to ensure that readers understand the intent + */ + predicate inNullSensitiveContext() { + exists(ExprOrStmt ctx | + // bases cases + this = ctx.(PropAccess).getBase() + or + this = ctx.(IndexExpr).getPropertyNameExpr() + or + this = ctx.(InvokeExpr).getCallee() + or + this = ctx.(BinaryExpr).getAnOperand() and + not ctx instanceof LogicalBinaryExpr and // x LOGOP y is fine because of implicit conversion + not ctx instanceof EqualityTest and // x EQOP y is fine because of implicit conversion and lack thereof + not ctx.(BitOrExpr).getAnOperand().(NumberLiteral).getIntValue() = 0 and // x | 0 is fine because it's used to convert to numbers + not ctx.(RShiftExpr).getRightOperand().(NumberLiteral).getIntValue() = 0 and // x >> 0 is fine because it's used to convert to numbers + not ctx.(URShiftExpr).getRightOperand().(NumberLiteral).getIntValue() = 0 // x >>> 0 is fine because it's used to convert to numbers + or + this = ctx.(UnaryExpr).getOperand() and + not ctx instanceof LogNotExpr and // !x is fine because of implicit conversion + not ctx instanceof PlusExpr and // +x is fine because of implicit conversion + not ctx instanceof VoidExpr // void x is fine because it explicitly is for capturing void things + or + this = ctx.(UpdateExpr).getOperand() + or + this = ctx.(CompoundAssignExpr).getLhs() + or + this = ctx.(CompoundAssignExpr).getRhs() + or + this = ctx.(AssignExpr).getRhs() and + ctx.(AssignExpr).getLhs() instanceof DestructuringPattern + or + this = ctx.(SpreadElement).getOperand() + or + this = ctx.(ForOfStmt).getIterationDomain() + or + // recursive cases + this = ctx.(ParExpr).getExpression() and + ctx.(ParExpr).inNullSensitiveContext() + or + this = ctx.(SeqExpr).getLastOperand() and + ctx.(SeqExpr).inNullSensitiveContext() + or + this = ctx.(LogicalBinaryExpr).getRightOperand() and + ctx.(LogicalBinaryExpr).inNullSensitiveContext() + or + this = ctx.(ConditionalExpr).getABranch() and + ctx.(ConditionalExpr).inNullSensitiveContext() + ) + } +} + +/** + * An identifier. + * + * Example: + * + * ``` + * x + * ``` + */ +class Identifier extends @identifier, ExprOrType { + /** Gets the name of this identifier. */ + cached + string getName() { literals(result, _, this) } + + override string getAPrimaryQlClass() { result = "Identifier" } +} + +/** + * A statement or property label, that is, an identifier that + * does not refer to a variable. + * + * Examples: + * + * ``` + * outer: // `outer` is a statement label + * for(i=0; i console.log("Hi!"); // arrow function expression + */ +class ArrowFunctionExpr extends @arrow_function_expr, Expr, Function { + /** Gets the statement in which this expression appears. */ + override Stmt getEnclosingStmt() { result = Expr.super.getEnclosingStmt() } + + override StmtContainer getEnclosingContainer() { result = Expr.super.getContainer() } + + overlay[global] + override predicate isImpure() { none() } + + override Function getThisBinder() { + result = this.getEnclosingContainer().(Function).getThisBinder() + } + + override string getAPrimaryQlClass() { result = "ArrowFunctionExpr" } +} + +/** + * A sequence expression (also known as comma expression). + * + * Example: + * + * ``` + * x++, y++ + * ``` + */ +class SeqExpr extends @seq_expr, Expr { + /** Gets the `i`th expression in this sequence. */ + Expr getOperand(int i) { result = this.getChildExpr(i) } + + /** Gets an expression in this sequence. */ + Expr getAnOperand() { result = this.getOperand(_) } + + /** Gets the number of expressions in this sequence. */ + int getNumOperands() { result = count(this.getOperand(_)) } + + /** Gets the last expression in this sequence. */ + Expr getLastOperand() { result = this.getOperand(this.getNumOperands() - 1) } + + overlay[global] + override predicate isImpure() { this.getAnOperand().isImpure() } + + override Expr getUnderlyingValue() { result = this.getLastOperand().getUnderlyingValue() } + + override string getAPrimaryQlClass() { result = "SeqExpr" } +} + +/** + * A conditional expression. + * + * Example: + * + * ``` + * x == 0 ? 0 : 1/x + * ``` + */ +class ConditionalExpr extends @conditional_expr, Expr { + /** Gets the condition expression of this conditional. */ + Expr getCondition() { result = this.getChildExpr(0) } + + /** Gets the 'then' expression of this conditional. */ + Expr getConsequent() { result = this.getChildExpr(1) } + + /** Gets the 'else' expression of this conditional. */ + Expr getAlternate() { result = this.getChildExpr(2) } + + /** Gets either the 'then' or the 'else' expression of this conditional. */ + Expr getABranch() { result = this.getConsequent() or result = this.getAlternate() } + + overlay[global] + override predicate isImpure() { + this.getCondition().isImpure() or + this.getABranch().isImpure() + } + + override string getAPrimaryQlClass() { result = "ConditionalExpr" } +} + +/** + * An invocation expression, that is, either a function call or + * a `new` expression. + * + * Examples: + * + * ``` + * f(1) + * x.f(1, ...args) + * f.call(x, 1) + * new Array(16) + * ``` + */ +class InvokeExpr extends @invokeexpr, Expr { + /** Gets the expression specifying the function to be called. */ + Expr getCallee() { result = this.getChildExpr(-1) } + + /** Gets the name of the function or method being invoked, if it can be determined. */ + string getCalleeName() { + exists(Expr callee | callee = this.getCallee().getUnderlyingValue() | + result = callee.(Identifier).getName() or + result = callee.(PropAccess).getPropertyName() + ) + } + + /** Gets the `i`th argument of this invocation. */ + Expr getArgument(int i) { i >= 0 and result = this.getChildExpr(i) } + + /** Gets an argument of this invocation. */ + Expr getAnArgument() { result = this.getArgument(_) } + + /** Gets the last argument of this invocation, if any. */ + Expr getLastArgument() { result = this.getArgument(this.getNumArgument() - 1) } + + /** Gets the number of arguments of this invocation. */ + int getNumArgument() { result = count(this.getAnArgument()) } + + /** Gets the `i`th type argument of this invocation. */ + TypeExpr getTypeArgument(int i) { i >= 0 and result = this.getChildTypeExpr(-i - 2) } + + /** Gets a type argument of this invocation. */ + TypeExpr getATypeArgument() { result = this.getTypeArgument(_) } + + /** Gets the number of type arguments of this invocation. */ + int getNumTypeArgument() { result = count(this.getATypeArgument()) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getCallee().getFirstControlFlowNode() + } + + /** Holds if the argument list of this function has a trailing comma. */ + predicate hasTrailingComma() { + // check whether the last token of this invocation is a closing + // parenthesis, which itself is preceded by a comma + exists(PunctuatorToken rparen | rparen.getValue() = ")" | + rparen = this.getLastToken() and + rparen.getPreviousToken().getValue() = "," + ) + } + + /** + * Holds if the `i`th argument of this invocation is a spread element. + */ + predicate isSpreadArgument(int i) { this.getArgument(i).stripParens() instanceof SpreadElement } + + /** Gets the first index of a spread argument (`...`), if any. */ + int getFirstSpreadIndex() { result = min(int i | this.isSpreadArgument(i)) } + + /** + * Gets the index of the targeted call signature among the overload signatures + * on the invoked function. + * + * This predicate is only populated for files extracted with full TypeScript extraction. + */ + int getResolvedOverloadIndex() { invoke_expr_overload_index(this, result) } +} + +/** + * A `new` expression. + * + * Example: + * + * ``` + * new Array(16) + * ``` + */ +class NewExpr extends @new_expr, InvokeExpr { + override string getAPrimaryQlClass() { result = "NewExpr" } +} + +/** + * A function call expression. + * + * Examples: + * + * ``` + * f() + * require('express')() + * x.f() + * ``` + */ +class CallExpr extends @call_expr, InvokeExpr { + /** + * Gets the expression specifying the receiver on which the function + * is invoked, if any. + */ + Expr getReceiver() { result = this.getCallee().(PropAccess).getBase() } + + override string getAPrimaryQlClass() { result = "CallExpr" } +} + +/** + * A method call expression. + * + * Examples: + * + * ``` + * Object.create(null) + * [1, 2, 3].forEach(alert); + * ``` + */ +class MethodCallExpr extends CallExpr { + MethodCallExpr() { this.getCallee().stripParens() instanceof PropAccess } + + /** + * Gets the property access referencing the method to be invoked. + */ + private PropAccess getMethodRef() { result = this.getCallee().stripParens() } + + /** + * Gets the receiver expression of this method call. + */ + override Expr getReceiver() { result = this.getMethodRef().getBase() } + + /** + * Gets the name of the invoked method, if it can be determined. + */ + string getMethodName() { result = this.getMethodRef().getPropertyName() } + + /** Holds if this invocation calls method `m` on expression `base`. */ + predicate calls(Expr base, string m) { this.getMethodRef().accesses(base, m) } + + override string getAPrimaryQlClass() { result = "MethodCallExpr" } +} + +/** + * A property access, that is, either a dot expression of the form + * `e.f` or an index expression of the form `e[p]`. + * + * Examples: + * + * ``` + * Math.PI + * arguments[i] + * ``` + */ +class PropAccess extends @propaccess, Expr { + /** Gets the base expression on which the property is accessed. */ + Expr getBase() { result = this.getChildExpr(0) } + + /** + * Gets the expression specifying the name of the property being + * read or written. For dot expressions, this is an identifier; for + * index expressions it can be an arbitrary expression. + */ + Expr getPropertyNameExpr() { result = this.getChildExpr(1) } + + /** Gets the name of the accessed property, if it can be determined. */ + string getPropertyName() { none() } + + /** Gets the qualified name of the accessed property, if it can be determined. */ + string getQualifiedName() { + exists(string basename | + basename = this.getBase().(Identifier).getName() or + basename = this.getBase().(PropAccess).getQualifiedName() + | + result = basename + "." + this.getPropertyName() + ) + } + + /** Holds if this property name accesses property `p` on expression `base`. */ + predicate accesses(Expr base, string p) { + base = this.getBase() and + p = this.getPropertyName() + } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getBase().getFirstControlFlowNode() + } + + override Expr getUnderlyingReference() { result = this } +} + +/** + * A dot expression. + * + * Example: + * + * ``` + * Math.PI + * ``` + */ +class DotExpr extends @dot_expr, PropAccess { + override string getPropertyName() { result = this.getProperty().getName() } + + /** Gets the identifier specifying the name of the accessed property. */ + Identifier getProperty() { result = this.getChildExpr(1) } + + overlay[global] + override predicate isImpure() { this.getBase().isImpure() } + + override string getAPrimaryQlClass() { result = "DotExpr" } +} + +/** + * An index expression (also known as computed property access). + * + * Example: + * + * ``` + * arguments[i] + * ``` + */ +class IndexExpr extends @index_expr, PropAccess { + /** Gets the expression specifying the name of the accessed property. */ + Expr getIndex() { result = this.getChildExpr(1) } + + override string getPropertyName() { result = this.getIndex().(Literal).getValue() } + + overlay[global] + override predicate isImpure() { + this.getBase().isImpure() or + this.getIndex().isImpure() + } + + override string getAPrimaryQlClass() { result = "IndexExpr" } +} + +/** + * An expression with a unary operator. + * + * Examples: + * + * ``` + * -x + * !!done + * ``` + */ +class UnaryExpr extends @unaryexpr, Expr { + /** Gets the operand of this unary operator. */ + Expr getOperand() { result = this.getChildExpr(0) } + + /** Gets the operator of this expression. */ + string getOperator() { none() } + + overlay[global] + override predicate isImpure() { this.getOperand().isImpure() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getOperand().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "UnaryExpr" } +} + +/** + * An arithmetic negation expression (also known as unary minus). + * + * Example: + * + * ``` + * -x + * ``` + */ +class NegExpr extends @neg_expr, UnaryExpr { + override string getOperator() { result = "-" } + + override int getIntValue() { result = -this.getOperand().getIntValue() } +} + +/** + * A unary plus expression. + * + * Example: + * + * ``` + * +x + * ``` + */ +class PlusExpr extends @plus_expr, UnaryExpr { + override string getOperator() { result = "+" } +} + +/** + * A logical negation expression. + * + * Example: + * + * ``` + * !done + * ``` + */ +class LogNotExpr extends @log_not_expr, UnaryExpr { + override string getOperator() { result = "!" } +} + +/** + * A bitwise negation expression. + * + * Example: + * + * ``` + * ~bitmask + * ``` + */ +class BitNotExpr extends @bit_not_expr, UnaryExpr { + override string getOperator() { result = "~" } +} + +/** + * A `typeof` expression. + * + * Example: + * + * ``` + * typeof A.prototype + * ``` + */ +class TypeofExpr extends @typeof_expr, UnaryExpr { + override string getOperator() { result = "typeof" } +} + +/** + * A `void` expression. + * + * Example: + * + * ``` + * void(0) + * ``` + */ +class VoidExpr extends @void_expr, UnaryExpr { + override string getOperator() { result = "void" } +} + +/** + * A `delete` expression. + * + * Example: + * + * ``` + * delete elt[_expando] + * ``` + */ +class DeleteExpr extends @delete_expr, UnaryExpr { + override string getOperator() { result = "delete" } + + overlay[global] + override predicate isImpure() { any() } +} + +/** + * A spread element. + * + * Example: + * + * ``` + * [].concat( + * ...lists // a spread element + * ) + * ``` + */ +class SpreadElement extends @spread_element, UnaryExpr { + override string getOperator() { result = "..." } + + override string getAPrimaryQlClass() { result = "SpreadElement" } +} + +/** + * An expression with a binary operator. + * + * Examples: + * + * ``` + * x + 1 + * a instanceof Array + * ``` + */ +class BinaryExpr extends @binaryexpr, Expr { + /** Gets the left operand of this binary operator. */ + Expr getLeftOperand() { result = this.getChildExpr(0) } + + /** Gets the right operand of this binary operator. */ + Expr getRightOperand() { result = this.getChildExpr(1) } + + /** Gets an operand of this binary operator. */ + Expr getAnOperand() { result = this.getAChildExpr() } + + /** Holds if `e` and `f` (in either order) are the two operands of this expression. */ + predicate hasOperands(Expr e, Expr f) { + e = this.getAnOperand() and + f = this.getAnOperand() and + e != f + } + + /** Gets the operator of this expression. */ + string getOperator() { none() } + + overlay[global] + override predicate isImpure() { this.getAnOperand().isImpure() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getLeftOperand().getFirstControlFlowNode() + } + + /** + * Gets the number of whitespace characters around the operator of this expression. + * + * This predicate is only defined if both operands are on the same line, and if the + * amount of whitespace before and after the operator are the same. + */ + int getWhitespaceAroundOperator() { + exists(Token lastLeft, Token operator, Token firstRight, int l, int c1, int c2, int c3, int c4 | + lastLeft = this.getLeftOperand().getLastToken() and + operator = lastLeft.getNextToken() and + firstRight = operator.getNextToken() and + lastLeft.getLocation().hasLocationInfo(_, _, _, l, c1) and + operator.getLocation().hasLocationInfo(_, l, c2, l, c3) and + firstRight.getLocation().hasLocationInfo(_, l, c4, _, _) and + result = c2 - c1 - 1 and + result = c4 - c3 - 1 + ) + } + + override string getAPrimaryQlClass() { result = "BinaryExpr" } +} + +/** + * A comparison expression, that is, either an equality test + * (`==`, `!=`, `===`, `!==`) or a relational expression + * (`<`, `<=`, `>=`, `>`). + * + * Examples: + * + * ``` + * x !== y + * y < 0 + * ``` + */ +class Comparison extends @comparison, BinaryExpr { } + +/** + * An equality test using `==`, `!=`, `===` or `!==`. + * + * Examples: + * + * ``` + * "" == arg + * x != null + * recv === undefined + * res !== res + * ``` + */ +class EqualityTest extends @equality_test, Comparison { + /** Gets the polarity of this test: `true` for equalities, `false` for inequalities. */ + boolean getPolarity() { + (this instanceof EqExpr or this instanceof StrictEqExpr) and + result = true + or + (this instanceof NEqExpr or this instanceof StrictNEqExpr) and + result = false + } + + /** + * Holds if the equality operator is strict (`===` or `!==`). + */ + predicate isStrict() { this instanceof StrictEqExpr or this instanceof StrictNEqExpr } +} + +/** + * An equality test using `==`. + * + * Example: + * + * ``` + * "" == arg + * ``` + */ +class EqExpr extends @eq_expr, EqualityTest { + override string getOperator() { result = "==" } +} + +/** + * An inequality test using `!=`. + * + * Example: + * + * ``` + * x != null + * ``` + */ +class NEqExpr extends @neq_expr, EqualityTest { + override string getOperator() { result = "!=" } +} + +/** + * A strict equality test using `===`. + * + * Example: + * + * ``` + * recv === undefined + * ``` + */ +class StrictEqExpr extends @eqq_expr, EqualityTest { + override string getOperator() { result = "===" } +} + +/** + * A strict inequality test using `!==`. + * + * Example: + * + * ``` + * res !== res + * ``` + */ +class StrictNEqExpr extends @neqq_expr, EqualityTest { + override string getOperator() { result = "!==" } +} + +/** + * A less-than expression. + * + * Example: + * + * ``` + * i < 10 + * ``` + */ +class LTExpr extends @lt_expr, Comparison { + override string getOperator() { result = "<" } +} + +/** + * A less-than-or-equal expression. + * + * Example: + * + * ``` + * x+1 <= a.length + * ``` + */ +class LEExpr extends @le_expr, Comparison { + override string getOperator() { result = "<=" } +} + +/** + * A greater-than expression. + * + * Example: + * + * ``` + * a[j] > a[k] + * ``` + */ +class GTExpr extends @gt_expr, Comparison { + override string getOperator() { result = ">" } +} + +/** + * A greater-than-or-equal expression. + * + * Example: + * + * ``` + * x >= 0 + * ``` + */ +class GEExpr extends @ge_expr, Comparison { + override string getOperator() { result = ">=" } +} + +/** + * A left-shift expression using `<<`. + * + * Example: + * + * ``` + * 2 << i + * ``` + */ +class LShiftExpr extends @lshift_expr, BinaryExpr { + override string getOperator() { result = "<<" } +} + +/** + * A right-shift expression using `>>`. + * + * Example: + * + * ``` + * r >> 8 + * ``` + */ +class RShiftExpr extends @rshift_expr, BinaryExpr { + override string getOperator() { result = ">>" } +} + +/** + * An unsigned right-shift expression using `>>>`. + * + * Example: + * + * ``` + * u >>> v + * ``` + */ +class URShiftExpr extends @urshift_expr, BinaryExpr { + override string getOperator() { result = ">>>" } +} + +/** + * An addition or string-concatenation expression. + * + * Examples: + * + * ``` + * a + b + * msg + "\n" + * ``` + */ +class AddExpr extends @add_expr, BinaryExpr { + override string getOperator() { result = "+" } +} + +/** + * Gets the string value for the expression `e`. + * This string-value is either a constant-string, or the result from a simple string-concatenation. + */ +private string getStringValue(Expr e) { result = getConstantString(e) } + +/** + * Gets the constant string value for the expression `e`. + */ +private string getConstantString(Expr e) { + result = getConstantString(e.getUnderlyingValue()) + or + result = e.(StringLiteral).getValue() + or + exists(TemplateLiteral lit | lit = e | + // fold singletons + lit.getNumChildExpr() = 0 and + result = "" + or + e.getNumChildExpr() = 1 and + result = getConstantString(lit.getElement(0)) + ) + or + result = e.(TemplateElement).getValue() +} + +/** + * A subtraction expression. + * + * Example: + * + * ``` + * w - len + * ``` + */ +class SubExpr extends @sub_expr, BinaryExpr { + override string getOperator() { result = "-" } +} + +/** + * A multiplication expression. + * + * Example: + * + * ``` + * x * y + * ``` + */ +class MulExpr extends @mul_expr, BinaryExpr { + override string getOperator() { result = "*" } +} + +/** + * A division expression. + * + * Example: + * + * ``` + * gg / ac + * ``` + */ +class DivExpr extends @div_expr, BinaryExpr { + override string getOperator() { result = "/" } +} + +/** + * A modulo expression. + * + * Example: + * + * ``` + * n % 2 + * ``` + */ +class ModExpr extends @mod_expr, BinaryExpr { + override string getOperator() { result = "%" } +} + +/** + * An exponentiation expression. + * + * Example: + * + * ``` + * p ** 10 + * ``` + */ +class ExpExpr extends @exp_expr, BinaryExpr { + override string getOperator() { result = "**" } +} + +/** + * A bitwise 'or' expression. + * + * Example: + * + * ``` + * O_RDWR | O_APPEND + * ``` + */ +class BitOrExpr extends @bitor_expr, BinaryExpr { + override string getOperator() { result = "|" } +} + +/** + * An exclusive 'or' expression. + * + * Example: + * + * ``` + * x ^ 1 + * ``` + */ +class XOrExpr extends @xor_expr, BinaryExpr { + override string getOperator() { result = "^" } +} + +/** + * A bitwise 'and' expression. + * + * Example: + * + * ``` + * flags & O_APPEND + * ``` + */ +class BitAndExpr extends @bitand_expr, BinaryExpr { + override string getOperator() { result = "&" } +} + +/** + * An `in` expression. + * + * Example: + * + * ``` + * "leftpad" in String.prototype + * ``` + */ +class InExpr extends @in_expr, BinaryExpr { + override string getOperator() { result = "in" } +} + +/** + * An `instanceof` expression. + * + * Example: + * + * ``` + * b instanceof Buffer + * ``` + */ +class InstanceofExpr extends @instanceof_expr, BinaryExpr { + override string getOperator() { result = "instanceof" } +} + +/** + * A logical 'and' expression. + * + * Example: + * + * ``` + * x != null && x.f + * ``` + */ +class LogAndExpr extends @logand_expr, BinaryExpr { + override string getOperator() { result = "&&" } + + override ControlFlowNode getFirstControlFlowNode() { result = this } +} + +/** + * A logical 'or' expression. + * + * Example: + * + * ``` + * x == null || x.f + * ``` + */ +class LogOrExpr extends @logor_expr, BinaryExpr { + override string getOperator() { result = "||" } + + override ControlFlowNode getFirstControlFlowNode() { result = this } +} + +/** + * A nullish coalescing '??' expression. + * + * Example: + * + * ``` + * x ?? f + * ``` + */ +class NullishCoalescingExpr extends @nullishcoalescing_expr, BinaryExpr { + override string getOperator() { result = "??" } + + override ControlFlowNode getFirstControlFlowNode() { result = this } +} + +/** + * A short-circuiting logical binary expression, that is, a logical 'or' expression, + * a logical 'and' expression, or a nullish-coalescing expression. + * + * Examples: + * + * ``` + * x && x.f + * !x || x.f + * x ?? f + * ``` + */ +class LogicalBinaryExpr extends BinaryExpr { + LogicalBinaryExpr() { + this instanceof LogAndExpr or + this instanceof LogOrExpr or + this instanceof NullishCoalescingExpr + } +} + +/** + * A bitwise binary expression, that is, either a bitwise + * 'and', a bitwise 'or', or an exclusive 'or' expression. + * + * Examples: + * + * ``` + * qw & 0xffff + * O_RDWR | O_APPEND + * x ^ 1 + * ``` + */ +class BitwiseBinaryExpr extends BinaryExpr { + BitwiseBinaryExpr() { + this instanceof BitAndExpr or + this instanceof BitOrExpr or + this instanceof XOrExpr + } +} + +/** + * A shift expression. + * + * Examples: + * + * ``` + * 2 << i + * r >> 8 + * u >>> v + * ``` + */ +class ShiftExpr extends BinaryExpr { + ShiftExpr() { + this instanceof LShiftExpr or + this instanceof RShiftExpr or + this instanceof URShiftExpr + } +} + +/** + * An assignment expression, either compound or simple. + * + * Examples: + * + * ``` + * x = y + * sum += element + * ``` + */ +class Assignment extends @assignment, Expr { + /** Gets the left hand side of this assignment. */ + Expr getLhs() { result = this.getChildExpr(0) } + + /** Gets the right hand side of this assignment. */ + Expr getRhs() { result = this.getChildExpr(1) } + + /** Gets the variable or property this assignment writes to, if any. */ + Expr getTarget() { result = this.getLhs().stripParens() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getLhs().getFirstControlFlowNode() + } +} + +/** + * A simple assignment expression. + * + * Example: + * + * ``` + * x = y + * ``` + */ +class AssignExpr extends @assign_expr, Assignment { + override Expr getUnderlyingValue() { result = this.getRhs().getUnderlyingValue() } + + override string getAPrimaryQlClass() { result = "AssignExpr" } +} + +private class TCompoundAssignExpr = + @assign_add_expr or @assign_sub_expr or @assign_mul_expr or @assign_div_expr or + @assign_mod_expr or @assign_exp_expr or @assign_lshift_expr or @assign_rshift_expr or + @assign_urshift_expr or @assign_or_expr or @assign_xor_expr or @assign_and_expr or + @assignlogandexpr or @assignlogorexpr or @assignnullishcoalescingexpr; + +/** + * A compound assign expression. + * + * Examples: + * + * ``` + * sum += element + * x /= 2 + * ``` + */ +class CompoundAssignExpr extends TCompoundAssignExpr, Assignment { + override string getAPrimaryQlClass() { result = "CompoundAssignExpr" } + + /** + * Holds if this compound assignment always returns a number value. + */ + predicate isNumeric() { + not ( + this instanceof AssignAddExpr or + this instanceof AssignLogOrExpr or + this instanceof AssignLogAndExpr or + this instanceof AssignNullishCoalescingExpr + ) + } +} + +/** + * A compound add-assign expression. + * + * Example: + * + * ``` + * sum += element + * ``` + */ +class AssignAddExpr extends @assign_add_expr, CompoundAssignExpr { } + +/** + * A compound subtract-assign expression. + * + * Example: + * + * ``` + * i -= 2 + * ``` + */ +class AssignSubExpr extends @assign_sub_expr, CompoundAssignExpr { } + +/** + * A compound multiply-assign expression. + * + * Example: + * + * ``` + * x *= y + * ``` + */ +class AssignMulExpr extends @assign_mul_expr, CompoundAssignExpr { } + +/** + * A compound divide-assign expression. + * + * Example: + * + * ``` + * n /= 10 + * ``` + */ +class AssignDivExpr extends @assign_div_expr, CompoundAssignExpr { } + +/** + * A compound modulo-assign expression. + * + * Example: + * + * ``` + * m %= 3 + * ``` + */ +class AssignModExpr extends @assign_mod_expr, CompoundAssignExpr { } + +/** + * A compound exponentiate-assign expression. + * + * Example: + * + * ``` + * scale **= 10 + * ``` + */ +class AssignExpExpr extends @assign_exp_expr, CompoundAssignExpr { } + +/** + * A compound left-shift-assign expression. + * + * Example: + * + * ``` + * exp <<= 2 + * ``` + */ +class AssignLShiftExpr extends @assign_lshift_expr, CompoundAssignExpr { } + +/** + * A compound right-shift-assign expression. + * + * Example: + * + * ``` + * qw >>= 8 + * ``` + */ +class AssignRShiftExpr extends @assign_rshift_expr, CompoundAssignExpr { } + +/** + * A compound unsigned-right-shift-assign expression. + * + * Example: + * + * ``` + * bits >>>= 16 + * ``` + */ +class AssignURShiftExpr extends @assign_urshift_expr, CompoundAssignExpr { } + +/** + * A compound bitwise-'or'-assign expression. + * + * Example: + * + * ``` + * flags |= O_CREAT + * ``` + */ +class AssignOrExpr extends @assign_or_expr, CompoundAssignExpr { } + +/** + * A compound exclusive-'or'-assign expression. + * + * Example: + * + * ``` + * bits ^= mask + * ``` + */ +class AssignXOrExpr extends @assign_xor_expr, CompoundAssignExpr { } + +/** + * A compound bitwise-'and'-assign expression. + * + * Example: + * + * ``` + * data &= 0xffff + * ``` + */ +class AssignAndExpr extends @assign_and_expr, CompoundAssignExpr { } + +/** + * A logical-'or'-assign expression. + * + * Example: + * + * ``` + * x ||= y + * ``` + */ +class AssignLogOrExpr extends @assignlogorexpr, CompoundAssignExpr { } + +/** + * A logical-'and'-assign expression. + * + * Example: + * + * ``` + * x &&= y + * ``` + */ +class AssignLogAndExpr extends @assignlogandexpr, CompoundAssignExpr { } + +/** + * A 'nullish-coalescing'-assign expression. + * + * Example: + * + * ``` + * x ??= y + * ``` + */ +class AssignNullishCoalescingExpr extends @assignnullishcoalescingexpr, CompoundAssignExpr { } + +/** + * An update expression, that is, an increment or decrement expression. + * + * Examples: + * + * ``` + * ++i + * --i + * i++ + * i-- + * ``` + */ +class UpdateExpr extends @updateexpr, Expr { + /** Gets the operand of this update. */ + Expr getOperand() { result = this.getChildExpr(0) } + + /** Holds if this is a prefix increment or prefix decrement expression. */ + predicate isPrefix() { none() } + + /** Gets the operator of this update expression. */ + string getOperator() { none() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getOperand().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "UpdateExpr" } +} + +/** + * A prefix increment expression. + * + * Example: + * + * ``` + * ++i + * ``` + */ +class PreIncExpr extends @preinc_expr, UpdateExpr { + override predicate isPrefix() { any() } + + override string getOperator() { result = "++" } +} + +/** + * A postfix increment expression. + * + * Example: + * + * ``` + * i++ + * ``` + */ +class PostIncExpr extends @postinc_expr, UpdateExpr { + override string getOperator() { result = "++" } +} + +/** + * A prefix decrement expression. + * + * Example: + * + * ``` + * --i + * ``` + */ +class PreDecExpr extends @predec_expr, UpdateExpr { + override predicate isPrefix() { any() } + + override string getOperator() { result = "--" } +} + +/** + * A postfix decrement expression. + * + * Example: + * + * ``` + * i-- + * ``` + */ +class PostDecExpr extends @postdec_expr, UpdateExpr { + override string getOperator() { result = "--" } +} + +/** + * A `yield` expression. + * + * Example: + * + * ``` + * yield next + * ``` + */ +class YieldExpr extends @yield_expr, Expr { + /** Gets the operand of this `yield` expression. */ + Expr getOperand() { result = this.getChildExpr(0) } + + /** Holds if this is a `yield*` expression. */ + predicate isDelegating() { is_delegating(this) } + + overlay[global] + override predicate isImpure() { any() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getOperand().getFirstControlFlowNode() + or + not exists(this.getOperand()) and result = this + } + + override string getAPrimaryQlClass() { result = "YieldExpr" } +} + +/** + * A comprehension expression, that is, either an array comprehension + * expression or a generator expression. + * + * Examples: + * + * ``` + * [for (x of xs) x*x] + * (for (x of xs) x*x) + * ``` + */ +class ComprehensionExpr extends @comprehension_expr, Expr { + /** Gets the `n`th comprehension block in this comprehension. */ + ComprehensionBlock getBlock(int n) { + exists(int idx | + result = this.getChildExpr(idx) and + idx > 0 and + n = idx - 1 + ) + } + + /** Gets a comprehension block in this comprehension. */ + ComprehensionBlock getABlock() { result = this.getBlock(_) } + + /** Gets the number of comprehension blocks in this comprehension. */ + int getNumBlock() { result = count(this.getABlock()) } + + /** Gets the `n`th filter expression in this comprehension. */ + Expr getFilter(int n) { + exists(int idx | + result = this.getChildExpr(idx) and + idx < 0 and + n = -idx - 1 + ) + } + + /** Gets a filter expression in this comprehension. */ + Expr getAFilter() { result = this.getFilter(_) } + + /** Gets the number of filter expressions in this comprehension. */ + int getNumFilter() { result = count(this.getAFilter()) } + + /** Gets the body expression of this comprehension. */ + Expr getBody() { result = this.getChildExpr(0) } + + overlay[global] + override predicate isImpure() { + this.getABlock().isImpure() or + this.getAFilter().isImpure() or + this.getBody().isImpure() + } + + /** Holds if this is a legacy postfix comprehension expression. */ + predicate isPostfix() { + exists(Token tk | tk = this.getFirstToken().getNextToken() | not tk.getValue() = ["if", "for"]) + } + + override string getAPrimaryQlClass() { result = "ComprehensionExpr" } +} + +/** + * An array comprehension expression. + * + * Example: + * + * ``` + * [for (x of xs) x*x] + * ``` + */ +class ArrayComprehensionExpr extends @array_comprehension_expr, ComprehensionExpr { } + +/** + * A generator expression. + * + * Example: + * + * ``` + * (for (x of xs) x*x) + * ``` + */ +class GeneratorExpr extends @generator_expr, ComprehensionExpr { } + +/** + * A comprehension block in a comprehension expression. + * + * Examples: + * + * ``` + * [ + * for (x of [1, 2 3]) // comprehension block + * x*x + * ] + * + * [ + * for (x in o) // comprehension block + * "_" + x + * ] + * ``` + */ +class ComprehensionBlock extends @comprehension_block, Expr { + /** Gets the iterating variable or pattern of this comprehension block. */ + BindingPattern getIterator() { result = this.getChildExpr(0) } + + /** Gets the domain over which this comprehension block iterates. */ + Expr getDomain() { result = this.getChildExpr(1) } + + overlay[global] + override predicate isImpure() { + this.getIterator().isImpure() or + this.getDomain().isImpure() + } + + override string getAPrimaryQlClass() { result = "ComprehensionBlock" } +} + +/** + * A `for`-`in` comprehension block in a comprehension expression. + * + * Example: + * + * ``` + * [ + * for (x in o) // comprehension block + * "_" + x + * ] + * ``` + */ +class ForInComprehensionBlock extends @for_in_comprehension_block, ComprehensionBlock { } + +/** + * A `for`-`of` comprehension block in a comprehension expression. + * + * Example: + * + * ``` + * [ + * for (x of [1, 2 3]) // comprehension block + * x*x + * ] + * ``` + */ +class ForOfComprehensionBlock extends @for_of_comprehension_block, ComprehensionBlock { } + +/** + * A binary arithmetic expression using `+`, `-`, `/`, `%` or `**`. + * + * Examples: + * + * ``` + * x + y + * i - 1 + * dist / scale + * k % 2 + * p ** 10 + * ``` + */ +class ArithmeticExpr extends BinaryExpr { + ArithmeticExpr() { + this instanceof AddExpr or + this instanceof SubExpr or + this instanceof MulExpr or + this instanceof DivExpr or + this instanceof ModExpr or + this instanceof ExpExpr + } +} + +/** + * A logical expression using `&&`, `||`, or `!`. + * + * Examples: + * + * ``` + * x && x.f + * x == null || x.f + * !x + * ``` + */ +class LogicalExpr extends Expr { + LogicalExpr() { + this instanceof LogicalBinaryExpr or + this instanceof LogNotExpr + } +} + +/** + * A bitwise expression using `&`, `|`, `^`, `~`, `<<`, `>>`, or `>>>`. + * + * Examples: + * + * ``` + * qw & 0xffff + * O_RDWR | O_APPEND + * x ^ 1 + * ~bitmask + * 2 << i + * r >> 8 + * u >>> v + * ``` + */ +class BitwiseExpr extends Expr { + BitwiseExpr() { + this instanceof BitwiseBinaryExpr or + this instanceof BitNotExpr or + this instanceof ShiftExpr + } +} + +/** + * A strict equality test using `!==` or `===`. + * + * Examples: + * + * ``` + * recv === undefined + * res !== res + * ``` + */ +class StrictEqualityTest extends EqualityTest { + StrictEqualityTest() { + this instanceof StrictEqExpr or + this instanceof StrictNEqExpr + } +} + +/** + * A non-strict equality test using `!=` or `==`. + * + * Examples: + * + * ``` + * "" == arg + * x != null + * ``` + */ +class NonStrictEqualityTest extends EqualityTest { + NonStrictEqualityTest() { + this instanceof EqExpr or + this instanceof NEqExpr + } +} + +/** + * A relational comparison using `<`, `<=`, `>=`, or `>`. + * + * Examples: + * + * ``` + * i < 10 + * x+1 <= a.length + * x >= 0 + * a[j] > a[k] + * ``` + */ +class RelationalComparison extends Comparison { + RelationalComparison() { + this instanceof LTExpr or + this instanceof LEExpr or + this instanceof GEExpr or + this instanceof GTExpr + } + + /** + * Gets the lesser operand of this comparison, that is, the left operand for + * a `<` or `<=` comparison, and the right operand for `>=` or `>`. + */ + Expr getLesserOperand() { + (this instanceof LTExpr or this instanceof LEExpr) and + result = this.getLeftOperand() + or + (this instanceof GTExpr or this instanceof GEExpr) and + result = this.getRightOperand() + } + + /** + * Gets the greater operand of this comparison, that is, the right operand for + * a `<` or `<=` comparison, and the left operand for `>=` or `>`. + */ + Expr getGreaterOperand() { result = this.getAnOperand() and result != this.getLesserOperand() } + + /** + * Holds if this is a comparison with `<=` or `>=`. + */ + predicate isInclusive() { + this instanceof LEExpr or + this instanceof GEExpr + } +} + +/** + * A (pre or post) increment expression. + * + * Examples: + * + * ``` + * ++i + * i++ + * ``` + */ +class IncExpr extends UpdateExpr { + IncExpr() { this instanceof PreIncExpr or this instanceof PostIncExpr } +} + +/** + * A (pre or post) decrement expression. + * + * Examples: + * + * ``` + * --i + * i-- + * ``` + */ +class DecExpr extends UpdateExpr { + DecExpr() { this instanceof PreDecExpr or this instanceof PostDecExpr } +} + +/** + * An old-style `let` expression of the form `let(vardecls) expr`. + * + * Example: + * + * ``` + * let (x = f()) x*x + * ``` + */ +class LegacyLetExpr extends Expr, @legacy_letexpr { + /** Gets the `i`th declarator in this `let` expression. */ + VariableDeclarator getDecl(int i) { result = this.getChildExpr(i) and i >= 0 } + + /** Gets a declarator in this declaration expression. */ + VariableDeclarator getADecl() { result = this.getDecl(_) } + + /** Gets the expression this `let` expression scopes over. */ + Expr getBody() { result = this.getChildExpr(-1) } + + override string getAPrimaryQlClass() { result = "LegacyLetExpr" } +} + +/** + * An immediately invoked function expression (IIFE). + * + * Example: + * + * ``` + * (function() { return this; })() + * ``` + */ +class ImmediatelyInvokedFunctionExpr extends Function { + /** The invocation expression of this IIFE. */ + InvokeExpr invk; + /** + * The kind of invocation by which this IIFE is invoked: `"call"` + * for a direct function call, `"call"` or `"apply"` for a reflective + * invocation through `call` or `apply`, respectively. + */ + string kind; + + ImmediatelyInvokedFunctionExpr() { + // direct call + this = invk.getCallee().getUnderlyingValue() and kind = "direct" + or + // reflective call + exists(MethodCallExpr mce | mce = invk | + this = mce.getReceiver().getUnderlyingValue() and + kind = mce.getMethodName() and + (kind = "call" or kind = "apply") + ) + } + + /** Gets the invocation of this IIFE. */ + InvokeExpr getInvocation() { result = invk } + + /** + * Gets a string describing the way this IIFE is invoked + * (one of `"direct"`, `"call"` or `"apply"`). + */ + string getInvocationKind() { result = kind } + + /** + * Gets the `i`th argument of this IIFE. + */ + Expr getArgument(int i) { result = invk.getArgument(i) } + + /** + * Holds if the `i`th argument of this IIFE is a spread element. + */ + predicate isSpreadArgument(int i) { invk.isSpreadArgument(i) } + + /** + * Gets the offset of argument positions relative to parameter + * positions: for direct IIFEs the offset is zero, for IIFEs + * using `Function.prototype.call` the offset is one, and for + * IIFEs using `Function.prototype.apply` the offset is not defined. + */ + int getArgumentOffset() { + kind = "direct" and result = 0 + or + kind = "call" and result = 1 + } + + /** + * Holds if `p` is a parameter of this IIFE and `arg` is + * the corresponding argument. + * + * Note that rest parameters do not have corresponding arguments; + * conversely, arguments after a spread element do not have a corresponding + * parameter. + */ + predicate argumentPassing(Parameter p, Expr arg) { + exists(int parmIdx, int argIdx | + p = this.getParameter(parmIdx) and + not p.isRestParameter() and + argIdx = parmIdx + this.getArgumentOffset() and + arg = this.getArgument(argIdx) and + not this.isSpreadArgument([0 .. argIdx]) + ) + } +} + +/** + * An `await` expression. + * + * Example: + * + * ``` + * await p() + * ``` + */ +class AwaitExpr extends @await_expr, Expr { + /** Gets the operand of this `await` expression. */ + Expr getOperand() { result = this.getChildExpr(0) } + + overlay[global] + override predicate isImpure() { any() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getOperand().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "AwaitExpr" } +} + +/** + * A `function.sent` expression. + * + * Inside a generator function, `function.sent` evaluates to the value passed + * to the generator by the `next` method that most recently resumed execution + * of the generator. + * + * Example: + * + * ``` + * function.sent + * ``` + */ +class FunctionSentExpr extends @function_sent_expr, Expr { + overlay[global] + override predicate isImpure() { none() } + + override string getAPrimaryQlClass() { result = "FunctionSentExpr" } +} + +/** + * A decorator applied to a class, property or member definition. + * + * + * Example: + * + * ``` + * @A @testable(true) class C { // `@A` and `@testable(true)` are decorators + * @Test test1() { // `@Test` is a decorator + * } + * } + * ``` + */ +class Decorator extends @decorator, Expr { + /** + * Gets the element this decorator is applied to. + * + * For example, in the class declaration `@A class C { }`, + * the element decorator `@A` is applied to is `C`. + */ + Decoratable getElement() { this = result.getADecorator() } + + /** + * Gets the expression of this decorator. + * + * For example, the decorator `@A` has expression `A`, + * and `@testable(true)` has expression `testable(true)`. + */ + Expr getExpression() { result = this.getChildExpr(0) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getExpression().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "Decorator" } +} + +/** + * A program element to which decorators can be applied, + * that is, a class, a property or a member definition. + * + * Examples: + * + * ``` + * @A @testable(true) class C { // class `C` is decoratable + * @Test test1() { // method `test1` is decoratable + * } + * } + * ``` + */ +class Decoratable extends AstNode { + Decoratable() { + this instanceof ClassDefinition or + this instanceof Property or + this instanceof MemberDefinition or + this instanceof EnumDeclaration or + this instanceof Parameter + } + + /** + * Gets the `i`th decorator applied to this element. + */ + Decorator getDecorator(int i) { + result = this.(ClassDefinition).getDecorator(i) or + result = this.(Property).getDecorator(i) or + result = this.(MemberDefinition).getDecorator(i) or + result = this.(EnumDeclaration).getDecorator(i) or + result = this.(Parameter).getDecorator(i) + } + + /** + * Gets a decorator applied to this element, if any. + */ + Decorator getADecorator() { result = this.getDecorator(_) } +} + +/** + * A function-bind expression. + * + * Examples: + * + * ``` + * b::f + * ::b.f + * ``` + */ +class FunctionBindExpr extends @bind_expr, Expr { + /** + * Gets the object of this function bind expression; undefined for + * expressions of the form `::b.f`. + */ + Expr getObject() { result = this.getChildExpr(0) } + + /** Gets the callee of this function bind expression. */ + Expr getCallee() { result = this.getChildExpr(1) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getObject().getFirstControlFlowNode() + or + not exists(this.getObject()) and result = this.getCallee().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "FunctionBindExpr" } +} + +/** + * A dynamic import expression. + * + * Example: + * + * ``` + * import("fs") + * import("foo", { with: { type: "json" }}) + * ``` + */ +class DynamicImportExpr extends @dynamic_import, Expr { + /** Gets the expression specifying the path of the imported module. */ + Expr getSource() { result = this.getChildExpr(0) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getSource().getFirstControlFlowNode() + } + + Expr getImportedPathExpr() { result = this.getSource() } + + /** + * Gets the second "argument" to the import expression, that is, the `Y` in `import(X, Y)`. + * + * For example, gets the `{ with: { type: "json" }}` expression in the following: + * ```js + * import('foo', { with: { type: "json" }}) + * ``` + */ + Expr getImportOptions() { result = this.getChildExpr(1) } + + /** + * DEPRECATED: use `getImportOptions` instead. + * Gets the second "argument" to the import expression, that is, the `Y` in `import(X, Y)`. + * + * For example, gets the `{ with: { type: "json" }}` expression in the following: + * ```js + * import('foo', { with: { type: "json" }}) + * ``` + */ + deprecated Expr getImportAttributes() { result = this.getImportOptions() } + + override string getAPrimaryQlClass() { result = "DynamicImportExpr" } +} + +/** + * A call or member access that evaluates to `undefined` if its base operand evaluates to + * `undefined` or `null`. + * + * Examples: + * + * ``` + * x?.f + * x?.() + * ``` + */ +class OptionalUse extends Expr, @optionalchainable { + OptionalUse() { isOptionalChaining(this) } + + override string getAPrimaryQlClass() { result = "OptionalUse" } +} + +private class ChainElem extends Expr, @optionalchainable { + /** + * Gets the base operand of this chainable element. + */ + ChainElem getChainBase() { + result = this.(CallExpr).getCallee() or + result = this.(PropAccess).getBase() + } +} + +/** + * INTERNAL: This class should not be used by queries. + * + * The root in a chain of calls or property accesses, where at least one call or property access is optional. + */ +class OptionalChainRoot extends ChainElem { + OptionalUse optionalUse; + + OptionalChainRoot() { + this.getChainBase*() = optionalUse and + not exists(ChainElem other | this = other.getChainBase()) + } + + /** + * Gets an optional call or property access in the chain of this root. + */ + OptionalUse getAnOptionalUse() { result = optionalUse } +} + +/** + * An `import.meta` expression. + * + * Example: + * ```js + * let url = import.meta.url; + * ``` + */ +class ImportMetaExpr extends @import_meta_expr, Expr { + overlay[global] + override predicate isImpure() { none() } + + override string getAPrimaryQlClass() { result = "ImportMetaExpr" } +} + +/** + * A placeholder for some code generated by a templating engine, + * speculatively parsed as an expression. + * + * For example, the right-hand side of the following assignments will each be parsed + * as `GeneratedNodeExpr` nodes: + * ```js + * let data1 = {{ user_data1 }}; + * let data2 = {{{ user_data2 }}}; + * ``` + * + * Note that templating placeholders occurring inside strings literals are not parsed, + * and are simply seen as being part of the string literal. + * For example, following snippet does not contain any `GeneratedCodeExpr` nodes: + * ```js + * let data1 = "{{ user_data }}"; + * ``` + */ +class GeneratedCodeExpr extends @generated_code_expr, Expr { + override string getAPrimaryQlClass() { result = "GeneratedCodeExpr" } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/FileSystem.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/FileSystem.qll new file mode 100644 index 000000000000..afe6d9cc5466 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/FileSystem.qll @@ -0,0 +1,3 @@ +/** Provides classes for working with files and folders. */ + +import Files diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Files.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Files.qll new file mode 100644 index 000000000000..c6cb34e43c2d --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Files.qll @@ -0,0 +1,173 @@ +/** Provides classes for working with files and folders. */ +overlay[local?] +module; + +import minimal +private import NodeModuleResolutionImpl +private import codeql.util.FileSystem + +private module FsInput implements InputSig { + abstract class ContainerBase extends @container { + abstract string getAbsolutePath(); + + ContainerBase getParentContainer() { containerparent(result, this) } + + string toString() { result = this.getAbsolutePath() } + } + + class FolderBase extends ContainerBase, @folder { + override string getAbsolutePath() { folders(this, result) } + } + + class FileBase extends ContainerBase, @file { + override string getAbsolutePath() { files(this, result) } + } + + predicate hasSourceLocationPrefix = sourceLocationPrefix/1; +} + +private module Impl = Make; + +class Container = Impl::Container; + +module Folder = Impl::Folder; + +/** A folder. */ +class Folder extends Container, Impl::Folder { + /** Gets the file or subfolder in this folder that has the given `name`, if any. */ + overlay[global] + Container getChildContainer(string name) { + result = this.getAChildContainer() and + result.getBaseName() = name + } + + /** Gets the file in this folder that has the given `stem` and `extension`, if any. */ + overlay[global] + File getFile(string stem, string extension) { + result = this.getAChildContainer() and + result.getStem() = stem and + result.getExtension() = extension + } + + /** Like `getFile` except `d.ts` is treated as a single extension. */ + overlay[global] + private File getFileLongExtension(string stem, string extension) { + not (stem.matches("%.d") and extension = "ts") and + result = this.getFile(stem, extension) + or + extension = "d.ts" and + result = this.getFile(stem + ".d", "ts") + } + + /** + * Gets the file in this folder that has the given `stem` and any of the supported JavaScript extensions. + * + * If there are multiple such files, the one with the "best" extension is chosen based on a + * prioritized list of file extensions. + * + * `js` files are given less preference than files that compile to `js`, to ensure we pick the + * original source file rather than its compiled output. + * + * HTML files will not be found by this method. + */ + overlay[global] + File getJavaScriptFile(string stem) { + result = + min(int p, string ext | + p = getFileExtensionPriority(ext) + | + this.getFileLongExtension(stem, ext) order by p + ) + } + + /** + * Gets an implementation file and/or a typings file from this folder that has the given `stem`. + * This could be a single `.ts` file or a pair of `.js` and `.d.ts` files. + */ + overlay[global] + File getJavaScriptFileOrTypings(string stem) { + exists(File jsFile | jsFile = this.getJavaScriptFile(stem) | + result = jsFile + or + not jsFile.getFileType().isTypeScript() and + result = this.getFile(stem + ".d.ts") + ) + } + + /** Gets a subfolder contained in this folder. */ + overlay[global] + Folder getASubFolder() { result = this.getAChildContainer() } +} + +/** A file. */ +class File extends Container, Impl::File { + /** + * Gets the location of this file. + * + * Note that files have special locations starting and ending at line zero, column zero. + */ + Location getLocation() { hasLocation(this, result) } + + /** Gets the number of lines in this file. */ + int getNumberOfLines() { result = sum(int loc | numlines(this, loc, _, _) | loc) } + + /** Gets the number of lines containing code in this file. */ + int getNumberOfLinesOfCode() { result = sum(int loc | numlines(this, _, loc, _) | loc) } + + /** Gets the number of lines containing comments in this file. */ + int getNumberOfLinesOfComments() { result = sum(int loc | numlines(this, _, _, loc) | loc) } + + /** Gets a toplevel piece of JavaScript code in this file. */ + TopLevel getATopLevel() { result.getFile() = this } + + /** + * Holds if line number `lineno` of this file is indented to depth `d` + * using character `c`. + * + * This predicate only holds for lines that belong to JavaScript code that + * start with one or more occurrences of the same whitespace character, + * followed by at least one non-whitespace character. + * + * It does not hold for lines that do not start with a whitespace character, + * or for lines starting with a string of different whitespace characters + * (for instance, a mix of tabs and spaces). + */ + predicate hasIndentation(int lineno, string c, int d) { indentation(this, lineno, c, d) } + + /** + * Gets the type of this file. + */ + FileType getFileType() { filetype(this, result) } +} + +/** + * A file type. + */ +class FileType extends string { + FileType() { this = ["javascript", "html", "typescript", "json", "yaml"] } + + /** + * Holds if this is the JavaScript file type. + */ + predicate isJavaScript() { this = "javascript" } + + /** + * Holds if this is the HTML file type. + */ + predicate isHtml() { this = "html" } + + /** + * Holds if this is the TypeScript file type. + */ + predicate isTypeScript() { this = "typescript" } + + /** + * Holds if this is the JSON file type. + */ + predicate isJson() { this = "json" } + + /** + * Holds if this is the YAML file type. + */ + predicate isYaml() { this = "yaml" } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Functions.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Functions.qll new file mode 100644 index 000000000000..5f0201787078 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Functions.qll @@ -0,0 +1,585 @@ +/** Provides classes for working with functions. */ +overlay[local?] +module; + +import minimal +private import Util + +/** + * 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(Property p | + this = p.getInit() and + result = p.getName() + ) + or + exists(AssignExpr assign | this = assign.getRhs().getUnderlyingValue() | + result = assign.getTarget().(PropAccess).getPropertyName() + or + result = assign.getTarget().(VarRef).getName() + ) + 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 result = "function " + getNameFromContext(this) + } + + /** + * 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" + } +} + +/** + * 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()) } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSDoc.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSDoc.qll new file mode 100644 index 000000000000..04905bd87776 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSDoc.qll @@ -0,0 +1,623 @@ +/** Provides classes for working with JSDoc comments. */ +overlay[local?] +module; + +import minimal + +/** + * A JSDoc comment. + * + * Example: + * + *
+ * /**
+ *  * An example JSDoc comment documenting a constructor function.
+ *  *
+ *  * @constructor
+ *  * @param {Object=} options An object literal with options.
+ *  */
+ * 
+ */ +class JSDoc extends @jsdoc, Locatable { + /** Gets the description text of this JSDoc comment. */ + string getDescription() { jsdoc(this, result, _) } + + /** Gets the raw comment this JSDoc comment is an interpretation of. */ + Comment getComment() { jsdoc(this, _, result) } + + /** Gets a JSDoc tag within this JSDoc comment. */ + JSDocTag getATag() { result.getParent() = this } + + /** Gets a JSDoc tag within this JSDoc comment with the given title. */ + JSDocTag getATagByTitle(string title) { + result = this.getATag() and + result.getTitle() = title + } + + /** Gets the element to which this JSDoc comment is attached */ + Documentable getDocumentedElement() { result.getDocumentation() = this } + + override string toString() { result = this.getComment().toString() } +} + +/** + * A program element that can have a JSDoc comment. + * + * Example: + * + *
+ * /**
+ *  * An example JSDoc comment documenting a constructor function.
+ *  *
+ *  * @constructor
+ *  * @param {Object=} options An object literal with options.
+ *  */
+ * function MyConstructor(options) {  // documentable
+ *   this.options = options || {};
+ * }
+ * 
+ */ +abstract class Documentable extends AstNode { + /** Gets the JSDoc comment for this element, if any. */ + cached + JSDoc getDocumentation() { result.getComment().getNextToken() = this.getFirstToken() } +} + +/** + * A syntactic element that a JSDoc type expression may be nested in, that is, + * either a JSDoc tag or another JSDoc type expression. + * + * Examples: + * + * ``` + * // the `@param` tag and the `...=` type expressions are JSDoc type expression parents + * @param {Object=} options An object literal with options. + * ``` + */ +class JSDocTypeExprParent extends @jsdoc_type_expr_parent, Locatable { + /** Gets the JSDoc comment to which this element belongs. */ + JSDoc getJSDocComment() { none() } +} + +/** + * A JSDoc tag. + * + * Examples: + * + * ``` + * @param {Object=} options An object literal with options. + * @return {!Server} + * ``` + */ +class JSDocTag extends @jsdoc_tag, JSDocTypeExprParent { + /** Gets the tag title; for instance, the title of a `@param` tag is `"param"`. */ + string getTitle() { jsdoc_tags(this, result, _, _, _) } + + /** Gets the JSDoc comment this tag belongs to. */ + JSDoc getParent() { jsdoc_tags(this, _, result, _, _) } + + /** Gets the index of this tag within its parent comment. */ + int getIndex() { jsdoc_tags(this, _, _, result, _) } + + override string toString() { jsdoc_tags(this, _, _, _, result) } + + /** Gets the description associated with the tag. */ + string getDescription() { jsdoc_tag_descriptions(this, result) } + + /** + * Gets the (optional) name associated with the tag, such as the name of the documented parameter + * for a `@param` tag. + */ + string getName() { jsdoc_tag_names(this, result) } + + /** + * Gets the (optional) type associated with the tag, such as the type of the documented parameter + * for a `@param` tag. + */ + JSDocTypeExpr getType() { result.getParent() = this } + + /** Holds if this tag documents a simple name (as opposed to a name path). */ + predicate documentsSimpleName() { + exists(string name | name = this.getName() | not name.matches("%.%")) + } + + /** Gets the toplevel in which this tag appears. */ + TopLevel getTopLevel() { result = this.getParent().getComment().getTopLevel() } + + override JSDoc getJSDocComment() { result.getATag() = this } +} + +/** + * A `@param` tag. + * + * Example: + * + * ``` + * @param {Object=} options An object literal with options. + * ``` + */ +class JSDocParamTag extends JSDocTag { + JSDocParamTag() { this.getTitle().regexpMatch("param|arg(ument)?") } + + /** Gets the parameter this tag refers to, if it can be determined. */ + Variable getDocumentedParameter() { + exists(Parameterized parm | parm.getDocumentation() = this.getParent() | + result = pragma[only_bind_out](parm).getParameterVariable(this.getName()) + ) + } +} + +/** + * A JSDoc type expression. + * + * Examples: + * + * ``` + * * + * Array + * !Object + * ``` + */ +class JSDocTypeExpr extends @jsdoc_type_expr, JSDocTypeExprParent, TypeAnnotation { + /** + * Gets the syntactic element in which this type expression is nested, which may either + * be another type expression or a JSDoc tag. + */ + JSDocTypeExprParent getParent() { jsdoc_type_exprs(this, _, result, _, _) } + + /** + * Gets the index of this type expression within its parent. + */ + int getIndex() { jsdoc_type_exprs(this, _, _, result, _) } + + /** + * Gets the `i`th child type expression of this type expression. + * + * _Note_: the indices of child type expressions in their parent elements are an implementation + * detail that may change between versions of the extractor. + */ + JSDocTypeExpr getChild(int i) { jsdoc_type_exprs(result, _, this, i, _) } + + override string toString() { jsdoc_type_exprs(this, _, _, _, result) } + + override JSDoc getJSDocComment() { result = this.getParent().getJSDocComment() } + + override Stmt getEnclosingStmt() { + exists(Documentable astNode | astNode.getDocumentation() = this.getJSDocComment() | + result = astNode + or + result = astNode.(ExprOrType).getEnclosingStmt() + or + result = astNode.(Property).getObjectExpr().getEnclosingStmt() + ) + } + + override Function getEnclosingFunction() { result = this.getContainer() } + + override TopLevel getTopLevel() { result = this.getEnclosingStmt().getTopLevel() } +} + +/** + * An `any` type expression. + * + * Example: + * + * ``` + * * + * ``` + */ +class JSDocAnyTypeExpr extends @jsdoc_any_type_expr, JSDocTypeExpr { + override predicate isAny() { any() } +} + +/** + * A null type expression. + * + * Example: + * + * ``` + * null + * ``` + */ +class JSDocNullTypeExpr extends @jsdoc_null_type_expr, JSDocTypeExpr { + override predicate isNull() { any() } +} + +/** + * A type expression representing the type of `undefined`. + * + * Example: + * + * ``` + * undefined + * ``` + */ +class JSDocUndefinedTypeExpr extends @jsdoc_undefined_type_expr, JSDocTypeExpr { + override predicate isUndefined() { any() } +} + +/** + * A type expression representing an unknown type. + * + * Example: + * + * ``` + * ? + * ``` + */ +class JSDocUnknownTypeExpr extends @jsdoc_unknown_type_expr, JSDocTypeExpr { + override predicate isUnknownKeyword() { any() } +} + +/** + * A type expression representing the void type. + * + * Example: + * + * ``` + * void + * ``` + */ +class JSDocVoidTypeExpr extends @jsdoc_void_type_expr, JSDocTypeExpr { + override predicate isVoid() { any() } +} + +/** + * An identifier in a JSDoc type expression, such as `Object` or `string`. + * + * Note that qualified names consist of multiple identifier nodes. + */ +class JSDocIdentifierTypeExpr extends @jsdoc_identifier_type_expr, JSDocTypeExpr { + /** + * Gets the name of the identifier. + */ + string getName() { result = this.toString() } + + override predicate isString() { this.getName() = "string" } + + override predicate isStringy() { + exists(string name | name = this.getName() | + name = "string" or + name = "String" + ) + } + + override predicate isNumber() { this.getName() = "number" } + + override predicate isNumbery() { + exists(string name | name = this.getName() | + name = ["number", "Number", "double", "Double", "int", "integer", "Integer"] + ) + } + + override predicate isBoolean() { this.getName() = "boolean" } + + override predicate isBooleany() { + this.getName() = "boolean" or + this.getName() = "Boolean" or + this.getName() = "bool" + } + + override predicate isRawFunction() { this.getName() = "Function" } +} + +private AstNode getAncestorInScope(Documentable doc) { + any(JSDocLocalTypeAccess t).getJSDocComment() = doc.getDocumentation() and // restrict to cases where we need this + result = doc.getParent() + or + exists(AstNode mid | + mid = getAncestorInScope(doc) and + not mid = any(Scope s).getScopeElement() and + result = mid.getParent() + ) +} + +private Scope getScope(Documentable doc) { result.getScopeElement() = getAncestorInScope(doc) } + +pragma[nomagic] +private predicate shouldResolveName(TopLevel top, string name) { + exists(JSDocLocalTypeAccess access | + access.getName() = name and + access.getTopLevel() = top + ) +} + +private LexicalName getOwnLocal(Scope scope, string name, DeclarationSpace space) { + scope = result.getScope() and + name = result.getName() and + space = result.getDeclarationSpace() and + shouldResolveName(scope.getScopeElement().getTopLevel(), name) // restrict size of predicate +} + +private LexicalName resolveLocal(Scope scope, string name, DeclarationSpace space) { + result = getOwnLocal(scope, name, space) + or + result = resolveLocal(scope.getOuterScope(), name, space) and + not exists(getOwnLocal(scope, name, space)) +} + +/** + * An unqualified identifier in a JSDoc type expression. + * + * Example: + * + * ``` + * string + * Object + * ``` + */ +class JSDocLocalTypeAccess extends JSDocIdentifierTypeExpr { + JSDocLocalTypeAccess() { not this = any(JSDocQualifiedTypeAccess a).getNameNode() } + + /** Gets a variable, type-name, or namespace that this expression may resolve to. */ + LexicalName getALexicalName() { + result = + resolveLocal(getScope(this.getJSDocComment().getDocumentedElement()), this.getName(), _) + } +} + +/** + * A qualified type name in a JSDoc type expression, such as `X.Y`. + */ +class JSDocQualifiedTypeAccess extends @jsdoc_qualified_type_expr, JSDocTypeExpr { + /** + * Gets the base of this access, such as the `X` in `X.Y`. + */ + JSDocTypeExpr getBase() { result = this.getChild(0) } + + /** + * Gets the node naming the member being accessed, such as the `Y` node in `X.Y`. + */ + JSDocIdentifierTypeExpr getNameNode() { result = this.getChild(1) } + + /** + * Gets the name being accessed, such as `Y` in `X.Y`. + */ + string getName() { result = this.getNameNode().getName() } +} + +/** + * A type expression referring to a named type. + * + * Example: + * + * ``` + * string + * Object + * Namespace.Type + * ``` + */ +class JSDocNamedTypeExpr extends JSDocTypeExpr { + JSDocNamedTypeExpr() { + this instanceof JSDocLocalTypeAccess + or + this instanceof JSDocQualifiedTypeAccess + } + + /** + * Gets the name directly as it appears in this type, including any qualifiers. + * + * For example, for `X.Y` this gets the string `"X.Y"`. + */ + string getRawName() { result = this.toString() } + + /** + * DEPRECATED. Use `getRawName()` instead. + */ + deprecated string getName() { result = this.toString() } + + /** + * Holds if this name consists of the unqualified name `prefix` + * followed by a (possibly empty) `suffix`. + * + * For example: + * - `foo.bar.Baz` has prefix `foo` and suffix `.bar.Baz`. + * - `Baz` has prefix `Baz` and an empty suffix. + */ + deprecated predicate hasNameParts(string prefix, string suffix) { + not this = any(JSDocQualifiedTypeAccess a).getBase() and // restrict size of predicate + exists(string regex, string name | regex = "([^.]+)(.*)" | + name = this.getRawName() and + prefix = name.regexpCapture(regex, 1) and + suffix = name.regexpCapture(regex, 2) + ) + } +} + +/** + * An applied type expression. + * + * Example: + * + * ``` + * Array + * ``` + */ +class JSDocAppliedTypeExpr extends @jsdoc_applied_type_expr, JSDocTypeExpr { + /** Gets the head type expression, such as `Array` in `Array`. */ + JSDocTypeExpr getHead() { result = this.getChild(-1) } + + /** + * Gets the `i`th argument type of the applied type expression. + * + * For example, in `Array`, `string` is the 0th argument type. + */ + JSDocTypeExpr getArgument(int i) { i >= 0 and result = this.getChild(i) } + + /** + * Gets an argument type of the applied type expression. + * + * For example, in `Array`, `string` is the only argument type. + */ + JSDocTypeExpr getAnArgument() { result = this.getArgument(_) } +} + +/** + * A nullable type expression. + * + * Example: + * + * ``` + * ?Array + * ``` + */ +class JSDocNullableTypeExpr extends @jsdoc_nullable_type_expr, JSDocTypeExpr { + /** Gets the argument type expression. */ + JSDocTypeExpr getTypeExpr() { result = this.getChild(0) } + + /** Holds if the `?` operator of this type expression is written in prefix notation. */ + predicate isPrefix() { jsdoc_prefix_qualifier(this) } + + override JSDocTypeExpr getAnUnderlyingType() { result = this.getTypeExpr().getAnUnderlyingType() } +} + +/** + * A non-nullable type expression. + * + * Example: + * + * ``` + * !Array + * ``` + */ +class JSDocNonNullableTypeExpr extends @jsdoc_non_nullable_type_expr, JSDocTypeExpr { + /** Gets the argument type expression. */ + JSDocTypeExpr getTypeExpr() { result = this.getChild(0) } + + /** Holds if the `!` operator of this type expression is written in prefix notation. */ + predicate isPrefix() { jsdoc_prefix_qualifier(this) } + + override JSDocTypeExpr getAnUnderlyingType() { result = this.getTypeExpr().getAnUnderlyingType() } +} + +/** + * A record type expression. + * + * Example: + * + * ``` + * { x: number, y: string } + * ``` + */ +class JSDocRecordTypeExpr extends @jsdoc_record_type_expr, JSDocTypeExpr { + /** Gets the name of the `i`th field of the record type. */ + string getFieldName(int i) { jsdoc_record_field_name(this, i, result) } + + /** Gets the name of some field of the record type. */ + string getAFieldName() { result = this.getFieldName(_) } + + /** Gets the type of the `i`th field of the record type. */ + JSDocTypeExpr getFieldType(int i) { result = this.getChild(i) } + + /** Gets the type of the field with the given name. */ + JSDocTypeExpr getFieldTypeByName(string fieldname) { + exists(int idx | fieldname = this.getFieldName(idx) and result = this.getFieldType(idx)) + } +} + +/** + * An array type expression. + * + * Example: + * + * ``` + * [string] + * ``` + */ +class JSDocArrayTypeExpr extends @jsdoc_array_type_expr, JSDocTypeExpr { + /** Gets the type of the `i`th element of this array type. */ + JSDocTypeExpr getElementType(int i) { result = this.getChild(i) } + + /** Gets an element type of this array type. */ + JSDocTypeExpr getAnElementType() { result = this.getElementType(_) } +} + +/** + * A union type expression. + * + * Example: + * + * ``` + * number|string + * ``` + */ +class JSDocUnionTypeExpr extends @jsdoc_union_type_expr, JSDocTypeExpr { + /** Gets one of the type alternatives of this union type. */ + JSDocTypeExpr getAnAlternative() { result = this.getChild(_) } + + override JSDocTypeExpr getAnUnderlyingType() { + result = this.getAnAlternative().getAnUnderlyingType() + } +} + +/** + * A function type expression. + * + * Example: + * + * ``` + * function(string): number + * ``` + */ +class JSDocFunctionTypeExpr extends @jsdoc_function_type_expr, JSDocTypeExpr { + /** Gets the result type of this function type. */ + JSDocTypeExpr getResultType() { result = this.getChild(-1) } + + /** Gets the receiver type of this function type. */ + JSDocTypeExpr getReceiverType() { result = this.getChild(-2) } + + /** Gets the `i`th parameter type of this function type. */ + JSDocTypeExpr getParameterType(int i) { i >= 0 and result = this.getChild(i) } + + /** Gets a parameter type of this function type. */ + JSDocTypeExpr getAParameterType() { result = this.getParameterType(_) } + + /** Holds if this function type is a constructor type. */ + predicate isConstructorType() { jsdoc_has_new_parameter(this) } +} + +/** + * An optional parameter type. + * + * Example: + * + * ``` + * number= + * ``` + */ +class JSDocOptionalParameterTypeExpr extends @jsdoc_optional_type_expr, JSDocTypeExpr { + /** Gets the underlying type of this optional type. */ + JSDocTypeExpr getUnderlyingType() { result = this.getChild(0) } + + override JSDocTypeExpr getAnUnderlyingType() { + result = this.getUnderlyingType().getAnUnderlyingType() + } +} + +/** + * A rest parameter type. + * + * Example: + * + * ``` + * string... + * ``` + */ +class JSDocRestParameterTypeExpr extends @jsdoc_rest_type_expr, JSDocTypeExpr { + /** Gets the underlying type of this rest parameter type. */ + JSDocTypeExpr getUnderlyingType() { result = this.getChild(0) } +} + +/** + * An error encountered while parsing a JSDoc comment. + */ +class JSDocError extends @jsdoc_error { + /** Gets the tag that triggered the error. */ + JSDocTag getTag() { jsdoc_errors(this, result, _, _) } + + /** Gets the message associated with the error. */ + string getMessage() { jsdoc_errors(this, _, result, _) } + + /** Gets a textual representation of this element. */ + string toString() { jsdoc_errors(this, _, _, result) } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSON.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSON.qll new file mode 100644 index 000000000000..934445da5258 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSON.qll @@ -0,0 +1,164 @@ +/** + * Provides classes for working with JSON data. + */ +overlay[local?] +module; + +import minimal + +/** + * A JSON-encoded value, which may be a primitive value, an array or an object. + * + * Examples: + * + * ``` + * null + * true + * false + * 42 + * "a string" + * [ 1, 2, 3 ] + * { "value": 0 } + * ``` + */ +class JsonValue extends @json_value, Locatable { + /** Gets the parent value to which this value belongs, if any. */ + JsonValue getParent() { json(this, _, result, _, _) } + + /** Gets the `i`th child value of this value. */ + JsonValue getChild(int i) { json(result, _, this, i, _) } + + /** Holds if this JSON value is the top level element in its enclosing file. */ + predicate isTopLevel() { not exists(this.getParent()) } + + override string toString() { json(this, _, _, _, result) } + + /** Gets the JSON file containing this value. */ + File getJsonFile() { exists(Location loc | json_locations(this, loc) and result = loc.getFile()) } + + /** If this is an object, gets the value of property `name`. */ + JsonValue getPropValue(string name) { json_properties(this, name, result) } + + /** If this is an array, gets the value of the `i`th element. */ + JsonValue getElementValue(int i) { result = this.(JsonArray).getChild(i) } + + /** If this is a string constant, gets the value of the string. */ + string getStringValue() { result = this.(JsonString).getValue() } + + /** If this is an integer constant, gets its numeric value. */ + int getIntValue() { result = this.(JsonNumber).getValue().toInt() } + + /** If this is a boolean constant, gets its boolean value. */ + boolean getBooleanValue() { + result.toString() = this.(JsonBoolean).getValue() and result = [true, false] + } + + override string getAPrimaryQlClass() { result = "JsonValue" } +} + +/** + * A JSON-encoded primitive value. + * + * Examples: + * + * ``` + * null + * true + * false + * 42 + * "a string" + * ``` + */ +abstract class JsonPrimitiveValue extends JsonValue { + /** Gets a string representation of the encoded value. */ + string getValue() { json_literals(result, _, this) } + + /** Gets the source text of the encoded value; for strings, this includes quotes. */ + string getRawValue() { json_literals(_, result, this) } +} + +/** + * A JSON-encoded null value. + * + * Example: + * + * ``` + * null + * ``` + */ +class JsonNull extends @json_null, JsonPrimitiveValue { + override string getAPrimaryQlClass() { result = "JsonNull" } +} + +/** + * A JSON-encoded Boolean value. + * + * Examples: + * + * ``` + * true + * false + * ``` + */ +class JsonBoolean extends @json_boolean, JsonPrimitiveValue { + override string getAPrimaryQlClass() { result = "JsonBoolean" } +} + +/** + * A JSON-encoded number. + * + * Examples: + * + * ``` + * 42 + * 1.0 + * ``` + */ +class JsonNumber extends @json_number, JsonPrimitiveValue { + override string getAPrimaryQlClass() { result = "JsonNumber" } +} + +/** + * A JSON-encoded string value. + * + * Example: + * + * ``` + * "a string" + * ``` + */ +class JsonString extends @json_string, JsonPrimitiveValue { + override string getAPrimaryQlClass() { result = "JsonString" } +} + +/** + * A JSON-encoded array. + * + * Example: + * + * ``` + * [ 1, 2, 3 ] + * ``` + */ +class JsonArray extends @json_array, JsonValue { + override string getAPrimaryQlClass() { result = "JsonArray" } + + /** Gets the string value of the `i`th element of this array. */ + string getElementStringValue(int i) { result = this.getElementValue(i).getStringValue() } +} + +/** + * A JSON-encoded object. + * + * Example: + * + * ``` + * { "value": 0 } + * ``` + */ +class JsonObject extends @json_object, JsonValue { + override string getAPrimaryQlClass() { result = "JsonObject" } + + /** Gets the string value of property `name` of this object. */ + string getPropStringValue(string name) { result = this.getPropValue(name).getStringValue() } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSPaths.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSPaths.qll new file mode 100644 index 000000000000..c297892bbdfd --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSPaths.qll @@ -0,0 +1,70 @@ +/** + * Provides predicates for use in `Folder::ResolveSig` in order to resolve + * paths using JavaScript semantics. + */ + +private import minimal +private import TSConfig + +/** + * Gets a folder name that is a common source folder name. + */ +string getASrcFolderName() { result = ["ts", "js", "src", "lib"] } + +/** + * Gets a folder name that is a common build output folder name. + */ +string getABuildOutputFolderName() { result = ["dist", "build", "out", "lib"] } + +/** + * Provides predicates for use in a `Folder::ResolveSig` in order to resolve + * paths using JavaScript semantics. + * + * This accounts for two things: + * - automatic file extensions (e.g `./foo` may resolve to `./foo.js`) + * - mapping compiled-generated files back to their original source files + */ +module JSPaths { + private Container getAnAdditionalChildFromBuildMapping(Container base, string name) { + // When importing a .js file, map to the original file that compiles to the .js file. + exists(string stem | + result = base.(Folder).getJavaScriptFileOrTypings(stem) and + name = stem + ".js" + ) + or + // Redirect './bar' to 'foo' given a tsconfig like: + // + // { include: ["foo"], compilerOptions: { outDir: "./bar" }} + // + exists(TSConfig tsconfig | + name = + tsconfig.getCompilerOptions().getPropStringValue("outDir").regexpReplaceAll("^\\./", "") and + base = tsconfig.getFolder() and + result = tsconfig.getEffectiveRootDir() + ) + } + + /** + * Gets an additional child of `base` to include when resolving JS paths. + */ + pragma[nomagic] + Container getAnAdditionalChild(Container base, string name) { + // Automatically fill in file extensions + result = base.(Folder).getJavaScriptFileOrTypings(name) + or + result = getAnAdditionalChildFromBuildMapping(base, name) + or + // Heuristic version of the above based on commonly used source and build folder names + not exists(getAnAdditionalChildFromBuildMapping(base, name)) and + exists(Folder folder | base = folder | + folder = any(PackageJson pkg).getFolder() and + name = getABuildOutputFolderName() and + not exists(folder.getJavaScriptFileOrTypings(name)) and + ( + result = folder.getChildContainer(getASrcFolderName()) + or + result = folder + ) + ) + } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSX.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSX.qll new file mode 100644 index 000000000000..9a30fbbab472 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/JSX.qll @@ -0,0 +1,258 @@ +/** + * Provides classes for working with JSX code. + */ +overlay[local?] +module; + +import minimal + +/** + * A JSX element or fragment. + * + * Examples: + * + * ``` + * {linkText()} + * + * <>

Title

Some text + * ``` + */ +class JsxNode extends Expr, @jsx_element { + /** Gets the `i`th element in the body of this element or fragment. */ + Expr getBodyElement(int i) { i >= 0 and result = this.getChildExpr(-i - 2) } + + /** Gets an element in the body of this element or fragment. */ + Expr getABodyElement() { result = this.getBodyElement(_) } + + /** + * Gets the parent JSX element or fragment of this element. + */ + JsxNode getJsxParent() { this = result.getABodyElement() } + + override string getAPrimaryQlClass() { result = "JsxNode" } +} + +/** + * A JSX element. + * + * Examples: + * + * ``` + * {linkText()} + * + * ``` + */ +class JsxElement extends JsxNode { + JsxName name; + + JsxElement() { name = this.getChildExpr(-1) } + + /** Gets the expression denoting the name of this element. */ + JsxName getNameExpr() { result = name } + + /** Gets the name of this element. */ + string getName() { result = name.getValue() } + + /** Gets the `i`th attribute of this element. */ + JsxAttribute getAttribute(int i) { properties(result, this, i, _, _) } + + /** Gets an attribute of this element. */ + JsxAttribute getAnAttribute() { result = this.getAttribute(_) } + + /** Gets the attribute of this element with the given name, if any. */ + JsxAttribute getAttributeByName(string n) { + result = this.getAnAttribute() and result.getName() = n + } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getNameExpr().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "JsxElement" } + + /** + * Holds if this JSX element is an HTML element. + * That is, the name starts with a lowercase letter. + */ + predicate isHtmlElement() { this.getName().regexpMatch("[a-z].*") } +} + +/** + * A JSX fragment. + * + * Example: + * + * ``` + * <>

Title

Some text + * ``` + */ +class JsxFragment extends JsxNode { + JsxFragment() { not exists(this.getChildExpr(-1)) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getBodyElement(0).getFirstControlFlowNode() + or + not exists(this.getABodyElement()) and result = this + } + + override string getAPrimaryQlClass() { result = "JsxFragment" } +} + +/** + * An attribute of a JSX element, including spread attributes. + * + * Examples: + * + * ``` + * link // `href={linkTarget()}` is an attribute + * // `name={user.name}` is an attribute + *
// `{...attrs}` is a (spread) attribute + * ``` + */ +class JsxAttribute extends AstNode, @jsx_attribute { + /** + * Gets the expression denoting the name of this attribute. + * + * This is not defined for spread attributes. + */ + JsxName getNameExpr() { result = this.getChildExpr(0) } + + /** + * Gets the name of this attribute. + * + * This is not defined for spread attributes. + */ + string getName() { result = this.getNameExpr().getValue() } + + /** Gets the expression denoting the value of this attribute. */ + Expr getValue() { result = this.getChildExpr(1) } + + /** Gets the value of this attribute as a constant string, if possible. */ + string getStringValue() { result = this.getValue().getStringValue() } + + /** Gets the JSX element to which this attribute belongs. */ + JsxElement getElement() { this = result.getAnAttribute() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getNameExpr().getFirstControlFlowNode() + or + not exists(this.getNameExpr()) and result = this.getValue().getFirstControlFlowNode() + } + + override string toString() { properties(this, _, _, _, result) } + + override string getAPrimaryQlClass() { result = "JsxAttribute" } +} + +/** + * A spread attribute of a JSX element. + * + * Example: + * + * ``` + *
// `{...attrs}` is a spread attribute + * ``` + */ +class JsxSpreadAttribute extends JsxAttribute { + JsxSpreadAttribute() { not exists(this.getNameExpr()) } + + override SpreadElement getValue() { + // override for more precise result type + result = super.getValue() + } +} + +/** + * A namespace-qualified name such as `n:a`. + * + * Example: + * + * ``` + * html:href + * ``` + */ +class JsxQualifiedName extends Expr, @jsx_qualified_name { + /** Gets the namespace component of this qualified name. */ + Identifier getNamespace() { result = this.getChildExpr(0) } + + /** Gets the name component of this qualified name. */ + Identifier getName() { result = this.getChildExpr(1) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getNamespace().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "JsxQualifiedName" } +} + +/** + * A name of an JSX element or attribute (which is + * always an identifier, a dot expression, or a qualified + * namespace name). + * + * Examples: + * + * ``` + * href + * html:href + * data.path + * ``` + */ +class JsxName extends Expr { + JsxName() { + this instanceof Identifier or + this instanceof ThisExpr or + this.(DotExpr).getBase() instanceof JsxName or + this instanceof JsxQualifiedName + } + + /** + * Gets the string value of this name. + */ + string getValue() { + result = this.(Identifier).getName() + or + exists(DotExpr dot | dot = this | + result = dot.getBase().(JsxName).getValue() + "." + dot.getPropertyName() + ) + or + exists(JsxQualifiedName qual | qual = this | + result = qual.getNamespace().getName() + ":" + qual.getName().getName() + ) + or + this instanceof ThisExpr and + result = "this" + } +} + +/** + * An interpolating expression that interpolates nothing. + * + * Example: + * + *
+ * { /* TBD */ }
+ * 
+ */ +class JsxEmptyExpr extends Expr, @jsx_empty_expr { + override string getAPrimaryQlClass() { result = "JsxEmptyExpr" } +} + +/** + * A legacy `@jsx` pragma. + * + * Example: + * + * ``` + * @jsx React.DOM + * ``` + */ +class JsxPragma extends JSDocTag { + JsxPragma() { this.getTitle() = "jsx" } + + /** + * Gets the DOM name specified by the pragma; for `@jsx React.DOM`, + * the result is `React.DOM`. + */ + string getDomName() { result = this.getDescription().trim() } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Lines.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Lines.qll new file mode 100644 index 000000000000..bfefc5a03a77 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Lines.qll @@ -0,0 +1,52 @@ +/** + * Provides classes for working with lines of text in source files. + * + * This information is only available for snapshots that have been extracted with + * the `--extract-program-text` flag. + */ +overlay[local?] +module; + +import minimal + +/** + * A line of text (code, comment, or whitespace) in a source file. + * + * Note that textual information is only available for snapshots that have been + * extracted with the `--extract-program-text` flag. + */ +class Line extends @line, Locatable { + /** Gets the toplevel element this line belongs to. */ + TopLevel getTopLevel() { lines(this, result, _, _) } + + /** Gets the text of this line, excluding the terminator character(s). */ + string getText() { lines(this, _, result, _) } + + /** + * Gets the terminator character(s) of this line. + * + * This predicate may return: + * + * - the empty string if this line is the last line in a file + * and there is no line terminator after it; + * - a single-character string containing the character `\n` (newline), + * `\r` (carriage return), `\u2028` (Unicode character LINE SEPARATOR) + * or `\u2029` (Unicode character PARAGRAPH SEPARATOR); + * - the two-character string `\r\n` (carriage return followed by newline). + */ + string getTerminator() { lines(this, _, _, result) } + + /** + * Gets the indentation character used by this line. + * + * The indentation character of a line is defined to be the whitespace character + * `c` such that the line starts with one or more instances of `c`, followed by a + * non-whitespace character. + * + * If the line does not start with a whitespace character, or with a mixture of + * different whitespace characters, its indentation character is undefined. + */ + string getIndentChar() { result = this.getText().regexpCapture("(\\s)\\1*\\S.*", 1) } + + override string toString() { result = this.getText() } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Locations.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Locations.qll new file mode 100644 index 000000000000..6ae4e3010e68 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Locations.qll @@ -0,0 +1,160 @@ +/** Provides classes for working with locations and program elements that have locations. */ +overlay[local?] +module; + +import minimal + +/** + * A location as given by a file, a start line, a start column, + * an end line, and an end column. + * + * This class is restricted to locations created by the extractor. + * + * For more information about locations see [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/). + */ +final class Location extends @location_default { + /** Gets the file for this location. */ + File getFile() { locations_default(this, result, _, _, _, _) } + + /** Gets the 1-based line number (inclusive) where this location starts. */ + int getStartLine() { locations_default(this, _, result, _, _, _) } + + /** Gets the 1-based column number (inclusive) where this location starts. */ + int getStartColumn() { locations_default(this, _, _, result, _, _) } + + /** Gets the 1-based line number (inclusive) where this location ends. */ + int getEndLine() { locations_default(this, _, _, _, result, _) } + + /** Gets the 1-based column number (inclusive) where this location ends. */ + int getEndColumn() { locations_default(this, _, _, _, _, result) } + + /** Gets the number of lines covered by this location. */ + int getNumLines() { result = this.getEndLine() - this.getStartLine() + 1 } + + /** Holds if this location starts before location `that`. */ + overlay[caller?] + pragma[inline] + predicate startsBefore(Location that) { + exists(string f, int sl1, int sc1, int sl2, int sc2 | + this.hasLocationInfo(f, sl1, sc1, _, _) and + that.hasLocationInfo(f, sl2, sc2, _, _) + | + sl1 < sl2 + or + sl1 = sl2 and sc1 < sc2 + ) + } + + /** Holds if this location ends after location `that`. */ + overlay[caller?] + pragma[inline] + predicate endsAfter(Location that) { + exists(string f, int el1, int ec1, int el2, int ec2 | + this.hasLocationInfo(f, _, _, el1, ec1) and + that.hasLocationInfo(f, _, _, el2, ec2) + | + el1 > el2 + or + el1 = el2 and ec1 > ec2 + ) + } + + /** + * Holds if this location contains location `that`, meaning that it starts + * before and ends after it. + */ + predicate contains(Location that) { this.startsBefore(that) and this.endsAfter(that) } + + /** Holds if this location is empty. */ + predicate isEmpty() { exists(int l, int c | this.hasLocationInfo(_, l, c, l, c - 1)) } + + /** Gets a textual representation of this element. */ + string toString() { result = this.getFile().getBaseName() + ":" + this.getStartLine().toString() } + + /** + * 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 + ) { + exists(File f | + locations_default(this, f, startline, startcolumn, endline, endcolumn) and + filepath = f.getAbsolutePath() + ) + } +} + +cached +private Location getLocatableLocation(@locatable l) { + hasLocation(l, result) or + xmllocations(l, result) or + json_locations(l, result) or + yaml_locations(l, result) +} + +/** A program element with a location. */ +class Locatable extends @locatable { + /** Gets the file this program element comes from. */ + File getFile() { result = this.getLocation().getFile() } + + /** Gets this element's location. */ + final Location getLocation() { result = getLocatableLocation(this) } + + /** + * Gets the line on which this element starts. + * + * This predicate is only defined for program elements from snapshots that + * have been extracted with the `--extract-program-text` flag. + */ + Line getStartLine() { + exists(Location l1, Location l2 | + l1 = this.getLocation() and + l2 = result.getLocation() and + l1.getFile() = l2.getFile() and + l1.getStartLine() = l2.getStartLine() + ) + } + + /** + * Gets the line on which this element ends. + * + * This predicate is only defined for program elements from snapshots that + * have been extracted with the `--extract-program-text` flag. + */ + Line getEndLine() { + exists(Location l1, Location l2 | + l1 = this.getLocation() and + l2 = result.getLocation() and + l1.getFile() = l2.getFile() and + l1.getEndLine() = l2.getStartLine() + ) + } + + /** Gets the number of lines covered by this element. */ + int getNumLines() { result = this.getLocation().getNumLines() } + + /** Gets a textual representation of this element. */ + string toString() { + // to be overridden by subclasses + none() + } + + /** + * Gets a comma-separated list of the names of the primary CodeQL classes to which this element belongs. + */ + final string getPrimaryQlClasses() { result = concat(this.getAPrimaryQlClass(), ",") } + + /** + * Gets the primary QL class for the Locatable. + */ + string getAPrimaryQlClass() { result = "???" } +} + +/** + * DEPRECATED. Use `Location` instead. + */ +deprecated class DbLocation = Location; diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/NPM.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/NPM.qll new file mode 100644 index 000000000000..a0d028f8fb97 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/NPM.qll @@ -0,0 +1,414 @@ +/** + * Provides classes for working with NPM module definitions and dependencies. + */ +overlay[local?] +module; + +import minimal +private import NodeModuleResolutionImpl +private import PackageJsonEx + +/** A `package.json` configuration object. */ +class PackageJson extends JsonObject { + PackageJson() { + this.getJsonFile().getBaseName() = "package.json" and + this.isTopLevel() + } + + /** Gets the folder containing this `package.json` file. */ + Folder getFolder() { result = this.getJsonFile().getParentContainer() } + + /** + * Gets the name of this package as it appears in the `name` field. + */ + pragma[nomagic] + string getDeclaredPackageName() { result = this.getPropStringValue("name") } + + /** + * Gets the nearest `package.json` file found in the parent directories, if any. + */ + PackageJson getEnclosingPackage() { + result.getFolder() = packageInternalParent*(this.getFolder().getParentContainer()) + } + + /** + * Gets the name of this package. + * If the package is located under the package `pkg1` and its relative path is `foo/bar`, then the resulting package name will be `pkg1/foo/bar`. + */ + string getPackageName() { + result = this.getDeclaredPackageName() + or + not exists(this.getDeclaredPackageName()) and + exists(PackageJson parent | + parent = this.getEnclosingPackage() and + not parent.isPrivate() and + result = + parent.getDeclaredPackageName() + + this.getFolder().getRelativePath().suffix(parent.getFolder().getRelativePath().length()) + ) + } + + /** Gets the version of this package. */ + string getVersion() { result = this.getPropStringValue("version") } + + /** Gets the description of this package. */ + string getDescription() { result = this.getPropStringValue("description") } + + /** Gets the array of keywords for this package. */ + JsonArray getKeywords() { result = this.getPropValue("keywords") } + + /** Gets a keyword for this package. */ + string getAKeyword() { result = this.getKeywords().getElementStringValue(_) } + + /** Gets the homepage URL of this package. */ + string getHomepage() { result = this.getPropStringValue("homepage") } + + /** Gets the bug tracker information of this package. */ + BugTrackerInfo getBugs() { result = this.getPropValue("bugs") } + + /** Gets the license information of this package. */ + string getLicense() { result = this.getPropStringValue("license") } + + /** Gets the author information of this package. */ + ContributorInfo getAuthor() { result = this.getPropValue("author") } + + /** Gets information for a contributor to this package. */ + ContributorInfo getAContributor() { + result = this.getPropValue("contributors").getElementValue(_) + } + + /** Gets the array of files for this package. */ + JsonArray getFiles() { result = this.getPropValue("files") } + + /** Gets a file for this package. */ + string getAFile() { result = this.getFiles().getElementStringValue(_) } + + /** + * Gets the main module of this package. + * + * This can be given by the `main` or `module` property, or via the + * `exports` property with the relative path `"."`. + */ + string getMain() { result = this.getExportedPath(".") } + + /** + * Gets the path to the file exported with the given relative path. + * + * This can be given by the `exports` property, but also considers `main` and + * `module` paths to be exported under the relative path `"."`. + */ + string getExportedPath(string relativePath) { + this.(PackageJsonEx).hasExactPathMapping(relativePath, result) + or + relativePath = "." and + result = this.(PackageJsonEx).getMainPath() + } + + /** Gets the path of a command defined for this package. */ + string getBin(string cmd) { + cmd = this.getPackageName() and result = this.getPropStringValue("bin") + or + result = this.getPropValue("bin").getPropValue(cmd).getStringValue() + } + + /** Gets a manual page for this package. */ + string getAManFile() { + result = this.getPropStringValue("man") or + result = this.getPropValue("man").getElementValue(_).getStringValue() + } + + /** Gets information about the directories of this package. */ + JsonObject getDirectories() { result = this.getPropValue("directories") } + + /** Gets repository information for this package. */ + RepositoryInfo getRepository() { result = this.getPropValue("repository") } + + /** Gets information about the scripts of this package. */ + JsonObject getScripts() { result = this.getPropValue("scripts") } + + /** Gets configuration information for this package. */ + JsonObject getConfig() { result = this.getPropValue("config") } + + /** Gets the dependencies of this package. */ + PackageDependencies getDependencies() { result = this.getPropValue("dependencies") } + + /** Gets the development dependencies of this package. */ + PackageDependencies getDevDependencies() { result = this.getPropValue("devDependencies") } + + /** Gets the peer dependencies of this package. */ + PackageDependencies getPeerDependencies() { result = this.getPropValue("peerDependencies") } + + /** Gets the bundled dependencies of this package. */ + PackageDependencies getBundledDependencies() { + result = this.getPropValue("bundledDependencies") or + result = this.getPropValue("bundleDependencies") + } + + /** Gets the optional dependencies of this package. */ + PackageDependencies getOptionalDependencies() { + result = this.getPropValue("optionalDependencies") + } + + /** + * Gets a JSON object describing a group of dependencies of + * this package of the kind specified by `depkind`: + * `""` for normal dependencies, `"dev"` for `devDependencies`, + * `"bundled"` for `bundledDependencies` and `"opt"` for + * `optionalDependencies`. + */ + PackageDependencies getADependenciesObject(string depkind) { + result = this.getDependencies() and depkind = "" + or + result = this.getDevDependencies() and depkind = "dev" + or + result = this.getBundledDependencies() and depkind = "bundled" + or + result = this.getOptionalDependencies() and depkind = "opt" + } + + /** + * Holds if this package declares a dependency (including + * optional, development and bundled dependencies) on the given version + * of the given package. + * + * This does _not_ consider peer dependencies, which are semantically + * different from the other dependency types. + */ + predicate declaresDependency(string pkg, string version) { + this.getADependenciesObject(_).getADependency(pkg, version) + } + + /** Gets the engine dependencies of this package. */ + PackageDependencies getEngines() { result = this.getPropValue("engines") } + + /** Holds if this package has strict engine requirements. */ + predicate isEngineStrict() { this.getPropValue("engineStrict").(JsonBoolean).getValue() = "true" } + + /** Gets information about operating systems supported by this package. */ + JsonArray getOSs() { result = this.getPropValue("os") } + + /** Gets an operating system supported by this package. */ + string getWhitelistedOS() { + result = this.getOSs().getElementStringValue(_) and + not result.matches("!%") + } + + /** Gets an operating system not supported by this package. */ + string getBlacklistedOS() { + exists(string str | str = this.getOSs().getElementStringValue(_) | + result = str.regexpCapture("!(.*)", 1) + ) + } + + /** Gets information about platforms supported by this package. */ + JsonArray getCPUs() { result = this.getPropValue("cpu") } + + /** Gets a platform supported by this package. */ + string getWhitelistedCpu() { + result = this.getCPUs().getElementStringValue(_) and + not result.matches("!%") + } + + /** Gets a platform not supported by this package. */ + string getBlacklistedCpu() { + exists(string str | str = this.getCPUs().getElementStringValue(_) | + result = str.regexpCapture("!(.*)", 1) + ) + } + + /** Holds if this package prefers to be installed globally. */ + predicate isPreferGlobal() { this.getPropValue("preferGlobal").(JsonBoolean).getValue() = "true" } + + /** Holds if this is a private package. */ + predicate isPrivate() { this.getPropValue("private").(JsonBoolean).getValue() = "true" } + + /** Gets publishing configuration information about this package. */ + JsonValue getPublishConfig() { result = this.getPropValue("publishConfig") } + + /** + * Gets the main module of this package. + */ + Module getMainModule() { result.getFile() = this.(PackageJsonEx).getMainFileOrBestGuess() } + + /** + * Gets the module exported under the given relative path. + * + * The main module is considered exported under the path `"."`. + */ + Module getExportedModule(string relativePath) { + this.(PackageJsonEx).hasExactPathMappingTo(relativePath, result.getFile()) + or + relativePath = "." and + result = this.getMainModule() + } + + /** + * Gets the `types` or `typings` field of this package. + */ + string getTypings() { result = this.getPropStringValue(["types", "typings"]) } + + /** + * Gets the file containing the typings of this package, which can either be from the `types` or + * `typings` field, or derived from the `main` or `module` fields. + */ + File getTypingsFile() { none() } // implemented in PackageJsonEx + + /** + * Gets the module containing the typings of this package, which can either be from the `types` or + * `typings` field, or derived from the `main` or `module` fields. + */ + Module getTypingsModule() { result.getFile() = this.getTypingsFile() } +} + +/** + * A representation of bug tracker information for an NPM package. + */ +class BugTrackerInfo extends JsonValue { + BugTrackerInfo() { + exists(PackageJson pkg | pkg.getPropValue("bugs") = this) and + (this instanceof JsonObject or this instanceof JsonString) + } + + /** Gets the bug tracker URL. */ + string getUrl() { + result = this.getPropValue("url").getStringValue() or + result = this.getStringValue() + } + + /** Gets the bug reporting email address. */ + string getEmail() { result = this.getPropValue("email").getStringValue() } +} + +/** + * A representation of contributor information for an NPM package. + */ +class ContributorInfo extends JsonValue { + ContributorInfo() { + exists(PackageJson pkg | + this = pkg.getPropValue("author") or + this = pkg.getPropValue("contributors").getElementValue(_) + ) and + (this instanceof JsonObject or this instanceof JsonString) + } + + /** + * Gets the `i`th item of information about a contributor, where the first + * item is their name, the second their email address, and the third their + * homepage URL. + */ + private string parseInfo(int group) { + result = this.getStringValue().regexpCapture("(.*?)(?: <(.*?)>)?(?: \\((.*)?\\))", group) + } + + /** Gets the contributor's name. */ + string getName() { + result = this.getPropValue("name").getStringValue() or + result = this.parseInfo(1) + } + + /** Gets the contributor's email address. */ + string getEmail() { + result = this.getPropValue("email").getStringValue() or + result = this.parseInfo(2) + } + + /** Gets the contributor's homepage URL. */ + string getUrl() { + result = this.getPropValue("url").getStringValue() or + result = this.parseInfo(3) + } +} + +/** + * A representation of repository information for an NPM package. + */ +class RepositoryInfo extends JsonObject { + RepositoryInfo() { exists(PackageJson pkg | this = pkg.getPropValue("repository")) } + + /** Gets the repository type. */ + string getType() { result = this.getPropStringValue("type") } + + /** Gets the repository URL. */ + string getUrl() { result = this.getPropStringValue("url") } +} + +/** + * A representation of package dependencies for an NPM package. + */ +class PackageDependencies extends JsonObject { + PackageDependencies() { + exists(PackageJson pkg, string name | + name.regexpMatch("(.+D|d)ependencies|engines") and + this = pkg.getPropValue(name) + ) + } + + /** Holds if this package depends on version 'version' of package 'pkg'. */ + predicate getADependency(string pkg, string version) { version = this.getPropStringValue(pkg) } +} + +/** + * An NPM package. + */ +class NpmPackage extends @folder { + /** The `package.json` file of this package. */ + PackageJson pkg; + + NpmPackage() { pkg.getJsonFile().getParentContainer() = this } + + /** Gets a textual representation of this package. */ + string toString() { result = this.(Folder).toString() } + + /** Gets the full file system path of this package. */ + string getPath() { result = this.(Folder).getAbsolutePath() } + + /** Gets the `package.json` object of this package. */ + PackageJson getPackageJson() { result = pkg } + + /** Gets the name of this package. */ + string getPackageName() { result = this.getPackageJson().getPackageName() } + + /** Gets the `node_modules` folder of this package. */ + Folder getNodeModulesFolder() { + result.getBaseName() = "node_modules" and + result.getParentContainer() = this + } + + /** + * Gets a file belonging to this package. + * + * We only consider files to belong to the nearest enclosing package, + * and files inside the `node_modules` folder of a package are not + * considered to belong to that package. + */ + File getAFile() { this = packageInternalParent*(result.getParentContainer()) } + + /** + * Gets a Node.js module belonging to this package. + * + * We only consider modules to belong to the nearest enclosing package, + * and modules inside the `node_modules` folder of a package are not + * considered to belong to that package. + */ + Module getAModule() { result.getFile() = this.getAFile() } + + /** + * Gets the main module of this package. + */ + Module getMainModule() { result = pkg.getMainModule() } + + /** + * Holds if this package declares a dependency on version `v` of package `p`. + */ + predicate declaresDependency(string p, string v) { pkg.declaresDependency(p, v) } +} + +/** + * Gets the parent folder of `c`, provided that they belong to the same NPM + * package; that is, `c` must not be a `node_modules` folder. + */ +private Folder packageInternalParent(Container c) { + result = c.getParentContainer() and + not c.(Folder).getBaseName() = "node_modules" and + not c = any(PackageJson pkg).getFolder() +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/NodeModuleResolutionImpl.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/NodeModuleResolutionImpl.qll new file mode 100644 index 000000000000..2a4f490b9da9 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/NodeModuleResolutionImpl.qll @@ -0,0 +1,42 @@ +/** + * INTERNAL: Do not use directly. + * + * Provides predicates for modeling Node.js module resolution. + */ + +import minimal + +/** + * Gets the priority with which a given file extension should be found by module resolution. + * Extensions with a lower numeric priority value are preferred. + * + * File types that compile to `js` are preferred over the `js` file type itself. + * This is to ensure we find the original source file in case the compiled output is also present. + */ +int getFileExtensionPriority(string ext) { + ext = "tsx" and result = 0 + or + ext = "ts" and result = 1 + or + ext = "jsx" and result = 2 + or + ext = "es6" and result = 3 + or + ext = "es" and result = 4 + or + ext = "mjs" and result = 5 + or + ext = "cjs" and result = 6 + or + ext = "js" and result = 7 + or + ext = "json" and result = 8 + or + ext = "node" and result = 9 + or + ext = "d.ts" and result = 10 +} + +int prioritiesPerCandidate() { result = 3 * (numberOfExtensions() + 1) } + +int numberOfExtensions() { result = count(getFileExtensionPriority(_)) } diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Overlay.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Overlay.qll new file mode 100644 index 000000000000..b65644c6bff7 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Overlay.qll @@ -0,0 +1 @@ +private import semmle.javascript.internal.Overlay // this file is already minimal diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PackageJsonEx.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PackageJsonEx.qll new file mode 100644 index 000000000000..bd1749f74dbb --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PackageJsonEx.qll @@ -0,0 +1,151 @@ +private import minimal +private import JSPaths + +/** + * A `package.json` file. The class is an extension of the `PackageJson` class with some internal path-resolution predicates. + */ +class PackageJsonEx extends PackageJson { + private JsonValue getAPartOfExportsSection(string pattern) { + result = this.getPropValue("exports") and + pattern = "" + or + exists(string prop, string prevPath | + result = this.getAPartOfExportsSection(prevPath).getPropValue(prop) and + if prop.matches("./%") then pattern = prop.suffix(2) else pattern = prevPath + ) + } + + predicate hasPathMapping(string pattern, string newPath) { + this.getAPartOfExportsSection(pattern).getStringValue() = newPath + } + + predicate hasExactPathMapping(string pattern, string newPath) { + this.getAPartOfExportsSection(pattern).getStringValue() = newPath and + not pattern.matches("%*%") + } + + predicate hasPrefixPathMapping(string pattern, string newPath) { + this.hasPathMapping(pattern + "*", newPath + "*") + } + + predicate hasExactPathMappingTo(string pattern, Container target) { + exists(string newPath | + this.hasExactPathMapping(pattern, newPath) and + target = Resolver::resolve(this.getFolder(), newPath) + ) + } + + predicate hasPrefixPathMappingTo(string pattern, Container target) { + exists(string newPath | + this.hasPrefixPathMapping(pattern, newPath) and + target = Resolver::resolve(this.getFolder(), newPath) + ) + } + + string getMainPath() { result = this.getPropStringValue(["main", "module"]) } + + File getMainFile() { + exists(Container main | main = Resolver::resolve(this.getFolder(), this.getMainPath()) | + result = main + or + result = main.(Folder).getJavaScriptFileOrTypings("index") + ) + } + + File getMainFileOrBestGuess() { + result = this.getMainFile() + or + result = guessPackageJsonMain1(this) + or + result = guessPackageJsonMain2(this) + } + + string getAPathInFilesArray() { + result = this.getPropValue("files").(JsonArray).getElementStringValue(_) + } + + Container getAFileInFilesArray() { + result = Resolver::resolve(this.getFolder(), this.getAPathInFilesArray()) + } + + override File getTypingsFile() { + result = Resolver::resolve(this.getFolder(), this.getTypings()) + or + not exists(this.getTypings()) and + exists(File mainFile | + mainFile = this.getMainFileOrBestGuess() and + result = + mainFile + .getParentContainer() + .getFile(mainFile.getStem().regexpReplaceAll("\\.d$", "") + ".d.ts") + ) + } +} + +private module ResolverConfig implements Folder::ResolveSig { + additional predicate shouldResolve(PackageJsonEx pkg, Container base, string path) { + base = pkg.getFolder() and + ( + pkg.hasExactPathMapping(_, path) + or + pkg.hasPrefixPathMapping(_, path) + or + path = pkg.getMainPath() + or + path = pkg.getAPathInFilesArray() + or + path = pkg.getTypings() + ) + } + + predicate shouldResolve(Container base, string path) { shouldResolve(_, base, path) } + + predicate getAnAdditionalChild = JSPaths::getAnAdditionalChild/2; + + predicate isOptionalPathComponent(string segment) { + // Try to omit paths can might refer to a build format, .e.g `dist/cjs/foo.cjs` -> `src/foo.ts` + segment = ["cjs", "mjs", "js"] + } + + bindingset[segment] + string rewritePathSegment(string segment) { + // Try removing anything after the first dot, such as foo.min.js -> foo (the extension is then filled in by getAdditionalChild) + result = segment.regexpReplaceAll("\\..*", "") + } +} + +private module Resolver = Folder::Resolve; + +/** + * Removes the scope from a package name, e.g. `@foo/bar` -> `bar`. + */ +bindingset[name] +private string stripPackageScope(string name) { result = name.regexpReplaceAll("^@[^/]+/", "") } + +private predicate isImplementationFile(File f) { not f.getBaseName().matches("%.d.ts") } + +File guessPackageJsonMain1(PackageJsonEx pkg) { + not isImplementationFile(pkg.getMainFile()) and + exists(Folder folder, Folder subfolder | + folder = pkg.getFolder() and + ( + subfolder = folder or + subfolder = folder.getChildContainer(getASrcFolderName()) or + subfolder = + folder + .getChildContainer(getASrcFolderName()) + .(Folder) + .getChildContainer(getASrcFolderName()) + ) + | + result = subfolder.getJavaScriptFileOrTypings("index") + or + result = subfolder.getJavaScriptFileOrTypings(stripPackageScope(pkg.getDeclaredPackageName())) + ) +} + +File guessPackageJsonMain2(PackageJsonEx pkg) { + not isImplementationFile(pkg.getMainFile()) and + not isImplementationFile(guessPackageJsonMain1(pkg)) and + result = pkg.getAFileInFilesArray() +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PathConcatenation.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PathConcatenation.qll new file mode 100644 index 000000000000..f2fa997a7350 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PathConcatenation.qll @@ -0,0 +1,39 @@ +overlay[local?] +module; + +private import minimal + +/** + * A path expression that can be constant-folded by concatenating subexpressions. + */ +abstract class PathConcatenation extends Expr { + /** Gets the separator to insert between paths */ + string getSeparator() { result = "" } + + /** Gets the `n`th operand to concatenate. */ + abstract Expr getOperand(int n); +} + +private class AddExprConcatenation extends PathConcatenation, AddExpr { + override Expr getOperand(int n) { + n = 0 and result = this.getLeftOperand() + or + n = 1 and result = this.getRightOperand() + } +} + +private class TemplateConcatenation extends PathConcatenation, TemplateLiteral { + override Expr getOperand(int n) { result = this.getElement(n) } +} + +private class JoinCallConcatenation extends PathConcatenation, CallExpr { + JoinCallConcatenation() { + // Heuristic recognition of path.join and path.resolve since we can't rely on SourceNode at this stage. + this.getReceiver().(VarAccess).getName() = "path" and + this.getCalleeName() = ["join", "resolve"] + } + + override Expr getOperand(int n) { result = this.getArgument(n) } + + override string getSeparator() { result = "/" } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PathMapping.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PathMapping.qll new file mode 100644 index 000000000000..2ae16f4d87ae --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/PathMapping.qll @@ -0,0 +1,31 @@ +/** + * Provides an extensible mechanism for modeling path mappings. + */ + +private import minimal +private import TSConfig + +/** + * A `tsconfig.json`-like configuration object that can affect import resolution via path mappings. + */ +abstract class PathMapping extends Locatable { + /** + * Gets a file affected by this path mapping. + */ + abstract File getAnAffectedFile(); + + /** + * Holds if imports paths exactly matching `pattern` should be redirected to `newPath` + * resolved relative to `newContext`. + */ + predicate hasExactPathMapping(string pattern, Container newContext, string newPath) { none() } + + /** + * Holds if imports paths starting with `pattern` should have the matched prefix replaced by `newPath` + * and then resolved relative to `newContext`. + */ + predicate hasPrefixPathMapping(string pattern, Container newContext, string newPath) { none() } + + /** Holds if non-relative paths in affected files should be resolved relative to `base`. */ + predicate hasBaseUrl(Container base) { none() } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Paths.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Paths.qll new file mode 100644 index 000000000000..7d6bf5b7f6c6 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Paths.qll @@ -0,0 +1,91 @@ +/** + * Provides classes for working with file system paths and program expressions + * that denote them. + */ + +import minimal + +/** + * Gets a regular expression that can be used to parse slash-separated paths. + * + * The first capture group captures the dirname of the path, that is, everything + * before the last slash, or the empty string if there isn't a slash. + * + * The second capture group captures the basename of the path, that is, everything + * after the last slash, or the entire path if there isn't a slash. + * + * The third capture group captures the stem of the basename, that is, everything + * before the last dot, or the entire basename if there isn't a dot. + * + * Finally, the fourth and fifth capture groups capture the extension of the basename, + * that is, everything after the last dot. The fourth group includes the dot, the + * fifth does not. + */ +private string pathRegex() { result = "(.*)(?:/|^)(([^/]*?)(\\.([^.]*))?)" } + +/** + * A `string` with some additional member predicates for extracting parts of a file path. + */ +class FilePath extends string { + bindingset[this] + FilePath() { any() } + + /** Gets the `i`th component of this path. */ + bindingset[this] + string getComponent(int i) { result = this.splitAt("/", i) } + + /** Gets the number of components of this path. */ + bindingset[this] + int getNumComponent() { result = count(int i | exists(this.getComponent(i))) } + + /** Gets the base name of the folder or file this path refers to. */ + bindingset[this] + string getBaseName() { result = this.regexpCapture(pathRegex(), 2) } + + /** + * Gets stem of the folder or file this path refers to, that is, the prefix of its base name + * up to (but not including) the last dot character if there is one, or the entire + * base name if there is not + */ + bindingset[this] + string getStem() { result = this.regexpCapture(pathRegex(), 3) } + + /** Gets the path of the parent folder of the folder or file this path refers to. */ + bindingset[this] + string getDirName() { result = this.regexpCapture(pathRegex(), 1) } + + /** + * Gets the extension of the folder or file this path refers to, that is, the suffix of the base name + * starting at the last dot character, if there is one. + * + * Has no result if the base name does not contain a dot. + */ + bindingset[this] + string getExtension() { result = this.regexpCapture(pathRegex(), 4) } + + /** + * Holds if this is a relative path starting with an explicit `./` or similar syntax meaning it + * must be resolved relative to its enclosing folder. + * + * Specifically this holds when the string is `.` or `..`, or starts with `./` or `../` or + * `.\` or `..\`. + */ + bindingset[this] + pragma[inline_late] + predicate isDotRelativePath() { this.regexpMatch("\\.\\.?(?:[/\\\\].*)?") } + + /** + * Gets the NPM package name from the beginning of the given import path. + * + * Has no result for paths starting with a `.` or `/` + * + * For example: + * - `foo/bar` maps to `foo` + * - `@example/foo/bar` maps to `@example/foo` + * - `./foo` maps to nothing. + */ + bindingset[this] + string getPackagePrefix() { + result = this.regexpFind("^(@[^/\\\\]+[/\\\\])?[^@./\\\\][^/\\\\]*", _, _) + } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Stmt.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Stmt.qll new file mode 100644 index 000000000000..268ad7a297cb --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Stmt.qll @@ -0,0 +1,1185 @@ +/** Provides classes for working with statements. */ +overlay[local?] +module; + +import minimal + +/** + * A statement. + * + * Examples: + * + * ``` + * x = 0; + * + * if (typeof console !== "undefined") { + * log = console.log; + * } else { + * log = alert; + * } + * ``` + */ +class Stmt extends @stmt, ExprOrStmt, Documentable { + /** Holds if this statement has an implicitly inserted semicolon. */ + predicate hasSemicolonInserted() { + this.isSubjectToSemicolonInsertion() and + this.getLastToken().getValue() != ";" + } + + /** Holds if automatic semicolon insertion applies to this statement. */ + predicate isSubjectToSemicolonInsertion() { none() } + + /** + * Gets the kind of this statement, which is an integer + * value representing the statement's node type. + * + * _Note_: The mapping from node types to integers is considered an implementation detail + * and may change between versions of the extractor. + */ + int getKind() { stmts(this, result, _, _, _) } + + override string toString() { stmts(this, _, _, _, result) } + + /** + * Gets the statement that is the parent of this statement in the AST, if any. + */ + Stmt getParentStmt() { this = result.getAChildStmt() } + + /** + * Holds if this statement is lexically nested inside statement `outer`. + */ + predicate nestedIn(Stmt outer) { + outer = this.getParentStmt+() or + this.getContainer().(Expr).getEnclosingStmt().nestedIn(outer) + } + + /** + * Gets the `try` statement with a catch block containing this statement without + * crossing function boundaries or other `try ` statements with catch blocks. + */ + TryStmt getEnclosingTryCatchStmt() { + this.getParentStmt+() = result.getBody() and + exists(result.getACatchClause()) and + not exists(TryStmt mid | exists(mid.getACatchClause()) | + this.getParentStmt+() = mid.getBody() and mid.getParentStmt+() = result.getBody() + ) + } +} + +private class TControlStmt = + TLoopStmt or @if_stmt or @with_stmt or @switch_stmt or @try_stmt or @catch_clause; + +private class TLoopStmt = TEnhancedForLoop or @while_stmt or @do_while_stmt or @for_stmt; + +private class TEnhancedForLoop = @for_in_stmt or @for_each_stmt or @for_of_stmt; + +/** + * A control statement, that is, is a loop, an if statement, a switch statement, + * a with statement, a try statement, or a catch clause. + * + * Examples: + * + * ``` + * if (typeof console !== "undefined") { + * log = console.log; + * } else { + * log = alert; + * } + * + * while(hasNext()) { + * handle(getNext()); + * } + * ``` + */ +class ControlStmt extends TControlStmt, Stmt { + /** Gets a statement controlled by this control statement. */ + abstract Stmt getAControlledStmt(); +} + +/** + * A loop, that is, a while loop, a do-while loop, a for loop, or a for-in loop. + * + * Examples: + * + * ``` + * while(hasNext()) { + * handle(getNext()); + * } + * + * do { + * handle(lines[i]); + * } while(++i < lines.length); + * ``` + */ +class LoopStmt extends TLoopStmt, ControlStmt { + /** Gets the body of this loop. */ + abstract Stmt getBody(); + + /** Gets the loop test of this loop. */ + abstract Expr getTest(); + + override Stmt getAControlledStmt() { result = this.getBody() } +} + +/** + * An empty statement. + * + * Example: + * + * ``` + * ; + * ``` + */ +class EmptyStmt extends @empty_stmt, Stmt { + override string getAPrimaryQlClass() { result = "EmptyStmt" } +} + +/** + * A block of statements. + * + * Example: + * + * ``` + * { + * console.log(msg); + * } + * ``` + */ +class BlockStmt extends @block_stmt, Stmt { + /** Gets the `i`th statement in this block. */ + Stmt getStmt(int i) { result = this.getChildStmt(i) } + + /** Gets a statement in this block. */ + Stmt getAStmt() { result = this.getStmt(_) } + + /** Gets the number of statements in this block. */ + int getNumStmt() { result = count(this.getAStmt()) } + + /** Holds if this block is a function body. */ + predicate isFunctionBody() { this.getParent() instanceof Function } + + override string getAPrimaryQlClass() { result = "BlockStmt" } +} + +/** + * An expression statement. + * + * Examples: + * + * ``` + * x = 0; + * console.log("Restart."); + * ``` + */ +class ExprStmt extends @expr_stmt, Stmt { + /** Gets the expression of this expression statement. */ + Expr getExpr() { result = this.getChildExpr(0) } + + override predicate isSubjectToSemicolonInsertion() { not this.isDoubleColonMethod(_, _, _) } + + /** + * Holds if this expression statement is a JScript-style double colon method declaration. + */ + predicate isDoubleColonMethod(Identifier interface, Identifier id, Function f) { + // the parser converts double colon method declarations into assignments, but we + // can consult token-level information to identify them + exists(Assignment assgn, DotExpr dot, Token tk | + assgn = this.getExpr() and + dot = assgn.getLhs() and + interface = dot.getBase() and + // check if the interface name is followed by two colons + tk = interface.getLastToken().getNextToken() and + ( + tk.getValue() = ":" and tk.getNextToken().getValue() = ":" + or + tk.getValue() = "::" + ) and + id = dot.getProperty() and + f = assgn.getRhs() + ) + } + + override string getAPrimaryQlClass() { result = "ExprStmt" } +} + +/** + * An expression statement wrapping a string literal (which may + * be a directive). + */ +private class MaybeDirective extends ExprStmt { + MaybeDirective() { this.getExpr() instanceof StringLiteral } + + /** + * Gets the raw text of the string literal wrapped by this statement. + * + * The surrounding quotes are removed, but escape sequences are not + * interpreted. For example, the text of the directive + * + * ``` + * 'use strict'; + * ``` + * + * is `use strict`, while the text of the directive + * + * ``` + * "use\x20strict"; + * ``` + * + * is `use\x20strict`. (Note in particular that the latter is not + * a valid strict mode declaration, even though the value of the + * string literal is the same as in the former case.) + */ + string getDirectiveText() { + exists(string text | text = this.getExpr().(StringLiteral).getRawValue() | + result = text.substring(1, text.length() - 1) + ) + } +} + +/** + * A directive: string literal expression statement in the beginning of a statement container. + * + * Examples: + * + * ``` + * function f() { + * "use strict"; + * "a custom directive"; + * } + * ``` + */ +class Directive extends MaybeDirective { + Directive() { + exists(StmtContainer sc, AstNode body, int i | + // directives must be toplevel statements in their container + body = sc.getBody() and + this = body.getChildStmt(i) and + // and all preceding statements must be directives as well + forall(Stmt pred | pred = body.getChildStmt([0 .. i - 1]) | pred instanceof MaybeDirective) + ) + } +} + +/** + * Module containing subclasses of the `Directive` class. + */ +module Directive { + /** + * A known directive, such as a strict mode declaration. + * + * Example: + * + * ``` + * "use strict"; + * ``` + */ + abstract class KnownDirective extends Directive { } + + /** + * A strict mode declaration. + * + * Example: + * + * ``` + * "use strict"; + * ``` + */ + class StrictModeDecl extends KnownDirective { + StrictModeDecl() { this.getDirectiveText() = "use strict" } + } + + /** + * An asm.js directive. + * + * Example: + * + * ``` + * "use asm"; + * ``` + */ + class AsmJSDirective extends KnownDirective { + AsmJSDirective() { this.getDirectiveText() = "use asm" } + } + + /** + * A Babel directive. + * + * Example: + * + * ``` + * "use babel"; + * ``` + */ + class BabelDirective extends KnownDirective { + BabelDirective() { this.getDirectiveText() = "use babel" } + } + + /** + * A legacy 6to5 directive. + * + * Example: + * + * ``` + * "use 6to5"; + * ``` + */ + class SixToFiveDirective extends KnownDirective { + SixToFiveDirective() { this.getDirectiveText() = "use 6to5" } + } + + /** + * A SystemJS `format` directive. + * + * Example: + * + * ``` + * "format global"; + * ``` + */ + class SystemJSFormatDirective extends KnownDirective { + SystemJSFormatDirective() { + this.getDirectiveText().regexpMatch("format (cjs|esm|global|register)") + } + } + + /** + * A SystemJS `format register` directive. + * + * Example: + * + * ``` + * "format register"; + * ``` + */ + class FormatRegisterDirective extends SystemJSFormatDirective { + FormatRegisterDirective() { this.getDirectiveText() = "format register" } + } + + /** + * A `ngInject` or `ngNoInject` directive. + * + * Example: + * + * ``` + * "ngInject"; + * ``` + */ + class NgInjectDirective extends KnownDirective { + NgInjectDirective() { this.getDirectiveText().regexpMatch("ng(No)?Inject") } + } + + /** + * A YUI compressor directive. + * + * Example: + * + * ``` + * "console:nomunge"; + * ``` + */ + class YuiDirective extends KnownDirective { + YuiDirective() { + this.getDirectiveText().regexpMatch("([a-z0-9_]+:nomunge, ?)*([a-z0-9_]+:nomunge)") + } + } + + /** + * A SystemJS `deps` directive. + * + * Example: + * + * ``` + * "deps fs"; + * ``` + */ + class SystemJSDepsDirective extends KnownDirective { + SystemJSDepsDirective() { this.getDirectiveText().regexpMatch("deps [^ ]+") } + } + + /** + * A `bundle` directive. + * + * Example: + * + * ``` + * "bundle"; + * ``` + */ + class BundleDirective extends KnownDirective { + BundleDirective() { this.getDirectiveText() = "bundle" } + } + + /** + * A `use server` directive. + * + * Example: + * + * ``` + * "use server"; + * ``` + */ + class UseServerDirective extends KnownDirective { + UseServerDirective() { this.getDirectiveText() = "use server" } + } + + /** + * A `use client` directive. + * + * Example: + * + * ``` + * "use client"; + * ``` + */ + class UseClientDirective extends KnownDirective { + UseClientDirective() { this.getDirectiveText() = "use client" } + } + + /** + * A `use cache` directive. + * + * Examples: + * + * ``` + * "use cache"; + * "use cache: remote"; + * "use cache: private"; + * ``` + */ + class UseCacheDirective extends KnownDirective { + UseCacheDirective() { this.getDirectiveText().regexpMatch("use cache(:.*)?") } + } +} + +/** + * An `if` statement. + * + * Example: + * + * ``` + * if (typeof console !== "undefined") { + * log = console.log; + * } else { + * log = alert; + * } + * ``` + */ +class IfStmt extends @if_stmt, ControlStmt { + /** Gets the condition of this `if` statement. */ + Expr getCondition() { result = this.getChildExpr(0) } + + /** Gets the "then" branch of this `if` statement. */ + Stmt getThen() { result = this.getChildStmt(1) } + + /** Gets the "else" branch of this `if` statement, if any. */ + Stmt getElse() { result = this.getChildStmt(2) } + + /** Gets the `if` token of this `if` statement. */ + KeywordToken getIfToken() { result = this.getFirstToken() } + + /** Gets the `else` token of this `if` statement, if any. */ + KeywordToken getElseToken() { + result = this.getThen().getLastToken().getNextToken() and + result.getIndex() < this.getLastToken().getIndex() + } + + override Stmt getAControlledStmt() { + result = this.getThen() or + result = this.getElse() + } + + /** Holds if this `if` statement is an `else if` of an outer `if` statement. */ + predicate isElseIf() { exists(IfStmt outer | outer.getElse() = this) } + + override string getAPrimaryQlClass() { result = "IfStmt" } +} + +/** + * A labeled statement. + * + * Example: + * + * ``` + * outer: + * for(i=0; i<10; ++i) { + * for(j=0; j= 1 and + i = idx - 1 + ) + } + + /** Gets a `catch` clause of this `try` statement. */ + CatchClause getACatchClause() { result = this.getCatchClause(_) } + + /** Gets the (unique) unguarded `catch` clause of this `try` statement, if any. */ + CatchClause getCatchClause() { + result = this.getACatchClause() and + not exists(result.getGuard()) + } + + /** Gets the number of `catch` clauses of this `try` statement. */ + int getNumCatchClause() { result = count(this.getACatchClause()) } + + /** Gets the `finally` block of this `try` statement, if any. */ + BlockStmt getFinally() { result = this.getChildStmt(-1) } + + override string getAPrimaryQlClass() { result = "TryStmt" } +} + +/** + * A `while` loop. + * + * Example: + * + * ``` + * while(hasNext()) { + * handle(getNext()); + * } + * ``` + */ +class WhileStmt extends @while_stmt, LoopStmt { + /** Gets the loop condition of this `while` loop. */ + Expr getExpr() { result = this.getChildExpr(0) } + + override Expr getTest() { result = this.getExpr() } + + override Stmt getBody() { result = this.getChildStmt(1) } + + override string getAPrimaryQlClass() { result = "WhileStmt" } +} + +/** + * A `do`-`while` loop. + * + * Example: + * + * ``` + * do { + * handle(lines[i]); + * } while(++i < lines.length); + * ``` + */ +class DoWhileStmt extends @do_while_stmt, LoopStmt { + /** Gets the loop condition of this `do`-`while` loop. */ + Expr getExpr() { result = this.getChildExpr(1) } + + override Expr getTest() { result = this.getExpr() } + + override Stmt getBody() { result = this.getChildStmt(0) } + + override predicate isSubjectToSemicolonInsertion() { any() } + + override string getAPrimaryQlClass() { result = "DoWhileStmt" } +} + +/** + * An expression or a variable declaration statement. + * + * Examples: + * + * ``` + * i = 0; + * var i = 1; + * ``` + */ +class ExprOrVarDecl extends AstNode { + ExprOrVarDecl() { + this instanceof Expr or + this instanceof DeclStmt + } +} + +/** + * A `for` loop. + * + * Example: + * + * ``` + * for(var i=0; i<10; ++i) { + * sample(i); + * } + * ``` + */ +class ForStmt extends @for_stmt, LoopStmt { + /** Gets the init part of this `for` loop. */ + ExprOrVarDecl getInit() { + result = this.getChildExpr(0) or + result = this.getChildStmt(0) + } + + override Expr getTest() { result = this.getChildExpr(1) } + + /** Gets the update part of this `for` loop. */ + Expr getUpdate() { result = this.getChildExpr(2) } + + override Stmt getBody() { result = this.getChildStmt(3) } + + override string getAPrimaryQlClass() { result = "ForStmt" } +} + +/** + * A `for`-`in`, `for`-`of` or `for each`-`in` loop. + * + * Examples: + * + * ``` + * for(var p in src) { + * dest[p] = src[p]; + * } + * + * for(var elt of arr) { + * sum += elt; + * } + * ``` + */ +class EnhancedForLoop extends TEnhancedForLoop, LoopStmt { + /** + * Gets the iterator of this `for`-`in` or `for`-`of` loop; this can be either a + * pattern, a property reference, or a variable declaration statement. + */ + ExprOrVarDecl getIterator() { + result = this.getChildExpr(0) or + result = this.getChildStmt(0) + } + + /** + * Gets the default value of the loop's iterator, if any. + */ + Expr getDefault() { result = this.getChildExpr(-1) } + + /** + * Gets the iterator expression of this `for`-`in` or `for`-`of` loop; this can be + * either a variable access or a variable declarator. + */ + Expr getIteratorExpr() { + result = this.getIterator() or + result = this.getIterator().(DeclStmt).getADecl() + } + + /** + * Gets the property, variable, or destructuring pattern occurring as the iterator + * expression in this `for`-`in` or `for`-`of` loop. + */ + Expr getLValue() { + result = this.getIterator() and + (result instanceof BindingPattern or result instanceof PropAccess) + or + result = this.getIterator().(DeclStmt).getADecl().getBindingPattern() + } + + /** + * Gets an iterator variable of this `for`-`in` or `for`-`of` loop. + */ + Variable getAnIterationVariable() { + result = this.getIterator().(DeclStmt).getADecl().getBindingPattern().getAVariable() or + result = this.getIterator().(BindingPattern).getAVariable() + } + + override Expr getTest() { none() } + + /** Gets the expression this `for`-`in` or `for`-`of` loop iterates over. */ + Expr getIterationDomain() { result = this.getChildExpr(1) } + + override Stmt getBody() { result = this.getChildStmt(2) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getIteratorExpr().getFirstControlFlowNode() + } +} + +/** + * A `for`-`in` loop. + * + * Example: + * + * ``` + * for(var p in src) { + * dest[p] = src[p]; + * } + * ``` + */ +class ForInStmt extends @for_in_stmt, EnhancedForLoop { + override string getAPrimaryQlClass() { result = "ForInStmt" } +} + +/** + * A `for`-`of` loop. + * + * Example: + * + * ``` + * for(var elt of arr) { + * sum += elt; + * } + * ``` + */ +class ForOfStmt extends @for_of_stmt, EnhancedForLoop { + /** + * Holds if this is a `for-await-of` statement. + */ + predicate isAwait() { is_for_await_of(this) } + + override string getAPrimaryQlClass() { result = "ForOfStmt" } +} + +/** + * A `for each`-`in` loop. + * + * Example: + * + * ``` + * for each(var elt in arr) { + * sum += elt; + * } + * ``` + */ +class ForEachStmt extends @for_each_stmt, EnhancedForLoop { + override string getAPrimaryQlClass() { result = "ForEachStmt" } +} + +/** + * A `debugger` statement. + * + * Example: + * + * ``` + * debugger; + * ``` + */ +class DebuggerStmt extends @debugger_stmt, Stmt { + override predicate isSubjectToSemicolonInsertion() { any() } + + override string getAPrimaryQlClass() { result = "DebuggerStmt" } +} + +/** + * A function declaration statement. + * + * Example: + * + * ``` + * function abs(x) { + * return abs < 0 ? -abs : abs; + * } + * ``` + */ +class FunctionDeclStmt extends @function_decl_stmt, Stmt, Function { + override Stmt getEnclosingStmt() { result = this } + + override string getAPrimaryQlClass() { result = "FunctionDeclStmt" } +} + +/** + * A declaration statement, that is, a `var`, `const` or `let` declaration + * (including legacy 'let' statements). + * + * Examples: + * + * ``` + * const fs = require('fs'); + * var count = 0; + * let i = 1, j = i-1; + * ``` + */ +class DeclStmt extends @decl_stmt, Stmt { + /** Gets the `i`th declarator in this declaration statement. */ + VariableDeclarator getDecl(int i) { result = this.getChildExpr(i) and i >= 0 } + + /** Gets a declarator in this declaration statement. */ + VariableDeclarator getADecl() { result = this.getDecl(_) } + + override predicate isSubjectToSemicolonInsertion() { + // exclude variable declarations in the init part of for/for-in/for-of loops + not exists(LoopStmt for | this = for.getAChildStmt() and this != for.getBody()) + } + + override string getAPrimaryQlClass() { result = "DeclStmt" } +} + +/** + * A `var` declaration statement. + * + * Example: + * + * ``` + * var count = 0; + * ``` + */ +class VarDeclStmt extends @var_decl_stmt, DeclStmt { } + +/** + * A `const` declaration statement. + * + * Example: + * + * ``` + * const fs = require('fs'); + * ``` + */ +class ConstDeclStmt extends @const_decl_stmt, DeclStmt { } + +/** + * A `using` declaration statement. + * + * Example: + * + * ``` + * using file = new TextFile("file.txt"); + * ``` + */ +class UsingDeclStmt extends @using_decl_stmt, DeclStmt { } + +/** + * A `let` declaration statement. + * + * Example: + * + * ``` + * let i = 1, j = i-1; + * ``` + */ +class LetStmt extends @let_stmt, DeclStmt { } + +/** + * A legacy `let` statement, that is, a statement of the form `let(vardecls) stmt`. + * + * Example: + * + * ``` + * let(i = 1) { + * console.log(i); + * } + * ``` + */ +class LegacyLetStmt extends @legacy_let_stmt, DeclStmt { + /** Gets the statement this let statement scopes over. */ + Stmt getBody() { result = this.getChildStmt(-1) } + + override predicate isSubjectToSemicolonInsertion() { none() } +} + +/** + * A `case` or `default` clause in a `switch` statement. + * + * Examples: + * + * ``` + * case 1: + * default: + * ``` + */ +class Case extends @case, Stmt { + /** Gets the test expression of this `case` clause. */ + Expr getExpr() { result = this.getChildExpr(-1) } + + /** Holds if this is a `default` clause. */ + predicate isDefault() { not exists(this.getExpr()) } + + /** Gets the `i`th statement in this `case` clause. */ + Stmt getBodyStmt(int i) { result = this.getChildStmt(i) } + + /** Gets a statement in this `case` clause. */ + Stmt getABodyStmt() { result = this.getChildStmt(_) } + + /** Gets the number of statements in this `case` clause. */ + int getNumBodyStmt() { result = count(this.getABodyStmt()) } + + /** Gets the `switch` statement to which this clause belongs. */ + SwitchStmt getSwitch() { result = this.getParent() } + + override string getAPrimaryQlClass() { result = "Case" } +} + +/** + * A `catch` clause. + * + * Example: + * + * ``` + * catch(e) { + * log(e); + * } + * ``` + */ +class CatchClause extends @catch_clause, ControlStmt, Parameterized { + /** Gets the body of this `catch` clause. */ + BlockStmt getBody() { result = this.getChildStmt(1) } + + /** Gets the guard expression of this `catch` clause, if any. */ + Expr getGuard() { result = this.getChildExpr(2) } + + override Stmt getAControlledStmt() { result = this.getBody() } + + /** Gets the scope induced by this `catch` clause. */ + CatchScope getScope() { result.getCatchClause() = this } + + override string getAPrimaryQlClass() { result = "CatchClause" } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/StmtContainers.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/StmtContainers.qll new file mode 100644 index 000000000000..2dfbe01fad46 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/StmtContainers.qll @@ -0,0 +1,49 @@ +/** + * INTERNAL. DO NOT IMPORT DIRECTLY. + * + * Provides predicates and classes for relating nodes to their + * enclosing `StmtContainer`. + */ +overlay[local?] +module; + +private import minimal + +cached +private StmtContainer getStmtContainer(NodeInStmtContainer node) { + expr_containers(node, result) + or + stmt_containers(node, result) + or + // Properties + exists(AstNode parent | properties(node, parent, _, _, _) | + expr_containers(parent, result) + or + stmt_containers(parent, result) + ) + or + // Synthetic CFG nodes + entry_cfg_node(node, result) + or + exit_cfg_node(node, result) + or + exists(Expr test | + guard_node(node, _, test) and + expr_containers(test, result) + ) +} + +/** + * A node that occurs inside a function or top-level or is itself a top-level. + * + * Specifically, this is the union type of `ControlFlowNode`, `TypeAnnotation`, + * and `TopLevel`. + */ +class NodeInStmtContainer extends Locatable, @node_in_stmt_container { + /** + * Gets the function or toplevel to which this node belongs. + */ + overlay[caller?] + pragma[inline] + final StmtContainer getContainer() { result = getStmtContainer(this) } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TSConfig.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TSConfig.qll new file mode 100644 index 000000000000..67398b2d98f2 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TSConfig.qll @@ -0,0 +1,222 @@ +/** + * Provides a class for working with `tsconfig.json` files. + */ + +private import minimal +private import PathMapping + +/** + * A TypeScript configuration file, usually named `tsconfig.json`. + */ +class TSConfig extends JsonObject { + TSConfig() { + this.getJsonFile().getBaseName().matches("%tsconfig%.json") and + this.isTopLevel() + } + + /** Gets the folder containing this file. */ + Folder getFolder() { result = this.getJsonFile().getParentContainer() } + + /** Gets the `compilerOptions` object. */ + JsonObject getCompilerOptions() { result = this.getPropValue("compilerOptions") } + + /** Gets the string value in the `extends` property. */ + string getExtendsPath() { result = this.getPropStringValue("extends") } + + /** Gets the file referred to by the `extends` property. */ + File getExtendedFile() { result = Resolver::resolve(this.getFolder(), this.getExtendsPath()) } + + /** Gets the `TSConfig` file referred to by the `extends` property. */ + TSConfig getExtendedTSConfig() { result.getJsonFile() = this.getExtendedFile() } + + /** Gets the string value in the `baseUrl` property. */ + string getBaseUrlPath() { result = this.getCompilerOptions().getPropStringValue("baseUrl") } + + /** Gets the folder referred to by the `baseUrl` property in this file, not taking `extends` into account. */ + Folder getOwnBaseUrlFolder() { + result = Resolver::resolve(this.getFolder(), this.getBaseUrlPath()) + } + + /** Gets the effective baseUrl folder for this tsconfig file. */ + Folder getBaseUrlFolder() { + result = this.getOwnBaseUrlFolder() + or + not exists(this.getOwnBaseUrlFolder()) and + result = this.getExtendedTSConfig().getBaseUrlFolder() + } + + /** Gets the effective baseUrl folder for this tsconfig file, or its enclosing folder if there is no baseUrl. */ + Folder getBaseUrlFolderOrOwnFolder() { + result = this.getBaseUrlFolder() + or + not exists(this.getBaseUrlFolder()) and + result = this.getFolder() + } + + /** Gets a path mentioned in the `include` property. */ + string getAnIncludePath() { + result = this.getPropStringValue("include") + or + result = this.getPropValue("include").(JsonArray).getElementStringValue(_) + } + + /** + * Gets a file or folder refenced by a path the `include` property, possibly + * inherited from an extended tsconfig file. + * + * Does not include all the files within includes directories, use `getAnIncludedContainer` for that. + */ + Container getAnIncludePathTarget() { + result = Resolver::resolve(this.getFolder(), this.getAnIncludePath()) + or + not exists(this.getPropValue("include")) and + result = this.getExtendedTSConfig().getAnIncludePathTarget() + } + + /** + * Gets a file or folder inside the directory tree mentioned in the `include` property. + */ + Container getAnIncludedContainer() { + result = this.getAnIncludePathTarget() + or + result = this.getAnIncludedContainer().getAChildContainer() + } + + /** Gets the path mentioned in the `rootDir` property. */ + string getRootDirPath() { result = this.getCompilerOptions().getPropStringValue("rootDir") } + + private Container getOwnRootDir() { + result = Resolver::resolve(this.getFolder(), this.getRootDirPath()) + } + + /** Gets the file or folder referenced by the `rootDir` property. */ + Container getRootDir() { + result = this.getOwnRootDir() + or + not exists(this.getRootDirPath()) and + result = this.getExtendedTSConfig().getOwnRootDir() + } + + private string getATopLevelIncludePath() { + result = this.getAnIncludePath().(FilePath).getComponent(0) + } + + private string getUniqueTopLevelIncludePath() { + result = unique( | | this.getATopLevelIncludePath()) + } + + /** + * Gets the folder referred to by the `rootDir` property, or if absent, an effective root dir + * derived from `include` paths. + */ + Container getEffectiveRootDir() { + result = this.getRootDir() + or + not exists(this.getRootDir()) and + ( + result = this.getFolder().getFolder(this.getUniqueTopLevelIncludePath()) + or + not exists(this.getUniqueTopLevelIncludePath()) and + exists(this.getATopLevelIncludePath()) and + result = this.getFolder() + or + not exists(this.getATopLevelIncludePath()) and + result = this.getExtendedTSConfig().getEffectiveRootDir() + ) + } + + private JsonObject getPathMappings() { result = this.getCompilerOptions().getPropValue("paths") } + + /** + * Holds if this has a path mapping from `pattern` to `newPath`. + * + * For example, `"paths": { "@/*": "./src/*" }` maps the `@/*` pattern to `./src/*`. + * + * Does not include path mappings from extended tsconfig files. + */ + predicate hasPathMapping(string pattern, string newPath) { + this.getPathMappings().getPropStringValue(pattern) = newPath + or + this.getPathMappings().getPropValue(pattern).(JsonArray).getElementStringValue(_) = newPath + } + + /** + * Holds if this has an exact path mapping (i.e. no wildcards) from `pattern` to `newPath`. + * + * For example, `"paths": { "@": "./src/index.ts" }` maps the `@` path to `./src/index.ts`. + * + * Does not include path mappings from extended tsconfig files. + */ + predicate hasExactPathMapping(string pattern, string newPath) { + this.hasPathMapping(pattern, newPath) and + not pattern.matches("%*%") + } + + /** + * Holds if this has a path mapping from the `pattern` prefix to the `newPath` prefix. + * The trailing `*` is not included. + * + * For example, `"paths": { "@/*": "./src/*" }` maps the `@/` pattern to `./src/`. + * + * Does not include path mappings from extended tsconfig files. + */ + predicate hasPrefixPathMapping(string pattern, string newPath) { + this.hasPathMapping(pattern + "*", newPath + "*") + } +} + +/** For resolving paths in a tsconfig file, except `paths` mappings. */ +private module ResolverConfig implements Folder::ResolveSig { + predicate shouldResolve(Container base, string path) { + exists(TSConfig cfg | + base = cfg.getFolder() and + path = + [cfg.getExtendsPath(), cfg.getBaseUrlPath(), cfg.getRootDirPath(), cfg.getAnIncludePath()] + ) + } + + predicate allowGlobs() { any() } // "include" can use globs +} + +private module Resolver = Folder::Resolve; + +/** + * Gets a tsconfig file to use as fallback for handling paths in `c`. + * + * This holds for files and folders where no tsconfig seems to include it, + * but it has one or more tsconfig files in parent directories. + */ +private TSConfig getFallbackTSConfig(Container c) { + not c = any(TSConfig t).getAnIncludedContainer() and + ( + c = result.getFolder() + or + result = getFallbackTSConfig(c.getParentContainer()) + ) +} + +private class TSConfigPathMapping extends PathMapping, TSConfig { + override File getAnAffectedFile() { + result = this.getAnIncludedContainer() + or + this = getFallbackTSConfig(result) + } + + override predicate hasExactPathMapping(string pattern, Container newContext, string newPath) { + exists(TSConfig tsconfig | + tsconfig = this.getExtendedTSConfig*() and + tsconfig.hasExactPathMapping(pattern, newPath) and + newContext = tsconfig.getBaseUrlFolderOrOwnFolder() + ) + } + + override predicate hasPrefixPathMapping(string pattern, Container newContext, string newPath) { + exists(TSConfig tsconfig | + tsconfig = this.getExtendedTSConfig*() and + tsconfig.hasPrefixPathMapping(pattern, newPath) and + newContext = tsconfig.getBaseUrlFolderOrOwnFolder() + ) + } + + override predicate hasBaseUrl(Container base) { base = this.getBaseUrlFolder() } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Templates.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Templates.qll new file mode 100644 index 000000000000..72115999ba1c --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Templates.qll @@ -0,0 +1,106 @@ +/** Provides classes for working with ECMAScript 2015-style template expressions. */ +overlay[local?] +module; + +import minimal + +/** + * A tagged template literal expression. + * + * Example: + * + * ``` + * highlight `Hello, ${user.name}!` + * ``` + */ +class TaggedTemplateExpr extends Expr, @tagged_template_expr { + /** Gets the tagging expression of this tagged template. */ + Expr getTag() { result = this.getChildExpr(0) } + + /** Gets the tagged template itself. */ + TemplateLiteral getTemplate() { result = this.getChildExpr(1) } + + /** Gets the `i`th type argument to the tag of this template literal. */ + TypeExpr getTypeArgument(int i) { i >= 0 and result = this.getChildTypeExpr(2 + i) } + + /** Gets a type argument of the tag of this template literal. */ + TypeExpr getATypeArgument() { result = this.getTypeArgument(_) } + + /** Gets the number of type arguments appearing on the tag of this template literal. */ + int getNumTypeArgument() { result = count(this.getATypeArgument()) } + + override predicate isImpure() { any() } + + override string getAPrimaryQlClass() { result = "TaggedTemplateExpr" } +} + +/** + * A template literal. + * + * Example: + * + * ``` + * `Hello, ${user.name}!` + * ``` + */ +class TemplateLiteral extends Expr, @template_literal { + /** + * Gets the `i`th element of this template literal, which may either + * be an interpolated expression or a constant template element. + */ + Expr getElement(int i) { result = this.getChildExpr(i) } + + /** + * Gets an element of this template literal. + */ + Expr getAnElement() { result = this.getElement(_) } + + /** + * Gets the number of elements of this template literal. + */ + int getNumElement() { result = count(this.getAnElement()) } + + overlay[global] + override predicate isImpure() { this.getAnElement().isImpure() } + + override string getAPrimaryQlClass() { result = "TemplateLiteral" } +} + +/** + * A constant template element. + * + * Example: + * + * ``` + * `Hello, ${user.name}!` // "Hello, " and "!" are constant template elements + * ``` + */ +class TemplateElement extends Expr, @template_element { + /** + * Holds if this template element has a "cooked" value. + * + * Starting with ECMAScript 2018, tagged template literals may contain + * elements with invalid escape sequences, which only have a raw value but + * no cooked value. + */ + predicate hasValue() { exists(this.getValue()) } + + /** + * Gets the "cooked" value of this template element, if any. + * + * Starting with ECMAScript 2018, tagged template literals may contain + * elements with invalid escape sequences, which only have a raw value but + * no cooked value. + */ + string getValue() { + // empty string means no cooked value + literals(result, _, this) and result != "" + } + + /** Gets the raw value of this template element. */ + string getRawValue() { literals(_, result, this) } + + override predicate isImpure() { none() } + + override string getAPrimaryQlClass() { result = "TemplateElement" } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Tokens.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Tokens.qll new file mode 100644 index 000000000000..6e088166cc09 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Tokens.qll @@ -0,0 +1,142 @@ +/** + * Provides classes for working with the token-based representation of JavaScript programs. + */ +overlay[local?] +module; + +import minimal + +pragma[nomagic] +private predicate adjacentTokens(Token token1, Token token2) { + exists(TopLevel top, int index | + tokeninfo(token1, _, top, index, _) and + tokeninfo(token2, _, top, index + 1, _) + ) +} + +/** + * A token occurring in a piece of JavaScript source code. + * + * Examples: + * + * ``` + * x + * /\w+/ + * 12.3 + * ; + * ``` + */ +class Token extends Locatable, @token { + /** Gets the toplevel syntactic structure to which this token belongs. */ + TopLevel getTopLevel() { tokeninfo(this, _, result, _, _) } + + /** Gets the index of the token inside its toplevel structure. */ + int getIndex() { tokeninfo(this, _, _, result, _) } + + /** Gets the source text of this token. */ + string getValue() { tokeninfo(this, _, _, _, result) } + + /** Gets the token following this token inside the same toplevel structure, if any. */ + Token getNextToken() { adjacentTokens(this, result) } + + /** Gets the token preceding this token inside the same toplevel structure, if any. */ + Token getPreviousToken() { result.getNextToken() = this } + + override string toString() { result = this.getValue() } +} + +/** An end-of-file token. */ +class EOFToken extends Token, @token_eof { } + +/** + * A null literal token. + * + * Example: + * + * ``` + * null + * ``` + */ +class NullLiteralToken extends Token, @token_null_literal { } + +/** + * A Boolean literal token. + * + * Examples: + * + * ``` + * true + * false + * ``` + */ +class BooleanLiteralToken extends Token, @token_boolean_literal { } + +/** + * A numeric literal token. + * + * Examples: + * + * ``` + * 1 + * 2.3 + * ``` + */ +class NumericLiteralToken extends Token, @token_numeric_literal { } + +/** + * A string literal token. + * + * Examples: + * + * ``` + * "hello" + * 'world!' + * ``` + */ +class StringLiteralToken extends Token, @token_string_literal { } + +/** + * A regular expression literal token. + * + * Example: + * + * ``` + * /\w+/ + * ``` + */ +class RegularExpressionToken extends Token, @token_regular_expression { } + +/** + * An identifier token. + * + * Example: + * + * ``` + * x + * ``` + */ +class IdentifierToken extends Token, @token_identifier { } + +/** + * A keyword token. + * + * Examples: + * + * ``` + * function + * this + * ``` + */ +class KeywordToken extends Token, @token_keyword { } + +/** + * A punctuator token. + * + * Examples: + * + * ``` + * ; + * + + * ``` + */ +class PunctuatorToken extends Token, @token_punctuator { } diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TypeAnnotations.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TypeAnnotations.qll new file mode 100644 index 000000000000..c14ada9f301d --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TypeAnnotations.qll @@ -0,0 +1,89 @@ +/** + * Provides classes for reasoning about type annotations independently of dialect. + */ +overlay[local?] +module; + +import minimal +private import StmtContainers + +/** + * A type annotation, either in the form of a TypeScript type or a JSDoc comment. + */ +class TypeAnnotation extends @type_annotation, NodeInStmtContainer { + /** Holds if this is the `any` type. */ + predicate isAny() { none() } + + /** Holds if this is the `string` type. Does not hold for the (rarely used) `String` type. */ + predicate isString() { none() } + + /** Holds if this is the `string` or `String` type. */ + predicate isStringy() { none() } + + /** Holds if this is the `number` type. Does not hold for the (rarely used) `Number` type. */ + predicate isNumber() { none() } + + /** Holds if this is the `number` or `Number`s type. */ + predicate isNumbery() { none() } + + /** Holds if this is the `boolean` type. Does not hold for the (rarely used) `Boolean` type. */ + predicate isBoolean() { none() } + + /** Holds if this is the `boolean` or `Boolean` type. */ + predicate isBooleany() { none() } + + /** Holds if this is the `undefined` type. */ + predicate isUndefined() { none() } + + /** Holds if this is the `null` type. */ + predicate isNull() { none() } + + /** Holds if this is the `void` type. */ + predicate isVoid() { none() } + + /** Holds if this is the `never` type, or an equivalent type representing the empty set of values. */ + predicate isNever() { none() } + + /** Holds if this is the `this` type. */ + predicate isThis() { none() } + + /** Holds if this is the `symbol` type. */ + predicate isSymbol() { none() } + + /** Holds if this is the `unique symbol` type. */ + predicate isUniqueSymbol() { none() } + + /** Holds if this is the `Function` type. */ + predicate isRawFunction() { none() } + + /** Holds if this is the `object` type. */ + predicate isObjectKeyword() { none() } + + /** Holds if this is the `unknown` type. */ + predicate isUnknownKeyword() { none() } + + /** Holds if this is the `bigint` type. */ + predicate isBigInt() { none() } + + /** Holds if this is the `const` keyword, occurring in a type assertion such as `x as const`. */ + predicate isConstKeyword() { none() } + + /** + * Repeatedly unfolds unions, intersections, parentheses, and nullability/readonly modifiers and gets any of the underlying types, + * or this type itself if it cannot be unfolded. + * + * Note that this only unfolds the syntax of the type annotation. Type aliases are not followed to their definition. + */ + TypeAnnotation getAnUnderlyingType() { result = this } + + /** Gets the statement in which this type appears. */ + Stmt getEnclosingStmt() { none() } + + /** Gets the function in which this type appears, if any. */ + Function getEnclosingFunction() { none() } + + /** + * Gets the top-level containing this type annotation. + */ + TopLevel getTopLevel() { none() } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TypeScript.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TypeScript.qll new file mode 100644 index 000000000000..6af68ba2fd6f --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/TypeScript.qll @@ -0,0 +1,1655 @@ +overlay[local?] +module; + +import minimal + +/** + * A statement that defines a namespace, that is, a namespace declaration or enum declaration. + * + * Declarations that declare an alias for a namespace (i.e. an import) are not + * considered to be namespace definitions. + */ +class NamespaceDefinition extends Stmt, @namespace_definition, AST::ValueNode { + /** + * Gets the identifier naming the namespace. + */ + Identifier getIdentifier() { none() } // Overridden in subtypes. + + /** + * Gets unqualified name of the namespace being defined. + */ + string getName() { + result = this.(NamespaceDeclaration).getName() + or + result = this.(EnumDeclaration).getName() + } + + /** + * Gets the local namespace name induced by this namespace. + */ + LocalNamespaceName getLocalNamespaceName() { + result = this.getIdentifier().(LocalNamespaceDecl).getLocalNamespaceName() + } +} + +/** + * A TypeScript namespace declaration. + * + * This is also known as an "internal module" and can be declared + * using the `module` or `namespace` keyword. For example: + * ``` + * namespace util { ... } + * module util { ... } // equivalent + * ``` + * + * Note that modules whose name is a string literal, called "external modules", + * and are not namespace declarations. + * For example, `declare module "X" {...}` is an external module declaration. + * These are represented by `ExternalModuleDeclaration`. + */ +class NamespaceDeclaration extends NamespaceDefinition, StmtContainer, @namespace_declaration { + /** Gets the name of this namespace. */ + override Identifier getIdentifier() { result = this.getChildExpr(-1) } + + /** Gets the name of this namespace as a string. */ + override string getName() { result = this.getIdentifier().getName() } + + /** Gets the `i`th statement in this namespace. */ + Stmt getStmt(int i) { + i >= 0 and + result = this.getChildStmt(i) + } + + /** Gets a statement in this namespace. */ + override Stmt getAStmt() { result = this.getStmt(_) } + + /** Gets the number of statements in this namespace. */ + int getNumStmt() { result = count(this.getAStmt()) } + + override StmtContainer getEnclosingContainer() { result = this.getContainer() } + + /** + * Holds if this declaration implies the existence of a concrete namespace object at runtime. + * + * A namespace that is empty or only contains interfaces and type aliases is not instantiated, + * and thus has no namespace object at runtime and is not associated with a variable. + */ + predicate isInstantiated() { is_instantiated(this) } + + override ControlFlowNode getFirstControlFlowNode() { + if has_declare_keyword(this) then result = this else result = this.getIdentifier() + } + + override string getAPrimaryQlClass() { result = "NamespaceDeclaration" } +} + +/** + * A statement that defines a named type, that is, a class, interface, type alias, or enum declaration. + * + * Note that imports and type parameters are not type definitions. Consider using `TypeDecl` to capture + * a wider class of type declarations. + */ +class TypeDefinition extends AstNode, @type_definition { + /** + * Gets the identifier naming the type. + */ + Identifier getIdentifier() { + result = this.(ClassDefinition).getIdentifier() or + result = this.(InterfaceDeclaration).getIdentifier() or + result = this.(TypeAliasDeclaration).getIdentifier() or + result = this.(EnumDeclaration).getIdentifier() or + result = this.(EnumMember).getIdentifier() + } + + /** + * Gets the unqualified name of the type being defined. + */ + string getName() { result = this.getIdentifier().getName() } + + override string getAPrimaryQlClass() { result = "TypeDefinition" } +} + +/** + * A TypeScript declaration of form `declare module "X" {...}` where `X` + * is the name of an external module. + */ +class ExternalModuleDeclaration extends Stmt, StmtContainer, @external_module_declaration { + /** + * Gets the string literal denoting the module name, such as `"fs"` in: + * ``` + * declare module "fs" {...} + * ``` + */ + StringLiteral getNameLiteral() { result = this.getChildExpr(-1) } + + /** + * Gets the module name, such as `fs` in: + * ``` + * declare module "fs" {...} + * ``` + */ + string getName() { result = this.getNameLiteral().getStringValue() } + + /** Gets the `i`th statement in this namespace. */ + Stmt getStmt(int i) { + i >= 0 and + result = this.getChildStmt(i) + } + + /** Gets a statement in this namespace. */ + override Stmt getAStmt() { result = this.getStmt(_) } + + /** Gets the number of statements in this namespace. */ + int getNumStmt() { result = count(this.getAStmt()) } + + override StmtContainer getEnclosingContainer() { result = this.getContainer() } + + override string getAPrimaryQlClass() { result = "ExternalModuleDeclaration" } +} + +/** + * A TypeScript declaration of form `declare global {...}`. + */ +class GlobalAugmentationDeclaration extends Stmt, StmtContainer, @global_augmentation_declaration { + /** Gets the `i`th statement in this namespace. */ + Stmt getStmt(int i) { + i >= 0 and + result = this.getChildStmt(i) + } + + /** Gets a statement in this namespace. */ + override Stmt getAStmt() { result = this.getStmt(_) } + + /** Gets the number of statements in this namespace. */ + int getNumStmt() { result = count(this.getAStmt()) } + + override StmtContainer getEnclosingContainer() { result = this.getContainer() } + + override string getAPrimaryQlClass() { result = "GlobalAugmentationDeclaration" } +} + +/** A TypeScript "import-equals" declaration. */ +class ImportEqualsDeclaration extends Stmt, @import_equals_declaration { + /** Gets the name under which the imported entity is imported. */ + Identifier getIdentifier() { result = this.getChildExpr(0) } + + /** Gets the expression specifying the imported module or entity. */ + Expr getImportedEntity() { result = this.getChildExpr(1) } + + override ControlFlowNode getFirstControlFlowNode() { result = this.getIdentifier() } + + override string getAPrimaryQlClass() { result = "ImportEqualsDeclaration" } +} + +/** + * A `require()` call in a TypeScript import-equals declaration, such as `require("foo")` in: + * ``` + * import foo = require("foo"); + * ``` + * + * These are special in that they may only occur in an import-equals declaration, + * and the compiled output depends on the `--module` flag passed to the + * TypeScript compiler. + */ +class ExternalModuleReference extends Expr, @external_module_reference { + /** Gets the expression specifying the module. */ + Expr getExpression() { result = this.getChildExpr(0) } + + Expr getImportedPathExpr() { result = this.getExpression() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getExpression().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "ExternalModuleReference" } +} + +/** A TypeScript "export-assign" declaration. */ +class ExportAssignDeclaration extends Stmt, @export_assign_declaration { + /** Gets the expression exported by this declaration. */ + Expr getExpression() { result = this.getChildExpr(0) } + + override string getAPrimaryQlClass() { result = "ExportAssignDeclaration" } +} + +/** A TypeScript export of form `export as namespace X` where `X` is an identifier. */ +class ExportAsNamespaceDeclaration extends Stmt, @export_as_namespace_declaration { + /** + * Gets the `X` in `export as namespace X`. + */ + Identifier getIdentifier() { result = this.getChildExpr(0) } + + override string getAPrimaryQlClass() { result = "ExportAsNamespaceDeclaration" } +} + +/** + * A type alias declaration, that is, a statement of form `type A = T`. + */ +class TypeAliasDeclaration extends @type_alias_declaration, TypeParameterized, Stmt { + /** Gets the name of this type alias as a string. */ + string getName() { result = this.getIdentifier().getName() } + + Identifier getIdentifier() { result = this.getChildTypeExpr(0) } + + override TypeParameter getTypeParameter(int n) { result = this.getChildTypeExpr(n + 2) } + + /** + * Gets the `T` in `type A = T`. + */ + TypeExpr getDefinition() { result = this.getChildTypeExpr(1) } + + override string describe() { result = "type alias " + this.getName() } + + override string getAPrimaryQlClass() { result = "TypeAliasDeclaration" } +} + +/** + * A TypeScript interface declaration, inline interface type, or function type. + */ +class InterfaceDefinition extends @interface_definition, ClassOrInterface { + override predicate isAbstract() { any() } + + override string getAPrimaryQlClass() { result = "InterfaceDefinition" } +} + +/** A TypeScript interface declaration. */ +class InterfaceDeclaration extends Stmt, InterfaceDefinition, @interface_declaration { + override Identifier getIdentifier() { result = this.getChildTypeExpr(0) } + + override TypeParameter getTypeParameter(int n) { + exists(int astIndex | typeexprs(result, _, this, astIndex, _) | + astIndex <= -2 and astIndex % 2 = 0 and n = -(astIndex + 2) / 2 + ) + } + + override string describe() { result = "interface " + this.getName() } + + /** + * Gets the `n`th type from the `extends` clause of this interface, starting at 0. + */ + override TypeExpr getSuperInterface(int n) { + exists(int astIndex | typeexprs(result, _, this, astIndex, _) | + astIndex <= -1 and astIndex % 2 = -1 and n = -(astIndex + 1) / 2 + ) + } + + /** + * Gets any type from the `extends` clause of this interface. + */ + override TypeExpr getASuperInterface() { result = InterfaceDefinition.super.getASuperInterface() } + + override string getAPrimaryQlClass() { result = "InterfaceDeclaration" } +} + +/** An inline TypeScript interface type, such as `{x: number; y: number}`. */ +class InterfaceTypeExpr extends TypeExpr, InterfaceDefinition, @interface_typeexpr { + override Identifier getIdentifier() { none() } + + override string describe() { result = "anonymous interface" } + + override string getAPrimaryQlClass() { result = "InterfaceTypeExpr" } +} + +/** + * A TypeScript function type, such as `(x: string) => number` or a + * constructor type such as `new (x: string) => Object`. + */ +class FunctionTypeExpr extends TypeExpr, @function_typeexpr { + /** Holds if this is a constructor type, such as `new (x: string) => Object`. */ + predicate isConstructor() { this instanceof ConstructorTypeExpr } + + /** Gets the function AST node that holds the parameters and return type. */ + FunctionExpr getFunction() { result = this.getChildExpr(0) } + + /** Gets the `n`th parameter of this function type. */ + Parameter getParameter(int n) { result = this.getFunction().getParameter(n) } + + /** Gets any of the parameters of this function type. */ + Parameter getAParameter() { result = this.getFunction().getAParameter() } + + /** Gets the number of parameters of this function type. */ + int getNumParameter() { result = this.getFunction().getNumParameter() } + + /** Gets the `n`th type parameter of this function type. */ + TypeParameter getTypeParameter(int n) { result = this.getFunction().getTypeParameter(n) } + + /** Gets any of the type parameters of this function type. */ + TypeParameter getATypeParameter() { result = this.getFunction().getATypeParameter() } + + /** Gets the number of type parameters of this function type. */ + int getNumTypeParameter() { result = this.getFunction().getNumTypeParameter() } + + /** Gets the return type of this function type, if any. */ + TypeExpr getReturnTypeAnnotation() { result = this.getFunction().getReturnTypeAnnotation() } + + override string getAPrimaryQlClass() { result = "FunctionTypeExpr" } +} + +/** A constructor type, such as `new (x: string) => Object`. */ +class ConstructorTypeExpr extends FunctionTypeExpr, @constructor_typeexpr { } + +/** A function type that is not a constructor type, such as `(x: string) => number`. */ +class PlainFunctionTypeExpr extends FunctionTypeExpr, @plain_function_typeexpr { } + +/** A possibly qualified identifier that declares or refers to a type. */ +abstract class TypeRef extends AstNode { } + +/** An identifier declaring a type name, that is, the name of a class, interface, type parameter, or import. */ +class TypeDecl extends Identifier, TypeRef, LexicalDecl { + TypeDecl() { + this = any(ClassOrInterface ci).getIdentifier() or + this = any(TypeParameter tp).getIdentifier() or + this = any(ImportSpecifier im).getLocal() or + this = any(ImportEqualsDeclaration im).getIdentifier() or + this = any(TypeAliasDeclaration td).getIdentifier() or + this = any(EnumDeclaration ed).getIdentifier() or + this = any(EnumMember member).getIdentifier() + } + + /** + * Gets the local type name being declared. + * + * If this is an import or type alias, the local type name represents the local alias. + */ + LocalTypeName getLocalTypeName() { result.getADeclaration() = this } + + /** + * Gets a string describing the type being declared, consisting of the declaration kind and + * the name being declared, such as `class C` for a class declaration `C`. + */ + string describe() { + exists(ClassOrInterface ci | this = ci.getIdentifier() | result = ci.describe()) + or + exists(TypeParameter tp | this = tp.getIdentifier() | + result = "type parameter " + this.getName() + ) + or + exists(TypeAliasDeclaration td | this = td.getIdentifier() | + result = "type alias " + this.getName() + ) + or + exists(EnumDeclaration enum | this = enum.getIdentifier() | result = "enum " + this.getName()) + or + exists(EnumMember member | this = member.getIdentifier() | + result = "enum member " + member.getPrefixedName() + ) + or + exists(ImportSpecifier im | this = im.getLocal() | result = "imported type " + this.getName()) + } +} + +/** + * The local name for a type in a particular scope. + * + * It is possible for two distinct local type names to refer to the same underlying + * type through imports or type aliases. For example: + * ``` + * namespace A { + * export class C {} + * } + * namespace B { + * import C = A.C; + * } + * ``` + * In the above example, two distinct local type names exist for the type `C`: + * one in `A` and one in `B`. + * Since a local type name is always specific to one scope, it is not possible + * for the two namespaces to share a single local type name for `C`. + * + * There may be multiple declarations of a given local type name, for example: + * ``` + * interface Point { x: number; } + * interface Point { y: number; } + * ``` + * In the above example, the two declarations of `Point` refer to the same + * local type name. + */ +class LocalTypeName extends @local_type_name, LexicalName { + /** Gets the local name of this type. */ + override string getName() { local_type_names(this, result, _) } + + /** Gets the scope this type name is declared in. */ + override Scope getScope() { local_type_names(this, _, result) } + + /** Gets a textual representation of this element. */ + override string toString() { result = this.getName() } + + /** + * Gets a declaration of this type name. + * + * All local type names have at least one declaration and may have + * multiple declarations in case these are interface declarations. + */ + TypeDecl getADeclaration() { typedecl(result, this) } + + /** + * Gets the first declaration of this type name. + */ + TypeDecl getFirstDeclaration() { + result = + min(this.getADeclaration() as decl + order by + decl.getLocation().getStartLine(), decl.getLocation().getStartColumn() + ) + } + + /** Gets a use of this type name in a type annotation. */ + LocalTypeAccess getATypeAccess() { typebind(result, this) } + + /** Gets a use of this type name in an export. */ + ExportVarAccess getAnExportAccess() { typebind(result, this) } + + /** Gets an identifier that refers to this type name. */ + Identifier getAnAccess() { typebind(result, this) } + + override DeclarationSpace getDeclarationSpace() { result = "type" } +} + +/** + * The local name for a namespace in a particular scope. + * + * Namespace declarations and imports can give rise to local namespace names. + * For example, the following declarations declare two local namespace names, + * `A` and `B`: + * ``` + * import A from './A'; + * namespace B {} + * ``` + * + * It is possible for a namespace to have multiple aliases; each alias corresponds + * to a distinct local namespace name. For example, there are three distinct local + * namespace names for `A` in this example: + * ``` + * namespace A {} + * namespace Q { + * import B = A; + * import C = A; + * } + * ``` + * There is one local namespace name for the declaration of `A` and one for each import. + */ +class LocalNamespaceName extends @local_namespace_name, LexicalName { + /** Gets the local name of this namespace. */ + override string getName() { local_namespace_names(this, result, _) } + + /** Gets the scope this namespace name is declared in. */ + override Scope getScope() { local_namespace_names(this, _, result) } + + /** Gets a textual representation of this element. */ + override string toString() { result = this.getName() } + + /** + * Gets a declaration of this namespace name. + * + * All local namespace names have at least one declaration and may have + * multiple declarations unless it comes from an import. + */ + LocalNamespaceDecl getADeclaration() { namespacedecl(result, this) } + + /** + * Gets the first declaration of this namespace name. + */ + LocalNamespaceDecl getFirstDeclaration() { + result = + min(this.getADeclaration() as decl + order by + decl.getLocation().getStartLine(), decl.getLocation().getStartColumn() + ) + } + + /** Gets a use of this namespace name in a type annotation. */ + LocalNamespaceAccess getATypeAccess() { namespacebind(result, this) } + + /** Gets a use of this namespace in an export. */ + ExportVarAccess getAnExportAccess() { namespacebind(result, this) } + + /** + * Gets an access to a type member of this namespace alias, + * such as `http.ServerRequest` where `http` is a reference to this namespace. + */ + QualifiedTypeAccess getAMemberAccess(string member) { + result.getIdentifier().getName() = member and + result.getQualifier() = this.getAnAccess() + } + + /** Gets an identifier that refers to this namespace name. */ + Identifier getAnAccess() { namespacebind(result, this) } + + override DeclarationSpace getDeclarationSpace() { result = "namespace" } +} + +/** + * A type expression, that is, an AST node that is part of a TypeScript type annotation. + * + * This class includes only explicit type annotations - + * types inferred by the TypeScript compiler are not type expressions. + */ +class TypeExpr extends ExprOrType, @typeexpr, TypeAnnotation { + override string toString() { typeexprs(this, _, _, _, result) } + + override Stmt getEnclosingStmt() { result = ExprOrType.super.getEnclosingStmt() } + + override Function getEnclosingFunction() { result = ExprOrType.super.getEnclosingFunction() } + + override TopLevel getTopLevel() { result = ExprOrType.super.getTopLevel() } +} + +/** + * Classes that are internally represented as a keyword type. + */ +private class KeywordTypeExpr extends @keyword_typeexpr, TypeExpr { + string getName() { literals(result, _, this) } + + override predicate isAny() { this.getName() = "any" } + + override predicate isString() { this.getName() = "string" } + + override predicate isStringy() { this.getName() = "string" } + + override predicate isNumber() { this.getName() = "number" } + + override predicate isNumbery() { this.getName() = "number" } + + override predicate isBoolean() { this.getName() = "boolean" } + + override predicate isBooleany() { this.getName() = "boolean" } + + override predicate isUndefined() { this.getName() = "undefined" } + + override predicate isNull() { this.getName() = "null" } + + override predicate isVoid() { this.getName() = "void" } + + override predicate isNever() { this.getName() = "never" } + + override predicate isThis() { this.getName() = "this" } + + override predicate isSymbol() { this.getName() = "symbol" } + + override predicate isUniqueSymbol() { this.getName() = "unique symbol" } + + override predicate isObjectKeyword() { this.getName() = "object" } + + override predicate isUnknownKeyword() { this.getName() = "unknown" } + + override predicate isBigInt() { this.getName() = "bigint" } + + override predicate isConstKeyword() { this.getName() = "const" } + + override string getAPrimaryQlClass() { result = "KeywordTypeExpr" } +} + +/** + * A use of the predefined type `any`, `string`, `number`, `boolean`, `null`, `undefined`, `void`, `never`, `symbol`, or `object`. + */ +class PredefinedTypeExpr extends KeywordTypeExpr { + PredefinedTypeExpr() { + not this.isThis() // The only keyword type that we don't consider a "predefined" type + } +} + +/** + * A use of the `this` type. + */ +class ThisTypeExpr extends KeywordTypeExpr { + ThisTypeExpr() { this.isThis() } +} + +/** + * A possibly qualified name that is used as part of a type, such as `Date` or `http.ServerRequest`. + * + * Type arguments are not part of a type access but there are convenience methods in this class + * for accessing them. + */ +class TypeAccess extends @typeaccess, TypeExpr, TypeRef { + /** Gets the identifier naming the type without any qualifier, such as `ServerRequest` in `http.ServerRequest`. */ + Identifier getIdentifier() { none() } + + /** + * Gets the enclosing generic type expression providing type arguments to this type, if any. + * + * For example, in `Array`, the `Array` part is a type access with `Array` + * being its enclosing generic type. + */ + GenericTypeExpr getAsGenericType() { result.getTypeAccess() = this } + + /** + * Holds if there are type arguments provided to this type by an enclosing generic type expression. + * + * For example, in `Array`, the `Array` part is a type access having type arguments. + */ + predicate hasTypeArguments() { exists(this.getAsGenericType()) } + + /** + * Gets the `n`th type argument provided to this type by the enclosing generic type expression, if any. + * + * For example, in `Array`, the `Array` part is a type access having the type argument `number`. + */ + TypeExpr getTypeArgument(int n) { result = this.getAsGenericType().getTypeArgument(n) } + + /** + * Gets a type argument provided to this type by the enclosing generic type expression, if any. + * + * For example, in `Array`, the `Array` part is a type access having the type argument `number`. + */ + TypeExpr getATypeArgument() { result = this.getAsGenericType().getATypeArgument() } + + /** + * Gets the number of type arguments provided to this type by the enclosing generic type expression, + * if any, or 0 otherwise. + * + * For example, in `Array`, the `Array` part is a type access having 1 type argument. + */ + int getNumTypeArgument() { + if exists(this.getAsGenericType()) + then result = this.getAsGenericType().getNumTypeArgument() + else result = 0 + } + + override string getAPrimaryQlClass() { result = "TypeAccess" } +} + +/** An identifier that is used as part of a type, such as `Date`. */ +class LocalTypeAccess extends @local_type_access, TypeAccess, Identifier, LexicalAccess { + override predicate isStringy() { this.getName() = "String" } + + override predicate isNumbery() { this.getName() = "Number" } + + override predicate isBooleany() { this.getName() = "Boolean" } + + override predicate isRawFunction() { this.getName() = "Function" } + + override Identifier getIdentifier() { result = this } + + /** + * Gets the local type name being referenced by this identifier, if any. + * + * Names that refer to a declaration in an external `d.ts` file, such as in + * the built-in `lib.d.ts` prelude, do not have a local typename. + * + * For example, in `Array`, the `Array` name will usually not have + * a local type name as it is declared in `lib.d.ts`. + */ + LocalTypeName getLocalTypeName() { result.getAnAccess() = this } + + private TypeAliasDeclaration getAlias() { + this.getLocalTypeName().getADeclaration() = result.getIdentifier() + } + + override TypeExpr getAnUnderlyingType() { + result = this.getAlias().getDefinition().getAnUnderlyingType() + or + not exists(this.getAlias()) and + result = this + } + + override string getAPrimaryQlClass() { result = "LocalTypeAccess" } +} + +/** + * A qualified name that is used as part of a type, such as `http.ServerRequest`. + */ +class QualifiedTypeAccess extends @qualified_type_access, TypeAccess { + /** + * Gets the qualifier in front of the name, such as `http` in `http.ServerRequest`. + * + * If the prefix consists of multiple identifiers, the qualifier is itself a qualified namespace access. + * For example, the qualifier of `lib.util.List` is `lib.util`. + */ + NamespaceAccess getQualifier() { result = this.getChildTypeExpr(0) } + + /** Gets the last identifier in the name, such as `ServerRequest` in `http.ServerRequest`. */ + override Identifier getIdentifier() { result = this.getChildTypeExpr(1) } +} + +/** + * A type consisting of a name and at least one type argument, such as `Array`. + * + * For convenience, the methods for accessing type arguments are also made available + * on the `TypeAccess` class. + */ +class GenericTypeExpr extends @generic_typeexpr, TypeExpr { + /** Gets the name of the type, such as `Array` in `Array`. */ + TypeAccess getTypeAccess() { result = this.getChildTypeExpr(-1) } + + /** Gets the `n`th type argument, starting at 0. */ + TypeExpr getTypeArgument(int n) { result = this.getChildTypeExpr(n) and n >= 0 } + + /** Gets any of the type arguments. */ + TypeExpr getATypeArgument() { result = this.getTypeArgument(_) } + + /** Gets the number of type arguments. This is always at least one. */ + int getNumTypeArgument() { result = count(this.getATypeArgument()) } + + override string getAPrimaryQlClass() { result = "GenericTypeExpr" } +} + +/** + * A string, number, or boolean literal used as a type. + * + * Note that the `null` and `undefined` types are considered predefined types, not literal types. + */ +class LiteralTypeExpr extends @literal_typeexpr, TypeExpr { + /** Gets the value of this literal, as a string. */ + string getValue() { literals(result, _, this) } + + /** + * Gets the raw source text of this literal, including quotes for + * string literals. + */ + string getRawValue() { literals(_, result, this) } + + override string getAPrimaryQlClass() { result = "LiteralTypeExpr" } +} + +/** A string literal used as a type. */ +class StringLiteralTypeExpr extends @string_literal_typeexpr, LiteralTypeExpr { } + +/** A number literal used as a type. */ +class NumberLiteralTypeExpr extends @number_literal_typeexpr, LiteralTypeExpr { + /** Gets the integer value of this literal type. */ + int getIntValue() { result = this.getValue().toInt() } +} + +/** A boolean literal used as a type. */ +class BooleanLiteralTypeExpr extends @boolean_literal_typeexpr, LiteralTypeExpr { + predicate isTrue() { this.getValue() = "true" } + + predicate isFalse() { this.getValue() = "false" } +} + +/** A bigint literal used as a TypeScript type annotation. */ +class BigIntLiteralTypeExpr extends @bigint_literal_typeexpr, LiteralTypeExpr { + /** Gets the integer value of the bigint literal, if it can be represented as a QL integer. */ + int getIntValue() { result = this.getValue().toInt() } + + /** + * Gets the floating point value of this literal if it can be represented + * as a QL floating point value. + */ + float getFloatValue() { result = this.getValue().toFloat() } +} + +/** + * An array type, such as `number[]`, or in general `T[]` where `T` is a type. + * + * This class includes only type expressions that use the notation `T[]`. + * Named types such as `Array` and tuple types such as `[number, string]` + * are not array type expressions. + */ +class ArrayTypeExpr extends @array_typeexpr, TypeExpr { + /** Gets the type of the array elements. */ + TypeExpr getElementType() { result = this.getChildTypeExpr(0) } + + override string getAPrimaryQlClass() { result = "ArrayTypeExpr" } +} + +private class RawUnionOrIntersectionTypeExpr = @union_typeexpr or @intersection_typeexpr; + +/** + * A union or intersection type, such as `string|number|boolean` or `A & B`. + */ +class UnionOrIntersectionTypeExpr extends RawUnionOrIntersectionTypeExpr, TypeExpr { + /** Gets the `n`th type in the union or intersection, starting at 0. */ + TypeExpr getElementType(int n) { result = this.getChildTypeExpr(n) } + + /** Gets any of the types in the union or intersection. */ + TypeExpr getAnElementType() { result = this.getElementType(_) } + + /** Gets the number of types in the union or intersection. This is always at least two. */ + int getNumElementType() { result = count(this.getAnElementType()) } + + override TypeExpr getAnUnderlyingType() { result = this.getAnElementType().getAnUnderlyingType() } +} + +/** + * A union type, such as `string|number|boolean`. + */ +class UnionTypeExpr extends @union_typeexpr, UnionOrIntersectionTypeExpr { + override string getAPrimaryQlClass() { result = "UnionTypeExpr" } +} + +/** + * A type of form `T[K]` where `T` and `K` are types. + */ +class IndexedAccessTypeExpr extends @indexed_access_typeexpr, TypeExpr { + /** Gets the type `T` in `T[K]`, denoting the object type whose properties are to be extracted. */ + TypeExpr getObjectType() { result = this.getChildTypeExpr(0) } + + /** Gets the type `K` in `T[K]`, denoting the property names to extract from the object type. */ + TypeExpr getIndexType() { result = this.getChildTypeExpr(1) } + + override string getAPrimaryQlClass() { result = "IndexedAccessTypeExpr" } +} + +/** + * A type of form `S&T`, denoting the intersection of type `S` and type `T`. + * + * In general, there are can more than two operands to an intersection type. + */ +class IntersectionTypeExpr extends @intersection_typeexpr, UnionOrIntersectionTypeExpr { + override string getAPrimaryQlClass() { result = "IntersectionTypeExpr" } +} + +/** + * A type expression enclosed in parentheses. + */ +class ParenthesizedTypeExpr extends @parenthesized_typeexpr, TypeExpr { + /** Gets the type inside the parentheses. */ + TypeExpr getElementType() { result = this.getChildTypeExpr(0) } + + override TypeExpr stripParens() { result = this.getElementType().stripParens() } + + override TypeExpr getAnUnderlyingType() { result = this.getElementType().getAnUnderlyingType() } + + override string getAPrimaryQlClass() { result = "ParenthesizedTypeExpr" } +} + +/** + * A tuple type such as `[number, string]`. + */ +class TupleTypeExpr extends @tuple_typeexpr, TypeExpr { + /** Gets the `n`th element type in the tuple, starting at 0. */ + TypeExpr getElementType(int n) { result = this.getChildTypeExpr(n) and n >= 0 } + + /** Gets any of the element types in the tuple. */ + TypeExpr getAnElementType() { result = this.getElementType(_) } + + /** Gets the number of elements in the tuple type. */ + int getNumElementType() { result = count(this.getAnElementType()) } + + /** + * Gets the name of the `n`th tuple member, starting at 0. + * Only has a result if the tuple members are named. + */ + Identifier getElementName(int n) { + // Type element names are at indices -1, -2, -3, ... + result = this.getChild(-(n + 1)) and n >= 0 + } + + override string getAPrimaryQlClass() { result = "TupleTypeExpr" } +} + +/** + * A type of form `keyof T` where `T` is a type. + */ +class KeyofTypeExpr extends @keyof_typeexpr, TypeExpr { + /** Gets the type `T` in `keyof T`, denoting the object type whose property names are to be extracted. */ + TypeExpr getElementType() { result = this.getChildTypeExpr(0) } + + override string getAPrimaryQlClass() { result = "KeyofTypeExpr" } +} + +/** + * A type of form `{ [K in C]: T }` where `K in C` declares a type parameter with `C` + * as the bound, and `T` is a type that may refer to `K`. + */ +class MappedTypeExpr extends @mapped_typeexpr, TypeParameterized, TypeExpr { + /** + * Gets the `K in C` part from `{ [K in C]: T }`. + */ + TypeParameter getTypeParameter() { result = this.getChildTypeExpr(0) } + + override TypeParameter getTypeParameter(int n) { n = 0 and result = this.getTypeParameter() } + + /** + * Gets the `T` part from `{ [K in C]: T }`. + */ + TypeExpr getElementType() { result = this.getChildTypeExpr(1) } + + override string describe() { result = "mapped type" } + + override string getAPrimaryQlClass() { result = "MappedTypeExpr" } +} + +/** + * A type of form `typeof E` where `E` is a possibly qualified name referring to a variable, + * function, class, or namespace. + */ +class TypeofTypeExpr extends @typeof_typeexpr, TypeExpr { + /** + * Gets the `E` in `typeof E`, denoting the qualified the name of a + * variable, function, class, or namespace whose type is to be extracted. + */ + VarTypeAccess getExpressionName() { result = this.getChildTypeExpr(0) } + + override string getAPrimaryQlClass() { result = "TypeofTypeExpr" } +} + +/** + * A function return type that refines the type of one of its parameters or `this`. + * + * Examples: + * ```js + * function f(x): x is string {} + * function f(x): asserts x is string {} + * function f(x): asserts x {} + * ``` + */ +class PredicateTypeExpr extends @predicate_typeexpr, TypeExpr { + /** + * Gets the parameter name or `this` token `E` in `E is T`. + */ + VarTypeAccess getParameterName() { result = this.getChildTypeExpr(0) } + + /** + * Gets the type `T` in `E is T` or `asserts E is T`. + * + * Has no results for types of form `asserts E`. + */ + TypeExpr getPredicateType() { result = this.getChildTypeExpr(1) } + + /** + * Holds if this is a type of form `asserts E is T` or `asserts E`. + */ + predicate hasAssertsKeyword() { has_asserts_keyword(this) } + + override string getAPrimaryQlClass() { result = "PredicateTypeExpr" } +} + +/** + * A function return type of form `x is T` or `asserts x is T`. + * + * Examples: + * ```js + * function f(x): x is string {} + * function f(x): asserts x is string {} + * ``` + */ +class IsTypeExpr extends PredicateTypeExpr { + IsTypeExpr() { exists(this.getPredicateType()) } + + override string getAPrimaryQlClass() { result = "IsTypeExpr" } +} + +/** + * An optional type element in a tuple type, such as `number?` in `[string, number?]`. + */ +class OptionalTypeExpr extends @optional_typeexpr, TypeExpr { + /** Gets the type `T` in `T?` */ + TypeExpr getElementType() { result = this.getChildTypeExpr(0) } + + override TypeExpr getAnUnderlyingType() { result = this.getElementType().getAnUnderlyingType() } + + override string getAPrimaryQlClass() { result = "OptionalTypeExpr" } +} + +/** + * A rest element in a tuple type, such as `...string[]` in `[number, ...string[]]`. + */ +class RestTypeExpr extends @rest_typeexpr, TypeExpr { + /** Gets the type `T[]` in `...T[]`, such as `string[]` in `[number, ...string[]]`. */ + TypeExpr getArrayType() { result = this.getChildTypeExpr(0) } + + /** Gets the type `T` in `...T[]`, such as `string` in `[number, ...string[]]`. */ + TypeExpr getElementType() { result = this.getArrayType().(ArrayTypeExpr).getElementType() } + + override string getAPrimaryQlClass() { result = "RestTypeExpr" } +} + +/** + * A type of form `readonly T`, such as `readonly number[]`. + */ +class ReadonlyTypeExpr extends @readonly_typeexpr, TypeExpr { + /** Gets the type `T` in `readonly T`. */ + TypeExpr getElementType() { result = this.getChildTypeExpr(0) } + + override TypeExpr getAnUnderlyingType() { result = this.getElementType().getAnUnderlyingType() } + + override string getAPrimaryQlClass() { result = "ReadonlyTypeExpr" } +} + +/** + * A possibly qualified name that refers to a variable from inside a type. + * + * This can occur as + * - part of the operand to a `typeof` type, or + * - as the first operand to a predicate type + * + * For example, it may occur as the `E` in these examples: + * ``` + * var x : typeof E + * function f(...) : E is T {} + * function f(...) : asserts E {} + * ``` + * + * In the latter two cases, this may also refer to the pseudo-variable `this`. + */ +class VarTypeAccess extends @vartypeaccess, TypeExpr { + override string getAPrimaryQlClass() { result = "VarTypeAccess" } +} + +/** + * An identifier that refers to a variable from inside a type. + * + * This can occur as part of the operand to a `typeof` type or as the first operand to an `is` type. + */ +class LocalVarTypeAccess extends @local_var_type_access, VarTypeAccess, LexicalAccess, Identifier { + /** Gets the variable being referenced, or nothing if this is a `this` keyword. */ + Variable getVariable() { bind(this, result) } + + override string getAPrimaryQlClass() { result = "LocalVarTypeAccess" } +} + +/** + * A `this` keyword used as the first operand to an `is` type. + * + * For example, it may occur as the `this` in the following example: + * ``` + * interface Node { isLeaf(): this is Leaf; } + * ``` + */ +class ThisVarTypeAccess extends @this_var_type_access, VarTypeAccess { + override string getAPrimaryQlClass() { result = "ThisVarTypeAccess" } +} + +/** + * A qualified name that refers to a variable from inside a type. + * + * This can only occur as part of the operand to a `typeof` type. + */ +class QualifiedVarTypeAccess extends @qualified_var_type_access, VarTypeAccess { + /** + * Gets the qualifier in front of the name being accessed, such as `http` in `http.ServerRequest`. + */ + VarTypeAccess getQualifier() { result = this.getChildTypeExpr(0) } + + /** + * Gets the last identifier in the name, such as `ServerRequest` in `http.ServerRequest`. + */ + Identifier getIdentifier() { result = this.getChildTypeExpr(1) } +} + +/** + * A conditional type annotation, such as `T extends any[] ? A : B`. + */ +class ConditionalTypeExpr extends @conditional_typeexpr, TypeExpr { + /** + * Gets the type to the left of the `extends` keyword, such as `T` in `T extends any[] ? A : B`. + */ + TypeExpr getCheckType() { result = this.getChildTypeExpr(0) } + + /** + * Gets the type to the right of the `extends` keyword, such as `any[]` in `T extends any[] ? A : B`. + */ + TypeExpr getExtendsType() { result = this.getChildTypeExpr(1) } + + /** + * Gets the type to be used if the `extend` condition holds, such as `A` in `T extends any[] ? A : B`. + */ + TypeExpr getTrueType() { result = this.getChildTypeExpr(2) } + + /** + * Gets the type to be used if the `extend` condition fails, such as `B` in `T extends any[] ? A : B`. + */ + TypeExpr getFalseType() { result = this.getChildTypeExpr(3) } + + override string getAPrimaryQlClass() { result = "ConditionalTypeExpr" } +} + +/** + * A type annotation of form `infer R`. + */ +class InferTypeExpr extends @infer_typeexpr, TypeParameterized, TypeExpr { + /** + * Gets the type parameter capturing the matched type, such as `R` in `infer R`. + */ + TypeParameter getTypeParameter() { result = this.getChildTypeExpr(0) } + + override TypeParameter getTypeParameter(int n) { n = 0 and result = this.getTypeParameter() } + + override string describe() { result = "'infer' type " + this.getTypeParameter().getName() } + + override string getAPrimaryQlClass() { result = "InferTypeExpr" } +} + +/** + * A template literal used as a type. + */ +class TemplateLiteralTypeExpr extends @template_literal_typeexpr, TypeExpr { + /** + * Gets the `i`th element of this template literal, which may either + * be a type expression or a constant template element. + */ + ExprOrType getElement(int i) { result = this.getChild(i) } + + /** + * Gets an element of this template literal. + */ + ExprOrType getAnElement() { result = this.getElement(_) } + + /** + * Gets the number of elements of this template literal. + */ + int getNumElement() { result = count(this.getAnElement()) } + + override string getAPrimaryQlClass() { result = "TemplateLiteralTypeExpr" } +} + +/** + * A scope induced by a conditional type expression whose `extends` type + * contains `infer` types. + */ +class ConditionalTypeScope extends @conditional_type_scope, Scope { + /** Gets the conditional type expression that induced this scope. */ + ConditionalTypeExpr getConditionalTypeExpr() { result = Scope.super.getScopeElement() } +} + +/** + * An expression with type arguments, occurring as the super-class expression of a class, for example: + * ``` + * class StringList extends List + * ``` + * In the above example, `List` is a concrete expression, `string` is a type annotation, + * and `List` is thus an expression with type arguments. + */ +class ExpressionWithTypeArguments extends @expression_with_type_arguments, Expr { + /** + * Gets the expression, such as `List` in `List`. + * + * Although it is common for this expression to take the form of a qualified name, + * it can in general be any kind of expression. + */ + Expr getExpression() { result = this.getChildExpr(-1) } + + /** + * Gets the `n`th type argument, starting at 0. + */ + TypeExpr getTypeArgument(int n) { result = this.getChildTypeExpr(n) } + + /** + * Gets any of the type arguments. + */ + TypeExpr getATypeArgument() { result = this.getTypeArgument(_) } + + /** + * Gets the number of type arguments. This is always at least one. + */ + int getNumTypeArgument() { result = count(this.getATypeArgument()) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getExpression().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "ExpressionWithTypeArguments" } +} + +/** + * A program element that supports type parameters, that is, a function, class, interface, type alias, mapped type, or `infer` type. + */ +class TypeParameterized extends @type_parameterized, AstNode { + /** Gets the `n`th type parameter declared on this function or type. */ + TypeParameter getTypeParameter(int n) { none() } // Overridden in subtypes. + + /** Gets any type parameter declared on this function or type. */ + TypeParameter getATypeParameter() { result = this.getTypeParameter(_) } + + /** Gets the number of type parameters declared on this function or type. */ + int getNumTypeParameter() { result = count(this.getATypeParameter()) } + + /** Holds if this function or type declares any type parameters. */ + predicate hasTypeParameters() { exists(this.getATypeParameter()) } + + /** Gets a description of this function or type. */ + string describe() { none() } +} + +/** + * A type parameter declared on a class, interface, function, or type alias. + */ +class TypeParameter extends @type_parameter, TypeExpr { + /** + * Gets the name of the type parameter as a string. + */ + string getName() { result = this.getIdentifier().getName() } + + /** + * Gets the identifier node of the type parameter, such as `T` in `class C`. + */ + Identifier getIdentifier() { result = this.getChildTypeExpr(0) } + + /** + * Gets the upper bound of the type parameter, such as `U` in `class C`. + */ + TypeExpr getBound() { result = this.getChildTypeExpr(1) } + + /** + * Gets the default value of the type parameter, such as `D` in `class C`. + */ + TypeExpr getDefault() { result = this.getChildTypeExpr(2) } + + /** + * Gets the function or type that declares this type parameter. + */ + TypeParameterized getHost() { result.getATypeParameter() = this } + + /** + * Gets the local type name declared by this type parameter. + */ + LocalTypeName getLocalTypeName() { result = this.getIdentifier().(TypeDecl).getLocalTypeName() } + + override string getAPrimaryQlClass() { result = "TypeParameter" } +} + +/** + * A type assertion, also known as an unchecked type cast, is a TypeScript expression + * of form `E as T` or ` E` where `E` is an expression and `T` is a type. + */ +class TypeAssertion extends Expr, @type_assertion { + /** Gets the expression whose type to assert, that is, the `E` in `E as T` or ` E`. */ + Expr getExpression() { result = this.getChildExpr(0) } + + /** Gets the type to cast to, that is, the `T` in `E as T` or ` E`. */ + TypeExpr getTypeAnnotation() { result = this.getChildTypeExpr(1) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getExpression().getFirstControlFlowNode() + } + + override Expr getUnderlyingValue() { result = this.getExpression().getUnderlyingValue() } + + override Expr getUnderlyingReference() { result = this.getExpression().getUnderlyingReference() } + + override string getAPrimaryQlClass() { result = "TypeAssertion" } +} + +/** + * A type assertion specifically of the form `E as T` (as opposed to the ` E` syntax). + */ +class AsTypeAssertion extends TypeAssertion, @as_type_assertion { } + +/** + * A type assertion specifically of the form ` E` (as opposed to the `E as T` syntax). + */ +class PrefixTypeAssertion extends TypeAssertion, @prefix_type_assertion { } + +/** + * A satisfies type asserion of the form `E satisfies T` where `E` is an expression and `T` is a type. + */ +class SatisfiesExpr extends Expr, @satisfies_expr { + /** Gets the expression whose type to assert, that is, the `E` in `E as T` or ` E`. */ + Expr getExpression() { result = this.getChildExpr(0) } + + /** Gets the type to cast to, that is, the `T` in `E as T` or ` E`. */ + TypeExpr getTypeAnnotation() { result = this.getChildTypeExpr(1) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getExpression().getFirstControlFlowNode() + } + + override Expr getUnderlyingValue() { result = this.getExpression().getUnderlyingValue() } + + override Expr getUnderlyingReference() { result = this.getExpression().getUnderlyingReference() } + + override string getAPrimaryQlClass() { result = "SatisfiesExpr" } +} + +/** + * A TypeScript expression of form `E!`, asserting that `E` is not null. + */ +class NonNullAssertion extends Expr, @non_null_assertion { + /** Gets the expression whose type to assert. */ + Expr getExpression() { result = this.getChildExpr(0) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getExpression().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "NonNullAssertion" } +} + +/** + * A possibly qualified identifier that refers to or declares a local name for a namespace. + */ +abstract class NamespaceRef extends AstNode { } + +/** + * An identifier that declares a local name for a namespace, that is, + * the name of an actual namespace declaration or the local name of an import. + * + * For instance, this includes the `N` in each of the following examples: + * ``` + * namespace N {} + * import N = require('./lib') + * import N from './lib' + * import { N } from './lib' + * import { X as N } from './lib' + * import * as N from './lib' + * ``` + */ +class LocalNamespaceDecl extends VarDecl, NamespaceRef { + LocalNamespaceDecl() { + any(NamespaceDeclaration nd).getIdentifier() = this or + any(ImportEqualsDeclaration im).getIdentifier() = this or + any(ImportSpecifier im).getLocal() = this or + any(EnumDeclaration ed).getIdentifier() = this + } + + /** Gets the local name being declared. */ + LocalNamespaceName getLocalNamespaceName() { namespacedecl(this, result) } +} + +/** + * A possibly qualified name that refers to a namespace from inside a type annotation. + * + * For example, in the type access `A.B.C`, the prefix `A.B` is a qualified namespace access, and + * the prefix `A` is a local namespace access. + * + * *Expressions* that refer to namespaces are represented as `VarAccess` and `PropAccess` expressions, + * as opposed to `NamespaceAccess`. + */ +class NamespaceAccess extends TypeExpr, NamespaceRef, @namespace_access { + Identifier getIdentifier() { none() } + + override string getAPrimaryQlClass() { result = "NamespaceAccess" } +} + +/** + * An identifier that refers to a namespace from inside a type annotation. + */ +class LocalNamespaceAccess extends NamespaceAccess, LexicalAccess, Identifier, + @local_namespace_access +{ + override Identifier getIdentifier() { result = this } + + /** Gets the local name being accessed. */ + LocalNamespaceName getLocalNamespaceName() { namespacebind(this, result) } + + override string getAPrimaryQlClass() { result = "LocalNamespaceAccess" } +} + +/** + * A qualified name that refers to a namespace from inside a type annotation. + */ +class QualifiedNamespaceAccess extends NamespaceAccess, @qualified_namespace_access { + NamespaceAccess getQualifier() { result = this.getChildTypeExpr(0) } + + override Identifier getIdentifier() { result = this.getChildTypeExpr(1) } +} + +/** + * An import inside a type annotation, such as in `import("http").ServerRequest`. + */ +class ImportTypeExpr extends TypeExpr, @import_typeexpr { + /** + * Gets the string literal with the imported path, such as `"http"` in `import("http")`. + */ + TypeExpr getPathExpr() { result = this.getChildTypeExpr(0) } + + /** + * Gets the unresolved path string, such as `http` in `import("http")`. + */ + string getPath() { result = this.getPathExpr().(StringLiteralTypeExpr).getValue() } + + /** Holds if this import is used in the context of a type, such as in `let x: import("foo")`. */ + predicate isTypeAccess() { this instanceof @import_type_access } + + /** Holds if this import is used in the context of a namespace, such as in `let x: import("http").ServerRequest"`. */ + predicate isNamespaceAccess() { this instanceof @import_namespace_access } + + /** Holds if this import is used in the context of a variable type, such as `let x: typeof import("fs")`. */ + predicate isVarTypeAccess() { this instanceof @import_var_type_access } + + override string getAPrimaryQlClass() { result = "ImportTypeExpr" } +} + +/** + * An import used in the context of a type, such as in `let x: import("foo")`. + */ +class ImportTypeAccess extends TypeAccess, ImportTypeExpr, @import_type_access { + override string getAPrimaryQlClass() { result = "ImportTypeAccess" } +} + +/** + * An import used in the context of a namespace inside a type annotation, such as in `let x: import("http").ServerRequest`. + */ +class ImportNamespaceAccess extends NamespaceAccess, ImportTypeExpr, @import_namespace_access { + override string getAPrimaryQlClass() { result = "ImportNamespaceAccess" } +} + +/** + * An import used in the context of a variable type, such as in `let x: typeof import("fs")`. + */ +class ImportVarTypeAccess extends VarTypeAccess, ImportTypeExpr, @import_var_type_access { + override string getAPrimaryQlClass() { result = "ImportVarTypeAccess" } +} + +/** + * A TypeScript enum declaration, such as the following declaration: + * ``` + * enum Color { red = 1, green, blue } + * ``` + */ +class EnumDeclaration extends NamespaceDefinition, @enum_declaration, AST::ValueNode { + /** Gets the name of this enum, such as `E` in `enum E { A, B }`. */ + override Identifier getIdentifier() { result = this.getChildExpr(0) } + + /** Gets the name of this enum as a string. */ + override string getName() { result = this.getIdentifier().getName() } + + /** + * Gets the variable holding the enumeration object. + * + * For example, this would be the variable `E` introduced by `enum E { A, B }`. + * + * If the enumeration is a `const enum` then this variable will not exist at runtime + * and all uses of the variable will be constant-folded by the TypeScript compiler, + * but the analysis models it as an ordinary variable. + */ + Variable getVariable() { result = this.getIdentifier().(VarDecl).getVariable() } + + /** + * Gets the local type name introduced by the enumeration. + * + * For example, this would be the type `E` introduced by `enum E { A, B }`. + */ + LocalTypeName getLocalTypeName() { result = this.getIdentifier().(TypeDecl).getLocalTypeName() } + + /** + * Gets the local namespace name introduced by the enumeration, for use in + * types that reference the enum members directly. + * + * For example, in the type `E.A` below, the enum `E` is accessed through the + * local namespace name `E`: + * ``` + * enum E { A = 1, B = 2 } + * var x: E.A = 1 + * ``` + */ + override LocalNamespaceName getLocalNamespaceName() { + result = this.getIdentifier().(LocalNamespaceDecl).getLocalNamespaceName() + } + + /** Gets the `n`th enum member, starting at 0, such as `A` or `B` in `enum E { A, B }`. */ + EnumMember getMember(int n) { properties(result, this, n + 1, _, _) } + + /** Gets the enum member with the given name, if any. */ + EnumMember getMemberByName(string name) { result = this.getAMember() and result.getName() = name } + + /** Gets any of the enum members. */ + EnumMember getAMember() { result = this.getMember(_) } + + /** Gets the number of enum members. */ + int getNumMember() { result = count(this.getAMember()) } + + /** Gets the `n`th decorator applied to this enum declaration. */ + Decorator getDecorator(int n) { result = this.getChildExpr(-n - 1) } + + /** Gets a decorator applied to this enum declaration. */ + Decorator getADecorator() { result = this.getDecorator(_) } + + /** Gets the number of decorators applied to this enum declaration. */ + int getNumDecorator() { result = count(this.getADecorator()) } + + /** Holds if this enumeration is declared with the `const` keyword. */ + predicate isConst() { is_const_enum(this) } + + override ControlFlowNode getFirstControlFlowNode() { result = this.getIdentifier() } + + override string getAPrimaryQlClass() { result = "EnumDeclaration" } +} + +/** + * A member of a TypeScript enum declaration, such as `red` in the following declaration: + * ``` + * enum Color { red = 1, green, blue } + * ``` + */ +class EnumMember extends AstNode, @enum_member { + /** + * Gets the name of the enum member, such as `off` in `enum State { on, off }`. + * + * Note that if the name of the member is written as a string literal, + * a synthetic identifier node is created to represent the name. + * In other words, the name will always be an identifier node. + */ + Identifier getIdentifier() { result = this.getChildExpr(0) } + + /** Gets the name of the enum member as a string. */ + string getName() { result = this.getIdentifier().getName() } + + /** Gets the explicit initializer expression of this member, if any. */ + Expr getInitializer() { result = this.getChildExpr(1) } + + /** Gets the enum declaring this member. */ + EnumDeclaration getDeclaringEnum() { result.getAMember() = this } + + override string toString() { properties(this, _, _, _, result) } + + override ControlFlowNode getFirstControlFlowNode() { result = this.getIdentifier() } + + /** + * Gets the name of the member prefixed by the declaring enum name. + * + * For example, the prefixed name of the `red` member below is `Color.red`: + * ``` + * enum Color { red, green, blue } + * ``` + */ + string getPrefixedName() { result = this.getDeclaringEnum().getName() + "." + this.getName() } + + override string getAPrimaryQlClass() { result = "EnumMember" } +} + +/** + * A scope induced by an interface declaration, containing the type parameters declared on the interface. + * + * Interfaces that do not declare type parameters have no scope object. + */ +class InterfaceScope extends @interface_scope, Scope { + override string toString() { result = "interface scope" } +} + +/** + * A scope induced by a type alias declaration, containing the type parameters declared the the alias. + * + * Type aliases that do not declare type parameters have no scope object. + */ +class TypeAliasScope extends @type_alias_scope, Scope { + override string toString() { result = "type alias scope" } +} + +/** + * A scope induced by a mapped type expression, containing the type parameter declared as part of the type. + */ +class MappedTypeScope extends @mapped_type_scope, Scope { + override string toString() { result = "mapped type scope" } +} + +/** + * A scope induced by an enum declaration, containing the names of its enum members. + * + * Initializers of enum members are resolved in this scope since they can reference + * previously-defined enum members by their unqualified name. + */ +class EnumScope extends @enum_scope, Scope { + override string toString() { result = "enum scope" } +} + +/** + * A scope induced by a declaration of form `declare module "X" {...}`. + */ +class ExternalModuleScope extends @external_module_scope, Scope { + override string toString() { result = "external module scope" } +} + +/** + * A TypeScript comment of one of the two forms: + * ``` + * /// + * /// + * ``` + */ +class ReferenceImport extends LineComment { + string attribute; + string value; + + ReferenceImport() { + this.getFile().getFileType().isTypeScript() and + exists(string regex | + regex = "/\\s*\\s*" + | + attribute = this.getText().regexpCapture(regex, 1) and + value = this.getText().regexpCapture(regex, 2) + ) and + (attribute = "path" or attribute = "types") + } + + /** + * Gets the value of `path` or `types` attribute. + */ + string getAttributeValue() { result = value } + + /** + * Gets the name of the attribute, i.e. "`path`" or "`types`". + */ + string getAttributeName() { result = attribute } +} + +/** + * A TypeScript comment of the form: + * ``` + * /// + * ``` + */ +class ReferencePathImport extends ReferenceImport { + ReferencePathImport() { attribute = "path" } +} + +/** + * A TypeScript comment of the form: + * ``` + * /// + * ``` + */ +class ReferenceTypesImport extends ReferenceImport { + ReferenceTypesImport() { attribute = "types" } +} + +/** + * A folder where TypeScript finds declaration files for imported modules. + * + * Currently, this is any folder whose path ends with `node_modules/@types`. + */ +class TypeRootFolder extends Folder { + TypeRootFolder() { + exists(Folder nodeModules | + nodeModules.getBaseName() = "node_modules" and + nodeModules.getFolder("@types") = this + ) + } + + /** + * Gets the enclosing `node_modules` folder. + */ + Folder getNodeModulesFolder() { result = this.getParentContainer() } + + /** + * Gets the `d.ts` file correspnding to the given module name. + * + * Concretely, this is the file at `node_modules/@types//index.d.ts` if it exists. + */ + File getModuleFile(string moduleName) { + result = this.getFolder(moduleName).getFile("index.d.ts") + } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Util.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Util.qll new file mode 100644 index 000000000000..60d2d4595614 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Util.qll @@ -0,0 +1,24 @@ +private import minimal + +string getNameFromLValue(Expr e) { + result = e.(Identifier).getName() + or + result = e.(PropAccess).getPropertyName() +} + +string getNameFromContext(Expr e) { + exists(AssignExpr assign | + e = assign.getRhs() and + result = getNameFromLValue(assign.getTarget()) + ) + or + exists(VariableDeclarator decl | + e = decl.getInit() and + result = getNameFromLValue(decl.getBindingPattern()) + ) + // or + // exists(Property prop | + // e = prop.getInit() and + // result = prop.getName() + // ) +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Variables.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Variables.qll new file mode 100644 index 000000000000..fde48b9c5cb6 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/Variables.qll @@ -0,0 +1,998 @@ +/** Provides classes for modeling program variables. */ +overlay[local?] +module; + +import minimal + +/** A scope in which variables can be declared. */ +class Scope extends @scope { + /** Gets a textual representation of this element. */ + string toString() { none() } + + /** Gets the scope in which this scope is nested, if any. */ + Scope getOuterScope() { scopenesting(this, result) } + + /** Gets a scope nested in this one, if any. */ + Scope getAnInnerScope() { result.getOuterScope() = this } + + /** Gets the program element this scope is associated with, if any. */ + AstNode getScopeElement() { scopenodes(result, this) } + + /** Gets the location of the program element this scope is associated with, if any. */ + Location getLocation() { result = this.getScopeElement().getLocation() } + + /** Gets a variable declared in this scope. */ + Variable getAVariable() { result.getScope() = this } + + /** Gets the variable with the given name declared in this scope. */ + Variable getVariable(string name) { + result = this.getAVariable() and + result.getName() = name + } + + /** Gets the local type name with the given name declared in this scope. */ + LocalTypeName getLocalTypeName(string name) { + result.getScope() = this and + result.getName() = name + } +} + +/** + * A program element that induces a scope. + */ +class ScopeElement extends AstNode { + Scope s; + + ScopeElement() { this = s.getScopeElement() } + + /** Gets the scope induced by this element. */ + Scope getScope() { result = s } +} + +/** The global scope. */ +class GlobalScope extends Scope, @global_scope { + override string toString() { result = "global scope" } +} + +/** A local scope, that is, a scope that is not the global scope. */ +class LocalScope extends Scope { + LocalScope() { not this instanceof GlobalScope } +} + +/** + * A scope induced by a Node.js or ES2015 module + */ +class ModuleScope extends Scope, @module_scope { + /** Gets the module that induces this scope. */ + overlay[global] + Module getModule() { result = this.getScopeElement() } + + override string toString() { result = "module scope" } +} + +/** A scope induced by a function. */ +class FunctionScope extends Scope, @function_scope { + /** Gets the function that induces this scope. */ + Function getFunction() { result = this.getScopeElement() } + + override string toString() { result = "function scope" } +} + +/** A scope induced by a catch clause. */ +class CatchScope extends Scope, @catch_scope { + /** Gets the catch clause that induces this scope. */ + CatchClause getCatchClause() { result = this.getScopeElement() } + + override string toString() { result = "catch scope" } +} + +/** A scope induced by a block of statements. */ +class BlockScope extends Scope, @block_scope { + /** Gets the block of statements that induces this scope. */ + BlockStmt getBlock() { result = this.getScopeElement() } + + override string toString() { result = "block scope" } +} + +/** A scope induced by a `for` statement. */ +class ForScope extends Scope, @for_scope { + /** Gets the `for` statement that induces this scope. */ + ForStmt getLoop() { result = this.getScopeElement() } + + override string toString() { result = "for scope" } +} + +/** A scope induced by a `for`-`in` or `for`-`of` statement. */ +class ForInScope extends Scope, @for_in_scope { + /** Gets the `for`-`in` or `for`-`of` statement that induces this scope. */ + EnhancedForLoop getLoop() { result = this.getScopeElement() } + + override string toString() { result = "for-in scope" } +} + +/** A scope induced by a comprehension block. */ +class ComprehensionBlockScope extends Scope, @comprehension_block_scope { + /** Gets the comprehension block that induces this scope. */ + ComprehensionBlock getComprehensionBlock() { result = this.getScopeElement() } + + override string toString() { result = "comprehension block scope" } +} + +/** + * The lexical scope induced by a TypeScript namespace declaration. + * + * This scope is specific to a single syntactic declaration of a namespace, + * and currently does not include variables exported from other declarations + * of the same namespace. + */ +class NamespaceScope extends Scope, @namespace_scope { + override string toString() { result = "namespace scope" } +} + +/** A variable declared in a scope. */ +class Variable extends @variable, LexicalName { + /** Gets the name of this variable. */ + override string getName() { variables(this, result, _) } + + /** Gets the scope this variable is declared in. */ + override Scope getScope() { variables(this, _, result) } + + /** + * Holds if this variable is declared in the top-level of a module using a `declare` statement. + * + * For example: + * ```js + * declare var $: any; + * ``` + * + * Such variables are generally treated as a global variables, except for type-checking related purposes. + */ + pragma[nomagic] + predicate isTopLevelWithAmbientDeclaration() { + this.getScope() instanceof ModuleScope and + forex(VarDecl decl | decl = this.getADeclaration() | decl.isAmbient()) + } + + /** Holds if this is a global variable. */ + predicate isGlobal() { + this.getScope() instanceof GlobalScope or this.isTopLevelWithAmbientDeclaration() + } + + /** + * Holds if this is a variable exported from a TypeScript namespace. + * + * Note that such variables are also considered local for the time being. + */ + predicate isNamespaceExport() { + this.getScope() instanceof NamespaceScope and + exists(ExportNamedDeclaration decl | decl.getADecl().getVariable() = this) + } + + /** + * Holds if this is a local variable. + * + * Parameters and `arguments` variables are considered to be local. + */ + predicate isLocal() { not this.isGlobal() } + + /** Holds if this variable is a parameter. */ + predicate isParameter() { exists(Parameter p | p.getAVariable() = this) } + + /** Gets a reference to this variable. */ + VarRef getAReference() { result.getVariable() = this } + + /** Gets an access to this variable. */ + VarAccess getAnAccess() { result.getVariable() = this } + + /** Gets a declaration declaring this variable, if any. */ + VarDecl getADeclaration() { result.getVariable() = this } + + /** Gets a declaration statement declaring this variable, if any. */ + DeclStmt getADeclarationStatement() { + result.getADecl().getBindingPattern().getAVariable() = this + } + + /** Gets an expression that is directly stored in this variable. */ + Expr getAnAssignedExpr() { + // result is an expression that this variable is initialized to + exists(VariableDeclarator vd | vd.getBindingPattern().(VarDecl).getVariable() = this | + result = vd.getInit() + ) + or + // if this variable represents a function binding, return the function + exists(FunctionExpr fn | fn.getVariable() = this | result = fn) + or + // there is an assignment to this variable + exists(Assignment assgn | assgn.getLhs() = this.getAnAccess() and assgn.getRhs() = result) + } + + /** Like `getAnAccess()` but includes `this` expressions, which are not typed as `VarAccess`. */ + private Expr getAnAccessOrThisExpr() { bind(result, this) } + + /** + * Holds if this variable is captured in the closure of a nested function. + * + * Global variables are always considered to be captured. + */ + predicate isCaptured() { + this instanceof GlobalVariable or + this.getAnAccessOrThisExpr().getContainer().getFunctionBoundary() != + this.(LocalVariable).getDeclaringContainer().getFunctionBoundary() + } + + /** Holds if there is a declaration of this variable in `tl`. */ + predicate declaredIn(TopLevel tl) { this.getADeclaration().getTopLevel() = tl } + + /** Gets a textual representation of this element. */ + override string toString() { result = this.getName() } + + override DeclarationSpace getDeclarationSpace() { result = "variable" } +} + +/** An `arguments` variable of a function. */ +class ArgumentsVariable extends Variable { + ArgumentsVariable() { is_arguments_object(this) } + + override FunctionScope getScope() { result = Variable.super.getScope() } + + /** Gets the function declaring this 'arguments' variable. */ + Function getFunction() { result = this.getScope().getFunction() } +} + +/** + * An identifier that refers to a variable, either in a declaration or in a variable access. + * + * Examples: + * + * ``` + * function f(o) { // `f` and `o` are variable references + * var w = 0, { x : y, z } = o; // `w`, `y`, `z` and `o` are variable references; `x` is not + * o = null; // `o` is a variable reference; `null` is not + * } + * ``` + */ +class VarRef extends @varref, Identifier, BindingPattern, LexicalRef { + /** Gets the variable this identifier refers to. */ + override Variable getVariable() { none() } // Overridden in VarAccess and VarDecl + + override string getName() { result = Identifier.super.getName() } + + override VarRef getABindingVarRef() { result = this } + + overlay[global] + override predicate isImpure() { none() } + + override Expr getUnderlyingReference() { result = this } + + override string getAPrimaryQlClass() { result = "VarRef" } +} + +/** + * An identifier that refers to a variable in a non-declaring position. + * + * Examples: + * + * ``` + * function f(o) { + * var w = 0, { x : y, z } = o; // `o` is a variable access + * o = null; // `o` is a variable access + * } + * ``` + */ +class VarAccess extends @varaccess, VarRef, LexicalAccess { + /** + * Gets the variable this identifier refers to. + * + * When analyzing TypeScript code, a variable may spuriously be resolved as a + * global due to incomplete modeling of exported variables in namespaces. + */ + override Variable getVariable() { bind(this, result) } + + override predicate isLValue() { + exists(Assignment assgn | assgn.getTarget() = this) + or + exists(UpdateExpr upd | upd.getOperand().getUnderlyingReference() = this) + or + exists(EnhancedForLoop efl | efl.getIterator() = this) + or + exists(BindingPattern p | this = p.getABindingVarRef() and p.isLValue()) + } + + override Variable getAVariable() { result = this.getVariable() } +} + +/** + * An identifier that occurs in a named export declaration. + * + * Example: + * + * ``` + * export { A }; + * ``` + * + * Such an identifier may refer to a variable, type, or namespace, or a combination of these. + */ +class ExportVarAccess extends VarAccess, @export_varaccess { + /** Gets the type being exported, if this identifier refers to a type. */ + LocalTypeName getLocalTypeName() { result.getAnAccess() = this } + + /** Gets the namespace being exported, if this identifier refers to a namespace. */ + LocalNamespaceName getLocalNamespaceName() { result.getAnAccess() = this } +} + +/** A global variable. */ +class GlobalVariable extends Variable { + GlobalVariable() { this.isGlobal() } +} + +/** A local variable or a parameter. */ +class LocalVariable extends Variable { + LocalVariable() { this.isLocal() } + + /** + * Alias for `getDeclaringContainer()`. + */ + StmtContainer getDeclaringCallable() { result = this.getDeclaringContainer() } + + /** + * Gets the function or toplevel in which this variable is declared; + * `arguments` variables are taken to be implicitly declared in the function + * to which they belong. + * + * Note that for a function expression `function f() { ... }` the variable + * `f` is declared in the function itself, while for a function statement + * it is declared in the enclosing container. + */ + StmtContainer getDeclaringContainer() { + this = result.getScope().getAVariable() + or + exists(VarDecl d | d = this.getADeclaration() | + if d = any(FunctionDeclStmt fds).getIdentifier() + then + exists(FunctionDeclStmt fds | d = fds.getIdentifier() | + result = fds.getEnclosingContainer() + ) + else result = d.getContainer() + ) + } + + /** + * Gets the location of a declaration of this variable. + * + * If the variable has one or more declarations, the location of the first declaration is used. + * If the variable has no declaration, the entry point of its declaring container is used. + */ + Location getLocation() { + result = + min(Location loc | + loc = this.getADeclaration().getLocation() + | + loc order by loc.getStartLine(), loc.getStartColumn() + ) + or + not exists(this.getADeclaration()) and + result = this.getDeclaringContainer().getEntry().getLocation() + } +} + +/** A local variable that is not captured. */ +class PurelyLocalVariable extends LocalVariable { + PurelyLocalVariable() { not this.isCaptured() } +} + +/** + * An identifier that refers to a global variable. + * + * Examples: + * + * ``` + * NaN + * undefined + * ``` + */ +class GlobalVarAccess extends VarAccess { + GlobalVarAccess() { this.getVariable().isGlobal() } +} + +/** + * A binding pattern, that is, either an identifier or a destructuring pattern. + * + * Examples: + * + * ``` + * function f(x, { y: z }, ...rest) { // `x`, `{ y: z }`, `z` and `rest` are binding patterns + * var [ a, b ] = rest; // `[ a, b ]`, `a` and `b` are binding patterns + * var c; // `c` is a binding pattern + * } + * ``` + * + * Binding patterns can appear on the left hand side of assignments or in + * variable or parameter declarations. + */ +class BindingPattern extends @pattern, Expr { + /** Gets the name of this binding pattern if it is an identifier. */ + string getName() { none() } + + /** Gets the variable this binding pattern refers to if it is an identifier. */ + Variable getVariable() { none() } + + /** Gets a variable reference in binding position within this pattern. */ + VarRef getABindingVarRef() { none() } + + /** Gets a variable bound by this pattern. */ + Variable getAVariable() { result = this.getABindingVarRef().getVariable() } + + /** Holds if this pattern appears in an l-value position. */ + predicate isLValue() { any() } + + /** + * Returns the type annotation for this variable or pattern, if any. + * + * Only the outermost part of a binding pattern can have a type annotation. + * For instance, in the declaration, + *
+   * var {x}: Point
+   * 
+ * the variable `x` has no type annotation, whereas the pattern `{x}` has the type + * annotation `Point`. + */ + TypeAnnotation getTypeAnnotation() { + exists(VariableDeclarator decl | decl.getBindingPattern() = this | + result = decl.getTypeAnnotation() + ) + // note: Parameter overrides this to handle the parameter case + } +} + +private class TDestructuringPattern = @array_pattern or @object_pattern; + +/** + * A destructuring pattern, that is, either an array pattern or an object pattern. + * + * Examples: + * + * ``` + * function f(x, { y: z }, ...rest) { // `{ y: z }` is a destructuring pattern + * var [ a, b ] = rest; // `[ a, b ]` is a destructuring pattern + * var c; + * } + * ``` + */ +class DestructuringPattern extends TDestructuringPattern, BindingPattern { + /** Gets the rest pattern of this destructuring pattern, if any. */ + Expr getRest() { none() } // Overridden in subtypes. +} + +/** + * An identifier that declares a variable. + * + * Examples: + * + * ``` + * function f(o) { // `f` and `o` are variable declarations + * var w = 0, { x : y, z } = o; // `w`, `y` and `z` are variable declarations + * o = null; + * } + */ +class VarDecl extends @var_decl, VarRef, LexicalDecl { + override Variable getVariable() { decl(this, result) } + + override predicate isLValue() { + exists(VariableDeclarator vd | vd.getBindingPattern() = this | + exists(vd.getInit()) or + exists(EnhancedForLoop efl | this = efl.getIterator()) + ) + or + exists(BindingPattern p | this = p.getABindingVarRef() and p.isLValue()) + } + + override string getAPrimaryQlClass() { result = "VarDecl" } +} + +/** + * An identifier that declares a global variable. + * + * Example: + * + * ``` + * var m; // `m` is a global variable declaration if this is a top-level statement + * // (and not in a module) + * ``` + */ +class GlobalVarDecl extends VarDecl { + GlobalVarDecl() { this.getVariable() instanceof GlobalVariable } +} + +/** + * An array pattern. + * + * Example: + * + * ``` + * function f(x, { y: z }, ...rest) { + * var [ a, b ] = rest; // `[ a, b ]` is an array pattern + * var c; + * } + * ``` + */ +class ArrayPattern extends DestructuringPattern, @array_pattern { + /** Gets the `i`th element of this array pattern. */ + Expr getElement(int i) { + i >= 0 and + result = this.getChildExpr(i) + } + + /** Gets an element of this array pattern. */ + Expr getAnElement() { exists(int i | i >= -1 | result = this.getChildExpr(i)) } + + /** Gets the default expression for the `i`th element of this array pattern, if any. */ + Expr getDefault(int i) { + i in [0 .. this.getSize() - 1] and + result = this.getChildExpr(-2 - i) + } + + /** Holds if the `i`th element of this array pattern has a default expression. */ + predicate hasDefault(int i) { exists(this.getDefault(i)) } + + /** Gets the rest pattern of this array pattern, if any. */ + override Expr getRest() { result = this.getChildExpr(-1) } + + /** Holds if this array pattern has a rest pattern. */ + predicate hasRest() { exists(this.getRest()) } + + /** Gets the number of elements in this array pattern, not including any rest pattern. */ + int getSize() { array_size(this, result) } + + /** Holds if the `i`th element of this array pattern is omitted. */ + predicate elementIsOmitted(int i) { + i in [0 .. this.getSize() - 1] and + not exists(this.getElement(i)) + } + + /** Holds if this array pattern has an omitted element. */ + predicate hasOmittedElement() { this.elementIsOmitted(_) } + + overlay[global] + override predicate isImpure() { this.getAnElement().isImpure() } + + override VarRef getABindingVarRef() { + result = this.getAnElement().(BindingPattern).getABindingVarRef() + } + + override string getAPrimaryQlClass() { result = "ArrayPattern" } +} + +/** + * An object pattern. + * + * Example: + * + * ``` + * function f(x, { y: z }, ...rest) { // `{ y: z }` is an object pattern + * var [ a, b ] = rest; + * var c; + * } + * ``` + */ +class ObjectPattern extends DestructuringPattern, @object_pattern { + /** Gets the `i`th property pattern in this object pattern. */ + PropertyPattern getPropertyPattern(int i) { properties(result, this, i, _, _) } + + /** Gets a property pattern in this object pattern. */ + PropertyPattern getAPropertyPattern() { result = this.getPropertyPattern(_) } + + /** Gets the number of property patterns in this object pattern. */ + int getNumProperty() { result = count(this.getAPropertyPattern()) } + + /** Gets the property pattern with the given name, if any. */ + PropertyPattern getPropertyPatternByName(string name) { + result = this.getAPropertyPattern() and + result.getName() = name + } + + /** Gets the rest property pattern of this object pattern, if any. */ + override Expr getRest() { result = this.getChildExpr(-1) } + + overlay[global] + override predicate isImpure() { this.getAPropertyPattern().isImpure() } + + override VarRef getABindingVarRef() { + result = this.getAPropertyPattern().getValuePattern().(BindingPattern).getABindingVarRef() or + result = this.getRest().(BindingPattern).getABindingVarRef() + } + + override string getAPrimaryQlClass() { result = "ObjectPattern" } +} + +/** + * A property pattern in an object pattern. + * + * Examples: + * + * ``` + * function f(x, { y: z }, ...rest) { // `y: z` is a property pattern + * var [ a, b ] = rest; + * var c; + * } + * ``` + */ +class PropertyPattern extends @property, AstNode { + PropertyPattern() { + // filter out ordinary properties + exists(ObjectPattern obj | properties(this, obj, _, _, _)) + } + + /** Holds if the name of this property pattern is computed. */ + predicate isComputed() { is_computed(this) } + + /** Gets the expression specifying the name of the matched property. */ + Expr getNameExpr() { result = this.getChildExpr(0) } + + /** Gets the expression the matched property value is assigned to. */ + Expr getValuePattern() { result = this.getChildExpr(1) } + + /** Gets the default value of this property pattern, if any. */ + Expr getDefault() { result = this.getChildExpr(2) } + + /** Holds if this property pattern is a shorthand pattern. */ + predicate isShorthand() { + this.getNameExpr().getLocation() = this.getValuePattern().getLocation() + } + + /** Gets the name of the property matched by this pattern. */ + string getName() { + not this.isComputed() and + result = this.getNameExpr().(Identifier).getName() + or + result = this.getNameExpr().(Literal).getValue() + } + + /** Gets the object pattern this property pattern belongs to. */ + ObjectPattern getObjectPattern() { properties(this, result, _, _, _) } + + /** Holds if this pattern is impure, that is, if its evaluation could have side effects. */ + overlay[global] + predicate isImpure() { + this.isComputed() and this.getNameExpr().isImpure() + or + this.getValuePattern().isImpure() + } + + override string toString() { properties(this, _, _, _, result) } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getNameExpr().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "PropertyPattern" } +} + +/** + * A variable declarator declaring a local or global variable. + * + * Examples: + * + * ``` + * var + * x, // variable declarator + * y = z; // variable declarator + * ``` + */ +class VariableDeclarator extends Expr, @var_declarator { + /** Gets the pattern specifying the declared variable(s). */ + BindingPattern getBindingPattern() { result = this.getChildExpr(0) } + + /** Gets the expression specifying the initial value of the declared variable(s), if any. */ + Expr getInit() { result = this.getChildExpr(1) } + + /** Gets the type annotation for the declared variable or binding pattern. */ + TypeAnnotation getTypeAnnotation() { + result = this.getChildTypeExpr(2) + or + result = this.getDeclStmt().getDocumentation().getATagByTitle("type").getType() + } + + /** Holds if this is a TypeScript variable marked as definitely assigned with the `!` operator. */ + predicate hasDefiniteAssignmentAssertion() { has_definite_assignment_assertion(this) } + + /** Gets the declaration statement this declarator belongs to, if any. */ + DeclStmt getDeclStmt() { this = result.getADecl() } + + override ControlFlowNode getFirstControlFlowNode() { + result = this.getBindingPattern().getFirstControlFlowNode() + } + + override string getAPrimaryQlClass() { result = "VariableDeclarator" } +} + +/** + * For internal use, holding the decorators of a function parameter. + */ +private class DecoratorList extends Expr, @decorator_list { + Decorator getDecorator(int i) { result = this.getChildExpr(i) } + + override ControlFlowNode getFirstControlFlowNode() { + if exists(this.getDecorator(0)) + then result = this.getDecorator(0).getFirstControlFlowNode() + else result = this + } +} + +/** + * A program element that declares parameters, that is, either a function or + * a catch clause. + * + * Examples: + * + * ``` + * function f(x, y) { // a parameterized element + * try { + * g(); + * } catch(e) { // a parameterized element + * } + * } + * ``` + */ +class Parameterized extends @parameterized, Documentable { + /** Gets a parameter declared by this element. */ + Parameter getAParameter() { this = result.getParent() } + + /** Gets the number of parameters declared by this element. */ + int getNumParameter() { result = count(this.getAParameter()) } + + /** Gets a variable of the given name that is a parameter of this element. */ + Variable getParameterVariable(string name) { + result = this.getAParameter().getAVariable() and + result.getName() = name + } +} + +/** + * A parameter declaration in a function or catch clause. + * + * Examples: + * + * ``` + * function f(x, { y: z }, ...rest) { // `x`, `{ y: z }` and `rest` are parameter declarations + * var [ a, b ] = rest; + * var c; + * try { + * x.m(); + * } catch(e) {} // `e` is a parameter declaration + * } + * ``` + */ +class Parameter extends BindingPattern { + Parameter() { + exists(Parameterized p, int n | + this = p.getChildExpr(n) and + n >= 0 + ) + } + + /** + * Gets the index of this parameter within the parameter list of its + * declaring entity. + */ + int getIndex() { exists(Parameterized p | this = p.getChildExpr(result)) } + + /** Gets the element declaring this parameter. */ + override Parameterized getParent() { result = super.getParent() } + + /** Gets the default expression for this parameter, if any. */ + Expr getDefault() { + exists(Function f, int n | this = f.getParameter(n) | result = f.getChildExpr(-(4 * n + 5))) + } + + /** Gets the type annotation for this parameter, if any. */ + override TypeAnnotation getTypeAnnotation() { + exists(Function f, int n | this = f.getParameter(n) | result = f.getChildTypeExpr(-(4 * n + 6))) + or + result = this.getJSDocTag().getType() + } + + /** Holds if this parameter is a rest parameter. */ + predicate isRestParameter() { + exists(Function f, int n | this = f.getParameter(n) | + n = f.getNumParameter() - 1 and + f.hasRestParameter() + ) + } + + private DecoratorList getDecoratorList() { + exists(Function f, int n | this = f.getParameter(n) | result = f.getChildExpr(-(4 * n + 8))) + } + + /** Gets the `i`th decorator applied to this parameter. */ + Decorator getDecorator(int i) { result = this.getDecoratorList().getDecorator(i) } + + /** Gets a decorator applied to this parameter. */ + Decorator getADecorator() { result = this.getDecorator(_) } + + /** Gets the number of decorators applied to this parameter. */ + int getNumDecorator() { result = count(this.getADecorator()) } + + override predicate isLValue() { any() } + + /** + * Gets the JSDoc tag describing this parameter, if any. + */ + JSDocTag getJSDocTag() { + none() // overridden in SimpleParameter + } + + /** + * Holds if this is a parameter declared optional with the `?` token. + * + * Note that this does not hold for rest parameters, and does not in general + * hold for parameters with defaults. + * + * For example, `x`, is declared optional below: + * ``` + * function f(x?: number) {} + * ``` + */ + predicate isDeclaredOptional() { is_optional_parameter_declaration(this) } + + override string getAPrimaryQlClass() { result = "Parameter" } +} + +/** + * A parameter declaration that is not an object or array pattern. + * + * Examples: + * + * ``` + * function f(x, { y: z }, ...rest) { // `x` and `rest` are simple parameter declarations + * var [ a, b ] = rest; + * var c; + * } + * ``` + */ +class SimpleParameter extends Parameter, VarDecl { + override predicate isLValue() { Parameter.super.isLValue() } + + override JSDocParamTag getJSDocTag() { + exists(Function fun | + this = fun.getAParameter() and + result = fun.getDocumentation().getATag() + ) and + // Avoid joining on name + exists(string tagName, string paramName | + tagName = result.getName() and + paramName = this.getName() and + tagName <= paramName and + paramName <= tagName + ) + } + + override string getAPrimaryQlClass() { result = "SimpleParameter" } +} + +/** + * A constructor parameter that induces a field in its class. + * + * Example: + * + * ``` + * class C { + * constructor( + * public x: number // constructor parameter + * ) {} + * } + * ``` + */ +class FieldParameter extends SimpleParameter { + FieldParameter() { exists(ParameterField field | field.getParameter() = this) } + + /** Gets the field induced by this parameter. */ + ParameterField getField() { result.getParameter() = this } + + override string getAPrimaryQlClass() { result = "FieldParameter" } +} + +/** + * A string representing one of the three TypeScript declaration spaces: `variable`, `type`, or `namespace`. + */ +class DeclarationSpace extends string { + DeclarationSpace() { this = "variable" or this = "type" or this = "namespace" } +} + +/** Module containing the `DeclarationSpace` constants. */ +module DeclarationSpace { + /** Gets the declaration space for variables/values. */ + DeclarationSpace variable() { result = "variable" } + + /** Gets the declaration space for types. */ + DeclarationSpace type() { result = "type" } + + /** Gets the declaration space for namespaces. */ + DeclarationSpace namespace() { result = "namespace" } +} + +/** + * A name that is declared in a particular scope. + * + * This can be a variable or a local name for a TypeScript type or namespace. + * + * Examples: + * + * ``` + * // `id`, `x` and `String` are lexical names + * function id(x: String) : any { + * return x; + * } + * ``` + */ +class LexicalName extends @lexical_name { + /** Gets the scope in which this name was declared. */ + abstract Scope getScope(); + + /** Gets the name of this variable, type or namespace. */ + abstract string getName(); + + /** Gets a string representation of this element. */ + abstract string toString(); + + /** + * Gets the declaration space this name belongs to. + * + * This can be either `variable`, `type`, or `namespace`. + */ + abstract DeclarationSpace getDeclarationSpace(); +} + +/** + * An identifier that refers to a variable, type, or namespace, or a combination of these. + * + * Example: + * + * ``` + * function id(x: String) : any { // `id`, `x` and `String` are lexical references + * return x; // `x` is a lexical reference + * } + * ``` + */ +class LexicalRef extends Identifier, @lexical_ref { + /** + * Gets any of the names referenced by this identifier. + * + * Note that each identifier may reference up to one lexical name per declaration space. + * For example, a class name declares both a type and a variable. + */ + LexicalName getALexicalName() { + bind(this, result) or + decl(this, result) or + typebind(this, result) or + typedecl(this, result) or + namespacebind(this, result) or + namespacedecl(this, result) + } +} + +/** + * An identifier that declares a variable, type, or namespace, or a combination of these. + * + * Example: + * + * ``` + * function id(x: number) : any { // `id` and `x` are lexical declarations + * return x; + * } + * ``` + */ +class LexicalDecl extends LexicalRef, @lexical_decl { } + +/** + * An identifier that refers to a variable, type, or namespace, or a combination of these, + * in a non-declaring position. + * + * Example: + * + * ``` + * function id(x: String) : any { // `String` is a lexical access + * return x; // `x` is a lexical access + * } + * ``` + */ +class LexicalAccess extends LexicalRef, @lexical_access { } diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/XML.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/XML.qll new file mode 100644 index 000000000000..f2fde43ca8c7 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/XML.qll @@ -0,0 +1,71 @@ +/** + * Provides classes and predicates for working with XML files and their content. + */ +overlay[local?] +module; + +import FileSystem +private import codeql.xml.Xml + +private module Input implements InputSig { + class XmlLocatableBase = @xmllocatable or @xmlnamespaceable; + + predicate xmllocations_(XmlLocatableBase e, Location loc) { xmllocations(e, loc) } + + class XmlParentBase = @xmlparent; + + class XmlNamespaceableBase = @xmlnamespaceable; + + class XmlElementBase = @xmlelement; + + class XmlFileBase = File; + + predicate xmlEncoding_(XmlFileBase f, string enc) { xmlEncoding(f, enc) } + + class XmlDtdBase = @xmldtd; + + predicate xmlDTDs_(XmlDtdBase e, string root, string publicId, string systemId, XmlFileBase file) { + xmlDTDs(e, root, publicId, systemId, file) + } + + predicate xmlElements_( + XmlElementBase e, string name, XmlParentBase parent, int idx, XmlFileBase file + ) { + xmlElements(e, name, parent, idx, file) + } + + class XmlAttributeBase = @xmlattribute; + + predicate xmlAttrs_( + XmlAttributeBase e, XmlElementBase elementid, string name, string value, int idx, + XmlFileBase file + ) { + xmlAttrs(e, elementid, name, value, idx, file) + } + + class XmlNamespaceBase = @xmlnamespace; + + predicate xmlNs_(XmlNamespaceBase e, string prefixName, string uri, XmlFileBase file) { + xmlNs(e, prefixName, uri, file) + } + + predicate xmlHasNs_(XmlNamespaceableBase e, XmlNamespaceBase ns, XmlFileBase file) { + xmlHasNs(e, ns, file) + } + + class XmlCommentBase = @xmlcomment; + + predicate xmlComments_(XmlCommentBase e, string text, XmlParentBase parent, XmlFileBase file) { + xmlComments(e, text, parent, file) + } + + class XmlCharactersBase = @xmlcharacters; + + predicate xmlChars_( + XmlCharactersBase e, string text, XmlParentBase parent, int idx, int isCDATA, XmlFileBase file + ) { + xmlChars(e, text, parent, idx, isCDATA, file) + } +} + +import Make diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/YAML.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/YAML.qll new file mode 100644 index 000000000000..60b202f0515f --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/YAML.qll @@ -0,0 +1,56 @@ +/** + * Provides classes for working with YAML data. + * + * YAML documents are represented as abstract syntax trees whose nodes + * are either YAML values or alias nodes referring to another YAML value. + */ +overlay[local?] +module; + +import minimal +private import codeql.yaml.Yaml as LibYaml + +private module YamlSig implements LibYaml::InputSig { + class LocatableBase extends @yaml_locatable, Locatable { } + + import minimal + + class NodeBase extends Locatable, LocatableBase, @yaml_node { + NodeBase getChildNode(int i) { yaml(result, _, this, i, _, _) } + + string getTag() { yaml(this, _, _, _, result, _) } + + string getAnchor() { yaml_anchors(this, result) } + + override string toString() { yaml(this, _, _, _, _, result) } + } + + class ScalarNodeBase extends NodeBase, @yaml_scalar_node { + int getStyle() { yaml_scalars(this, result, _) } + + string getValue() { yaml_scalars(this, _, result) } + } + + class CollectionNodeBase extends NodeBase, @yaml_collection_node { } + + class MappingNodeBase extends CollectionNodeBase, @yaml_mapping_node { } + + class SequenceNodeBase extends CollectionNodeBase, @yaml_sequence_node { } + + class AliasNodeBase extends NodeBase, @yaml_alias_node { + string getTarget() { yaml_aliases(this, result) } + } + + class ParseErrorBase extends LocatableBase, @yaml_error { + string getMessage() { yaml_errors(this, result) } + } +} + +import LibYaml::Make + +// private class to forward the `toString` etc. predicates from `YamlNode` to `Locatable`. +private class MyYmlNode extends Locatable instanceof YamlNode { + override string getAPrimaryQlClass() { result = YamlNode.super.getAPrimaryQlClass() } + + override string toString() { result = YamlNode.super.toString() } +} diff --git a/javascript/ql/lib/semmle/javascript/internal/unified/minimal/minimal.qll b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/minimal.qll new file mode 100644 index 000000000000..81b6ce99fd95 --- /dev/null +++ b/javascript/ql/lib/semmle/javascript/internal/unified/minimal/minimal.qll @@ -0,0 +1,30 @@ +/** + * Provides classes for working with JavaScript programs, as well as JSON, YAML and HTML. + */ + +import semmle.javascript.internal.unified.minimal.AST +import semmle.javascript.internal.unified.minimal.BasicBlocks +import semmle.javascript.internal.unified.minimal.CFG +import semmle.javascript.internal.unified.minimal.Classes +import semmle.javascript.internal.unified.minimal.Comments +import semmle.javascript.internal.unified.minimal.ES2015Modules +import semmle.javascript.internal.unified.minimal.Expr +import semmle.javascript.internal.unified.minimal.Files +import semmle.javascript.internal.unified.minimal.Functions +import semmle.javascript.internal.unified.minimal.JSDoc +import semmle.javascript.internal.unified.minimal.JSON +import semmle.javascript.internal.unified.minimal.JSX +import semmle.javascript.internal.unified.minimal.Lines +import semmle.javascript.internal.unified.minimal.Locations +import semmle.javascript.internal.unified.minimal.Paths +import semmle.javascript.internal.unified.minimal.Stmt +import semmle.javascript.internal.unified.minimal.Templates +import semmle.javascript.internal.unified.minimal.Tokens +import semmle.javascript.internal.unified.minimal.TypeAnnotations +import semmle.javascript.internal.unified.minimal.TypeScript +import semmle.javascript.internal.unified.minimal.Variables +import semmle.javascript.internal.unified.minimal.XML +import semmle.javascript.internal.unified.minimal.YAML +import semmle.javascript.internal.unified.minimal.PackageJsonEx +import semmle.javascript.internal.unified.minimal.NPM +private import semmle.javascript.internal.unified.minimal.Overlay diff --git a/javascript/ql/lib/utils/test/InlineFlowTestUnified.qll b/javascript/ql/lib/utils/test/InlineFlowTestUnified.qll new file mode 100644 index 000000000000..dbd2892f7922 --- /dev/null +++ b/javascript/ql/lib/utils/test/InlineFlowTestUnified.qll @@ -0,0 +1,25 @@ +/** + * Inline flow tests for JavaScript. + * See `shared/util/codeql/dataflow/test/InlineFlowTest.qll` + */ + +private import semmle.javascript.internal.unified.minimal.minimal +private import semmle.javascript.internal.unified.minimal.Locations +private import codeql.dataflow.test.InlineFlowTest +private import semmle.javascript.internal.unified.JSUnified +private import semmle.javascript.frameworks.data.internal.ApiGraphModelsExtensions as ApiGraphModelsExtensions +private import internal.InlineExpectationsTestImpl + +private module FlowTestImpl implements InputSig { + import utils.test.InlineFlowTestUnifiedUtil + + bindingset[src, sink] + string getArgString(DataFlow2::Node src, DataFlow2::Node sink) { + (if exists(getSourceArgString(src)) then result = getSourceArgString(src) else result = "") and + exists(sink) + } + + predicate interpretModelForTest = ApiGraphModelsExtensions::interpretModelForTest/2; +} + +import InlineFlowTestMake diff --git a/javascript/ql/lib/utils/test/InlineFlowTestUnifiedUtil.qll b/javascript/ql/lib/utils/test/InlineFlowTestUnifiedUtil.qll new file mode 100644 index 000000000000..db2e40f3d131 --- /dev/null +++ b/javascript/ql/lib/utils/test/InlineFlowTestUnifiedUtil.qll @@ -0,0 +1,28 @@ +/** + * Defines the default source and sink recognition for `InlineFlowTest.qll`. + * + * We reuse these predicates in some type-tracking tests that don't wish to bring in the + * test configuration from `InlineFlowTest`. + */ + +private import semmle.javascript.internal.unified.minimal.minimal +private import semmle.javascript.internal.unified.JSUnified + +predicate defaultSource(DataFlow2::Node src) { + exists(InvokeExpr call | + call.getCalleeName() = "source" and + src.isValueOf(call) + ) +} + +predicate defaultSink(DataFlow2::Node sink) { + exists(InvokeExpr call | + call.getCalleeName() = "sink" and + sink.isValueOf(call.getAnArgument()) + ) +} + +bindingset[src] +string getSourceArgString(DataFlow2::Node src) { + result = src.asAstNode().(InvokeExpr).getAnArgument().getStringValue() +} diff --git a/javascript/ql/lib/utils/test/internal/UnifiedInlineExpectationsTestImpl.qll b/javascript/ql/lib/utils/test/internal/UnifiedInlineExpectationsTestImpl.qll new file mode 100644 index 000000000000..1de54475197d --- /dev/null +++ b/javascript/ql/lib/utils/test/internal/UnifiedInlineExpectationsTestImpl.qll @@ -0,0 +1,25 @@ +private import semmle.javascript.internal.unified.minimal.minimal as JS +private import codeql.util.test.InlineExpectationsTest + +module Impl implements InlineExpectationsTestSig { + private import semmle.javascript.internal.unified.minimal.minimal + + final class ExpectationComment = ExpectationCommentImpl; + + class Location = JS::Location; + + abstract private class ExpectationCommentImpl extends Locatable { + abstract string getContents(); + + /** Gets this element's location. */ + Location getLocation() { result = super.getLocation() } + } + + private class JSComment extends ExpectationCommentImpl instanceof Comment { + override string getContents() { result = super.getText() } + } + + private class HtmlComment extends ExpectationCommentImpl instanceof HTML::CommentNode { + override string getContents() { result = super.getText() } + } +} diff --git a/javascript/ql/src/meta/alerts/CallGraph.ql b/javascript/ql/src/meta/alerts/CallGraph.ql index c721e72f6404..2d2862bf6238 100644 --- a/javascript/ql/src/meta/alerts/CallGraph.ql +++ b/javascript/ql/src/meta/alerts/CallGraph.ql @@ -18,4 +18,4 @@ where invoke.(DataFlow::PropRef).getAnAccessorCallee().getFunction() = f and kind = "Accessor call" ) and not f.getTopLevel().isExterns() -select invoke, kind + " to $@", f, f.describe() +select invoke, kind + " to $@", f, f.toString() diff --git a/javascript/ql/src/meta/alerts/CallGraphForComparison.ql b/javascript/ql/src/meta/alerts/CallGraphForComparison.ql index f828e6ed3612..67efd9aed74b 100644 --- a/javascript/ql/src/meta/alerts/CallGraphForComparison.ql +++ b/javascript/ql/src/meta/alerts/CallGraphForComparison.ql @@ -1,5 +1,5 @@ /** - * @name Call graph for comparison + * @name Call graph unified * @description An edge in the call graph. * @kind problem * @problem.severity recommendation @@ -8,13 +8,18 @@ * @precision very-low */ -import javascript -private import semmle.javascript.dataflow.internal.DataFlowPrivate +import semmle.javascript.internal.unified.JSUnified +import semmle.javascript.internal.unified.minimal.minimal -from InvokeExpr invoke, Function f, string kind +from Function f, InvokeExpr invoke, string kind where - viableCallable(any(DataFlowCall call | call.asOrdinaryCall().getInvokeExpr() = invoke)) - .asSourceCallable() = f and - kind = "Call" and + ( + DataFlowInput::CallGraphOutput::viableCallable(any(DataFlowInput::DataFlowCall c | + c.asSourceCall().getUnderlyingInvokeExpr() = invoke + )).asSourceCallable() = f and + kind = "Call" + or + none() and kind = "Accessor call" + ) and not f.getTopLevel().isExterns() select invoke, kind + " to $@", f, f.toString() diff --git a/javascript/ql/src/meta/alerts/FullCallGraphBaseline.ql b/javascript/ql/src/meta/alerts/FullCallGraphBaseline.ql new file mode 100644 index 000000000000..7f5518c4b2c4 --- /dev/null +++ b/javascript/ql/src/meta/alerts/FullCallGraphBaseline.ql @@ -0,0 +1,20 @@ +/** + * @name Call graph baseline + * @description An edge in the call graph. + * @kind problem + * @problem.severity recommendation + * @id js/meta/alerts/full-call-graph + * @tags meta + * @precision very-low + */ + +import javascript +private import semmle.javascript.dataflow.internal.DataFlowPrivate + +from InvokeExpr invoke, Function f, string kind +where + viableCallable(any(DataFlowCall call | call.asOrdinaryCall().getInvokeExpr() = invoke)) + .asSourceCallable() = f and + kind = "Call" and + not f.getTopLevel().isExterns() +select invoke, kind + " to $@", f, f.toString() diff --git a/javascript/ql/src/meta/alerts/FullCallGraphUnified.ql b/javascript/ql/src/meta/alerts/FullCallGraphUnified.ql new file mode 100644 index 000000000000..1090ff70713b --- /dev/null +++ b/javascript/ql/src/meta/alerts/FullCallGraphUnified.ql @@ -0,0 +1,25 @@ +/** + * @name Call graph unified + * @description An edge in the call graph. + * @kind problem + * @problem.severity recommendation + * @id js/meta/alerts/full-call-graph-unified + * @tags meta + * @precision very-low + */ + +import semmle.javascript.internal.unified.JSUnified +import semmle.javascript.internal.unified.minimal.minimal + +from Function f, InvokeExpr invoke, string kind +where + ( + DataFlowInput::CallGraphOutput::viableCallable(any(DataFlowInput::DataFlowCall c | + c.asSourceCall().getUnderlyingInvokeExpr() = invoke + )).asSourceCallable() = f and + kind = "Call" + or + none() and kind = "Accessor call" + ) and + not f.getTopLevel().isExterns() +select invoke, kind + " to $@", f, f.toString() diff --git a/javascript/ql/test/library-tests/FlowSummary/DataFlowConsistency.ql b/javascript/ql/test/library-tests/FlowSummary/DataFlowConsistency.ql index 02dd5540b6fb..6d529e931fd8 100644 --- a/javascript/ql/test/library-tests/FlowSummary/DataFlowConsistency.ql +++ b/javascript/ql/test/library-tests/FlowSummary/DataFlowConsistency.ql @@ -1,2 +1,3 @@ -import javascript -import semmle.javascript.dataflow.internal.DataFlowImplConsistency::Consistency +import semmle.javascript.internal.unified.minimal.minimal +import semmle.javascript.internal.unified.JSUnified +import Consistency diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/accessors.js b/javascript/ql/test/library-tests/UnifiedDataFlow/accessors.js new file mode 100644 index 000000000000..71f76f1f20d6 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/accessors.js @@ -0,0 +1,136 @@ +import 'dummy'; + +function t1() { + class C { + get x() { + return source('t1.1'); + } + } + sink(new C().x); // $ hasValueFlow=t1.1 + sink(new C().x()); // no flow +} + +function t2() { + class C { + set x(v) { + sink(v); // $ hasValueFlow=t2.1 + } + } + new C().x = source('t2.1'); + new C().x(source('t2.2')); // no flow +} + +function t3() { + const obj = { + get x() { + return source('t3.1'); + } + }; + sink(obj.x); // $ hasValueFlow=t3.1 +} + +function t4() { + const obj = { + set x(v) { + sink(v); // $ hasValueFlow=t4.1 + } + }; + obj.x = source('t4.1'); +} + +function t5() { + function C() {} + C.prototype = { + get x() { + return source('t5.1'); + } + } + sink(new C().x); // $ MISSING: hasValueFlow=t5.1 +} + +function t6() { + function C() {} + C.prototype = { + set x(v) { + sink(v); // $ MISSING: hasValueFlow=t6.1 + } + } + new C().x = source('t6.1'); +} + +function t7() { + class C { + get x() { + return source('t7.1'); + } + set x(v) { + sink(v); // $ hasValueFlow=t7.2 + } + } + class D extends C {} + sink(new D().x); // $ hasValueFlow=t7.1 + new D().x = source('t7.2'); +} + +function t8() { + class C { + get x() { + return this.foo(); + } + foo() { + return source('t8.1'); + } + } + sink(new C().x); // $ hasValueFlow=t8.1 +} + +function t9() { + const captured = source('t9.1'); + class C { + get x() { + return captured; + } + } + sink(new C().x); // $ MISSING: hasValueFlow=t9.1 +} + +function t10() { + class C { + constructor(value) { + this.value = value; + } + get x() { + return this.value; + } + } + const taint = source('t10.1'); + sink(new C(taint).x); // $ hasValueFlow=t10.1 + sink(new C("safe").x); // no flow +} + +function t11() { + class Base { + foo() { + return source('t11.1'); + } + } + class Sub extends Base { + get foo() { + return () => source('t11.2'); + } + } + sink(new Sub().foo()); // $ hasValueFlow=t11.2 +} + +function t12() { + class C { + static get foo() { + return source('t12.1'); + } + static set foo(v) { + sink(v); // $ hasValueFlow=t12.2 + } + } + sink(C.foo); // $ hasValueFlow=t12.1 + C.foo = source('t12.2'); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/array-with.js b/javascript/ql/test/library-tests/UnifiedDataFlow/array-with.js new file mode 100644 index 000000000000..34c9763faf76 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/array-with.js @@ -0,0 +1,25 @@ +function t1() { + const arr = [1, 2, 3]; + const newArr = arr.with(1, source('with.1')); + sink(newArr[1]); // $ MISSING: hasValueFlow=with.1 +} + +function t2() { + const arr = [source('with.2.1'), 2, source('with.2.3')]; + const newArr = arr.with(1, 'replaced'); + sink(newArr[0]); // $ MISSING: hasValueFlow=with.2.1 + sink(newArr[2]); // $ MISSING: hasValueFlow=with.2.3 +} + +function t3() { + const arr = [1, 2, 3]; + const index = source('with.3.index'); + const newArr = arr.with(index, 'new value'); + // No assertions here as the index is tainted, not the value +} + +function t4() { + const arr = [1, 2, 3]; + const newArr = arr.with(1, source('with.4')); + sink(arr[1]); // This should NOT have value flow as with() returns a new array +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/arrays.js b/javascript/ql/test/library-tests/UnifiedDataFlow/arrays.js new file mode 100644 index 000000000000..ebc8f8ae184c --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/arrays.js @@ -0,0 +1,46 @@ +import 'dummy'; + +function shiftKnown() { + let array = [source('shift.1'), source('shift.2')]; + sink(array.shift()); // $ MISSING: hasValueFlow=shift.1 + sink(array.shift()); // $ MISSING: hasValueFlow=shift.2 +} + +function shiftUnknown() { + const array = new Array(Math.floor(Math.random() * 10)); + array.push(source('shift.unkn')); + sink(array.shift()); // $ MISSING: hasValueFlow=shift.unkn + sink(array.shift()); // $ MISSING: hasValueFlow=shift.unkn + sink(array.shift()); // $ MISSING: hasValueFlow=shift.unkn +} + +function shiftTaint() { + const array = source('shift.directly-tainted'); + sink(array.shift()); // $ MISSING: hasTaintFlow=shift.directly-tainted + sink(array.shift()); // $ MISSING: hasTaintFlow=shift.directly-tainted + sink(array.shift()); // $ MISSING: hasTaintFlow=shift.directly-tainted +} + +function implicitToString() { + const array = [source('implicitToString.1')]; + array.push(source('implicitToString.2')) + + sink(array + "foo"); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink("foo" + array); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink("" + array); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink(array + 1); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink(1 + array); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink(unknown() + array); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink(array + unknown()); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + + sink(`${array}`); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink(`${array} foo`); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + + sink(String(array)); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + + sink(array.toString()); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink(array.toString("utf8")); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + + sink(Array.prototype.toString.call(array)); // $ MISSING: hasTaintFlow=implicitToString.1 hasTaintFlow=implicitToString.2 + sink(Object.prototype.toString.call(array)); // OK - returns "[object Array]" +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/arrow-function.js b/javascript/ql/test/library-tests/UnifiedDataFlow/arrow-function.js new file mode 100644 index 000000000000..b166c93fbff4 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/arrow-function.js @@ -0,0 +1,14 @@ +import 'dummy'; + +function t1() { + class C { + foo() { + const fn = () => { + sink(this.bar()); // $ hasValueFlow=t1.1 + } + } + bar() { + return source('t1.1'); + } + } +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/assumed-receiver.js b/javascript/ql/test/library-tests/UnifiedDataFlow/assumed-receiver.js new file mode 100644 index 000000000000..ec7fc01c2cc9 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/assumed-receiver.js @@ -0,0 +1,41 @@ +import 'dummy'; + +function t1() { + class C { + foo() { + const self = this; + function h() { + self.bar(source("t1.1")); + } + } + bar(x) { + sink(x); // $ hasValueFlow=t1.1 + } + } +} + +function t2() { + function Foo() {} + + Foo.prototype.first = function(x) { + sink(x); // $ hasValueFlow=t2.1 + } + + Foo.prototype.second = function() { + this.first(source("t2.1")); + } +} + +function t3() { + function Foo() {} + + Foo.prototype = {}; + + Foo.prototype.first = function(x) { + sink(x); // $ hasValueFlow=t3.1 + } + + Foo.prototype.second = function() { + this.first(source("t3.1")); + } +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/augmented-library-object.js b/javascript/ql/test/library-tests/UnifiedDataFlow/augmented-library-object.js new file mode 100644 index 000000000000..65915be4363f --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/augmented-library-object.js @@ -0,0 +1,19 @@ +import * as express from 'express'; + +function createApp() { + const app = express(); + app.helper = function() { + return source('t1.1'); + } + return app; +} + +function t1() { + const app = createApp(); + sink(app.helper()); // $ hasValueFlow=t1.1 + + function helper(appArg) { + sink(appArg.helper()); // $ hasValueFlow=t1.1 + } + helper(app); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/buffer.js b/javascript/ql/test/library-tests/UnifiedDataFlow/buffer.js new file mode 100644 index 000000000000..71369546b7e7 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/buffer.js @@ -0,0 +1,7 @@ +import 'dummy'; + +function t1() { + const b1 = Buffer.from(source("t1.1")); + const b2 = Buffer.from(source("t1.2")); + sink(Buffer.concat([b1, b2]).toString("utf8")); // $ MISSING: hasTaintFlow=t1.1 hasTaintFlow=t1.2 +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/callbacks.js b/javascript/ql/test/library-tests/UnifiedDataFlow/callbacks.js new file mode 100644 index 000000000000..e62888460309 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/callbacks.js @@ -0,0 +1,81 @@ +import 'dummy'; + +function t1() { + function simpleCallback(value, callback) { + callback(value); + } + class C { + foo(x) { + sink(x); // $ hasValueFlow=t1.1 + } + } + class D { + foo(x) { + sink(x); // $ hasValueFlow=t1.2 + } + } + simpleCallback( + new C(), + c => c.foo(source('t1.1')) + ); + simpleCallback( + new D(), + d => d.foo(source('t1.2')) + ); +} + +function t2() { + class C { + foo(x) { + sink(x); // $ hasValueFlow=t2.1 + } + } + function simpleCallback(callback) { + callback(new C()); + } + simpleCallback(c => c.foo(source('t2.1'))); +} + + +function t3() { + class C { + foo(x) { + sink(x); // $ hasValueFlow=t3.1 + } + } + function simpleCallback(callback) { + callback(new C()); + } + function h() { + let captured = null; + simpleCallback(c => { captured = c; }); + return captured; + } + h().foo(source('t3.1')); +} + +function t4() { + function customForEach(values, callback) { + for (let value of values) { + callback(value); + } + } + class C { + foo(x) { + sink(x); // $ MISSING: hasValueFlow=t4.1 + } + } + class D { + foo(x) { + sink(x); // $ MISSING: hasValueFlow=t4.2 + } + } + customForEach( + [new C()], + c => c.foo(source('t4.1')) + ); + customForEach( + [new D()], + d => d.foo(source('t4.2')) + ); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/class-flow.js b/javascript/ql/test/library-tests/UnifiedDataFlow/class-flow.js new file mode 100644 index 000000000000..78162c34ba92 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/class-flow.js @@ -0,0 +1,28 @@ +import 'dummy'; + +function t1() { + class C { + foo(x) { + sink(x); // $ hasValueFlow=f1 + } + } + + function f1(c) { + c.foo(source("f1")); + } + function f2() { + return new C(); + } + f1(f2()); +} + +function t2() { + class C { + foo() { + this.fn = () => source("f2"); + } + bar() { + sink(this.fn()); // $ hasValueFlow=f2 + } + } +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/base-class-def.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/base-class-def.js new file mode 100644 index 000000000000..1edb80d29741 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/base-class-def.js @@ -0,0 +1,5 @@ +export class Base { + foo() { + return source('Base.foo'); + } +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/base-class-re-export.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/base-class-re-export.js new file mode 100644 index 000000000000..1795d9d72b04 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/base-class-re-export.js @@ -0,0 +1 @@ +export * from "./base-class-def.js"; diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/subclass.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/subclass.js new file mode 100644 index 000000000000..13ec115720ce --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exported-base-class/subclass.js @@ -0,0 +1,5 @@ +import { Base } from './base-class-re-export'; + +class Subclass extends Base {} + +sink(new Subclass().foo()); // $ hasValueFlow=Base.foo diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exporting.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exporting.js new file mode 100644 index 000000000000..bb7c9fbd5b0d --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/bulk-exporting.js @@ -0,0 +1,7 @@ +export * from './file2.js'; + +export * as file2 from './file2.js'; + +export function foo() { + return source('shadowing.foo'); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/file1.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/file1.js new file mode 100644 index 000000000000..2e06eaa48172 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/file1.js @@ -0,0 +1,17 @@ +require('./file2.js').f1(source('file1.1')); + +const m = require('./file2.js'); +const c = new m.C(); +c.f2(source('file1.2')); + +require('./file2.js').f3(source('file1.3')); +require('./file2.js').f4(source('file1.4')); +require('./file2.js').f5(source('file1.5')); +require('./function-raw-export.js')(source('file1.6')); + +sink(require('./file2.js').foo()); // $ hasValueFlow=reExported.foo +sink(require('./file2.js').bar()); // $ hasValueFlow=reExported.foo +sink(require('./bulk-exporting.js').foo()); // $ hasValueFlow=shadowing.foo +sink(require('./bulk-exporting.js').bar()); // $ hasValueFlow=reExported.foo +sink(require('./bulk-exporting.js').file2.foo()); // $ hasValueFlow=reExported.foo +sink(require('./bulk-exporting.js').file2.bar()); // $ hasValueFlow=reExported.foo diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/file2.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/file2.js new file mode 100644 index 000000000000..fe5491d29f3d --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/file2.js @@ -0,0 +1,30 @@ +export function f1(x) { + sink(x); // $ hasValueFlow=file1.1 +} + +export class C { + f2(x) { + sink(x); // $ hasValueFlow=file1.2 + } +} + +exports.f3 = function(x) { + sink(x); // $ hasValueFlow=file1.3 +} + +module.exports.f4 = function(x) { + sink(x); // $ hasValueFlow=file1.4 +} + +module.exports = { + f5: function(x) { + sink(x); // $ hasValueFlow=file1.5 + } +} + +module.exports = function f6() { + sink(x); // $ MISSING: hasValueFlow=file1.6 +} + +export { foo } from './reExported.js'; +export { foo as bar } from './reExported.js'; diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/function-raw-export.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/function-raw-export.js new file mode 100644 index 000000000000..eee450b4e2cb --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/function-raw-export.js @@ -0,0 +1,3 @@ +module.exports = function f6(x) { + sink(x); // $ hasValueFlow=file1.6 +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/lib.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/lib.js new file mode 100644 index 000000000000..9a7dc3227b0f --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/lib.js @@ -0,0 +1,5 @@ +const fs = require('fs'); + +module.exports = function(x) { + return source('hardlink.foo'); +}; diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/reexport.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/reexport.js new file mode 100644 index 000000000000..959ef626e223 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/reexport.js @@ -0,0 +1,3 @@ +const fs = require('fs'); + +module.exports = require('./lib.js'); diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/use.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/use.js new file mode 100644 index 000000000000..78bcc7020e1a --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/hard-link-reexport/use.js @@ -0,0 +1 @@ +sink(require('./lib')()); // $ hasValueFlow=hardlink.foo diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/reExported.js b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/reExported.js new file mode 100644 index 000000000000..e3cbbb333746 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/cross-file/reExported.js @@ -0,0 +1,3 @@ +export function foo(x) { + return source("reExported.foo"); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/curried.js b/javascript/ql/test/library-tests/UnifiedDataFlow/curried.js new file mode 100644 index 000000000000..b2100d76d153 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/curried.js @@ -0,0 +1,16 @@ +import 'dummy'; + +function t1() { + function getInner() { + return () => source('t1.1'); + } + const x = getInner()(); + sink(x); // $ hasValueFlow=t1.1 +} + +function t2() { + function getInner() { + return (x) => sink(x); // $ hasValueFlow=t2.1 + } + getInner()(source('t2.1')); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/export-default-only.js b/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/export-default-only.js new file mode 100644 index 000000000000..f077224b3e8a --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/export-default-only.js @@ -0,0 +1,3 @@ +export default { + foo() { return source('default.foo'); } +}; diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/export-named-only.js b/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/export-named-only.js new file mode 100644 index 000000000000..59b1e0119f7d --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/export-named-only.js @@ -0,0 +1,3 @@ +export function foo() { + return source('named.foo'); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/import.js b/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/import.js new file mode 100644 index 000000000000..581618ee44cd --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/default-interop/import.js @@ -0,0 +1,17 @@ + +// Standard "default" export semantics +import defaultImport from "./export-default-only.js"; +sink(defaultImport.foo()); // $ hasValueFlow=default.foo + +// Interop: the exported object can also refer to the default export +import * as ns from "./export-default-only.js"; +sink(ns.default.foo()); // $ hasValueFlow=default.foo +sink(ns.foo()); // $ hasValueFlow=default.foo + +// Standard named export semantics +import * as ns2 from "./export-named-only.js"; +sink(ns2.foo()); // $ hasValueFlow=named.foo + +// Interop: default-importing a module with only named exports gets the exported object +import namedAsDefault from "./export-named-only.js"; +sink(namedAsDefault.foo()); // $ hasValueFlow=named.foo diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/exceptions.js b/javascript/ql/test/library-tests/UnifiedDataFlow/exceptions.js new file mode 100644 index 000000000000..1a2597b6455f --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/exceptions.js @@ -0,0 +1,81 @@ +import 'dummy'; + +function e1() { + let array = [source('e1.1')]; + try { + array.forEach(x => { + throw x; + }); + array.forEach(x => { + throw source('e1.2'); + }); + array.forEach(() => { + throw source('e1.3'); // Same as e1.2 but without callback parameters + }); + } catch (err) { + sink(err); // $ MISSING: hasValueFlow=e1.1 hasValueFlow=e1.2 hasValueFlow=e1.3 + } +} + +function e2() { + let array = [source('e2.1')]; + try { + array.unknown(x => { + throw x; + }); + array.unknown(x => { + throw source('e2.2'); + }); + } catch (err) { + sink(err); // $ MISSING: hasValueFlow=e2.2 + } +} + +function e3() { + const events = getSomething(); + try { + events.addEventListener('click', () =>{ + throw source('e3.1'); + }); + events.addListener('click', () =>{ + throw source('e3.2'); + }); + events.on('click', () =>{ + throw source('e3.3'); + }); + events.unknownMethod('click', () =>{ + throw source('e3.4'); + }); + } catch (err) { + sink(err); // $ MISSING: hasValueFlow=e3.4 + } +} + +function e4() { + function thrower(array) { + array.forEach(x => { throw x }); + } + try { + thrower([source("e4.1")]); + } catch (e) { + sink(e); // $ MISSING: hasValueFlow=e4.1 + } + try { + thrower(["safe"]); + } catch (e) { + sink(e); + } +} + +async function e5() { + try { + Promise.resolve(0).finally(() => { + throw source("e5.1"); + }); + await Promise.resolve(0).finally(() => { + throw source("e5.2"); + }); + } catch (e) { + sink(e); // $ MISSING: hasValueFlow=e5.2 + } +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/field-parameter.ts b/javascript/ql/test/library-tests/UnifiedDataFlow/field-parameter.ts new file mode 100644 index 000000000000..502acf359549 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/field-parameter.ts @@ -0,0 +1,15 @@ +import 'dummy'; + +function t1() { + class FieldParam { + constructor(public field: any) { + this.field.foo(source('t1')); + } + } + class C { + foo(x: any) { + sink(x); // $ hasValueFlow=t1 + } + } + new FieldParam(new C()); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/fields.js b/javascript/ql/test/library-tests/UnifiedDataFlow/fields.js new file mode 100644 index 000000000000..6e6e6cef07ae --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/fields.js @@ -0,0 +1,58 @@ +function t1() { + class C { + static x = source('t1.1'); + } + sink(C.x); // $ hasValueFlow=t1.1 +} + +function t2() { + class C { + static x = source('t2.1'); + static y = sink(C.x); // $ hasValueFlow=t2.1 + } +} + +function t3() { + class C { + static y = sink(C.x); // no flow + static x = source('t3.1'); + } +} + +function t4() { + class C { + x = source('t4.1'); + } + sink(C.x); // no flow + sink(new C().x); // $ hasValueFlow=t4.1 +} + +function t5() { + class C { + constructor() { + this.x = source('t5.1'); + } + } + sink(C.x); // no flow + sink(new C().x); // $ hasValueFlow=t5.1 +} + +function t6() { + class C { + a = sink(this.x); // no flow + x = source('t6.1'); + y = sink(this.x); // $ hasValueFlow=t6.1 + z = this.x; + w = sink(this.z); // $ hasValueFlow=t6.1 + } + sink(new C().z); // $ hasValueFlow=t6.1 +} + +function t7() { + class C { + x = source('t7.1'); + constructor() { + sink(this.x); // $ hasValueFlow=t7.1 + } + } +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/flow-through.js b/javascript/ql/test/library-tests/UnifiedDataFlow/flow-through.js new file mode 100644 index 000000000000..c82bfd9f2360 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/flow-through.js @@ -0,0 +1,18 @@ +import 'dummy'; + +function identity(t) { + return t; +} +class C { + foo(x) { + return x; + } + self() { + return this; + } +} + +const c = new C(); +sink(c.foo(source('t.1'))); // $ hasValueFlow=t.1 +sink(identity(c).foo(source('t.2'))); // $ hasValueFlow=t.2 +sink(c.self().foo(source('t.3'))); // $ hasValueFlow=t.3 diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/iife.js b/javascript/ql/test/library-tests/UnifiedDataFlow/iife.js new file mode 100644 index 000000000000..1697ce8d3342 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/iife.js @@ -0,0 +1,82 @@ +function f1() { + function inner(x) { + return (function(p) { + return p; // argument to return + })(x); + } + sink(inner(source("f1.1"))); // $ hasValueFlow=f1.1 + sink(inner(source("f1.2"))); // $ hasValueFlow=f1.2 +} + +function f2() { + function inner(x) { + let y; + (function(p) { + y = p; // parameter to captured variable + })(x); + return y; + } + sink(inner(source("f2.1"))); // $ hasValueFlow=f2.1 + sink(inner(source("f2.2"))); // $ hasValueFlow=f2.2 +} + +function f3() { + function inner(x) { + return (function() { + return x; // captured variable to return + })(); + } + sink(inner(source("f3.1"))); // $ hasValueFlow=f3.1 + sink(inner(source("f3.2"))); // $ hasValueFlow=f3.2 +} + +function f4() { + function inner(x) { + let y; + (function() { + y = x; // captured variable to captured variable + })(); + return y; + } + sink(inner(source("f4.1"))); // $ hasValueFlow=f4.1 + sink(inner(source("f4.2"))); // $ hasValueFlow=f4.2 +} + +function f5() { + function inner(x) { + let y; + function nested(p) { + y = p; + } + nested(x); + return y; + } + sink(inner(source("f5.1"))); // $ hasValueFlow=f5.1 + sink(inner(source("f5.2"))); // $ hasValueFlow=f5.2 +} + +function f6() { + function inner(x) { + let y; + function nested(p) { + y = p; + } + (nested)(x); // same as f5, except the callee is parenthesised here + return y; + } + sink(inner(source("f6.1"))); // $ hasValueFlow=f6.1 + sink(inner(source("f6.2"))); // $ hasValueFlow=f6.2 +} + +function f7() { + function inner(x) { + let y; + let nested = (function (p) { + y = p; + }); + nested(x); // same as f5, except the function definition is parenthesised + return y; + } + sink(inner(source("f7.1"))); // $ hasValueFlow=f7.1 + sink(inner(source("f7.2"))); // $ hasValueFlow=f7.2 +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/inheritance.js b/javascript/ql/test/library-tests/UnifiedDataFlow/inheritance.js new file mode 100644 index 000000000000..e859ad8a6add --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/inheritance.js @@ -0,0 +1,106 @@ +function t1() { + class Base { + x = source('t1.1'); + } + class Derived extends Base {} + sink(new Derived().x); // $ hasValueFlow=t1.1 +} + +function t2() { + class Base { + constructor() { + this.x = source('t2.1'); + } + } + class Derived extends Base {} + sink(new Derived().x); // $ hasValueFlow=t2.1 +} + +function t3() { + class Base { + x = source('t3.1'); + + constructor() { + this.y = source('t3.2'); + } + } + class Derived extends Base { + z = sink(this.x); // $ hasValueFlow=t3.1 + w = sink(this.y); // $ hasValueFlow=t3.2 + } + class Derived2 extends Base { + constructor() { + super(); + sink(this.x); // $ hasValueFlow=t3.1 + sink(this.y); // $ hasValueFlow=t3.2 + } + } +} + +function t4() { + class C1 { + foo() { + return source('C1.foo'); + } + baz() { + return source('C1.baz'); + } + } + class C2 extends C1 { + foo() { + return source('C2.foo'); + } + bar() { + return source('C2.bar'); + } + } + class C3 extends C2 { + baz() { + return source('C3.baz'); + } + } + sink(new C1().foo()); // $ hasValueFlow=C1.foo + sink(new C1().bar()); // not defined + sink(new C1().baz()); // $ hasValueFlow=C1.baz + + sink(new C2().foo()); // $ hasValueFlow=C2.foo + sink(new C2().bar()); // $ hasValueFlow=C2.bar + sink(new C2().baz()); // $ hasValueFlow=C1.baz + + sink(new C3().foo()); // $ hasValueFlow=C2.foo + sink(new C3().bar()); // $ hasValueFlow=C2.bar + sink(new C3().baz()); // $ hasValueFlow=C3.baz +} + +function t5() { + class C { + foo() { + sink(this.bar()); // $ hasValueFlow=C.bar + } + bar() { + return source('C.bar'); + } + } +} + +function t6() { + class Base { + foo() { + sink(this.bar()); // $ hasValueFlow=Sub.bar + } + } + class Sub extends Base { + bar() { + return source('Sub.bar'); + } + } +} + +function t7() { + function Base() {} + Base.prototype.foo = function(x) { + sink(x); // $ hasValueFlow=t7.1 + } + + new Base().foo(source('t7.1')); +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/jsx.jsx b/javascript/ql/test/library-tests/UnifiedDataFlow/jsx.jsx new file mode 100644 index 000000000000..851ff9eab6ae --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/jsx.jsx @@ -0,0 +1,26 @@ +import 'dummy'; + +function t1() { + function Foo(props) { + sink(props.field1); // $ hasValueFlow=t1.1 + sink(props.field2); // safe + return
foo
; + } + + function Bar() { + const taint = source("t1.1"); + return ; + } +} + +function t2() { + function WithCallback({callback}) { + callback(source("t2.1")); + return
with callback
; + } + function Caller() { + return { + sink(x); // $ hasValueFlow=t2.1 + }} />; + } +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/promise-try.js b/javascript/ql/test/library-tests/UnifiedDataFlow/promise-try.js new file mode 100644 index 000000000000..579d87d8c1a3 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/promise-try.js @@ -0,0 +1,29 @@ +async function t1() { + const promise = Promise.try(() => { + return source('try.1'); + }); + sink(await promise); // $ MISSING: hasValueFlow=try.1 +} + +async function t2() { + const promise = Promise.try((x) => { + return x + }, source('try.2')); + sink(await promise); // $ MISSING: hasValueFlow=try.2 +} + +async function t3() { + const promise = Promise.try((x) => { + throw x; + }, source('try.3')); + promise.catch(err => { + sink(err); // $ MISSING: hasValueFlow=try.3 + }); +} + +async function t4() { + const promise = Promise.try((x, y) => { + return y; + }, source('try.4.1'), source('try.4.2')); + sink(await promise); // $ MISSING: hasValueFlow=try.4.2 +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/react-use.js b/javascript/ql/test/library-tests/UnifiedDataFlow/react-use.js new file mode 100644 index 000000000000..294a4f303eba --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/react-use.js @@ -0,0 +1,12 @@ +import { use } from "react"; + +async function fetchData() { + return new Promise((resolve) => { + resolve(source("fetchedData")); + }); +} + +function Component() { + const data = use(fetchData()); + sink(data); // $ MISSING: hasValueFlow=fetchedData +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/side-effect.js b/javascript/ql/test/library-tests/UnifiedDataFlow/side-effect.js new file mode 100644 index 000000000000..62d549d14e30 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/side-effect.js @@ -0,0 +1,74 @@ +import 'dummy'; + +function t1() { + function change(x) { + x.foo = source('t1.1'); + } + const o = {}; + change(o); + sink(o.foo); // $ hasValueFlow=t1.1 +} + +function t2() { + class C { + constructor() { + this.foo = source('t2.1'); + } + } + const o = new C(); + sink(o.foo); // $ hasValueFlow=t2.1 +} + +function t3() { + class C { + foo = source('t3.1'); + } + const o = new C(); + sink(o.foo); // $ hasValueFlow=t3.1 +} + +function t4() { + function change(x) { + x.foo = source('t4.1'); + } + function change2(safe, x) { + change(x); + } + const o1 = {}; + const o2 = {}; + change2(o1, o2); + sink(o1.foo); // no flow here + sink(o2.foo); // $ hasValueFlow=t4.1 +} + +function t5() { + class C { + foo = source('t5.1'); + } + class D extends C { + constructor() { + super(); + } + } + const o = new D(); + sink(o.foo); // $ hasValueFlow=t5.1 +} + +function t6() { + class C { + foo = source('t6.1'); + } + class D extends C { + } + const o = new D(); + sink(o.foo); // $ hasValueFlow=t6.1 +} + +function t7() { + class C { + foo = source('t7.1'); + } + class D extends C { + } + sink(new D().foo); // $ hasValueFlow=t7.1 +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/simple.js b/javascript/ql/test/library-tests/UnifiedDataFlow/simple.js new file mode 100644 index 000000000000..b4471eec5480 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/simple.js @@ -0,0 +1,97 @@ +test("flow through direct assignment", function() { + sink(source('t1.1')); // $ hasValueFlow=t1.1 + + var x = source('t1.2'); + sink(x); // $ hasValueFlow=t1.2 + + var y; + sink(y); + y = source('t1.3'); + sink(y); // $ hasValueFlow=t1.3 + + var z = source('t1.4'); + var w = z; + sink(w); // $ hasValueFlow=t1.4 +}); + +test("flow in and out of function", function() { + function foo(x) { + return x; + } + + var y = foo(source('t2.0')); + sink(y); // $ hasValueFlow=t2.0 +}); + +test("flow into function body", function() { + function foo(x) { + sink(x); // $ hasValueFlow=t2.0a + } + + foo(source('t2.0a')); +}); + +test("flow out of nested function body", function() { + function foo() { + return source('t2.0b'); + } + + var y = foo(); + sink(y); // $ hasValueFlow=t2.0b +}); + +test("flow through captured variables", function() { + var x = source('t2.1'); + + function inner() { + sink(x); // $ hasValueFlow=t2.1 + } + + inner(); +}); + +test("flow through captured variable with reassignment", function() { + var x; + x = source('t2.2'); + + function inner() { + sink(x); // $ hasValueFlow=t2.2 + } + + inner(); +}); + +test("flow through captured variable assigned in inner function", function() { + var x; + + function assign() { + x = source('t2.3'); + } + + assign(); + sink(x); // $ hasValueFlow=t2.3 +}); + +test("flow through multiple levels of captured variables", function() { + var x = source('t2.4'); + + function mid() { + function inner() { + sink(x); // $ hasValueFlow=t2.4 + } + inner(); + } + + mid(); +}); + +test("flow through captured variable with intermediate variable", function() { + var x = source('t2.5'); + + function inner() { + var y = x; + sink(y); // $ hasValueFlow=t2.5 + } + + inner(); +}); diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/subclass.js b/javascript/ql/test/library-tests/UnifiedDataFlow/subclass.js new file mode 100644 index 000000000000..6a7f3f593fa0 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/subclass.js @@ -0,0 +1,34 @@ +import 'dummy'; + +class Base { + baseMethod(x) { + this.subclassMethod(x); + } +} + +class Subclass1 extends Base { + work() { + this.baseMethod(source("sub1")); + } + subclassMethod(x) { + sink(x); // $ hasValueFlow=sub1 SPURIOUS: hasValueFlow=sub2 + } +} + +class Subclass2 extends Base { + work() { + this.baseMethod(source("sub2")); + } + subclassMethod(x) { + sink(x); // $ hasValueFlow=sub2 SPURIOUS: hasValueFlow=sub1 + } +} + +class Subclass3 extends Base { + work() { + this.baseMethod("safe"); + } + subclassMethod(x) { + sink(x); // $ SPURIOUS: hasValueFlow=sub1 hasValueFlow=sub2 + } +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/test.expected b/javascript/ql/test/library-tests/UnifiedDataFlow/test.expected new file mode 100644 index 000000000000..5551eebec89b --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/test.expected @@ -0,0 +1,2 @@ +testFailures +dummy diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/test.ql b/javascript/ql/test/library-tests/UnifiedDataFlow/test.ql new file mode 100644 index 000000000000..dc80867f2d0b --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/test.ql @@ -0,0 +1,4 @@ +import utils.test.InlineFlowTestUnified +import DefaultFlowTest + +query predicate dummy() { any() } diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/tst.js b/javascript/ql/test/library-tests/UnifiedDataFlow/tst.js new file mode 100644 index 000000000000..f24caa55998d --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/tst.js @@ -0,0 +1,174 @@ +import 'dummy'; + +function t1() { + function target(...rest) { + sink(rest[0]); // $ hasValueFlow=t1.1 + sink(rest[1]); // $ hasValueFlow=t1.2 + sink(rest.join(',')); // $ MISSING: hasTaintFlow=t1.1 hasTaintFlow=t1.2 + } + target(source('t1.1'), source('t1.2')); +} + +function t2() { + function target(x, ...rest) { + sink(x); // $ hasValueFlow=t2.1 + sink(rest.join(',')); // $ MISSING: hasTaintFlow=t2.2 hasTaintFlow=t2.3 + } + target(source('t2.1'), source('t2.2'), source('t2.3')); +} + +function t3() { + function finalTarget(x, y, z) { + sink(x); // $ hasValueFlow=t3.1 + sink(y); // $ hasValueFlow=t3.2 + sink(z); // $ hasValueFlow=t3.3 + } + function target(...rest) { + finalTarget(...rest); + } + target(source('t3.1'), source('t3.2'), source('t3.3')); +} + +function t4() { + function finalTarget(w, x, y, z) { + sink(w); // $ hasValueFlow=t4.0 + sink(x); // $ hasValueFlow=t4.1 + sink(y); // $ hasValueFlow=t4.2 + sink(z); // $ hasValueFlow=t4.3 + } + function target(...rest) { + finalTarget(source('t4.0'), ...rest); + } + target(source('t4.1'), source('t4.2'), source('t4.3')); +} + +function t5() { + function finalTarget(w, x, y, z) { + sink(w); // $ hasValueFlow=t5.0 + sink(x); // $ hasValueFlow=t5.1 + sink(y); // $ hasValueFlow=t5.2 + sink(z); // $ hasValueFlow=t5.3 + } + function target(array) { + finalTarget(source('t5.0'), ...array); + } + target([source('t5.1'), source('t5.2'), source('t5.3')]); +} + +function t6() { + function target(x) { + sink(x); // $ hasValueFlow=t6.1 + sink(arguments[0]);// $ hasValueFlow=t6.1 + sink(arguments[1]);// $ hasValueFlow=t6.2 + sink(arguments[2]);// $ hasValueFlow=t6.3 + } + target(source('t6.1'), source('t6.2'), source('t6.3')); +} + +function t7() { + function finalTarget(x, y, z) { + sink(x); // $ hasValueFlow=t7.1 + sink(y); // $ hasValueFlow=t7.2 + sink(z); // $ hasValueFlow=t7.3 + } + function target() { + finalTarget(...arguments); + } + target(source('t7.1'), source('t7.2'), source('t7.3')); +} + +function t8() { + function finalTarget(x, y, z) { + sink(x); // $ hasValueFlow=t8.1 SPURIOUS: hasValueFlow=t8.3 hasValueFlow=t8.4 + sink(y); // $ hasValueFlow=t8.2 SPURIOUS: hasValueFlow=t8.3 hasValueFlow=t8.4 + sink(z); // $ hasValueFlow=t8.3 SPURIOUS: hasValueFlow=t8.4 + } + function target(array1, array2) { + finalTarget(...array1, ...array2); + } + target([source('t8.1'), source('t8.2')], [source('t8.3'), source('t8.4')]); +} + +function t9() { + function finalTarget(x, y, z) { + sink(x); // $ hasValueFlow=t9.1 + sink(y); // $ hasValueFlow=t9.2 + sink(z); // $ hasValueFlow=t9.3 + } + function target() { + finalTarget.apply(undefined, arguments); + } + target(source('t9.1'), source('t9.2'), source('t9.3')); +} + +function t10() { + function finalTarget(x, y, z) { + sink(x); // $ hasValueFlow=t10.1 + sink(y); // $ hasValueFlow=t10.2 + sink(z); // $ hasValueFlow=t10.3 + } + function target(...rest) { + finalTarget.apply(undefined, rest); + } + target(source('t10.1'), source('t10.2'), source('t10.3')); +} + +function t11() { + function target(x, y) { + sink(x); // $ MISSING: hasTaintFlow=t11.1 + sink(y); // $ MISSING: hasTaintFlow=t11.1 + } + target(...source('t11.1')); +} + +function t12() { + function target(x, y) { + sink(x); // $ SPURIOUS: hasTaintFlow=t12.1 + sink(y); // $ hasTaintFlow=t12.1 + } + target("safe", ...source('t12.1')); +} + +function t13() { + function target(x, y, ...rest) { + sink(x); // $ SPURIOUS: hasTaintFlow=t13.1 + sink(y); // $ hasTaintFlow=t13.1 + sink(rest); // $ MISSING: hasTaintFlow=t13.1 + sink(rest[0]); // $ hasTaintFlow=t13.1 + } + target("safe", ...source('t13.1')); +} + +function t14() { + function target(x, y, ...rest) { + sink(x); // $ MISSING: hasValueFlow=t14.1 + sink(y); // $ MISSING: hasValueFlow=t14.1 + sink(rest.pop()); // $ MISSING: hasValueFlow=t14.1 + sink(rest); // $ MISSING: hasTaintFlow=t14.1 + } + const args = new Array(Math.floor(Math.random() * 10)); + args.push(source('t14.1')); + target(...args); +} + +function t15() { + function target(safe, x, y, ...rest) { + sink(safe); + sink(x); // $ MISSING: hasValueFlow=t15.1 + sink(y); // $ MISSING: hasValueFlow=t15.1 + sink(rest.pop()); // $ MISSING: hasValueFlow=t15.1 + sink(rest); // $ MISSING: hasTaintFlow=t15.1 + } + const args = new Array(Math.floor(Math.random() * 10)); + args.push(source('t15.1')); + target('safe', ...args); +} + +function t16() { + let array = new Array(Math.floor(Math.random() * 10)) + array.push(source("t16.1")); + sink(array[0]); // $ MISSING: hasValueFlow=t16.1 + sink(array[1]); // $ MISSING: hasValueFlow=t16.1 + sink(array[2]); // $ MISSING: hasValueFlow=t16.1 + sink(array); // $ MISSING: hasTaintFlow=t16.1 +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/underscore.string.js b/javascript/ql/test/library-tests/UnifiedDataFlow/underscore.string.js new file mode 100644 index 000000000000..41ab5813a490 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/underscore.string.js @@ -0,0 +1,130 @@ +var s = require("underscore.string"); + +function strToStr() { + sink(s.slugify(source("s1"))); // $ MISSING: hasTaintFlow=s1 + sink(s.capitalize(source("s2"))); // $ MISSING: hasTaintFlow=s2 + sink(s.decapitalize(source("s3"))); // $ MISSING: hasTaintFlow=s3 + sink(s.clean(source("s4"))); // $ MISSING: hasTaintFlow=s4 + sink(s.cleanDiacritics(source("s5"))); // $ MISSING: hasTaintFlow=s5 + sink(s.swapCase(source("s6"))); // $ MISSING: hasTaintFlow=s6 + sink(s.escapeHTML(source("s7"))); // $ MISSING: hasTaintFlow=s7 + sink(s.unescapeHTML(source("s8"))); // $ MISSING: hasTaintFlow=s8 + sink(s.wrap(source("s9"), {})); // $ MISSING: hasTaintFlow=s9 + sink(s.dedent(source("s10"), " ")); // $ MISSING: hasTaintFlow=s10 + sink(s.reverse(source("s11"))); // $ MISSING: hasTaintFlow=s11 + sink(s.pred(source("s12"))); // $ MISSING: hasTaintFlow=s12 + sink(s.succ(source("s13"))); // $ MISSING: hasTaintFlow=s13 + sink(s.titleize(source("s14"))); // $ MISSING: hasTaintFlow=s14 + sink(s.camelize(source("s15"))); // $ MISSING: hasTaintFlow=s15 + sink(s.classify(source("s16"))); // $ MISSING: hasTaintFlow=s16 + sink(s.underscored(source("s17"))); // $ MISSING: hasTaintFlow=s17 + sink(s.dasherize(source("s18"))); // $ MISSING: hasTaintFlow=s18 + sink(s.humanize(source("s19"))); // $ MISSING: hasTaintFlow=s19 + sink(s.trim(source("s20"),"charsToStrim")); // $ MISSING: hasTaintFlow=s20 + sink(s.ltrim(source("s21"),"charsToStrim")); // $ MISSING: hasTaintFlow=s21 + sink(s.rtrim(source("s22"),"charsToStrim")); // $ MISSING: hasTaintFlow=s22 + sink(s.truncate(source("s23"), 10)); // $ MISSING: hasTaintFlow=s23 + sink(s.sprintf(source("s24"), 1.17)); // $ MISSING: hasTaintFlow=s24 + sink(s.strRight(source("s25"), "pattern")); // $ MISSING: hasTaintFlow=s25 + sink(s.strRightBack(source("s26"), "pattern")); // $ MISSING: hasTaintFlow=s26 + sink(s.strLeft(source("s27"), "pattern")); // $ MISSING: hasTaintFlow=s27 + sink(s.strLeftBack(source("s28"), "pattern")); // $ MISSING: hasTaintFlow=s28 + sink(s.stripTags(source("s29"))); // $ MISSING: hasTaintFlow=s29 + sink(s.unquote(source("s30"), "quote")); // $ MISSING: hasTaintFlow=s30 + sink(s.map(source("s31"), (x) => {return x;})); // $ MISSING: hasTaintFlow=s31 + sink(s.strip(source("s32"),"charsToStrim")); // $ MISSING: hasTaintFlow=s32 + sink(s.lstrip(source("s33"),"charsToStrim")); // $ MISSING: hasTaintFlow=s33 + sink(s.rstrip(source("s34"),"charsToStrim")); // $ MISSING: hasTaintFlow=s34 + sink(s.camelcase(source("s35"))); // $ MISSING: hasTaintFlow=s35 +} + +function strToArray() { + sink(s.chop(source("s1"), 3)); // $ MISSING: hasTaintFlow=s1 + sink(s.chars(source("s2"))[0]); // $ MISSING: hasTaintFlow=s2 + sink(s.words(source("s3"))[0]); // $ MISSING: hasTaintFlow=s3 + sink(s.lines(source("s7"))[0]); // $ MISSING: hasTaintFlow=s7 + sink(s.chop(source("s1"), 3).length); +} + +function arrayToStr() { + sink(s.toSentence([source("s1")])); // $ MISSING: hasTaintFlow=s1 + sink(s.toSentenceSerial([source("s2")])); // $ MISSING: hasTaintFlow=s2 +} + +function multiSource() { + sink(s.insert("str", 4, source("s1"))); // $ MISSING: hasTaintFlow=s1 + sink(s.insert(source("s2"), 4, "")); // $ MISSING: hasTaintFlow=s2 + + sink(s.replaceAll("astr", "a", source("s3"))); // $ MISSING: hasTaintFlow=s3 + sink(s.replaceAll(source("s4"), "a", "")); // $ MISSING: hasTaintFlow=s4 + + sink(s.join(",", source("s5"), "str")); // $ MISSING: hasTaintFlow=s5 + sink(s.join(",", "str", source("s6"))); // $ MISSING: hasTaintFlow=s6 + + sink(s.splice(source("s7"), 1, 2, "str")); // $ MISSING: hasTaintFlow=s7 + sink(s.splice("str", 1, 2, source("s8"))); // $ MISSING: hasTaintFlow=s8 + + sink(s.prune(source("s9"), 1, "additional")); // $ MISSING: hasTaintFlow=s9 + sink(s.prune("base", 1, source("s10"))); // $ MISSING: hasTaintFlow=s10 + + sink(s.pad(source("s11"), 10, "charsToPad", "right")); // $ MISSING: hasTaintFlow=s11 + sink(s.pad("base", 10, source("s12"), "right")); // $ MISSING: hasTaintFlow=s12 + + sink(s.lpad(source("s13"), 10, "charsToPad")); // $ MISSING: hasTaintFlow=s13 + sink(s.lpad("base", 10, source("s14"))); // $ MISSING: hasTaintFlow=s14 + + sink(s.rpad(source("s15"), 10, "charsToPad")); // $ MISSING: hasTaintFlow=s15 + sink(s.rpad("base", 10, source("s16"))); // $ MISSING: hasTaintFlow=s16 + + sink(s.repeat(source("s17"), 3, "seperator")); // $ MISSING: hasTaintFlow=s17 + sink(s.repeat("base", 3, source("s18"))); // $ MISSING: hasTaintFlow=s18 + + sink(s.surround(source("s19"), "wrap")); // $ MISSING: hasTaintFlow=s19 + sink(s.surround("base", source("s20"))); // $ MISSING: hasTaintFlow=s20 + + sink(s.quote(source("s21"), "quote")); // $ MISSING: hasTaintFlow=s21 + sink(s.quote("base", source("s22"))); // $ MISSING: hasTaintFlow=s22 + + sink(s.q(source("s23"), "quote")); // $ MISSING: hasTaintFlow=s23 + sink(s.q("base", source("s24"))); // $ MISSING: hasTaintFlow=s24 + + sink(s.rjust(source("s25"), 10, "charsToPad")); // $ MISSING: hasTaintFlow=s25 + sink(s.rjust("base", 10, source("s26"))); // $ MISSING: hasTaintFlow=s26 + + sink(s.ljust(source("s27"), 10, "charsToPad")); // $ MISSING: hasTaintFlow=s27 + sink(s.ljust("base", 10, source("s28"))); // $ MISSING: hasTaintFlow=s28 +} + +function chaining() { + sink(s(source("s1")) + .slugify().capitalize().decapitalize().clean().cleanDiacritics() + .swapCase().escapeHTML().unescapeHTML().wrap().dedent() + .reverse().pred().succ().titleize().camelize().classify() + .underscored().dasherize().humanize().trim().ltrim().rtrim() + .truncate().sprintf().strRight().strRightBack() + .strLeft().strLeftBack().stripTags().unquote().value()); // $ MISSING: hasTaintFlow=s1 + + sink(s(source("s2")) + .insert(4, source("s3")).replaceAll("a", source("s4")) + .join(",", source("s5")).splice(1, 2, source("s6")) + .prune(1, source("s7")).pad(10, source("s8"), "right") + .lpad(10, source("s9")).rpad(10, source("s10")) + .repeat(3, source("s11")).surround(source("s12")) + .quote(source("s13")).value()); // $ MISSING: hasTaintFlow=s2 hasTaintFlow=s3 hasTaintFlow=s4 hasTaintFlow=s5 hasTaintFlow=s6 hasTaintFlow=s7 hasTaintFlow=s8 hasTaintFlow=s9 hasTaintFlow=s10 hasTaintFlow=s11 hasTaintFlow=s12 hasTaintFlow=s13 + + sink(s(source("s14")).toUpperCase().toLowerCase().replace().slice(1).substring(1).substr(1).concat(source("s15")).split()); // $ MISSING: hasTaintFlow=s14 hasTaintFlow=s15 + + sink(s(source("s16")) + .strip().lstrip().rstrip().camelcase() + .q(source("s17")).ljust(10, source("s18")) + .rjust(10, source("s19"))); // $ MISSING: hasTaintFlow=s16 hasTaintFlow=s17 hasTaintFlow=s18 hasTaintFlow=s19 + + sink(s(source("s20")).tap(function(value) { + return value + source("s21"); + }).value()); // $ MISSING: hasTaintFlow=s20 hasTaintFlow=s21 +} + +function mapTests(){ + sink(s.map(source("s1"), (x) => {return x + source("s2");})); // $ MISSING: hasTaintFlow=s1 hasTaintFlow=s2 + s.map(source("s1"), (x) => { sink(x); return x;}); // $ MISSING: hasTaintFlow=s1 +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/urlsearchparams.js b/javascript/ql/test/library-tests/UnifiedDataFlow/urlsearchparams.js new file mode 100644 index 000000000000..4660c2929e38 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/urlsearchparams.js @@ -0,0 +1,15 @@ +function u1() { + const searchParams = new URLSearchParams(source("u1.1")); + sink(searchParams.get("x")); // $ MISSING: hasTaintFlow=u1.1 + sink(searchParams.get(unknown())); // $ MISSING: hasTaintFlow=u1.1 + sink(searchParams.getAll("x")); // $ MISSING: hasTaintFlow=u1.1 + sink(searchParams.getAll(unknown())); // $ MISSING: hasTaintFlow=u1.1 +} + +function u2() { + const url = new URL(source("u2.1")); + sink(url.searchParams.get("x")); // $ MISSING: hasTaintFlow=u2.1 + sink(url.searchParams.get(unknown())); // $ MISSING: hasTaintFlow=u2.1 + sink(url.searchParams.getAll("x")); // $ MISSING: hasTaintFlow=u2.1 + sink(url.searchParams.getAll(unknown())); // $ MISSING: hasTaintFlow=u2.1 +} diff --git a/javascript/ql/test/library-tests/UnifiedDataFlow/useuse.js b/javascript/ql/test/library-tests/UnifiedDataFlow/useuse.js new file mode 100644 index 000000000000..76039af81ad9 --- /dev/null +++ b/javascript/ql/test/library-tests/UnifiedDataFlow/useuse.js @@ -0,0 +1,175 @@ +import 'dummy'; + +function t1() { + const obj = {}; + + sink(obj.field); + + obj.field = source('t1.1'); + sink(obj.field); // $ hasValueFlow=t1.1 + + obj.field = "safe"; + sink(obj.field); + + obj.field = source('t1.2'); + sink(obj.field); // $ hasValueFlow=t1.2 +} + +function t2() { + let obj; + + if (Math.random()) { + obj = {}; + sink(obj.field); + } else { + obj = {}; + obj.field = source('t2.1'); + sink(obj.field); // $ hasValueFlow=t2.1 + } + sink(obj.field); // $ hasValueFlow=t2.1 +} + +function t3() { + function inner(obj) { + sink(obj.foo); // $ hasValueFlow=t3.2 hasValueFlow=t3.1 + } + + inner({foo: source('t3.1')}); + + let obj = {}; + obj.foo = source('t3.2'); + inner(obj); +} + +function t4() { + class C { + constructor(x) { + this.foo = x; + sink(this.foo); // $ hasValueFlow=t4.1 + } + } + const c = new C(source('t4.1')); + sink(c.foo); // $ hasValueFlow=t4.1 +} + +function t5() { + class C { + field = source('t5.1') + constructor() { + sink(this.field); // $ hasValueFlow=t5.1 + } + } + const c = new C(); + sink(c.field); // $ hasValueFlow=t5.1 +} + + +function t6() { + function invoke(fn) { + fn(); + } + class C { + constructor(x, y) { + this.x = x; + invoke(() => { + this.y = y; + }); + + sink(this.x); // $ hasValueFlow=t6.1 + sink(this.y); // $ MISSING: hasValueFlow=t6.2 + + invoke(() => { + sink(this.x); // $ hasValueFlow=t6.1 + sink(this.y); // $ MISSING: hasValueFlow=t6.2 + }); + + this.methodLike = function() { + sink(this.x); // $ hasValueFlow=t6.1 + sink(this.y); // $ MISSING: hasValueFlow=t6.2 + } + } + } + const c = new C(source('t6.1'), source('t6.2')); + sink(c.x); // $ hasValueFlow=t6.1 + sink(c.y); // $ MISSING: hasValueFlow=t6.2 + c.methodLike(); +} + +function t7() { + class Base { + constructor(x) { + this.field = x; + sink(this.field); // $ hasTaintFlow=t7.1 + } + } + class Sub extends Base { + constructor(x) { + super(x + '!'); + sink(this.field); // $ hasTaintFlow=t7.1 + } + } + const c = new Sub(source('t7.1')); + sink(c.field); // $ hasTaintFlow=t7.1 +} + +function t8() { + function foo(x) { + const obj = {}; + obj.field = x; + + sink(obj.field); // $ hasTaintFlow=t8.1 + + if (obj) { + sink(obj.field); // $ hasTaintFlow=t8.1 + } else { + sink(obj.field); // $ SPURIOUS: hasTaintFlow=t8.1 + } + + if (!obj) { + sink(obj.field); // $ SPURIOUS: hasTaintFlow=t8.1 + } else { + sink(obj.field); // $ hasTaintFlow=t8.1 + } + + if (!obj || !obj) { + sink(obj.field); // $ SPURIOUS: hasTaintFlow=t8.1 + } else { + sink(obj.field); // $ hasTaintFlow=t8.1 + } + } + + // The guards used above are specific to taint-tracking, to ensure only taint flows in + const taint = source('t8.1') + ' taint'; + foo(taint); +} + +function t9() { // same as t8 but with a SanitizerGuard that isn't just a variable access + function foo(x) { + const obj = {}; + obj.field = x; + + sink(obj.field); // $ hasTaintFlow=t9.1 + + if (typeof obj !== "undefined") { + sink(obj.field); // $ hasTaintFlow=t9.1 + } else { + sink(obj.field); // $ SPURIOUS: hasTaintFlow=t9.1 + } + + if (typeof obj === "undefined") { + sink(obj.field); // $ SPURIOUS: hasTaintFlow=t9.1 + } else { + sink(obj.field); // $ hasTaintFlow=t9.1 + } + + if (typeof obj === "undefined" || typeof obj === "undefined") { + sink(obj.field); // $ SPURIOUS: hasTaintFlow=t9.1 + } else { + sink(obj.field); // $ hasTaintFlow=t9.1 + } + } + + // The guards used above are specific to taint-tracking, to ensure only taint flows in + const taint = source('t9.1') + ' taint'; + foo(taint); +} diff --git a/shared/unified/codeql/unified/UnifiedDataFlow.qll b/shared/unified/codeql/unified/UnifiedDataFlow.qll new file mode 100644 index 000000000000..eb4fe6fff408 --- /dev/null +++ b/shared/unified/codeql/unified/UnifiedDataFlow.qll @@ -0,0 +1,3950 @@ +overlay[local?] +module; + +private import codeql.util.Location +private import codeql.util.DualGraph +private import codeql.util.FileSystem +private import codeql.util.Boolean +private import codeql.util.Unit +private import codeql.util.Void +private import codeql.dataflow.internal.AccessPathSyntax as AccessPathSyntax +private import codeql.controlflow.BasicBlock as BB +private import codeql.ssa.Ssa as Ssa +private import codeql.dataflow.VariableCapture as VariableCapture +private import codeql.dataflow.DataFlow as DataFlow +private import codeql.dataflow.TaintTracking as TaintTracking + +module MakeUnifiedDataFlow0 Cfg> { + private class BasicBlock = Cfg::BasicBlock; + + private class ControlFlowNode = Cfg::ControlFlowNode; + + signature module UnifiedDataFlowSig1 { + class AstNode { + string toString(); + + Location getLocation(); + + AstNode getParent(); + } + + class Call { + string toString(); + + Location getLocation(); + } + + class NamespaceObject { + string toString(); + + Location getLocation(); + + /** + * Gets the enclosing callable, if this namespace object represents an allocation site with a concrete + * value at runtime. + */ + Callable getEnclosingCallable(); // TODO: maybe we should not require namespace to exist in a callable? + } + + class ClassLikeObject extends NamespaceObject { + NamespaceObject getInstancePrototype(); + } + + /** + * Holds if `callable` should be treated as a method during call graph construction. + * + * For such callables, the receiver is assumed to refer to or inherit from the object on which the `callable` is stored. + */ + predicate isMethod(Callable callable); + + /** + * Holds if `callable` is called to initialize instances of a class. + */ + predicate isInstanceInitializer(Callable callable, ClassLikeObject cls); + + class Callable extends AstNode; + + Callable getCallableFromBasicBlock(BasicBlock bb); + + Callable getCallableFromCfgNode(ControlFlowNode node); + + class LocalVariable { + string toString(); + + Location getLocation(); + + Callable getDeclaringCallable(); + + predicate isCaptured(); + } + + /** An abstract value that a `Guard` may evaluate to. */ + class GuardValue { + /** Gets a textual representation of this value. */ + string toString(); + } + + /** A (potential) guard. */ + class Guard { + /** Gets a textual representation of this guard. */ + string toString(); + + /** + * Holds if the evaluation of this guard to `val` corresponds to the edge + * from `bb1` to `bb2`. + */ + predicate hasValueBranchEdge(BasicBlock bb1, BasicBlock bb2, GuardValue val); + + /** + * Holds if this guard evaluating to `val` controls the control-flow + * branch edge from `bb1` to `bb2`. That is, following the edge from + * `bb1` to `bb2` implies that this guard evaluated to `val`. + * + * This predicate differs from `hasValueBranchEdge` in that it also covers + * indirect guards, such as: + * ``` + * b = guard; + * ... + * if (b) { ... } + * ``` + */ + predicate valueControlsBranchEdge(BasicBlock bb1, BasicBlock bb2, GuardValue val); + } + + /** Holds if `guard` directly controls block `bb` upon evaluating to `val`. */ + predicate guardDirectlyControlsBlock(Guard guard, BasicBlock bb, GuardValue val); + + /** + * Holds if `node` appears in a context where it receives an incoming value, such as + * the target of an assignment. + * + * Typically this holds for: + * - Parameters + * - Variable names and binding patterns in a variable declaration or pattern-matching context + * - The left-hand side of assignments + * - The left-hand side of iterator for-loops (The `x` in `for (x in iterator) {...}`) + * + * This is not mutually exclusive with `hasCompletionValue`. For example, the target of a compound assignment (`x += y`) + * will typically have both an incoming value and a completion value, with different associated CFG nodes. + */ + predicate hasIncomingValue(AstNode node, ControlFlowNode when); + + /** + * Holds if `node` completes with a value at the given CFG node. + * + * For a typical CFG construction where expressions are post-order, this is simply the CFG wrapper around `node`. + * + * If `node` has multiple exits with a value, this predicate should hold for each of the exits. Exceptional exits should not be included. + */ + predicate hasCompletionValue(AstNode node, ControlFlowNode when); + + /** + * Holds if `node` needs a post-update node that can be referenced with `node.isPostUpdatedValue()` in the step relation. + * + * This should generally hold when `node` appears in a context where its result is mutated by a store after `node` has completed. + * + * Typically this will hold for `obj` in `obj.f = E`, and `when` should refer to the point in time where the assignment + * happens. + */ + predicate hasPostUpdatedValue(AstNode node, ControlFlowNode when); + + /** Gets the control-flow node where the given call happens. */ + ControlFlowNode getCfgNodeFromCall(Call call); + + /** + * Holds if exceptions thrown inside `tryBlock` should not automatically propagate out of that node. + * + * For a classic `try` statement, `try {S} catch ...`, this should hold for the `{S}` block. + * + * A data flow node will be generated for this case, to be accessed with `node.isInterceptedException(tryBlock)`. + * Corresponding data flow steps should be added to propagate the caught value into the catch block(s) as appropriate. + */ + predicate isInterceptingExceptions(AstNode tryBlock); + + default predicate definitelyInitialized(LocalVariable v) { none() } + + class Constant { + int asArrayIndex(); + + /** Gets the operand used when referencing this constant from a MaD token. */ + string getAsOperand(); + + /** Gets a string-representation of the constant. Should generally equal `getAsOperand()` when that exists. */ + string toString(); + } + + /** + * A type of container that associates values with a key or index, such as arrays, lists, tuples, maps, dictionaries, etc. + * + * In some languages, lists and maps may be difficult to distinguish at their use sites as they are interacted with using the same syntax or method names. + * In such cases it can make sense to unify the relevant container kinds. + * + * Non-indexable containers such as sets, iterators, and streams should either be represented by a `LanguageContent`, or be treated interchangeably + * with the contents of another container kind. For example: iterator contents could be represented by array contents, so the conversion between + * arrays and iterators becomes a no-op. In other languages it may be better for it to have its own `LanguageContent`. The choice depends on how + * difficult it is to recognise conversions between the container kinds, versus the value of pruning false flow based on more fine-grained contents. + */ + class IndexedContainerKind { + /** Holds if data flowing into the keys themselves should be tracked. For array-like containers this should be `none()`. */ + predicate trackFlowIntoKeys(); + + /** Gets the MaD token to associate with keys in this map-like container. Should have no result for array-like containers. */ + string getKeyToken(); + + /** Gets the MaD token to associate with values in this container (i.e. map values or array elements). */ + string getValueToken(); + + /** + * Holds if values that are associated with `key` should be tracked precisely. + * + * For array-like containers, this should hold for non-negative integers up to a certain size. + * + * For map-like containers, this should hold for all keys that are likely worth tracking. + */ + predicate trackValuesAssociatedWithKey(Constant key); + } + + /** + * A language-specific content that is not handled by `IndexedContainerKind`. + */ + class LanguageContent { + /** Holds if models-as-data can refer to this content (as a singleton content set) as `head[operand]`. */ + predicate hasModelToken(string head, string operand); + + string toString(); + + Location getLocation(); + + /** + * Holds if a store step using the this content (wrapped in a singleton content set) + * should clear the previous value stored in that content. + * + * Specifically, when a store step targets a post-update node, and `supportsStrongUpdate()` holds for the content set, + * all values from `getAStoreContent()` coming from the pre-update node are blocked. + * + * Should generally hold for contents representing a single storage location, + * such as a field, and should not hold for abstract storage locations, + * such as an "unknown field" content. + * + * For contents that aren't used for storing into post-update nodes, it does not matter whether + * this predicate holds or not. + */ + predicate supportsStrongUpdate(); + } + } + + module MakeUnifiedDataFlow1 { + private import D1 + + pragma[nomagic] + predicate hasIncomingValue(AstNode node) { hasIncomingValue(node, _) } + + pragma[nomagic] + predicate hasCompletionValue(AstNode node) { hasCompletionValue(node, _) } + + private newtype TBuiltinReturnPosition = + TReturnValue() or + TReturnException() + + class BuiltinReturnPosition extends TBuiltinReturnPosition { + string toString() { + this = TReturnValue() and result = "value" + or + this = TReturnException() and result = "exception" + } + } + + private newtype TContent = + TCaptureContent(LocalVariable v) { v.isCaptured() } or + TContainerSlot(IndexedContainerKind kind, Constant key) { + kind.trackValuesAssociatedWithKey(key) + } or + TContainerUnknownSlot(IndexedContainerKind kind) or + TContainerKey(IndexedContainerKind kind) { kind.trackFlowIntoKeys() } or + TLanguageContent(LanguageContent kind) + + class Content extends TContent { + predicate hasModelToken(string head, string operand) { + exists(IndexedContainerKind kind, Constant key | + key = this.asContainerSlot(kind) and + head = kind.getValueToken() and + operand = key.getAsOperand() + "!" // use "!" when referring to this without the unknown element + ) + or + exists(IndexedContainerKind kind | + this.isUnknownContainerSlot(kind) and + head = kind.getValueToken() and + operand = "?" + or + this.isContainerKey(kind) and + head = kind.getKeyToken() and + operand = "" + ) + or + this.asLanguageContent().hasModelToken(head, operand) + } + + LocalVariable asCapturedVariable() { this = TCaptureContent(result) } + + Constant asContainerSlot(IndexedContainerKind kind) { this = TContainerSlot(kind, result) } + + int asArrayIndex(IndexedContainerKind kind) { + result = this.asContainerSlot(kind).asArrayIndex() + } + + predicate isUnknownContainerSlot(IndexedContainerKind kind) { + this = TContainerUnknownSlot(kind) + } + + predicate isContainerKey(IndexedContainerKind kind) { this = TContainerKey(kind) } + + LanguageContent asLanguageContent() { this = TLanguageContent(result) } + + string toString() { + // Note: these strings are visible to end-users in the generated data flow paths. + result = this.asCapturedVariable().toString() + or + exists(IndexedContainerKind kind | + result = kind.getValueToken() + "[" + this.asContainerSlot(kind).toString() + "]" + or + this.isUnknownContainerSlot(kind) and + result = kind.getValueToken() + or + this.isContainerKey(kind) and + result = kind.getKeyToken() + ) + or + result = this.asLanguageContent().toString() + } + + Location getLocation() { + result = this.asCapturedVariable().getLocation() + or + result = this.asLanguageContent().getLocation() + } + } + + signature module UnifiedDataFlowSig2 { + predicate synthesizeNode(AstNode node, string tag, ControlFlowNode cfgNode); + + class LanguageContentSet { + Content getAReadContent(); + + Content getAStoreContent(); + + /** Holds if models-as-data can refer to this content set as `head[operand]`. */ + predicate hasModelToken(string head, string operand); + + string toString(); + + Location getLocation(); + + /** + * Holds if store steps using this content set should clear the previous value. + * + * Specifically, when a store step targets a post-update node, and `supportsStrongUpdate()` holds for the content set, + * all values from `getAStoreContent()` coming from the pre-update node are blocked. + * + * Should generally hold for content sets representing a single storage location, + * such as a field, and should not hold for abstract storage locations, + * such as an "unknown field" content. + */ + predicate supportsStrongUpdate(); + } + } + + module MakeUnifiedDataFlow2 { + private import D2 + + private newtype TContentSet = + TSingleton(TContent content) or + TContainerKnownSlot(IndexedContainerKind kind, Constant key) { + kind.trackValuesAssociatedWithKey(key) + } or + TArrayElementLowerBound(IndexedContainerKind kind, int bound) { + exists(Constant key | + kind.trackValuesAssociatedWithKey(key) and + bound = key.asArrayIndex() + ) + } or + TContainerAnySlot(IndexedContainerKind kind) or + TLanguageContentSet(LanguageContentSet contents) + + class ContentSet extends TContentSet { + predicate hasModelToken(string head, string operand) { + this.asSingleton().hasModelToken(head, operand) + or + this.asLanguageContentSet().hasModelToken(head, operand) + or + exists(IndexedContainerKind kind, Constant key | + key = this.asContainerSlot(kind) and + head = kind.getValueToken() and + operand = key.getAsOperand() + ) + or + exists(IndexedContainerKind kind, int n | + n = this.asArrayElementLowerBound(kind) and + head = kind.getValueToken() and + operand = n + ".." + ) + or + exists(IndexedContainerKind kind | + this.isAnyContainerSlot(kind) and + head = kind.getValueToken() and + operand = "" + ) + } + + Content asSingleton() { this = TSingleton(result) } + + Constant asContainerSlot(IndexedContainerKind kind) { + this = TContainerKnownSlot(kind, result) + } + + predicate isAnyContainerSlot(IndexedContainerKind kind) { this = TContainerAnySlot(kind) } + + int asArrayElementLowerBound(IndexedContainerKind kind) { + this = TArrayElementLowerBound(kind, result) + } + + LanguageContentSet asLanguageContentSet() { this = TLanguageContentSet(result) } + + string toString() { + result = this.asSingleton().toString() + or + exists(IndexedContainerKind kind | + result = kind.getValueToken() + "[" + this.asArrayElementLowerBound(kind) + "..]" + or + result = kind.getValueToken() + "[" + this.asContainerSlot(kind) + "]" + or + this.isAnyContainerSlot(kind) and + result = kind.getValueToken() + ) + or + result = this.asLanguageContentSet().toString() + } + + Location getLocation() { result = this.asLanguageContentSet().getLocation() } + + cached + Content getAReadContent() { + result = this.asSingleton() + or + exists(IndexedContainerKind kind | + this.asArrayElementLowerBound(kind) <= result.asArrayIndex(kind) + or + this.asContainerSlot(kind) = result.asContainerSlot(kind) + or + this.isAnyContainerSlot(kind) and + exists(result.asContainerSlot(kind)) + or + ( + exists(this.asArrayElementLowerBound(kind)) or + exists(this.asContainerSlot(kind)) or + this.isAnyContainerSlot(kind) + ) and + result.isUnknownContainerSlot(kind) + ) + or + result = this.asLanguageContentSet().getAReadContent() + } + + cached + Content getAStoreContent() { + result = this.asSingleton() + or + exists(IndexedContainerKind kind | + result.asContainerSlot(kind) = this.asContainerSlot(kind) + or + exists(this.asArrayElementLowerBound(kind)) and + result.isUnknownContainerSlot(kind) // nothing better can be done at the moment, but this is usually not used for stores anyway + or + this.isAnyContainerSlot(kind) and + result.isUnknownContainerSlot(kind) + ) + or + result = this.asLanguageContentSet().getAStoreContent() + } + } + + private predicate contentSetSupportsStrongUpdate(ContentSet contents) { + contents.asSingleton().asLanguageContent().supportsStrongUpdate() + or + contents.asLanguageContentSet().supportsStrongUpdate() + or + contents instanceof TContainerKnownSlot + } + + signature class IndexedContainerKindSig extends IndexedContainerKind; + + /** Generates a module with accessors for content sets related to the given array-like container kind. */ + module ArrayContentAccessor { + class Kind = KindArg; + + Kind kind() { any() } + + private Constant preciseKey() { kind().trackValuesAssociatedWithKey(result) } + + private int preciseIndex() { result = preciseKey().asArrayIndex() } + + pragma[nomagic] + private int maxPreciseIndex() { result = max(preciseIndex()) } + + /** Read from a index or higher. Using this in a store will result in an unknown index. */ + pragma[nomagic] + ContentSet lowerBound(int index) { result.asArrayElementLowerBound(kind()) = index } + + /** Any element of the array. */ + pragma[nomagic] + ContentSet anyElement() { result = lowerBound(0) } + + pragma[nomagic] + private ContentSet maxLowerBound() { result = lowerBound(maxPreciseIndex()) } + + pragma[nomagic] + private ContentSet knownIndex(int index) { + result.asContainerSlot(kind()).asArrayIndex() = index + } + + /** + * Read or store to a specific index. + * + * Reading from this content set will also observe values that were originally stored at an unknown index. + * + * Has no result for negative indices. Always has a result for non-negative indices, + * but indices above a certain threshold will be associated with a less precise content set. + */ + bindingset[index] + ContentSet elementAt(int index) { + result = knownIndex(index) + or + // If the index is larger than we can track, use the greatest lower bound instead. + index > maxPreciseIndex() and + result = maxLowerBound() + } + + final private class FinalContentSet = ContentSet; + + /** + * A singleton content for array elements at a known index, or unknown index. + * + * This can be used to generate a set of read and store edges that copy parts + * of an array to another value. For such purposes, it is best to only rely on + * singleton (exact) content sets to avoid precision loss. + * + * ```codeql + * exists(Array::ExactContent content | + * node1 = ... and + * step.read() = content and + * node2 = ... + * or + * node1 = ... and + * step.store() = content.shiftUpBy(1) and + * node2 = ... + * ) + * ``` + */ + class ExactContent extends FinalContentSet { + ExactContent() { + exists(this.asSingleton().asArrayIndex(kind())) or + this.asSingleton().isUnknownContainerSlot(kind()) + } + + /** Increase the index by the given value, if it is a known index. */ + bindingset[index] + ContentSet shiftUpBy(int index) { + result = elementAt(this.asSingleton().asArrayIndex(kind()) + index) + or + this.asSingleton().isUnknownContainerSlot(kind()) and result = this + } + } + } + + /** Generates a module with accessors for the content sets related to the given map-like kind. */ + module MapContentAccessor { + private Kind kind() { any() } + + /** One of the keys in a key-value pair stored in a map. */ + pragma[nomagic] + ContentSet key() { result.asSingleton().isContainerKey(kind()) } + + /** One of the values from a key-value pair stored in a map. */ + pragma[nomagic] + ContentSet value() { result.isAnyContainerSlot(kind()) } + + pragma[nomagic] + private ContentSet valueAtExact(Constant key) { + result.asSingleton().asContainerSlot(kind()) = key + } + + /** + * The value associated with `key` in map. + * + * If `key` is not one of the keys that are tracked precisely, this will return + * the same as `value()`. + */ + bindingset[key] + ContentSet valueAt(Constant key) { + result = valueAtExact(key) + or + not exists(valueAtExact(key)) and + result = value() + } + } + + signature module UnifiedDataFlowSig3 { + class TransformBase; + } + + module MakeUnifiedDataFlow3 { + final private class FinalTransformBase = TransformBase; + + class Transform extends FinalTransformBase { + abstract string toString(); + + abstract predicate step(string node1, DataFlowBuilder::Step step, string node2); + } + + module StandardTransforms { + module ShiftArrayContent { + predicate step(int amount, string node1, DataFlowBuilder::Step step, string node2) { + amount = 0 and + node1 = "input" and + (step.withContent(ArrayContentAccessor::anyElement()) or step.taint()) and + node2 = "output" + or + amount = [-10 .. 10] and + amount != 0 and + exists(ArrayContentAccessor::ExactContent c | + node1 = "input" and + ( + step.read(c) + or + c.asSingleton().isUnknownContainerSlot(any(Kind k)) and + step.taint() + ) and + node2 = "element " + c + or + node1 = "element " + c and + step.store(c.shiftUpBy(amount)) and + node2 = "output" + ) + } + } + } + + private import D3 + + private newtype TStep = + TValueStep() or + TCopyStep() or + TTaintStep() or + TReadStep(ContentSet contents) or + TStoreStep(ContentSet contents) or + TWithContentStep(ContentSet contents) or + TWithoutContentStep(ContentSet contents) or + TTransformStep(Transform transform) or + TStoreBaseObject() or + TInstantiateStep() or + TStoreAsGetterStep(ContentSet contents) or + TStoreAsSetterStep(ContentSet contents) + + cached + private newtype TDataFlowNode = + TUnknown() or + TValueNode(AstNode node) { hasCompletionValue(node) } or + TIncomingValueNode(AstNode node) { hasIncomingValue(node) } or + TInterceptedExceptionNode(AstNode tryBlock) { isInterceptingExceptions(tryBlock) } or + TCallTargetNode(Call call) or + TOutNode(Call call) or + TExceptionalOutNode(Call call) or + TArgumentObjectNode(Call call) or + TReceiverArgumentNode(Call call) or + TPostUpdatedReceiverArgumentNode(Call call) or + TPostUpdatedArgumentObjectNode(Call call) or + TDynamicArgumentObjectNode(Call call) or + TParameterObjectNode(Callable callable) or + TReceiverParameterNode(Callable callable) or + TDynamicParameterObjectNode(Callable callable) or + TCallableNode(Callable callable) or + TCallableSelfReferenceNode(Callable callable) or + TReturnNode(Callable callable) or + TInnerReturnNode(Callable callable) or + TExceptionalReturnNode(Callable callable) or + TInnerExceptionalReturnNode(Callable callable) or + TSyntheticNode(AstNode node, string tag) { synthesizeNode(node, tag, _) } or + TPostUpdatedValueNode(AstNode node) { hasPostUpdatedValue(node, _) } or + TNamespaceNode(NamespaceObject ns) or + TVariableNode(LocalVariable v, Boolean isRead) or // used in builder stage, replaced with more precise variants below + TVariableReadNode(LocalVariable v, BasicBlock bb, int i) { + hasVariableReadAt(v, bb, i, _) + } or + TVariablePostUpdateNode(LocalVariable v, BasicBlock bb, int i) { + hasVariableReadAt(v, bb, i, true) + } or + TVariableWriteNode(LocalVariable v, BasicBlock bb, int i) { hasVariableWriteAt(v, bb, i) } or + TDerivedPostUpdateNode(DataFlowNode1 pre) { needsDerivedPostUpdateNode(pre) } or + TClosureExprNode(Callable callable, BasicBlock bb, int i) { + dataflowNodeHasSuccessorCfgNode(TCallableNode(callable), bb, i) + } or + TLocalSsaNode(LocalSsaDataFlow::SsaNode node) or + TTransformStepNode(TBuilderNode node, string nodeName) { + exists(Transform t | + dataflowStep1(_, TTransformStep(t), node) and + t.step(_, _, nodeName) and + not nodeName = ["input", "output"] + ) + } or + TWithContentHelper(TDataFlowNode3 node, ContentSet contents) { + dataflowStep3(_, TWithContentStep(contents), node) + } or + TWithoutContentHelper(TDataFlowNode3 node, ContentSet contents) { + dataflowStep3(_, TWithoutContentStep(contents), node) + } or + TCaptureSsaNode(CaptureSsa::SynthesizedCaptureNode node) + + predicate hasUniquePredecessor(Transform t, string nodeName) { + nodeName != "output" and // the output node is merged with an existing node, which might have other predecessors + strictcount(string n, TStep step | t.step(n, step, nodeName)) = 1 + } + + cached + private predicate dataflowStep1( + DataFlowBuilder::Node node1, DataFlowBuilder::Step step, DataFlowBuilder::Node node2 + ) { + any(DataFlowBuilder::DataFlowSteps s).step(node1, step, node2) + } + + cached // TODO: only cache while we rely on the expensive toString/getLocation predicates + private class TBuilderNode = + TUnknown or TValueNode or TIncomingValueNode or TInterceptedExceptionNode or + TCallTargetNode or TOutNode or TExceptionalOutNode or TArgumentObjectNode or + TReceiverArgumentNode or TPostUpdatedReceiverArgumentNode or TReceiverParameterNode or + TPostUpdatedArgumentObjectNode or TParameterObjectNode or TCallableNode or + TCallableSelfReferenceNode or TReturnNode or TInnerReturnNode or + TExceptionalReturnNode or TInnerExceptionalReturnNode or TSyntheticNode or + TPostUpdatedValueNode or TVariableNode or TNamespaceNode; + + private class BuilderNode extends TBuilderNode { + pragma[nomagic] + predicate isValueOf(AstNode node) { this = TValueNode(node) } + + pragma[nomagic] + predicate isIncomingValue(AstNode node) { this = TIncomingValueNode(node) } + + /** + * Holds if this is the post-update node for `node`, representing the new state of its returned value + * after a mutation. + * + * This should generally be used when `node` appears in a context where its result is mutated by a store after `node` has completed. + * + * For example, for an assignment `obj.f = E`, there should be a store step from `E` to the post-update node of `obj`. + */ + pragma[nomagic] + predicate isPostUpdatedValue(AstNode node) { this = TPostUpdatedValueNode(node) } + + pragma[nomagic] + predicate isNamespaceObject(NamespaceObject object) { this = TNamespaceNode(object) } + + /** + * Holds if this represents the exception thrown inside `tryBlock`. + * + * Only has a result for AST nodes where `isInterceptingExceptions` holds. + */ + pragma[nomagic] + predicate isInterceptedException(AstNode tryBlock) { + this = TInterceptedExceptionNode(tryBlock) + } + + pragma[nomagic] + predicate isArgumentObject(Call call) { this = TArgumentObjectNode(call) } + + pragma[nomagic] + predicate isReceiverArgument(Call call) { this = TReceiverArgumentNode(call) } + + pragma[nomagic] + predicate isPostUpdatedReceiverArgument(Call call) { + this = TPostUpdatedReceiverArgumentNode(call) + } + + pragma[nomagic] + predicate isPostUpdatedArgumentObject(Call call) { + this = TPostUpdatedArgumentObjectNode(call) + } + + pragma[nomagic] + predicate isParameterObject(Callable callable) { this = TParameterObjectNode(callable) } + + pragma[nomagic] + predicate isReceiverParameter(Callable callable) { + this = TReceiverParameterNode(callable) + } + + pragma[nomagic] + predicate isCallTarget(Call call) { this = TCallTargetNode(call) } + + pragma[nomagic] + predicate isReturnValueFromCall(Call call) { this = TOutNode(call) } + + pragma[nomagic] + predicate isExceptionThrownFromCall(Call call) { this = TExceptionalOutNode(call) } + + /** + * Holds for the node representing values that callers of this callable will see being returned from the call. + * + * This is also known as the "outer" return node. + * + * The "inner" return node captures values that are explicitly returned within the body, whereas the "outer" return value captures + * the value that callers will actually see. Usually they are the same, but not always. + * + * For callables that implicitly transform their return values, such as JS `async` functions, the transformation + * should happen as a step from the inner to the outer return value. If no explicit step is added from the + * inner return value, a default value step is added from the inner to the outer return node. + */ + pragma[nomagic] + predicate isReturnValue(Callable callable) { this = TReturnNode(callable) } + + /** + * Holds for the node representing values that are explicitly returned from the body of the callable, + * e.g via a return statement. + * + * The "inner" return node captures values that are explicitly returned within the body, whereas the "outer" return value captures + * the value that callers will actually see. Usually they are the same, but not always. + * + * For callables that implicitly transform their return values, such as JS `async` functions, the transformation + * should happen as a step from the inner to the outer return value. If no explicit step is added from the + * inner return value, a default value step is added from the inner to the outer return node. + */ + pragma[nomagic] + predicate isInnerReturnValue(Callable callable) { this = TInnerReturnNode(callable) } + + /** + * Holds for the node representing exceptions that callers of this callable will see being thrown from the call. + * + * This is also known as the "outer" exceptional return node. + * + * The "inner" exceptional return node captures values that are explicitly thrown or propagated by code within the body, + * whereas the "outer" node captures the value that callers will actually see being thrown. Usually they are the same, but not always. + * + * For callables that implicitly catches and transforms thrown values, such as JS `async` functions, the transformation + * should happen as a step from the inner exceptional return value to one of the other return nodes. If no explicit step is added from the + * inner exceptional return value, a default value step is added from the inner to the outer exceptional return node. + */ + pragma[nomagic] + predicate isExceptionalReturnValue(Callable callable) { + this = TExceptionalReturnNode(callable) + } + + /** + * Holds for the node representing exceptions that are thrown or propagated from the body of the callable, + * for example, via a throw statement, or via a call not wrapped in a try/catch. + * + * The "inner" exceptional return node captures values that are explicitly thrown or propagated by calls within the body, + * whereas the "outer" node captures the value that callers will actually see being thrown. Usually they are the same, but not always. + * + * For callables that implicitly catches and transforms thrown values, such as JS `async` functions, the transformation + * should happen as a step from the inner exceptional return value to one of the other return nodes. If no explicit step is added from the + * inner exceptional return value, a default value step is added from the inner to the outer exceptional return node. + */ + pragma[nomagic] + predicate isInnerExceptionalReturnValue(Callable callable) { + this = TInnerExceptionalReturnNode(callable) + } + + /** Holds if this is the node representing the given callable. */ + pragma[nomagic] + predicate isCallable(Callable callable) { this = TCallableNode(callable) } + + /** Holds if this is the special parameter by which a callable referes to itself. */ + pragma[nomagic] + predicate isCallableSelfReferenceParameter(Callable callable) { + this = TCallableSelfReferenceNode(callable) + } + + /** + * Holds if this node represents read of the variable `v`. + * + * If this node is used as the target of a store step, it means the current value of `v` is read and mutated by + * the store. It should not be used as the target of other kinds of steps. + * + * To ensure the read can be placed properly in the CFG, this should be connected to nodes that have a known associated CFG node. + * These are: + * - Value nodes (`.isValueOf`) + * - Incoming-value nodes (`.isIncomingValue`) + * - Parameter objects (`.isParameterObject`) + * - Return nodes (`.isReturNode` or `.isInnerReturnNode`) + * - Exceptional return nodes (`.isExceptionalReturNode` or `.isInnerExceptionalReturnNode`) + */ + pragma[nomagic] + predicate isVariableRead(LocalVariable v) { this = TVariableNode(v, true) } + + /** + * Holds if this node represents a value being assigned to the variable `v`. + * + * This should only be used as the target of a flow step, never as the source node. + */ + pragma[nomagic] + predicate isVariableWrite(LocalVariable v) { this = TVariableNode(v, false) } + + /** Holds if the incoming value is unknown or, if used as a destination node, indicate that the value is used somehow but we don't model precisely how. */ + pragma[nomagic] + predicate isUnknown() { this = TUnknown() } + + predicate isSyntheticNode(AstNode node, string tag) { this = TSyntheticNode(node, tag) } + + string toString() { + exists(AstNode node | this.isValueOf(node) | result = "[value] " + node.toString()) + or + exists(AstNode node | this.isIncomingValue(node) | result = "[incoming] " + node) + or + exists(AstNode node | this.isPostUpdatedValue(node) | result = "[post-updated] " + node) + or + exists(NamespaceObject object | this.isNamespaceObject(object) | + result = "[namespace] " + object.toString() + ) + or + exists(AstNode tryBlock | this.isInterceptedException(tryBlock) | + result = "[intercepted exception] " + tryBlock + ) + or + exists(Call call | this.isArgumentObject(call) | result = "[argument object] " + call) + or + exists(Call call | this.isReceiverArgument(call) | + result = "[receiver argument] " + call + ) + or + exists(Call call | this.isPostUpdatedReceiverArgument(call) | + result = "[post receiver argument] " + call + ) + or + exists(Call call | this.isPostUpdatedArgumentObject(call) | + result = "[post argument object] " + call + ) + or + exists(Callable callable | this.isParameterObject(callable) | + result = "[parameter object] " + callable + ) + or + exists(Callable callable | this.isReceiverParameter(callable) | + result = "[receiver parameter] " + callable + ) + or + exists(Call call | this.isCallTarget(call) | result = "[call target] " + call) + or + exists(Call call | this.isReturnValueFromCall(call) | result = "[out] " + call) + or + exists(Call call | this.isExceptionThrownFromCall(call) | + result = "[exceptional out] " + call + ) + or + exists(Callable callable | this.isCallable(callable) | + result = "[callable] " + callable + ) + or + exists(Callable callable | this.isCallableSelfReferenceParameter(callable) | + result = "[self-reference] " + callable + ) + or + exists(Callable callable | this.isReturnValue(callable) | + result = "[return] " + callable + ) + or + exists(Callable callable | this.isInnerReturnValue(callable) | + result = "[inner return] " + callable + ) + or + exists(Callable callable | this.isExceptionalReturnValue(callable) | + result = "[exceptional return] " + callable + ) + or + exists(Callable callable | this.isInnerExceptionalReturnValue(callable) | + result = "[inner exceptional return] " + callable + ) + or + exists(AstNode node, string tag | this.isSyntheticNode(node, tag) | + result = "[synthetic " + tag + "] " + node + ) + or + exists(LocalVariable v | this.isVariableRead(v) | + result = "[variable-read-builder] " + v.toString() + ) + or + exists(LocalVariable v | this.isVariableWrite(v) | + result = "[variable-write-builder] " + v.toString() + ) + or + this.isUnknown() and result = "[unknown]" + } + + Location getLocation() { + exists(AstNode node | this.isValueOf(node) | result = node.getLocation()) + or + exists(AstNode node | this.isIncomingValue(node) | result = node.getLocation()) + or + exists(AstNode node | this.isPostUpdatedValue(node) | result = node.getLocation()) + or + exists(NamespaceObject object | this.isNamespaceObject(object) | + result = object.getLocation() + ) + or + exists(AstNode tryBlock | this.isInterceptedException(tryBlock) | + result = tryBlock.getLocation() + ) + or + exists(Call call | this.isArgumentObject(call) | result = call.getLocation()) + or + exists(Call call | this.isReceiverArgument(call) | result = call.getLocation()) + or + exists(Call call | this.isPostUpdatedReceiverArgument(call) | + result = call.getLocation() + ) + or + exists(Call call | this.isPostUpdatedArgumentObject(call) | result = call.getLocation()) + or + exists(Callable callable | this.isParameterObject(callable) | + result = callable.getLocation() + ) + or + exists(Callable callable | this.isReceiverParameter(callable) | + result = callable.getLocation() + ) + or + exists(Callable callable | this.isCallableSelfReferenceParameter(callable) | + result = callable.getLocation() + ) + or + exists(Call call | this.isCallTarget(call) | result = call.getLocation()) + or + exists(Call call | this.isReturnValueFromCall(call) | result = call.getLocation()) + or + exists(Call call | this.isExceptionThrownFromCall(call) | result = call.getLocation()) + or + exists(Callable callable | this.isCallable(callable) | result = callable.getLocation()) + or + exists(Callable callable | this.isReturnValue(callable) | + result = callable.getLocation() + ) + or + exists(Callable callable | this.isExceptionalReturnValue(callable) | + result = callable.getLocation() + ) + or + exists(Callable callable | this.isInnerReturnValue(callable) | + result = callable.getLocation() + ) + or + exists(Callable callable | this.isInnerExceptionalReturnValue(callable) | + result = callable.getLocation() + ) + or + exists(AstNode node, string tag | this.isSyntheticNode(node, tag) | + result = node.getLocation() + ) + or + exists(LocalVariable v | this.isVariableRead(v) or this.isVariableWrite(v) | + result = v.getLocation() + ) + } + } + + /** A type of data flow step. */ + class StepBase extends TStep { + predicate receiverHint() { none() } + + pragma[nomagic] + predicate value() { this = TValueStep() } + + /** + * A step that acts as a value step, except it does not establish aliasing, so side-effects on the resulting + * object will not affect the input object. + */ + pragma[nomagic] + predicate copy() { this = TCopyStep() } + + pragma[nomagic] + predicate taint() { this = TTaintStep() } + + pragma[nomagic] + predicate read(ContentSet contents) { this = TReadStep(contents) } + + pragma[nomagic] + predicate store(ContentSet contents) { this = TStoreStep(contents) } + + pragma[nomagic] + predicate withContent(ContentSet contents) { this = TWithContentStep(contents) } + + pragma[nomagic] + predicate withoutContent(ContentSet contents) { this = TWithoutContentStep(contents) } + + pragma[nomagic] + predicate storeAsGetter(ContentSet contents) { this = TStoreAsGetterStep(contents) } + + pragma[nomagic] + predicate storeAsSetter(ContentSet contents) { this = TStoreAsSetterStep(contents) } + + pragma[nomagic] + predicate transform(Transform t) { this = TTransformStep(t) } + + /** + * Stores an object as the base object of another object. + * + * That is, for a step `node1 -> node2`, the value of `node1` becomes the base object of + * the value in `node2`. In other words, `node2` will inherit the contents of `node1`. + * + * Currently this does not affect `ConfigSig`-style data flow, but can affect the call graph. + */ + pragma[nomagic] + predicate storeBaseObject() { this = TStoreBaseObject() } + + pragma[nomagic] + predicate instantiate() { this = TInstantiateStep() } + + string toString() { + this.value() and result = "value" + or + this.copy() and result = "copy" + or + this.taint() and result = "taint" + or + exists(ContentSet contents | + this.read(contents) and result = "read(" + contents.toString() + ")" + or + this.store(contents) and result = "store(" + contents.toString() + ")" + or + this.withContent(contents) and result = "withContent(" + contents.toString() + ")" + or + this.withoutContent(contents) and + result = "withoutContent(" + contents.toString() + ")" + or + this.storeAsGetter(contents) and result = "storeAsGetter(" + contents.toString() + ")" + or + this.storeAsSetter(contents) and result = "storeAsSetter(" + contents.toString() + ")" + ) + or + exists(Transform t | this.transform(t) and result = "transform(" + t + ")") + or + this.storeBaseObject() and result = "storeBaseObject" + or + this.instantiate() and result = "instantiate" + } + } + + // + // Additional data flow nodes are materialised based on the step relation. + // + private class TDataFlowNode1 = + TBuilderNode or TVariableReadNode or TVariablePostUpdateNode or TVariableWriteNode or + TDynamicArgumentObjectNode or TDynamicParameterObjectNode or TClosureExprNode; + + private class TDataFlowNode2 = TDataFlowNode1 or TLocalSsaNode or TDerivedPostUpdateNode; + + private class TDataFlowNode3 = TDataFlowNode2 or TTransformStepNode; + + private class TDataFlowNode4 = + TDataFlowNode3 or TWithContentHelper or TWithoutContentHelper; + + private class TDataFlowNode5 = TDataFlowNode4 or TCaptureSsaNode; + + /** + * Data flow nodes materialised for stage 1. + * + * The `toString` and `getLocation` in this class are for supporting quick-eval only, and should + * not be materialised at runtime. + */ + private class DataFlowNode1 extends TDataFlowNode1 { + string toString() { + result = this.(BuilderNode).toString() + or + exists(LocalVariable v | + this = TVariableReadNode(v, _, _) and + result = "[variable read] " + v + or + this = TVariableWriteNode(v, _, _) and + result = "[variable write] " + v + or + this = TVariablePostUpdateNode(v, _, _) and + result = "[variable post-update] " + v + ) + or + exists(Call call | + this = TDynamicArgumentObjectNode(call) and + result = "[dynamic argument object] " + call + ) + or + exists(Callable callable | + this = TDynamicParameterObjectNode(callable) and + result = "[dynamic parameter object] " + callable + ) + or + exists(Callable callable, BasicBlock bb, int i | + this = TClosureExprNode(callable, bb, i) and + result = "[closure] " + callable.toString() + ) + } + + Location getLocation() { + result = this.(BuilderNode).getLocation() + or + exists(LocalVariable v, BasicBlock bb, int i | + this = TVariableReadNode(v, bb, i) or + this = TVariableWriteNode(v, bb, i) or + this = TVariablePostUpdateNode(v, bb, i) + | + result = bb.getNode(i).getLocation() + or + not exists(bb.getNode(i)) and result = v.getLocation() + ) + or + exists(Call call | + this = TDynamicArgumentObjectNode(call) and + result = call.getLocation() + ) + or + exists(Callable callable | + this = TDynamicParameterObjectNode(callable) and + result = callable.getLocation() + ) + or + exists(Callable callable, BasicBlock bb, int i | + this = TClosureExprNode(callable, bb, i) + | + result = bb.getNode(i).getLocation() + or + not exists(bb.getNode(i)) and + result = callable.getLocation() + ) + } + } + + /** + * Data flow nodes materialised for stage 2. + * + * The `toString` and `getLocation` in this class are for supporting quick-eval only, and should + * not be materialised at runtime. + */ + private class DataFlowNode2 extends TDataFlowNode2 { + string toString() { + result = this.(DataFlowNode1).toString() + or + exists(LocalSsaDataFlow::Node ssaNode | + this = TLocalSsaNode(ssaNode) and + result = "[ssa] " + ssaNode.toString() + ) + or + exists(DataFlowNode1 pre | + this = TDerivedPostUpdateNode(pre) and + result = "[post] " + pre.toString() + ) + } + + Location getLocation() { + result = this.(DataFlowNode1).getLocation() + or + exists(LocalSsaDataFlow::Node ssaNode | + this = TLocalSsaNode(ssaNode) and + result = ssaNode.getLocation() + ) + or + exists(DataFlowNode1 pre | + this = TDerivedPostUpdateNode(pre) and + result = pre.getLocation() + ) + } + } + + /** + * Data flow nodes materialised for stage 3. + * + * The `toString` and `getLocation` in this class are for supporting quick-eval only, and should + * not be materialised at runtime. + */ + private class DataFlowNode3 extends TDataFlowNode3 { + string toString() { + result = this.(DataFlowNode2).toString() + or + exists(BuilderNode node, string nodeName | + this = TTransformStepNode(node, nodeName) and + result = "[transform " + nodeName + "] " + node.toString() + ) + } + + Location getLocation() { + result = this.(DataFlowNode2).getLocation() + or + exists(BuilderNode node, string nodeName | + this = TTransformStepNode(node, nodeName) and + result = node.getLocation() + ) + } + } + + /** + * Data flow nodes materialised for stage 4. + * + * The `toString` and `getLocation` in this class are for supporting quick-eval only, and should + * not be materialised at runtime. + */ + private class DataFlowNode4 extends TDataFlowNode4 { + string toString() { + result = this.(DataFlowNode3).toString() + or + exists(DataFlowNode3 node, ContentSet contents | + this = TWithContentHelper(node, contents) and + result = "[with-content] " + node.toString() + or + this = TWithoutContentHelper(node, contents) and + result = "[without-content] " + node.toString() + ) + } + + Location getLocation() { + result = this.(DataFlowNode3).getLocation() + or + exists(DataFlowNode3 node, ContentSet contents | + this = TWithContentHelper(node, contents) and + result = node.getLocation() + or + this = TWithoutContentHelper(node, contents) and + result = node.getLocation() + ) + } + } + + /** + * Data flow nodes materialised for stage 5. + * + * The `toString` and `getLocation` in this class are for supporting quick-eval only, and should + * not be materialised at runtime. + */ + private class DataFlowNode5 extends TDataFlowNode5 { + string toString() { + result = this.(DataFlowNode4).toString() + or + exists(CaptureSsa::SynthesizedCaptureNode node | + this = TCaptureSsaNode(node) and + result = "[capture] " + node.toString() + ) + } + + Location getLocation() { + result = this.(DataFlowNode4).getLocation() + or + exists(CaptureSsa::SynthesizedCaptureNode node | + this = TCaptureSsaNode(node) and + result = node.getLocation() + ) + } + } + + /** Provides classes for constructing data flow steps. */ + module DataFlowBuilder { + class Step extends StepBase { + // Step() { any() } // Help catch some bugs in pracitce + } + + private Node getExceptionTargetFromContext(AstNode node) { + isInterceptingExceptions(node) and + result.isInterceptedException(node) + or + node instanceof Callable and + result.isInnerExceptionalReturnValue(node) + or + not node instanceof Callable and + not isInterceptingExceptions(node) and + result = getExceptionTargetFromContext(node.getParent()) + } + + class Node extends BuilderNode { + // // Use a binding set to quickly catch bugs in the step relation + // bindingset[this] + // Node() { any() } + /** + * Holds if this node is where exceptions thrown at `node` should propagate. + * + * This looks for the nearest enclosing AST which for which `isInterceptingExceptions` holds + * and gets the data flow node representing the intercepted exception. If no such node exists, + * this gets the inner exceptional return node of the enclosing callable. + */ + predicate isExceptionTargetFromContext(AstNode node) { + this = getExceptionTargetFromContext(node) + } + } + + // Note: the 'step' predicate cannot be provided via a module parameter, since its + // signature depends on 'Node' and the underlying newtype for 'Node' must reference the 'step' predicate. + class DataFlowSteps extends Unit { + pragma[inline] + abstract predicate step(Node node1, Step step, Node node2); + } + } + + final private class FinalLocalVariable = LocalVariable; + + pragma[nomagic] + private Cfg::EntryBasicBlock getEntryBlock(Callable scope) { + getCallableFromBasicBlock(result) = scope + } + + cached + private predicate dataflowNodeHasOwnCfgNode(BuilderNode node, BasicBlock bb, int i) { + exists(AstNode astNode | + node.isValueOf(astNode) and hasCompletionValue(astNode, bb.getNode(i)) + or + node.isIncomingValue(astNode) and hasIncomingValue(astNode, bb.getNode(i)) + or + node.isPostUpdatedValue(astNode) and hasPostUpdatedValue(astNode, bb.getNode(i)) + ) + or + exists(Callable callable | + node.isParameterObject(callable) + or + node.isReceiverParameter(callable) + or + node.isCallableSelfReferenceParameter(callable) + or + exists(NamespaceObject ns | + node.isNamespaceObject(ns) and + callable = ns.getEnclosingCallable() + ) + | + bb = getEntryBlock(callable) and + i = -1 + // TODO: do we need return nodes be associated with the exit block? + ) + or + exists(Call call | + node.isArgumentObject(call) + or + node.isPostUpdatedArgumentObject(call) + or + node.isReceiverArgument(call) + or + node.isPostUpdatedReceiverArgument(call) + or + node.isCallTarget(call) + or + node.isReturnValueFromCall(call) + or + node.isExceptionThrownFromCall(call) + | + bb.getNode(i) = getCfgNodeFromCall(call) + ) + or + exists(AstNode astNode, string tag | + synthesizeNode(astNode, tag, bb.getNode(i)) and + node.isSyntheticNode(astNode, tag) + ) + } + + private predicate dataflowNodeHasAmbiguousCfg(BuilderNode node) { + node instanceof TVariableNode + or + node instanceof TInterceptedExceptionNode + or + node instanceof TExceptionalReturnNode + or + node instanceof TInnerExceptionalReturnNode + or + node instanceof TReturnNode // TODO: return nodes should use the exit node + or + node instanceof TInnerReturnNode + } + + private predicate dataflowNodeHasPredecessorCfgNode(BuilderNode node, BasicBlock bb, int i) { + dataflowNodeHasOwnCfgNode(node, bb, i) + or + not dataflowNodeHasOwnCfgNode(node, _, _) and + not dataflowNodeHasAmbiguousCfg(node) and + exists(BuilderNode pred | + dataflowStep1(pred, _, node) and + dataflowNodeHasPredecessorCfgNode(pred, bb, i) + ) + } + + private predicate dataflowNodeHasSuccessorCfgNode(BuilderNode node, BasicBlock bb, int i) { + dataflowNodeHasOwnCfgNode(node, bb, i) + or + not dataflowNodeHasOwnCfgNode(node, _, _) and + not dataflowNodeHasAmbiguousCfg(node) and + exists(BuilderNode succ | + dataflowStep1(node, _, succ) and + dataflowNodeHasSuccessorCfgNode(succ, bb, i) + ) + } + + private predicate hasVariableReadAt( + LocalVariable v, BasicBlock bb, int i, boolean needsPostUpdate + ) { + exists(BuilderNode node | + dataflowStep1(TVariableNode(v, true), _, node) and + dataflowNodeHasSuccessorCfgNode(node, bb, i) and + needsPostUpdate = false + or + dataflowStep1(node, _, TVariableNode(v, true)) and + dataflowNodeHasPredecessorCfgNode(node, bb, i) and + needsPostUpdate = true + ) + } + + private predicate hasVariableWriteAt(LocalVariable v, BasicBlock bb, int i) { + exists(BuilderNode node | + dataflowStep1(node, _, TVariableNode(v, false)) and + dataflowNodeHasPredecessorCfgNode(node, bb, i) + ) + or + not definitelyInitialized(v) and + bb = getEntryBlock(v.getDeclaringCallable()) and + i = -2 // put implicit initialization at index -2, before parameters are assigned + } + + /** Gets the data flow node representing the value of `v` before its initialization. */ + private DataFlowNode1 getVariablePreInitializer(LocalVariable v) { + result = TVariableWriteNode(v, getEntryBlock(v.getDeclaringCallable()), -2) + } + + bindingset[node] + pragma[inline_late] + private predicate isVariableNode(BuilderNode node) { node instanceof TVariableNode } + + private predicate dataflowStep1A(DataFlowNode1 node1, StepBase step, DataFlowNode1 node2) { + dataflowStep1(node1, step, node2) and + not isVariableNode(node1) and + not isVariableNode(node2) + or + exists(LocalVariable v, BasicBlock bb, int i | + dataflowStep1(TVariableNode(v, true), step, node2) and + dataflowNodeHasSuccessorCfgNode(node2, bb, i) and + node1 = TVariableReadNode(v, bb, i) + or + dataflowStep1(node1, step, TVariableNode(v, false)) and + dataflowNodeHasPredecessorCfgNode(node1, bb, i) and + node2 = TVariableWriteNode(v, bb, i) + or + dataflowStep1(node1, step, TVariableNode(v, true)) and + dataflowNodeHasPredecessorCfgNode(node1, bb, i) and + node2 = TVariablePostUpdateNode(v, bb, i) + ) + or + exists(Callable callable, BasicBlock bb, int i | + dataflowStep1(TCallableNode(callable), step, node2) and + dataflowNodeHasSuccessorCfgNode(node2, bb, i) and + node1 = TClosureExprNode(callable, bb, i) + ) + } + + private StepBase valueOrReadStep() { + result instanceof TValueStep or result instanceof TReadStep + } + + pragma[nomagic] + private DataFlowNode1 getPostUpdateNode0(DataFlowNode1 pre) { + exists(AstNode astNode | + pre = TValueNode(astNode) and + result = TPostUpdatedValueNode(astNode) + ) + or + exists(Call call | + pre = TReceiverArgumentNode(call) and + result = TPostUpdatedReceiverArgumentNode(call) + ) + } + + private predicate needsDerivedPostUpdateNode1(DataFlowNode1 node) { + exists(DataFlowNode1 target | + dataflowStep1A(node, valueOrReadStep(), target) and + exists(getPostUpdateNode0(target)) + // example: TVariableReadNode flows to the `x` in `x.f = E`. `x` has a post update, we need a derived post-update for TVariableReadNode as well. + ) + or + dataflowStep1A(node, any(TStoreStep s), any(TArgumentObjectNode arg)) + or + node instanceof TCallTargetNode // for captured variables, we need to post-update the call target + or + exists(DataFlowNode1 prev | + needsDerivedPostUpdateNode(prev) and + dataflowStep1A(node, valueOrReadStep(), prev) + // Example: `x` in `x.f.g = E` where `x` has a read step to `x.f` which has a post-updated value. + ) + } + + private predicate needsDerivedPostUpdateNode(DataFlowNode1 node) { + needsDerivedPostUpdateNode1(node) and + not exists(getPostUpdateNode0(node)) + } + + pragma[nomagic] + private DataFlowNode2 getPostUpdateNode1(DataFlowNode2 pre) { + result = getPostUpdateNode0(pre) + or + exists(LocalVariable v, BasicBlock bb, int i | + pre = TVariableReadNode(v, bb, i) and + result = TVariablePostUpdateNode(v, bb, i) + ) + or + result = TDerivedPostUpdateNode(pre) + } + + /** + * Instantiation of SSA for non-captured variables. + */ + private module LocalSsaConfig implements Ssa::InputSig { + class SourceVariable extends FinalLocalVariable { + SourceVariable() { not this.isCaptured() } + } + + predicate variableWrite(BasicBlock bb, int i, SourceVariable v, boolean certain) { + certain = true and + hasVariableWriteAt(v, bb, i) + } + + predicate variableRead(BasicBlock bb, int i, SourceVariable v, boolean certain) { + certain = true and + hasVariableReadAt(v, bb, i, _) + } + } + + private module LocalSsa = Ssa::Make; + + private module LocalSsaDataFlowConfig implements LocalSsa::DataFlowIntegrationInputSig { + class Expr extends DataFlowNode1, TVariableReadNode { + predicate hasCfgNode(BasicBlock bb, int i) { this = TVariableReadNode(_, bb, i) } + } + + class GuardValue = D1::GuardValue; + + class Guard = D1::Guard; + + predicate guardDirectlyControlsBlock = D1::guardDirectlyControlsBlock/3; + + predicate includeWriteDefsInFlowStep() { none() } // not needed as we have lvalue nodes already + } + + private module LocalSsaDataFlow = LocalSsa::DataFlowIntegration; + + pragma[nomagic] + private DataFlowNode2 getNodeFromLocalSsa(LocalSsaDataFlow::Node node) { + result = TLocalSsaNode(node) // note: only holds for the SsaNode subclass + or + result = node.(LocalSsaDataFlow::ExprNode).getExpr() + or + result = getPostUpdateNode1(node.(LocalSsaDataFlow::ExprPostUpdateNode).getExpr()) + or + exists(LocalVariable v, BasicBlock bb, int i | + node.(LocalSsaDataFlow::WriteDefSourceNode).getDefinition().definesAt(v, bb, i) and + result = TVariableWriteNode(v, bb, i) + ) + } + + /** + * Holds if `pre`/`post` is a post-update pair, for which the pre-existing value of `contents` + * coming from `pre` should be blocked, so we only propagate the value coming from `post`. + */ + private predicate clearContentAfterPostUpdate( + DataFlowNode2 pre, DataFlowNode2 post, ContentSet contents + ) { + contentSetSupportsStrongUpdate(contents) and + dataflowStep1A(_, TStoreStep(contents), post) and + post = getPostUpdateNode1(pre) + or + exists(DataFlowNode2 pre1, DataFlowNode2 post1 | + clearContentAfterPostUpdate(pre1, post1, contents) and + dataflowStep1A(pre, TValueStep(), pre1) and + post = getPostUpdateNode1(pre) + ) + } + + pragma[nomagic] + private predicate dataflowStep2( + DataFlowNode2 node1, DataFlowBuilder::Step step, DataFlowNode2 node2 + ) { + dataflowStep1A(node1, step, node2) + or + exists( + LocalSsaDataFlow::Node ssaNode1, LocalSsaDataFlow::Node ssaNode2, boolean isUseStep + | + LocalSsaDataFlow::localFlowStep(_, ssaNode1, ssaNode2, isUseStep) and + node1 = getNodeFromLocalSsa(ssaNode1) and + node2 = getNodeFromLocalSsa(ssaNode2) and + ( + if isUseStep = true and clearContentAfterPostUpdate(node1, _, _) + then + step.withoutContent(any(ContentSet contents | + clearContentAfterPostUpdate(node1, _, contents) + )) + else step.value() + ) + ) + or + exists(DataFlowNode1 value1, DataFlowNode1 value2 | + // Add reverse value steps between post-update nodes. + // For a value step v1 -> v2, add post(v2) -> post(v1). + // Note that DataFlowImplCommon will handle this for read steps, so only handle value steps here. + dataflowStep1A(value1, step, value2) and + node1 = getPostUpdateNode1(value2) and + node2 = getPostUpdateNode1(value1) and + step.value() + ) + or + // Add default flow from inner return to actual return + exists(Callable callable | + node1 = TInnerReturnNode(callable) and + step.value() and + node2 = TReturnNode(callable) + or + node1 = TInnerExceptionalReturnNode(callable) and + step.value() and + node2 = TExceptionalReturnNode(callable) + | + not dataflowStep1(node1, _, _) // Suppress default flow if explicit flow is given + ) + } + + private DataFlowNode3 getTransformNode( + DataFlowNode2 node1, DataFlowNode2 node2, Transform transform, string nodeName + ) { + dataflowStep2(node1, TTransformStep(transform), node2) and + ( + nodeName = "input" and result = node1 + or + nodeName = "output" and result = node2 + or + result = TTransformStepNode(node2, nodeName) + ) + } + + private predicate dataflowStep3(DataFlowNode3 node1, StepBase step, DataFlowNode3 node2) { + dataflowStep2(node1, step, node2) and + not step instanceof TTransformStep + or + exists(BuilderNode input, Transform transform, BuilderNode output | + dataflowStep2(input, TTransformStep(transform), output) + | + exists(string tmp1, string tmp2 | + transform.step(tmp1, step, tmp2) and + node1 = getTransformNode(input, output, transform, tmp1) and + node2 = getTransformNode(input, output, transform, tmp2) + ) + ) + } + + private predicate dataflowStep4(DataFlowNode4 node1, StepBase step, DataFlowNode4 node2) { + dataflowStep3(node1, step, node2) and + not step instanceof TWithContentStep and + not step instanceof TWithoutContentStep + or + step.copy() and + exists(ContentSet contents, DataFlowNode3 orig1, DataFlowNode3 orig2 | + dataflowStep3(orig1, TWithContentStep(contents), orig2) and + ( + node1 = orig1 and + node2 = TWithContentHelper(orig2, contents) + or + node1 = TWithContentHelper(orig2, contents) and + node2 = orig2 + ) + or + dataflowStep3(orig1, TWithoutContentStep(contents), orig2) and + ( + node1 = orig1 and + node2 = TWithoutContentHelper(orig2, contents) + or + node1 = TWithoutContentHelper(orig2, contents) and + node2 = orig2 + ) + ) + } + + final private class FinalCallable = Callable; + + private predicate approxCaptureStep(DataFlowNode4 node1, DataFlowNode4 node2) { + // For captured variables, step from any write to any read. For non-captured variables we instead rely on local SSA flow paths. + // To avoid N^2 edges we use the TVariableNode from the builder stage as a pivot connecting any write to any read. + exists(VariableCaptureConfig::CapturedVariable v | + node1 = TVariableWriteNode(v, _, _) and + node2.(BuilderNode).isVariableRead(v) + or + node1.(BuilderNode).isVariableRead(v) and + node2 = TVariableReadNode(v, _, _) + ) + } + + cached + private predicate valueStepApprox(DataFlowNode4 node1, DataFlowNode4 node2) { + dataflowStep4(node1, TValueStep(), node2) + or + approxCaptureStep(node1, node2) + } + + cached + private DataFlowNode4 callableInitialNode(Callable callable) { + result = TClosureExprNode(callable, _, _) + or + result = TCallableSelfReferenceNode(callable) + } + + module VariableCaptureConfig implements VariableCapture::InputSig { + Callable basicBlockGetEnclosingCallable(BasicBlock bb) { + result = getCallableFromBasicBlock(bb) + } + + class CapturedVariable extends FinalLocalVariable { + CapturedVariable() { this.isCaptured() } + + /** Gets the callable that defines this variable. */ + Callable getCallable() { result = this.getDeclaringCallable() } + } + + class CapturedParameter extends CapturedVariable { + CapturedParameter() { none() } // Not needed here, as parameters and local variable writes (lvalues) are separate nodes + } + + class Expr extends DataFlowNode4 { + predicate hasCfgNode(BasicBlock bb, int i) { + this = TVariableReadNode(_, bb, i) + or + this = TClosureExprNode(_, bb, i) + } + } + + class VariableWrite extends DataFlowNode4, TVariableWriteNode { + VariableWrite() { this = TVariableWriteNode(any(CapturedVariable v), _, _) } + + CapturedVariable getVariable() { this = TVariableWriteNode(result, _, _) } + + predicate hasCfgNode(BasicBlock bb, int i) { this = TVariableWriteNode(_, bb, i) } + } + + class VariableRead extends Expr, TVariableReadNode { + VariableRead() { this = TVariableReadNode(any(CapturedVariable v), _, _) } + + CapturedVariable getVariable() { this = TVariableReadNode(result, _, _) } + } + + additional predicate callableHasAlias(Callable callable, DataFlowNode4 node) { + valueStepApprox*(callableInitialNode(callable), node) + } + + class ClosureExpr extends Expr, TClosureExprNode { + predicate hasAliasedAccess(Expr f) { + exists(Callable callable | + this.hasBody(callable) and + callableHasAlias(callable, f) + ) + // TODO: check that `f` has a post-update node? + } + + predicate hasBody(Callable callable) { this = TClosureExprNode(callable, _, _) } + } + + class Callable extends FinalCallable { + predicate isConstructor() { + none() // TODO - not needed for JS + } + } + } + + private module CaptureSsa = VariableCapture::Flow; + + private DataFlowNode5 getNodeFromCaptureSsa(CaptureSsa::ClosureNode node) { + result = TCaptureSsaNode(node) // note: only holds for SynthesizedClosureNode subclass + or + result = node.(CaptureSsa::ExprNode).getExpr() + or + result = getPostUpdateNode1(node.(CaptureSsa::ExprPostUpdateNode).getExpr()) + or + result = node.(CaptureSsa::VariableWriteSourceNode).getVariableWrite() + or + result = TCallableSelfReferenceNode(node.(CaptureSsa::ThisParameterNode).getCallable()) + } + + private predicate dataflowStep5(DataFlowNode5 node1, StepBase step, DataFlowNode5 node2) { + dataflowStep4(node1, step, node2) + or + exists(CaptureSsa::ClosureNode closureNode1, CaptureSsa::ClosureNode closureNode2 | + node1 = getNodeFromCaptureSsa(closureNode1) and + node2 = getNodeFromCaptureSsa(closureNode2) + | + CaptureSsa::localFlowStep(closureNode1, closureNode2) and step = TValueStep() + or + exists(LocalVariable v | + CaptureSsa::readStep(closureNode1, v, closureNode2) and + step = TReadStep(TSingleton(TCaptureContent(v))) + or + CaptureSsa::storeStep(closureNode1, v, closureNode2) and + step = TStoreStep(TSingleton(TCaptureContent(v))) + ) + ) + } + + private DataFlowNode5 getPostUpdateNode2(DataFlowNode5 pre) { + result = getPostUpdateNode1(pre) + or + exists(CaptureSsa::ClosureNode closurePost, CaptureSsa::ClosureNode closurePre | + CaptureSsa::capturePostUpdateNode(closurePost, closurePre) and + result = getNodeFromCaptureSsa(closurePost) and + pre = getNodeFromCaptureSsa(closurePre) + ) + } + + private class DataFlowNodeLastStage = DataFlowNode5; // Alias for the final stage, but with internal toString method + + final private class DataFlowNode extends DataFlowNodeLastStage { + predicate isValueOf(AstNode node) { this = TValueNode(node) } + + predicate isIncomingValue(AstNode node) { this = TIncomingValueNode(node) } + + predicate isPostUpdatedValue(AstNode node) { this = TPostUpdatedValueNode(node) } + + predicate isNamespaceObject(NamespaceObject ns) { this = TNamespaceNode(ns) } + + /** + * Holds if this represents the value of `v` before it has been assigned in the code. + * Depending on the language, this may refer to the uninitialised state of the variable (garbage memory), + * or its predefined value coming from the language prelude. + * + * When working with parameters, note that the pre-initializer node does _not_ refer to the incoming parameter value. + * "Pre-initialization" occurs before the parameters are assigned their incoming values. + */ + predicate isVariablePreInitializer(LocalVariable v) { + this = getVariablePreInitializer(v) + } + + predicate isSyntheticNode(AstNode node, string tag) { this = TSyntheticNode(node, tag) } + + AstNode asAstNode() { this.isValueOf(result) or this.isIncomingValue(result) } + + Callable getEnclosingSourceCallable() { + result = nodeGetEnclosingCallable(this).asSourceCallable() + } + // TODO: For now we reuse the expensive toString and getLocation predicates. + // Eventually we want to shadow them here with more efficient implementations to ensure they don't get evaluated at runtime. + } + + private class Node = DataFlowNode; + + private newtype TDataFlowCallable = MkSourceCallable(Callable c) + + class DataFlowCallable extends TDataFlowCallable { + Callable asSourceCallable() { this = MkSourceCallable(result) } + + string toString() { result = this.asSourceCallable().toString() } + + Location getLocation() { result = this.asSourceCallable().getLocation() } + } + + DataFlowCallable nodeGetEnclosingCallable(Node node) { + exists(BasicBlock bb | + dataflowNodeHasOwnCfgNode(node, bb, _) and + result.asSourceCallable() = getCallableFromBasicBlock(bb) + ) + or + exists(Call call | + node = TDynamicArgumentObjectNode(call) and + result.asSourceCallable() = getCallableFromCfgNode(getCfgNodeFromCall(call)) + ) + or + exists(Callable callable | + node = TReturnNode(callable) + or + node = TInnerReturnNode(callable) + or + node = TExceptionalReturnNode(callable) + or + node = TInnerExceptionalReturnNode(callable) + or + node = TParameterObjectNode(callable) + or + node = TDynamicParameterObjectNode(callable) + | + result.asSourceCallable() = callable + ) + or + exists(LocalSsaDataFlow::SsaNode n | + node = TLocalSsaNode(n) and + result.asSourceCallable() = getCallableFromBasicBlock(n.getBasicBlock()) + ) + or + exists(CaptureSsa::SynthesizedCaptureNode n | + node = TCaptureSsaNode(n) and + result.asSourceCallable() = n.getEnclosingCallable() + ) + or + exists(BasicBlock bb | + ( + node = TVariableReadNode(_, bb, _) or + node = TVariableWriteNode(_, bb, _) or + node = TVariablePostUpdateNode(_, bb, _) or + node = TClosureExprNode(_, bb, _) + ) and + result.asSourceCallable() = getCallableFromBasicBlock(bb) + ) + or + exists(DataFlowNode1 pre | + node = TDerivedPostUpdateNode(pre) and + result = nodeGetEnclosingCallable(pre) + ) + or + exists(BuilderNode inner, string nodeName | + node = TTransformStepNode(inner, nodeName) and + result = nodeGetEnclosingCallable(inner) + ) + or + exists(DataFlowNode3 inner, ContentSet contents | + node = TWithContentHelper(inner, contents) and + result = nodeGetEnclosingCallable(inner) + or + node = TWithoutContentHelper(inner, contents) and + result = nodeGetEnclosingCallable(inner) + ) + } + + signature module UnifiedDataFlowSig4 { + predicate isTrackedAllocationSite(Node node); + + predicate modelEntryPoint(string head, string operand, Node node); + + predicate argumentParameterContent(string operand, Content content); + + /** Gets the content of a parameter object that contains the argument to a setter. */ + Content setterParameter(); + + /* + * Stages of steps + * - Builder nodes + * - CONTRIBUTE builder steps overlay[local] + * - Materialise all nodes + * - Evaluate local alias models + * - Import resolution + * - CONTRIBUTE pre-call graph local steps + * - CONTRIBUTE pre-call graph global steps + * - Build call graph + * - CONTRIBUTE post-call graph steps + * - Global data flow + */ + + /** + * A data flow step generated in the "late stage", i.e. after all data-flow nodes have been materialised. + */ + predicate lateStageStep(Node node1, StepBase step, Node node2); + } + + module MakeUnifiedDataFlow4 { + predicate parameterReadStep(Callable callable, ContentSet contents, Node node2) { + dataflowStep5(TParameterObjectNode(callable), TReadStep(contents), node2) + } + + predicate argumentStoreStep(Node node1, ContentSet contents, Call call) { + dataflowStep5(node1, TStoreStep(contents), TArgumentObjectNode(call)) + } + + predicate argumentPostUpdateReadStep(Call call, ContentSet contents, Node node2) { + dataflowStep5(TPostUpdatedArgumentObjectNode(call), TReadStep(contents), node2) + } + + private newtype TParameterPosition = + MkParameterPosition(ContentSet contents) { parameterReadStep(_, contents, _) } or + /** + * Parameter position corresponding to the "full" parameter node with all its original outgoing flow steps. + */ + MkStaticParameterObjectPosition() or + /** + * Parameter position corresponding to the dynamic parameter node, which only has the non-read outgoing flow steps. + * These are the parameters that can't be translated to a statically-known `MkParameterPosition`. + */ + MkDynamicParameterObjectPosition() or + MkFunctionSelfReferenceParameterPosition() or + MkReceiverParameterPosition() + + class ParameterPosition extends TParameterPosition { + ContentSet asContentSet() { this = MkParameterPosition(result) } + + string toString() { + result = this.asContentSet().toString() + or + this = MkStaticParameterObjectPosition() and result = "static parameter object" + or + this = MkDynamicParameterObjectPosition() and result = "dynamic parameter object" + or + this = MkFunctionSelfReferenceParameterPosition() and + result = "function self-reference" + or + this = MkReceiverParameterPosition() and result = "receiver parameter" + } + + Location getLocation() { result = this.asContentSet().getLocation() } + } + + private newtype TArgumentPosition = + MkArgumentPosition(ContentSet contents) { argumentStoreStep(_, contents, _) } or + /** + * Argument position corresponding to the "full" argument node with all its original ingoing flow steps. + */ + MkStaticArgumentObjectPosition() or + /** + * Argument position corresponding to the dynamic argument node, which only has the non-store ingoing flow steps. + * These are the arguments that can't be translated to a statically-known `MkArgumentPosition`. + */ + MkDynamicArgumentObjectPosition() or + MkFunctionSelfReferenceArgumentPosition() or + MkReceiverArgumentPosition() or + MkSetterArgumentPosition() + + class ArgumentPosition extends TArgumentPosition { + ContentSet asContentSet() { this = MkArgumentPosition(result) } + + string toString() { + result = this.asContentSet().toString() + or + this = MkStaticArgumentObjectPosition() and result = "static argument object" + or + this = MkDynamicArgumentObjectPosition() and result = "dynamic argument object" + or + this = MkFunctionSelfReferenceArgumentPosition() and + result = "function self-reference" + or + this = MkReceiverArgumentPosition() and result = "receiver argument" + or + this = MkSetterArgumentPosition() and result = "setter argument" + } + + Location getLocation() { result = this.asContentSet().getLocation() } + } + + predicate dataflowStep6(Node node1, DataFlowBuilder::Step step, Node node2) { + dataflowStep5(node1, step, node2) and + not node2 instanceof TPostUpdatedArgumentObjectNode + or + // For non-store steps into the argument object, also flow to the dynamic argument object. + exists(Call call | + dataflowStep5(node1, step, TArgumentObjectNode(call)) and + not step instanceof TStoreStep and + node2 = TDynamicArgumentObjectNode(call) + ) + or + // For non-read steps from the parameter object, also flow from the dynamic parameter object. + exists(Callable callable | + dataflowStep5(TParameterObjectNode(callable), step, node2) and + not step instanceof TReadStep and + node1 = TDynamicParameterObjectNode(callable) + ) + or + exists(ContentSet contents, Call call, DataFlowNode5 arg | + argumentStoreStep(arg, contents, call) and + argumentPostUpdateReadStep(call, contents, node2) and + node1 = getPostUpdateNode2(arg) and + step.value() + ) + } + + predicate dataflowStep7(Node node1, DataFlowBuilder::Step step, Node node2) { + dataflowStep6(node1, step, node2) + or + lateStageStep(node1, step, node2) + } + + private predicate dataflowStepFinal = dataflowStep7/3; + + private predicate getPostUpdateNodeFinal = getPostUpdateNode2/1; + + private Node getPreUpdateNodeFinal(Node node) { getPostUpdateNodeFinal(result) = node } + + predicate parameterNodeImpl(Callable callable, ParameterPosition pos, Node node) { + parameterReadStep(callable, pos.asContentSet(), node) + or + pos = MkStaticParameterObjectPosition() and + node = TParameterObjectNode(callable) + or + pos = MkDynamicParameterObjectPosition() and + node = TDynamicParameterObjectNode(callable) + or + pos = MkFunctionSelfReferenceParameterPosition() and + node = TCallableSelfReferenceNode(callable) + or + pos = MkReceiverParameterPosition() and + node = TReceiverParameterNode(callable) + } + + predicate argumentNodeImpl(Call call, ArgumentPosition pos, Node node) { + argumentStoreStep(node, pos.asContentSet(), call) + or + pos = MkStaticArgumentObjectPosition() and + node = TArgumentObjectNode(call) + or + pos = MkDynamicArgumentObjectPosition() and + node = TDynamicArgumentObjectNode(call) + or + pos = MkFunctionSelfReferenceArgumentPosition() and + node = TCallTargetNode(call) + or + pos = MkReceiverArgumentPosition() and + node = TReceiverArgumentNode(call) + } + + private int getAstNodeDepth(AstNode node) { + not exists(node.getParent()) and result = 0 + or + result = 1 + getAstNodeDepth(node.getParent()) + } + + private string getDisambiguatingTag1(Node node) { + exists(AstNode astNode | + ( + node.isValueOf(astNode) or + node.isIncomingValue(astNode) or + node.isPostUpdatedValue(astNode) + ) and + result = getAstNodeDepth(astNode).toString() + ) + } + + private string getDisambiguatingTag(Node node) { + result = getDisambiguatingTag1(node) + or + not exists(getDisambiguatingTag1(node)) and + result = "" + } + + /** A node to be included in the output of `TestOutput`. */ + signature class RelevantNodeSig extends Node; + + private module TestTestOutput = TestOutput; + + bindingset[s] + signature string escapeSig(string s); + + signature string lineBreakSig(); + + /** + * Renders a node label, either for a Mermaid diagram, or a `graph` output relation. + * Escaping works differently in these contexts. + */ + module MakeLabeler { + private string getAnAnnotationFor(Node node) { + exists(ContentSet contents | + DataFlowInput::clearsContent(node, contents) and + result = lineBreak() + "[clears " + escape(contents.toString()) + "]" + or + DataFlowInput::expectsContent(node, contents) and + result = lineBreak() + "[expects " + escape(contents.toString()) + "]" + ) + } + + string getLabel(Node node) { + result = escape(node.toString()) + concat(getAnAnnotationFor(node)) + } + } + + /** + * Import this module into a `.ql` file to output a Mermaid graph. The + * graph is restricted to nodes from `RelevantNode`. + */ + module TestOutput { + /** Holds if `pred -> succ` is an edge in the relevant part of the data flow graph. */ + query predicate edges(RelevantNode node1, RelevantNode node2, string label) { + label = + strictconcat(string s | + exists(StepBase step | + dataflowStepFinal(node1, step, node2) and + s = step.toString() + ) + or + s = "post-update" and + node2 = getPostUpdateNodeFinal(node1) + | + s, ", " order by s + ) + } + + /** + * Provides logic for representing a relevant part of the data flow graph as a [Mermaid diagram](https://mermaid.js.org/). + */ + module Mermaid { + private string nodeId(RelevantNode n) { + result = + any(int i | + n = + rank[i](RelevantNode p, string filePath, int startLine, int startColumn, + int endLine, int endColumn | + p.getLocation() + .hasLocationInfo(filePath, startLine, startColumn, endLine, endColumn) + | + p + order by + filePath, startLine, startColumn, endLine, endColumn, p.(Node).toString(), + getDisambiguatingTag(p) + ) + ).toString() + } + + predicate ambiguousId(RelevantNode n, string id, TDataFlowNode raw) { + id = nodeId(n) and + not exists(unique(RelevantNode m | id = nodeId(m))) and + raw = n + } + + bindingset[s] + private string escapeMermaidLabel(string s) { + result = + s.replaceAll("\\", "\") + .replaceAll("\"", """) + .replaceAll("\n", 9226.toUnicode()) // replace line breaks with 'LF' symbol + } + + private string lineBreak() { result = "\\n" } // becomes an actual line break inside the label + + private module Labeler = MakeLabeler; + + private string nodes() { + result = + concat(RelevantNode n, string id | + id = nodeId(n) + | + id + "[\"" + Labeler::getLabel(n) + "\"]", "\n" order by id + ) + } + + private string edge(RelevantNode pred, RelevantNode succ) { + edges(pred, succ, _) and + exists(string label | + edges(pred, succ, label) and + if label = "" + then result = nodeId(pred) + " --> " + nodeId(succ) + else + result = + nodeId(pred) + " -- " + escapeMermaidLabel(label) + " --> " + nodeId(succ) + ) + } + + private string edges() { + result = + concat(RelevantNode pred, RelevantNode succ, string edge, string filePath, + int startLine, int startColumn, int endLine, int endColumn | + edge = edge(pred, succ) and + pred.getLocation() + .hasLocationInfo(filePath, startLine, startColumn, endLine, endColumn) + | + edge, "\n" + order by + filePath, startLine, startColumn, endLine, endColumn, pred.(Node).toString() + ) + } + + /** Holds if the Mermaid representation is `s`. */ + query predicate mermaid(string s) { + s = "flowchart TD\n" + nodes() + "\n\n" + edges() + } + } + } + + /** Provides the input to `ViewCfgQuery`. */ + signature module ViewDfgQueryInputSig { + /** The source file selected in the IDE. Should be an `external` predicate. */ + string selectedSourceFile(); + + /** The source line selected in the IDE. Should be an `external` predicate. */ + int selectedSourceLine(); + + /** The source column selected in the IDE. Should be an `external` predicate. */ + int selectedSourceColumn(); + + File getFileFromLocation(Location loc); + } + + /** + * Provides an implementation for a `View DFG` query (which currently has to happen by hijacking the `View CFG` query). + * + * Import this module into a `.ql` that looks like + * + * ```ql + * @name Print DFG + * @description Produces a representation of a file's Data Flow Graph + * This query is used by the VS Code extension. + * @id /print-dfg + * @kind graph + * @tags ide-contextual-queries/print-cfg + * ``` + * + * Note that the tag name is `print-cfg` because `print-dfg` is not currently recognised by vscode. + */ + module ViewDfgQuery ViewCfgQueryInput> { + private import ViewCfgQueryInput + + predicate callableSpan( + Callable callable, File file, int startLine, int startColumn, int endLine, + int endColumn + ) { + exists(Location loc | + loc = callable.getLocation() and + file = getFileFromLocation(callable.getLocation()) and + loc.hasLocationInfo(_, startLine, startColumn, endLine, endColumn) + ) + } + + bindingset[file, line, column] + private Callable smallestEnclosingScope(File file, int line, int column) { + result = + min(Callable scope, int startLine, int startColumn, int endLine, int endColumn | + callableSpan(scope, file, startLine, startColumn, endLine, endColumn) and + ( + startLine < line + or + startLine = line and startColumn <= column + ) and + ( + endLine > line + or + endLine = line and endColumn >= column + ) + | + scope order by startLine desc, startColumn desc, endLine, endColumn + ) + } + + private import IdeContextual + + final private class FinalDataFlowNode = Node; + + private class RelevantNode extends FinalDataFlowNode { + RelevantNode() { + nodeGetEnclosingCallable(this).asSourceCallable() = + smallestEnclosingScope(getFileBySourceArchiveName(selectedSourceFile()), + selectedSourceLine(), selectedSourceColumn()) + } + + string getOrderDisambiguation() { result = "" } + } + + private module Output = TestOutput; + + import Output::Mermaid + + bindingset[s] + private string escape(string s) { result = s } + + private string lineBreak() { result = " " } // It seems there is no way to make line breaks here + + private module Labeler = MakeLabeler; + + query predicate nodes(RelevantNode node, string attr, string val) { + attr = "semmle.label" and + val = Labeler::getLabel(node) + } + + /** Holds if `pred` -> `succ` is an edge in the CFG. */ + query predicate edges(RelevantNode pred, RelevantNode succ, string attr, string val) { + attr = "semmle.label" and + Output::edges(pred, succ, val) + } + } + + final private class FinalContent = Content; + + final private class FinalContentSet = ContentSet; + + final private class FinalParameterPosition = ParameterPosition; + + final private class FinalArgumentPosition = ArgumentPosition; + + private import D4 + + private signature predicate aliasModelSig(string aliasName, string accessPath); + + private signature predicate dataflowStepSig( + Node node1, DataFlowBuilder::Step step, Node node2 + ); + + module MakeModelsAsData { + private predicate aliasAccessPath(string accessPath) { aliasModel(_, accessPath) } + + private module AliasAccessPaths = AccessPathSyntax::AccessPath; + + private import AliasAccessPaths + + signature predicate relevantAliasNamesSig(string name); + + /** Makes a MaD evaluator using the given set of data flow steps. */ + private module MakeStage< + dataflowStepSig/3 step, relevantAliasNamesSig/1 relevantAliasNames> + { + private predicate relevantAliasNamesEx(string name) { + relevantAliasNames(name) + or + exists(string otherAlias, AccessPath accessPath | + // We need to evaluate 'X' and there is a model `X -> Alias[Y].Blah`, so we need to evaluate 'Y' + aliasModel(otherAlias, accessPath) and + unfoldToken(accessPath.getToken(0), "Alias", name) + ) + } + + private module Steps { + predicate readStep(Node node1, ContentSet contents, Node node2) { + step(node1, TReadStep(contents), node2) + } + + predicate storeStep(Node node1, ContentSet contents, Node node2) { + step(node1, TStoreStep(contents), node2) + } + } + + private predicate unfoldToken(AccessPathToken tok, string head, string operand) { + head = tok.getName() and + ( + operand = tok.getAnArgument() + or + not exists(tok.getArgumentList()) and + operand = "" + ) + } + + pragma[nomagic] + private Content contentFromToken(AccessPathToken tok) { + exists(string head, string operand | + unfoldToken(tok, head, operand) and + result.hasModelToken(head, operand) + ) + } + + Node getAnAliasSource(string aliasName) { + exists(AccessPath path, int n | + result = getAnAliasSource(aliasName, path, n) and + n = path.getNumToken() + ) + } + + Node getAnAliasSource(string aliasName, AccessPath accessPath, int n) { + aliasModel(aliasName, accessPath) and + relevantAliasNamesEx(aliasName) and + n = 1 and + exists(string head, string operand | + unfoldToken(accessPath.getToken(0), head, operand) + | + modelEntryPoint(head, operand, result) + or + head = "Alias" and + result = getAnAliasSource(operand) + ) + or + valueStepApprox(getAnAliasSource(aliasName, accessPath, n), result) + or + exists(Node prev, AccessPathToken token | + prev = getAnAliasSource(aliasName, accessPath, n - 1) and + token = accessPath.getToken(n - 1) + | + exists(ContentSet contents | + Steps::readStep(prev, contents, result) and + contents.getAReadContent() = contentFromToken(token) + ) + ) + or + exists(ContentSet contents | + Steps::readStep(getAnAliasSourceParameterObject(aliasName, accessPath, n - 1), + contents, result) and + argumentParameterContent(accessPath.getToken(n - 1).getAnArgument(), + contents.getAReadContent()) + ) + } + + Node getAnAliasSinkArgumentObject(string aliasName, AccessPath accessPath, int n) { + exists(Call call | + getAnAliasSource(aliasName, accessPath, n) = TCallTargetNode(call) and + accessPath.getToken(n).getName() = "Argument" and + result = TArgumentObjectNode(call) + ) + or + valueStepApprox(result, getAnAliasSinkArgumentObject(aliasName, accessPath, n)) + } + + Node getAnAliasSink(string aliasName, AccessPath accessPath, int n) { + exists(ContentSet contents | + Steps::storeStep(result, contents, + getAnAliasSinkArgumentObject(aliasName, accessPath, n - 1)) and + argumentParameterContent(accessPath.getToken(n - 1).getAnArgument(), + contents.getAStoreContent()) + ) + } + + Node getAnAliasSourceParameterObject(string aliasName, AccessPath accessPath, int n) { + exists(Callable callable | + getAnAliasSink(aliasName, accessPath, n) = TClosureExprNode(callable, _, _) and + accessPath.getToken(n).getName() = "Parameter" and + result = TParameterObjectNode(callable) + ) + or + valueStepApprox(getAnAliasSourceParameterObject(aliasName, accessPath, n), result) + } + } + + module EvaluatePreCallGraph { + import MakeStage + } + } + + /** Holds if `(node, step)` is the only flow step targeting `node2`. */ + private predicate uniqueIncomingStep(Node node1, StepBase step, Node node2) { + node1 = unique(Node pred, StepBase s | dataflowStep6(pred, s, node2) | pred) and + dataflowStep6(node1, step, node2) // bind 'step' again because 'unique' can only report back 1 column + } + + private predicate uniqueIncomingValueStep(Node node1, Node node2) { + uniqueIncomingStep(node1, TValueStep(), node2) + } + + private predicate localMustFlowStep(Node node1, Node node2) { + exists(LocalSsaDataFlow::Node ssaNode1, LocalSsaDataFlow::Node ssaNode2 | + LocalSsaDataFlow::localMustFlowStep(_, ssaNode1, ssaNode2) and + node1 = getNodeFromLocalSsa(ssaNode1) and + node2 = getNodeFromLocalSsa(ssaNode2) + ) + or + uniqueIncomingValueStep(node1, node2) + } + + signature module ValuePropagationInputSig { + bindingset[this] + class Value; + + predicate shouldComputeValue(Node node); + + /** Holds if may-flow steps are allowed. */ + predicate allowMayFlow(); + + /** + * Holds if captured variables that only have a single explicit assignment should be assumed to + * have the constant-value from that assignment, even if it cannot be proven that the variable + * has been initialized before the read. + */ + default predicate assumeInitializedCapturedVariables() { any() } + + /** + * Holds if the given variable `v` should be treated as having a constant value. + * + * This is in addition to the captured variable covered by `assumeInitializedCapturedVariables()`. + */ + default predicate treatAsConstant(LocalVariable v) { none() } + } + + module MakeValuePropagation { + private import Input + + pragma[nomagic] + private predicate shouldComputeValueEx(Node node) { + shouldComputeValue(node) + or + exists(Node other | + shouldComputeValueEx(other) and + node = any(ValuePropagationRule r).getADependency(other) + ) + } + + pragma[nomagic] + Value getValue(Node node) { + shouldComputeValueEx(node) and + result = any(ValuePropagationRule r).computeValue(node) + } + + pragma[nomagic] + Value getValueOfExpr(AstNode node) { result = getValue(TValueNode(node)) } + + pragma[nomagic] + Value getIncomingValue(AstNode node) { result = getValue(TIncomingValueNode(node)) } + + pragma[nomagic] + private predicate hasConstantValue(Node node) { exists(getValue(node)) } + + class ValuePropagationRule extends Unit { + /** + * Gets the data flow nodes whose constant-value should be computed, in order to compute the constant-value of `node`. + * + * The caller will restrict `node` to nodes whose constant-value is already known to be needed. + * + * For example, a binary expression would typically depend on its operands. + * + * Note that the framework assumes that calls whose call target is a constant have a dependency on the arguments + * of that call (i.e. the nodes that are directly stored on its argument object). + */ + bindingset[node] + abstract Node getADependency(Node node); + + /** + * Computes the constant value of `node`, if any. The implementation should use `getValue` to access + * already-computed constants. + */ + bindingset[node] + abstract Value computeValue(Node node); + } + + private predicate treatAsConstantEx(LocalVariable v) { + Input::treatAsConstant(v) + or + Input::assumeInitializedCapturedVariables() and + v.isCaptured() and + exists( + unique(BuilderNode node1, StepBase step | + dataflowStep1(node1, step, TVariableNode(v, false)) + | + node1 + ) + ) + } + + pragma[nomagic] + private predicate constantStep(Node node1, Node node2) { + localMustFlowStep(node1, node2) + or + exists(LocalVariable v | treatAsConstantEx(v) | + node1 = TVariableWriteNode(v, _, _) and + node2 = TVariableNode(v, true) + or + node1 = TVariableNode(v, true) and + node2 = TVariableReadNode(v, _, _) + ) + } + + private class MustFlow extends ValuePropagationRule { + bindingset[node] + override Node getADependency(Node node) { constantStep(result, node) } + + override Value computeValue(Node node) { + exists(Node pred | + constantStep(pred, node) and + result = getValue(pred) + ) + } + } + + private class CallHelper extends ValuePropagationRule { + bindingset[node] + override Node getADependency(Node node) { + exists(Call call | + hasConstantValue(TCallTargetNode(call)) and + node = TOutNode(call) and + argumentStoreStep(result, _, call) + ) + } + + override Value computeValue(Node node) { none() } + } + } + + private predicate nodeGetEnclosingCallableAlias = nodeGetEnclosingCallable/1; + + final private class DataFlowCallableAlias = DataFlowCallable; + + private module Steps { + pragma[nomagic] + predicate readStep(Node node1, ContentSet contents, Node node2) { + dataflowStepFinal(node1, TReadStep(contents), node2) + } + + pragma[nomagic] + predicate storeStep(Node node1, ContentSet contents, Node node2) { + dataflowStepFinal(node1, TStoreStep(contents), node2) + } + + pragma[nomagic] + predicate copyStep(Node node1, Node node2) { + dataflowStepFinal(node1, TCopyStep(), node2) + } + + pragma[nomagic] + predicate simpleValueStep(Node node1, Node node2) { + dataflowStepFinal(node1, TValueStep(), node2) + } + + pragma[nomagic] + predicate storeContentNoCapture(Node node1, Content content, Node node2) { + exists(ContentSet contents | + dataflowStepFinal(node1, TStoreStep(contents), node2) and + content = contents.getAStoreContent() and + not content instanceof TCaptureContent + ) + } + + pragma[nomagic] + predicate storeGetter(Node node1, Content content, Node node2) { + exists(ContentSet contents | + dataflowStepFinal(node1, TStoreAsGetterStep(contents), node2) and + content = contents.getAStoreContent() + ) + } + + pragma[nomagic] + predicate storeSetter(Node node1, Content content, Node node2) { + exists(ContentSet contents | + dataflowStepFinal(node1, TStoreAsSetterStep(contents), node2) and + content = contents.getAStoreContent() + ) + } + } + + private import Steps + + module DataFlowInput implements DataFlow::InputSig { + final class Node = DataFlowNode; + + DataFlowCallable nodeGetEnclosingCallable(Node n) { + result = nodeGetEnclosingCallableAlias(n) + } + + class DataFlowCallable = DataFlowCallableAlias; + + class ArgumentPosition = FinalArgumentPosition; + + class ParameterPosition = FinalParameterPosition; + + class ParameterNode extends Node { + ParameterNode() { parameterNodeImpl(_, _, this) } + } + + predicate isParameterNode( + ParameterNode node, DataFlowCallable callable, ParameterPosition pos + ) { + parameterNodeImpl(callable.asSourceCallable(), pos, node) + } + + private predicate argumentNodeImplEx(DataFlowCall call, ArgumentPosition pos, Node node) { + argumentNodeImpl(call.asSourceCall(), pos, node) + or + exists(Node receiver, ContentSet contents, Node value, boolean isSetter | + call.isAccessorCall(receiver, contents, value, isSetter) + | + pos = MkReceiverArgumentPosition() and + node = receiver + or + isSetter = true and + pos = MkSetterArgumentPosition() and + node = value + ) + } + + class ArgumentNode extends Node { + ArgumentNode() { argumentNodeImplEx(_, _, this) } + } + + predicate isArgumentNode(ArgumentNode node, DataFlowCall call, ArgumentPosition pos) { + argumentNodeImplEx(call, pos, node) + } + + private newtype TReturnKind = + TValueReturn() or + TExceptionalReturn() + + class ReturnKind extends TReturnKind { + string toString() { + this = TValueReturn() and result = "value" + or + this = TExceptionalReturn() and result = "exception" + } + } + + additional Node getReturnNodeOfKind(Callable c, ReturnKind kind) { + result = TReturnNode(c) and kind = TValueReturn() + or + result = TExceptionalReturnNode(c) and kind = TExceptionalReturn() + } + + additional Node getOutNodeOfKind(DataFlowCall c, ReturnKind kind) { + result = TOutNode(c.asSourceCall()) and kind = TValueReturn() + or + result = TExceptionalOutNode(c.asSourceCall()) and kind = TExceptionalReturn() + or + c.isGetterCall(_, _, result) and kind = TValueReturn() + } + + class ReturnNode extends Node { + ReturnNode() { this = getReturnNodeOfKind(_, _) } + + ReturnKind getKind() { this = getReturnNodeOfKind(_, result) } + } + + class OutNode extends Node { + OutNode() { this = getOutNodeOfKind(_, _) } + + ReturnKind getKind() { this = getOutNodeOfKind(_, result) } + } + + OutNode getAnOutNode(DataFlowCall call, ReturnKind kind) { + result = getOutNodeOfKind(call, kind) + } + + class PostUpdateNode extends Node { + PostUpdateNode() { this = getPostUpdateNodeFinal(_) } + + Node getPreUpdateNode() { this = getPostUpdateNodeFinal(result) } + } + + private newtype TDataFlowCall = + MkSourceCall(Call c) or + MkAccessorCall(Node receiver, ContentSet contents, Node value, boolean isSetter) { + // TODO: restrict based on which contents are viable getter/setter to avoid too many calls + // TODO: add means for bypassing getter/setter calls for "raw" reads and writes + isSetter = false and + readStep(receiver, contents, value) and + (receiver instanceof TValueNode or receiver instanceof TVariableReadNode) + or + isSetter = true and + storeStep(value, contents, receiver) and + ( + receiver instanceof TPostUpdatedValueNode or + receiver instanceof TVariablePostUpdateNode + ) + } + + class DataFlowCall extends TDataFlowCall { + Call asSourceCall() { this = MkSourceCall(result) } + + predicate isAccessorCall( + Node receiver, ContentSet contents, Node value, boolean isSetter + ) { + this = MkAccessorCall(receiver, contents, value, isSetter) + } + + predicate isGetterCall(Node receiver, ContentSet contents, Node value) { + this = MkAccessorCall(receiver, contents, value, false) + } + + predicate isSetterCall(Node receiver, ContentSet contents, Node value) { + this = MkAccessorCall(receiver, contents, value, true) + } + + string toString() { + result = this.asSourceCall().toString() + or + exists(ContentSet contents | + this.isGetterCall(_, contents, _) and + result = "getter call (" + contents + ")" + or + this.isSetterCall(_, contents, _) and + result = "setter call (" + contents + ")" + ) + } + + Location getLocation() { + result = this.asSourceCall().getLocation() + or + exists(Node value | + this.isAccessorCall(_, _, value, _) and + result = value.getLocation() + ) + } + + DataFlowCallable getEnclosingCallable() { + result.asSourceCallable() = + getCallableFromCfgNode(getCfgNodeFromCall(this.asSourceCall())) + or + exists(Node receiver | + this.isAccessorCall(receiver, _, _, _) and + result = nodeGetEnclosingCallable(receiver) + ) + } + } + + // TODO: Instantiating DataFlowType is tricky since Callables and NamespaceObjects overlap, but we can't detect this + private newtype TDataFlowType = + TNamespaceType(NamespaceObject obj) or + TAnyType() + + class DataFlowType extends TDataFlowType { + NamespaceObject asNamespace() { this = TNamespaceType(result) } + + predicate isAny() { this = TAnyType() } + + string toString() { + result = this.asNamespace().toString() + or + this.isAny() and result = "any" + } + + Location getLocation() { result = this.asNamespace().getLocation() } + } + + predicate typeStrongerThan(DataFlowType t1, DataFlowType t2) { + NamespaceTracking::namespaceHasBaseObject+(t1.asNamespace(), t2.asNamespace()) + or + exists(t1) and t2.isAny() + } + + overlay[global] + pragma[inline] + private predicate compatibleTypesWithAny(DataFlowType t1, DataFlowType t2) { + t1 != TAnyType() and + t2 = TAnyType() + } + + private module DataFlowTypeDualInput implements DualGraphInputSig { + class Node = DataFlowType; + + predicate edge(Node node1, Node node2) { + NamespaceTracking::namespaceHasBaseObject(node1.asNamespace(), node2.asNamespace()) + } + } + + private module DataFlowTypeDual = MakeDualGraph; + + overlay[global] + pragma[inline] + predicate compatibleTypes(DataFlowType t1, DataFlowType t2) { + DataFlowTypeDual::hasCommonAncestor(t1, t2) // relation is symmetric, only check one direction + or + compatibleTypesWithAny(t1, t2) + or + compatibleTypesWithAny(t2, t1) + or + t1 = t2 + } + + private DataFlowType getNodeType1(Node node) { + node = CallGraph::trackNamespaceEx(result.asNamespace(), false) + or + node = NamespaceTracking::trackNamespace(result.asNamespace()) + } + + private DataFlowType getNodeType2(Node node) { + result = unique( | | getNodeType1(node)) + } + + DataFlowType getNodeType(Node node) { + result = getNodeType2(node) + or + not exists(getNodeType2(node)) and + result.isAny() + } + + class CastNode extends Node { + CastNode() { none() } // TODO + } + + predicate nodeIsHidden(Node node) { + not (node instanceof TValueNode or node instanceof TIncomingValueNode) + } + + class DataFlowExpr = AstNode; + + Node exprNode(DataFlowExpr e) { result = TValueNode(e) } + + private newtype TStoreKind = + TNormalStoreKind() or + TGetterStoreKind() or + TSetterStoreKind() + + private class StoreKind extends TStoreKind { + predicate isNormalStoreKind() { this = TNormalStoreKind() } + + predicate isGetterStoreKind() { this = TGetterStoreKind() } + + predicate isSetterStoreKind() { this = TSetterStoreKind() } + + string toString() { + this = TNormalStoreKind() and result = "normal" + or + this = TGetterStoreKind() and result = "getter" + or + this = TSetterStoreKind() and result = "setter" + } + } + + private module NamespaceTracking { + pragma[nomagic] + Node trackNamespace(NamespaceObject ns) { + result.isNamespaceObject(ns) + or + valueStepEx(trackNamespace(ns), result) + } + + private Node trackCallableSimple(Callable method) { + isMethod(method) and + result = TClosureExprNode(method, _, _) + or + simpleValueStep(trackCallableSimple(method), result) + } + + private predicate storeLikeStep(Node node1, Node node2) { + exists(StepBase step | dataflowStepFinal(node1, step, node2) | + step instanceof TStoreStep or + step instanceof TStoreAsGetterStep or + step instanceof TStoreAsSetterStep + ) + } + + private Node methodAssumedReceiver(Callable method) { + exists(Node target | + isMethod(method) and + storeLikeStep(trackCallableSimple(method), target) and + result = [target, getPreUpdateNodeFinal(target)] + ) + } + + predicate assumedReceiverStep(Node node1, Node node2) { + exists(Callable method | + node1 = methodAssumedReceiver(method) and + node2 = TReceiverParameterNode(method) + or + exists(ClassLikeObject cls | + isInstanceInitializer(method, cls) and + node1.isNamespaceObject(cls.getInstancePrototype()) and + node2 = TReceiverParameterNode(method) + ) + ) + } + + pragma[nomagic] + private predicate valueStep(Node node1, Node node2) { + simpleValueStep(node1, node2) + or + // Like in the variable-capture stage, use an over-approximation of capture flow + approxCaptureStep(node1, node2) + or + assumedReceiverStep(node1, node2) + or + node2 = getPostUpdateNodeFinal(node1) + } + + pragma[nomagic] + private predicate storeIntoNamespace( + Node node1, Content content, NamespaceObject node2 + ) { + storeContentNoCapture(node1, content, trackNamespace(node2)) + } + + pragma[nomagic] + predicate storeGetterIntoNamespace( + Callable getter, Content content, NamespaceObject node2 + ) { + storeGetter(trackCallableSimple(getter), content, trackNamespace(node2)) + } + + pragma[nomagic] + predicate storeSetterIntoNamespace( + Callable setter, Content content, NamespaceObject node2 + ) { + storeSetter(trackCallableSimple(setter), content, trackNamespace(node2)) + } + + pragma[nomagic] + Content getSimpleContentFromContentSet(ContentSet contents) { + result = contents.asSingleton() + or + exists(IndexedContainerKind kind | + result.asContainerSlot(kind) = contents.asContainerSlot(kind) + ) + } + + pragma[nomagic] + predicate readContentStep(Node node1, Content content, Node node2) { + exists(ContentSet contents | DataFlowInput::readStep(node1, contents, node2) | + content = getSimpleContentFromContentSet(contents) + ) + } + + pragma[nomagic] + Node getSetterParameter(Callable setter) { + readContentStep(TParameterObjectNode(setter), setterParameter(), result) + } + + pragma[nomagic] + private predicate readFromNamespace(NamespaceObject node1, Content content, Node node2) { + readContentStep(trackNamespace(node1), content, node2) + } + + pragma[nomagic] + predicate derivedStep(Node node1, Node node2) { + exists(NamespaceObject ns, Content content | + namespaceHasOwnContent(ns, content, node1) and + readFromNamespace(ns, content, node2) + or + exists(Callable accessor | + readFromNamespace(ns, content, node2) and + namespaceHasOwnGetter(ns, content, accessor) and + node1 = TReturnNode(accessor) + or + storeIntoNamespace(node1, content, ns) and + namespaceHasOwnSetter(ns, content, accessor) and + node2 = getSetterParameter(accessor) + ) + ) + } + + pragma[inline] + predicate valueStepEx(Node node1, Node node2) { + valueStep(node1, node2) + or + derivedStep(node1, node2) + } + + pragma[nomagic] + predicate hasOwnContent(NamespaceObject ns, Content content) { + storeIntoNamespace(_, content, ns) + } + + pragma[nomagic] + predicate storeBaseStep(Node parentObj, Node childObj) { + dataflowStepFinal(parentObj, TStoreBaseObject(), childObj) + } + + pragma[nomagic] + predicate namespaceHasBaseValue(NamespaceObject namespace, Node baseObj) { + storeBaseStep(baseObj, trackNamespace(namespace)) + } + + pragma[nomagic] + predicate namespaceHasBaseObject(NamespaceObject namespace, NamespaceObject baseObj) { + namespaceHasBaseValue(namespace, trackNamespace(baseObj)) + } + + pragma[nomagic] + predicate withoutContentStepToNamespace( + Node node, ContentSet contents, NamespaceObject target + ) { + dataflowStep3(node, TWithoutContentStep(contents), TNamespaceNode(target)) + } + + pragma[nomagic] + predicate withoutContentStepToNamespace2( + NamespaceObject ns, ContentSet contents, NamespaceObject target + ) { + withoutContentStepToNamespace(trackNamespace(ns), contents, target) + } + + pragma[nomagic] + predicate withContentStepToNamespace( + Node node, ContentSet contents, NamespaceObject target + ) { + dataflowStep3(node, TWithContentStep(contents), TNamespaceNode(target)) + } + + pragma[nomagic] + predicate withContentStepToNamespace2( + NamespaceObject ns, ContentSet contents, NamespaceObject target + ) { + withContentStepToNamespace(trackNamespace(ns), contents, target) + } + + predicate namespaceHasOwnContentOrAccessor(NamespaceObject ns, Content content) { + namespaceHasOwnContent(ns, content, _) + or + namespaceHasOwnGetter(ns, content, _) + or + namespaceHasOwnSetter(ns, content, _) + } + + predicate namespaceHasOwnContent(NamespaceObject ns, Content content, Node value) { + storeIntoNamespace(value, content, ns) + or + exists(NamespaceObject other, ContentSet contents | + withoutContentStepToNamespace2(other, contents, ns) and + namespaceHasOwnContent(other, content, value) and + not contents.getAReadContent() = content + ) + or + exists(NamespaceObject other, ContentSet contents | + withContentStepToNamespace2(other, contents, ns) and + namespaceHasOwnContent(other, content, value) and + contents.getAReadContent() = content + ) + } + + predicate namespaceHasContent(NamespaceObject ns, Content content, Node value) { + namespaceHasOwnContent(ns, content, value) + or + not namespaceHasOwnContentOrAccessor(ns, content) and + exists(NamespaceObject base | + namespaceHasBaseObject(ns, base) and + namespaceHasContent(base, content, value) + ) + } + + predicate namespaceHasOwnGetter(NamespaceObject ns, Content content, Callable getter) { + storeGetterIntoNamespace(getter, content, ns) + } + + predicate namespaceHasGetter(NamespaceObject ns, Content content, Callable getter) { + namespaceHasOwnGetter(ns, content, getter) + or + not namespaceHasOwnContentOrAccessor(ns, content) and + exists(NamespaceObject base | + namespaceHasBaseObject(ns, base) and + namespaceHasGetter(base, content, getter) + ) + } + + predicate namespaceHasOwnSetter(NamespaceObject ns, Content content, Callable setter) { + storeSetterIntoNamespace(setter, content, ns) + } + + predicate namespaceHasSetter(NamespaceObject ns, Content content, Callable setter) { + namespaceHasOwnSetter(ns, content, setter) + or + not namespaceHasOwnContentOrAccessor(ns, content) and + exists(NamespaceObject base | + namespaceHasBaseObject(ns, base) and + namespaceHasSetter(base, content, setter) + ) + } + + /** + * Holds if `node1 -> node2` is a value step propagating a value (usually a method) + * from a base class to a subclass. + * + * For example, the method `bar` is propagated downwards from `Base` to the `this.bar` expression. + * ```js + * class Base { + * bar() { ... } + * } + * class Sub extends Base { + * foo() { this.bar() } + * } + * ``` + */ + predicate downwardsInheritedContentStep(Node node1, Node node2) { + exists(NamespaceObject ns, Content content | + // simplify debugging by not overlapping with valueStepEx + not namespaceHasOwnContentOrAccessor(ns, content) + | + namespaceHasContent(ns, content, node1) and + readFromNamespace(ns, content, node2) + or + exists(Callable accessor | + namespaceHasGetter(ns, content, accessor) and + readFromNamespace(ns, content, node2) and + node1 = TReturnNode(accessor) + or + namespaceHasSetter(ns, content, accessor) and + storeIntoNamespace(node1, content, ns) and + node2 = getSetterParameter(accessor) + ) + ) + } + + /** + * Gets a node that refers to a receiver in a method of `ns`, meaning it + * may also refer to an object that inherits from `ns`. + */ + Node trackNamespaceAsAssumedReceiver(NamespaceObject ns) { + assumedReceiverStep(NamespaceTracking::trackNamespace(ns), result) + or + // note: do not use 'valueStepEx' here as we don't want to follow store/read steps + valueStep(trackNamespaceAsAssumedReceiver(ns), result) + } + + predicate localReceiverReadStep(NamespaceObject ns, Content content, Node value) { + readContentStep(trackNamespaceAsAssumedReceiver(ns), content, value) + } + + predicate localReceiverStoreStep(NamespaceObject ns, Content content, Node value) { + storeContentNoCapture(value, content, trackNamespaceAsAssumedReceiver(ns)) + } + + /** + * Holds if `node1 -> node2` is a value step propagating a value (usually a method) + * from a subclass to a base class. + * + * For example, the method `bar` is propagated upwards from `Sub` to the `this.bar` expression. + * ```js + * class Base { + * foo() { this.bar() } + * } + * class Sub extends Base { + * bar() { ... } + * } + * ``` + */ + predicate upwardsInheritedContentStep(Node node1, Node node2) { + exists(NamespaceObject child, NamespaceObject ns, Content content | + localReceiverReadStep(ns, content, node2) and + namespaceHasBaseObject+(child, ns) + | + namespaceHasOwnContent(child, content, node1) + or + exists(Callable getter | + namespaceHasOwnGetter(child, content, getter) and + node1 = TReturnNode(getter) + ) + ) + } + + predicate localContentStep(Node node1, Node node2) { + exists(NamespaceObject ns, Content content | + localReceiverStoreStep(ns, content, node1) and + localReceiverReadStep(ns, content, node2) + ) + } + } + + private module CallGraph { + private newtype TCallableTrackingState = + TNeutral() or + TOut() or + TSimpleCall() or + TNonSimpleCall() + + class CallableTrackingState extends TCallableTrackingState { + /** Holds if the value has not been tracked into or out of any calls. */ + predicate isNeutral() { this = TNeutral() } + + /** Holds if the value has been tracked out of a call, but not into a call. */ + predicate isOut() { this = TOut() } + + /** + * Holds if the value has been tracked into a call, not followed by any namespace reads, + * and not preceeded by any out steps. + * + * If a call site is reached in this state, we'll assume it acts as a callback-style call. + * To avoid spurious flows, we treat suchs callbacks as form of return edge. + */ + predicate isSimpleCall() { this = TSimpleCall() } + + /** + * Holds if the value has been tracked into a call, followed by a namespace reads and/or + * preceeded by out steps. + * + * If a call site is reached in this state, we'll generate an ordinary call edge for it. + */ + predicate isNonSimpleCall() { this = TNonSimpleCall() } + + pragma[nomagic] + CallableTrackingState appendCall() { + this.isNeutral() and result.isSimpleCall() + or + this.isOut() and result.isNonSimpleCall() + or + this.isSimpleCall() and result = this + or + this.isNonSimpleCall() and result = this + } + + pragma[nomagic] + CallableTrackingState appendNamespaceRead() { + this.isNeutral() and result = this + or + this.isOut() and result = this + or + this.isSimpleCall() and result.isNonSimpleCall() + or + this.isNonSimpleCall() and result = this + } + + pragma[nomagic] + CallableTrackingState appendOut() { + this.isNeutral() and result.isOut() + or + this.isOut() and result = this + } + + string toString() { + this.isNeutral() and result = "neutral" + or + this.isSimpleCall() and result = "simple call" + or + this.isNonSimpleCall() and result = "non-simple call" + or + this.isOut() and result = "out" + } + } + + private Node trackCallable(DataFlowCallable callable, CallableTrackingState state) { + state.isNeutral() and + result = callableInitialNode(callable.asSourceCallable()) + or + valueStepEx(trackCallable(callable, state), result) + or + instanceMemberContentStepIn(trackCallable(callable, _), result) and + state.isNonSimpleCall() + or + exists(CallableTrackingState prev | + valueStepIn(trackCallable(callable, prev), result) and + state = prev.appendCall() + or + instanceMemberContentStep(trackCallable(callable, prev), result) and + state = prev.appendNamespaceRead() + or + valueStepOut(trackCallable(callable, prev), result) and + state = prev.appendOut() + ) + } + + pragma[nomagic] + private predicate instantiateStep(Node node1, Node node2) { + dataflowStepFinal(node1, TInstantiateStep(), node2) + } + + Node trackNamespaceEx(NamespaceObject ns, boolean hasCall) { + hasCall = false and + result.isNamespaceObject(ns) + or + hasCall = false and + exists(ClassLikeObject cls | + ns = cls.getInstancePrototype() and + instantiateStep(trackNamespaceEx(cls, hasCall), result) + ) + or + valueStepEx(trackNamespaceEx(ns, hasCall), result) + or + valueStepIn(trackNamespaceEx(ns, _), result) and + hasCall = true + or + valueStepOut(trackNamespaceEx(ns, false), result) and + hasCall = false + or + instanceMemberContentStep(trackNamespaceEx(ns, hasCall), result) + } + + /** + * Holds if `origin` is a store target whose aliases won't be found by namespace-tracking, + * since no namespace flows there, e.g. because its allocation site is outside the database. + */ + private predicate shouldTrackAdHocStoreTarget(Node origin) { + storeContentNoCapture(trackCallable(_, _), _, origin) and + not origin instanceof TArgumentObjectNode and + not origin = NamespaceTracking::trackNamespace(_) + } + + Node trackAdHocStoreTarget(Node origin, boolean hasCall) { + shouldTrackAdHocStoreTarget(origin) and + result = origin and + hasCall = false + or + valueStepEx(trackAdHocStoreTarget(origin, hasCall), result) + or + instanceMemberContentStep(trackAdHocStoreTarget(origin, hasCall), result) + or + valueStepIn(trackAdHocStoreTarget(origin, _), result) and + hasCall = true + or + valueStepOut(trackAdHocStoreTarget(origin, false), result) and + hasCall = false + } + + pragma[nomagic] + private predicate adHocDerivedStep(Node node1, Node node2, boolean call) { + exists(Content content, Node object | + storeContentNoCapture(node1, content, object) and + NamespaceTracking::readContentStep(trackAdHocStoreTarget(object, call), content, + node2) + ) + } + + pragma[inline] + private predicate valueStepEx(Node node1, Node node2) { + NamespaceTracking::valueStepEx(node1, node2) + or + callSiteFlowThrough(node1, node2) + or + NamespaceTracking::downwardsInheritedContentStep(node1, node2) + or + NamespaceTracking::upwardsInheritedContentStep(node1, node2) + or + NamespaceTracking::localContentStep(node1, node2) + or + copyStep(node1, node2) and + // With/without content steps targeting a namespace node have already been + // handled precisely in the previous stage. + // Propagating through the resulting copy steps without respecting clears/expects contents + // would negate the precise handling they received in the previous stage. + not node2 instanceof TNamespaceNode + or + adHocDerivedStep(node1, node2, false) + } + + private predicate isMethodReceiverRef(Node n) { + exists(Callable callable | + isMethod(callable) and + n = TParameterObjectNode(callable) + ) + or + exists(Node prev | + isMethodReceiverRef(prev) and + dataflowStepFinal(prev, TValueStep(), n) + ) + } + + /** + * Holds if `node1 -> node2` is a step from an instance member definition + * to one of its uses. The instance object has not been passed into a call before the read. + */ + predicate instanceMemberContentStep(Node node1, Node node2) { + exists(NamespaceObject ns, Content content | + NamespaceTracking::namespaceHasContent(ns, content, node1) and + NamespaceTracking::readContentStep(trackNamespaceEx(ns, false), content, node2) + ) + } + + /** + * Holds if `node1 -> node2` is a step from an instance member definition + * to one of its uses. The instance object has been passed into a call before the read. + */ + predicate instanceMemberContentStepIn(Node node1, Node node2) { + exists(NamespaceObject ns, Content content | + NamespaceTracking::namespaceHasContent(ns, content, node1) and + NamespaceTracking::readContentStep(trackNamespaceEx(ns, true), content, node2) + ) + or + adHocDerivedStep(node1, node2, true) + } + + private Node trackParameter(DataFlowCallable callable, ParameterPosition pos) { + parameterNodeImpl(callable.asSourceCallable(), pos, result) + or + exists(Node prev | + prev = trackParameter(callable, pos) and + simpleLocalFlowStep(prev, result, _) + ) + } + + private predicate parameterFlowsToReturn( + DataFlowCallable callable, ParameterPosition pos, ReturnKind kind + ) { + getReturnNodeOfKind(callable.asSourceCallable(), kind) = + trackParameter(callable, pos) + } + + private predicate argumentFlowsToReturn( + DataFlowCallable callable, ArgumentPosition apos, ReturnKind kind + ) { + exists(ParameterPosition ppos | + parameterFlowsToReturn(callable, ppos, kind) and + parameterMatch(ppos, apos) + ) + } + + predicate callSiteFlowThrough(Node node1, Node node2) { + exists( + DataFlowCall call, DataFlowCallable callable, ArgumentPosition apos, + ReturnKind kind + | + callable = viableCallable(call) and + isArgumentNode(node1, call, apos) and + argumentFlowsToReturn(callable, apos, kind) and + node2 = getOutNodeOfKind(call, kind) + ) + or + exists( + DataFlowCall call, DataFlowCallable callable, DataFlowCallable callback, + ArgumentPosition callbackPos, ArgumentPosition valuePos1, + ParameterPosition valuePos2 + | + callable = viableCallable(call) and + callbackFlowsToArgument(call, callback, callbackPos) and + parameterFlowsToCallbackArgEx(callable, callbackPos, valuePos1, valuePos2) and + isArgumentNode(node1, call, valuePos1) and + isParameterNode(node2, callback, valuePos2) + ) + } + + private predicate callbackFlowsToArgument( + DataFlowCall call, DataFlowCallable callback, ArgumentPosition pos + ) { + exists(CallableTrackingState state | + isArgumentNode(trackCallable(callback, state), call, pos) and + (state.isNeutral() or state.isSimpleCall()) + ) + } + + pragma[nomagic] + private predicate parameterFlowsToCallTarget( + DataFlowCallable callable, ParameterPosition pos, Call call + ) { + trackParameter(callable, pos) = TCallTargetNode(call) + } + + /** + * Holds if `callable` invokes the given `callback`, passing its own parameter at `valuePos1` + * into the callback at `valuePos2`. + */ + pragma[nomagic] + private predicate parameterFlowsToCallbackArg( + DataFlowCallable callable, ParameterPosition callback, ParameterPosition valuePos1, + ArgumentPosition valuePos2 + ) { + exists(Call call | + parameterFlowsToCallTarget(callable, callback, call) and + argumentNodeImpl(call, valuePos2, trackParameter(callable, valuePos1)) and + not valuePos2 = MkFunctionSelfReferenceArgumentPosition() + ) + } + + /** + * Like `parameterFlowsToCallbackArg` but all arg/param positions have been flipped by `parameterMatch`. + */ + pragma[nomagic] + private predicate parameterFlowsToCallbackArgEx( + DataFlowCallable callable, ArgumentPosition callback, ArgumentPosition valuePos1, + ParameterPosition valuePos2 + ) { + parameterFlowsToCallbackArg(callable, argToParamPos(callback), + argToParamPos(valuePos1), paramToArgPos(valuePos2)) + } + + pragma[nomagic] + private predicate parameterNodeRestricted( + Node node, DataFlowCallable callable, ParameterPosition pos + ) { + isParameterNode(node, callable, pos) and + not ( + ( + isMethod(callable.asSourceCallable()) or + isInstanceInitializer(callable.asSourceCallable(), _) + ) and + pos = MkReceiverParameterPosition() + or + pos = MkFunctionSelfReferenceParameterPosition() + ) + } + + /** + * Holds if `node1 -> node2` is a step into a call. Flow into method receivers is blocked. + */ + pragma[inline] + private predicate valueStepIn(Node node1, Node node2) { + exists( + DataFlowCall call, DataFlowCallable callable, ArgumentPosition apos, + ParameterPosition ppos + | + callable = viableCallableNotCallback(call) and + isArgumentNode(node1, call, apos) and + parameterNodeRestricted(node2, callable, ppos) and + parameterMatch(ppos, apos) + ) + or + instanceMemberContentStepIn(node1, node2) + } + + /** + * Holds if `node1 -> node2` is a step out of a call. + */ + pragma[inline] + private predicate valueStepOut(Node node1, Node node2) { + exists(DataFlowCall call, DataFlowCallable callable, ReturnKind kind | + callable = viableCallable(call) and + node1 = getReturnNodeOfKind(callable.asSourceCallable(), kind) and + node2 = getOutNodeOfKind(call, kind) + ) + or + exists( + DataFlowCall call, DataFlowCallable callable, ArgumentPosition apos, + ParameterPosition ppos + | + // Parameter-passing into a callback is treated as an out-step + callable = viableCallableAsCallback(call) and + isArgumentNode(node1, call, apos) and + parameterNodeRestricted(node2, callable, ppos) and + parameterMatch(ppos, apos) + ) + } + + private predicate accessorContentCall( + DataFlowCall call, Node receiver, Content content, boolean isSetterCall + ) { + exists(ContentSet contents | + call.isAccessorCall(receiver, contents, _, isSetterCall) and + content = NamespaceTracking::getSimpleContentFromContentSet(contents) + ) + } + + DataFlowCallable viableCallableNotCallback(DataFlowCall c) { + exists(CallableTrackingState state | + trackCallable(result, state) = TCallTargetNode(c.asSourceCall()) and + not state.isSimpleCall() + ) + or + exists(Content content, NamespaceObject ns, boolean isSetterCall | + accessorContentCall(c, trackNamespaceEx(ns, _), content, isSetterCall) + | + isSetterCall = false and + NamespaceTracking::namespaceHasGetter(ns, content, result.asSourceCallable()) + or + isSetterCall = true and + NamespaceTracking::namespaceHasSetter(ns, content, result.asSourceCallable()) + ) + } + + DataFlowCallable viableCallableAsCallback(DataFlowCall c) { + exists(CallableTrackingState state | + trackCallable(result, state) = TCallTargetNode(c.asSourceCall()) and + state.isSimpleCall() + ) + } + + DataFlowCallable viableCallable(DataFlowCall c) { + result = viableCallableAsCallback(c) + or + result = viableCallableNotCallback(c) + } + + DataFlowCallable getCallTargetFromSourceCall(Call c) { + result = viableCallable(MkSourceCall(c)) + } + } + + predicate viableCallable = CallGraph::viableCallable/1; + + class Content = FinalContent; + + predicate forceHighPrecision(Content c) { none() } + + class ContentSet = FinalContentSet; + + class ContentApprox = Unit; // TODO: approx + + ContentApprox getContentApprox(Content c) { exists(c) and exists(result) } + + predicate parameterMatch(ParameterPosition param, ArgumentPosition arg) { + arg.asContentSet().getAStoreContent() = param.asContentSet().getAReadContent() + or + // Pass static->dynamic and dynamic->static argument/parameter object + arg = MkStaticArgumentObjectPosition() and + param = MkDynamicParameterObjectPosition() + or + arg = MkDynamicArgumentObjectPosition() and + param = MkStaticParameterObjectPosition() + or + arg = MkFunctionSelfReferenceArgumentPosition() and + param = MkFunctionSelfReferenceParameterPosition() + or + arg = MkReceiverArgumentPosition() and param = MkReceiverParameterPosition() + or + arg = MkSetterArgumentPosition() and + param.asContentSet().getAReadContent() = setterParameter() + } + + private ArgumentPosition paramToArgPos(ParameterPosition pos) { + parameterMatch(pos, result) + } + + private ParameterPosition argToParamPos(ArgumentPosition pos) { + parameterMatch(result, pos) + } + + additional predicate localFlowStep(Node node1, Node node2) { + simpleValueStep(node1, node2) + or + copyStep(node1, node2) + } + + predicate simpleLocalFlowStep(Node node1, Node node2, string model) { + localFlowStep(node1, node2) and + sameContainer(node1, node2) and + model = "" + } + + predicate readStep = Steps::readStep/3; + + predicate storeStep = Steps::storeStep/3; + + predicate clearsContent(Node node, ContentSet contents) { + node = TWithoutContentHelper(_, contents) + or + exists(CaptureSsa::ClosureNode closureNode, LocalVariable v | + CaptureSsa::clearsContent(closureNode, v) and + node = getNodeFromCaptureSsa(closureNode) and + contents.asSingleton().asCapturedVariable() = v + ) + } + + predicate expectsContent(Node node, ContentSet contents) { + node = TWithContentHelper(_, contents) + } + + bindingset[node1, node2] + pragma[inline_late] + additional predicate sameContainer(Node node1, Node node2) { + nodeGetEnclosingCallable(node1) = nodeGetEnclosingCallable(node2) + } + + predicate jumpStep(Node node1, Node node2) { + localFlowStep(node1, node2) and + not sameContainer(node1, node2) + } + + class NodeRegion extends Void { + predicate contains(Node n) { none() } + } + + predicate isUnreachableInCall(NodeRegion nr, DataFlowCall call) { none() } + + predicate allowParameterReturnInSelf(ParameterNode p) { + exists(Callable callable | + p = TCallableSelfReferenceNode(callable) and + CaptureSsa::heuristicAllowInstanceParameterReturnInSelf(callable) + ) + } + + predicate localMustFlowStep(Node node1, Node node2) { + none() // TODO + } + + class LambdaCallKind = Void; + + /** Holds if `creation` is an expression that creates a lambda of kind `kind` for `c`. */ + predicate lambdaCreation(Node creation, LambdaCallKind kind, DataFlowCallable c) { + none() // TODO + } + + /** Holds if `call` is a lambda call of kind `kind` where `receiver` is the lambda expression. */ + predicate lambdaCall(DataFlowCall call, LambdaCallKind kind, Node receiver) { + none() // TODO + } + + /** Extra data-flow steps needed for lambda flow analysis. */ + predicate additionalLambdaFlowStep(Node nodeFrom, Node nodeTo, boolean preservesValue) { + none() // TODO + } + + predicate knownSourceModel(Node source, string model) { + none() // TODO + } + + predicate knownSinkModel(Node sink, string model) { + none() // TODO + } + + class DataFlowSecondLevelScope = Void; + + additional module CallGraphOutput { + predicate viableCallable = CallGraph::viableCallable/1; + } + } + + private module DataFlowMake = DataFlow::DataFlowMake; + + module DataFlowPublic { + import DataFlowMake + + class Node = DataFlowNode; + } + + module TaintTrackingInput implements TaintTracking::InputSig { + predicate defaultTaintSanitizer(Node node) { none() } + + predicate defaultAdditionalTaintStep(Node src, Node sink, string model) { + dataflowStepFinal(src, TTaintStep(), sink) and model = "" + } + + bindingset[node] + predicate defaultImplicitTaintRead(Node node, ContentSet c) { none() } + + predicate speculativeTaintStep(Node src, Node sink) { none() } + } + + module TaintTrackingPublic = + TaintTracking::TaintFlowMake; + + private import codeql.dataflow.internal.DataFlowImplConsistency as DataFlowImplConsistency + + private module ConsistencyConfig implements + DataFlowImplConsistency::InputSig + { + predicate argHasPostUpdateExclude(DataFlowInput::ArgumentNode n) { + n instanceof TArgumentObjectNode or + n instanceof TDynamicArgumentObjectNode or + n instanceof TTransformStepNode + } + } + + module Consistency = + DataFlowImplConsistency::MakeConsistency; + } + } + } + } +} diff --git a/shared/unified/qlpack.yml b/shared/unified/qlpack.yml new file mode 100644 index 000000000000..3f8f55e3ace5 --- /dev/null +++ b/shared/unified/qlpack.yml @@ -0,0 +1,11 @@ +name: codeql/unified +version: 0.0.1-dev +groups: shared +library: true +dependencies: + codeql/controlflow: ${workspace} + codeql/dataflow: ${workspace} + codeql/ssa: ${workspace} + codeql/typetracking: ${workspace} + codeql/util: ${workspace} +warnOnImplicitThis: true diff --git a/shared/util/codeql/util/DualGraph.qll b/shared/util/codeql/util/DualGraph.qll new file mode 100644 index 000000000000..faeb1e498be4 --- /dev/null +++ b/shared/util/codeql/util/DualGraph.qll @@ -0,0 +1,92 @@ +/** + * Provides an efficient mechanism for checking if two nodes have + * a common ancestor in a graph. + */ + +private import Location + +signature module DualGraphInputSig { + class Node { + string toString(); + + Location getLocation(); + } + + predicate edge(Node node1, Node node2); +} + +/** + * Creates a "dual graph" in which each node in the given graph has a "forward" and "backward" + * copy. + * + * All original edges are present in both copies, but reversed in the backward copy. + * + * In addition, all nodes have an edge from their backward node to their forward node. + * + * This can be used to check if two nodes have a common ancestor in the graph, by checking + * if a path exists from the reverse node of one node, to the forward node of another. + */ +module MakeDualGraph Input> { + private import Input + + private newtype TDualNode = + TForward(Node n) or + TBackward(Node n) + + /** A forward or backward copy of a node from the original graph. */ + class DualNode extends TDualNode { + /** Gets the underlying node if this is a forward node. */ + Node asForward() { this = TForward(result) } + + /** Gets the underlying node if this is a backward node. */ + Node asBackward() { this = TBackward(result) } + + /** Gets a string representation of this node. */ + string toString() { + result = this.asForward().toString() + or + result = "[rev] " + this.asBackward().toString() + } + + /** Gets the location of this node. */ + Location getLocation() { + result = this.asForward().getLocation() + or + result = this.asBackward().getLocation() + } + } + + /** Gets the node representing the backward node wrapping `n`. */ + DualNode getBackwardNode(Node n) { result.asBackward() = n } + + /** Gets the node representing the forward node wrapping `n`. */ + DualNode getForwardNode(Node n) { result.asForward() = n } + + /** + * Holds if the dual graph contains the edge `node1 -> node2`. See `MakeDualGraph`. + */ + predicate dualEdge(DualNode node1, DualNode node2) { + edge(node1.asForward(), node2.asForward()) + or + edge(node2.asBackward(), node1.asBackward()) + or + node1.asBackward() = node2.asForward() + } + + /** + * Holds if there is a non-empty path from `node1 -> node2` in the dual graph. + */ + cached + predicate dualPath(DualNode node1, DualNode node2) = fastTC(dualEdge/2)(node1, node2) + + /** + * Holds if `node1` and `node2` have a common ancestor in the original graph, that is, + * there exists a node from which both nodes are reachable. + */ + pragma[inline] + predicate hasCommonAncestor(Node node1, Node node2) { + // Note: `fastTC` only checks for non-empty paths, but there is no need to special-case + // `node1 = node2` because the path `Backward(n) -> Forward(n)` is non-empty. + dualPath(getBackwardNode(node1), getForwardNode(node2)) + } +}