mirror of
https://github.com/github/codeql.git
synced 2026-05-21 06:37:10 +02:00
Compare commits
42 Commits
tausbn/pyt
...
redsun82/j
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
024130f791 | ||
|
|
fd97208960 | ||
|
|
994e5510bd | ||
|
|
0f28502e68 | ||
|
|
469d09c9af | ||
|
|
9d52a08793 | ||
|
|
db83285c9f | ||
|
|
15e8e4803d | ||
|
|
c67e1230b6 | ||
|
|
f1febac3ec | ||
|
|
4284d66afb | ||
|
|
0e3ee6efd7 | ||
|
|
365b2ebd6d | ||
|
|
f2a6503efe | ||
|
|
7e7afbabcd | ||
|
|
103745b5d2 | ||
|
|
92672836dc | ||
|
|
c51f2f8780 | ||
|
|
c9cda74195 | ||
|
|
bd003c58a8 | ||
|
|
08097157fd | ||
|
|
b8b01ce71c | ||
|
|
7f72f87204 | ||
|
|
aa09288462 | ||
|
|
8ba7efd455 | ||
|
|
bb467d4abf | ||
|
|
d987aa67ec | ||
|
|
e8bcbbd6df | ||
|
|
acc7e3f32d | ||
|
|
c4305151c3 | ||
|
|
fba96c4eae | ||
|
|
cb652f3dc8 | ||
|
|
6e14111337 | ||
|
|
d7d7cf920a | ||
|
|
a4acf0890e | ||
|
|
812fc2349b | ||
|
|
5b9436a95f | ||
|
|
4768ebabee | ||
|
|
9e31fb50c8 | ||
|
|
2dea9da38c | ||
|
|
1202af1c5c | ||
|
|
9c284b1778 |
14
.github/commands/rerun.yml
vendored
Normal file
14
.github/commands/rerun.yml
vendored
Normal 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."
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
3
java/justfile
Normal file
@@ -0,0 +1,3 @@
|
||||
import '../lib.just'
|
||||
|
||||
build: (_build_dist "java")
|
||||
6
java/ql/justfile
Normal file
6
java/ql/justfile
Normal file
@@ -0,0 +1,6 @@
|
||||
import "../../lib.just"
|
||||
|
||||
[no-cd]
|
||||
format *ARGS=".": (_format_ql ARGS)
|
||||
|
||||
consistency_queries := source_dir() / "consistency-queries"
|
||||
15
java/ql/test-kotlin1/justfile
Normal file
15
java/ql/test-kotlin1/justfile
Normal 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)
|
||||
16
java/ql/test-kotlin2/justfile
Normal file
16
java/ql/test-kotlin2/justfile
Normal 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
16
java/ql/test/justfile
Normal 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
4
justfile
Normal file
@@ -0,0 +1,4 @@
|
||||
# see misc/just/README.md for an overview
|
||||
|
||||
import 'lib.just'
|
||||
import 'misc/just/forward.just'
|
||||
5
misc/bazel/justfile
Normal file
5
misc/bazel/justfile
Normal file
@@ -0,0 +1,5 @@
|
||||
import '../just/lib.just'
|
||||
|
||||
[no-cd, positional-arguments, no-exit-message]
|
||||
hello +ARGS:
|
||||
@echo "hello from bzl" "$@"
|
||||
@@ -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
5
misc/codegen/justfile
Normal 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
44
misc/just/README.md
Normal 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
19
misc/just/build.just
Normal 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)
|
||||
159
misc/just/codeql-test-run.ts
Normal file
159
misc/just/codeql-test-run.ts
Normal 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
58
misc/just/defs.just
Normal 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
20
misc/just/format.just
Normal 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')"
|
||||
)
|
||||
131
misc/just/forward-command.ts
Normal file
131
misc/just/forward-command.ts
Normal 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
38
misc/just/forward.just
Normal 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
2
misc/just/justfile
Normal file
@@ -0,0 +1,2 @@
|
||||
format *ARGS=".":
|
||||
npx prettier --write {{ ARGS }}
|
||||
33
misc/just/language-tests.ts
Normal file
33
misc/just/language-tests.ts
Normal 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
47
misc/just/lib.just
Normal 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
|
||||
1
misc/just/semmle-code-stub.just
Normal file
1
misc/just/semmle-code-stub.just
Normal file
@@ -0,0 +1 @@
|
||||
export SEMMLE_CODE := ""
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() +
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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__'."
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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__'. |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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__ |
|
||||
|
||||
@@ -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__ |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
14
rust/justfile
Normal 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')
|
||||
52
rust/lint.py
52
rust/lint.py
@@ -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())
|
||||
|
||||
10
rust/ql/integration-tests/justfile
Normal file
10
rust/ql/integration-tests/justfile
Normal 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
6
rust/ql/justfile
Normal file
@@ -0,0 +1,6 @@
|
||||
import "../../lib.just"
|
||||
|
||||
[no-cd]
|
||||
format *ARGS=".": (_format_ql ARGS)
|
||||
|
||||
consistency_queries := source_dir() / "consistency-queries"
|
||||
@@ -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
|
||||
|
||||
@@ -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
7
rust/ql/test/justfile
Normal 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
17
swift/justfile
Normal 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")
|
||||
9
swift/ql/integration-tests/justfile
Normal file
9
swift/ql/integration-tests/justfile
Normal 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
6
swift/ql/justfile
Normal 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
11
swift/ql/test/justfile
Normal 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)
|
||||
Reference in New Issue
Block a user