Compare commits

..

42 Commits

Author SHA1 Message Date
Paolo Tranquilli
024130f791 Just: port kotlin tests to new language test definition
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 12:50:11 +02:00
Paolo Tranquilli
fd97208960 Just: use bazel test with -as-test targets for dist building
This avoids reinstalling dists when nothing changed, by leveraging
bazel test's caching behavior with the existing -as-test target
variants.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 12:07:45 +02:00
Paolo Tranquilli
994e5510bd Merge branch 'main' into redsun82/just2 2026-03-10 14:02:14 +01:00
Paolo Tranquilli
0f28502e68 Merge branch 'main' into redsun82/just2 2025-11-14 12:28:00 +01:00
Paolo Tranquilli
469d09c9af Merge branch 'main' into redsun82/just2 2025-08-14 15:05:44 +02:00
Paolo Tranquilli
9d52a08793 Just: add some docs in source 2025-08-12 10:00:17 +02:00
Paolo Tranquilli
db83285c9f Merge branch 'main' into redsun82/just2 2025-08-12 09:59:46 +02:00
Paolo Tranquilli
15e8e4803d Just: run prettier on .ts files 2025-08-12 09:46:27 +02:00
Paolo Tranquilli
c67e1230b6 Just: add README.md 2025-08-12 09:46:00 +02:00
Paolo Tranquilli
f1febac3ec Merge branch 'main' into redsun82/just2 2025-08-11 14:06:16 +02:00
Paolo Tranquilli
4284d66afb Merge branch 'main' into redsun82/just2 2025-07-18 15:27:26 +02:00
Paolo Tranquilli
0e3ee6efd7 Just: reorganize code and revamp formatting 2025-07-18 15:27:12 +02:00
Paolo Tranquilli
365b2ebd6d Merge branch 'main' into redsun82/just2 2025-07-18 14:26:56 +02:00
Paolo Tranquilli
f2a6503efe Just: use bazel directly for dist building 2025-07-18 14:26:48 +02:00
Paolo Tranquilli
7e7afbabcd Merge branch 'main' into redsun82/just2 2025-07-17 16:57:44 +02:00
Paolo Tranquilli
103745b5d2 Just: group just invocations in forwarder and improve logging 2025-07-10 10:34:35 +02:00
Paolo Tranquilli
92672836dc Merge branch 'main' into redsun82/just2 2025-07-10 10:17:03 +02:00
Paolo Tranquilli
c51f2f8780 Merge branch 'main' into redsun82/just2 2025-07-09 14:50:41 +02:00
Paolo Tranquilli
c9cda74195 Just: allow mixing different verb implementations
This allows to mix different verb implementations in a single
invocation, for example:
```
just build ql/rust ql/java
just test ql/rust/ql/test/some/test ql/rust/ql/integrartion-test/some other
```
If a common justfile recipe is found, it is used for all arguments in
one go. If on the other hand no common justfile recipe is found, each
argument is processed separately in sequence.

This does require that any flags passed are compatible with all recipes
involved (like is the case for `--learn` or `--codeql=built` for
language and integration tests).
2025-07-09 14:47:38 +02:00
Paolo Tranquilli
bd003c58a8 Just: add _if_not_on_ci_just helper, and add generation prerequisites 2025-07-08 17:22:27 +02:00
Paolo Tranquilli
08097157fd Merge branch 'main' into redsun82/just2 2025-07-08 16:01:41 +02:00
Paolo Tranquilli
b8b01ce71c Just: rename _build to _build_dist 2025-07-08 15:53:51 +02:00
Paolo Tranquilli
7f72f87204 Just: fix just rust format and similar 2025-07-08 15:50:37 +02:00
Paolo Tranquilli
aa09288462 Just: introduce aliases 2025-07-08 15:49:23 +02:00
Paolo Tranquilli
8ba7efd455 Just: fix mono-argument case for argumentless recipes 2025-07-08 15:41:18 +02:00
Paolo Tranquilli
bb467d4abf Just: fix CI build for rust 2025-07-08 15:40:35 +02:00
Paolo Tranquilli
d987aa67ec Merge branch 'main' into redsun82/just2 2025-07-08 14:06:30 +02:00
Paolo Tranquilli
e8bcbbd6df Just: add language-tests.ts helper 2025-07-08 14:06:14 +02:00
Paolo Tranquilli
acc7e3f32d Just: add generate prerequisite to rust ql tests 2025-07-08 10:52:37 +02:00
Paolo Tranquilli
c4305151c3 Just: simplify forwarder using --justfile 2025-07-08 10:52:15 +02:00
Paolo Tranquilli
fba96c4eae Just: add root lib.just 2025-07-07 17:39:59 +02:00
Paolo Tranquilli
cb652f3dc8 Just: add format to just directory 2025-07-07 17:37:19 +02:00
Paolo Tranquilli
6e14111337 Just: use --all-checks and --codeql special flags, and relativize flags in forwarder 2025-07-07 17:35:06 +02:00
Paolo Tranquilli
d7d7cf920a Just: format ts files 2025-07-07 15:52:53 +02:00
Paolo Tranquilli
a4acf0890e Just: fix ram option for codeql test run 2025-07-07 15:52:20 +02:00
Paolo Tranquilli
812fc2349b Merge branch 'main' into redsun82/just2 2025-07-07 15:50:09 +02:00
Paolo Tranquilli
5b9436a95f Just: fix swift tests 2025-07-04 17:27:43 +02:00
Paolo Tranquilli
4768ebabee Merge branch 'main' into redsun82/just2 2025-07-04 17:22:17 +02:00
Paolo Tranquilli
9e31fb50c8 Just: fix and add windows 2025-07-04 16:07:31 +02:00
Paolo Tranquilli
2dea9da38c Just: add codegen 2025-07-04 13:45:45 +02:00
Paolo Tranquilli
1202af1c5c Just: fix for windows 2025-07-04 13:45:10 +02:00
Paolo Tranquilli
9c284b1778 Just: introduce scaffolding for common verbs, and apply to rust 2025-07-04 12:32:14 +02:00
80 changed files with 1105 additions and 746 deletions

14
.github/commands/rerun.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
---
trigger: rerun
title: Rerun failed internal checks
surfaces:
- pull_request
description: >
Finds all failed internal CI checks for this PR and reruns their failed jobs.
steps:
- type: repository_dispatch
eventType: rerun-workflow
- type: fill
submit_form: true
template: "Rerun has been triggered."

View File

@@ -21,7 +21,7 @@ bazel_dep(name = "rules_java", version = "9.0.3")
bazel_dep(name = "rules_pkg", version = "1.0.1")
bazel_dep(name = "rules_nodejs", version = "6.7.3")
bazel_dep(name = "rules_python", version = "1.9.0")
bazel_dep(name = "rules_shell", version = "0.6.1")
bazel_dep(name = "rules_shell", version = "0.5.0")
bazel_dep(name = "bazel_skylib", version = "1.8.1")
bazel_dep(name = "abseil-cpp", version = "20260107.1", repo_name = "absl")
bazel_dep(name = "nlohmann_json", version = "3.11.3", repo_name = "json")
@@ -29,7 +29,7 @@ bazel_dep(name = "fmt", version = "12.1.0-codeql.1")
bazel_dep(name = "rules_kotlin", version = "2.2.2-codeql.1")
bazel_dep(name = "gazelle", version = "0.47.0")
bazel_dep(name = "rules_dotnet", version = "0.21.5-codeql.1")
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
bazel_dep(name = "googletest", version = "1.14.0.bcr.1")
bazel_dep(name = "rules_rust", version = "0.68.1.codeql.1")
bazel_dep(name = "zstd", version = "1.5.7.bcr.1")

View File

@@ -1663,7 +1663,7 @@ private module Cached {
private predicate compares_ge(
ValueNumber test, Operand left, Operand right, int k, boolean isGe, GuardValue value
) {
compares_lt(test, right, left, 1 - k, isGe, value)
exists(int onemk | k = 1 - onemk | compares_lt(test, right, left, onemk, isGe, value))
}
/** Rearrange various simple comparisons into `left < right + k` form. */

View File

@@ -6,67 +6,117 @@ private import OverlayXml
/**
* Holds always for the overlay variant and never for the base variant.
* This local predicate is used to define local predicates that behave
* differently for the base and overlay variant.
*/
overlay[local]
predicate isOverlay() { databaseMetadata("isOverlay", "true") }
overlay[local]
private string getLocationFilePath(@location_default loc) {
exists(@file file | locations_default(loc, file, _, _, _, _) | files(file, result))
}
/**
* Holds if the TRAP file or tag `t` is reachable from source file `sourceFile`
* in the base (isOverlayVariant=false) or overlay (isOverlayVariant=true) variant.
* Gets the file path for an element with a single location.
*/
overlay[local]
private predicate locallyReachableTrapOrTag(
boolean isOverlayVariant, string sourceFile, @trap_or_tag t
) {
exists(@source_file sf, @trap trap |
(if isOverlay() then isOverlayVariant = true else isOverlayVariant = false) and
source_file_uses_trap(sf, trap) and
source_file_name(sf, sourceFile) and
(t = trap or trap_uses_tag(trap, t))
private string getSingleLocationFilePath(@element e) {
exists(@location_default loc |
var_decls(e, _, _, _, loc)
or
fun_decls(e, _, _, _, loc)
or
type_decls(e, _, loc)
or
namespace_decls(e, _, loc, _)
or
macroinvocations(e, _, loc, _)
or
preprocdirects(e, _, loc)
or
diagnostics(e, _, _, _, _, loc)
or
usings(e, _, loc, _)
or
static_asserts(e, _, _, loc, _)
or
derivations(e, _, _, _, loc)
or
frienddecls(e, _, _, loc)
or
comments(e, _, loc)
or
exprs(e, _, loc)
or
stmts(e, _, loc)
or
initialisers(e, _, _, loc)
or
attributes(e, _, _, _, loc)
or
attribute_args(e, _, _, _, loc)
or
namequalifiers(e, _, _, loc)
or
enumconstants(e, _, _, _, _, loc)
or
type_mentions(e, _, loc, _)
or
lambda_capture(e, _, _, _, _, _, loc)
or
concept_templates(e, _, loc)
|
result = getLocationFilePath(loc)
)
}
/**
* Holds if element `e` is in TRAP file or tag `t`
* in the base (isOverlayVariant=false) or overlay (isOverlayVariant=true) variant.
* Gets the file path for an element with potentially multiple locations.
*/
overlay[local]
private predicate locallyInTrapOrTag(boolean isOverlayVariant, @element e, @trap_or_tag t) {
(if isOverlay() then isOverlayVariant = true else isOverlayVariant = false) and
in_trap_or_tag(e, t)
private string getMultiLocationFilePath(@element e) {
exists(@location_default loc |
var_decls(_, e, _, _, loc)
or
fun_decls(_, e, _, _, loc)
or
type_decls(_, e, loc)
or
namespace_decls(_, e, loc, _)
|
result = getLocationFilePath(loc)
)
}
/**
* A local helper predicate that holds in the base variant and never in the
* overlay variant.
*/
overlay[local]
private predicate isBase() { not isOverlay() }
/**
* Holds if `path` was extracted in the overlay database.
*/
overlay[local]
private predicate overlayHasFile(string path) {
isOverlay() and
files(_, path) and
path != ""
}
/**
* Discards an element from the base variant if:
* - We have knowledge about what TRAP file or tag it is in (in the base).
* - It is not in any overlay TRAP file or tag that is reachable from an overlay source file.
* - For every base TRAP file or tag that contains it and is reachable from a base source file,
* either the source file has changed, or the overlay has redefined the TRAP file or tag,
* or the overlay runner has re-extracted the same source file.
* - It has a single location in a file extracted in the overlay, or
* - All of its locations are in files extracted in the overlay.
*/
overlay[discard_entity]
private predicate discardElement(@element e) {
// If we don't have any knowledge about what TRAP file something
// is in, then we don't want to discard it, so we only consider
// entities that are known to be in a base TRAP file or tag.
locallyInTrapOrTag(false, e, _) and
// Anything that is reachable from an overlay source file should
// not be discarded.
not exists(@trap_or_tag t | locallyInTrapOrTag(true, e, t) |
locallyReachableTrapOrTag(true, _, t)
) and
// Finally, we have to make sure the base variant does not retain it.
// If it is reachable from a base source file, then that is
// sufficient unless either the base source file has changed (in
// particular, been deleted), or the overlay has redefined the TRAP
// file or tag it is in, or the overlay runner has re-extracted the same
// source file (e.g. because a header it includes has changed).
forall(@trap_or_tag t, string sourceFile |
locallyInTrapOrTag(false, e, t) and
locallyReachableTrapOrTag(false, sourceFile, t)
|
overlayChangedFiles(sourceFile) or
locallyReachableTrapOrTag(true, _, t) or
locallyReachableTrapOrTag(true, sourceFile, _)
isBase() and
(
overlayHasFile(getSingleLocationFilePath(e))
or
forex(string path | path = getMultiLocationFilePath(e) | overlayHasFile(path))
)
}

3
java/justfile Normal file
View File

@@ -0,0 +1,3 @@
import '../lib.just'
build: (_build_dist "java")

6
java/ql/justfile Normal file
View File

@@ -0,0 +1,6 @@
import "../../lib.just"
[no-cd]
format *ARGS=".": (_format_ql ARGS)
consistency_queries := source_dir() / "consistency-queries"

View File

@@ -0,0 +1,15 @@
import "../justfile"
base_flags := """\
CODEQL_EXTRACTOR_KOTLIN_DIAGNOSTIC_LIMIT= \
"""
all_checks := default_db_checks + """\
--check-undefined-labels \
--check-repeated-labels \
--check-redefined-labels \
--check-use-before-definition \
--consistency-queries=""" + consistency_queries
[no-cd]
test *ARGS=".": (_codeql_test "java" base_flags all_checks ARGS)

View File

@@ -0,0 +1,16 @@
import "../justfile"
base_flags := """\
CODEQL_EXTRACTOR_KOTLIN_DIAGNOSTIC_LIMIT= \
CODEQL_KOTLIN_LEGACY_TEST_EXTRACTION_KOTLIN2=true \
"""
all_checks := default_db_checks + """\
--check-undefined-labels \
--check-repeated-labels \
--check-redefined-labels \
--check-use-before-definition \
--consistency-queries=""" + consistency_queries
[no-cd]
test *ARGS=".": (_codeql_test "java" base_flags all_checks ARGS)

16
java/ql/test/justfile Normal file
View File

@@ -0,0 +1,16 @@
import "../justfile"
base_flags := """\
CODEQL_EXTRACTOR_KOTLIN_DIAGNOSTIC_LIMIT="\\ " \
"""
all_checks := default_db_checks + """\
--check-undefined-labels \
--check-repeated-labels \
--check-redefined-labels \
--check-use-before-definition \
--consistency-queries=""" + consistency_queries
[no-cd]
test *ARGS=".": (_codeql_test "java" base_flags all_checks ARGS)

4
justfile Normal file
View File

@@ -0,0 +1,4 @@
# see misc/just/README.md for an overview
import 'lib.just'
import 'misc/just/forward.just'

1
lib.just Normal file
View File

@@ -0,0 +1 @@
import "misc/just/lib.just"

5
misc/bazel/justfile Normal file
View File

@@ -0,0 +1,5 @@
import '../just/lib.just'
[no-cd, positional-arguments, no-exit-message]
hello +ARGS:
@echo "hello from bzl" "$@"

View File

@@ -43,7 +43,7 @@ use_repo(apphost_packs_extension, "dotnet.apphost_packs")
bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "bazel_lib", version = "3.0.0")
bazel_dep(name = "rules_shell", version = "0.6.1")
bazel_dep(name = "rules_shell", version = "0.5.0")
# Dev dependencies
bazel_dep(name = "rules_pkg", version = "1.1.0", dev_dependency = True)

5
misc/codegen/justfile Normal file
View File

@@ -0,0 +1,5 @@
import "../just/lib.just"
test *ARGS="": (_bazel "test" "@codeql//misc/codegen/...")
format *ARGS=".": (_format_py ARGS)

44
misc/just/README.md Normal file
View File

@@ -0,0 +1,44 @@
This directory contains an infrastructure for [`just`](https://github.com/casey/just)
recipes that can be used throughout this and the internal repository. In particular we
have common verbs (`build`, `test`, `format`, `lint`, `generate`) that individual parts
of the project can implement, and some common functionality that can be used to that
effect.
# Forwarding
The core of the functionality is given by forwarding. The idea is that:
- if you are in the directory where a verb is implemented, you will get that as per
standard `just` behaviour (possibly using fallback).
- if on the other hand you are beneath it, and you run something like
`just test ql/rust/ql/test/{a,b}`, then a forwarder script finds a common justfile
implementing the verb for all the positional arguments passed there, and then retries
calling `just test` from there. So if `test` is implemented beneath that (in that case,
it is in `rust/ql/test`), it uses that recipe.
- even if there isn't a recipe that is common to all the positional arguments, the
forwarder will still group the arguments in batches using the same recipe. So
`just build ql/rust ql/java`, or
`just test ql/rust/ql/test/some/language/test ql/rust/ql/integration-test/some/integration/test`
will also work, with corresponding recipes run sequentially.
Another point is how launching QL tests can be tweaked:
- by default, the corresponding CLI is built from the internal repo (nothing is done if
working in `codeql` standalone), and no additional database or consistency checks are
made
- `--codeql=built` can be passed to skip the build step (if no changes were made to the
CLI/extractors). This is consistent with the same pytest option
- you can add the additional checks that CI does with `--all-checks` or the `+`
abbreviation. These additional checks are configured in justfiles per language, and
correspond to all the additional checks that CI adds (but that a dev might not want to
run by default).
Some caveats:
- passing arguments with spaces generally doesn't work, although setting arguments with
spaces in `justfile`s (for the base arguments) is supported using escaping as in `\\`.
This is a known limitation of just (see
<https://github.com/casey/just/issues/1988>)
- when running different recipes for the same verb, non-positional arguments need to be
supported by all recipes involved. For example, this will work ok for `--learn` or
`--codeql` options in language and integration tests

19
misc/just/build.just Normal file
View File

@@ -0,0 +1,19 @@
# Helper build recipes
import "defs.just"
# Build the given language-specific CLI distribution
_build_dist LANGUAGE: _require_semmle_code (_maybe_build_dist LANGUAGE)
# Build the language-specific distribution if we are in an internal repository checkout
# Otherwise, do nothing
[no-exit-message]
_maybe_build_dist LANGUAGE: (_if_in_semmle_code ('cd "$SEMMLE_CODE"; tools/bazel test //language-packs:intree-' + LANGUAGE + '-as-test --test_output=all') '# using codeql from PATH, if any')
# Call bazel. Uses our official bazel wrapper if we are in an internal repository checkout
[no-cd, no-exit-message]
_bazel COMMAND *ARGS: (_if_in_semmle_code 'cd "$SEMMLE_CODE"; tools/bazel' 'bazel' COMMAND ARGS)
# Call sembuild (requires an internal repository checkout)
[no-cd, no-exit-message]
_sembuild *ARGS: (_run_in_semmle_code "./build" ARGS)

View File

@@ -0,0 +1,159 @@
import * as child_process from "child_process";
import * as path from "path";
import * as os from "os";
import * as fs from "fs";
const vars = {
just: process.env["JUST_EXECUTABLE"] || "just",
error: process.env["JUST_ERROR"] || "error",
cmd_begin: process.env["CMD_BEGIN"] || "",
cmd_end: process.env["CMD_END"] || "",
semmle_code: process.env["SEMMLE_CODE"],
};
function invoke(
invocation: string[],
options: { cwd?: string; log_prefix?: string } = {},
): number {
const log_prefix =
options.log_prefix && options.log_prefix !== ""
? `${options.log_prefix} `
: "";
console.log(
`${vars.cmd_begin}${log_prefix}${invocation.join(" ")}${vars.cmd_end}`,
);
try {
child_process.execFileSync(invocation[0], invocation.slice(1), {
stdio: "inherit",
cwd: options.cwd,
});
} catch (error) {
return 1;
}
return 0;
}
type Args = {
tests: string[];
flags: string[];
env: string[];
codeql: string;
all: boolean;
};
const old_console_error = console.error;
console.error = (message: string) => {
old_console_error(vars.error + message);
};
function parseArgs(args: Args, argv: string) {
argv.split(/(?<!\\) /)
.map((arg) => arg.replace("\\ ", " "))
.forEach((arg) => {
if (arg.startsWith("--codeql=")) {
args.codeql = arg.split("=")[1];
} else if (arg === "+" || arg === "--all-checks") {
args.all = true;
} else if (arg.startsWith("-")) {
args.flags.push(arg);
} else if (/^[A-Z_][A-Z_0-9]*=.*$/.test(arg)) {
args.env.push(arg);
} else if (arg !== "") {
args.tests.push(arg);
}
});
}
function codeqlTestRun(argv: string[]): number {
const [language, base_args, all_args, extra_args] = argv;
const ram_per_thread = process.platform === "linux" ? 3000 : 2048;
const cpus = os.cpus().length;
let args: Args = {
tests: [],
flags: [`--ram=${ram_per_thread * cpus}`, `-j${cpus}`],
env: [],
codeql: vars.semmle_code ? "build" : "host",
all: false,
};
parseArgs(args, base_args);
parseArgs(args, extra_args);
if (args.all) {
parseArgs(args, all_args);
}
if (
!vars.semmle_code &&
(args.codeql === "build" || args.codeql === "built")
) {
console.error(
"Using `--codeql=build` or `--codeql=built` requires working with the internal repository",
);
return 1;
}
if (args.tests.length === 0) {
args.tests.push(".");
}
if (args.codeql === "build") {
if (
invoke([vars.just, language, "build"], {
cwd: vars.semmle_code,
}) !== 0
) {
return 1;
}
}
if (args.codeql !== "host") {
// disable the default implicit config file, but keep an explicit one
// this is the same behavior wrt to `--codeql` as the integration test runner
process.env["CODEQL_CONFIG_FILE"] ||= ".";
}
// Set and unset environment variables
args.env.forEach((envVar) => {
const [key, value] = envVar.split("=", 2);
if (key) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
} else {
console.error(`Invalid environment variable assignment: ${envVar}`);
process.exit(1);
}
});
let codeql;
function check_codeql() {
if (!fs.existsSync(codeql)) {
console.error(`CodeQL executable not found: ${codeql}`);
process.exit(1);
}
}
if (args.codeql === "built" || args.codeql === "build") {
codeql = path.join(
vars.semmle_code!,
"target",
"intree",
`codeql-${language}`,
"codeql",
);
check_codeql();
} else if (args.codeql === "host") {
codeql = "codeql";
} else {
codeql = args.codeql;
check_codeql();
}
if (fs.lstatSync(codeql).isDirectory()) {
codeql = path.join(codeql, "codeql");
if (process.platform === "win32") {
codeql += ".exe";
}
check_codeql();
}
return invoke([codeql, "test", "run", ...args.flags, "--", ...args.tests], {
log_prefix: args.env.join(" "),
});
}
process.exit(codeqlTestRun(process.argv.slice(2)));

58
misc/just/defs.just Normal file
View File

@@ -0,0 +1,58 @@
import? '../../../semmle-code.just' # internal repo just file, if present
import 'semmle-code-stub.just'
set fallback
set allow-duplicate-recipes
set allow-duplicate-variables
set unstable
export PATH_SEP := if os() == "windows" { ";" } else { ":" }
export JUST_EXECUTABLE := just_executable()
error := style("error") + "error" + NORMAL + ": "
cmd_sep := "\n#--------------------------------------------------------\n"
export CMD_BEGIN := style("command") + cmd_sep
export CMD_END := cmd_sep + NORMAL
export JUST_ERROR := error
tsx := "npx tsx@4.19.0"
default_db_checks := """\
--check-databases \
--check-diff-informed \
--fail-on-trap-errors \
"""
[no-exit-message]
@_require_semmle_code:
{{ if SEMMLE_CODE == "" { '''
echo "''' + error + ''' running this recipe requires doing so from an internal repository checkout" >&2
exit 1
''' } else { "" } }}
[no-cd]
_run +ARGS:
{{ cmd_sep }}{{ ARGS }}{{ cmd_sep }}
[no-cd]
_run_in DIR +ARGS:
{{ cmd_sep }}cd "{{ DIR }}"; {{ ARGS }}{{ cmd_sep }}
[no-cd]
_run_in_semmle_code +ARGS: _require_semmle_code (_run_in "$SEMMLE_CODE" ARGS)
[no-cd, positional-arguments, no-exit-message]
@_just +ARGS:
echo "-> just $@"
"{{ JUST_EXECUTABLE }}" "$@"
[no-cd, positional-arguments]
@_if_not_on_ci_just +ARGS:
if [ "${GITHUB_ACTIONS:-}" != "true" ]; then \
echo "-> just $@"; \
"$JUST_EXECUTABLE" "$@"; \
fi
[no-cd, no-exit-message]
_if_in_semmle_code THEN ELSE *ARGS:
{{ cmd_sep }}{{ if SEMMLE_CODE != "" { THEN } else { ELSE } }} {{ ARGS }}{{ cmd_sep }}

20
misc/just/format.just Normal file
View File

@@ -0,0 +1,20 @@
import "build.just"
[no-cd, no-exit-message]
_format_ql +ARGS: (_maybe_build_dist "nolang") (
_if_in_semmle_code
'"$SEMMLE_CODE/target/intree/codeql-nolang/codeql"'
'codeql'
("query format --in-place -v $(find " + ARGS + " -type f -name '*.ql' -or -name '*.qll')")
)
[no-cd, no-exit-message]
_format_py *ARGS=".": (_if_in_semmle_code "uv run black" "black" ARGS)
[no-cd, no-exit-message]
_format_cpp *ARGS=".": (
_if_in_semmle_code
"uv run clang-format"
"clang-format"
"-i --verbose $(find " + ARGS + " -type f -name '*.h' -or -name '*.cpp')"
)

View File

@@ -0,0 +1,131 @@
import * as child_process from "child_process";
import * as path from "path";
import * as fs from "fs";
const vars = {
just: process.env["JUST_EXECUTABLE"] || "just",
error: process.env["JUST_ERROR"] || "",
};
console.debug = (...args: any[]) => {}; // comment out to debug script
const old_console_error = console.error;
console.error = (message: string) => {
old_console_error(vars.error + message);
};
function checkJustCommand(
justfile: string,
command: string,
postitionalArgs: string[],
): boolean {
if (!fs.existsSync(justfile)) {
return false;
}
let { cwd, args } = getJustContext(justfile, command, [], postitionalArgs);
console.debug(
`Checking: ${cwd ? `cd ${cwd}; ` : ""}just ${args.join(", ")}`,
);
const res = child_process.spawnSync(vars.just, ["--dry-run", ...args], {
stdio: ["ignore", "ignore", "pipe"],
encoding: "utf8",
cwd,
});
console.debug("result:", res);
// avoid having the forwarder find itself
return (
res.status === 0 &&
!res.stderr.includes(`forward-command.ts" ${command} "$@"`)
);
}
function findJustfile(command: string, arg: string): string | undefined {
for (let p = arg; ; p = path.dirname(p)) {
const candidate = path.join(p, "justfile");
if (checkJustCommand(candidate, command, [arg])) {
return candidate;
}
if (p === "/" || p === ".") {
return undefined;
}
}
}
function forwardCommand(args: string[]): number {
if (args.length == 0) {
console.error("No command provided");
return 1;
}
return forward(args[0], args.slice(1));
}
function forward(cmd: string, args: string[]): number {
// non-positional arguments are flags, + (used by language tests) or environment variable settings
const is_non_positional = /^(-.*|\+|[A-Z_][A-Z_0-9]*=.*)$/;
const flags = args.filter((arg) => is_non_positional.test(arg));
const positionalArgs = args.filter((arg) => !is_non_positional.test(arg));
let justfiles: Map<string, string[]> = new Map();
for (const arg of positionalArgs.length > 0 ? positionalArgs : ["."]) {
const justfile = findJustfile(cmd, arg);
if (!justfile) {
console.error(`No justfile found for ${cmd} on ${arg}`);
return 1;
}
justfiles.set(justfile, [...(justfiles.get(justfile) || []), arg]);
}
const invocations = Array.from(justfiles.entries()).map(
([justfile, positionalArgs]) => {
const { cwd, args } = getJustContext(
justfile,
cmd,
flags,
positionalArgs,
);
console.log(`-> ${cwd ? `cd ${cwd}; ` : ""}just ${args.join(" ")}`);
return { cwd, args };
},
);
for (const { cwd, args } of invocations) {
if (invokeJust(cwd, args) !== 0) {
return 1;
}
}
return 0;
}
function getJustContext(
justfile: string,
cmd: string,
flags: string[],
positionalArgs: string[],
): { args: string[]; cwd?: string } {
if (
positionalArgs.length === 1 &&
justfile == path.join(positionalArgs[0], "justfile")
) {
// If there's only one positional argument and it matches the justfile path, suppress arguments
// so for example `just build ql/rust` becomes `just build` in the `ql/rust` directory
return {
cwd: positionalArgs[0],
args: [cmd, ...flags],
};
} else {
return {
cwd: undefined,
args: ["--justfile", justfile, cmd, ...flags, ...positionalArgs],
};
}
}
function invokeJust(cwd: string | undefined, args: string[]): number {
try {
child_process.execFileSync(vars.just, args, {
stdio: "inherit",
cwd,
});
} catch (error) {
return 1;
}
return 0;
}
process.exit(forwardCommand(process.argv.slice(2)));

38
misc/just/forward.just Normal file
View File

@@ -0,0 +1,38 @@
# Common verbs
# See README.md in this directory for an overview.
import "lib.just"
# copy&paste necessary for each command until proper forwarding of multiple args is implemented
# see https://github.com/casey/just/issues/1988
_forward := tsx + ' "' + source_dir() + '/forward-command.ts"'
alias t := test
alias b := build
alias g := generate
alias gen := generate
alias f := format
alias l := lint
[no-cd, positional-arguments, no-exit-message]
@test *ARGS:
{{ _forward }} test "$@"
[no-cd, positional-arguments, no-exit-message]
@build *ARGS:
{{ _forward }} build "$@"
[no-cd, positional-arguments, no-exit-message]
@generate *ARGS:
{{ _forward }} generate "$@"
[no-cd, positional-arguments, no-exit-message]
@lint *ARGS:
{{ _forward }} lint "$@"
[no-cd, positional-arguments, no-exit-message]
@format *ARGS:
{{ _forward }} format "$@"

2
misc/just/justfile Normal file
View File

@@ -0,0 +1,2 @@
format *ARGS=".":
npx prettier --write {{ ARGS }}

View File

@@ -0,0 +1,33 @@
import * as path from "path";
import * as process from "process";
import * as child_process from "child_process";
function languageTests(argv: string[]): number {
const [extra_args, dir, ...relativeRoots] = argv;
const semmle_code = process.env["SEMMLE_CODE"]!;
let roots = relativeRoots.map((root) =>
path.relative(semmle_code, path.join(dir, root)),
);
const invocation = [
process.env["JUST_EXECUTABLE"] || "just",
"--justfile",
path.join(roots[0], "justfile"),
"test",
"--all-checks",
"--codeql=built",
...extra_args.split(" "),
...roots,
];
console.log(`-> just ${invocation.slice(1).join(" ")}`);
try {
child_process.execFileSync(invocation[0], invocation.slice(1), {
stdio: "inherit",
cwd: semmle_code,
});
} catch (error) {
return 1;
}
return 0;
}
process.exit(languageTests(process.argv.slice(2)));

47
misc/just/lib.just Normal file
View File

@@ -0,0 +1,47 @@
# Helper recipes
import "build.just"
import "format.just"
# Run language tests. `BASE_FLAGS` are used by default, `ALL_CHECK_FLAGS` are used if
# `--all-checks` or `+` is passed along in `EXTRA_ARGS`
[no-cd, positional-arguments, no-exit-message]
@_codeql_test LANGUAGE BASE_FLAGS ALL_CHECKS_FLAGS EXTRA_ARGS:
{{ tsx }} "{{ source_dir() }}/codeql-test-run.ts" "$@"
# Run all language tests in ROOTS for a language. This is intended to be called by CI
[no-cd, positional-arguments, no-exit-message]
@_language_tests EXTRA_ARGS SOURCE_DIR +ROOTS: _require_semmle_code
{{ tsx }} "{{ source_dir() }}/language-tests.ts" "$@"
# Run integration tests. Requires an internal repository checkout
[no-cd, no-exit-message]
_integration_test *ARGS: _require_semmle_code (_run "$SEMMLE_CODE/tools/pytest" "--codeql=build-as-test" ARGS)
# Generic run command recipe that can be used by other recipes, with nice rendering
[no-cd]
_run +ARGS:
{{ cmd_sep }}{{ ARGS }}{{ cmd_sep }}
# Run a command in a specific directory
[no-cd]
_run_in DIR +ARGS:
{{ cmd_sep }}cd "{{ DIR }}"; {{ ARGS }}{{ cmd_sep }}
# Run a command in the internal repository checkout
[no-cd]
_run_in_semmle_code +ARGS: _require_semmle_code (_run_in "$SEMMLE_CODE" ARGS)
# Run a just recipe
[no-cd, positional-arguments, no-exit-message]
@_just +ARGS:
echo "-> just $@"
"{{ JUST_EXECUTABLE }}" "$@"
# Run a just recipe, but only if we are not on CI
[no-cd, positional-arguments]
@_if_not_on_ci_just +ARGS:
if [ "${GITHUB_ACTIONS:-}" != "true" ]; then \
echo "-> just $@"; \
"$JUST_EXECUTABLE" "$@"; \
fi

View File

@@ -0,0 +1 @@
export SEMMLE_CODE := ""

View File

@@ -32,9 +32,7 @@ module Builtins {
"UnicodeDecodeError", "UnicodeEncodeError", "UnicodeError", "UnicodeTranslateError",
"UnicodeWarning", "UserWarning", "ValueError", "Warning", "ZeroDivisionError",
// Added for compatibility
"exec",
// Added by the `site` module (available by default unless `-S` is used)
"copyright", "credits", "exit", "quit"
"exec"
]
or
// Built-in constants shared between Python 2 and 3
@@ -53,8 +51,8 @@ module Builtins {
or
// Python 2 only
result in [
"apply", "basestring", "cmp", "execfile", "file", "long", "raw_input", "reduce", "reload",
"unichr", "unicode", "xrange"
"basestring", "cmp", "execfile", "file", "long", "raw_input", "reduce", "reload", "unichr",
"unicode", "xrange"
]
}

View File

@@ -1977,174 +1977,3 @@ private module OutNodes {
* `kind`.
*/
OutNode getAnOutNode(DataFlowCall call, ReturnKind kind) { call = result.getCall(kind) }
/**
* Provides predicates for approximating type properties of user-defined classes
* based on their structure (method declarations, base classes).
*
* This module should _not_ be used in the call graph computation itself, as parts of it may depend
* on layers that themselves build upon the call graph (e.g. API graphs).
*/
module DuckTyping {
private import semmle.python.ApiGraphs
/**
* Holds if `cls` or any of its resolved superclasses declares a method with the given `name`.
*/
predicate hasMethod(Class cls, string name) {
cls.getAMethod().getName() = name
or
hasMethod(getADirectSuperclass(cls), name)
}
/**
* Holds if `cls` has a base class that cannot be resolved to a user-defined class
* and is not just `object`, meaning it may inherit methods from an unknown class.
*/
predicate hasUnresolvedBase(Class cls) {
exists(Expr base | base = cls.getABase() |
not base = classTracker(_).asExpr() and
not base = API::builtin("object").getAValueReachableFromSource().asExpr()
)
}
/**
* Holds if `cls` supports the container protocol, i.e. it declares
* `__contains__`, `__iter__`, or `__getitem__`.
*/
predicate isContainer(Class cls) {
hasMethod(cls, "__contains__") or
hasMethod(cls, "__iter__") or
hasMethod(cls, "__getitem__")
}
/**
* Holds if `cls` supports the iterable protocol, i.e. it declares
* `__iter__` or `__getitem__`.
*/
predicate isIterable(Class cls) {
hasMethod(cls, "__iter__") or
hasMethod(cls, "__getitem__")
}
/**
* Holds if `cls` supports the iterator protocol, i.e. it declares
* both `__iter__` and `__next__`.
*/
predicate isIterator(Class cls) {
hasMethod(cls, "__iter__") and
hasMethod(cls, "__next__")
}
/**
* Holds if `cls` supports the context manager protocol, i.e. it declares
* both `__enter__` and `__exit__`.
*/
predicate isContextManager(Class cls) {
hasMethod(cls, "__enter__") and
hasMethod(cls, "__exit__")
}
/**
* Holds if `cls` supports the descriptor protocol, i.e. it declares
* `__get__`, `__set__`, or `__delete__`.
*/
predicate isDescriptor(Class cls) {
hasMethod(cls, "__get__") or
hasMethod(cls, "__set__") or
hasMethod(cls, "__delete__")
}
/**
* Holds if `cls` directly assigns to an attribute named `name` in its class body.
* This covers attribute assignments like `x = value`, but not method definitions.
*/
predicate declaresAttribute(Class cls, string name) { exists(getAnAttributeValue(cls, name)) }
/**
* Gets the value expression assigned to attribute `name` directly in the class body of `cls`.
*/
Expr getAnAttributeValue(Class cls, string name) {
exists(Assign a |
a.getScope() = cls and
a.getATarget().(Name).getId() = name and
result = a.getValue()
)
}
/**
* Holds if `cls` is callable, i.e. it declares `__call__`.
*/
predicate isCallable(Class cls) { hasMethod(cls, "__call__") }
/**
* Holds if `cls` supports the mapping protocol, i.e. it declares
* `__getitem__` and `keys`, or `__getitem__` and `__iter__`.
*/
predicate isMapping(Class cls) {
hasMethod(cls, "__getitem__") and
(hasMethod(cls, "keys") or hasMethod(cls, "__iter__"))
}
/**
* Holds if `cls` is a new-style class. In Python 3, all classes are new-style.
* In Python 2, a class is new-style if it (transitively) inherits from `object`,
* or has a declared `__metaclass__`, or has an unresolved base class.
*/
predicate isNewStyle(Class cls) {
major_version() = 3
or
major_version() = 2 and
(
cls.getABase() = API::builtin("object").getAValueReachableFromSource().asExpr()
or
isNewStyle(getADirectSuperclass(cls))
or
hasUnresolvedBase(cls)
or
exists(cls.getMetaClass())
or
// Module-level __metaclass__ = type makes all classes in the module new-style
exists(Assign a |
a.getScope() = cls.getEnclosingModule() and
a.getATarget().(Name).getId() = "__metaclass__"
)
)
}
/**
* Gets the `__init__` function that will be invoked when `cls` is constructed,
* resolved according to the MRO.
*/
Function getInit(Class cls) { result = invokedFunctionFromClassConstruction(cls, "__init__") }
/**
* Holds if `cls` or any of its superclasses uses multiple inheritance, or
* has an unresolved base class. In these cases, our MRO approximation may
* resolve to the wrong `__init__`, so we should not flag argument mismatches.
*/
predicate hasUnreliableMro(Class cls) {
exists(Class sup | sup = getADirectSuperclass*(cls) |
exists(sup.getBase(1))
or
hasUnresolvedBase(sup)
)
}
/**
* Holds if `f` overrides a method in a superclass with the same name.
*/
predicate overridesMethod(Function f) {
exists(Class cls | f.getScope() = cls | hasMethod(getADirectSuperclass(cls), f.getName()))
}
/**
* Holds if `f` is a property accessor (decorated with `@property`, `@name.setter`,
* or `@name.deleter`).
*/
predicate isPropertyAccessor(Function f) {
exists(Attribute a | a = f.getADecorator() | a.getName() = "setter" or a.getName() = "deleter")
or
f.getADecorator().(Name).getId() = "property"
}
}

View File

@@ -152,7 +152,11 @@ predicate missingCallToSuperclassMethod(Class base, Function shouldCall, string
*/
predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCall, string name) {
missingCallToSuperclassMethod(base, shouldCall, name) and
not superclassAlsoMissesCall(base, shouldCall, name) and
not exists(Class superBase |
// Alert only on the highest base class that has the issue
superBase = getADirectSuperclass+(base) and
missingCallToSuperclassMethod(superBase, shouldCall, name)
) and
not exists(Function subShouldCall |
// Mention in the alert only the lowest method we're missing the call to
subShouldCall.getScope() = getADirectSubclass+(shouldCall.getScope()) and
@@ -160,15 +164,6 @@ predicate missingCallToSuperclassMethodRestricted(Class base, Function shouldCal
)
}
/**
* Holds if a strict superclass of `base` is also missing a call to `shouldCall` named `name`,
* indicating that `base` is not the highest class in the hierarchy with this issue.
*/
pragma[nomagic]
private predicate superclassAlsoMissesCall(Class base, Function shouldCall, string name) {
missingCallToSuperclassMethod(getADirectSuperclass+(base), shouldCall, name)
}
/**
* If `base` contains a `super()` call, gets a method in the inheritance hierarchy of `name` in the MRO of `base`
* that does not contain a `super()` call, but would call `shouldCall` if it did, which does not otherwise get called

View File

@@ -12,24 +12,19 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
/**
* Gets the `i`th base class of `cls`, if it can be resolved to a user-defined class.
*/
Class getBaseType(Class cls, int i) { cls.getBase(i) = classTracker(result).asExpr() }
Class left_base(Class type, Class base) {
exists(int i | i > 0 and getBaseType(type, i) = base and result = getBaseType(type, i - 1))
ClassObject left_base(ClassObject type, ClassObject base) {
exists(int i | i > 0 and type.getBaseType(i) = base and result = type.getBaseType(i - 1))
}
predicate invalid_mro(Class t, Class left, Class right) {
DuckTyping::isNewStyle(t) and
predicate invalid_mro(ClassObject t, ClassObject left, ClassObject right) {
t.isNewStyle() and
left = left_base(t, right) and
left = getADirectSuperclass*(right)
left = right.getAnImproperSuperType()
}
from Class t, Class left, Class right
from ClassObject t, ClassObject left, ClassObject right
where invalid_mro(t, left, right)
select t,
"Construction of class " + t.getName() +

View File

@@ -11,13 +11,10 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
from Function prop, Class cls
where
prop.getScope() = cls and
prop.getADecorator().(Name).getId() = "property" and
not DuckTyping::isNewStyle(cls)
from PropertyObject prop, ClassObject cls
where cls.declaredAttribute(_) = prop and not cls.failedInference() and not cls.isNewStyle()
select prop,
"Property " + prop.getName() + " will not work properly, as class " + cls.getName() +
" is an old-style class."

View File

@@ -14,12 +14,10 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
from Class c
where
not DuckTyping::isContextManager(c) and
DuckTyping::hasMethod(c, "__del__")
from ClassValue c
where not c.isBuiltin() and not c.isContextManager() and exists(c.declaredAttribute("__del__"))
select c,
"Class " + c.getName() +
" implements __del__ (presumably to release some resource). Consider making it a context manager."

View File

@@ -12,11 +12,9 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
from Class c
where
not DuckTyping::isNewStyle(c) and
DuckTyping::declaresAttribute(c, "__slots__")
from ClassObject c
where not c.isNewStyle() and c.declaresAttribute("__slots__") and not c.failedInference()
select c,
"Using '__slots__' in an old style class just creates a class attribute called '__slots__'."

View File

@@ -11,13 +11,14 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
predicate uses_of_super_in_old_style_class(Call s) {
exists(Function f, Class c |
exists(Function f, ClassObject c |
s.getScope() = f and
f.getScope() = c and
not DuckTyping::isNewStyle(c) and
f.getScope() = c.getPyClass() and
not c.failedInference() and
not c.isNewStyle() and
s.getFunc().(Name).getId() = "super"
)
}

View File

@@ -13,7 +13,7 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
predicate fewer_than_two_public_methods(Class cls, int methods) {
(methods = 0 or methods = 1) and
@@ -25,8 +25,13 @@ predicate does_not_define_special_method(Class cls) {
}
predicate no_inheritance(Class c) {
not exists(getADirectSubclass(c)) and
not exists(getADirectSuperclass(c)) and
not exists(ClassValue cls, ClassValue other |
cls.getScope() = c and
other != ClassValue::object()
|
other.getABaseType() = cls or
cls.getABaseType() = other
) and
not exists(Expr base | base = c.getABase() |
not base instanceof Name or base.(Name).getId() != "object"
)

View File

@@ -15,35 +15,12 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
import Expressions.CallArgs
private import LegacyPointsTo
/**
* Holds if `name` is a legal argument name for calling `init`.
*/
bindingset[name]
predicate isLegalArgumentName(Function init, string name) {
exists(init.getArgByName(name))
or
init.hasKwArg()
}
/**
* Holds if `call` constructs class `cls` and passes a keyword argument `name`
* that does not correspond to any parameter of `cls.__init__`.
*/
predicate illegally_named_parameter(Call call, Class cls, string name) {
exists(Function init |
resolveClassCall(call.getAFlowNode(), cls) and
init = DuckTyping::getInit(cls) and
name = call.getANamedArgumentName() and
not isLegalArgumentName(init, name)
)
}
from Call call, Class cls, string name, Function init
from Call call, ClassValue cls, string name, FunctionValue init
where
illegally_named_parameter(call, cls, name) and
not DuckTyping::hasUnreliableMro(cls) and
init = DuckTyping::getInit(cls)
init = get_function_or_initializer(cls)
select call, "Keyword argument '" + name + "' is not a supported parameter name of $@.", init,
init.getQualifiedName()

View File

@@ -14,60 +14,10 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
import Expressions.CallArgs
private import LegacyPointsTo
/**
* Gets the number of positional arguments in `call`, including elements of any
* literal list passed as `*args`, plus keyword arguments that don't match
* keyword-only parameters (when the function doesn't accept `**kwargs`).
*/
int positional_arg_count(Call call, Class cls, Function init) {
resolveClassCall(call.getAFlowNode(), cls) and
init = DuckTyping::getInit(cls) and
exists(int positional_keywords |
if init.hasKwArg()
then positional_keywords = 0
else
positional_keywords =
count(Keyword kw |
kw = call.getAKeyword() and
not init.getAKeywordOnlyArg().getId() = kw.getArg()
)
|
result =
count(call.getAnArg()) + count(call.getStarargs().(List).getAnElt()) + positional_keywords
)
}
/**
* Holds if `call` constructs `cls` with too many arguments, where `limit` is the maximum.
*/
predicate too_many_args(Call call, Class cls, int limit) {
exists(Function init |
not init.hasVarArg() and
// Subtract 1 from max to account for `self` parameter
limit = init.getMaxPositionalArguments() - 1 and
limit >= 0 and
positional_arg_count(call, cls, init) > limit
)
}
/**
* Holds if `call` constructs `cls` with too few arguments, where `limit` is the minimum.
*/
predicate too_few_args(Call call, Class cls, int limit) {
resolveClassCall(call.getAFlowNode(), cls) and
exists(Function init |
init = DuckTyping::getInit(cls) and
not exists(call.getStarargs()) and
not exists(call.getKwargs()) and
// Subtract 1 from min to account for `self` parameter
limit = init.getMinPositionalArguments() - 1 and
count(call.getAnArg()) + count(call.getAKeyword()) < limit
)
}
from Call call, Class cls, string too, string should, int limit, Function init
from Call call, ClassValue cls, string too, string should, int limit, FunctionValue init
where
(
too_many_args(call, cls, limit) and
@@ -78,7 +28,6 @@ where
too = "too few arguments" and
should = "no fewer than "
) and
not DuckTyping::hasUnreliableMro(cls) and
init = DuckTyping::getInit(cls)
init = get_function_or_initializer(cls)
select call, "Call to $@ with " + too + "; should be " + should + limit.toString() + ".", init,
init.getQualifiedName()

View File

@@ -12,44 +12,25 @@
*/
import python
import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
predicate rhs_in_expr(Expr rhs, Compare cmp) {
exists(Cmpop op, int i | cmp.getOp(i) = op and cmp.getComparator(i) = rhs |
predicate rhs_in_expr(ControlFlowNode rhs, Compare cmp) {
exists(Cmpop op, int i | cmp.getOp(i) = op and cmp.getComparator(i) = rhs.getNode() |
op instanceof In or op instanceof NotIn
)
}
/**
* Holds if `origin` is the result of applying a class as a decorator to a function.
* Such decorator classes act as proxies, and the runtime value of the decorated
* attribute may be of a different type than the decorator class itself.
*/
predicate isDecoratorApplication(DataFlow::LocalSourceNode origin) {
exists(FunctionExpr fe | origin.asExpr() = fe.getADecoratorCall())
}
/**
* Holds if `cls` has methods dynamically added via `setattr`, so we cannot
* statically determine its full interface.
*/
predicate hasDynamicMethods(Class cls) {
exists(CallNode setattr_call |
setattr_call.getFunction().(NameNode).getId() = "setattr" and
setattr_call.getArg(0).(NameNode).getId() = cls.getName() and
setattr_call.getScope() = cls.getScope()
)
}
from Compare cmp, DataFlow::LocalSourceNode origin, DataFlow::Node rhs, Class cls
from
ControlFlowNodeWithPointsTo non_seq, Compare cmp, Value v, ClassValue cls, ControlFlowNode origin
where
origin = classInstanceTracker(cls) and
origin.flowsTo(rhs) and
not DuckTyping::isContainer(cls) and
not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and
not isDecoratorApplication(origin) and
not hasDynamicMethods(cls) and
rhs_in_expr(rhs.asExpr(), cmp)
rhs_in_expr(non_seq, cmp) and
non_seq.pointsTo(_, v, origin) and
v.getClass() = cls and
not Types::failedInference(cls, _) and
not cls.hasAttribute("__contains__") and
not cls.hasAttribute("__iter__") and
not cls.hasAttribute("__getitem__") and
not cls = ClassValue::nonetype() and
not cls = Value::named("types.MappingProxyType")
select cmp, "This test may raise an Exception as the $@ may be of non-container class $@.", origin,
"target", cls, cls.getName()

View File

@@ -12,97 +12,76 @@
*/
import python
import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import semmle.python.ApiGraphs
private import LegacyPointsTo
/**
* Holds if `cls` explicitly sets `__hash__` to `None`, making instances unhashable.
/*
* This assumes that any indexing operation where the value is not a sequence or numpy array involves hashing.
* For sequences, the index must be an int, which are hashable, so we don't need to treat them specially.
* For numpy arrays, the index may be a list, which are not hashable and needs to be treated specially.
*/
predicate setsHashToNone(Class cls) {
DuckTyping::getAnAttributeValue(cls, "__hash__") instanceof None
predicate numpy_array_type(ClassValue na) {
exists(ModuleValue np | np.getName() = "numpy" or np.getName() = "numpy.core" |
na.getASuperType() = np.attr("ndarray")
)
}
/**
* Holds if `cls` is a user-defined class whose instances are unhashable.
* A new-style class without `__hash__` is unhashable, as is one that explicitly
* sets `__hash__ = None`.
*/
predicate isUnhashableUserClass(Class cls) {
DuckTyping::isNewStyle(cls) and
not DuckTyping::hasMethod(cls, "__hash__") and
not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls))
predicate has_custom_getitem(Value v) {
v.getClass().lookup("__getitem__") instanceof PythonFunctionValue
or
setsHashToNone(cls)
numpy_array_type(v.getClass())
}
/**
* Gets the name of a builtin type whose instances are unhashable.
*/
string getUnhashableBuiltinName() { result = ["list", "set", "dict", "bytearray"] }
/**
* Holds if `origin` is a local source node tracking an unhashable instance that
* flows to `node`, with `clsName` describing the class for the alert.
*/
predicate isUnhashable(DataFlow::LocalSourceNode origin, DataFlow::Node node, string clsName) {
exists(Class c |
isUnhashableUserClass(c) and
origin = classInstanceTracker(c) and
origin.flowsTo(node) and
clsName = c.getName()
)
or
clsName = getUnhashableBuiltinName() and
origin = API::builtin(clsName).getAnInstance().asSource() and
origin.flowsTo(node)
}
predicate explicitly_hashed(DataFlow::Node node) {
node = API::builtin("hash").getACall().getArg(0)
}
/**
* Holds if the subscript object in `sub[...]` is known to use hashing for indexing,
* i.e. it does not have a custom `__getitem__` that could accept unhashable indices.
*/
predicate subscriptUsesHashing(Subscript sub) {
DataFlow::exprNode(sub.getObject()) =
API::builtin("dict").getAnInstance().getAValueReachableFromSource()
or
exists(Class cls |
classInstanceTracker(cls)
.(DataFlow::LocalSourceNode)
.flowsTo(DataFlow::exprNode(sub.getObject())) and
not DuckTyping::hasMethod(cls, "__getitem__")
predicate explicitly_hashed(ControlFlowNode f) {
exists(CallNode c, GlobalVariable hash |
c.getArg(0) = f and c.getFunction().(NameNode).uses(hash) and hash.getId() = "hash"
)
}
predicate unhashable_subscript(DataFlow::LocalSourceNode origin, DataFlow::Node node, string clsName) {
exists(Subscript sub |
node = DataFlow::exprNode(sub.getIndex()) and
subscriptUsesHashing(sub)
|
isUnhashable(origin, node, clsName)
predicate unhashable_subscript(ControlFlowNode f, ClassValue c, ControlFlowNode origin) {
is_unhashable(f, c, origin) and
exists(SubscriptNode sub | sub.getIndex() = f |
exists(Value custom_getitem |
sub.getObject().(ControlFlowNodeWithPointsTo).pointsTo(custom_getitem) and
not has_custom_getitem(custom_getitem)
)
)
}
/**
* Holds if `e` is inside a `try` that catches `TypeError`.
*/
predicate typeerror_is_caught(Expr e) {
exists(Try try |
try.getBody().contains(e) and
try.getAHandler().getType() = API::builtin("TypeError").getAValueReachableFromSource().asExpr()
)
}
from DataFlow::LocalSourceNode origin, DataFlow::Node node, string clsName
where
not typeerror_is_caught(node.asExpr()) and
(
explicitly_hashed(node) and isUnhashable(origin, node, clsName)
predicate is_unhashable(ControlFlowNodeWithPointsTo f, ClassValue cls, ControlFlowNode origin) {
exists(Value v | f.pointsTo(v, origin) and v.getClass() = cls |
not cls.hasAttribute("__hash__") and not cls.failedInference(_) and cls.isNewStyle()
or
unhashable_subscript(origin, node, clsName)
cls.lookup("__hash__") = Value::named("None")
)
select node, "This $@ of $@ is unhashable.", origin, "instance", origin, clsName
}
/**
* Holds if `f` is inside a `try` that catches `TypeError`. For example:
*
* try:
* ... f ...
* except TypeError:
* ...
*
* This predicate is used to eliminate false positive results. If `hash`
* is called on an unhashable object then a `TypeError` will be thrown.
* But this is not a bug if the code catches the `TypeError` and handles
* it.
*/
predicate typeerror_is_caught(ControlFlowNode f) {
exists(Try try |
try.getBody().contains(f.getNode()) and
try.getAHandler().getType().(ExprWithPointsTo).pointsTo(ClassValue::typeError())
)
}
from ControlFlowNode f, ClassValue c, ControlFlowNode origin
where
not typeerror_is_caught(f) and
(
explicitly_hashed(f) and is_unhashable(f, c, origin)
or
unhashable_subscript(f, c, origin)
)
select f.getNode(), "This $@ of $@ is unhashable.", origin, "instance", c, c.getQualifiedName()

View File

@@ -10,10 +10,9 @@
*/
import python
private import semmle.python.ApiGraphs
private import LegacyPointsTo
private import semmle.python.types.Builtins
from CallNode call
where
major_version() = 2 and
call = API::builtin("apply").getACall().asCfgNode()
from CallNode call, ControlFlowNodeWithPointsTo func
where major_version() = 2 and call.getFunction() = func and func.pointsTo(Value::named("apply"))
select call, "Call to the obsolete builtin function 'apply'."

View File

@@ -10,17 +10,16 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
predicate slice_method_name(string name) {
name = "__getslice__" or name = "__setslice__" or name = "__delslice__"
}
from Function f, string meth
from PythonFunctionValue f, string meth
where
f.isMethod() and
f.getScope().isMethod() and
not f.isOverridingMethod() and
slice_method_name(meth) and
f.getName() = meth and
not DuckTyping::overridesMethod(f) and
not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(f.getScope()))
f.getName() = meth
select f, meth + " method has been deprecated since Python 2.0."

View File

@@ -11,7 +11,7 @@
*/
import python
private import semmle.python.ApiGraphs
private import LegacyPointsTo
/**
* Holds if the module `name` was deprecated in Python version `major`.`minor`,
@@ -80,7 +80,7 @@ where
name = imp.getName() and
deprecated_module(name, instead, _, _) and
not exists(Try try, ExceptStmt except | except = try.getAHandler() |
except.getType() = API::builtin("ImportError").getAValueReachableFromSource().asExpr() and
except.getType().(ExprWithPointsTo).pointsTo(ClassValue::importError()) and
except.containsInScope(imp)
)
select imp, deprecation_message(name) + replacement_message(name)

View File

@@ -17,7 +17,7 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
predicate needs_docstring(Scope s) {
s.isPublic() and
@@ -29,15 +29,15 @@ predicate needs_docstring(Scope s) {
}
predicate function_needs_docstring(FunctionMetrics f) {
not exists(Function base |
DuckTyping::overridesMethod(f) and
base.getScope() = getADirectSuperclass+(f.getScope()) and
base.getName() = f.getName() and
not function_needs_docstring(base)
not exists(FunctionValue fo, FunctionValue base | fo.overrides(base) and fo.getScope() = f |
not function_needs_docstring(base.getScope())
) and
f.getName() != "lambda" and
(f.getNumberOfLinesOfCode() - count(f.getADecorator())) > 2 and
not DuckTyping::isPropertyAccessor(f)
not exists(PythonPropertyObject p |
p.getGetter().getFunction() = f or
p.getSetter().getFunction() = f
)
}
string scope_type(Scope s) {

View File

@@ -12,10 +12,10 @@
*/
import python
private import semmle.python.ApiGraphs
private import LegacyPointsTo
predicate originIsLocals(ControlFlowNode n) {
API::builtin("locals").getReturn().getAValueReachableFromSource().asCfgNode() = n
predicate originIsLocals(ControlFlowNodeWithPointsTo n) {
n.pointsTo(_, _, Value::named("locals").getACall())
}
predicate modification_of_locals(ControlFlowNode f) {
@@ -37,5 +37,5 @@ where
// in module level scope `locals() == globals()`
// see https://docs.python.org/3/library/functions.html#locals
// FP report in https://github.com/github/codeql/issues/6674
not a.getScope() instanceof Module
not a.getScope() instanceof ModuleScope
select a, "Modification of the locals() dictionary will have no effect on the local variables."

View File

@@ -12,48 +12,16 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import semmle.python.ApiGraphs
private import LegacyPointsTo
/**
* Holds if `cls_arg` references a known iterable builtin type, either directly
* (e.g. `list`) or as an element of a tuple (e.g. `(list, tuple)`).
*/
private predicate isIterableTypeArg(DataFlow::Node cls_arg) {
cls_arg =
API::builtin([
"list", "tuple", "set", "frozenset", "dict", "str", "bytes", "bytearray", "range",
"memoryview"
]).getAValueReachableFromSource()
or
isIterableTypeArg(DataFlow::exprNode(cls_arg.asExpr().(Tuple).getAnElt()))
}
/**
* Holds if `iter` is guarded by an `isinstance` check that tests for
* an iterable type (e.g. `list`, `tuple`, `set`, `dict`).
*/
predicate guardedByIsinstanceIterable(DataFlow::Node iter) {
exists(
ConditionBlock guard, DataFlow::CallCfgNode isinstance_call, DataFlow::LocalSourceNode src
|
isinstance_call = API::builtin("isinstance").getACall() and
src.flowsTo(isinstance_call.getArg(0)) and
src.flowsTo(iter) and
isIterableTypeArg(isinstance_call.getArg(1)) and
guard.getLastNode() = isinstance_call.asCfgNode() and
guard.controls(iter.asCfgNode().getBasicBlock(), true)
)
}
from For loop, DataFlow::Node iter, Class cls
from For loop, ControlFlowNodeWithPointsTo iter, Value v, ClassValue t, ControlFlowNode origin
where
iter.asExpr() = loop.getIter() and
iter = classInstanceTracker(cls) and
not DuckTyping::isIterable(cls) and
not DuckTyping::isDescriptor(cls) and
not (loop.isAsync() and DuckTyping::hasMethod(cls, "__aiter__")) and
not DuckTyping::hasUnresolvedBase(getADirectSuperclass*(cls)) and
not guardedByIsinstanceIterable(iter)
select loop, "This for-loop may attempt to iterate over a $@ of class $@.", iter.asExpr(),
"non-iterable instance", cls, cls.getName()
loop.getIter().getAFlowNode() = iter and
iter.pointsTo(_, v, origin) and
v.getClass() = t and
not t.isIterable() and
not t.failedInference(_) and
not v = Value::named("None") and
not t.isDescriptorType()
select loop, "This for-loop may attempt to iterate over a $@ of class $@.", origin,
"non-iterable instance", t, t.getName()

View File

@@ -13,7 +13,7 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import LegacyPointsTo
predicate calls_close(Call c) { exists(Attribute a | c.getFunc() = a and a.getName() = "close") }
@@ -23,12 +23,18 @@ predicate only_stmt_in_finally(Try t, Call c) {
)
}
from Call close, Try t, Class cls
predicate points_to_context_manager(ControlFlowNodeWithPointsTo f, ClassValue cls) {
forex(Value v | f.pointsTo(v) | v.getClass() = cls) and
cls.isContextManager()
}
from Call close, Try t, ClassValue cls
where
only_stmt_in_finally(t, close) and
calls_close(close) and
classInstanceTracker(cls).asExpr() = close.getFunc().(Attribute).getObject() and
DuckTyping::isContextManager(cls)
exists(ControlFlowNode f | f = close.getFunc().getAFlowNode().(AttrNode).getObject() |
points_to_context_manager(f, cls)
)
select close,
"Instance of context-manager class $@ is closed in a finally block. Consider using 'with' statement.",
cls, cls.getName()

View File

@@ -13,7 +13,7 @@
*/
import python
private import semmle.python.ApiGraphs
private import LegacyPointsTo
predicate func_with_side_effects(Expr e) {
exists(string name | name = e.(Attribute).getName() or name = e.(Name).getId() |
@@ -24,11 +24,11 @@ predicate func_with_side_effects(Expr e) {
}
predicate call_with_side_effect(Call e) {
e.getAFlowNode() =
API::moduleImport("subprocess")
.getMember(["call", "check_call", "check_output"])
.getACall()
.asCfgNode()
e.getAFlowNode() = Value::named("subprocess.call").getACall()
or
e.getAFlowNode() = Value::named("subprocess.check_call").getACall()
or
e.getAFlowNode() = Value::named("subprocess.check_output").getACall()
}
predicate probable_side_effect(Expr e) {

View File

@@ -12,6 +12,7 @@
*/
import python
private import LegacyPointsTo
predicate main_eq_name(If i) {
exists(Name n, StringLiteral m, Compare c |
@@ -31,19 +32,10 @@ predicate is_print_stmt(Stmt s) {
)
}
/**
* Holds if module `m` is likely used as a module (imported by another module),
* as opposed to being exclusively used as a script.
*/
predicate is_used_as_module(Module m) {
m.isPackageInit()
or
exists(ImportingStmt i | i.getAnImportedModuleName() = m.getName())
}
from Stmt p
where
is_print_stmt(p) and
is_used_as_module(p.getScope()) and
// TODO: Need to discuss how we would like to handle ModuleObject.getKind in the glorious future
exists(ModuleValue m | m.getScope() = p.getScope() and m.isUsedAsModule()) and
not exists(If i | main_eq_name(i) and i.getASubStatement().getASubStatement*() = p)
select p, "Print statement may execute during import."

View File

@@ -13,7 +13,7 @@
*/
import python
private import semmle.python.ApiGraphs
private import LegacyPointsTo
predicate isInsideLoop(AstNode node) {
node.getParentNode() instanceof While
@@ -33,9 +33,9 @@ where
not isInsideLoop(del) and
// False positive: calling `sys.exc_info` within a function results in a
// reference cycle, and an explicit call to `del` helps break this cycle.
not exists(API::CallNode call |
call = API::moduleImport("sys").getMember("exc_info").getACall() and
call.getScope() = f
not exists(FunctionValue ex |
ex = Value::named("sys.exc_info") and
ex.getACall().getScope() = f
)
select del, "Unnecessary deletion of local variable $@ in function $@.", e, e.toString(), f,
f.getName()

View File

@@ -13,7 +13,7 @@
*/
import python
private import semmle.python.ApiGraphs
private import LegacyPointsTo
predicate typing_import(ImportingStmt is) {
exists(Module m |
@@ -34,7 +34,11 @@ predicate unique_yield(Stmt s) {
/** Holds if `contextlib.suppress` may be used in the same scope as `s` */
predicate suppression_in_scope(Stmt s) {
exists(With w |
w.getContextExpr() = API::moduleImport("contextlib").getMember("suppress").getACall().asExpr() and
w.getContextExpr()
.(Call)
.getFunc()
.(ExprWithPointsTo)
.pointsTo(Value::named("contextlib.suppress")) and
w.getScope() = s.getScope()
)
}

View File

@@ -12,49 +12,11 @@
*/
import python
private import semmle.python.dataflow.new.internal.DataFlowDispatch
private import semmle.python.dataflow.new.internal.Builtins
private import semmle.python.ApiGraphs
private import LegacyPointsTo
/**
* Holds if `cls` is a user-defined exception class, i.e. it transitively
* extends one of the builtin exception base classes.
*/
predicate isUserDefinedExceptionClass(Class cls) {
cls.getABase() =
API::builtin(["BaseException", "Exception"]).getAValueReachableFromSource().asExpr()
or
isUserDefinedExceptionClass(getADirectSuperclass(cls))
}
/**
* Gets the name of a builtin exception class.
*/
string getBuiltinExceptionName() {
result = Builtins::getBuiltinName() and
(
result.matches("%Error") or
result.matches("%Exception") or
result.matches("%Warning") or
result =
["GeneratorExit", "KeyboardInterrupt", "StopIteration", "StopAsyncIteration", "SystemExit"]
)
}
/**
* Holds if `call` is an instantiation of an exception class.
*/
predicate isExceptionInstantiation(Call call) {
exists(Class cls |
classTracker(cls).asExpr() = call.getFunc() and
isUserDefinedExceptionClass(cls)
)
or
call.getFunc() = API::builtin(getBuiltinExceptionName()).getAValueReachableFromSource().asExpr()
}
from Call call
from Call call, ClassValue ex
where
isExceptionInstantiation(call) and
call.getFunc().(ExprWithPointsTo).pointsTo(ex) and
ex.getASuperType() = ClassValue::exception() and
exists(ExprStmt s | s.getValue() = call)
select call, "Instantiating an exception, but not raising it, has no effect."

View File

@@ -12,12 +12,10 @@
*/
import python
private import semmle.python.ApiGraphs
private import LegacyPointsTo
from CallNode call, string name
where
name = ["exit", "quit"] and
call = API::builtin(name).getACall().asCfgNode()
where call.getFunction().(ControlFlowNodeWithPointsTo).pointsTo(Value::siteQuitter(name))
select call,
"The '" + name +
"' site.Quitter object may not exist if the 'site' module is not loaded or is modified."

View File

@@ -12,7 +12,7 @@
*/
import python
private import semmle.python.ApiGraphs
private import LegacyPointsTo
import Definition
predicate is_increment(Stmt s) {
@@ -41,16 +41,23 @@ predicate one_item_only(For f) {
)
}
/** Holds if `node` is a call to `range`, `xrange`, or `list(range(...))`. */
predicate call_to_range(DataFlow::Node node) {
node = API::builtin(["range", "xrange"]).getACall()
predicate points_to_call_to_range(ControlFlowNode f) {
/* (x)range is a function in Py2 and a class in Py3, so we must treat it as a plain object */
exists(Value range |
range = Value::named("range") or
range = Value::named("xrange")
|
f = range.getACall()
)
or
/* Handle 'from six.moves import range' or similar. */
node = API::moduleImport("six").getMember("moves").getMember(["range", "xrange"]).getACall()
/* In case points-to fails due to 'from six.moves import range' or similar. */
exists(string range | f.getNode().(Call).getFunc().(Name).getId() = range |
range = "range" or range = "xrange"
)
or
/* Handle list(range(...)) and list(list(range(...))) */
node = API::builtin("list").getACall() and
call_to_range(node.(DataFlow::CallCfgNode).getArg(0))
f.(CallNode).(ControlFlowNodeWithPointsTo).pointsTo().getClass() = ClassValue::list() and
points_to_call_to_range(f.(CallNode).getArg(0))
}
/** Whether n is a use of a variable that is a not effectively a constant. */
@@ -95,8 +102,8 @@ from For f, Variable v, string msg
where
f.getTarget() = v.getAnAccess() and
not f.getAStmt().contains(v.getAnAccess()) and
not call_to_range(DataFlow::exprNode(f.getIter())) and
not call_to_range(DataFlow::exprNode(get_comp_iterable(f).getNode())) and
not points_to_call_to_range(f.getIter().getAFlowNode()) and
not points_to_call_to_range(get_comp_iterable(f)) and
not name_acceptable_for_unused_variable(v) and
not f.getScope().getName() = "genexpr" and
not empty_loop(f) and

View File

@@ -1,5 +0,0 @@
---
category: majorAnalysis
---
- Several quality queries have been ported away from using the legacy points-to library. This may lead to changes in alerts.

View File

@@ -1 +1 @@
| inconsistent_mro.py:9:1:9:14 | Class Z | Construction of class Z can fail due to invalid method resolution order(MRO) for bases $@ and $@. | inconsistent_mro.py:3:1:3:16 | Class X | X | inconsistent_mro.py:6:1:6:11 | Class Y | Y |
| inconsistent_mro.py:9:1:9:14 | class Z | Construction of class Z can fail due to invalid method resolution order(MRO) for bases $@ and $@. | inconsistent_mro.py:3:1:3:16 | class X | X | inconsistent_mro.py:6:1:6:11 | class Y | Y |

View File

@@ -1 +1 @@
| property_old_style.py:9:5:9:20 | Function piosc | Property piosc will not work properly, as class OldStyle is an old-style class. |
| property_old_style.py:8:6:8:13 | Property piosc | Property piosc will not work properly, as class OldStyle is an old-style class. |

View File

@@ -1 +1 @@
| newstyle_test.py:4:1:4:16 | Class OldStyle1 | Using '__slots__' in an old style class just creates a class attribute called '__slots__'. |
| newstyle_test.py:4:1:4:16 | class OldStyle1 | Using '__slots__' in an old style class just creates a class attribute called '__slots__'. |

View File

@@ -1 +1,2 @@
| inconsistent_mro.py:9:1:9:14 | Class Z | Construction of class Z can fail due to invalid method resolution order(MRO) for bases $@ and $@. | inconsistent_mro.py:3:1:3:16 | Class X | X | inconsistent_mro.py:6:1:6:11 | Class Y | Y |
| inconsistent_mro.py:9:1:9:14 | class Z | Construction of class Z can fail due to invalid method resolution order(MRO) for bases $@ and $@. | inconsistent_mro.py:3:1:3:16 | class X | X | inconsistent_mro.py:6:1:6:11 | class Y | Y |
| inconsistent_mro.py:16:1:16:19 | class N | Construction of class N can fail due to invalid method resolution order(MRO) for bases $@ and $@. | file://:Compiled Code:0:0:0:0 | builtin-class object | object | inconsistent_mro.py:12:1:12:8 | class O | O |

View File

@@ -1 +1,2 @@
| async_iterator.py:26:11:26:34 | For | This for-loop may attempt to iterate over a $@ of class $@. | async_iterator.py:26:20:26:33 | MissingAiter() | non-iterable instance | async_iterator.py:13:1:13:19 | Class MissingAiter | MissingAiter |
| async_iterator.py:26:11:26:34 | For | This for-loop may attempt to iterate over a $@ of class $@. | async_iterator.py:26:20:26:33 | ControlFlowNode for MissingAiter() | non-iterable instance | async_iterator.py:13:1:13:19 | class MissingAiter | MissingAiter |
| statements_test.py:34:5:34:19 | For | This for-loop may attempt to iterate over a $@ of class $@. | statements_test.py:34:18:34:18 | ControlFlowNode for IntegerLiteral | non-iterable instance | file://:0:0:0:0 | builtin-class int | int |

View File

@@ -1,4 +1,4 @@
| wrong_arguments.py:65:1:65:7 | F0() | Keyword argument 'y' is not a supported parameter name of $@. | wrong_arguments.py:4:5:4:26 | Function __init__ | F0.__init__ |
| wrong_arguments.py:66:1:66:7 | F1() | Keyword argument 'z' is not a supported parameter name of $@. | wrong_arguments.py:8:5:8:36 | Function __init__ | F1.__init__ |
| wrong_arguments.py:67:1:67:12 | F2() | Keyword argument 'y' is not a supported parameter name of $@. | wrong_arguments.py:12:5:12:30 | Function __init__ | F2.__init__ |
| wrong_arguments.py:92:1:92:27 | F6() | Keyword argument 'z' is not a supported parameter name of $@. | wrong_arguments.py:28:5:28:30 | Function __init__ | F6.__init__ |
| wrong_arguments.py:65:1:65:7 | F0() | Keyword argument 'y' is not a supported parameter name of $@. | wrong_arguments.py:4:5:4:26 | Function F0.__init__ | F0.__init__ |
| wrong_arguments.py:66:1:66:7 | F1() | Keyword argument 'z' is not a supported parameter name of $@. | wrong_arguments.py:8:5:8:36 | Function F1.__init__ | F1.__init__ |
| wrong_arguments.py:67:1:67:12 | F2() | Keyword argument 'y' is not a supported parameter name of $@. | wrong_arguments.py:12:5:12:30 | Function F2.__init__ | F2.__init__ |
| wrong_arguments.py:92:1:92:27 | F6() | Keyword argument 'z' is not a supported parameter name of $@. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ |

View File

@@ -1,13 +1,15 @@
| wrong_arguments.py:37:1:37:4 | F0() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:4:5:4:26 | Function __init__ | F0.__init__ |
| wrong_arguments.py:38:1:38:4 | F1() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:8:5:8:36 | Function __init__ | F1.__init__ |
| wrong_arguments.py:39:1:39:4 | F2() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:12:5:12:30 | Function __init__ | F2.__init__ |
| wrong_arguments.py:40:1:40:4 | F3() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:16:5:16:40 | Function __init__ | F3.__init__ |
| wrong_arguments.py:41:1:41:4 | F4() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:20:5:20:31 | Function __init__ | F4.__init__ |
| wrong_arguments.py:42:1:42:4 | F5() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:24:5:24:42 | Function __init__ | F5.__init__ |
| wrong_arguments.py:43:1:43:5 | F6() | Call to $@ with too few arguments; should be no fewer than 2. | wrong_arguments.py:28:5:28:30 | Function __init__ | F6.__init__ |
| wrong_arguments.py:44:1:44:7 | F7() | Call to $@ with too few arguments; should be no fewer than 3. | wrong_arguments.py:32:5:32:33 | Function __init__ | F7.__init__ |
| wrong_arguments.py:48:1:48:7 | F0() | Call to $@ with too many arguments; should be no more than 1. | wrong_arguments.py:4:5:4:26 | Function __init__ | F0.__init__ |
| wrong_arguments.py:49:1:49:9 | F1() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:8:5:8:36 | Function __init__ | F1.__init__ |
| wrong_arguments.py:50:1:50:9 | F5() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:24:5:24:42 | Function __init__ | F5.__init__ |
| wrong_arguments.py:51:1:51:9 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function __init__ | F6.__init__ |
| wrong_arguments.py:52:1:52:11 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function __init__ | F6.__init__ |
| wrong_arguments.py:37:1:37:4 | F0() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:4:5:4:26 | Function F0.__init__ | F0.__init__ |
| wrong_arguments.py:38:1:38:4 | F1() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:8:5:8:36 | Function F1.__init__ | F1.__init__ |
| wrong_arguments.py:39:1:39:4 | F2() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:12:5:12:30 | Function F2.__init__ | F2.__init__ |
| wrong_arguments.py:40:1:40:4 | F3() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:16:5:16:40 | Function F3.__init__ | F3.__init__ |
| wrong_arguments.py:41:1:41:4 | F4() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:20:5:20:31 | Function F4.__init__ | F4.__init__ |
| wrong_arguments.py:42:1:42:4 | F5() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:24:5:24:42 | Function F5.__init__ | F5.__init__ |
| wrong_arguments.py:43:1:43:5 | F6() | Call to $@ with too few arguments; should be no fewer than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ |
| wrong_arguments.py:44:1:44:7 | F7() | Call to $@ with too few arguments; should be no fewer than 3. | wrong_arguments.py:32:5:32:33 | Function F7.__init__ | F7.__init__ |
| wrong_arguments.py:48:1:48:7 | F0() | Call to $@ with too many arguments; should be no more than 1. | wrong_arguments.py:4:5:4:26 | Function F0.__init__ | F0.__init__ |
| wrong_arguments.py:49:1:49:9 | F1() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:8:5:8:36 | Function F1.__init__ | F1.__init__ |
| wrong_arguments.py:50:1:50:9 | F5() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:24:5:24:42 | Function F5.__init__ | F5.__init__ |
| wrong_arguments.py:51:1:51:9 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ |
| wrong_arguments.py:52:1:52:11 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ |
| wrong_arguments.py:85:1:85:12 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ |
| wrong_arguments.py:86:1:86:7 | F6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:28:5:28:30 | Function F6.__init__ | F6.__init__ |

View File

@@ -1,2 +1,2 @@
| should_be_context_manager.py:3:1:3:22 | Class MegaDel | Class MegaDel implements __del__ (presumably to release some resource). Consider making it a context manager. |
| should_be_context_manager.py:16:1:16:22 | Class MiniDel | Class MiniDel implements __del__ (presumably to release some resource). Consider making it a context manager. |
| should_be_context_manager.py:3:1:3:22 | class MegaDel | Class MegaDel implements __del__ (presumably to release some resource). Consider making it a context manager. |
| should_be_context_manager.py:16:1:16:22 | class MiniDel | Class MiniDel implements __del__ (presumably to release some resource). Consider making it a context manager. |

View File

@@ -1,2 +1,2 @@
| expressions_test.py:89:8:89:15 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | Class XIter | XIter |
| expressions_test.py:91:8:91:19 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | Class XIter | XIter |
| expressions_test.py:89:8:89:15 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | class XIter | XIter |
| expressions_test.py:91:8:91:19 | Compare | This test may raise an Exception as the $@ may be of non-container class $@. | expressions_test.py:88:11:88:17 | ControlFlowNode for XIter() | target | expressions_test.py:77:1:77:20 | class XIter | XIter |

View File

@@ -1 +1 @@
| expressions_test.py:42:20:42:25 | ControlFlowNode for unhash | This $@ of $@ is unhashable. | expressions_test.py:41:32:41:37 | ControlFlowNode for list() | instance | expressions_test.py:41:32:41:37 | ControlFlowNode for list() | list |
| expressions_test.py:42:20:42:25 | unhash | This $@ of $@ is unhashable. | expressions_test.py:41:32:41:37 | ControlFlowNode for list() | instance | file://:0:0:0:0 | builtin-class list | list |

View File

@@ -279,41 +279,3 @@ def useofapply():
def apply(f):
pass
apply(foo)([1])
# Class used as a decorator: the runtime value at attribute access is the
# function's return value, not the decorator class instance.
class cached_property(object):
def __init__(self, func):
self.func = func
def __get__(self, obj, cls):
val = self.func(obj)
setattr(obj, self.func.__name__, val)
return val
class MyForm(object):
@cached_property
def changed_data(self):
return [1, 2, 3]
def test_decorator_class(form):
f = MyForm()
# OK: cached_property is a descriptor; the actual runtime value is a list.
if "name" in f.changed_data:
pass
# Class with dynamically added methods via setattr: we cannot statically
# determine its full interface, so we should not flag it.
class DynamicProxy(object):
def __init__(self, args):
self._args = args
for method_name in ["__contains__", "__iter__", "__len__"]:
def wrapper(self, *args, __method_name=method_name):
pass
setattr(DynamicProxy, method_name, wrapper)
def test_dynamic_methods():
proxy = DynamicProxy(())
# OK: __contains__ is added dynamically via setattr.
if "name" in proxy:
pass

View File

@@ -1,3 +1,3 @@
| functions_test.py:95:5:95:40 | Function __getslice__ | __getslice__ method has been deprecated since Python 2.0. |
| functions_test.py:98:5:98:47 | Function __setslice__ | __setslice__ method has been deprecated since Python 2.0. |
| functions_test.py:101:5:101:40 | Function __delslice__ | __delslice__ method has been deprecated since Python 2.0. |
| functions_test.py:95:5:95:40 | Function DeprecatedSliceMethods.__getslice__ | __getslice__ method has been deprecated since Python 2.0. |
| functions_test.py:98:5:98:47 | Function DeprecatedSliceMethods.__setslice__ | __setslice__ method has been deprecated since Python 2.0. |
| functions_test.py:101:5:101:40 | Function DeprecatedSliceMethods.__delslice__ | __delslice__ method has been deprecated since Python 2.0. |

View File

@@ -1 +1 @@
| test.py:50:1:50:23 | For | This for-loop may attempt to iterate over a $@ of class $@. | test.py:50:10:50:22 | NonIterator() | non-iterable instance | test.py:45:1:45:26 | Class NonIterator | NonIterator |
| test.py:50:1:50:23 | For | This for-loop may attempt to iterate over a $@ of class $@. | test.py:50:10:50:22 | ControlFlowNode for NonIterator() | non-iterable instance | test.py:45:1:45:26 | class NonIterator | NonIterator |

View File

@@ -1 +1 @@
| test.py:168:9:168:17 | Attribute() | Instance of context-manager class $@ is closed in a finally block. Consider using 'with' statement. | test.py:151:1:151:17 | Class CM | CM |
| test.py:168:9:168:17 | Attribute() | Instance of context-manager class $@ is closed in a finally block. Consider using 'with' statement. | test.py:151:1:151:17 | class CM | CM |

View File

@@ -174,16 +174,3 @@ def assert_ok(seq):
# False positive. ODASA-8042. Fixed in PR #2401.
class false_positive:
e = (x for x in [])
# isinstance guard should suppress non-iterable warning
def guarded_iteration(x):
ni = NonIterator()
if isinstance(ni, (list, tuple)):
for item in ni:
pass
def guarded_iteration_single(x):
ni = NonIterator()
if isinstance(ni, list):
for item in ni:
pass

View File

@@ -2,7 +2,7 @@
set -eu
source misc/bazel/runfiles.sh 2>/dev/null || source external/ql+/misc/bazel/runfiles.sh
source misc/bazel/runfiles.sh 2>/dev/null || source ../ql+/misc/bazel/runfiles.sh
ast_generator="$(rlocation "$1")"
grammar_file="$(rlocation "$2")"

14
rust/justfile Normal file
View File

@@ -0,0 +1,14 @@
import '../lib.just'
install: (_bazel "run" "@codeql//rust:install")
build: (_if_not_on_ci_just "generate" source_dir()) (_build_dist "rust")
generate: (_bazel "run" "@codeql//rust/codegen")
lint: (_run_in source_dir() "python3" "lint.py")
format: (_run_in source_dir() "python3" "lint.py" "--format-only")
[group('test')]
language-tests *EXTRA_ARGS: (_language_tests EXTRA_ARGS source_dir() 'ql/test')

View File

@@ -4,6 +4,14 @@ import subprocess
import pathlib
import shutil
import sys
import argparse
def options():
parser = argparse.ArgumentParser(description="lint rust language pack code")
parser.add_argument("--format-only", action="store_true", help="Only apply formatting")
return parser.parse_args()
def tool(name):
@@ -12,27 +20,35 @@ def tool(name):
return ret
this_dir = pathlib.Path(__file__).resolve().parent
cargo = tool("cargo")
bazel = tool("bazel")
runs = []
def main():
args = options()
this_dir = pathlib.Path(__file__).resolve().parent
def run(tool, args, *, cwd=this_dir):
print("+", tool, args)
runs.append(subprocess.run([tool] + args.split(), cwd=cwd))
cargo = tool("cargo")
bazel = tool("bazel")
runs = []
# make sure bazel-provided sources are put in tree for `cargo` to work with them
run(bazel, "run ast-generator:inject-sources")
run(cargo, "fmt --all --quiet")
def run(tool, args, *, cwd=this_dir):
print("+", tool, args)
runs.append(subprocess.run([tool] + args.split(), cwd=cwd))
for manifest in this_dir.rglob("Cargo.toml"):
if not manifest.is_relative_to(this_dir / "ql") and not manifest.is_relative_to(this_dir / "integration-tests"):
run(cargo,
"clippy --fix --allow-dirty --allow-staged --quiet -- -D warnings",
cwd=manifest.parent)
sys.exit(max(r.returncode for r in runs))
# make sure bazel-provided sources are put in tree for `cargo` to work with them
run(bazel, "run ast-generator:inject-sources")
run(cargo, "fmt --all --quiet")
if not args.format_only:
for manifest in this_dir.rglob("Cargo.toml"):
if not manifest.is_relative_to(this_dir / "ql") and not manifest.is_relative_to(this_dir / "integration-tests"):
run(cargo,
"clippy --fix --allow-dirty --allow-staged --quiet -- -D warnings",
cwd=manifest.parent)
return max(r.returncode for r in runs)
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,10 @@
import "../../../lib.just"
[no-cd]
test *ARGS=".": (_if_not_on_ci_just "generate" source_dir()) (_integration_test ARGS)
# TODO in separate PR
# [no-cd]
# format *ARGS=".": (_format_ql ARGS) (_format_py ARGS)

6
rust/ql/justfile Normal file
View File

@@ -0,0 +1,6 @@
import "../../lib.just"
[no-cd]
format *ARGS=".": (_format_ql ARGS)
consistency_queries := source_dir() / "consistency-queries"

View File

@@ -39,8 +39,8 @@ predicate pathTypeAsTraitAssoc(
/**
* Holds if `assoc` is accessed on `tp` in `path`.
*
* That is, this is the case when `path` is of the form `<tp as Trait>::AssocType`
* or `tp::AssocType`; and `AssocType` resolves to `assoc`.
* That is, this is the case when `path` is of the form `<tp as
* Trait>::AssocType` or `tp::AssocType`; and `AssocType` resolves to `assoc`.
*/
predicate tpAssociatedType(TypeParam tp, AssocType assoc, Path path) {
resolvePath(path.getQualifier()) = tp and

View File

@@ -699,13 +699,9 @@ class PreTypeMention = PreTypeMention::TypeMention;
/**
* Holds if `path` accesses an associated type `alias` from `trait` on a
* concrete type given by `tm`.
*
* `implOrTmTrait` is either the mention that resolves to `trait` when `path`
* is of the form `<Type as Trait>::AssocType`, or the enclosing `impl` block
* when `path` is of the form `Self::AssocType`.
*/
private predicate pathConcreteTypeAssocType(
Path path, PreTypeMention tm, TraitItemNode trait, AstNode implOrTmTrait, TypeAlias alias
Path path, PreTypeMention tm, TraitItemNode trait, PreTypeMention tmTrait, TypeAlias alias
) {
exists(Path qualifier |
qualifier = path.getQualifier() and
@@ -714,19 +710,19 @@ private predicate pathConcreteTypeAssocType(
// path of the form `<Type as Trait>::AssocType`
// ^^^ tm ^^^^^^^^^ name
exists(string name |
pathTypeAsTraitAssoc(path, tm, implOrTmTrait, trait, name) and
pathTypeAsTraitAssoc(path, tm, tmTrait, trait, name) and
getTraitAssocType(trait, name) = alias
)
or
// path of the form `Self::AssocType` within an `impl` block
// tm ^^^^ ^^^^^^^^^ name
implOrTmTrait =
any(ImplItemNode impl |
alias = resolvePath(path) and
qualifier = impl.getASelfPath() and
tm = impl.(Impl).getSelfTy() and
trait.getAnAssocItem() = alias
)
exists(ImplItemNode impl |
alias = resolvePath(path) and
qualifier = impl.getASelfPath() and
tm = impl.(Impl).getSelfTy() and
trait.getAnAssocItem() = alias and
tmTrait = impl.getTraitPath()
)
)
}
@@ -745,26 +741,21 @@ private module PathSatisfiesConstraint =
*/
private Type getPathConcreteAssocTypeAt(Path path, TypePath typePath) {
exists(
PreTypeMention tm, ImplItemNode impl, TraitItemNode trait, TraitType t, AstNode implOrTmTrait,
PreTypeMention tm, ImplItemNode impl, TraitItemNode trait, TraitType t, PreTypeMention tmTrait,
TypeAlias alias, TypePath path0
|
pathConcreteTypeAssocType(path, tm, trait, implOrTmTrait, alias) and
pathConcreteTypeAssocType(path, tm, trait, tmTrait, alias) and
t = TTrait(trait) and
PathSatisfiesConstraint::satisfiesConstraintTypeThrough(tm, impl, t, path0, result) and
path0.isCons(TAssociatedTypeTypeParameter(trait, alias), typePath)
|
implOrTmTrait instanceof Impl
tmTrait.getTypeAt(TypePath::nil()) != t
or
// When `path` is of the form `<Type as Trait>::AssocType` we need to check
// that `impl` is not more specific than the mentioned trait
implOrTmTrait =
any(PreTypeMention tmTrait |
not exists(TypePath path1, Type t1 |
t1 = impl.getTraitPath().(PreTypeMention).getTypeAt(path1) and
not t1 instanceof TypeParameter and
t1 != tmTrait.getTypeAt(path1)
)
)
not exists(TypePath path1, Type t1 |
t1 = impl.getTraitPath().(PreTypeMention).getTypeAt(path1) and
not t1 instanceof TypeParameter and
t1 != tmTrait.getTypeAt(path1)
)
)
}

7
rust/ql/test/justfile Normal file
View File

@@ -0,0 +1,7 @@
import "../justfile"
all_checks := default_db_checks + """\
--consistency-queries=""" + consistency_queries
[no-cd]
test *ARGS=".": (_codeql_test "rust" "" all_checks ARGS)

17
swift/justfile Normal file
View File

@@ -0,0 +1,17 @@
import '../lib.just'
import "../../ql/swift/ql/justfile"
install: (_bazel "run" "@codeql//swift:install")
[group('build')]
build: (_build_dist "swift")
generate: (_bazel "run" "@codeql//swift/codegen")
format ARGS=".": (_format_cpp ARGS)
[group('test')]
language-tests *EXTRA_ARGS: (_language_tests EXTRA_ARGS source_dir() 'ql/test')
[group('test')]
extra-tests: (_sembuild "target/test/check-queries-swift") (_sembuild "target/test/check-db-upgrades-swift") (_sembuild "target/test/check-db-downgrades-swift")

View File

@@ -0,0 +1,9 @@
import "../../../lib.just"
[no-cd]
test *ARGS=".": (_just "generate") (_integration_test ARGS)
# TODO in separate PR
# [no-cd]
# format *ARGS=".": (_format_ql ARGS) (_format_py ARGS)

6
swift/ql/justfile Normal file
View File

@@ -0,0 +1,6 @@
import "../../lib.just"
[no-cd]
format *ARGS=".": (_format_ql ARGS)
consistency_queries := source_dir() / "consistency-queries"

11
swift/ql/test/justfile Normal file
View File

@@ -0,0 +1,11 @@
import "../justfile"
all_checks := default_db_checks + """\
--check-repeated-labels \
--check-redefined-labels \
--check-use-before-definition \
--consistency-queries=""" + consistency_queries
[no-cd]
test *ARGS=".": (_codeql_test "swift" "" all_checks ARGS)