Compare commits

..

19 Commits

Author SHA1 Message Date
Copilot
e51acdc541 Python: add new shared-SSA-backed SSA adapter
Preparatory refactor for the shared-CFG dataflow migration. Adds the
new Python SSA adapter additively, without changing any production
behaviour.

Library additions:

- semmle.python.dataflow.new.internal.SsaImpl — Python SSA
  implementation built on the new (shared) CFG. Mirrors the Java SSA
  adapter (java/ql/lib/semmle/code/java/dataflow/internal/SsaImpl.qll):
  an InputSig is defined in terms of positional (BasicBlock, int)
  variable references, and the shared
  codeql.ssa.Ssa::Make<Location, Cfg, Input> module is then
  instantiated.

  SourceVariable is the AST-level Py::Variable. Variable references
  are looked up via the new CFG facade's NameNode.defines/uses/deletes
  predicates (added in the preceding PR), which themselves are
  one-line bridges to AST-level Name.defines/uses/deletes.

  Implicit-entry definitions are inserted for non-local/global/builtin
  reads, captured variables, and (when needed) parameters.

Test additions:

- library-tests/dataflow-new-ssa/ — exercises the new SSA over a
  representative test corpus and checks expected def/use chains.

- library-tests/dataflow-new-ssa-vs-legacy/ — runs both new SSA and
  legacy ESSA over the same corpus and diffs the results, so any
  semantic divergence shows up as a test failure.

Production impact:

None. The new SSA adapter has zero callers in lib/ and src/ — the
legacy ESSA SSA (semmle/python/essa/*) remains the default. The
dataflow library is not migrated yet; that lands in a follow-up PR.

Verified by:
- All 367 lib + src + consistency-queries compile clean.
- All 641 ControlFlow + PointsTo + dataflow + essa + consistency
  library-tests pass.
- Both new dataflow-new-ssa[/vs-legacy] test packs pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 08:40:16 +00:00
yoff
c95c3fe638 Python: model exception edges for raise-prone expressions inside try/with
The new CFG previously only emitted exception edges for explicit `raise`
and `assert` statements. As a result, code that became reachable only
via the exception path of an arbitrary expression (e.g., the body of an
`except` handler following a try-body whose `call()` could raise) was
classified as dead, breaking analyses like StackTraceExposure,
FileNotAlwaysClosed, ExceptionInfo, UseOfExit, and CatchingBaseException.

This commit adds a `mayThrow` predicate over expressions that are known
sources of implicit exceptions in Python (calls, attribute access,
subscripts, arithmetic/comparison operators, imports, await/yield/yield
from) plus `from m import *` at the statement level, and routes them
through the shared CFG's `beginAbruptCompletion(_, _, ExceptionSuccessor,
always=false)` hook.

The set of exception sources is restricted to nodes that are
syntactically inside a `try`/`with` statement in the same scope.
This mirrors Java's `ControlFlowGraph::mayThrow`, which only emits
exception edges where local handling can observe them — outside such
contexts, the edges add CFG complexity (weakening BarrierGuard
precision and breaking SSA continuity around augmented assignments and
subscript stores) without analysis benefit, since exceptions just
propagate to the function exit anyway.

Net effect on the test suite: ~100 alerts restored across the exception-
related query tests (StackTraceExposure +29, ExceptionInfo +17,
FileNotAlwaysClosed +52, UseOfExit +1, CatchingBaseException restored)
with no precision regressions. Affected `.expected` files and the
regression-guard `dead_under_no_raise.py` are updated accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 08:15:45 +00:00
yoff
6c03194092 Python: add new shared-CFG-backed control flow graph facade (Cfg)
Adds the public facade on top of the AstNodeImpl adapter from the
previous commit. Re-exposes the same API surface as
semmle/python/Flow.qll (ControlFlowNode, CallNode, BasicBlock,
NameNode, DefinitionNode, CompareNode, ...), backed by the shared
codeql.controlflow.ControlFlowGraph library.

- semmle.python.controlflow.internal.Cfg — public facade.
- ControlFlow/store-load/* — basic store/load coverage via the facade.

The new CFG library is added additively: it has zero callers in lib/
and src/, and the legacy CFG in semmle/python/Flow.qll remains the
default. Dataflow, SSA, and production query migration land in
follow-up PRs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 08:15:45 +00:00
yoff
39e6bfc894 Python: add shared-CFG AstSig adapter (AstNodeImpl)
Preparatory refactor for the shared-CFG dataflow migration. Adds the
adapter that mediates between the Python AST and the shared
codeql.controlflow.ControlFlowGraph signature, plus the test suites
that validate the new CFG directly against this adapter. The public
facade is added in the following commit.

Library additions:

- semmle.python.controlflow.internal.AstNodeImpl — wraps Python's
  Stmt/Expr/Scope/Pattern and adds two synthetic kinds of node
  (BlockStmt for body slots, intermediate nodes for multi-operand
  boolean expressions) to satisfy the shared CFG signature.

- lib/printCfgNew.ql — debug/visualisation query for the new CFG.

- consistency-queries/CfgConsistency.ql — consistency query running
  the shared CFG's standard checks against Python.

Test additions (all driven directly off AstNodeImpl):

- ControlFlow/bindings/* — annotation-driven SSA-binding tests
  (annassign, compound, comprehension, decorated, except_handler,
  imports, match_pattern, parameters, simple, type_params,
  walrus_starred, with_stmt, dead_under_no_raise).

- ControlFlow/evaluation-order/NewCfg*.ql — mirrors of the existing
  OldCfg evaluation-order self-validation suite, run against the
  new CFG via NewCfgImpl.qll.

- Minor extensions to existing test_if.py / test_boolean.py +
  cosmetic .expected churn on a handful of OldCfg tests.

No dataflow, SSA, or production query is migrated yet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 08:15:33 +00:00
yoff
53cae687f7 Merge pull request #21931 from github/yoff/python-shared-cfg-loop-else
Shared CFG: add defaulted getWhileElse/getForeachElse to AstSig
2026-06-23 14:25:16 +02:00
Anders Schack-Mulligen
cfbf4a3927 Merge pull request #22037 from github/copilot/update-csharp-extractor-catch-clause
C# extractor: extract `catch(ExceptionType)` type as `TypeAccess` instead of `TypeMention`
2026-06-23 14:21:43 +02:00
copilot-swe-agent[bot]
b254aa7e0b C#: Extract catch(Ex) type as TypeAccess instead of TypeMention 2026-06-23 13:55:39 +02:00
yoff
d26102b263 Merge pull request #21920 from github/yoff/python-flow-py-namespace
Python: qualify Flow.qll's AST references with Py:: prefix
2026-06-23 13:20:26 +02:00
yoff
73ab3e6888 Update shared/controlflow/codeql/controlflow/ControlFlowGraph.qll
Co-authored-by: Anders Schack-Mulligen <aschackmull@users.noreply.github.com>
2026-06-23 12:41:02 +02:00
yoff
15cbbb82eb Shared CFG: add defaulted getLoopElse to AstSig
Adds a new defaulted signature predicates to the shared CFG library:

- getLoopElse: `else` block of a loop statement, if
  any (used by Python's `while-else` / `for-else` constructs).

The predicate defaults to `none()`, so behaviour is unchanged for any
language that doesn't override it (verified by re-running
java/ql/test/library-tests/controlflow/).

The Make0 succession rules are extended:
- WhileStmt/ForeachStmt: route the loop-exit edge through the else
  block before reaching the after-position.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 12:41:02 +02:00
yoff
7d95024487 Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-23 12:36:53 +02:00
Copilot
06fa46f664 Python: qualify Flow.qll's AST references with Py:: prefix
Preparatory refactor for the shared-CFG dataflow migration. Switches
'import python' to 'import python as Py' inside Flow.qll, and qualifies
every AST-class reference (Expr, Bytes, Dict, AssignExpr, Compare,
Module, Scope, Call, Attribute, SsaVariable, AugAssign, etc.) with the
Py:: prefix.

Flow.qll's own CFG types (ControlFlowNode, BasicBlock, CallNode,
NameNode, DefinitionNode, CompareNode, ...) keep their unqualified
names — they remain the public CFG API exported from this file.

This is a semantic noop: the qualification was applied mechanically by
script and no name resolution changes. Verified by:
- All 361 lib/ + src/ queries compile clean.
- All 186 ControlFlow + PointsTo + dataflow library-tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 12:36:53 +02:00
Geoffrey White
f6dce466a0 Merge pull request #22009 from geoffw0/rust-crypto
Rust: Additional test cases for rust/weak-sensitive-data-hashing
2026-06-23 10:53:45 +01:00
Geoffrey White
9e0e1bde28 Rust: Use Copilot suggested comment phrasing. 2026-06-22 16:12:54 +01:00
Geoffrey White
8c24acc99d Fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-22 16:10:57 +01:00
Geoffrey White
721070a191 Rust: Make the Seed case a tiny bit more realistic. 2026-06-18 23:43:18 +01:00
Geoffrey White
b86cb6df63 Rust: Additional test cases for weak sensitive data hashing. 2026-06-18 23:32:38 +01:00
Geoffrey White
3aaeb68553 Rust: Make the new test inline expectations. 2026-06-18 22:57:15 +01:00
Geoffrey White
e8923b7688 Rust: Output cryptographic operations in the weak sensitive data hashing query test. 2026-06-18 17:07:28 +01:00
115 changed files with 4938 additions and 720 deletions

View File

@@ -248,7 +248,6 @@ use_repo(
"kotlin-compiler-2.2.20-Beta2",
"kotlin-compiler-2.3.0",
"kotlin-compiler-2.3.20",
"kotlin-compiler-2.4.0",
"kotlin-compiler-embeddable-1.8.0",
"kotlin-compiler-embeddable-1.9.0-Beta",
"kotlin-compiler-embeddable-1.9.20-Beta",
@@ -260,7 +259,6 @@ use_repo(
"kotlin-compiler-embeddable-2.2.20-Beta2",
"kotlin-compiler-embeddable-2.3.0",
"kotlin-compiler-embeddable-2.3.20",
"kotlin-compiler-embeddable-2.4.0",
"kotlin-stdlib-1.8.0",
"kotlin-stdlib-1.9.0-Beta",
"kotlin-stdlib-1.9.20-Beta",
@@ -272,7 +270,6 @@ use_repo(
"kotlin-stdlib-2.2.20-Beta2",
"kotlin-stdlib-2.3.0",
"kotlin-stdlib-2.3.20",
"kotlin-stdlib-2.4.0",
)
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")

View File

@@ -25,7 +25,7 @@ namespace Semmle.Extraction.CSharp.Entities.Statements
{
var type = Type.Create(Context, Context.GetType(Stmt.Declaration!.Type));
trapFile.catch_type(this, type.TypeRef, true);
TypeMention.Create(Context, Stmt.Declaration!.Type, this, type);
Expression.Create(Context, Stmt.Declaration!.Type, this, 0);
}
else // A catch clause of the form 'catch { ... }'
{

View File

@@ -995,6 +995,23 @@ class SpecificCatchClause extends CatchClause {
/** Gets the local variable declaration of this catch clause, if any. */
LocalVariableDeclExpr getVariableDeclExpr() { result.getParent() = this }
/**
* Gets the type access of this catch clause, if it has no variable declaration.
*
* For example, the type access in
*
* ```csharp
* try { ... }
* catch (IOException) { ... }
* ```
*
* is `IOException`.
*/
TypeAccess getTypeAccess() {
not exists(this.getVariableDeclExpr()) and
result = this.getChild(0)
}
override string toString() { result = "catch (...) {...}" }
override string getAPrimaryQlClass() { result = "SpecificCatchClause" }

View File

@@ -90,6 +90,7 @@ module Ast implements AstSig<Location> {
private AstNode getStmtChild0(Stmt s, int i) {
not s instanceof FixedStmt and
not s instanceof UsingBlockStmt and
not skipControlFlow(result) and
result = s.getChild(i)
or
s =

View File

@@ -101,7 +101,8 @@ csharp6.cs:
# 32| 0: [IntLiteral] 2
# 32| 0: [IntLiteral] 1
# 34| 1: [SpecificCatchClause] catch (...) {...}
# 34| 0: [TypeMention] IndexOutOfRangeException
# 34| 0: [TypeAccess] access to type IndexOutOfRangeException
# 34| 0: [TypeMention] IndexOutOfRangeException
# 35| 1: [BlockStmt] {...}
# 34| 2: [EQExpr] ... == ...
# 34| 0: [PropertyCall] access to property Value

View File

@@ -21,7 +21,7 @@
Java,"Java 7 to 26 [6]_","javac (OpenJDK and Oracle JDK),
Eclipse compiler for Java (ECJ) [7]_",``.java``
Kotlin,"Kotlin 1.8.0 to 2.4.0\ *x*","kotlinc",``.kt``
Kotlin,"Kotlin 1.8.0 to 2.3.2\ *x*","kotlinc",``.kt``
JavaScript,ECMAScript 2022 or lower,Not applicable,"``.js``, ``.jsx``, ``.mjs``, ``.es``, ``.es6``, ``.htm``, ``.html``, ``.xhtm``, ``.xhtml``, ``.vue``, ``.hbs``, ``.ejs``, ``.njk``, ``.json``, ``.yaml``, ``.yml``, ``.raml``, ``.xml`` [8]_"
Python [9]_,"2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13",Not applicable,``.py``
Ruby [10]_,"up to 3.3",Not applicable,"``.rb``, ``.erb``, ``.gemspec``, ``Gemfile``"

View File

@@ -53,10 +53,6 @@ _extractor_name_prefix = "%s-%s" % (
"embeddable" if _for_embeddable else "standalone",
)
_compiler_plugin_registrar_service_source = "src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar"
_compiler_plugin_registrar_service_target = "META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar"
py_binary(
name = "generate_dbscheme",
srcs = ["generate_dbscheme.py"],
@@ -68,14 +64,8 @@ _resources = [
r[len("src/main/resources/"):],
)
for r in glob(["src/main/resources/**"])
if r != _compiler_plugin_registrar_service_source
]
_compiler_plugin_registrar_service = (
_compiler_plugin_registrar_service_source,
_compiler_plugin_registrar_service_target,
)
kt_javac_options(
name = "javac-options",
release = "8",
@@ -101,32 +91,19 @@ kt_javac_options(
# * `resource_strip_prefix` is unique per jar, so we must also put other resources under the same version prefix
genrule(
name = "resources-%s" % v,
srcs = [src for src, _ in _resources] + (
[_compiler_plugin_registrar_service[0]] if not version_less(v, "2.4.0") else []
),
srcs = [src for src, _ in _resources],
outs = [
"%s/com/github/codeql/extractor.name" % v,
] + [
"%s/%s" % (v, target)
for _, target in _resources
] + (
["%s/%s" % (
v,
_compiler_plugin_registrar_service[1],
)] if not version_less(v, "2.4.0") else []
),
],
cmd = "\n".join([
"echo %s-%s > $(RULEDIR)/%s/com/github/codeql/extractor.name" % (_extractor_name_prefix, v, v),
] + [
"cp $(execpath %s) $(RULEDIR)/%s/%s" % (source, v, target)
for source, target in _resources
] + (
["cp $(execpath %s) $(RULEDIR)/%s/%s" % (
_compiler_plugin_registrar_service[0],
v,
_compiler_plugin_registrar_service[1],
)] if not version_less(v, "2.4.0") else []
)),
]),
),
kt_jvm_library(
name = "%s-%s" % (_extractor_name_prefix, v),

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -27,7 +27,7 @@ import shutil
import io
import os
DEFAULT_VERSION = "2.4.0"
DEFAULT_VERSION = "2.3.20"
def options():

View File

@@ -3,21 +3,32 @@
package com.github.codeql
import com.intellij.mock.MockProject
import com.intellij.openapi.extensions.LoadingOrder
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.config.CompilerConfiguration
class KotlinExtractorComponentRegistrar : Kotlin2ComponentRegistrar() {
override fun doRegisterExtensions(configuration: CompilerConfiguration) {
override fun registerProjectComponents(
project: MockProject,
configuration: CompilerConfiguration
) {
val invocationTrapFile = configuration[KEY_INVOCATION_TRAP_FILE]
if (invocationTrapFile == null) {
throw Exception("Required argument for TRAP invocation file not given")
}
registerExtractorExtension(
// Register with LoadingOrder.LAST to ensure the extractor runs after other
// IR generation plugins (like kotlinx.serialization) have generated their code.
val extensionPoint = project.extensionArea.getExtensionPoint(IrGenerationExtension.extensionPointName)
extensionPoint.registerExtension(
KotlinExtractorExtension(
invocationTrapFile,
configuration[KEY_CHECK_TRAP_IDENTICAL] ?: false,
configuration[KEY_COMPILATION_STARTTIME],
configuration[KEY_EXIT_AFTER_EXTRACTION] ?: false
)
),
LoadingOrder.LAST,
project
)
}
}

View File

@@ -173,9 +173,9 @@ open class KotlinFileExtractor(
when (d) {
is IrFunction ->
when (d.name.asString()) {
"toString" -> d.codeQlValueParameters.isEmpty()
"hashCode" -> d.codeQlValueParameters.isEmpty()
"equals" -> d.codeQlValueParameters.singleOrNull()?.type?.isNullableAny() ?: false
"toString" -> d.valueParameters.isEmpty()
"hashCode" -> d.valueParameters.isEmpty()
"equals" -> d.valueParameters.singleOrNull()?.type?.isNullableAny() ?: false
else -> false
} && isJavaBinaryDeclaration(d)
else -> false
@@ -721,7 +721,7 @@ open class KotlinFileExtractor(
(it.type as? IrSimpleType)?.classFqName?.asString() != "kotlin.Deprecated"
} +
// Note we lose any arguments to @java.lang.Deprecated that were written in source.
codeQlAnnotationFromSymbolOwner(
IrConstructorCallImpl.fromSymbolOwner(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
jldConstructor.returnType,
@@ -781,13 +781,13 @@ open class KotlinFileExtractor(
val locId = tw.getLocation(constructorCall)
tw.writeHasLocation(id, locId)
for (i in 0 until constructorCall.codeQlValueArgumentsCount) {
val param = constructorCall.symbol.owner.codeQlValueParameters[i]
for (i in 0 until constructorCall.valueArgumentsCount) {
val param = constructorCall.symbol.owner.valueParameters[i]
val prop =
constructorCall.symbol.owner.parentAsClass.declarations
.filterIsInstance<IrProperty>()
.first { it.name == param.name }
val v = constructorCall.codeQlGetValueArgument(i) ?: param.defaultValue?.expression
val v = constructorCall.getValueArgument(i) ?: param.defaultValue?.expression
val getter = prop.getter
if (getter == null) {
logger.warnElement("Expected annotation property to define a getter", prop)
@@ -1115,9 +1115,9 @@ open class KotlinFileExtractor(
returnId,
0,
returnId,
f.codeQlValueParameters.size,
f.valueParameters.size,
{ argParent, idxOffset ->
f.codeQlValueParameters.forEachIndexed { idx, param ->
f.valueParameters.forEachIndexed { idx, param ->
val syntheticParamId = useValueParameter(param, proxyFunctionId)
extractVariableAccess(
syntheticParamId,
@@ -1695,9 +1695,9 @@ open class KotlinFileExtractor(
returnId,
0,
returnId,
f.codeQlValueParameters.size,
f.valueParameters.size,
{ argParentId, idxOffset ->
f.codeQlValueParameters.mapIndexed { idx, param ->
f.valueParameters.mapIndexed { idx, param ->
val syntheticParamId = useValueParameter(param, functionId)
extractVariableAccess(
syntheticParamId,
@@ -1792,7 +1792,7 @@ open class KotlinFileExtractor(
extractBody: Boolean,
extractMethodAndParameterTypeAccesses: Boolean
) {
if (f.codeQlValueParameters.none { it.defaultValue != null }) return
if (f.valueParameters.none { it.defaultValue != null }) return
val id = getDefaultsMethodLabel(f)
if (id == null) {
@@ -1800,7 +1800,7 @@ open class KotlinFileExtractor(
return
}
val locId = getLocation(f, null)
val extReceiver = f.codeQlExtensionReceiverParameter
val extReceiver = f.extensionReceiverParameter
val dispatchReceiver = if (f.shouldExtractAsStatic) null else f.dispatchReceiverParameter
val parameterTypes = getDefaultsMethodArgTypes(f)
val allParamTypeResults =
@@ -1869,7 +1869,7 @@ open class KotlinFileExtractor(
tw.writeCompiler_generated(id, CompilerGeneratedKinds.DEFAULT_ARGUMENTS_METHOD.kind)
if (extractBody) {
val nonSyntheticParams = listOfNotNull(dispatchReceiver) + f.codeQlValueParameters
val nonSyntheticParams = listOfNotNull(dispatchReceiver) + f.valueParameters
// This stack entry represents as if we're extracting the 'real' function `f`, giving
// the indices of its non-synthetic parameters
// such that when we extract the default expressions below, any reference to f's nth
@@ -1895,12 +1895,12 @@ open class KotlinFileExtractor(
val realParamsVarId = getValueParameterLabel(id, parameterTypes.size - 2)
val intType = pluginContext.irBuiltIns.intType
val paramIdxOffset =
listOf(dispatchReceiver, f.codeQlExtensionReceiverParameter).count { it != null }
listOf(dispatchReceiver, f.extensionReceiverParameter).count { it != null }
extractBlockBody(id, locId).also { blockId ->
var nextStmt = 0
// For each parameter with a default, sub in the default value if the caller
// hasn't supplied a value:
f.codeQlValueParameters.forEachIndexed { paramIdx, param ->
f.valueParameters.forEachIndexed { paramIdx, param ->
val defaultVal = param.defaultValue
if (defaultVal != null) {
extractIfStmt(locId, blockId, nextStmt++, id).also { ifId ->
@@ -1975,7 +1975,7 @@ open class KotlinFileExtractor(
id
)
tw.writeHasLocation(thisCallId, locId)
f.codeQlValueParameters.forEachIndexed { idx, param ->
f.valueParameters.forEachIndexed { idx, param ->
extractVariableAccess(
tw.getLabelFor<DbParam>(getValueParameterLabel(id, idx)),
param.type,
@@ -2003,9 +2003,9 @@ open class KotlinFileExtractor(
)
.also { thisCallId ->
val realFnIdxOffset =
if (f.codeQlExtensionReceiverParameter != null) 1 else 0
if (f.extensionReceiverParameter != null) 1 else 0
val paramMappings =
f.codeQlValueParameters.mapIndexed { idx, param ->
f.valueParameters.mapIndexed { idx, param ->
Triple(
param.type,
idx + paramIdxOffset,
@@ -2156,7 +2156,7 @@ open class KotlinFileExtractor(
val dispatchReceiver =
f.dispatchReceiverParameter?.let { IrGetValueImpl(-1, -1, it.symbol) }
val extensionReceiver =
f.codeQlExtensionReceiverParameter?.let { IrGetValueImpl(-1, -1, it.symbol) }
f.extensionReceiverParameter?.let { IrGetValueImpl(-1, -1, it.symbol) }
extractExpressionBody(overloadId, realFunctionLocId).also { returnId ->
extractsDefaultsCall(
@@ -2180,28 +2180,28 @@ open class KotlinFileExtractor(
if (!f.hasAnnotation(jvmOverloadsFqName)) {
if (
f is IrConstructor &&
f.codeQlValueParameters.isNotEmpty() &&
f.codeQlValueParameters.all { it.defaultValue != null } &&
f.valueParameters.isNotEmpty() &&
f.valueParameters.all { it.defaultValue != null } &&
f.parentClassOrNull?.let {
// Don't create a default constructor for an annotation class, or a class
// that explicitly declares a no-arg constructor.
!it.isAnnotationClass &&
it.declarations.none { d ->
d is IrConstructor && d.codeQlValueParameters.isEmpty()
d is IrConstructor && d.valueParameters.isEmpty()
}
} == true
) {
// Per https://kotlinlang.org/docs/classes.html#creating-instances-of-classes, a
// single default overload gets created specifically
// when we have all default parameters, regardless of `@JvmOverloads`.
extractGeneratedOverload(f.codeQlValueParameters.map { _ -> null })
extractGeneratedOverload(f.valueParameters.map { _ -> null })
}
return
}
val paramList: MutableList<IrValueParameter?> = f.codeQlValueParameters.toMutableList()
for (n in (f.codeQlValueParameters.size - 1) downTo 0) {
if (f.codeQlValueParameters[n].defaultValue != null) {
val paramList: MutableList<IrValueParameter?> = f.valueParameters.toMutableList()
for (n in (f.valueParameters.size - 1) downTo 0) {
if (f.valueParameters[n].defaultValue != null) {
paramList[n] = null // Remove this parameter, to be replaced by a default value
extractGeneratedOverload(paramList)
}
@@ -2327,7 +2327,7 @@ open class KotlinFileExtractor(
getClassByFqName(pluginContext, it)?.let { annotationClass ->
annotationClass.owner.declarations.firstIsInstanceOrNull<IrConstructor>()?.let {
annotationConstructor ->
codeQlAnnotationFromSymbolOwner(
IrConstructorCallImpl.fromSymbolOwner(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
annotationConstructor.returnType,
@@ -2388,13 +2388,13 @@ open class KotlinFileExtractor(
id
}
val extReceiver = f.codeQlExtensionReceiverParameter
val extReceiver = f.extensionReceiverParameter
// The following parameter order is correct, because member $default methods (where
// the order would be [dispatchParam], [extensionParam], normalParams) are not
// extracted here
val fParameters =
listOfNotNull(extReceiver) +
(overriddenAttributes?.valueParameters ?: f.codeQlValueParameters)
(overriddenAttributes?.valueParameters ?: f.valueParameters)
val paramTypes =
fParameters.mapIndexed { i, vp ->
extractValueParameter(
@@ -3069,14 +3069,14 @@ open class KotlinFileExtractor(
logger.errorElement("Unexpected dispatch receiver found", c)
}
if (c.codeQlValueArgumentsCount < 1) {
if (c.valueArgumentsCount < 1) {
logger.errorElement("No arguments found", c)
return
}
extractArgument(id, c, callable, enclosingStmt, 0, "Operand null")
if (c.codeQlValueArgumentsCount > 1) {
if (c.valueArgumentsCount > 1) {
logger.errorElement("Extra arguments found", c)
}
}
@@ -3095,21 +3095,21 @@ open class KotlinFileExtractor(
logger.errorElement("Unexpected dispatch receiver found", c)
}
if (c.codeQlValueArgumentsCount < 1) {
if (c.valueArgumentsCount < 1) {
logger.errorElement("No arguments found", c)
return
}
extractArgument(id, c, callable, enclosingStmt, 0, "LHS null")
if (c.codeQlValueArgumentsCount < 2) {
if (c.valueArgumentsCount < 2) {
logger.errorElement("No RHS found", c)
return
}
extractArgument(id, c, callable, enclosingStmt, 1, "RHS null")
if (c.codeQlValueArgumentsCount > 2) {
if (c.valueArgumentsCount > 2) {
logger.errorElement("Extra arguments found", c)
}
}
@@ -3122,7 +3122,7 @@ open class KotlinFileExtractor(
idx: Int,
msg: String
) {
val op = c.codeQlGetValueArgument(idx)
val op = c.getValueArgument(idx)
if (op == null) {
logger.errorElement(msg, c)
} else {
@@ -3267,8 +3267,8 @@ open class KotlinFileExtractor(
// and which should be replaced by defaults. The final Object parameter is apparently always
// null.
(listOfNotNull(if (f.shouldExtractAsStatic) null else f.dispatchReceiverParameter?.type) +
listOfNotNull(f.codeQlExtensionReceiverParameter?.type) +
f.codeQlValueParameters.map { it.type } +
listOfNotNull(f.extensionReceiverParameter?.type) +
f.valueParameters.map { it.type } +
listOf(pluginContext.irBuiltIns.intType, getDefaultsMethodLastArgType(f)))
.map { erase(it) }
@@ -3345,7 +3345,7 @@ open class KotlinFileExtractor(
val overriddenCallTarget =
(callTarget as? IrSimpleFunction)?.allOverridden(includeSelf = true)?.firstOrNull {
it.overriddenSymbols.isEmpty() &&
it.codeQlValueParameters.any { p -> p.defaultValue != null }
it.valueParameters.any { p -> p.defaultValue != null }
} ?: callTarget
if (isExternalDeclaration(overriddenCallTarget)) {
// Likewise, ensure the overridden target gets extracted.
@@ -3419,7 +3419,7 @@ open class KotlinFileExtractor(
}
val valueArgsWithDummies =
valueArguments.zip(callTarget.codeQlValueParameters).map { (expr, param) ->
valueArguments.zip(callTarget.valueParameters).map { (expr, param) ->
expr ?: IrConstImpl.defaultValueForType(0, 0, param.type)
}
@@ -3529,7 +3529,7 @@ open class KotlinFileExtractor(
callTarget: IrFunction,
valueArguments: List<IrExpression?>
): Boolean {
val varargParam = callTarget.codeQlValueParameters.withIndex().find { it.value.isVararg }
val varargParam = callTarget.valueParameters.withIndex().find { it.value.isVararg }
// If the vararg param is the only one not specified, and it has no default value, then we
// don't need to call a $default method,
// as omitting it already implies passing an empty vararg array.
@@ -3805,7 +3805,7 @@ open class KotlinFileExtractor(
) =
extractCallValueArguments(
callId,
(0 until call.codeQlValueArgumentsCount).map { call.codeQlGetValueArgument(it) },
(0 until call.valueArgumentsCount).map { call.getValueArgument(it) },
enclosingStmt,
enclosingCallable,
idxOffset
@@ -3874,7 +3874,7 @@ open class KotlinFileExtractor(
(owner.parentClassOrNull?.fqNameWhenAvailable?.asString() == type ||
(owner.parent is IrExternalPackageFragment &&
getFileClassFqName(owner)?.asString() == type)) &&
owner.codeQlValueParameters
owner.valueParameters
.map { it.type.classFqName?.asString() }
.toTypedArray() contentEquals parameterTypes
}
@@ -3926,8 +3926,8 @@ open class KotlinFileExtractor(
val result =
javaLangString?.declarations?.findSubType<IrFunction> {
it.name.asString() == "valueOf" &&
it.codeQlValueParameters.size == 1 &&
it.codeQlValueParameters[0].type == pluginContext.irBuiltIns.anyNType
it.valueParameters.size == 1 &&
it.valueParameters[0].type == pluginContext.irBuiltIns.anyNType
}
if (result == null) {
logger.error("Couldn't find declaration java.lang.String.valueOf(Object)")
@@ -3951,7 +3951,7 @@ open class KotlinFileExtractor(
val kotlinNoWhenBranchMatchedConstructor by lazy {
val result =
kotlinNoWhenBranchMatchedExn?.declarations?.findSubType<IrConstructor> {
it.codeQlValueParameters.isEmpty()
it.valueParameters.isEmpty()
}
if (result == null) {
logger.error("Couldn't find no-arg constructor for kotlin.NoWhenBranchMatchedException")
@@ -3990,7 +3990,7 @@ open class KotlinFileExtractor(
verboseln("No match as function name is ${target.name.asString()} not $fName")
return false
}
val extensionReceiverParameter = target.codeQlExtensionReceiverParameter
val extensionReceiverParameter = target.extensionReceiverParameter
val targetClass =
if (extensionReceiverParameter == null) {
if (isNullable == true) {
@@ -4098,8 +4098,8 @@ open class KotlinFileExtractor(
) {
val typeArgs =
if (extractMethodTypeArguments)
(0 until c.codeQlTypeArgumentsCount)
.map { c.codeQlGetTypeArgument(it) }
(0 until c.typeArgumentsCount)
.map { c.getTypeArgument(it) }
.requireNoNullsOrNull()
else listOf()
@@ -4116,9 +4116,9 @@ open class KotlinFileExtractor(
parent,
idx,
enclosingStmt,
(0 until c.codeQlValueArgumentsCount).map { c.codeQlGetValueArgument(it) },
(0 until c.valueArgumentsCount).map { c.getValueArgument(it) },
c.dispatchReceiver,
c.codeQlExtensionReceiver,
c.extensionReceiver,
typeArgs,
extractClassTypeArguments,
c.superQualifierSymbol
@@ -4126,12 +4126,12 @@ open class KotlinFileExtractor(
}
fun extractSpecialEnumFunction(fnName: String) {
if (c.codeQlTypeArgumentsCount != 1) {
if (c.typeArgumentsCount != 1) {
logger.errorElement("Expected to find exactly one type argument", c)
return
}
val enumType = (c.codeQlGetTypeArgument(0) as? IrSimpleType)?.classifier?.owner
val enumType = (c.getTypeArgument(0) as? IrSimpleType)?.classifier?.owner
if (enumType == null) {
logger.errorElement("Couldn't find type of enum type", c)
return
@@ -4178,13 +4178,13 @@ open class KotlinFileExtractor(
} else {
extractExpressionExpr(receiver, callable, id, 0, enclosingStmt)
}
if (c.codeQlValueArgumentsCount < 1) {
if (c.valueArgumentsCount < 1) {
logger.errorElement("No RHS found", c)
} else {
if (c.codeQlValueArgumentsCount > 1) {
if (c.valueArgumentsCount > 1) {
logger.errorElement("Extra arguments found", c)
}
val arg = c.codeQlGetValueArgument(0)
val arg = c.getValueArgument(0)
if (arg == null) {
logger.errorElement("RHS null", c)
} else {
@@ -4205,7 +4205,7 @@ open class KotlinFileExtractor(
} else {
extractExpressionExpr(receiver, callable, id, 0, enclosingStmt)
}
if (c.codeQlValueArgumentsCount > 0) {
if (c.valueArgumentsCount > 0) {
logger.errorElement("Extra arguments found", c)
}
}
@@ -4219,7 +4219,7 @@ open class KotlinFileExtractor(
}
fun binopExt(id: Label<out DbExpr>) {
binopReceiver(id, c.codeQlExtensionReceiver, "Extension receiver")
binopReceiver(id, c.extensionReceiver, "Extension receiver")
}
fun unaryopDisp(id: Label<out DbExpr>) {
@@ -4227,7 +4227,7 @@ open class KotlinFileExtractor(
}
fun unaryopExt(id: Label<out DbExpr>) {
unaryopReceiver(id, c.codeQlExtensionReceiver, "Extension receiver")
unaryopReceiver(id, c.extensionReceiver, "Extension receiver")
}
val dr = c.dispatchReceiver
@@ -4249,7 +4249,7 @@ open class KotlinFileExtractor(
parent,
idx,
enclosingStmt,
listOf(c.codeQlExtensionReceiver, c.codeQlGetValueArgument(0)),
listOf(c.extensionReceiver, c.getValueArgument(0)),
null,
null
)
@@ -4350,7 +4350,7 @@ open class KotlinFileExtractor(
// != gets desugared into not and ==. Here we resugar it.
c.origin == IrStatementOrigin.EXCLEQ &&
isFunction(target, "kotlin", "Boolean", "not") &&
c.codeQlValueArgumentsCount == 0 &&
c.valueArgumentsCount == 0 &&
dr != null &&
dr is IrCall &&
isBuiltinCallInternal(dr, "EQEQ") -> {
@@ -4362,7 +4362,7 @@ open class KotlinFileExtractor(
}
c.origin == IrStatementOrigin.EXCLEQEQ &&
isFunction(target, "kotlin", "Boolean", "not") &&
c.codeQlValueArgumentsCount == 0 &&
c.valueArgumentsCount == 0 &&
dr != null &&
dr is IrCall &&
isBuiltinCallInternal(dr, "EQEQEQ") -> {
@@ -4374,7 +4374,7 @@ open class KotlinFileExtractor(
}
c.origin == IrStatementOrigin.EXCLEQ &&
isFunction(target, "kotlin", "Boolean", "not") &&
c.codeQlValueArgumentsCount == 0 &&
c.valueArgumentsCount == 0 &&
dr != null &&
dr is IrCall &&
isBuiltinCallInternal(dr, "ieee754equals") -> {
@@ -4576,7 +4576,7 @@ open class KotlinFileExtractor(
parent,
idx,
enclosingStmt,
listOf(c.codeQlExtensionReceiver),
listOf(c.extensionReceiver),
null,
null
)
@@ -4596,8 +4596,8 @@ open class KotlinFileExtractor(
val locId = tw.getLocation(c)
extractExprContext(id, locId, callable, enclosingStmt)
if (c.codeQlTypeArgumentsCount == 1) {
val typeArgument = c.codeQlGetTypeArgument(0)
if (c.typeArgumentsCount == 1) {
val typeArgument = c.getTypeArgument(0)
if (typeArgument == null) {
logger.errorElement("Type argument missing in an arrayOfNulls call", c)
} else {
@@ -4618,8 +4618,8 @@ open class KotlinFileExtractor(
)
}
if (c.codeQlValueArgumentsCount == 1) {
val dim = c.codeQlGetValueArgument(0)
if (c.valueArgumentsCount == 1) {
val dim = c.getValueArgument(0)
if (dim != null) {
extractExpressionExpr(dim, callable, id, 0, enclosingStmt)
} else {
@@ -4651,8 +4651,8 @@ open class KotlinFileExtractor(
c.type.getArrayElementTypeCodeQL(pluginContext.irBuiltIns)
} else {
// TODO: is there any reason not to always use getArrayElementTypeCodeQL?
if (c.codeQlTypeArgumentsCount == 1) {
c.codeQlGetTypeArgument(0).also {
if (c.typeArgumentsCount == 1) {
c.getTypeArgument(0).also {
if (it == null) {
logger.errorElement(
"Type argument missing in an arrayOf call",
@@ -4670,7 +4670,7 @@ open class KotlinFileExtractor(
}
val arg =
if (c.codeQlValueArgumentsCount == 1) c.codeQlGetValueArgument(0)
if (c.valueArgumentsCount == 1) c.getValueArgument(0)
else {
logger.errorElement(
"Expected to find only one (vararg) argument in ${c.symbol.owner.name.asString()} call",
@@ -4719,7 +4719,7 @@ open class KotlinFileExtractor(
return
}
val ext = c.codeQlExtensionReceiver
val ext = c.extensionReceiver
if (ext == null) {
logger.errorElement(
"No extension receiver found for `KClass::java` call",
@@ -4826,8 +4826,8 @@ open class KotlinFileExtractor(
c.origin == IrStatementOrigin.EQ &&
c.dispatchReceiver != null -> {
val array = c.dispatchReceiver
val arrayIdx = c.codeQlGetValueArgument(0)
val assignedValue = c.codeQlGetValueArgument(1)
val arrayIdx = c.getValueArgument(0)
val assignedValue = c.getValueArgument(1)
if (array != null && arrayIdx != null && assignedValue != null) {
@@ -4882,22 +4882,22 @@ open class KotlinFileExtractor(
}
isBuiltinCall(c, "<unsafe-coerce>", "kotlin.jvm.internal") -> {
if (c.codeQlValueArgumentsCount != 1) {
if (c.valueArgumentsCount != 1) {
logger.errorElement(
"Expected to find one argument for a kotlin.jvm.internal.<unsafe-coerce>() call, but found ${c.codeQlValueArgumentsCount}",
"Expected to find one argument for a kotlin.jvm.internal.<unsafe-coerce>() call, but found ${c.valueArgumentsCount}",
c
)
return
}
if (c.codeQlTypeArgumentsCount != 2) {
if (c.typeArgumentsCount != 2) {
logger.errorElement(
"Expected to find two type arguments for a kotlin.jvm.internal.<unsafe-coerce>() call, but found ${c.codeQlTypeArgumentsCount}",
"Expected to find two type arguments for a kotlin.jvm.internal.<unsafe-coerce>() call, but found ${c.typeArgumentsCount}",
c
)
return
}
val valueArg = c.codeQlGetValueArgument(0)
val valueArg = c.getValueArgument(0)
if (valueArg == null) {
logger.errorElement(
"Cannot find value argument for a kotlin.jvm.internal.<unsafe-coerce>() call",
@@ -4905,7 +4905,7 @@ open class KotlinFileExtractor(
)
return
}
val typeArg = c.codeQlGetTypeArgument(1)
val typeArg = c.getTypeArgument(1)
if (typeArg == null) {
logger.errorElement(
"Cannot find type argument for a kotlin.jvm.internal.<unsafe-coerce>() call",
@@ -4924,7 +4924,7 @@ open class KotlinFileExtractor(
extractExpressionExpr(valueArg, callable, id, 1, enclosingStmt)
}
isBuiltinCallInternal(c, "dataClassArrayMemberToString") -> {
val arrayArg = c.codeQlGetValueArgument(0)
val arrayArg = c.getValueArgument(0)
val realArrayClass = arrayArg?.type?.classOrNull
if (realArrayClass == null) {
logger.errorElement(
@@ -4936,8 +4936,8 @@ open class KotlinFileExtractor(
val realCallee =
javaUtilArrays?.declarations?.findSubType<IrFunction> { decl ->
decl.name.asString() == "toString" &&
decl.codeQlValueParameters.size == 1 &&
decl.codeQlValueParameters[0].type.classOrNull?.let {
decl.valueParameters.size == 1 &&
decl.valueParameters[0].type.classOrNull?.let {
it == realArrayClass
} == true
}
@@ -4962,7 +4962,7 @@ open class KotlinFileExtractor(
}
}
isBuiltinCallInternal(c, "dataClassArrayMemberHashCode") -> {
val arrayArg = c.codeQlGetValueArgument(0)
val arrayArg = c.getValueArgument(0)
val realArrayClass = arrayArg?.type?.classOrNull
if (realArrayClass == null) {
logger.errorElement(
@@ -4974,8 +4974,8 @@ open class KotlinFileExtractor(
val realCallee =
javaUtilArrays?.declarations?.findSubType<IrFunction> { decl ->
decl.name.asString() == "hashCode" &&
decl.codeQlValueParameters.size == 1 &&
decl.codeQlValueParameters[0].type.classOrNull?.let {
decl.valueParameters.size == 1 &&
decl.valueParameters[0].type.classOrNull?.let {
it == realArrayClass
} == true
}
@@ -5155,7 +5155,7 @@ open class KotlinFileExtractor(
val type = useType(eType)
val isAnonymous = eType.isAnonymous
val locId = tw.getLocation(e)
val valueArgs = (0 until e.codeQlValueArgumentsCount).map { e.codeQlGetValueArgument(it) }
val valueArgs = (0 until e.valueArgumentsCount).map { e.getValueArgument(it) }
val id =
if (
@@ -5211,10 +5211,10 @@ open class KotlinFileExtractor(
realCallTarget is IrConstructor &&
realCallTarget.parentClassOrNull?.fqNameWhenAvailable?.asString() ==
"kotlin.Enum" &&
realCallTarget.codeQlValueParameters.size == 2 &&
realCallTarget.codeQlValueParameters[0].type ==
realCallTarget.valueParameters.size == 2 &&
realCallTarget.valueParameters[0].type ==
pluginContext.irBuiltIns.stringType &&
realCallTarget.codeQlValueParameters[1].type == pluginContext.irBuiltIns.intType
realCallTarget.valueParameters[1].type == pluginContext.irBuiltIns.intType
) {
val id0 =
@@ -5287,7 +5287,7 @@ open class KotlinFileExtractor(
}
val args =
(0 until e.codeQlTypeArgumentsCount).map { e.codeQlGetTypeArgument(it) }.requireNoNullsOrNull()
(0 until e.typeArgumentsCount).map { e.getTypeArgument(it) }.requireNoNullsOrNull()
if (args == null) {
logger.warnElement("Found null type argument in enum constructor call", e)
return
@@ -5365,7 +5365,7 @@ open class KotlinFileExtractor(
// Check for an expression like x = get(x).op(e):
val opReceiver = updateRhs.dispatchReceiver
if (isExpectedLhs(opReceiver)) {
updateRhs.codeQlGetValueArgument(0)
updateRhs.getValueArgument(0)
} else null
} else null
}
@@ -5560,7 +5560,7 @@ open class KotlinFileExtractor(
"set"
)
) {
val updateRhs0 = arraySetCall.codeQlGetValueArgument(1)
val updateRhs0 = arraySetCall.getValueArgument(1)
if (updateRhs0 == null) {
logger.errorElement("Update RHS not found", e)
return false
@@ -6403,12 +6403,12 @@ open class KotlinFileExtractor(
val ids = getLocallyVisibleFunctionLabels(e.function)
val locId = tw.getLocation(e)
val ext = e.function.codeQlExtensionReceiverParameter
val ext = e.function.extensionReceiverParameter
val parameters =
if (ext != null) {
listOf(ext) + e.function.codeQlValueParameters
listOf(ext) + e.function.valueParameters
} else {
e.function.codeQlValueParameters
e.function.valueParameters
}
var types = parameters.map { it.type }
@@ -6670,7 +6670,7 @@ open class KotlinFileExtractor(
is IrFunction -> {
if (
ownerParent.dispatchReceiverParameter == owner &&
ownerParent.codeQlExtensionReceiverParameter != null
ownerParent.extensionReceiverParameter != null
) {
val ownerParent2 = ownerParent.parent
@@ -7089,7 +7089,7 @@ open class KotlinFileExtractor(
makeReceiverInfo(callableReferenceExpr.dispatchReceiver, 0)
private val extensionReceiverInfo =
makeReceiverInfo(
callableReferenceExpr.codeQlExtensionReceiver,
callableReferenceExpr.extensionReceiver,
if (dispatchReceiverInfo == null) 0 else 1
)
@@ -7627,8 +7627,8 @@ open class KotlinFileExtractor(
}
val expressionTypeArguments =
(0 until propertyReferenceExpr.codeQlTypeArgumentsCount).mapNotNull {
propertyReferenceExpr.codeQlGetTypeArgument(it)
(0 until propertyReferenceExpr.typeArgumentsCount).mapNotNull {
propertyReferenceExpr.getTypeArgument(it)
}
val idPropertyRef = tw.getFreshIdLabel<DbPropertyref>()
@@ -7829,7 +7829,7 @@ open class KotlinFileExtractor(
if (
functionReferenceExpr.dispatchReceiver != null &&
functionReferenceExpr.codeQlExtensionReceiver != null
functionReferenceExpr.extensionReceiver != null
) {
logger.errorElement(
"Unexpected: dispatchReceiver and extensionReceiver are both non-null",
@@ -7840,7 +7840,7 @@ open class KotlinFileExtractor(
if (
target.owner.dispatchReceiverParameter != null &&
target.owner.codeQlExtensionReceiverParameter != null
target.owner.extensionReceiverParameter != null
) {
logger.errorElement(
"Unexpected: dispatch and extension parameters are both non-null",
@@ -7899,8 +7899,8 @@ open class KotlinFileExtractor(
null
}
expressionTypeArguments =
(0 until functionReferenceExpr.codeQlTypeArgumentsCount).mapNotNull {
functionReferenceExpr.codeQlGetTypeArgument(it)
(0 until functionReferenceExpr.typeArgumentsCount).mapNotNull {
functionReferenceExpr.getTypeArgument(it)
}
dispatchReceiverIdx = -1
}
@@ -7965,7 +7965,7 @@ open class KotlinFileExtractor(
functionReferenceExpr,
declarationParent,
null,
{ it.codeQlValueParameters.size == 1 }
{ it.valueParameters.size == 1 }
) {
// The argument to FunctionReference's constructor is the function arity.
extractConstantInteger(
@@ -8572,7 +8572,7 @@ open class KotlinFileExtractor(
reverse: Boolean = false
) {
val typeArguments =
(0 until c.codeQlTypeArgumentsCount).map { c.codeQlGetTypeArgument(it) }.requireNoNullsOrNull()
(0 until c.typeArgumentsCount).map { c.getTypeArgument(it) }.requireNoNullsOrNull()
if (typeArguments == null) {
logger.errorElement("Found a null type argument for a member access expression", c)
} else {
@@ -8923,11 +8923,11 @@ open class KotlinFileExtractor(
tw.writeVariableBinding(lhsId, fieldId)
val parameters = mutableListOf<IrValueParameter>()
val extParam = samMember.codeQlExtensionReceiverParameter
val extParam = samMember.extensionReceiverParameter
if (extParam != null) {
parameters.add(extParam)
}
parameters.addAll(samMember.codeQlValueParameters)
parameters.addAll(samMember.valueParameters)
fun extractArgument(
p: IrValueParameter,
@@ -9032,7 +9032,7 @@ open class KotlinFileExtractor(
elementToReportOn: IrElement,
declarationParent: IrDeclarationParent,
compilerGeneratedKindOverride: CompilerGeneratedKinds? = null,
superConstructorSelector: (IrFunction) -> Boolean = { it.codeQlValueParameters.isEmpty() },
superConstructorSelector: (IrFunction) -> Boolean = { it.valueParameters.isEmpty() },
extractSuperconstructorArgs: (Label<DbSuperconstructorinvocationstmt>) -> Unit = {},
): Label<out DbClassorinterface> {
// Write class

View File

@@ -12,7 +12,7 @@ import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.symbols.*
import com.github.codeql.utils.versions.codeQlAddAnnotations
import org.jetbrains.kotlin.ir.types.addAnnotations
import org.jetbrains.kotlin.ir.types.classFqName
import org.jetbrains.kotlin.ir.types.classifierOrNull
import org.jetbrains.kotlin.ir.types.classOrNull
@@ -355,7 +355,7 @@ open class KotlinUsesExtractor(
}
private fun propertySignature(p: IrProperty) =
((p.getter ?: p.setter)?.codeQlExtensionReceiverParameter?.let {
((p.getter ?: p.setter)?.extensionReceiverParameter?.let {
useType(erase(it.type)).javaResult.signature
} ?: "")
@@ -368,7 +368,7 @@ open class KotlinUsesExtractor(
// useDeclarationParent -> useFunction
// -> extractFunctionLaterIfExternalFileMember, which would result for `fun <T> f(t:
// T) { ... }` for example.
(listOfNotNull(d.codeQlExtensionReceiverParameter) + d.codeQlValueParameters)
(listOfNotNull(d.extensionReceiverParameter) + d.valueParameters)
.map { useType(erase(it.type)).javaResult.signature }
.joinToString(separator = ",", prefix = "(", postfix = ")")
is IrProperty -> propertySignature(d) + externalClassExtractor.propertySignature
@@ -488,8 +488,8 @@ open class KotlinUsesExtractor(
val result =
replacementClass.declarations.findSubType<IrSimpleFunction> { replacementDecl ->
replacementDecl.name == f.name &&
replacementDecl.codeQlValueParameters.size == f.codeQlValueParameters.size &&
replacementDecl.codeQlValueParameters.zip(f.codeQlValueParameters).all {
replacementDecl.valueParameters.size == f.valueParameters.size &&
replacementDecl.valueParameters.zip(f.valueParameters).all {
erase(it.first.type) == erase(it.second.type)
}
}
@@ -1265,7 +1265,7 @@ open class KotlinUsesExtractor(
private fun getWildcardSuppressionDirective(t: IrAnnotationContainer): Boolean? =
t.getAnnotation(jvmWildcardSuppressionAnnotation)?.let {
@Suppress("USELESS_CAST") // `as? Boolean` is not needed for Kotlin < 2.1
(it.codeQlGetValueArgument(0) as? CodeQLIrConst<Boolean>)?.value as? Boolean ?: true
(it.getValueArgument(0) as? CodeQLIrConst<Boolean>)?.value as? Boolean ?: true
}
private fun addJavaLoweringArgumentWildcards(
@@ -1376,9 +1376,9 @@ open class KotlinUsesExtractor(
f.parent,
parentId,
getFunctionShortName(f).nameInDB,
(maybeParameterList ?: f.codeQlValueParameters).map { it.type },
(maybeParameterList ?: f.valueParameters).map { it.type },
getAdjustedReturnType(f),
f.codeQlExtensionReceiverParameter?.type,
f.extensionReceiverParameter?.type,
getFunctionTypeParameters(f),
classTypeArgsIncludingOuterClasses,
overridesCollectionsMethodWithAlteredParameterTypes(f),
@@ -1401,12 +1401,12 @@ open class KotlinUsesExtractor(
// The name of the function; normally f.name.asString().
name: String,
// The types of the value parameters that the functions takes; normally
// f.codeQlValueParameters.map { it.type }.
// f.valueParameters.map { it.type }.
parameterTypes: List<IrType>,
// The return type of the function; normally f.returnType.
returnType: IrType,
// The extension receiver of the function, if any; normally
// f.codeQlExtensionReceiverParameter?.type.
// f.extensionReceiverParameter?.type.
extensionParamType: IrType?,
// The type parameters of the function. This does not include type parameters of enclosing
// classes.
@@ -1579,7 +1579,7 @@ open class KotlinUsesExtractor(
parentClass.fqNameWhenAvailable?.asString() !=
"java.util.concurrent.ConcurrentHashMap" ||
getFunctionShortName(f).nameInDB != "keySet" ||
f.codeQlValueParameters.isNotEmpty() ||
f.valueParameters.isNotEmpty() ||
f.returnType.classFqName?.asString() != "kotlin.collections.MutableSet"
) {
return f.returnType
@@ -1587,7 +1587,7 @@ open class KotlinUsesExtractor(
val otherKeySet =
parentClass.declarations.findSubType<IrFunction> {
it.name.asString() == "keySet" && it.codeQlValueParameters.size == 1
it.name.asString() == "keySet" && it.valueParameters.size == 1
} ?: return f.returnType
return otherKeySet.returnType.codeQlWithHasQuestionMark(false)
@@ -1695,8 +1695,8 @@ open class KotlinUsesExtractor(
javaClass.declarations.findSubType<IrFunction> { decl ->
!decl.isFakeOverride &&
decl.name.asString() == jvmName &&
decl.codeQlValueParameters.size == f.codeQlValueParameters.size &&
decl.codeQlValueParameters.zip(f.codeQlValueParameters).all { p ->
decl.valueParameters.size == f.valueParameters.size &&
decl.valueParameters.zip(f.valueParameters).all { p ->
erase(p.first.type).classifierOrNull ==
erase(p.second.type).classifierOrNull
}
@@ -2125,7 +2125,7 @@ open class KotlinUsesExtractor(
}
return if (t.arguments.isNotEmpty())
t.codeQlAddAnnotations(listOf(RawTypeAnnotation.annotationConstructor))
t.addAnnotations(listOf(RawTypeAnnotation.annotationConstructor))
else t
}
}
@@ -2153,7 +2153,7 @@ open class KotlinUsesExtractor(
val idxOffset =
if (
declarationParent is IrFunction &&
declarationParent.codeQlExtensionReceiverParameter != null
declarationParent.extensionReceiverParameter != null
)
// For extension functions increase the index to match what the java extractor sees:
1
@@ -2187,7 +2187,7 @@ open class KotlinUsesExtractor(
// Gets a field's corresponding property's extension receiver type, if any
fun getExtensionReceiverType(f: IrField) =
f.correspondingPropertySymbol?.owner?.let {
(it.getter ?: it.setter)?.codeQlExtensionReceiverParameter?.type
(it.getter ?: it.setter)?.extensionReceiverParameter?.type
}
fun getFieldLabel(f: IrField): String {
@@ -2222,14 +2222,14 @@ open class KotlinUsesExtractor(
val setter = p.setter
val func = getter ?: setter
val ext = func?.codeQlExtensionReceiverParameter
val ext = func?.extensionReceiverParameter
return if (ext == null) {
"@\"property;{$parentId};${p.name.asString()}\""
} else {
val returnType =
getter?.returnType
?: setter?.codeQlValueParameters?.singleOrNull()?.type
?: setter?.valueParameters?.singleOrNull()?.type
?: pluginContext.irBuiltIns.unitType
val typeParams = getFunctionTypeParameters(func)

View File

@@ -1,10 +1,5 @@
package com.github.codeql
import com.github.codeql.utils.versions.codeQlAnnotationFromSymbolOwner
import com.github.codeql.utils.versions.codeQlGetValueArgument
import com.github.codeql.utils.versions.codeQlPutValueArgument
import com.github.codeql.utils.versions.codeQlSetAnnotations
import com.github.codeql.utils.versions.codeQlSetDispatchReceiverParameter
import com.github.codeql.utils.versions.createImplicitParameterDeclarationWithWrappedDescriptor
import java.lang.annotation.ElementType
import java.util.HashSet
@@ -100,7 +95,7 @@ class MetaAnnotationSupport(
JvmAnnotationNames.REPEATABLE_ANNOTATION
}
return if (jvmRepeatable != null) {
((jvmRepeatable.codeQlGetValueArgument(0) as? IrClassReference)?.symbol as? IrClassSymbol)
((jvmRepeatable.getValueArgument(0) as? IrClassReference)?.symbol as? IrClassSymbol)
?.owner
} else {
getOrCreateSyntheticRepeatableAnnotationContainer(annotationClass)
@@ -122,12 +117,12 @@ class MetaAnnotationSupport(
)
return null
} else {
return codeQlAnnotationFromSymbolOwner(
return IrConstructorCallImpl.fromSymbolOwner(
containerClass.defaultType,
containerConstructor.symbol
)
.apply {
codeQlPutValueArgument(
putValueArgument(
0,
IrVarargImpl(
UNDEFINED_OFFSET,
@@ -149,7 +144,7 @@ class MetaAnnotationSupport(
// Taken from AdditionalClassAnnotationLowering.kt
private fun loadAnnotationTargets(targetEntry: IrConstructorCall): Set<KotlinTarget>? {
val valueArgument = targetEntry.codeQlGetValueArgument(0) as? IrVararg ?: return null
val valueArgument = targetEntry.getValueArgument(0) as? IrVararg ?: return null
return valueArgument.elements
.filterIsInstance<IrGetEnumValue>()
.mapNotNull { KotlinTarget.valueOrNull(it.symbol.owner.name.asString()) }
@@ -235,14 +230,14 @@ class MetaAnnotationSupport(
)
}
return codeQlAnnotationFromSymbolOwner(
return IrConstructorCallImpl.fromSymbolOwner(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
targetConstructor.returnType,
targetConstructor.symbol,
0
)
.apply { codeQlPutValueArgument(0, vararg) }
.apply { putValueArgument(0, vararg) }
}
private val javaAnnotationRetention by lazy {
@@ -268,7 +263,7 @@ class MetaAnnotationSupport(
// Taken from AnnotationCodegen.kt (not available in Kotlin < 1.6.20)
private fun IrClass.getAnnotationRetention(): KotlinRetention? {
val retentionArgument =
getAnnotation(StandardNames.FqNames.retention)?.codeQlGetValueArgument(0) as? IrGetEnumValue
getAnnotation(StandardNames.FqNames.retention)?.getValueArgument(0) as? IrGetEnumValue
?: return null
val retentionArgumentValue = retentionArgument.symbol.owner
return KotlinRetention.valueOf(retentionArgumentValue.name.asString())
@@ -288,7 +283,7 @@ class MetaAnnotationSupport(
val targetConstructor =
retentionType.declarations.firstIsInstanceOrNull<IrConstructor>() ?: return null
return codeQlAnnotationFromSymbolOwner(
return IrConstructorCallImpl.fromSymbolOwner(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
targetConstructor.returnType,
@@ -296,7 +291,7 @@ class MetaAnnotationSupport(
0
)
.apply {
codeQlPutValueArgument(
putValueArgument(
0,
IrGetEnumValueImpl(
UNDEFINED_OFFSET,
@@ -338,7 +333,7 @@ class MetaAnnotationSupport(
return
}
val newParam = thisReceiever.copyTo(this)
codeQlSetDispatchReceiverParameter(newParam)
dispatchReceiverParameter = newParam
body =
factory
.createBlockBody(UNDEFINED_OFFSET, UNDEFINED_OFFSET)
@@ -411,7 +406,7 @@ class MetaAnnotationSupport(
val repeatableContainerAnnotation =
kotlinAnnotationRepeatableContainer?.constructors?.single()
codeQlSetAnnotations(containerClass,
containerClass.annotations =
annotationClass.annotations
.filter {
it.isAnnotationWithEqualFqName(StandardNames.FqNames.retention) ||
@@ -420,7 +415,7 @@ class MetaAnnotationSupport(
.map { it.deepCopyWithSymbols(containerClass) } +
listOfNotNull(
repeatableContainerAnnotation?.let {
codeQlAnnotationFromSymbolOwner(
IrConstructorCallImpl.fromSymbolOwner(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
it.returnType,
@@ -429,7 +424,6 @@ class MetaAnnotationSupport(
)
}
)
)
containerClass
}
@@ -468,14 +462,14 @@ class MetaAnnotationSupport(
containerClass.symbol,
containerClass.defaultType
)
return codeQlAnnotationFromSymbolOwner(
return IrConstructorCallImpl.fromSymbolOwner(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
repeatableConstructor.returnType,
repeatableConstructor.symbol,
0
)
.apply { codeQlPutValueArgument(0, containerReference) }
.apply { putValueArgument(0, containerReference) }
}
private val javaAnnotationDocumented by lazy {
@@ -494,7 +488,7 @@ class MetaAnnotationSupport(
javaAnnotationDocumented?.declarations?.firstIsInstanceOrNull<IrConstructor>()
?: return null
return codeQlAnnotationFromSymbolOwner(
return IrConstructorCallImpl.fromSymbolOwner(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
documentedConstructor.returnType,

View File

@@ -1,7 +1,6 @@
package com.github.codeql
import com.github.codeql.KotlinUsesExtractor.LocallyVisibleFunctionLabels
import com.github.codeql.utils.versions.codeQlExtensionReceiver
import com.semmle.extractor.java.PopulateFile
import com.semmle.util.unicode.UTF8Util
import java.io.BufferedWriter
@@ -332,7 +331,7 @@ open class FileTrapWriter(
is IrCall -> {
// Calls have incorrect startOffset, so we adjust them:
val dr = e.dispatchReceiver?.let { getStartOffset(it) }
val er = e.codeQlExtensionReceiver?.let { getStartOffset(it) }
val er = e.extensionReceiver?.let { getStartOffset(it) }
offsetMinOf(e.startOffset, dr, er)
}
else -> e.startOffset

View File

@@ -2,7 +2,6 @@ package com.github.codeql.comments
import com.github.codeql.*
import com.github.codeql.utils.isLocalFunction
import com.github.codeql.utils.versions.codeQlExtensionReceiverParameter
import com.github.codeql.utils.versions.isDispatchReceiver
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.declarations.*
@@ -12,7 +11,7 @@ import org.jetbrains.kotlin.ir.util.parentClassOrNull
private fun IrValueParameter.isExtensionReceiver(): Boolean {
val parentFun = parent as? IrFunction ?: return false
return parentFun.codeQlExtensionReceiverParameter == this
return parentFun.extensionReceiverParameter == this
}
open class CommentExtractor(

View File

@@ -1,8 +1,6 @@
package com.github.codeql.utils
import com.github.codeql.utils.versions.CodeQLIrConst
import com.github.codeql.utils.versions.codeQlGetValueArgument
import com.github.codeql.utils.versions.codeQlValueArgumentsCount
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.ir.declarations.IrAnnotationContainer
import org.jetbrains.kotlin.ir.declarations.IrClass
@@ -78,9 +76,9 @@ private fun getSpecialJvmName(f: IrFunction): String? {
fun getJvmName(container: IrAnnotationContainer): String? {
for (a: IrConstructorCall in container.annotations) {
val t = a.type
if (t is IrSimpleType && a.codeQlValueArgumentsCount == 1) {
if (t is IrSimpleType && a.valueArgumentsCount == 1) {
val owner = t.classifier.owner
val v = a.codeQlGetValueArgument(0)
val v = a.getValueArgument(0)
if (owner is IrClass) {
val aPkg = owner.packageFqName?.asString()
val name = owner.name.asString()

View File

@@ -18,7 +18,7 @@ import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.impl.*
import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
import org.jetbrains.kotlin.ir.symbols.impl.DescriptorlessExternalPackageFragmentSymbol
import com.github.codeql.utils.versions.codeQlAddAnnotations
import org.jetbrains.kotlin.ir.types.addAnnotations
import org.jetbrains.kotlin.ir.types.classifierOrNull
import org.jetbrains.kotlin.ir.types.makeNotNull
import org.jetbrains.kotlin.ir.types.makeNullable
@@ -192,7 +192,7 @@ object RawTypeAnnotation {
addConstructor { isPrimary = true }
}
val constructor = annoClass.constructors.single()
codeQlAnnotationFromSymbolOwner(constructor.constructedClassType, constructor.symbol)
IrConstructorCallImpl.fromSymbolOwner(constructor.constructedClassType, constructor.symbol)
}
}
@@ -202,7 +202,7 @@ fun IrType.toRawType(): IrType =
when (val owner = this.classifier.owner) {
is IrClass -> {
if (this.arguments.isNotEmpty())
this.codeQlAddAnnotations(listOf(RawTypeAnnotation.annotationConstructor))
this.addAnnotations(listOf(RawTypeAnnotation.annotationConstructor))
else this
}
is IrTypeParameter -> owner.superTypes[0].toRawType()
@@ -215,7 +215,7 @@ fun IrType.toRawType(): IrType =
fun IrClass.toRawType(): IrType {
val result = this.typeWith(listOf())
return if (this.typeParameters.isNotEmpty())
result.codeQlAddAnnotations(listOf(RawTypeAnnotation.annotationConstructor))
result.addAnnotations(listOf(RawTypeAnnotation.annotationConstructor))
else result
}

View File

@@ -1,70 +0,0 @@
package com.github.codeql.utils.versions
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpression
import org.jetbrains.kotlin.ir.expressions.impl.*
import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.addAnnotations
/**
* Compatibility accessors for pre-2.4.0 API patterns.
* In pre-2.4.0 versions, these delegate directly to the existing APIs.
*/
// IrFunction: valueParameters
val IrFunction.codeQlValueParameters: List<IrValueParameter>
get() = valueParameters
// IrFunction: extensionReceiverParameter
val IrFunction.codeQlExtensionReceiverParameter: IrValueParameter?
get() = extensionReceiverParameter
// IrMemberAccessExpression: valueArgumentsCount
val IrMemberAccessExpression<*>.codeQlValueArgumentsCount: Int
get() = valueArgumentsCount
// IrMemberAccessExpression: getValueArgument
fun IrMemberAccessExpression<*>.codeQlGetValueArgument(index: Int): IrExpression? = getValueArgument(index)
// IrMemberAccessExpression: putValueArgument
fun IrMemberAccessExpression<*>.codeQlPutValueArgument(index: Int, value: IrExpression?) {
putValueArgument(index, value)
}
// IrMemberAccessExpression: extensionReceiver
val IrMemberAccessExpression<*>.codeQlExtensionReceiver: IrExpression?
get() = extensionReceiver
// IrMemberAccessExpression: typeArgumentsCount
val IrMemberAccessExpression<*>.codeQlTypeArgumentsCount: Int
get() = typeArgumentsCount
// IrMemberAccessExpression: getTypeArgument
fun IrMemberAccessExpression<*>.codeQlGetTypeArgument(index: Int): IrType? = getTypeArgument(index)
// addAnnotations compat: in pre-2.4.0, addAnnotations expects List<IrConstructorCall>
fun IrType.codeQlAddAnnotations(annotations: List<IrConstructorCall>): IrType =
addAnnotations(annotations)
// IrMutableAnnotationContainer.annotations setter: in pre-2.4.0, annotations is var with List<IrConstructorCall>
fun codeQlSetAnnotations(container: org.jetbrains.kotlin.ir.declarations.IrMutableAnnotationContainer, annotations: List<IrConstructorCall>) {
container.annotations = annotations
}
// IrFunction: set dispatch receiver parameter (pre-2.4.0 it's a var)
fun IrFunction.codeQlSetDispatchReceiverParameter(param: IrValueParameter?) {
dispatchReceiverParameter = param
}
// In pre-2.4.0, annotations are List<IrConstructorCall> so IrConstructorCallImpl works directly.
fun codeQlAnnotationFromSymbolOwner(
startOffset: Int, endOffset: Int, type: IrType, symbol: IrConstructorSymbol, typeArgumentsCount: Int
): IrConstructorCall =
IrConstructorCallImpl.fromSymbolOwner(startOffset, endOffset, type, symbol, typeArgumentsCount)
fun codeQlAnnotationFromSymbolOwner(type: IrType, symbol: IrConstructorSymbol): IrConstructorCall =
IrConstructorCallImpl.fromSymbolOwner(type, symbol)

View File

@@ -3,34 +3,10 @@
package com.github.codeql
import com.intellij.mock.MockProject
import com.intellij.openapi.extensions.LoadingOrder
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.config.CompilerConfiguration
@OptIn(ExperimentalCompilerApi::class)
abstract class Kotlin2ComponentRegistrar : ComponentRegistrar {
/* Nothing to do; supportsK2 doesn't exist yet. */
private var project: MockProject? = null
override fun registerProjectComponents(
project: MockProject,
configuration: CompilerConfiguration
) {
this.project = project
doRegisterExtensions(configuration)
}
abstract fun doRegisterExtensions(configuration: CompilerConfiguration)
fun registerExtractorExtension(extension: IrGenerationExtension) {
val p = project ?: throw IllegalStateException("registerExtractorExtension called before registerProjectComponents")
// Register with LoadingOrder.LAST to ensure the extractor runs after other
// IR generation plugins (like kotlinx.serialization) have generated their code.
val extensionPoint = p.extensionArea.getExtensionPoint(IrGenerationExtension.extensionPointName)
extensionPoint.registerExtension(extension, LoadingOrder.LAST, p)
}
}

View File

@@ -3,35 +3,11 @@
package com.github.codeql
import com.intellij.mock.MockProject
import com.intellij.openapi.extensions.LoadingOrder
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.config.CompilerConfiguration
@OptIn(ExperimentalCompilerApi::class)
abstract class Kotlin2ComponentRegistrar : ComponentRegistrar {
override val supportsK2: Boolean
get() = true
private var project: MockProject? = null
override fun registerProjectComponents(
project: MockProject,
configuration: CompilerConfiguration
) {
this.project = project
doRegisterExtensions(configuration)
}
abstract fun doRegisterExtensions(configuration: CompilerConfiguration)
fun registerExtractorExtension(extension: IrGenerationExtension) {
val p = project ?: throw IllegalStateException("registerExtractorExtension called before registerProjectComponents")
// Register with LoadingOrder.LAST to ensure the extractor runs after other
// IR generation plugins (like kotlinx.serialization) have generated their code.
val extensionPoint = p.extensionArea.getExtensionPoint(IrGenerationExtension.extensionPointName)
extensionPoint.registerExtension(extension, LoadingOrder.LAST, p)
}
}

View File

@@ -1,123 +0,0 @@
@file:Suppress("DEPRECATION")
package com.github.codeql.utils.versions
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
import org.jetbrains.kotlin.ir.expressions.IrAnnotation
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpression
import org.jetbrains.kotlin.ir.expressions.impl.IrAnnotationImpl
import org.jetbrains.kotlin.ir.expressions.impl.fromSymbolOwner
import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.addAnnotations
/**
* Compatibility accessors for pre-2.4.0 API patterns.
* In 2.4.0, valueParameters/extensionReceiverParameter/extensionReceiver/
* getValueArgument/putValueArgument/valueArgumentsCount/typeArgumentsCount/getTypeArgument
* have been removed. This file provides the 2.4.0 implementations.
*/
// IrFunction: valueParameters -> parameters filtered to Regular kind
val IrFunction.codeQlValueParameters: List<IrValueParameter>
get() = parameters.filter { it.kind == org.jetbrains.kotlin.ir.declarations.IrParameterKind.Regular }
// IrFunction: extensionReceiverParameter
val IrFunction.codeQlExtensionReceiverParameter: IrValueParameter?
get() = parameters.firstOrNull { it.kind == org.jetbrains.kotlin.ir.declarations.IrParameterKind.ExtensionReceiver }
// Helper: get the offset of value arguments in the arguments list
private fun IrMemberAccessExpression<*>.valueArgumentOffset(): Int {
val owner = symbol.owner as? IrFunction ?: return 0
return owner.parameters.count { it.kind != org.jetbrains.kotlin.ir.declarations.IrParameterKind.Regular }
}
// IrMemberAccessExpression: valueArgumentsCount
// In 2.4.0, arguments[] includes dispatch/extension receivers before regular params
val IrMemberAccessExpression<*>.codeQlValueArgumentsCount: Int
get() = arguments.size - valueArgumentOffset()
// IrMemberAccessExpression: getValueArgument
// In 2.4.0, arguments[] includes dispatch/extension receivers before regular params
fun IrMemberAccessExpression<*>.codeQlGetValueArgument(index: Int): IrExpression? = arguments[index + valueArgumentOffset()]
// IrMemberAccessExpression: putValueArgument
// In 2.4.0, arguments[] includes dispatch/extension receivers before regular params
fun IrMemberAccessExpression<*>.codeQlPutValueArgument(index: Int, value: IrExpression?) {
arguments[index + valueArgumentOffset()] = value
}
// Re-add accessor for the extensionReceiver property removed in Kotlin 2.4.0.
val IrMemberAccessExpression<*>.codeQlExtensionReceiver: IrExpression?
get() {
val erp = extensionReceiverParameterIndex() ?: return null
return arguments[erp]
}
// Find the argument index corresponding to the extension receiver parameter.
// Calls and function references expose an IrFunction owner directly; property
// references need to look through their getter or setter.
private fun IrMemberAccessExpression<*>.extensionReceiverParameterIndex(): Int? {
// Direct function owner (IrCall, IrFunctionReference, etc.)
(symbol.owner as? IrFunction)?.codeQlExtensionReceiverParameter?.let {
return it.indexInParameters
}
// Property reference: look at getter or setter function
(this as? org.jetbrains.kotlin.ir.expressions.IrPropertyReference)?.let { propRef ->
propRef.getter?.owner?.codeQlExtensionReceiverParameter?.let {
return it.indexInParameters
}
propRef.setter?.owner?.codeQlExtensionReceiverParameter?.let {
return it.indexInParameters
}
}
return null
}
// IrMemberAccessExpression: typeArgumentsCount
val IrMemberAccessExpression<*>.codeQlTypeArgumentsCount: Int
get() = typeArguments.size
// IrMemberAccessExpression: getTypeArgument
fun IrMemberAccessExpression<*>.codeQlGetTypeArgument(index: Int): IrType? = typeArguments[index]
// addAnnotations compat: in 2.4.0, addAnnotations expects List<IrAnnotation>
// IrConstructorCall implements IrAnnotation in 2.4.0, so filterIsInstance is identity
fun IrType.codeQlAddAnnotations(annotations: List<IrConstructorCall>): IrType =
addAnnotations(annotations.filterIsInstance<IrAnnotation>())
// IrMutableAnnotationContainer.annotations setter: in 2.4.0, expects List<IrAnnotation>
fun codeQlSetAnnotations(container: org.jetbrains.kotlin.ir.declarations.IrMutableAnnotationContainer, annotations: List<IrConstructorCall>) {
container.annotations = annotations.filterIsInstance<IrAnnotation>()
}
// IrFunction: set dispatch receiver parameter
// In 2.4.0, dispatchReceiverParameter is val; modify the parameters list directly.
fun IrFunction.codeQlSetDispatchReceiverParameter(param: IrValueParameter?) {
val existing = parameters.indexOfFirst { it.kind == org.jetbrains.kotlin.ir.declarations.IrParameterKind.DispatchReceiver }
val mutableParams = parameters.toMutableList()
if (existing >= 0) {
if (param != null) {
mutableParams[existing] = param
} else {
mutableParams.removeAt(existing)
}
} else if (param != null) {
param.kind = org.jetbrains.kotlin.ir.declarations.IrParameterKind.DispatchReceiver
mutableParams.add(0, param)
}
parameters = mutableParams
}
// In 2.4.0, annotation lists require IrAnnotation instances.
// Use IrAnnotationImpl.fromSymbolOwner instead of IrConstructorCallImpl.fromSymbolOwner.
fun codeQlAnnotationFromSymbolOwner(
startOffset: Int, endOffset: Int, type: IrType, symbol: IrConstructorSymbol, typeArgumentsCount: Int
): IrConstructorCall =
IrAnnotationImpl.fromSymbolOwner(startOffset, endOffset, type, symbol, typeArgumentsCount)
fun codeQlAnnotationFromSymbolOwner(type: IrType, symbol: IrConstructorSymbol): IrConstructorCall =
IrAnnotationImpl.fromSymbolOwner(type, symbol)

View File

@@ -1,45 +0,0 @@
package com.github.codeql
import com.intellij.mock.MockProject
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.jetbrains.kotlin.config.CompilerConfiguration
@OptIn(ExperimentalCompilerApi::class)
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
abstract class Kotlin2ComponentRegistrar :
CompilerPluginRegistrar(),
org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar {
override val supportsK2: Boolean
get() = true
override val pluginId: String
get() = "kotlin-extractor"
// ComponentRegistrar implementation (legacy path, still called by Kotlin compiler)
override fun registerProjectComponents(
project: MockProject,
configuration: CompilerConfiguration
) {
// Registration is done via ExtensionStorage in Kotlin 2.4+.
// This legacy entry point remains for compatibility with service discovery.
}
private var extensionStorage: CompilerPluginRegistrar.ExtensionStorage? = null
override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) {
this@Kotlin2ComponentRegistrar.extensionStorage = this
doRegisterExtensions(configuration)
}
abstract fun doRegisterExtensions(configuration: CompilerConfiguration)
protected fun registerExtractorExtension(extension: IrGenerationExtension) {
val storage = extensionStorage
?: throw IllegalStateException("registerExtractorExtension called before registerExtensions")
with(storage) {
IrGenerationExtension.registerExtension(extension)
}
}
}

View File

@@ -1,13 +0,0 @@
package com.github.codeql.utils.versions
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrParameterKind
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
fun parameterIndexExcludingReceivers(vp: IrValueParameter): Int {
val offset =
(vp.parent as? IrFunction)?.let { f ->
f.parameters.count { it.kind == IrParameterKind.DispatchReceiver || it.kind == IrParameterKind.ExtensionReceiver || it.kind == IrParameterKind.Context }
} ?: 0
return vp.indexInParameters - offset
}

View File

@@ -1 +0,0 @@
com.github.codeql.KotlinExtractorComponentRegistrar

View File

@@ -11,7 +11,6 @@ VERSIONS = [
"2.2.20-Beta2",
"2.3.0",
"2.3.20",
"2.4.0",
]
def _version_to_tuple(v):

View File

@@ -1,5 +1,5 @@
{
"markdownMessage": "The Kotlin version installed (`999.999.999`) is too recent for this version of CodeQL. Install a version lower than 2.4.10.",
"markdownMessage": "The Kotlin version installed (`999.999.999`) is too recent for this version of CodeQL. Install a version lower than 2.3.30.",
"severity": "error",
"source": {
"extractorName": "java",

View File

@@ -1,7 +1,7 @@
import pathlib
def test(codeql, java_full, kotlinc_2_3_20):
def test(codeql, java_full):
java_srcs = " ".join([str(s) for s in pathlib.Path().glob("*.java")])
codeql.database.create(
command=[

View File

@@ -1,6 +1,6 @@
import commands
def test(codeql, java_full, kotlinc_2_3_20):
def test(codeql, java_full):
commands.run("kotlinc -language-version 1.9 test.kt -d lib")
codeql.database.create(command="kotlinc -language-version 1.9 user.kt -cp lib")

View File

@@ -1,2 +1,2 @@
def test(codeql, java_full, kotlinc_2_3_20):
codeql.database.create(command=f"kotlinc -J-Xmx2G -language-version 1.9 SomeClass.kt")
def test(codeql, java_full):
codeql.database.create(command="kotlinc -J-Xmx2G -language-version 1.9 SomeClass.kt")

View File

@@ -1,6 +1,6 @@
import commands
def test(codeql, java_full, kotlinc_2_3_20):
def test(codeql, java_full):
commands.run("kotlinc -language-version 1.9 A.kt")
codeql.database.create(command="kotlinc -cp . -language-version 1.9 B.kt C.kt")

View File

@@ -1,6 +1,6 @@
import commands
def test(codeql, java_full, kotlinc_2_3_20):
def test(codeql, java_full):
commands.run(["javac", "Test.java", "-d", "bin"])
codeql.database.create(command="kotlinc -language-version 1.9 user.kt -cp bin")

View File

@@ -1,7 +1,7 @@
import commands
def test(codeql, java_full, kotlinc_2_3_20):
def test(codeql, java_full):
# Compile the JavaDefns2 copy outside tracing, to make sure the Kotlin view of it matches the Java view seen by the traced javac compilation of JavaDefns.java below.
commands.run(["javac", "JavaDefns2.java"])
codeql.database.create(

View File

@@ -1,4 +0,0 @@
---
category: feature
---
* Kotlin 2.4.0 can now be analysed.

View File

@@ -0,0 +1,2 @@
import semmle.python.controlflow.internal.AstNodeImpl
import ControlFlow::Consistency

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* A new Python control flow graph implementation has been added under `semmle.python.controlflow.internal.Cfg` (backed by `AstNodeImpl.qll`), built on the shared `codeql.controlflow.ControlFlowGraph` library. It is not yet used by the dataflow library or any production query; the legacy CFG in `semmle/python/Flow.qll` remains the default. The new library is exposed for tests and for upcoming migrations.

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* A new SSA adapter has been added under `semmle.python.dataflow.new.internal.SsaImpl`, built on the shared `codeql.ssa.Ssa` library and the new shared CFG (`semmle.python.controlflow.internal.Cfg`). It is not yet used by the dataflow library or any production query; the legacy ESSA SSA in `semmle/python/essa/*` remains the default. The new SSA adapter is exposed for tests and for the upcoming dataflow migration.

View File

@@ -0,0 +1,45 @@
/**
* @name Print CFG (New)
* @description Produces a representation of a file's Control Flow Graph
* using the new shared control flow library.
* This query is used by the VS Code extension.
* @id python/print-cfg
* @kind graph
* @tags ide-contextual-queries/print-cfg
*/
private import python as Py
import semmle.python.controlflow.internal.AstNodeImpl
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 ViewCfgQueryInput implements ControlFlow::ViewCfgQueryInputSig<Py::File> {
predicate selectedSourceFile = selectedSourceFileAlias/0;
predicate selectedSourceLine = selectedSourceLineAlias/0;
predicate selectedSourceColumn = selectedSourceColumnAlias/0;
predicate cfgScopeSpan(
Ast::Callable callable, Py::File file, int startLine, int startColumn, int endLine,
int endColumn
) {
exists(Py::Scope scope |
scope = callable.asScope() and
file = scope.getLocation().getFile() and
scope.getLocation().hasLocationInfo(_, startLine, startColumn, endLine, endColumn)
)
}
}
import ControlFlow::ViewCfgQuery<Py::File, ViewCfgQueryInput>

View File

@@ -1,7 +1,7 @@
overlay[local]
module;
import python
import python as Py
private import semmle.python.internal.CachedStages
private import codeql.controlflow.BasicBlock as BB
@@ -17,7 +17,7 @@ private import codeql.controlflow.BasicBlock as BB
*/
private predicate augstore(ControlFlowNode load, ControlFlowNode store) {
exists(Expr load_store | exists(AugAssign aa | aa.getTarget() = load_store) |
exists(Py::Expr load_store | exists(Py::AugAssign aa | aa.getTarget() = load_store) |
toAst(load) = load_store and
toAst(store) = load_store and
load.strictlyDominates(store)
@@ -25,7 +25,7 @@ private predicate augstore(ControlFlowNode load, ControlFlowNode store) {
}
/** A non-dispatched getNode() to avoid negative recursion issues */
private AstNode toAst(ControlFlowNode n) { py_flow_bb_node(n, result, _, _) }
private Py::AstNode toAst(ControlFlowNode n) { py_flow_bb_node(n, result, _, _) }
/**
* A control flow node. Control flow nodes have a many-to-one relation with syntactic nodes,
@@ -35,19 +35,19 @@ private AstNode toAst(ControlFlowNode n) { py_flow_bb_node(n, result, _, _) }
class ControlFlowNode extends @py_flow_node {
/** Whether this control flow node is a load (including those in augmented assignments) */
predicate isLoad() {
exists(Expr e | e = toAst(this) | py_expr_contexts(_, 3, e) and not augstore(_, this))
exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 3, e) and not augstore(_, this))
}
/** Whether this control flow node is a store (including those in augmented assignments) */
predicate isStore() {
exists(Expr e | e = toAst(this) | py_expr_contexts(_, 5, e) or augstore(_, this))
exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 5, e) or augstore(_, this))
}
/** Whether this control flow node is a delete */
predicate isDelete() { exists(Expr e | e = toAst(this) | py_expr_contexts(_, 2, e)) }
predicate isDelete() { exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 2, e)) }
/** Whether this control flow node is a parameter */
predicate isParameter() { exists(Expr e | e = toAst(this) | py_expr_contexts(_, 4, e)) }
predicate isParameter() { exists(Py::Expr e | e = toAst(this) | py_expr_contexts(_, 4, e)) }
/** Whether this control flow node is a store in an augmented assignment */
predicate isAugStore() { augstore(_, this) }
@@ -57,61 +57,61 @@ class ControlFlowNode extends @py_flow_node {
/** Whether this flow node corresponds to a literal */
predicate isLiteral() {
toAst(this) instanceof Bytes
toAst(this) instanceof Py::Bytes
or
toAst(this) instanceof Dict
toAst(this) instanceof Py::Dict
or
toAst(this) instanceof DictComp
toAst(this) instanceof Py::DictComp
or
toAst(this) instanceof Set
toAst(this) instanceof Py::Set
or
toAst(this) instanceof SetComp
toAst(this) instanceof Py::SetComp
or
toAst(this) instanceof Ellipsis
toAst(this) instanceof Py::Ellipsis
or
toAst(this) instanceof GeneratorExp
toAst(this) instanceof Py::GeneratorExp
or
toAst(this) instanceof Lambda
toAst(this) instanceof Py::Lambda
or
toAst(this) instanceof ListComp
toAst(this) instanceof Py::ListComp
or
toAst(this) instanceof List
toAst(this) instanceof Py::List
or
toAst(this) instanceof Num
toAst(this) instanceof Py::Num
or
toAst(this) instanceof Tuple
toAst(this) instanceof Py::Tuple
or
toAst(this) instanceof Unicode
toAst(this) instanceof Py::Unicode
or
toAst(this) instanceof NameConstant
toAst(this) instanceof Py::NameConstant
}
/** Whether this flow node corresponds to an attribute expression */
predicate isAttribute() { toAst(this) instanceof Attribute }
predicate isAttribute() { toAst(this) instanceof Py::Attribute }
/** Whether this flow node corresponds to an subscript expression */
predicate isSubscript() { toAst(this) instanceof Subscript }
predicate isSubscript() { toAst(this) instanceof Py::Subscript }
/** Whether this flow node corresponds to an import member */
predicate isImportMember() { toAst(this) instanceof ImportMember }
predicate isImportMember() { toAst(this) instanceof Py::ImportMember }
/** Whether this flow node corresponds to a call */
predicate isCall() { toAst(this) instanceof Call }
predicate isCall() { toAst(this) instanceof Py::Call }
/** Whether this flow node is the first in a module */
predicate isModuleEntry() { this.isEntryNode() and toAst(this) instanceof Module }
predicate isModuleEntry() { this.isEntryNode() and toAst(this) instanceof Py::Module }
/** Whether this flow node corresponds to an import */
predicate isImport() { toAst(this) instanceof ImportExpr }
predicate isImport() { toAst(this) instanceof Py::ImportExpr }
/** Whether this flow node corresponds to a conditional expression */
predicate isIfExp() { toAst(this) instanceof IfExp }
predicate isIfExp() { toAst(this) instanceof Py::IfExp }
/** Whether this flow node corresponds to a function definition expression */
predicate isFunction() { toAst(this) instanceof FunctionExpr }
predicate isFunction() { toAst(this) instanceof Py::FunctionExpr }
/** Whether this flow node corresponds to a class definition expression */
predicate isClass() { toAst(this) instanceof ClassExpr }
predicate isClass() { toAst(this) instanceof Py::ClassExpr }
/** Gets a predecessor of this flow node */
ControlFlowNode getAPredecessor() { this = result.getASuccessor() }
@@ -123,25 +123,25 @@ class ControlFlowNode extends @py_flow_node {
ControlFlowNode getImmediateDominator() { py_idoms(this, result) }
/** Gets the syntactic element corresponding to this flow node */
AstNode getNode() { py_flow_bb_node(this, result, _, _) }
Py::AstNode getNode() { py_flow_bb_node(this, result, _, _) }
/** Gets a textual representation of this element. */
cached
string toString() {
Stages::AST::ref() and
// Since modules can have ambigous names, entry nodes can too, if we do not collate them.
exists(Scope s | s.getEntryNode() = this |
exists(Py::Scope s | s.getEntryNode() = this |
result = "Entry node for " + concat( | | s.toString(), ",")
)
or
exists(Scope s | s.getANormalExit() = this | result = "Exit node for " + s.toString())
exists(Py::Scope s | s.getANormalExit() = this | result = "Exit node for " + s.toString())
or
not exists(Scope s | s.getEntryNode() = this or s.getANormalExit() = this) and
not exists(Py::Scope s | s.getEntryNode() = this or s.getANormalExit() = this) and
result = "ControlFlowNode for " + this.getNode().toString()
}
/** Gets the location of this ControlFlowNode */
Location getLocation() { result = this.getNode().getLocation() }
Py::Location getLocation() { result = this.getNode().getLocation() }
/** Whether this flow node is the first in its scope */
predicate isEntryNode() { py_scope_flow(this, _, -1) }
@@ -151,9 +151,9 @@ class ControlFlowNode extends @py_flow_node {
/** Gets the scope containing this flow node */
cached
Scope getScope() {
Py::Scope getScope() {
Stages::AST::ref() and
if this.getNode() instanceof Scope
if this.getNode() instanceof Py::Scope
then
/* Entry or exit node */
result = this.getNode()
@@ -161,7 +161,7 @@ class ControlFlowNode extends @py_flow_node {
}
/** Gets the enclosing module */
Module getEnclosingModule() { result = this.getScope().getEnclosingModule() }
Py::Module getEnclosingModule() { result = this.getScope().getEnclosingModule() }
/** Gets a successor for this node if the relevant condition is True. */
ControlFlowNode getATrueSuccessor() {
@@ -188,7 +188,7 @@ class ControlFlowNode extends @py_flow_node {
}
/** Whether the scope may be exited as a result of this node raising an exception */
predicate isExceptionalExit(Scope s) { py_scope_flow(this, s, 1) }
predicate isExceptionalExit(Py::Scope s) { py_scope_flow(this, s, 1) }
/** Whether this node is a normal (non-exceptional) exit */
predicate isNormalExit() { py_scope_flow(this, _, 0) or py_scope_flow(this, _, 2) }
@@ -236,7 +236,7 @@ class ControlFlowNode extends @py_flow_node {
/* join-ordering helper for `getAChild() */
pragma[noinline]
private ControlFlowNode getExprChild(BasicBlock dom) {
this.getNode().(Expr).getAChildNode() = result.getNode() and
this.getNode().(Py::Expr).getAChildNode() = result.getNode() and
result.getBasicBlock().dominates(dom) and
not this instanceof UnaryExprNode
}
@@ -249,16 +249,16 @@ class ControlFlowNode extends @py_flow_node {
*/
private class AnyNode extends ControlFlowNode {
override AstNode getNode() { result = super.getNode() }
override Py::AstNode getNode() { result = super.getNode() }
}
/** A control flow node corresponding to a call expression, such as `func(...)` */
class CallNode extends ControlFlowNode {
CallNode() { toAst(this) instanceof Call }
CallNode() { toAst(this) instanceof Py::Call }
/** Gets the flow node corresponding to the function expression for the call corresponding to this flow node */
ControlFlowNode getFunction() {
exists(Call c |
exists(Py::Call c |
this.getNode() = c and
c.getFunc() = result.getNode() and
result.getBasicBlock().dominates(this.getBasicBlock())
@@ -267,7 +267,7 @@ class CallNode extends ControlFlowNode {
/** Gets the flow node corresponding to the n'th positional argument of the call corresponding to this flow node */
ControlFlowNode getArg(int n) {
exists(Call c |
exists(Py::Call c |
this.getNode() = c and
c.getArg(n) = result.getNode() and
result.getBasicBlock().dominates(this.getBasicBlock())
@@ -276,7 +276,7 @@ class CallNode extends ControlFlowNode {
/** Gets the flow node corresponding to the named argument of the call corresponding to this flow node */
ControlFlowNode getArgByName(string name) {
exists(Call c, Keyword k |
exists(Py::Call c, Py::Keyword k |
this.getNode() = c and
k = c.getANamedArg() and
k.getValue() = result.getNode() and
@@ -292,7 +292,7 @@ class CallNode extends ControlFlowNode {
result = this.getArgByName(_)
}
override Call getNode() { result = super.getNode() }
override Py::Call getNode() { result = super.getNode() }
predicate isDecoratorCall() {
this.isClassDecoratorCall()
@@ -301,11 +301,11 @@ class CallNode extends ControlFlowNode {
}
predicate isClassDecoratorCall() {
exists(ClassExpr cls | this.getNode() = cls.getADecoratorCall())
exists(Py::ClassExpr cls | this.getNode() = cls.getADecoratorCall())
}
predicate isFunctionDecoratorCall() {
exists(FunctionExpr func | this.getNode() = func.getADecoratorCall())
exists(Py::FunctionExpr func | this.getNode() = func.getADecoratorCall())
}
/** Gets the first tuple (*) argument of this call, if any. */
@@ -323,11 +323,11 @@ class CallNode extends ControlFlowNode {
/** A control flow corresponding to an attribute expression, such as `value.attr` */
class AttrNode extends ControlFlowNode {
AttrNode() { toAst(this) instanceof Attribute }
AttrNode() { toAst(this) instanceof Py::Attribute }
/** Gets the flow node corresponding to the object of the attribute expression corresponding to this flow node */
ControlFlowNode getObject() {
exists(Attribute a |
exists(Py::Attribute a |
this.getNode() = a and
a.getObject() = result.getNode() and
result.getBasicBlock().dominates(this.getBasicBlock())
@@ -339,7 +339,7 @@ class AttrNode extends ControlFlowNode {
* with the matching name
*/
ControlFlowNode getObject(string name) {
exists(Attribute a |
exists(Py::Attribute a |
this.getNode() = a and
a.getObject() = result.getNode() and
a.getName() = name and
@@ -348,57 +348,57 @@ class AttrNode extends ControlFlowNode {
}
/** Gets the attribute name of the attribute expression corresponding to this flow node */
string getName() { exists(Attribute a | this.getNode() = a and a.getName() = result) }
string getName() { exists(Py::Attribute a | this.getNode() = a and a.getName() = result) }
override Attribute getNode() { result = super.getNode() }
override Py::Attribute getNode() { result = super.getNode() }
}
/** A control flow node corresponding to a `from ... import ...` expression */
class ImportMemberNode extends ControlFlowNode {
ImportMemberNode() { toAst(this) instanceof ImportMember }
ImportMemberNode() { toAst(this) instanceof Py::ImportMember }
/**
* Gets the flow node corresponding to the module in the import-member expression corresponding to this flow node,
* with the matching name
*/
ControlFlowNode getModule(string name) {
exists(ImportMember i | this.getNode() = i and i.getModule() = result.getNode() |
exists(Py::ImportMember i | this.getNode() = i and i.getModule() = result.getNode() |
i.getName() = name and
result.getBasicBlock().dominates(this.getBasicBlock())
)
}
override ImportMember getNode() { result = super.getNode() }
override Py::ImportMember getNode() { result = super.getNode() }
}
/** A control flow node corresponding to an artificial expression representing an import */
class ImportExprNode extends ControlFlowNode {
ImportExprNode() { toAst(this) instanceof ImportExpr }
ImportExprNode() { toAst(this) instanceof Py::ImportExpr }
override ImportExpr getNode() { result = super.getNode() }
override Py::ImportExpr getNode() { result = super.getNode() }
}
/** A control flow node corresponding to a `from ... import *` statement */
class ImportStarNode extends ControlFlowNode {
ImportStarNode() { toAst(this) instanceof ImportStar }
ImportStarNode() { toAst(this) instanceof Py::ImportStar }
/** Gets the flow node corresponding to the module in the import-star corresponding to this flow node */
ControlFlowNode getModule() {
exists(ImportStar i | this.getNode() = i and i.getModuleExpr() = result.getNode() |
exists(Py::ImportStar i | this.getNode() = i and i.getModuleExpr() = result.getNode() |
result.getBasicBlock().dominates(this.getBasicBlock())
)
}
override ImportStar getNode() { result = super.getNode() }
override Py::ImportStar getNode() { result = super.getNode() }
}
/** A control flow node corresponding to a subscript expression, such as `value[slice]` */
class SubscriptNode extends ControlFlowNode {
SubscriptNode() { toAst(this) instanceof Subscript }
SubscriptNode() { toAst(this) instanceof Py::Subscript }
/** flow node corresponding to the value of the sequence in a subscript operation */
ControlFlowNode getObject() {
exists(Subscript s |
exists(Py::Subscript s |
this.getNode() = s and
s.getObject() = result.getNode() and
result.getBasicBlock().dominates(this.getBasicBlock())
@@ -407,23 +407,23 @@ class SubscriptNode extends ControlFlowNode {
/** flow node corresponding to the index in a subscript operation */
ControlFlowNode getIndex() {
exists(Subscript s |
exists(Py::Subscript s |
this.getNode() = s and
s.getIndex() = result.getNode() and
result.getBasicBlock().dominates(this.getBasicBlock())
)
}
override Subscript getNode() { result = super.getNode() }
override Py::Subscript getNode() { result = super.getNode() }
}
/** A control flow node corresponding to a comparison operation, such as `x<y` */
class CompareNode extends ControlFlowNode {
CompareNode() { toAst(this) instanceof Compare }
CompareNode() { toAst(this) instanceof Py::Compare }
/** Whether left and right are a pair of operands for this comparison */
predicate operands(ControlFlowNode left, Cmpop op, ControlFlowNode right) {
exists(Compare c, Expr eleft, Expr eright |
predicate operands(ControlFlowNode left, Py::Cmpop op, ControlFlowNode right) {
exists(Py::Compare c, Py::Expr eleft, Py::Expr eright |
this.getNode() = c and left.getNode() = eleft and right.getNode() = eright
|
eleft = c.getLeft() and eright = c.getComparator(0) and op = c.getOp(0)
@@ -436,26 +436,26 @@ class CompareNode extends ControlFlowNode {
right.getBasicBlock().dominates(this.getBasicBlock())
}
override Compare getNode() { result = super.getNode() }
override Py::Compare getNode() { result = super.getNode() }
}
/** A control flow node corresponding to a conditional expression such as, `body if test else orelse` */
class IfExprNode extends ControlFlowNode {
IfExprNode() { toAst(this) instanceof IfExp }
IfExprNode() { toAst(this) instanceof Py::IfExp }
/** flow node corresponding to one of the operands of an if-expression */
ControlFlowNode getAnOperand() { result = this.getAPredecessor() }
override IfExp getNode() { result = super.getNode() }
override Py::IfExp getNode() { result = super.getNode() }
}
/** A control flow node corresponding to an assignment expression such as `lhs := rhs`. */
class AssignmentExprNode extends ControlFlowNode {
AssignmentExprNode() { toAst(this) instanceof AssignExpr }
AssignmentExprNode() { toAst(this) instanceof Py::AssignExpr }
/** Gets the flow node corresponding to the left-hand side of the assignment expression */
ControlFlowNode getTarget() {
exists(AssignExpr a |
exists(Py::AssignExpr a |
this.getNode() = a and
a.getTarget() = result.getNode() and
result.getBasicBlock().dominates(this.getBasicBlock())
@@ -464,27 +464,27 @@ class AssignmentExprNode extends ControlFlowNode {
/** Gets the flow node corresponding to the right-hand side of the assignment expression */
ControlFlowNode getValue() {
exists(AssignExpr a |
exists(Py::AssignExpr a |
this.getNode() = a and
a.getValue() = result.getNode() and
result.getBasicBlock().dominates(this.getBasicBlock())
)
}
override AssignExpr getNode() { result = super.getNode() }
override Py::AssignExpr getNode() { result = super.getNode() }
}
/** A control flow node corresponding to a binary expression, such as `x + y` */
class BinaryExprNode extends ControlFlowNode {
BinaryExprNode() { toAst(this) instanceof BinaryExpr }
BinaryExprNode() { toAst(this) instanceof Py::BinaryExpr }
/** flow node corresponding to one of the operands of a binary expression */
ControlFlowNode getAnOperand() { result = this.getLeft() or result = this.getRight() }
override BinaryExpr getNode() { result = super.getNode() }
override Py::BinaryExpr getNode() { result = super.getNode() }
ControlFlowNode getLeft() {
exists(BinaryExpr b |
exists(Py::BinaryExpr b |
this.getNode() = b and
result.getNode() = b.getLeft() and
result.getBasicBlock().dominates(this.getBasicBlock())
@@ -492,7 +492,7 @@ class BinaryExprNode extends ControlFlowNode {
}
ControlFlowNode getRight() {
exists(BinaryExpr b |
exists(Py::BinaryExpr b |
this.getNode() = b and
result.getNode() = b.getRight() and
result.getBasicBlock().dominates(this.getBasicBlock())
@@ -500,11 +500,11 @@ class BinaryExprNode extends ControlFlowNode {
}
/** Gets the operator of this binary expression node. */
Operator getOp() { result = this.getNode().getOp() }
Py::Operator getOp() { result = this.getNode().getOp() }
/** Whether left and right are a pair of operands for this binary expression */
predicate operands(ControlFlowNode left, Operator op, ControlFlowNode right) {
exists(BinaryExpr b, Expr eleft, Expr eright |
predicate operands(ControlFlowNode left, Py::Operator op, ControlFlowNode right) {
exists(Py::BinaryExpr b, Py::Expr eleft, Py::Expr eright |
this.getNode() = b and left.getNode() = eleft and right.getNode() = eright
|
eleft = b.getLeft() and eright = b.getRight() and op = b.getOp()
@@ -516,20 +516,20 @@ class BinaryExprNode extends ControlFlowNode {
/** A control flow node corresponding to a boolean shortcut (and/or) operation */
class BoolExprNode extends ControlFlowNode {
BoolExprNode() { toAst(this) instanceof BoolExpr }
BoolExprNode() { toAst(this) instanceof Py::BoolExpr }
/** flow node corresponding to one of the operands of a boolean expression */
ControlFlowNode getAnOperand() {
exists(BoolExpr b | this.getNode() = b and result.getNode() = b.getAValue()) and
exists(Py::BoolExpr b | this.getNode() = b and result.getNode() = b.getAValue()) and
this.getBasicBlock().dominates(result.getBasicBlock())
}
override BoolExpr getNode() { result = super.getNode() }
override Py::BoolExpr getNode() { result = super.getNode() }
}
/** A control flow node corresponding to a unary expression: (`+x`), (`-x`) or (`~x`) */
class UnaryExprNode extends ControlFlowNode {
UnaryExprNode() { toAst(this) instanceof UnaryExpr }
UnaryExprNode() { toAst(this) instanceof Py::UnaryExpr }
/**
* Gets flow node corresponding to the operand of a unary expression.
@@ -540,7 +540,7 @@ class UnaryExprNode extends ControlFlowNode {
*/
ControlFlowNode getOperand() { result = this.getAPredecessor() }
override UnaryExpr getNode() { result = super.getNode() }
override Py::UnaryExpr getNode() { result = super.getNode() }
override ControlFlowNode getAChild() { result = this.getAPredecessor() }
}
@@ -555,22 +555,22 @@ class DefinitionNode extends ControlFlowNode {
cached
DefinitionNode() {
Stages::AST::ref() and
exists(Assign a | this.getNode() = a.getATarget())
exists(Py::Assign a | this.getNode() = a.getATarget())
or
exists(AssignExpr a | this.getNode() = a.getTarget())
exists(Py::AssignExpr a | this.getNode() = a.getTarget())
or
exists(AnnAssign a | this.getNode() = a.getTarget() and exists(a.getValue()))
exists(Py::AnnAssign a | this.getNode() = a.getTarget() and exists(a.getValue()))
or
exists(Alias a | this.getNode() = a.getAsname())
exists(Py::Alias a | this.getNode() = a.getAsname())
or
augstore(_, this)
or
// `x, y = 1, 2` where LHS is a combination of list or tuples
exists(Assign a | this.getNode() = list_or_tuple_nested_element(a.getATarget()))
exists(Py::Assign a | this.getNode() = list_or_tuple_nested_element(a.getATarget()))
or
exists(For for | this.getNode() = for.getTarget())
exists(Py::For for | this.getNode() = for.getTarget())
or
exists(Parameter param | this.getNode() = param.asName() and exists(param.getDefault()))
exists(Py::Parameter param | this.getNode() = param.asName() and exists(param.getDefault()))
}
/** flow node corresponding to the value assigned for the definition corresponding to this flow node */
@@ -584,16 +584,16 @@ class DefinitionNode extends ControlFlowNode {
// since the default value for a parameter is evaluated in the same basic block as
// the function definition, but the parameter belongs to the basic block of the function,
// there is no dominance relationship between the two.
exists(Parameter param | this.getNode() = param.asName())
exists(Py::Parameter param | this.getNode() = param.asName())
)
}
}
private Expr list_or_tuple_nested_element(Expr list_or_tuple) {
exists(Expr elt |
elt = list_or_tuple.(Tuple).getAnElt()
private Py::Expr list_or_tuple_nested_element(Py::Expr list_or_tuple) {
exists(Py::Expr elt |
elt = list_or_tuple.(Py::Tuple).getAnElt()
or
elt = list_or_tuple.(List).getAnElt()
elt = list_or_tuple.(Py::List).getAnElt()
|
result = elt
or
@@ -603,12 +603,12 @@ private Expr list_or_tuple_nested_element(Expr list_or_tuple) {
/**
* A control flow node corresponding to a deletion statement, such as `del x`.
* There can be multiple `DeletionNode`s for each `Delete` such that each
* There can be multiple `DeletionNode`s for each `Py::Delete` such that each
* target has own `DeletionNode`. The CFG for `del a, x.y` looks like:
* `NameNode('a') -> DeletionNode -> NameNode('b') -> AttrNode('y') -> DeletionNode`.
*/
class DeletionNode extends ControlFlowNode {
DeletionNode() { toAst(this) instanceof Delete }
DeletionNode() { toAst(this) instanceof Py::Delete }
/** Gets the unique target of this deletion node. */
ControlFlowNode getTarget() { result.getASuccessor() = this }
@@ -617,9 +617,9 @@ class DeletionNode extends ControlFlowNode {
/** A control flow node corresponding to a sequence (tuple or list) literal */
abstract class SequenceNode extends ControlFlowNode {
SequenceNode() {
toAst(this) instanceof Tuple
toAst(this) instanceof Py::Tuple
or
toAst(this) instanceof List
toAst(this) instanceof Py::List
}
/** Gets the control flow node for an element of this sequence */
@@ -632,11 +632,11 @@ abstract class SequenceNode extends ControlFlowNode {
/** A control flow node corresponding to a tuple expression such as `( 1, 3, 5, 7, 9 )` */
class TupleNode extends SequenceNode {
TupleNode() { toAst(this) instanceof Tuple }
TupleNode() { toAst(this) instanceof Py::Tuple }
override ControlFlowNode getElement(int n) {
Stages::AST::ref() and
exists(Tuple t | this.getNode() = t and result.getNode() = t.getElt(n)) and
exists(Py::Tuple t | this.getNode() = t and result.getNode() = t.getElt(n)) and
(
result.getBasicBlock().dominates(this.getBasicBlock())
or
@@ -647,10 +647,10 @@ class TupleNode extends SequenceNode {
/** A control flow node corresponding to a list expression, such as `[ 1, 3, 5, 7, 9 ]` */
class ListNode extends SequenceNode {
ListNode() { toAst(this) instanceof List }
ListNode() { toAst(this) instanceof Py::List }
override ControlFlowNode getElement(int n) {
exists(List l | this.getNode() = l and result.getNode() = l.getElt(n)) and
exists(Py::List l | this.getNode() = l and result.getNode() = l.getElt(n)) and
(
result.getBasicBlock().dominates(this.getBasicBlock())
or
@@ -661,10 +661,10 @@ class ListNode extends SequenceNode {
/** A control flow node corresponding to a set expression, such as `{ 1, 3, 5, 7, 9 }` */
class SetNode extends ControlFlowNode {
SetNode() { toAst(this) instanceof Set }
SetNode() { toAst(this) instanceof Py::Set }
ControlFlowNode getAnElement() {
exists(Set s | this.getNode() = s and result.getNode() = s.getElt(_)) and
exists(Py::Set s | this.getNode() = s and result.getNode() = s.getElt(_)) and
(
result.getBasicBlock().dominates(this.getBasicBlock())
or
@@ -675,20 +675,20 @@ class SetNode extends ControlFlowNode {
/** A control flow node corresponding to a dictionary literal, such as `{ 'a': 1, 'b': 2 }` */
class DictNode extends ControlFlowNode {
DictNode() { toAst(this) instanceof Dict }
DictNode() { toAst(this) instanceof Py::Dict }
/**
* Gets a key of this dictionary literal node, for those items that have keys
* E.g, in {'a':1, **b} this returns only 'a'
*/
ControlFlowNode getAKey() {
exists(Dict d | this.getNode() = d and result.getNode() = d.getAKey()) and
exists(Py::Dict d | this.getNode() = d and result.getNode() = d.getAKey()) and
result.getBasicBlock().dominates(this.getBasicBlock())
}
/** Gets a value of this dictionary literal node */
ControlFlowNode getAValue() {
exists(Dict d | this.getNode() = d and result.getNode() = d.getAValue()) and
exists(Py::Dict d | this.getNode() = d and result.getNode() = d.getAValue()) and
result.getBasicBlock().dominates(this.getBasicBlock())
}
}
@@ -712,21 +712,23 @@ class IterableNode extends ControlFlowNode {
}
}
private AstNode assigned_value(Expr lhs) {
private Py::AstNode assigned_value(Py::Expr lhs) {
/* lhs = result */
exists(Assign a | a.getATarget() = lhs and result = a.getValue())
exists(Py::Assign a | a.getATarget() = lhs and result = a.getValue())
or
/* lhs := result */
exists(AssignExpr a | a.getTarget() = lhs and result = a.getValue())
exists(Py::AssignExpr a | a.getTarget() = lhs and result = a.getValue())
or
/* lhs : annotation = result */
exists(AnnAssign a | a.getTarget() = lhs and result = a.getValue())
exists(Py::AnnAssign a | a.getTarget() = lhs and result = a.getValue())
or
/* import result as lhs */
exists(Alias a | a.getAsname() = lhs and result = a.getValue())
exists(Py::Alias a | a.getAsname() = lhs and result = a.getValue())
or
/* lhs += x => result = (lhs + x) */
exists(AugAssign a, BinaryExpr b | b = a.getOperation() and result = b and lhs = b.getLeft())
exists(Py::AugAssign a, Py::BinaryExpr b |
b = a.getOperation() and result = b and lhs = b.getLeft()
)
or
/*
* ..., lhs, ... = ..., result, ...
@@ -734,31 +736,31 @@ private AstNode assigned_value(Expr lhs) {
* ..., (..., lhs, ...), ... = ..., (..., result, ...), ...
*/
exists(Assign a | nested_sequence_assign(a.getATarget(), a.getValue(), lhs, result))
exists(Py::Assign a | nested_sequence_assign(a.getATarget(), a.getValue(), lhs, result))
or
/* for lhs in seq: => `result` is the `for` node, representing the `iter(next(seq))` operation. */
result.(For).getTarget() = lhs
result.(Py::For).getTarget() = lhs
or
exists(Parameter param | lhs = param.asName() and result = param.getDefault())
exists(Py::Parameter param | lhs = param.asName() and result = param.getDefault())
}
predicate nested_sequence_assign(
Expr left_parent, Expr right_parent, Expr left_result, Expr right_result
Py::Expr left_parent, Py::Expr right_parent, Py::Expr left_result, Py::Expr right_result
) {
exists(Assign a |
exists(Py::Assign a |
a.getATarget().getASubExpression*() = left_parent and
a.getValue().getASubExpression*() = right_parent
) and
exists(int i, Expr left_elem, Expr right_elem |
exists(int i, Py::Expr left_elem, Py::Expr right_elem |
(
left_elem = left_parent.(Tuple).getElt(i)
left_elem = left_parent.(Py::Tuple).getElt(i)
or
left_elem = left_parent.(List).getElt(i)
left_elem = left_parent.(Py::List).getElt(i)
) and
(
right_elem = right_parent.(Tuple).getElt(i)
right_elem = right_parent.(Py::Tuple).getElt(i)
or
right_elem = right_parent.(List).getElt(i)
right_elem = right_parent.(Py::List).getElt(i)
)
|
left_result = left_elem and right_result = right_elem
@@ -769,9 +771,9 @@ predicate nested_sequence_assign(
/** A flow node for a `for` statement. */
class ForNode extends ControlFlowNode {
ForNode() { toAst(this) instanceof For }
ForNode() { toAst(this) instanceof Py::For }
override For getNode() { result = super.getNode() }
override Py::For getNode() { result = super.getNode() }
/** Holds if this `for` statement causes iteration over `sequence` storing each step of the iteration in `target` */
predicate iterates(ControlFlowNode target, ControlFlowNode sequence) {
@@ -782,7 +784,7 @@ class ForNode extends ControlFlowNode {
/** Gets the sequence node for this `for` statement. */
ControlFlowNode getSequence() {
exists(For for |
exists(Py::For for |
toAst(this) = for and
for.getIter() = result.getNode()
|
@@ -792,7 +794,7 @@ class ForNode extends ControlFlowNode {
/** A possible `target` for this `for` statement, not accounting for loop unrolling */
private ControlFlowNode possibleTarget() {
exists(For for |
exists(Py::For for |
toAst(this) = for and
for.getTarget() = result.getNode() and
this.getBasicBlock().dominates(result.getBasicBlock())
@@ -809,11 +811,11 @@ class ForNode extends ControlFlowNode {
/** A flow node for a `raise` statement */
class RaiseStmtNode extends ControlFlowNode {
RaiseStmtNode() { toAst(this) instanceof Raise }
RaiseStmtNode() { toAst(this) instanceof Py::Raise }
/** Gets the control flow node for the exception raised by this raise statement */
ControlFlowNode getException() {
exists(Raise r |
exists(Py::Raise r |
r = toAst(this) and
r.getException() = toAst(result) and
result.getBasicBlock().dominates(this.getBasicBlock())
@@ -827,36 +829,36 @@ class RaiseStmtNode extends ControlFlowNode {
*/
class NameNode extends ControlFlowNode {
NameNode() {
exists(Name n | py_flow_bb_node(this, n, _, _))
exists(Py::Name n | py_flow_bb_node(this, n, _, _))
or
exists(PlaceHolder p | py_flow_bb_node(this, p, _, _))
exists(Py::PlaceHolder p | py_flow_bb_node(this, p, _, _))
}
/** Whether this flow node defines the variable `v`. */
predicate defines(Variable v) {
exists(Name d | this.getNode() = d and d.defines(v)) and
predicate defines(Py::Variable v) {
exists(Py::Name d | this.getNode() = d and d.defines(v)) and
not this.isLoad()
}
/** Whether this flow node deletes the variable `v`. */
predicate deletes(Variable v) { exists(Name d | this.getNode() = d and d.deletes(v)) }
predicate deletes(Py::Variable v) { exists(Py::Name d | this.getNode() = d and d.deletes(v)) }
/** Whether this flow node uses the variable `v`. */
predicate uses(Variable v) {
predicate uses(Py::Variable v) {
this.isLoad() and
exists(Name u | this.getNode() = u and u.uses(v))
exists(Py::Name u | this.getNode() = u and u.uses(v))
or
exists(PlaceHolder u |
this.getNode() = u and u.getVariable() = v and u.getCtx() instanceof Load
exists(Py::PlaceHolder u |
this.getNode() = u and u.getVariable() = v and u.getCtx() instanceof Py::Load
)
or
Scopes::use_of_global_variable(this, v.getScope(), v.getId())
}
string getId() {
result = this.getNode().(Name).getId()
result = this.getNode().(Py::Name).getId()
or
result = this.getNode().(PlaceHolder).getId()
result = this.getNode().(Py::PlaceHolder).getId()
}
/** Whether this is a use of a local variable. */
@@ -868,37 +870,39 @@ class NameNode extends ControlFlowNode {
/** Whether this is a use of a global (including builtin) variable. */
predicate isGlobal() { Scopes::use_of_global_variable(this, _, _) }
predicate isSelf() { exists(SsaVariable selfvar | selfvar.isSelf() and selfvar.getAUse() = this) }
predicate isSelf() {
exists(Py::SsaVariable selfvar | selfvar.isSelf() and selfvar.getAUse() = this)
}
}
/** A control flow node corresponding to a named constant, one of `None`, `True` or `False`. */
class NameConstantNode extends NameNode {
NameConstantNode() { exists(NameConstant n | py_flow_bb_node(this, n, _, _)) }
NameConstantNode() { exists(Py::NameConstant n | py_flow_bb_node(this, n, _, _)) }
/*
* We ought to override uses as well, but that has
* a serious performance impact.
* deprecated predicate uses(Variable v) { none() }
* deprecated predicate uses(Py::Variable v) { none() }
*/
}
/** A control flow node corresponding to a starred expression, `*a`. */
class StarredNode extends ControlFlowNode {
StarredNode() { toAst(this) instanceof Starred }
StarredNode() { toAst(this) instanceof Py::Starred }
ControlFlowNode getValue() { toAst(result) = toAst(this).(Starred).getValue() }
ControlFlowNode getValue() { toAst(result) = toAst(this).(Py::Starred).getValue() }
}
/** The ControlFlowNode for an 'except' statement. */
class ExceptFlowNode extends ControlFlowNode {
ExceptFlowNode() { this.getNode() instanceof ExceptStmt }
ExceptFlowNode() { this.getNode() instanceof Py::ExceptStmt }
/**
* Gets the type handled by this exception handler.
* `ExceptionType` in `except ExceptionType as e:`
* `Py::ExceptionType` in `except Py::ExceptionType as e:`
*/
ControlFlowNode getType() {
exists(ExceptStmt ex |
exists(Py::ExceptStmt ex |
this.getBasicBlock().dominates(result.getBasicBlock()) and
ex = this.getNode() and
result.getNode() = ex.getType()
@@ -907,10 +911,10 @@ class ExceptFlowNode extends ControlFlowNode {
/**
* Gets the name assigned to the handled exception, if any.
* `e` in `except ExceptionType as e:`
* `e` in `except Py::ExceptionType as e:`
*/
ControlFlowNode getName() {
exists(ExceptStmt ex |
exists(Py::ExceptStmt ex |
this.getBasicBlock().dominates(result.getBasicBlock()) and
ex = this.getNode() and
result.getNode() = ex.getName()
@@ -920,30 +924,30 @@ class ExceptFlowNode extends ControlFlowNode {
/** The ControlFlowNode for an 'except*' statement. */
class ExceptGroupFlowNode extends ControlFlowNode {
ExceptGroupFlowNode() { this.getNode() instanceof ExceptGroupStmt }
ExceptGroupFlowNode() { this.getNode() instanceof Py::ExceptGroupStmt }
/**
* Gets the type handled by this exception handler.
* `ExceptionType` in `except* ExceptionType as e:`
* `Py::ExceptionType` in `except* Py::ExceptionType as e:`
*/
ControlFlowNode getType() {
this.getBasicBlock().dominates(result.getBasicBlock()) and
result.getNode() = this.getNode().(ExceptGroupStmt).getType()
result.getNode() = this.getNode().(Py::ExceptGroupStmt).getType()
}
/**
* Gets the name assigned to the handled exception, if any.
* `e` in `except* ExceptionType as e:`
* `e` in `except* Py::ExceptionType as e:`
*/
ControlFlowNode getName() {
this.getBasicBlock().dominates(result.getBasicBlock()) and
result.getNode() = this.getNode().(ExceptGroupStmt).getName()
result.getNode() = this.getNode().(Py::ExceptGroupStmt).getName()
}
}
private module Scopes {
private predicate fast_local(NameNode n) {
exists(FastLocalVariable v |
exists(Py::FastLocalVariable v |
n.uses(v) and
v.getScope() = n.getScope()
)
@@ -952,15 +956,15 @@ private module Scopes {
predicate local(NameNode n) {
fast_local(n)
or
exists(SsaVariable var |
exists(Py::SsaVariable var |
var.getAUse() = n and
n.getScope() instanceof Class and
n.getScope() instanceof Py::Class and
exists(var.getDefinition())
)
}
predicate non_local(NameNode n) {
exists(FastLocalVariable flv |
exists(Py::FastLocalVariable flv |
flv.getALoad() = n.getNode() and
not flv.getScope() = n.getScope()
)
@@ -968,20 +972,20 @@ private module Scopes {
// magic is fine, but we get questionable join-ordering of it
pragma[nomagic]
predicate use_of_global_variable(NameNode n, Module scope, string name) {
predicate use_of_global_variable(NameNode n, Py::Module scope, string name) {
n.isLoad() and
not non_local(n) and
not exists(SsaVariable var | var.getAUse() = n |
var.getVariable() instanceof FastLocalVariable
not exists(Py::SsaVariable var | var.getAUse() = n |
var.getVariable() instanceof Py::FastLocalVariable
or
n.getScope() instanceof Class and
n.getScope() instanceof Py::Class and
not maybe_undefined(var)
) and
name = n.getId() and
scope = n.getEnclosingModule()
}
private predicate maybe_undefined(SsaVariable var) {
private predicate maybe_undefined(Py::SsaVariable var) {
not exists(var.getDefinition()) and not py_ssa_phi(var, _)
or
var.getDefinition().isDelete()
@@ -1058,13 +1062,13 @@ class BasicBlock extends @py_flow_node {
private predicate oneNodeBlock() { this.firstNode() = this.getLastNode() }
private predicate startLocationInfo(string file, int line, int col) {
if this.firstNode().getNode() instanceof Scope
if this.firstNode().getNode() instanceof Py::Scope
then this.firstNode().getASuccessor().getLocation().hasLocationInfo(file, line, col, _, _)
else this.firstNode().getLocation().hasLocationInfo(file, line, col, _, _)
}
private predicate endLocationInfo(int endl, int endc) {
if this.getLastNode().getNode() instanceof Scope and not this.oneNodeBlock()
if this.getLastNode().getNode() instanceof Py::Scope and not this.oneNodeBlock()
then this.getLastNode().getAPredecessor().getLocation().hasLocationInfo(_, _, _, endl, endc)
else this.getLastNode().getLocation().hasLocationInfo(_, _, _, endl, endc)
}
@@ -1081,7 +1085,7 @@ class BasicBlock extends @py_flow_node {
/** Whether flow from this basic block reaches a normal exit from its scope */
predicate reachesExit() {
exists(Scope s | s.getANormalExit().getBasicBlock() = this)
exists(Py::Scope s | s.getANormalExit().getBasicBlock() = this)
or
this.getASuccessor().reachesExit()
}
@@ -1122,7 +1126,7 @@ class BasicBlock extends @py_flow_node {
/** Gets the scope of this block */
pragma[nomagic]
Scope getScope() {
Py::Scope getScope() {
exists(ControlFlowNode n | n.getBasicBlock() = this |
/* Take care not to use an entry or exit node as that node's scope will be the outer scope */
not py_scope_flow(n, _, -1) and
@@ -1145,17 +1149,17 @@ class BasicBlock extends @py_flow_node {
predicate reaches(BasicBlock other) { this = other or this.strictlyReaches(other) }
/**
* Gets the `ConditionBlock`, if any, that controls this block and
* does not control any other `ConditionBlock`s that control this block.
* That is the `ConditionBlock` that is closest dominator.
* Gets the `Py::ConditionBlock`, if any, that controls this block and
* does not control any other `Py::ConditionBlock`s that control this block.
* That is the `Py::ConditionBlock` that is closest dominator.
*/
ConditionBlock getImmediatelyControllingBlock() {
Py::ConditionBlock getImmediatelyControllingBlock() {
result = this.nonControllingImmediateDominator*().getImmediateDominator()
}
private BasicBlock nonControllingImmediateDominator() {
result = this.getImmediateDominator() and
not result.(ConditionBlock).controls(this, _)
not result.(Py::ConditionBlock).controls(this, _)
}
/**
@@ -1175,7 +1179,7 @@ private class ControlFlowNodeAlias = ControlFlowNode;
final private class FinalBasicBlock = BasicBlock;
module Cfg implements BB::CfgSig<Location> {
module Cfg implements BB::CfgSig<Py::Location> {
private import codeql.controlflow.SuccessorType
class ControlFlowNode = ControlFlowNodeAlias;
@@ -1186,7 +1190,7 @@ module Cfg implements BB::CfgSig<Location> {
// Using the location of the first node is simple
// and we just need a way to identify the basic block
// during debugging, so this will be serviceable.
Location getLocation() { result = super.getNode(0).getLocation() }
Py::Location getLocation() { result = super.getNode(0).getLocation() }
int length() { result = count(int i | exists(this.getNode(i))) }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,547 @@
/**
* Provides the Python SSA implementation built on the new (shared) CFG.
*
* Mirrors the Java SSA adapter at
* `java/ql/lib/semmle/code/java/dataflow/internal/SsaImpl.qll`:
* an `InputSig` is defined in terms of positional `(BasicBlock, int)`
* variable references, and the shared
* `codeql.ssa.Ssa::Make<Location, Cfg, Input>` module is then
* instantiated.
*
* `SourceVariable` is the AST-level `Py::Variable`. Variable references
* are looked up via the CFG facade's `NameNode.defines`/`uses`/`deletes`
* predicates, which themselves are one-line bridges to AST-level
* `Name.defines`/`uses`/`deletes`.
*
* Implicit-entry definitions are inserted for:
* - non-local / global / builtin variables that are read in the scope
* but never assigned (no enclosing CFG node defines them),
* - captured variables (variables defined in an enclosing scope that
* are read inside the scope), and
* - parameters, but only if the corresponding parameter name is *not*
* itself a CFG node. With the C#-style parameter wiring already
* installed in `AstNodeImpl.qll`, parameter names *are* CFG nodes,
* so the regular `variableWrite` path handles them — no `i = -1`
* entry is needed for ordinary parameters.
*/
overlay[local?]
module;
private import python as Py
private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
private import semmle.python.controlflow.internal.Cfg as Cfg
private import codeql.ssa.Ssa as SsaImplCommon
private import codeql.controlflow.BasicBlock as BB
/**
* Adapts the Python `Cfg` facade to the shared SSA library's `CfgSig`.
* All members are inherited from `Cfg::ControlFlowNode` and
* `Cfg::BasicBlock`.
*/
private module CfgForSsa implements BB::CfgSig<Py::Location> {
class ControlFlowNode = CfgImpl::ControlFlowNode;
class BasicBlock = CfgImpl::BasicBlock;
class EntryBasicBlock = CfgImpl::Cfg::EntryBasicBlock;
predicate dominatingEdge = CfgImpl::Cfg::dominatingEdge/2;
}
/**
* A source variable for SSA, wrapping a Python AST `Variable`.
*
* We only track variables that are read at least once in their scope —
* tracking write-only variables would be unnecessary work — *except*
* for module-scope globals, where the "read" can be external (e.g.
* `import mymodule; mymodule.x`). Such globals are tracked
* unconditionally so that import-resolution can find their defining
* write.
*/
private newtype TSsaSourceVariable =
TPyVar(Py::Variable v) {
// Has a use somewhere — read-relevant for SSA.
exists(Cfg::NameNode n | n.uses(v))
or
// Or has a deletion (treated as a write that destroys the value).
exists(Cfg::NameNode n | n.deletes(v))
or
// Or is a module-scope global written in this module — must be
// tracked even if never read locally, because importers may read
// it as an attribute on the module object.
v.getScope() instanceof Py::Module and
exists(Cfg::NameNode n | n.defines(v))
or
// Or is a parameter — parameters must always have a
// `ParameterDefinition` for dataflow argument-routing to work,
// even if the parameter is never read in its scope. Mirrors
// legacy ESSA's `ParameterDefinition` (which fired for every
// parameter binding regardless of liveness).
exists(Py::Parameter p | p.asName() = v.getAStore())
}
/**
* A source variable for SSA, wrapping a Python AST `Variable`.
*/
class SsaSourceVariable extends TSsaSourceVariable {
/** Gets the underlying Python AST variable. */
Py::Variable getVariable() { this = TPyVar(result) }
/** Gets the (textual) name of this variable. */
string getName() { result = this.getVariable().getId() }
/** Gets a textual representation of this source variable. */
string toString() { result = this.getVariable().toString() }
/** Gets the location of this source variable. */
Py::Location getLocation() { result = this.getVariable().getScope().getLocation() }
/** Gets the scope in which this variable lives. */
Py::Scope getScope() { result = this.getVariable().getScope() }
/**
* Gets a use of this variable as it appears in the source — a `NameNode`
* that loads or deletes the variable. Mirrors legacy
* `SsaSourceVariable.getASourceUse()`.
*/
Cfg::ControlFlowNode getASourceUse() {
exists(Cfg::NameNode n | result = n |
n.uses(this.getVariable()) or n.deletes(this.getVariable())
)
}
/**
* Gets an implicit use of this variable. The new SSA does not have
* implicit-use refinements, but we keep this for API parity — every
* normal-exit of the variable's scope counts as a sink, ensuring
* variables stay live to scope exit for taint-tracking.
*/
Cfg::ControlFlowNode getAnImplicitUse() {
result.isNormalExit() and result.getScope() = this.getScope()
}
/**
* Gets a use of this variable — either an explicit source use or an
* implicit use at scope exit. Mirrors legacy `SsaSourceVariable.getAUse()`.
*/
Cfg::ControlFlowNode getAUse() {
result = this.getASourceUse() or result = this.getAnImplicitUse()
}
}
/**
* Holds if `v` is a non-local read in scope `s`, in the sense that `s`
* uses `v` but does not write it within `s`. This includes globals,
* builtins, and variables captured from an enclosing function scope.
*
* The `Py::Variable` `v` lives in some defining scope (the module for
* globals, an outer function for closures, etc.); the reading scope
* `s` is the scope where the use of `v` occurs.
*/
private predicate nonLocalReadIn(Py::Variable v, Py::Scope s) {
exists(Cfg::NameNode n |
n.uses(v) and
n.getScope() = s and
not exists(Cfg::NameNode def | def.defines(v) and def.getScope() = s)
) and
// Match legacy ESSA: only create entry defs for variables that have
// at least one defining store somewhere — otherwise the entry def
// represents "nothing reaches here", which is the default anyway and
// introduces no useful flow. (Legacy's `ModuleVariable` required a
// store; this is the closure-aware generalisation.)
exists(Cfg::NameNode store | store.defines(v))
}
/**
* Holds if `bb` is the entry basic block of a scope where `v` should
* have an implicit entry definition. This covers:
* - non-local / global / builtin variables read in `s`, and
* - captured variables (defined in an enclosing scope but read in `s`).
*
* Each reading scope gets its own entry def, so a closure variable can
* have multiple entry defs across all functions/methods that read it.
*
* Parameters are *not* included: their bound `Name` is itself a CFG
* node (per the C#-style parameter wiring), so `variableWrite` fires at
* the parameter's natural CFG index.
*/
private predicate hasEntryDefIn(SsaSourceVariable v, CfgImpl::BasicBlock bb) {
exists(Py::Scope s |
nonLocalReadIn(v.getVariable(), s) and
bb = entryBlock(s)
)
}
/**
* Gets the entry basic block of scope `s`, where implicit entry
* definitions are placed (at synthetic index `-1`).
*/
private CfgImpl::BasicBlock entryBlock(Py::Scope s) {
exists(CfgImpl::ControlFlowNode entry |
entry instanceof CfgImpl::ControlFlow::EntryNode and
entry.getEnclosingCallable().asScope() = s and
result = entry.getBasicBlock()
)
}
/**
* The SSA `InputSig` for Python. References are positional
* `(BasicBlock, int)` pairs into the new CFG.
*/
private module SsaImplInput implements SsaImplCommon::InputSig<Py::Location, CfgImpl::BasicBlock> {
class SourceVariable = SsaSourceVariable;
predicate variableWrite(CfgImpl::BasicBlock bb, int i, SourceVariable v, boolean certain) {
// Explicit binding at a CFG node — includes assignments,
// parameter Names (wired in via the C# pattern), exception-handler
// `as`-bindings, import aliases, and match-pattern captures.
exists(Cfg::NameNode n |
bb.getNode(i) = n and
n.defines(v.getVariable()) and
certain = true
)
or
// `del x` — removes the binding. Modelled as a certain write that
// makes any subsequent read invalid.
exists(Cfg::NameNode n |
bb.getNode(i) = n and
n.deletes(v.getVariable()) and
certain = true
)
or
// Implicit entry definition for non-local / captured / global /
// builtin variables read in some scope. Each reading scope's entry
// block gets one such write, allowing closures: e.g. when `x` is a
// parameter of an outer function and read inside a nested
// function, both scopes get entry defs for `x`.
hasEntryDefIn(v, bb) and
i = -1 and
certain = true
or
// `from X import *` — possibly rebinds every name in the importing
// scope. Modelled as an uncertain write at the import-star's CFG
// position for every variable that lives in (or is referenced
// from) the same scope as the import-star. Mirrors legacy ESSA's
// `ImportStarRefinement` (see `essa/SsaDefinitions.qll`'s
// `import_star_refinement` predicate). The write is uncertain so
// that prior definitions of the variable remain available — the
// shared-SSA `SsaUncertainWrite` merges the new value with the
// immediately preceding definition.
exists(Cfg::ImportStarNode imp |
bb.getNode(i) = imp and
certain = false and
(
v.getVariable().getScope() = imp.getScope()
or
// Variable is defined in some other scope but referenced in
// the same scope as the import-star (matches legacy clause 2:
// `other.uses(v) and def.getScope() = other.getScope()`).
exists(Cfg::NameNode other |
other.uses(v.getVariable()) and
imp.getScope() = other.getScope()
)
)
)
}
predicate variableRead(CfgImpl::BasicBlock bb, int i, SourceVariable v, boolean certain) {
// Explicit source use — a `Name` load or a `del x` of the variable.
exists(Cfg::NameNode n |
bb.getNode(i) = n and
n.uses(v.getVariable()) and
certain = true
)
or
// Synthetic use at the normal exit of the variable's defining scope.
// This keeps every variable live to scope exit so that callers (e.g.
// `module_export` in ImportResolution.qll, or taint-tracking pass-through
// through unread locals) can ask "which definition reaches end of
// scope?". Mirrors legacy ESSA's `SsaSourceVariable.getAUse()` which
// included `getScope().getANormalExit()`.
exists(Cfg::ControlFlowNode exit |
exit.isNormalExit() and
exit.getScope() = v.getVariable().getScope() and
bb.getNode(i) = exit and
certain = true
)
}
}
/**
* The shared SSA instantiation for Python.
*
* Members:
* - `Definition` — the union of explicit, uncertain, and phi definitions
* - `WriteDefinition`, `UncertainWriteDefinition`, `PhiNode`
* - the standard SSA predicates (`getAUse`, `getAnUltimateDefinition`, ...).
*/
module Ssa = SsaImplCommon::Make<Py::Location, CfgForSsa, SsaImplInput>;
final class Definition = Ssa::Definition;
final class WriteDefinition = Ssa::WriteDefinition;
final class UncertainWriteDefinition = Ssa::UncertainWriteDefinition;
final class PhiNode = Ssa::PhiNode;
// ===========================================================================
// ESSA-shaped adapter layer
//
// The dataflow library (`python/ql/lib/semmle/python/dataflow/new/`) and
// related modules (`ApiGraphs.qll`, etc.) consume the legacy ESSA API
// (`EssaVariable`, `EssaDefinition`, `AssignmentDefinition`,
// `ScopeEntryDefinition`, `ParameterDefinition`, `WithDefinition`,
// `PhiFunction`, plus the `AdjacentUses` module). To migrate them off
// the legacy CFG, we expose the same API surface on top of the
// shared SSA built above.
//
// This adapter is intentionally narrow: it covers only the predicates
// that new dataflow consumes. The richer legacy ESSA — refinement
// nodes, attribute refinements, edge refinements — stays available
// via `semmle.python.essa.Essa` for points-to / legacy code.
// ===========================================================================
/**
* Gets the CFG node at which a write definition's binding takes place.
*
* For ordinary writes (assignment, deletion, parameter) this is the
* canonical CFG node of the bound Name. For implicit entry definitions
* (synthesised at position `-1` of a scope's entry BB) this is the
* scope's entry node.
*/
private Cfg::ControlFlowNode writeDefNode(Ssa::WriteDefinition def) {
exists(CfgImpl::BasicBlock bb, int i | def.definesAt(_, bb, i) |
i >= 0 and result = bb.getNode(i)
or
i = -1 and result = bb.getNode(0)
)
}
/**
* A write definition whose binding has a corresponding CFG node — i.e.
* everything that's not a phi node. Mirrors legacy ESSA's
* `EssaNodeDefinition`.
*/
class EssaNodeDefinition extends Ssa::WriteDefinition {
/** Gets the CFG node where this definition's binding takes place. */
Cfg::ControlFlowNode getDefiningNode() { result = writeDefNode(this) }
/** Gets the variable defined here (legacy name). */
SsaSourceVariable getVariable() { result = this.getSourceVariable() }
/** Gets the enclosing scope. */
Py::Scope getScope() {
exists(Cfg::ControlFlowNode n | n = this.getDefiningNode() | result = n.getScope())
}
/**
* Holds if this definition defines source variable `v` at CFG node
* `defNode`. Flatter form of `getSourceVariable()` +
* `getDefiningNode()`, matching legacy ESSA's `definedBy`.
*/
predicate definedBy(SsaSourceVariable v, Cfg::ControlFlowNode defNode) {
v = this.getSourceVariable() and defNode = this.getDefiningNode()
}
}
/**
* An assignment definition: any binding where the value being assigned
* is statically known via `Cfg::DefinitionNode.getValue()`. Includes
* plain assignments, walrus, annotated assignments, augmented
* assignments, import aliases (`import x` / `from m import x [as y]`),
* `with ... as x`, and for-target bindings (where `getValue()` returns
* the iter expression's CFG node). Excludes parameter bindings —
* those are modelled by `ParameterDefinition`.
*/
class AssignmentDefinition extends EssaNodeDefinition {
AssignmentDefinition() {
exists(Cfg::NameNode n | n = this.getDefiningNode() |
exists(n.(Cfg::DefinitionNode).getValue()) and
not n.(Cfg::ControlFlowNode).isParameter()
)
}
/** Gets the CFG node for the value being assigned, if statically known. */
Cfg::ControlFlowNode getValue() {
result = this.getDefiningNode().(Cfg::DefinitionNode).getValue()
}
}
/**
* A parameter definition — the binding of a parameter name in a
* function's scope.
*/
class ParameterDefinition extends EssaNodeDefinition {
ParameterDefinition() { this.getDefiningNode().isParameter() }
/** Gets the AST `Parameter` (a `Py::Name` in param context). */
Py::Name getParameter() { result = this.getDefiningNode().getNode() }
}
/**
* A definition introduced by a `with ... as x:` clause.
*/
class WithDefinition extends EssaNodeDefinition {
WithDefinition() {
exists(Cfg::NameNode n, Py::With w |
n = this.getDefiningNode() and
w.getOptionalVars() = n.getNode()
)
}
}
/**
* An assignment where the LHS is a tuple/list and the RHS is unpacked:
* `a, b = (1, 2)` or `a, *rest = xs`. The SSA def lives at the inner
* `Name` CFG node, but for IterableUnpacking integration we expose
* the enclosing `StarredNode` as the `getDefiningNode()` for `*rest`
* patterns — mirroring legacy ESSA's `multi_assignment_definition`,
* which placed the def at the StarredNode CFG node.
*/
class MultiAssignmentDefinition extends EssaNodeDefinition {
MultiAssignmentDefinition() {
exists(Cfg::NameNode n | n = super.getDefiningNode() |
exists(Py::Assign a, Py::Expr lhs |
a.getATarget() = lhs and
(lhs instanceof Py::Tuple or lhs instanceof Py::List) and
lhs.getASubExpression+() = n.getNode()
)
or
// For-loop with tuple/list target: `for a, b in xs:` —
// tuple-unpacking semantics applies to the for-target.
exists(Py::For f, Py::Expr lhs |
f.getTarget() = lhs and
(lhs instanceof Py::Tuple or lhs instanceof Py::List) and
lhs.getASubExpression+() = n.getNode()
)
)
}
override Cfg::ControlFlowNode getDefiningNode() {
// Default: the underlying `Name` CFG node (where the SSA def lives).
not exists(Cfg::StarredNode s |
s.getNode().(Py::Starred).getValue() = super.getDefiningNode().getNode()
) and
result = super.getDefiningNode()
or
// Exception: for `*rest`, expose the enclosing `Starred` CFG node
// so that `IterableUnpacking::iterableUnpackingStarredElementStoreStep`
// can attach the rest-list to it.
exists(Cfg::StarredNode s |
s.getNode().(Py::Starred).getValue() = super.getDefiningNode().getNode()
|
result = s
)
}
}
/**
* An implicit entry definition for a non-local / captured / global /
* builtin variable read in a scope but not defined there.
*
* Inherits from `EssaNodeDefinition` and exposes the scope's entry node
* as its defining node (matching legacy ESSA semantics).
*/
class ScopeEntryDefinition extends EssaNodeDefinition {
ScopeEntryDefinition() {
exists(CfgImpl::BasicBlock bb |
this.definesAt(_, bb, -1) and
bb instanceof CfgImpl::Cfg::EntryBasicBlock
)
}
/** Gets the enclosing scope (the scope whose entry block this def is in). */
override Py::Scope getScope() {
exists(CfgImpl::BasicBlock bb |
this.definesAt(_, bb, -1) and
result = bb.getNode(0).(Cfg::ControlFlowNode).getScope()
)
}
}
/** A phi node (alias matching legacy naming). */
class PhiFunction extends PhiNode {
/**
* Gets an input to this phi function (a definition that flows into
* the phi from one of its predecessor blocks). Mirrors legacy
* ESSA's `PhiFunction.getAnInput()`.
*/
Ssa::Definition getAnInput() { Ssa::phiHasInputFromBlock(this, result, _) }
}
/** Base class for all ESSA definitions (legacy-shaped). */
class EssaDefinition = Ssa::Definition;
/**
* An adapter representing a single SSA-defined "variable" — wrapping
* one `Ssa::Definition`. Mirrors legacy `EssaVariable` API.
*/
class EssaVariable extends Ssa::Definition {
/** Gets the underlying SSA definition (legacy name). */
Ssa::Definition getDefinition() { result = this }
/**
* Gets a CFG node where this definition is used. Includes regular
* `Name` reads as well as the synthetic scope-exit "use" registered
* via `SsaImplInput::variableRead` — mirrors legacy ESSA's
* `EssaVariable.getAUse()` which inherited the synthetic exit-use
* from `SsaSourceVariable`.
*/
Cfg::ControlFlowNode getAUse() {
exists(CfgImpl::BasicBlock bb, int i |
Ssa::ssaDefReachesRead(this.getSourceVariable(), this, bb, i) and
bb.getNode(i) = result
)
}
/** Gets the (textual) name of the underlying variable. */
string getName() { result = this.getSourceVariable().getVariable().getId() }
/** Gets the scope in which this variable lives. */
Py::Scope getScope() { result = this.getSourceVariable().getVariable().getScope() }
/** Gets an ultimate non-phi ancestor of this definition. */
EssaVariable getAnUltimateDefinition() {
if this instanceof PhiNode
then
exists(Ssa::Definition input |
Ssa::phiHasInputFromBlock(this, input, _) and
result = input.(EssaVariable).getAnUltimateDefinition()
)
else result = this
}
}
/**
* Adjacent use-use and def-use relations exposed by the shared SSA
* library. Provides the same interface as legacy
* `semmle.python.essa.SsaCompute::AdjacentUses`.
*/
module AdjacentUses {
/** Holds if `nodeFrom` and `nodeTo` are adjacent uses of the same SSA variable. */
predicate adjacentUseUse(Cfg::NameNode nodeFrom, Cfg::NameNode nodeTo) {
exists(SsaSourceVariable v, CfgImpl::BasicBlock bb1, int i1, CfgImpl::BasicBlock bb2, int i2 |
Ssa::adjacentUseUse(bb1, i1, bb2, i2, v, _) and
nodeFrom = bb1.getNode(i1) and
nodeTo = bb2.getNode(i2)
)
}
/** Holds if `use` is a first use of definition `def`. */
predicate firstUse(Ssa::Definition def, Cfg::NameNode use) {
exists(CfgImpl::BasicBlock bb, int i |
Ssa::firstUse(def, bb, i, _) and
use = bb.getNode(i)
)
}
/**
* Holds if `use` is any reachable use of definition `def`. Combines
* `firstUse` with transitive use-use adjacency.
*/
predicate useOfDef(Ssa::Definition def, Cfg::NameNode use) {
firstUse(def, use)
or
exists(Cfg::NameNode mid | useOfDef(def, mid) and adjacentUseUse(mid, use))
}
}

View File

@@ -0,0 +1,4 @@
consistencyOverview
| deadEnd | 1 |
deadEnd
| without_loop.py:7:5:7:9 | Break |

View File

@@ -0,0 +1,32 @@
/**
* Phase -1 of the dataflow CFG migration: verifies that every variable
* binding visible to the AST (`Name.defines(v)`) corresponds to a CFG node
* in the new CFG (`semmle.python.controlflow.internal.AstNodeImpl`).
*
* The expected tag is `cfgdefines=<name>`. Each binding annotation in the
* test sources looks like `# $ cfgdefines=x` for a binding currently
* covered by the new CFG, or `# $ MISSING: cfgdefines=x` for a binding
* that is known to be uncovered (a "red" test case that should be
* green-flipped once the corresponding `cfg-ext-*` extension lands).
*/
import python
import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
import utils.test.InlineExpectationsTest
module CfgBindingsTest implements TestSig {
string getARelevantTag() { result = "cfgdefines" }
predicate hasActualResult(Location location, string element, string tag, string value) {
exists(Name n, Variable v, CfgImpl::ControlFlowNode cfg |
n.defines(v) and
cfg.getAstNode().asExpr() = n and
location = n.getLocation() and
element = n.toString() and
tag = "cfgdefines" and
value = v.getId()
)
}
}
import MakeTest<CfgBindingsTest>

View File

@@ -0,0 +1,13 @@
# Annotated assignment (PEP 526). Both with and without an initializer.
a: int = 1 # $ cfgdefines=a
b: str = "hi" # $ cfgdefines=b
# Annotation without value: the AST records `c` as defined,
# and the new CFG now visits it via the AnnAssignStmt wrapper.
c: int # $ cfgdefines=c
class K: # $ cfgdefines=K
field: int = 0 # $ cfgdefines=field

View File

@@ -0,0 +1,14 @@
# Compound (tuple/list) assignment targets — actually wired in the new CFG.
a, b = (1, 2) # $ cfgdefines=a cfgdefines=b
[c, d] = [3, 4] # $ cfgdefines=c cfgdefines=d
# Nested unpacking.
(e, (f, g)) = (1, (2, 3)) # $ cfgdefines=e cfgdefines=f cfgdefines=g
# Star unpacking.
h, *i = [1, 2, 3] # $ cfgdefines=h cfgdefines=i
# Chained assignment with compound target.
j = k, l = (5, 6) # $ cfgdefines=j cfgdefines=k cfgdefines=l

View File

@@ -0,0 +1,21 @@
# Comprehension and `for` loop targets — wired in the new CFG.
# Comprehensions are nested function scopes with a synthetic `.0` parameter
# bound to the iterable.
# Bare-name `for` target.
for i in range(3): # $ cfgdefines=i
pass
# Compound `for` target.
for k, v in [(1, 2)]: # $ cfgdefines=k cfgdefines=v
pass
# Comprehension targets.
_ = [x for x in range(3)] # $ cfgdefines=_ cfgdefines=x cfgdefines=.0
_ = {y: z for y, z in []} # $ cfgdefines=_ cfgdefines=y cfgdefines=z cfgdefines=.0
_ = (a for a in []) # $ cfgdefines=_ cfgdefines=a cfgdefines=.0
# Nested comprehensions.
_ = [b for c in [] for b in c] # $ cfgdefines=_ cfgdefines=c cfgdefines=b cfgdefines=.0

View File

@@ -0,0 +1,53 @@
# Reachability of code following a try whose body always returns.
#
# The new CFG models exception edges for raise-prone expressions when
# they appear inside a `try` (or `with`) statement, mirroring Java's
# `mayThrow`. This means the body of a `try` has both a normal
# completion edge and an exception edge to its handlers, so code
# following the try-statement is reachable via the except-handler path
# even when the try-body would otherwise always return.
#
# Code that is not reachable under either normal or exception flow
# (for example, the `else` clause of a try whose body unconditionally
# raises) remains correctly classified as dead.
def f(obj): # $ cfgdefines=f cfgdefines=obj
try:
return len(obj)
except TypeError:
pass
# The try-body always returns, but `len(obj)` can raise (it is
# inside the try, so we model its exception edge). The
# `except TypeError: pass` handler falls through to here, making
# the code below reachable.
try:
hint = type(obj).__length_hint__ # $ cfgdefines=hint
except AttributeError:
return None
return hint
def g(): # $ cfgdefines=g
try:
raise Exception("inner")
except:
raise Exception("outer")
else:
# Unreachable: the inner try body always raises (via an explicit
# `raise`, which is modelled unconditionally), so the `else:`
# clause never runs.
hit_inner_else = True
def h(cache, key): # $ cfgdefines=h cfgdefines=cache cfgdefines=key
try:
return cache[key]
except KeyError:
pass
# Same pattern as `f`: reachable via the except-handler fall-through.
value = compute(key) # $ cfgdefines=value
cache[key] = value
return value

View File

@@ -0,0 +1,30 @@
# Decorated `def`/`class` — wired in the new CFG.
def deco(f): # $ cfgdefines=deco cfgdefines=f
return f
@deco
def decorated_func(): # $ cfgdefines=decorated_func
pass
@deco
class DecoratedClass: # $ cfgdefines=DecoratedClass
pass
# Stacked decorators.
@deco
@deco
def doubly(): # $ cfgdefines=doubly
pass
# Inside a class body.
class Outer: # $ cfgdefines=Outer
@staticmethod
def inner(): # $ cfgdefines=inner
pass

View File

@@ -0,0 +1,19 @@
# Exception-handler name bindings. These are already wired in the new
# CFG provided the try body can raise; `raise` statements are reliably
# treated as exception sources.
try:
raise ValueError("oops")
except ValueError as e: # $ cfgdefines=e
pass
try:
raise TypeError("oops")
except (TypeError, KeyError) as err: # $ cfgdefines=err
pass
# Exception groups (Python 3.11+).
try:
raise ValueError("oops")
except* ValueError as eg: # $ cfgdefines=eg
pass

View File

@@ -0,0 +1,14 @@
# Import aliases — all bound names below are now reachable via the new
# CFG's `ImportStmt` wrapper.
import os # $ cfgdefines=os
import os.path # $ cfgdefines=os
import os as o # $ cfgdefines=o
from os import path # $ cfgdefines=path
from os import path as p # $ cfgdefines=p
from os import sep, linesep # $ cfgdefines=sep cfgdefines=linesep
from os import (
getcwd, # $ cfgdefines=getcwd
getcwdb, # $ cfgdefines=getcwdb
)

View File

@@ -0,0 +1,24 @@
# Match-statement pattern bindings — wired in the new CFG.
def f(subject): # $ cfgdefines=f cfgdefines=subject
match subject:
case x: # $ cfgdefines=x
pass
case [a, b]: # $ cfgdefines=a cfgdefines=b
pass
case {"k": v}: # $ cfgdefines=v
pass
case Point(p, q): # $ cfgdefines=p cfgdefines=q
pass
case [_, *rest]: # $ cfgdefines=rest
pass
case (1 | 2) as n: # $ cfgdefines=n
pass
class Point: # $ cfgdefines=Point
__match_args__ = ("x", "y") # $ cfgdefines=__match_args__
x: int # $ cfgdefines=x
y: int # $ cfgdefines=y

View File

@@ -0,0 +1,42 @@
# Function parameters.
def positional(a, b): # $ cfgdefines=positional cfgdefines=a cfgdefines=b
pass
def with_default(x=1, y=2): # $ cfgdefines=with_default cfgdefines=x cfgdefines=y
pass
def with_vararg(*args): # $ cfgdefines=with_vararg cfgdefines=args
pass
def with_kwarg(**kwargs): # $ cfgdefines=with_kwarg cfgdefines=kwargs
pass
def with_kwonly(*, k1, k2=5): # $ cfgdefines=with_kwonly cfgdefines=k1 cfgdefines=k2
pass
def kitchen_sink(a, b=2, *args, k1, k2=5, **kw): # $ cfgdefines=kitchen_sink cfgdefines=a cfgdefines=b cfgdefines=args cfgdefines=k1 cfgdefines=k2 cfgdefines=kw
pass
# Methods get `self` / `cls`.
class C: # $ cfgdefines=C
def method(self, x): # $ cfgdefines=method cfgdefines=self cfgdefines=x
pass
@classmethod
def cmethod(cls, x): # $ cfgdefines=cmethod cfgdefines=cls cfgdefines=x
pass
# Lambda parameter.
_ = lambda p: p + 1 # $ cfgdefines=_ cfgdefines=p
# PEP 570 positional-only.
def pos_only(a, b, /, c): # $ cfgdefines=pos_only cfgdefines=a cfgdefines=b cfgdefines=c
pass

View File

@@ -0,0 +1,14 @@
# Simple bindings that should already work in the new CFG.
# No MISSING annotations expected.
x = 1 # $ cfgdefines=x
y = x + 1 # $ cfgdefines=y
def f(): # $ cfgdefines=f
pass
class C: # $ cfgdefines=C
pass
# Re-assignment.
x = 2 # $ cfgdefines=x

View File

@@ -0,0 +1,21 @@
# PEP 695 type parameters (Python 3.12+).
# PEP 695 type-param names on `def`/`class` bind in an annotation scope
# that nests the function/class body — they have no CFG node in the
# enclosing scope (matching the legacy CFG).
def func[T](x: T) -> T: # $ cfgdefines=func cfgdefines=x
return x
class Box[T]: # $ cfgdefines=Box
item: T # $ cfgdefines=item
# Multi-parameter, with bound and variadics.
def multi[T: int, *Ts, **P](x: T, *args: *Ts, **kwargs: P.kwargs) -> T: # $ cfgdefines=multi cfgdefines=x cfgdefines=args cfgdefines=kwargs
return x
# `type` statement (PEP 695).
type Alias[T] = list[T] # $ cfgdefines=Alias cfgdefines=T

View File

@@ -0,0 +1,14 @@
# Walrus and starred-target edge cases — wired in the new CFG.
# Walrus in expression context.
if (y := 5) > 0: # $ cfgdefines=y
pass
# Walrus in a comprehension. The comprehension introduces a synthetic
# `.0` parameter bound to the iterable.
_ = [w for _ in range(3) if (w := 1)] # $ cfgdefines=_ cfgdefines=w cfgdefines=.0
# Starred target in a Tuple LHS.
*head, tail = [1, 2, 3] # $ cfgdefines=head cfgdefines=tail

View File

@@ -0,0 +1,21 @@
# `with cm() as x:` bindings — wired in the new CFG.
class CM: # $ cfgdefines=CM
def __enter__(self): return self # $ cfgdefines=__enter__ cfgdefines=self
def __exit__(self, *a): pass # $ cfgdefines=__exit__ cfgdefines=self cfgdefines=a
with CM() as x: # $ cfgdefines=x
pass
# Multiple items.
with CM() as a, CM() as b: # $ cfgdefines=a cfgdefines=b
pass
# Parenthesised form (Python 3.10+).
with (CM() as p, CM() as q): # $ cfgdefines=p cfgdefines=q
pass
# Compound target in `with`.
with CM() as (m, n): # $ cfgdefines=m cfgdefines=n
pass

View File

@@ -5,6 +5,8 @@
* have separate CFGs and are excluded from this check.
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -2,6 +2,8 @@
* Checks that every timer annotation has a corresponding CFG node.
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -8,6 +8,8 @@
* edge leaves the basic block and the normal successor may be dead.
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -1,7 +1,7 @@
| test_boolean.py:9:10:9:43 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:9:59:9:59 | IntegerLiteral | timestamp 2 | test_boolean.py:9:19:9:19 | IntegerLiteral | timestamp 0 |
| test_boolean.py:15:10:15:43 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:15:50:15:50 | IntegerLiteral | timestamp 1 | test_boolean.py:15:20:15:20 | IntegerLiteral | timestamp 0 |
| test_boolean.py:21:10:21:42 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:21:49:21:49 | IntegerLiteral | timestamp 1 | test_boolean.py:21:19:21:19 | IntegerLiteral | timestamp 0 |
| test_boolean.py:27:10:27:34 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:27:50:27:50 | IntegerLiteral | timestamp 2 | test_boolean.py:27:20:27:20 | IntegerLiteral | timestamp 0 |
| test_boolean.py:27:10:27:43 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:27:59:27:59 | IntegerLiteral | timestamp 2 | test_boolean.py:27:20:27:20 | IntegerLiteral | timestamp 0 |
| test_boolean.py:40:10:40:61 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:40:86:40:86 | IntegerLiteral | timestamp 3 | test_boolean.py:40:16:40:16 | IntegerLiteral | timestamp 0 |
| test_boolean.py:46:10:46:61 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:46:86:46:86 | IntegerLiteral | timestamp 3 | test_boolean.py:46:16:46:16 | IntegerLiteral | timestamp 0 |
| test_boolean.py:52:10:52:95 | ControlFlowNode for BoolExpr | Basic block ordering: $@ appears before $@ | test_boolean.py:52:120:52:120 | IntegerLiteral | timestamp 4 | test_boolean.py:52:20:52:20 | IntegerLiteral | timestamp 0 |

View File

@@ -3,6 +3,8 @@
* increasing minimum-timestamp order.
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -11,6 +11,8 @@
* lambdas that have annotations in nested scopes).
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -4,6 +4,7 @@
* in at least one annotation (live or dead).
*/
import python
import TimerUtils
from TestFunction f, int missing, int maxTs, TimerAnnotation maxAnn

View File

@@ -4,6 +4,8 @@
* entry (including within the same basic block).
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -0,0 +1,14 @@
/** New-CFG version of AllLiveReachable. */
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from TimerCfgNode a, TestFunction f
where allLiveReachable(a, f)
select a, "Unreachable live annotation; entry of $@ does not reach this node", f, f.getName()

View File

@@ -0,0 +1,18 @@
/**
* New-CFG version of AnnotationHasCfgNode.
*
* Checks that every timer annotation has a corresponding CFG node.
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils::CfgTests
from TimerAnnotation ann
where annotationWithoutCfgNode(ann)
select ann, "Annotation in $@ has no CFG node", ann.getTestFunction(),
ann.getTestFunction().getName()

View File

@@ -0,0 +1,26 @@
/**
* New-CFG version of BasicBlockAnnotationGap.
*
* Original:
* Checks that within a basic block, if a node is annotated then its
* successor is also annotated (or excluded). A gap in annotations
* within a basic block indicates a missing annotation, since there
* are no branches to justify the gap.
*
* Nodes with exceptional successors are excluded, as the exception
* edge leaves the basic block and the normal successor may be dead.
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from TimerCfgNode a, CfgNode succ
where basicBlockAnnotationGap(a, succ)
select a, "Annotated node followed by unannotated $@ in the same basic block", succ,
succ.getNode().toString()

View File

@@ -0,0 +1,21 @@
/**
* New-CFG version of BasicBlockOrdering.
*
* Original:
* Checks that within a single basic block, annotations appear in
* increasing minimum-timestamp order.
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from TimerCfgNode a, TimerCfgNode b, int minA, int minB
where basicBlockOrdering(a, b, minA, minB)
select a, "Basic block ordering: $@ appears before $@", a.getTimestampExpr(minA),
"timestamp " + minA, b.getTimestampExpr(minB), "timestamp " + minB

View File

@@ -0,0 +1,80 @@
/**
* New-CFG version of BranchTimestamps.
*
* Checks that when a node has both a true and false successor, the
* live timestamps on one branch are marked as dead on the other.
* This ensures that boolean branches are fully annotated with dead()
* markers for the paths not taken.
*
* Limitation: the `@ t[ts, ...]` / `dead(ts)` annotation scheme can only
* model branch-dead-ness for plain boolean control flow that reconverges
* linearly after the split — i.e. `if`-with-else and `if`-expression.
* It cannot model:
*
* * loops (`while` / `for`): body timestamps repeat across iterations,
* so the loop-exit annotation can't list them as dead;
* * `match` statements: each `case` body is a syntactically distinct
* sub-tree, and the branches don't reconverge through a common
* annotation point in the timeline;
* * `try` / `with` and `raise` / `assert`: exception edges are modelled
* as true/false but flow to syntactically distinct handlers, with no
* reconvergence in the linear annotation order;
* * short-circuit `and` / `or` (`BoolExpr`): the branches reconverge at
* the BoolExpr's after-node, so timestamps on one branch are live
* downstream of the other rather than dead;
* * `if` without an `else` clause, and `if`/`elif` chains: the false
* branch reconverges with the true branch at the post-if statement
* (no-else) or fans out across multiple elif-test annotations,
* neither of which fit the binary annotation scheme.
*
* Branch nodes inside those constructs are therefore whitelisted out
* below. The check still fires (and is useful) for plain `if`/`else`
* and conditional-expression branching.
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
/**
* Holds if `f` contains a construct whose branches the linear-timestamp
* annotation scheme cannot describe (see file-level comment).
*/
private predicate hasUnmodellableBranching(Function f) {
exists(AstNode bad |
bad.getScope() = f and
(
bad instanceof While
or
bad instanceof For
or
bad instanceof MatchStmt
or
bad instanceof Try
or
bad instanceof With
or
bad instanceof Raise
or
bad instanceof Assert
or
bad instanceof BoolExpr
or
bad instanceof If and
(not exists(bad.(If).getAnOrelse()) or bad.(If).isElif())
)
)
}
from TimerCfgNode node, int ts, string branch
where
missingBranchTimestamp(node, ts, branch) and
not hasUnmodellableBranching(node.getTestFunction())
select node,
"Timestamp " + ts + " on true/false branch is missing a dead() annotation on the " + branch +
" successor in $@", node.getTestFunction(), node.getTestFunction().getName()

View File

@@ -0,0 +1,22 @@
/**
* New-CFG version of ConsecutivePredecessorTimestamps.
*
* Checks that each annotated node (except the minimum timestamp) has
* a predecessor annotation with timestamp `a - 1`. This is the reverse
* of ConsecutiveTimestamps: it catches nodes that are reachable but
* arrived at from the wrong place (skipping an intermediate node).
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from TimerAnnotation ann, int a
where consecutivePredecessorTimestamps(ann, a)
select ann, "$@ in $@ has no consecutive predecessor (expected " + (a - 1) + ")",
ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName()

View File

@@ -0,0 +1,29 @@
/**
* New-CFG version of ConsecutiveTimestamps.
*
* Original:
* Checks that consecutive annotated nodes have consecutive timestamps:
* for each annotation with timestamp `a`, some CFG node for that annotation
* must have a next annotation containing `a + 1`.
*
* Handles CFG splitting (e.g., finally blocks duplicated for normal/exceptional
* flow) by checking that at least one split has the required successor.
*
* Only applies to functions where all annotations are in the function's
* own scope (excludes tests with generators, async, comprehensions, or
* lambdas that have annotations in nested scopes).
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from TimerAnnotation ann, int a
where consecutiveTimestamps(ann, a)
select ann, "$@ in $@ has no consecutive successor (expected " + (a + 1) + ")",
ann.getTimestampExpr(a), "Timestamp " + a, ann.getTestFunction(), ann.getTestFunction().getName()

View File

@@ -0,0 +1,101 @@
/**
* Implementation of the evaluation-order CFG signature using the new
* shared control flow graph from AstNodeImpl.
*/
private import python as Py
import TimerUtils
private import semmle.python.controlflow.internal.AstNodeImpl as CfgImpl
private import codeql.controlflow.SuccessorType
private class NewControlFlowNode = CfgImpl::ControlFlowNode;
private class NewBasicBlock = CfgImpl::BasicBlock;
/** New (shared) CFG implementation of the evaluation-order signature. */
module NewCfg implements EvalOrderCfgSig {
class CfgNode instanceof NewControlFlowNode {
// Use the post-order representative for each AST node: the "after" node.
// For simple leaf nodes this is the merged before/after node. For
// post-order expressions this is the TAstNode. For pre-order expressions
// (and/or/not/ternary) this uses an AfterValueNode, which places the
// expression after its operands — matching the timer test expectations.
CfgNode() { NewControlFlowNode.super.isAfter(_) }
string toString() { result = NewControlFlowNode.super.toString() }
Py::Location getLocation() { result = NewControlFlowNode.super.getLocation() }
Py::AstNode getNode() {
result = CfgImpl::astNodeToPyNode(NewControlFlowNode.super.getAstNode())
}
CfgNode getASuccessor() { nextCfgNode(this, result) }
CfgNode getATrueSuccessor() {
NewControlFlowNode.super.isAfterTrue(_) and
// Only where there's also a false branch (true boolean split)
exists(NewControlFlowNode other | other.isAfterFalse(NewControlFlowNode.super.getAstNode())) and
nextCfgNodeFrom(this, result)
}
CfgNode getAFalseSuccessor() {
NewControlFlowNode.super.isAfterFalse(_) and
// Only where there's also a true branch (true boolean split)
exists(NewControlFlowNode other | other.isAfterTrue(NewControlFlowNode.super.getAstNode())) and
nextCfgNodeFrom(this, result)
}
CfgNode getAnExceptionalSuccessor() {
exists(NewControlFlowNode mid |
mid = NewControlFlowNode.super.getAnExceptionSuccessor() and
nextCfgNodeFrom(mid, result)
)
}
Py::Scope getScope() { result = NewControlFlowNode.super.getEnclosingCallable().asScope() }
BasicBlock getBasicBlock() {
exists(NewBasicBlock bb, int i | bb.getNode(i) = this and result = bb)
}
}
/**
* Holds if `next` is the nearest CfgNode reachable from `n` via
* one or more raw CFG successor edges, skipping non-CfgNode intermediaries.
*/
private predicate nextCfgNodeFrom(NewControlFlowNode n, CfgNode next) {
next = n.getASuccessor()
or
exists(NewControlFlowNode mid |
mid = n.getASuccessor() and
not mid instanceof CfgNode and
nextCfgNodeFrom(mid, next)
)
}
/**
* Holds if `next` is the nearest CfgNode successor of `n`,
* skipping synthetic intermediate nodes.
*/
private predicate nextCfgNode(CfgNode n, CfgNode next) { nextCfgNodeFrom(n, next) }
class BasicBlock instanceof NewBasicBlock {
string toString() { result = NewBasicBlock.super.toString() }
CfgNode getNode(int n) { result = NewBasicBlock.super.getNode(n) }
predicate reaches(BasicBlock bb) { this = bb or this.strictlyReaches(bb) }
predicate strictlyReaches(BasicBlock bb) { NewBasicBlock.super.getASuccessor+() = bb }
predicate strictlyDominates(BasicBlock bb) { NewBasicBlock.super.strictlyDominates(bb) }
}
CfgNode scopeGetEntryNode(Py::Scope s) {
exists(CfgImpl::ControlFlow::EntryNode entry |
entry.getEnclosingCallable().asScope() = s and
nextCfgNodeFrom(entry, result)
)
}
}

View File

@@ -0,0 +1,21 @@
/**
* New-CFG version of NeverReachable.
*
* Original:
* Checks that expressions annotated with `t.never` either have no CFG
* node, or if they do, that the node is not reachable from its scope's
* entry (including within the same basic block).
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils::CfgTests
from TimerAnnotation ann
where neverReachable(ann)
select ann, "Node annotated with t.never is reachable in $@", ann.getTestFunction(),
ann.getTestFunction().getName()

View File

@@ -0,0 +1,22 @@
/**
* New-CFG version of NoBackwardFlow.
*
* Original:
* Checks that time never flows backward between consecutive timer annotations
* in the CFG. For each pair of consecutive annotated nodes (A -> B), there must
* exist timestamps a in A and b in B with a < b.
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from TimerCfgNode a, TimerCfgNode b, int minA, int maxB
where noBackwardFlow(a, b, minA, maxB)
select a, "Backward flow: $@ flows to $@ (max timestamp $@)", a.getTimestampExpr(minA),
minA.toString(), b, b.getNode().toString(), b.getTimestampExpr(maxB), maxB.toString()

View File

@@ -0,0 +1,18 @@
/**
* New-CFG version of NoBasicBlock.
*
* Checks that every annotated CFG node belongs to a basic block.
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from CfgNode n, TestFunction f
where noBasicBlock(n, f)
select n, "CFG node in $@ does not belong to any basic block", f, f.getName()

View File

@@ -0,0 +1,21 @@
/**
* New-CFG version of NoSharedReachable.
*
* Original:
* Checks that two annotations sharing a timestamp value are on
* mutually exclusive CFG paths (neither can reach the other).
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from TimerCfgNode a, TimerCfgNode b, int ts
where noSharedReachable(a, b, ts)
select a, "Shared timestamp $@ but this node reaches $@", a.getTimestampExpr(ts), ts.toString(), b,
b.getNode().toString()

View File

@@ -0,0 +1,22 @@
/**
* New-CFG version of StrictForward.
*
* Original:
* Stronger version of NoBackwardFlow: for consecutive annotated nodes
* A -> B that both have a single timestamp (non-loop code) and B does
* NOT dominate A (forward edge), requires max(A) < min(B).
*/
import python
import TimerUtils
import NewCfgImpl
private module Utils = EvalOrderCfgUtils<NewCfg>;
private import Utils
private import Utils::CfgTests
from TimerCfgNode a, TimerCfgNode b, int maxA, int minB
where strictForward(a, b, maxA, minB)
select a, "Strict forward violation: $@ flows to $@", a.getTimestampExpr(maxA), "timestamp " + maxA,
b.getTimestampExpr(minB), "timestamp " + minB

View File

@@ -1,7 +1,7 @@
| test_boolean.py:9:10:9:43 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:9:59:9:59 | IntegerLiteral | 2 | test_boolean.py:9:10:9:13 | ControlFlowNode for True | True | test_boolean.py:9:19:9:19 | IntegerLiteral | 0 |
| test_boolean.py:15:10:15:43 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:15:50:15:50 | IntegerLiteral | 1 | test_boolean.py:15:10:15:14 | ControlFlowNode for False | False | test_boolean.py:15:20:15:20 | IntegerLiteral | 0 |
| test_boolean.py:21:10:21:42 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:21:49:21:49 | IntegerLiteral | 1 | test_boolean.py:21:10:21:13 | ControlFlowNode for True | True | test_boolean.py:21:19:21:19 | IntegerLiteral | 0 |
| test_boolean.py:27:10:27:34 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:27:50:27:50 | IntegerLiteral | 2 | test_boolean.py:27:10:27:14 | ControlFlowNode for False | False | test_boolean.py:27:20:27:20 | IntegerLiteral | 0 |
| test_boolean.py:27:10:27:43 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:27:59:27:59 | IntegerLiteral | 2 | test_boolean.py:27:10:27:14 | ControlFlowNode for False | False | test_boolean.py:27:20:27:20 | IntegerLiteral | 0 |
| test_boolean.py:40:10:40:61 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:40:86:40:86 | IntegerLiteral | 3 | test_boolean.py:40:10:40:10 | ControlFlowNode for IntegerLiteral | IntegerLiteral | test_boolean.py:40:16:40:16 | IntegerLiteral | 0 |
| test_boolean.py:46:10:46:61 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:46:86:46:86 | IntegerLiteral | 3 | test_boolean.py:46:10:46:10 | ControlFlowNode for IntegerLiteral | IntegerLiteral | test_boolean.py:46:16:46:16 | IntegerLiteral | 0 |
| test_boolean.py:52:10:52:95 | ControlFlowNode for BoolExpr | Backward flow: $@ flows to $@ (max timestamp $@) | test_boolean.py:52:120:52:120 | IntegerLiteral | 4 | test_boolean.py:52:11:52:47 | ControlFlowNode for BoolExpr | BoolExpr | test_boolean.py:52:63:52:63 | IntegerLiteral | 2 |

View File

@@ -4,6 +4,8 @@
* exist timestamps a in A and b in B with a < b.
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -2,6 +2,8 @@
* Checks that every annotated CFG node belongs to a basic block.
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -3,6 +3,8 @@
* mutually exclusive CFG paths (neither can reach the other).
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -3,14 +3,14 @@
* Python control flow graph.
*/
private import python as PY
private import python as Py
import TimerUtils
/** Existing Python CFG implementation of the evaluation-order signature. */
module OldCfg implements EvalOrderCfgSig {
class CfgNode = PY::ControlFlowNode;
class CfgNode = Py::ControlFlowNode;
class BasicBlock = PY::BasicBlock;
class BasicBlock = Py::BasicBlock;
CfgNode scopeGetEntryNode(PY::Scope s) { result = s.getEntryNode() }
CfgNode scopeGetEntryNode(Py::Scope s) { result = s.getEntryNode() }
}

View File

@@ -1,7 +1,7 @@
| test_boolean.py:9:10:9:43 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:9:59:9:59 | IntegerLiteral | timestamp 2 | test_boolean.py:9:19:9:19 | IntegerLiteral | timestamp 0 |
| test_boolean.py:15:10:15:43 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:15:50:15:50 | IntegerLiteral | timestamp 1 | test_boolean.py:15:20:15:20 | IntegerLiteral | timestamp 0 |
| test_boolean.py:21:10:21:42 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:21:49:21:49 | IntegerLiteral | timestamp 1 | test_boolean.py:21:19:21:19 | IntegerLiteral | timestamp 0 |
| test_boolean.py:27:10:27:34 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:27:50:27:50 | IntegerLiteral | timestamp 2 | test_boolean.py:27:20:27:20 | IntegerLiteral | timestamp 0 |
| test_boolean.py:27:10:27:43 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:27:59:27:59 | IntegerLiteral | timestamp 2 | test_boolean.py:27:20:27:20 | IntegerLiteral | timestamp 0 |
| test_boolean.py:40:10:40:61 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:40:86:40:86 | IntegerLiteral | timestamp 3 | test_boolean.py:40:16:40:16 | IntegerLiteral | timestamp 0 |
| test_boolean.py:46:10:46:61 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:46:86:46:86 | IntegerLiteral | timestamp 3 | test_boolean.py:46:16:46:16 | IntegerLiteral | timestamp 0 |
| test_boolean.py:52:10:52:95 | ControlFlowNode for BoolExpr | Strict forward violation: $@ flows to $@ | test_boolean.py:52:120:52:120 | IntegerLiteral | timestamp 4 | test_boolean.py:52:63:52:63 | IntegerLiteral | timestamp 2 |

View File

@@ -4,6 +4,8 @@
* NOT dominate A (forward edge), requires max(A) < min(B).
*/
import python
import TimerUtils
import OldCfgImpl
private module Utils = EvalOrderCfgUtils<OldCfg>;

View File

@@ -24,7 +24,7 @@ def test_or_short_circuit(t):
@test
def test_or_both_sides(t):
# False or X — both operands evaluated, result is X
x = (False @ t[0] or 42 @ t[1]) @ t[dead(1), 2]
x = (False @ t[0] or 42 @ t[1, dead(2)]) @ t[dead(1), 2]
@test

Some files were not shown because too many files have changed in this diff Show More