diff --git a/actions/justfile b/actions/justfile new file mode 100644 index 000000000000..b96eb20dfe26 --- /dev/null +++ b/actions/justfile @@ -0,0 +1,9 @@ +import '../lib.just' + +[group('build')] +build: (_build_dist "actions") + +roots := [source_dir() / 'ql/test'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) diff --git a/actions/ql/integration-tests/justfile b/actions/ql/integration-tests/justfile new file mode 100644 index 000000000000..8392c2b3ba40 --- /dev/null +++ b/actions/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_integration_test ARGS) diff --git a/actions/ql/justfile b/actions/ql/justfile new file mode 100644 index 000000000000..e613c25b52c1 --- /dev/null +++ b/actions/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := "" diff --git a/actions/ql/test/justfile b/actions/ql/test/justfile new file mode 100644 index 000000000000..a824f3029972 --- /dev/null +++ b/actions/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := [] + +all_checks := default_db_checks + +[no-cd] +test *ARGS=".": (_codeql_test "actions" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/cpp/justfile b/cpp/justfile new file mode 100644 index 000000000000..0fc87260ebec --- /dev/null +++ b/cpp/justfile @@ -0,0 +1,10 @@ +import '../lib.just' +import? '../../cpp-coding-standards.just' + +[group('build')] +build: (_build_dist "cpp") + +roots := [source_dir() / 'ql/test', SEMMLE_CODE / 'semmlecode-cpp-tests'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) diff --git a/cpp/ql/consistency-queries/badLocations.ql b/cpp/ql/consistency-queries/badLocations.ql new file mode 100644 index 000000000000..385d3d92fe6a --- /dev/null +++ b/cpp/ql/consistency-queries/badLocations.ql @@ -0,0 +1,9 @@ +import cpp + +// Locations should either be :0:0:0:0 locations (UnknownLocation, or +// a whole file), or all 4 fields should be positive. +from Location l +where + [l.getStartLine(), l.getEndLine(), l.getStartColumn(), l.getEndColumn()] != 0 and + [l.getStartLine(), l.getEndLine(), l.getStartColumn(), l.getEndColumn()] < 1 +select l diff --git a/cpp/ql/consistency-queries/nullInToString.ql b/cpp/ql/consistency-queries/nullInToString.ql new file mode 100644 index 000000000000..4a6385b519ab --- /dev/null +++ b/cpp/ql/consistency-queries/nullInToString.ql @@ -0,0 +1,5 @@ +import cpp + +from Element e +where e.toString().matches("%(null)%") +select e diff --git a/cpp/ql/consistency-queries/qlpack.yml b/cpp/ql/consistency-queries/qlpack.yml new file mode 100644 index 000000000000..303f2271be12 --- /dev/null +++ b/cpp/ql/consistency-queries/qlpack.yml @@ -0,0 +1,6 @@ +name: codeql/cpp-consistency-queries +groups: [cpp, test, consistency-queries] +dependencies: + codeql/cpp-all: ${workspace} +extractor: cpp +warnOnImplicitThis: true diff --git a/cpp/ql/consistency-queries/unusedLocations.ql b/cpp/ql/consistency-queries/unusedLocations.ql new file mode 100644 index 000000000000..875c60ba3251 --- /dev/null +++ b/cpp/ql/consistency-queries/unusedLocations.ql @@ -0,0 +1,10 @@ +import cpp + +from Location l +where + not any(Element e).getLocation() = l and + not any(LambdaCapture lc).getLocation() = l and + not any(MacroAccess ma).getActualLocation() = l and + not any(NamespaceDeclarationEntry nde).getBodyLocation() = l and + not any(XmlLocatable xml).getLocation() = l +select l diff --git a/cpp/ql/consistency-queries/variableDeclarationsWithoutTypes.ql b/cpp/ql/consistency-queries/variableDeclarationsWithoutTypes.ql new file mode 100644 index 000000000000..2573d660defd --- /dev/null +++ b/cpp/ql/consistency-queries/variableDeclarationsWithoutTypes.ql @@ -0,0 +1,5 @@ +import cpp + +from VariableDeclarationEntry i +where not exists(i.getType()) +select i diff --git a/cpp/ql/consistency-queries/variablesWithoutTypes.ql b/cpp/ql/consistency-queries/variablesWithoutTypes.ql new file mode 100644 index 000000000000..d004c175abd0 --- /dev/null +++ b/cpp/ql/consistency-queries/variablesWithoutTypes.ql @@ -0,0 +1,5 @@ +import cpp + +from Variable i +where not exists(i.getType()) +select i diff --git a/cpp/ql/integration-tests/justfile b/cpp/ql/integration-tests/justfile new file mode 100644 index 000000000000..8392c2b3ba40 --- /dev/null +++ b/cpp/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_integration_test ARGS) diff --git a/cpp/ql/justfile b/cpp/ql/justfile new file mode 100644 index 000000000000..ff0e4c1090f3 --- /dev/null +++ b/cpp/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" diff --git a/cpp/ql/test/justfile b/cpp/ql/test/justfile new file mode 100644 index 000000000000..7ccd81541018 --- /dev/null +++ b/cpp/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := ['--include-location-in-star'] + +all_checks := default_db_checks ++ ['--check-undefined-labels', '--check-unused-labels', '--consistency-queries=' + consistency_queries] + +[no-cd] +test *ARGS=".": (_codeql_test "cpp" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/csharp/justfile b/csharp/justfile new file mode 100644 index 000000000000..6f99dd8703e0 --- /dev/null +++ b/csharp/justfile @@ -0,0 +1,9 @@ +import '../lib.just' + +[group('build')] +build: (_build_dist "csharp") + +roots := [source_dir() / 'ql/test'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) diff --git a/csharp/ql/integration-tests/justfile b/csharp/ql/integration-tests/justfile new file mode 100644 index 000000000000..8392c2b3ba40 --- /dev/null +++ b/csharp/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_integration_test ARGS) diff --git a/csharp/ql/justfile b/csharp/ql/justfile new file mode 100644 index 000000000000..ff0e4c1090f3 --- /dev/null +++ b/csharp/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" diff --git a/csharp/ql/test/justfile b/csharp/ql/test/justfile new file mode 100644 index 000000000000..ba3e238580c5 --- /dev/null +++ b/csharp/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := [] + +all_checks := default_db_checks ++ ['--check-undefined-labels', '--check-repeated-labels', '--check-redefined-labels', '--additional-packs=ql', '--consistency-queries=' + consistency_queries] + +[no-cd] +test *ARGS=".": (_codeql_test "csharp" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/go/justfile b/go/justfile new file mode 100644 index 000000000000..fa4c18266af6 --- /dev/null +++ b/go/justfile @@ -0,0 +1,17 @@ +import '../lib.just' + +[group('build')] +build: (_build_dist "go") + +roots := [source_dir() / 'ql/test'] + +# The `IncorrectIntegerConversion` query treats `math.MaxInt`/`math.MaxUint` differently on 32- and +# 64-bit targets, so we run its test under `GOARCH=386` as well. `GOOS=linux` because +# `GOOS=darwin GOARCH=386` is no longer supported. +roots_386 := [source_dir() / 'ql/test/query-tests/Security/CWE-681/IncorrectIntegerConversion.qlref'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) + +[group('test')] +language-tests-386 *EXTRA_ARGS: (_language_tests (roots_386 ++ ['GOOS=linux', 'GOARCH=386'] ++ EXTRA_ARGS)) diff --git a/go/ql/integration-tests/justfile b/go/ql/integration-tests/justfile new file mode 100644 index 000000000000..8392c2b3ba40 --- /dev/null +++ b/go/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_integration_test ARGS) diff --git a/go/ql/justfile b/go/ql/justfile new file mode 100644 index 000000000000..ff0e4c1090f3 --- /dev/null +++ b/go/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" diff --git a/go/ql/test/justfile b/go/ql/test/justfile new file mode 100644 index 000000000000..e4f9665c1773 --- /dev/null +++ b/go/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := [] + +all_checks := default_db_checks ++ ['--check-undefined-labels', '--check-unused-labels', '--check-repeated-labels', '--check-redefined-labels', '--check-use-before-definition', '--consistency-queries=' + consistency_queries] + +[no-cd] +test *ARGS=".": (_codeql_test "go" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/java/justfile b/java/justfile new file mode 100644 index 000000000000..aba4ba7b21dd --- /dev/null +++ b/java/justfile @@ -0,0 +1,4 @@ +import '../lib.just' + +[group('build')] +build: (_build_dist "java") diff --git a/java/ql/integration-tests/justfile b/java/ql/integration-tests/justfile new file mode 100644 index 000000000000..8392c2b3ba40 --- /dev/null +++ b/java/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_integration_test ARGS) diff --git a/java/ql/justfile b/java/ql/justfile new file mode 100644 index 000000000000..ff0e4c1090f3 --- /dev/null +++ b/java/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" diff --git a/java/ql/test-kotlin1/justfile b/java/ql/test-kotlin1/justfile new file mode 100644 index 000000000000..a9815627d15e --- /dev/null +++ b/java/ql/test-kotlin1/justfile @@ -0,0 +1,13 @@ +import "../justfile" + +# These are CI shards of the Kotlin language tests, run as `just java +# kotlin-language-tests-1`, so they only run when asked for by name. +explicit_verbs := ['test'] + +# Kotlin tests may fail the diags.ql consistency test if the diagnostic limit is set. +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 ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/java/ql/test-kotlin2/justfile b/java/ql/test-kotlin2/justfile new file mode 100644 index 000000000000..bda00ff0ca75 --- /dev/null +++ b/java/ql/test-kotlin2/justfile @@ -0,0 +1,13 @@ +import "../justfile" + +# These are CI shards of the Kotlin language tests, run as `just java +# kotlin-language-tests-2`, so they only run when asked for by name. +explicit_verbs := ['test'] + +# Kotlin tests may fail the diags.ql consistency test if the diagnostic limit is set. +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 ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/java/ql/test/justfile b/java/ql/test/justfile new file mode 100644 index 000000000000..aedf78381a05 --- /dev/null +++ b/java/ql/test/justfile @@ -0,0 +1,12 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +# Kotlin tests may fail the diags.ql consistency test if the diagnostic limit is set. +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 ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/javascript/justfile b/javascript/justfile new file mode 100644 index 000000000000..769847a380d1 --- /dev/null +++ b/javascript/justfile @@ -0,0 +1,9 @@ +import '../lib.just' + +[group('build')] +build: (_build_dist "javascript") + +roots := [source_dir() / 'ql/test'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) diff --git a/javascript/ql/integration-tests/justfile b/javascript/ql/integration-tests/justfile new file mode 100644 index 000000000000..8392c2b3ba40 --- /dev/null +++ b/javascript/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_integration_test ARGS) diff --git a/javascript/ql/justfile b/javascript/ql/justfile new file mode 100644 index 000000000000..e613c25b52c1 --- /dev/null +++ b/javascript/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := "" diff --git a/javascript/ql/test/justfile b/javascript/ql/test/justfile new file mode 100644 index 000000000000..366b4e1e43dd --- /dev/null +++ b/javascript/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := [] + +all_checks := default_db_checks + +[no-cd] +test *ARGS=".": (_codeql_test "javascript" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/justfile b/justfile new file mode 100644 index 000000000000..6fe875f6facb --- /dev/null +++ b/justfile @@ -0,0 +1,9 @@ +# see misc/just/README.md for an overview + +import 'lib.just' +import 'misc/just/forward.just' + +# bazel files live all over the repository rather than under any one language, so they +# are formatted from here. `format` itself is the forwarder, hence `_root_`; see +# misc/just/README.md. +_root_format *ARGS=".": (_format_bazel ARGS) diff --git a/lib.just b/lib.just new file mode 100644 index 000000000000..0ddd926bcda5 --- /dev/null +++ b/lib.just @@ -0,0 +1 @@ +import "misc/just/lib.just" diff --git a/misc/bazel/buildifier/BUILD.bazel b/misc/bazel/buildifier/BUILD.bazel index b71712515595..ec7a152a144d 100644 --- a/misc/bazel/buildifier/BUILD.bazel +++ b/misc/bazel/buildifier/BUILD.bazel @@ -8,3 +8,11 @@ buildifier( ], lint_mode = "fix", ) + +# The binary behind the target above, which formats the paths it is given rather than +# always the whole workspace. `just format` goes through this so that formatting a +# directory formats that directory. +alias( + name = "binary", + actual = "@buildifier_prebuilt//:buildifier", +) diff --git a/misc/codegen/justfile b/misc/codegen/justfile new file mode 100644 index 000000000000..a65fa16e5679 --- /dev/null +++ b/misc/codegen/justfile @@ -0,0 +1,5 @@ +import "../just/lib.just" + +test *ARGS="": (_bazel ['test', '@codeql//misc/codegen/...']) + +format *ARGS=".": (_format_py ARGS) diff --git a/misc/just/README.md b/misc/just/README.md new file mode 100644 index 000000000000..16c91f90220a --- /dev/null +++ b/misc/just/README.md @@ -0,0 +1,151 @@ +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 above 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. +- finally, the forwarder also looks _below_ each argument, so that `just test ql/cpp` + runs the tests defined underneath it. The argument only says where to look in this + case, so each recipe found is run on its own directory rather than being passed the + argument. Several may be found, in which case they run sequentially: + `just format ql/cpp` formats everything under `ql/cpp` that knows how to format + itself. + +Both directions are searched, and every distinct recipe found runs. This matters because +a verb higher up is usually doing a different job from one further down rather than a +broader version of it: `rust` formats Rust sources while `rust/ql` formats QL, so +`just format rust` has to do both. A recipe that only arrived through `import` is the +same job, though, and runs once. + +A repository root forwards every verb, which leaves it no way to answer one itself: a +recipe written next to the `import` overrides the imported one and takes the forwarder's +place, so `just format cpp` would stop finding anything. The root spells its own +implementation `_root_` instead, and the forwarder picks that up wherever the +plain name turns out to be the forwarder's own: + +```just +import 'misc/just/forward.just' + +_root_format *ARGS=".": (_format_bazel ARGS) +``` + +This is for work that belongs to no single directory. bazel files are the case in hand: +they sit throughout the tree rather than under any one language, so formatting them is +the root's job, and taking the argument keeps `just format cpp` to the bazel files under +`cpp`. + +Note that this one delegates rather than doing the work itself. The forwarder reaches a +recipe above its argument with `--justfile`, which runs it from the directory of the +justfile defining it unless the recipe is `[no-cd]`. A `_root_` that grows a body +therefore reads its default `.` as the whole repository rather than the directory the +caller is in, so one that does its own work needs `[no-cd]` itself. + +Being a recipe like any other, a `_root_` is inherited by a justfile importing the +one defining it, which is how the internal repository gets this one for free. It runs +once either way, as the two spellings are the same recipe. A root that defines its own +instead replaces it, and then both run, each over the files of the repository that +defines it: bazel formatting asks bazel from the root of the checkout the files belong +to, so that a repository formats its own files with its own pin. + +That last part is arranged by variables rather than by recipes. `set +allow-duplicate-variables` in `defs.just` lets an importing justfile assign a variable +defined here and have its value win, which is how a consuming root points the bazel +formatter at its own workspace, its own buildifier and its own exclusions. The leading +underscore says these are not meant to be run, not that they are private: any of them a +root might reasonably want to redirect is an interface between the two repositories. + +Renaming one is therefore a breaking change that nothing reports. `just` has no notion +of an assignment that fails to override, so a root assigning the old name keeps parsing, +keeps listing, keeps passing CI, and silently reverts to the value here. Worse, a root +overriding several loses only the renamed one, leaving a half-applied configuration: +total failure would land in a state someone designed, while partial failure lands in one +nobody has ever seen. + +Nothing can see it either, because the underscore that keeps these out of `just --list` +keeps them out of `--variables` and a bare `--evaluate` as well. Asked by name they do +answer, which is how a root checks that an override of its own still overrides anything: + +```sh +just --evaluate _bazel_excluded # what mine is now +just --justfile /justfile --evaluate _bazel_excluded # what it would be +``` + +A name that has gone says so rather than reporting an empty value. That is a diagnostic +to reach for once something looks wrong, though: it answers whether a name still exists, +not whether its meaning has changed, so it passes happily when the value here gains or +loses a pattern. Rename freely, but say so when handing the change over. + +A directory that only makes sense when named explicitly can opt out of being found from +above: + +```just +explicit_verbs := ['test'] +``` + +This only affects the downward search. Running the verb from inside that directory, or +naming the directory on the command line, keeps working. A verb that passed over such a +directory says so and names it, so that a command covering a tree does not look like it +covered more than it did. That listing is part of the account of what ran and leaves the +exit status alone; only a verb that matched nothing at all fails. + +The QL test suites use this: `test` on a language runs the whole suite, which takes a +long time, so that has to be asked for by name. Integration tests and the sharded +Kotlin suites that CI runs opt out for the same reason. What is left discoverable from +above is what is cheap enough to run without meaning to. + +Being an ordinary variable, `explicit_verbs` is inherited by justfiles importing one +that sets it. That is normally what is wanted, as importing a suite's justfile means +being the same kind of suite, down to the reason for naming it explicitly. An importer +that disagrees can reassign it, and its own value wins: + +```just +import '../some/suite/justfile' + +explicit_verbs := [] +``` + +Duplicate variables are allowed throughout (see `defs.just`), so this is silent in both +directions: assigning `explicit_verbs` without realising one was inherited overrides it +without complaint, which can put a heavy suite back within reach of a verb aimed at a +parent directory. + +Justfiles are found through `git`, so a newly written one needs to be either tracked or +untracked-but-not-ignored to be picked up. + +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). + +Test arguments are passed around as `just` lists (`set lists`), so they reach the +underlying runner already split and arguments containing spaces survive intact. + +One caveat: when a verb ends up running several recipes, non-positional arguments need +to be understood by all of them. That is fine when they speak the same language, as +`--learn` or `--codeql` do across QL and integration tests. It is not when they do not: +a broad `just test .` reaches bazel and pytest suites alike, and a flag meant for one of +them will fail on the other. It fails rather than being quietly ignored, so the answer +is to aim the verb at something narrower. diff --git a/misc/just/build.just b/misc/just/build.just new file mode 100644 index 000000000000..f9739f40f397 --- /dev/null +++ b/misc/just/build.just @@ -0,0 +1,27 @@ +# 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 +# +# The install log is worth reading only when the install fails, and printing it +# regardless buried whatever was actually asked for underneath it. Note that bazel is +# not quietened any further than that here: unlike an error, a failing test's log is not +# something `--ui_event_filters` can let back through, and a build this long is one to +# see the progress of. +[no-exit-message] +_maybe_build_dist LANGUAGE: (_if_in_semmle_code (f'cd "$SEMMLE_CODE"; MSYS2_ARG_CONV_EXCL="*" tools/bazel test //language-packs:intree-{{ LANGUAGE }}-as-test --test_output=errors') '# 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 *ARGS: (_if_in_semmle_code 'cd "$SEMMLE_CODE"; MSYS2_ARG_CONV_EXCL="*" tools/bazel' 'bazel' ARGS) + +# Call sembuild (requires an internal repository checkout) +[no-cd] +[no-exit-message] +_sembuild *ARGS: (_run_in_semmle_code (['./build'] ++ ARGS)) diff --git a/misc/just/codeql_test_run.py b/misc/just/codeql_test_run.py new file mode 100755 index 000000000000..5bda2d5b86eb --- /dev/null +++ b/misc/just/codeql_test_run.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Run CodeQL tests with appropriate configuration. + +Called from just recipes as: + python3 codeql_test_run.py LANGUAGE [ARG...] + +Arguments are already split by `just` (see `set lists`), so each one is taken verbatim. +`--all-checks=FLAG` contributes FLAG to the set of extra checks that `--all-checks` (or +its `+` abbreviation) turns on. +""" + +import os +import re +import subprocess +import sys +from pathlib import Path + +JUST = os.environ.get("JUST_EXECUTABLE", "just") +ERROR = os.environ.get("JUST_ERROR", "error: ") +CMD_BEGIN = os.environ.get("CMD_BEGIN", "") +CMD_END = os.environ.get("CMD_END", "") +SEMMLE_CODE = os.environ.get("SEMMLE_CODE") + +ALL_CHECKS_PREFIX = "--all-checks=" +ENV_RE = re.compile(r"^[A-Z_][A-Z_0-9]*=.*$") + + +def invoke(invocation, *, cwd=None, log_prefix=""): + prefix = f"{log_prefix} " if log_prefix else "" + print(f"{CMD_BEGIN}{prefix}{' '.join(invocation)}{CMD_END}") + try: + subprocess.run(invocation, check=True, cwd=cwd) + except subprocess.CalledProcessError as e: + return e.returncode + return 0 + + +def error(message): + print(f"{ERROR}{message}", file=sys.stderr) + + +def parse_args(args, argv): + """Sort arguments into tests, flags and environment assignments.""" + for arg in argv: + if not arg: + # an empty argument can come from a caller interpolating an unset variable + continue + if arg.startswith(ALL_CHECKS_PREFIX): + args["all_checks"].append(arg[len(ALL_CHECKS_PREFIX) :]) + elif arg.startswith("--codeql="): + args["codeql"] = arg.split("=", 1)[1] + elif arg in ("+", "--all-checks"): + args["all"] = True + elif arg.startswith("-"): + args["flags"].append(arg) + elif ENV_RE.match(arg): + args["env"].append(arg) + else: + args["tests"].append(arg) + + +def env_value(args, name, default): + """Resolve a setting from test arguments, then the environment, then a default.""" + for assignment in reversed(args["env"]): + key, _, value = assignment.partition("=") + if key == name and value: + return value + return os.environ.get(name) or default + + +def main(): + argv = sys.argv[1:] + if not argv: + error("Usage: codeql_test_run.py LANGUAGE [ARG...]") + return 1 + + language, *rest = argv + + args = { + "tests": [], + "flags": [], + "env": [], + "all_checks": [], + "codeql": "build" if SEMMLE_CODE else "host", + "all": False, + } + parse_args(args, rest) + if args["all"]: + parse_args(args, args["all_checks"]) + + if not SEMMLE_CODE and args["codeql"] in ("build", "built"): + error( + "Using `--codeql=build` or `--codeql=built` requires working " + "with the internal repository" + ) + return 1 + + if not args["tests"]: + args["tests"].append(".") + + # Resolve these only once all arguments are known, so that a `RAM_PER_THREAD=` test + # argument can lower the default on memory-heavy suites. + default_ram = 3000 if sys.platform == "linux" else 2048 + ram_per_thread = int(env_value(args, "RAM_PER_THREAD", default_ram)) + cpus = int(env_value(args, "CPUS", os.cpu_count() or 1)) + args["flags"][:0] = [f"--ram={ram_per_thread * cpus}", f"-j{cpus}"] + + if args["codeql"] == "build": + if invoke([JUST, language, "build"], cwd=SEMMLE_CODE) != 0: + return 1 + + if args["codeql"] != "host": + # Disable the default implicit config file, but keep an explicit one. + # Same behavior wrt --codeql as the integration test runner. + os.environ.setdefault("CODEQL_CONFIG_FILE", ".") + + for env_var in args["env"]: + key, _, value = env_var.partition("=") + if not key: + error(f"Invalid environment variable assignment: {env_var}") + return 1 + os.environ[key] = value + + # Resolve codeql executable + if args["codeql"] in ("built", "build"): + codeql = Path(SEMMLE_CODE, "target", "intree", f"codeql-{language}", "codeql") + elif args["codeql"] == "host": + codeql = Path("codeql") + else: + codeql = Path(args["codeql"]) + + if codeql.is_dir(): + codeql = codeql / "codeql" + + # On Windows, prefer codeql.exe over the Unix shell wrapper + if sys.platform == "win32" and codeql.suffix != ".exe": + exe = codeql.with_suffix(".exe") + if exe.exists(): + codeql = exe + + if args["codeql"] != "host" and not codeql.exists(): + error(f"CodeQL executable not found: {codeql}") + return 1 + + return invoke( + [str(codeql), "test", "run", *args["flags"], "--", *args["tests"]], + log_prefix=" ".join(args["env"]), + ) + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(128 + 2) diff --git a/misc/just/defs.just b/misc/just/defs.just new file mode 100644 index 000000000000..47cedd124b44 --- /dev/null +++ b/misc/just/defs.just @@ -0,0 +1,61 @@ +import? '../../../semmle-code.just' # internal repo just file, if present +import 'semmle-code-stub.just' + +# `set lists` is what lets recipes forward argument lists without encoding them as +# whitespace separated strings. It is still unstable as of just 1.58. +set unstable +set lists +set fallback +set allow-duplicate-recipes +set allow-duplicate-variables + +export PATH_SEP := if os() == "windows" { ";" } else { ":" } +export JUST_EXECUTABLE := just_executable() + +error := f'{{ 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 + +py := "python3" + +default_db_checks := ['--check-databases', '--check-diff-informed', '--fail-on-trap-errors'] + +[no-exit-message] +@_require_semmle_code: + {{ if SEMMLE_CODE == "" { f''' + 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] +[no-exit-message] +[positional-arguments] +@_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 }} diff --git a/misc/just/format.just b/misc/just/format.just new file mode 100644 index 000000000000..ba6885b590d8 --- /dev/null +++ b/misc/just/format.just @@ -0,0 +1,83 @@ +import "build.just" + +_ql_formatter := if SEMMLE_CODE != "" { '"$SEMMLE_CODE/target/intree/codeql-nolang/codeql"' } else { "codeql" } + +_py_formatter := if SEMMLE_CODE != "" { "uv run black" } else { "black" } + +_cpp_formatter := if SEMMLE_CODE != "" { "uv run clang-format" } else { "clang-format" } + +# The `buildifier` bazel target always covers the whole workspace, so the binary behind +# it is used instead and given paths. bazel is asked from the root of this repository, +# even when it sits inside another, and the files are bounded to that root as well: each +# repository then formats its own bazel files with the buildifier version it pins, and a +# verb aimed at a tree spanning both is answered once by each. This is the opposite of +# building, where a target needs the enclosing workspace to resolve at all, hence +# `_bazel` in build.just going the other way. +_bazel_workspace := quote(parent_directory(parent_directory(source_dir()))) + +_bazel_formatter := "bazel run --noshow_progress --ui_event_filters=,+error,+fail //misc/bazel/buildifier:binary --" + +# bazel files are named rather than suffixed, and buildifier has no exclude option of its +# own, so the files skipped by the target above are skipped here too. That target formats +# the whole workspace at once and so cannot take the path this recipe is given, which is +# why the two run the same binary through different entry points. Their exclusions are +# therefore stated twice, in two places, in two syntaxes: keep them in step, or `just +# format` rewrites what pre-commit and CI deliberately leave alone. +# +# bazel knows `BUILD` and `WORKSPACE` by those names and everything else by the `.bazel` +# extension, so a file named `BUILD.` else is a template or a generator's input +# rather than a bazel file, and is none of the formatter's business to parse. +# +# Both lists are comma-separated, so a root defining its own `_root_format` can name +# several patterns in one variable. It has no need to repeat the ones here: the files +# they cover belong to this repository, which formats them itself. Exclusions match the +# path as walked rather than as spelled on the command line, so one naming a directory +# has to cover both the path it is reached by and the path it is walked from. +# +# As with the QL formatter, buildifier only names what it rewrote if it also accounts for +# every file it did not, so that accounting is dropped. It counts the warnings it could +# not fix there, which are left for linting to report rather than raised on every format. +_bazel_names := "BUILD,WORKSPACE,*.bazel,*.bzl,*.sky" + +_bazel_excluded := ".git/*,*/.git/*,*misc/bazel/3rdparty/*_deps/*" + +_bazel_accounting := ': applied fixes, [0-9]+ warnings left$' + +# `codeql query format` and `clang-format` take files rather than directories, so the +# files are collected by `run_on_files.py`. Arguments are passed positionally so that +# paths containing spaces survive, of which this repository has many. +# +# The files that were rewritten are worth reporting, but `codeql query format` only +# names those once it also names every file it leaves alone, which buries them under +# thousands of lines. So it is asked for all of it and the lines about files it did not +# touch are dropped. Only those are dropped, so errors still come through, as does +# anything unforeseen. +# +# `[no-cd]` is what keeps a relative argument meaning the directory the caller is in. +# The forwarder reaches a recipe above its argument with `--justfile`, which otherwise +# runs it from that justfile's own directory: dropping the attribute would silently turn +# the default `.` into the whole repository, and the only symptom would be slowness. + +[no-cd] +[no-exit-message] +[positional-arguments] +_format_ql +ARGS: (_maybe_build_dist "nolang") + {{ cmd_sep }}{{ py }} "{{ source_dir() }}/run_on_files.py" --drop '^Formatting ' --drop '^No change for ' "*.ql,*.qll" {{ _ql_formatter }} query format --in-place -v -- "$@"{{ cmd_sep }} + +[no-cd] +[no-exit-message] +[positional-arguments] +_format_py *ARGS=".": + {{ cmd_sep }}{{ _py_formatter }} "$@"{{ cmd_sep }} + +[no-cd] +[no-exit-message] +[positional-arguments] +_format_cpp *ARGS=".": + {{ cmd_sep }}{{ py }} "{{ source_dir() }}/run_on_files.py" "*.h,*.cpp" {{ _cpp_formatter }} -i -- "$@"{{ cmd_sep }} + +[no-cd] +[no-exit-message] +[positional-arguments] +_format_bazel *ARGS=".": + {{ cmd_sep }}export MSYS2_ARG_CONV_EXCL="*"; {{ py }} "{{ source_dir() }}/run_on_files.py" --absolute --chdir {{ _bazel_workspace }} --within {{ _bazel_workspace }} --drop '{{ _bazel_accounting }}' --exclude '{{ _bazel_excluded }}' "{{ _bazel_names }}" {{ _bazel_formatter }} -mode=fix -lint=fix -v -- "$@"{{ cmd_sep }} diff --git a/misc/just/forward.just b/misc/just/forward.just new file mode 100644 index 000000000000..571f5e31a806 --- /dev/null +++ b/misc/just/forward.just @@ -0,0 +1,30 @@ +# Common verbs +# See README.md in this directory for an overview. + +import "lib.just" + +# Verbs are recipe names, so each one needs its own recipe. They all delegate to the +# same forwarder, which decides where the verb is actually implemented. + +[no-cd] +[no-exit-message] +[positional-arguments] +@_forward VERB *ARGS: + {{ py }} "{{ source_dir() }}/forward_command.py" "$@" + +alias t := test +alias b := build +alias g := generate +alias gen := generate +alias f := format +alias l := lint + +test *ARGS: (_forward "test" ARGS) + +build *ARGS: (_forward "build" ARGS) + +generate *ARGS: (_forward "generate" ARGS) + +lint *ARGS: (_forward "lint" ARGS) + +format *ARGS: (_forward "format" ARGS) diff --git a/misc/just/forward_command.py b/misc/just/forward_command.py new file mode 100644 index 000000000000..f1acbfb185ca --- /dev/null +++ b/misc/just/forward_command.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python3 +"""Forward a common verb to the justfiles that implement it. + +Verbs like `test`, `build` and `format` are spelled the same everywhere, but what they +mean is defined per language, next to the code they act on. This finds the justfiles +implementing a verb for each of its arguments and runs every one of them, so that +`just test rust` or `just format .` work without a central list of who implements what. + +Justfiles are looked for in both directions from an argument: + +- above it, where a recipe is passed the argument itself, as that says what to act on +- below it, where a recipe is passed its own directory, as there the argument only said + where to look + +Every distinct recipe found this way runs. Recipes are compared by value, so one reached +through `import` is recognised as the same job and runs once, while a cross-cutting +recipe higher up composes with the more specific ones below instead of hiding them. + +Two things keep the search useful: recipes delegating back here are skipped, so it never +settles on a forwarder, and a directory can set `explicit_verbs` to stay out of reach of +a verb aimed at one of its parents. See README.md for the whole picture. + +Called from just recipes as: + python3 forward_command.py COMMAND [ARGS...] +""" + +import json +import os +import re +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +JUST = os.environ.get("JUST_EXECUTABLE", "just") +ERROR = os.environ.get("JUST_ERROR", "") + +# Recipes that delegate to this one do not implement a verb, they pass it on. Skipping +# them is what stops the search from settling on a forwarder, be it this one or the root +# justfile of a nested repository. +FORWARD_RECIPE = "_forward" + +# A justfile that forwards a verb has already spent the plain name on the forwarder, so +# it names its own implementation of that verb `_root_`. This is how a repository +# root gets to answer a verb for itself while still dispatching it everywhere else. +ROOT_PREFIX = "_root_" + +# Justfiles may list verbs that must be spelled out explicitly instead of being picked +# up by a verb aimed at one of their parent directories. +EXPLICIT_VERBS = "explicit_verbs" + +PROBE_WORKERS = 16 + + +def error(message): + # Anything already reported on stdout belongs before this, and the two streams are + # buffered differently when they are not both a terminal. + sys.stdout.flush() + print(f"{ERROR}{message}", file=sys.stderr) + + +def get_just_context(justfile, recipe, flags, positional_args): + """Get the (cwd, args) for invoking just with the given justfile.""" + if ( + len(positional_args) == 1 + and justfile == Path(positional_args[0]) / "justfile" + ): + # If there's only one positional argument and it matches the justfile + # path, suppress arguments so e.g. `just build ql/rust` becomes + # `just build` in the `ql/rust` directory + return positional_args[0], [recipe, *flags] + else: + return None, ["--justfile", str(justfile), recipe, *flags, *positional_args] + + +def dump_justfile(justfile): + """Parse a justfile with `just`, returning its JSON dump or an error message.""" + result = subprocess.run( + [JUST, "--dump", "--dump-format", "json", "--justfile", str(justfile)], + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + ) + if result.returncode != 0: + return None, result.stderr.strip() + return json.loads(result.stdout), None + + +def list_value(assignments, name): + """Read a list literal assignment from a justfile dump.""" + value = assignments.get(name, {}).get("value") + # A literal list is dumped as ["list", element...]. Anything else is an expression + # that cannot be evaluated without running just, and counts as absent. + if isinstance(value, list) and value[:1] == ["list"]: + return value[1:] + return [] + + +def accepts(recipe, argc): + """Check whether a recipe can be called with a given number of arguments.""" + parameters = recipe["parameters"] + variadic = parameters and parameters[-1]["kind"] in ("star", "plus") + required = sum( + 1 + for parameter in parameters + if parameter["default"] is None and parameter["kind"] != "star" + ) + return required <= argc and (variadic or argc <= len(parameters)) + + +def implements(dump, command, argc): + """Return the recipe a justfile runs for a command, if it has a usable one.""" + recipes = dump["recipes"] + recipe = recipes.get(dump["aliases"].get(command, command)) + if recipe is None or recipe["private"]: + return None + if any( + dependency["recipe"] == FORWARD_RECIPE for dependency in recipe["dependencies"] + ): + # Here the plain name is the forwarder's own, so it says nothing about what this + # directory does. A justfile that both forwards and answers the command itself + # spells its own answer `_root_`, the one name the two can share. + recipe = recipes.get(f"{ROOT_PREFIX}{command}") + if recipe is None: + return None + return recipe if accepts(recipe, argc) else None + + +def opts_out(dump, command): + """Whether a justfile asks to be named rather than found by a command.""" + return command in list_value(dump["assignments"], EXPLICIT_VERBS) + + +def dump_all(justfiles): + """Parse justfiles in parallel, reporting the ones that cannot be read.""" + with ThreadPoolExecutor(PROBE_WORKERS) as executor: + dumps = list(executor.map(dump_justfile, justfiles)) + parsed = [] + for justfile, (dump, failure) in zip(justfiles, dumps): + if dump is None: + error(f"could not read {justfile}:\n{failure}") + else: + parsed.append((justfile, dump)) + return parsed + + +def git(directory, *args): + """Run a git command in a directory, returning its output lines.""" + result = subprocess.run( + ["git", "-C", directory, *args], + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + ) + if result.returncode != 0: + error(f"`git {' '.join(args)}` failed in {directory}:\n{result.stderr.strip()}") + return [] + return result.stdout.splitlines() + + +def submodules(directory): + """List the initialised submodules under a directory.""" + toplevel = git(directory, "rev-parse", "--show-toplevel") + if not toplevel or not (Path(toplevel[0]) / ".gitmodules").exists(): + return [] + paths = [ + Path(toplevel[0]) / line.split(" ", 1)[1] + for line in git( + toplevel[0], "config", "--file", ".gitmodules", "--get-regexp", r"\.path$" + ) + ] + within = Path(directory).resolve() + return [ + Path(directory) / os.path.relpath(path, within) + for path in paths + # An uninitialised submodule is an empty directory, with nothing to run. + if path.is_relative_to(within) and (path / ".git").exists() + ] + + +def find_justfiles(directory): + """List every justfile under a directory. + + Submodules are listed separately, as `git ls-files` can either recurse into them or + report untracked files, but not both, and a justfile that has just been written is + worth finding. + """ + justfiles = set() + for repository in [directory, *submodules(directory)]: + justfiles.update( + Path(repository) / line + for line in git( + repository, + "ls-files", + "--cached", + "--others", + "--exclude-standard", + "--", + "justfile", + "*/justfile", + ) + ) + return justfiles + + +def invocation_path(path, *, like): + """Spell an absolute path like the user spelled the argument.""" + if Path(like).is_absolute(): + return path + return Path(os.path.relpath(path, Path.cwd())) + + +def find_justfiles_above(command, arg): + """Search up the directory tree for justfiles implementing the command. + + All of them are collected rather than just the nearest, because a recipe higher up + is often doing a different job from one further down rather than a broader version + of it. Returns (justfile, recipe) pairs, nearest first. + """ + directory = Path(arg).resolve() + candidates = [ + invocation_path(p / "justfile", like=arg) + for p in [directory, *directory.parents] + if (p / "justfile").exists() + ] + found = [] + seen = [] + for justfile, dump in dump_all(candidates): + # A justfile sitting exactly on the argument is called without it, as the + # argument would only repeat where it already is. + argc = 0 if justfile.parent.resolve() == directory else 1 + recipe = implements(dump, command, argc) + # These justfiles are nested, so a recipe that was seen already is one this + # one merely imported, and the nearest spelling of it has been taken. + if recipe is not None and recipe not in seen: + seen.append(recipe) + found.append((justfile, recipe)) + return found + + +def find_justfiles_below(command, directory, covered=()): + """Search down a directory for justfiles implementing the command. + + A justfile is skipped when the recipe it would run is one an enclosing directory + already contributes, which is what `import` makes happen: the recipe is the same + job, so running it once is enough. `covered` holds the recipes already found above + the directory. + + Returns the justfiles to run and, separately, the ones that implement the command + but ask to be named rather than found. + """ + # The justfile at `directory` is covered by the search above it. + candidates = sorted(find_justfiles(directory) - {Path(directory) / "justfile"}) + matches = [] + opted_out = [] + for justfile, dump in dump_all(candidates): + recipe = implements(dump, command, 0) + if recipe is None: + continue + if opts_out(dump, command): + opted_out.append(justfile) + else: + matches.append((justfile, recipe)) + contributed = {Path(directory): list(covered)} + found = [] + # Shallowest first, so that an enclosing justfile is always decided before the ones + # it may account for. + for justfile, recipe in sorted(matches, key=lambda match: len(match[0].parts)): + if any(recipe in contributed.get(p, []) for p in justfile.parent.parents): + continue + contributed.setdefault(justfile.parent, []).append(recipe) + found.append((justfile, recipe)) + return sorted(found, key=lambda match: match[0]), sorted(opted_out) + + +def resolve(command, arg): + """Find the justfiles implementing a command for an argument. + + Returns a list of (justfile, argument, recipe) triples, from both above and below + the argument. One found above gets the argument itself, as that selects what to act + on. One found below gets its own directory instead, as there the argument only said + where to look. Justfiles below that asked to be named are returned separately. + """ + above = find_justfiles_above(command, arg) + resolved = [(justfile, arg, recipe["name"]) for justfile, recipe in above] + opted_out = [] + if os.path.isdir(arg): + below, opted_out = find_justfiles_below( + command, arg, [recipe for _, recipe in above] + ) + resolved += [ + (justfile, str(justfile.parent), recipe["name"]) + for justfile, recipe in below + ] + return resolved, opted_out + + +def report_opted_out(command, justfiles, *, ran): + """Name the justfiles a command passed over because they ask to be named. + + Worth saying even when other recipes did run, as otherwise a command that looks + like it covered a whole directory quietly left parts of it alone. That case is + informational and goes to stdout with the rest of the account of what ran: the + command did what was asked of it. Only matching nothing at all is an error. + """ + if not justfiles: + return + directories = sorted(str(jf.parent) for jf in set(justfiles)) + # One per line: there can be dozens, and a single wrapped line is unreadable. + listed = "\n".join(f" {directory}" for directory in directories) + message = f"not run, as {command} must name these explicitly:\n{listed}" + if ran: + print(message) + else: + error(message) + + +def invoke_just(cwd, args): + """Run just with the given arguments.""" + try: + subprocess.run([JUST, *args], check=True, cwd=cwd) + except subprocess.CalledProcessError as e: + return e.returncode + return 0 + + +def forward(cmd, args): + """Forward a command to language-specific justfiles.""" + is_non_positional = re.compile(r"^(-.*|\+|[A-Z_][A-Z_0-9]*=.*)$") + flags = [arg for arg in args if is_non_positional.match(arg)] + positional_args = [arg for arg in args if not is_non_positional.match(arg)] + + justfiles = {} + opted_out = [] + for arg in positional_args or ["."]: + resolved, skipped = resolve(cmd, arg) + opted_out += skipped + if not resolved: + error(f"No justfile found for {cmd} on {arg}") + report_opted_out(cmd, skipped, ran=False) + return 1 + for justfile, justfile_arg, recipe in resolved: + justfiles.setdefault(justfile, (recipe, []))[1].append(justfile_arg) + + invocations = [] + for justfile, (recipe, pos_args) in justfiles.items(): + # An argument standing for the whole directory subsumes any more specific one + # that ended up on the same justfile. + whole_directory = str(justfile.parent) + if whole_directory in pos_args: + pos_args = [whole_directory] + cwd, just_args = get_just_context(justfile, recipe, flags, pos_args) + prefix = f"cd {cwd}; " if cwd else "" + print(f"-> {prefix}just {' '.join(just_args)}") + invocations.append((cwd, just_args)) + + report_opted_out(cmd, opted_out, ran=True) + + for cwd, just_args in invocations: + if invoke_just(cwd, just_args) != 0: + # Say which one, as a verb can fan out over a great many directories. + where = f" in {cwd}" if cwd else "" + error(f"{cmd} failed{where}: just {' '.join(just_args)}") + return 1 + return 0 + + +def main(): + argv = sys.argv[1:] + if not argv: + error("No command provided") + return 1 + return forward(argv[0], argv[1:]) + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(128 + 2) diff --git a/misc/just/justfile b/misc/just/justfile new file mode 100644 index 000000000000..679295f47801 --- /dev/null +++ b/misc/just/justfile @@ -0,0 +1,4 @@ +[no-cd] +[positional-arguments] +format *ARGS=".": + npx prettier --write "$@" diff --git a/misc/just/language_tests.py b/misc/just/language_tests.py new file mode 100755 index 000000000000..988a77d73cf3 --- /dev/null +++ b/misc/just/language_tests.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Run a whole language test suite for CI. + +Called from just recipes as: + python3 language_tests.py ROOT [ARG...] + +Arguments are already split by `just` (see `set lists`). The first one must be a test +root, which is used to locate the justfile implementing `test` for that suite. +""" + +import os +import subprocess +import sys +from pathlib import Path + + +def main(): + argv = sys.argv[1:] + if not argv: + print("Usage: language_tests.py ROOT [ARG...]", file=sys.stderr) + return 1 + + semmle_code = Path(os.environ["SEMMLE_CODE"]) + # Test roots are absolute, as justfiles build them from `source_dir()`. We run from + # the internal checkout, so relativize them there to keep command lines readable. + # Anything else (flags, environment assignments, relative paths) is passed verbatim. + args = [ + os.path.relpath(arg, semmle_code) if os.path.isabs(arg) else arg + for arg in argv + if arg + ] + + just = os.environ.get("JUST_EXECUTABLE", "just") + + # Find the nearest justfile at or above the first root + justfile_dir = Path(args[0]) + while not (semmle_code / justfile_dir / "justfile").exists(): + parent = justfile_dir.parent + if parent == justfile_dir: + print(f"No justfile found above {args[0]}", file=sys.stderr) + return 1 + justfile_dir = parent + + invocation = [ + just, + "--justfile", + str(justfile_dir / "justfile"), + "test", + "--all-checks", + "--codeql=built", + *args, + ] + + print(f"-> just {' '.join(invocation[1:])}") + try: + subprocess.run(invocation, check=True, cwd=semmle_code) + except subprocess.CalledProcessError as e: + return e.returncode + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(128 + 2) diff --git a/misc/just/lib.just b/misc/just/lib.just new file mode 100644 index 000000000000..8ce335677287 --- /dev/null +++ b/misc/just/lib.just @@ -0,0 +1,31 @@ +# Helper recipes + +import "build.just" +import "format.just" + +# Run language tests for LANGUAGE. +# +# Arguments tagged with `--all-checks=` are held back and only applied when `--all-checks` +# or `+` is passed along, which is how per-language justfiles express the extra checks CI +# runs on top of the default ones. +[no-cd] +[no-exit-message] +[positional-arguments] +@_codeql_test LANGUAGE *ARGS: + {{ py }} "{{ source_dir() }}/codeql_test_run.py" "$@" + +# Run a whole language test suite. The first argument must be a test root. This is +# intended to be called by CI +[no-cd] +[no-exit-message] +[positional-arguments] +@_language_tests *ARGS: _require_semmle_code + {{ py }} "{{ source_dir() }}/language_tests.py" "$@" + +# Run integration tests. Requires an internal repository checkout +[no-cd] +[no-exit-message] +[positional-arguments] +@_integration_test *ARGS: _require_semmle_code + echo "$CMD_BEGIN$SEMMLE_CODE/tools/pytest --codeql=build-as-test $*$CMD_END" + "$SEMMLE_CODE/tools/pytest" --codeql=build-as-test "$@" diff --git a/misc/just/run_on_files.py b/misc/just/run_on_files.py new file mode 100644 index 000000000000..bed9c66b8b93 --- /dev/null +++ b/misc/just/run_on_files.py @@ -0,0 +1,203 @@ +"""Run a command on the files matching the given patterns below the given paths. + +This is a portable `find ... -name -exec {} +`. It exists +because `find` is an unrelated program on Windows, and because a shell command +substitution splits the file names it produces on whitespace, which mangles the many +paths in this repository that contain spaces. + +The command is run once per batch of file names rather than once per file, and the +batches are sized so that no single command line runs into a length limit. Nothing is +run at all when no file matches. +""" + +import argparse +import os +import re +import subprocess +import sys +from fnmatch import fnmatch +from pathlib import Path + +def batch_limit(): + """How many characters of file names to put on one command line. + + Windows caps a whole command line at 32767 characters. Elsewhere the cap is + `ARG_MAX`, which the environment is counted against as well, so that is taken off + along with some slack. This is worth doing rather than assuming the tightest of the + two: `ARG_MAX` is 2MB on Linux, which turns the couple of thousand QL files of a + language into a single invocation rather than several. + + A single argument is capped far lower than the whole line, at 128KB on Linux, and a + command that hands its arguments on through a shell arrives as one of them. Batches + are kept below that too, as the resulting failure is reported by whatever did the + handing on rather than by anything naming this file. + """ + if sys.platform == "win32": + return 30000 + single_argument = 100000 + try: + arg_max = os.sysconf("SC_ARG_MAX") + except (ValueError, OSError): + return 30000 + environment = sum(len(name) + len(value) + 2 for name, value in os.environ.items()) + return max(4096, min(arg_max - environment - 4096, single_argument)) + + +def files_under(paths, patterns, excludes=(), absolute=False, within=None): + """Collect the files matching one of the patterns at or below each path. + + Patterns are matched against the file name, as bazel files are identified by name + rather than by extension. Exclusions are matched against the whole path instead, + which is how a directory of generated files is left alone. That path is the one the + walk built, so an exclusion has to allow for how the paths it is given are spelled: + `*//*` does not match what is walked from `` itself. + + A `within` directory bounds the result to the files below it, for a command that + answers for one project and may be handed a path reaching outside it. + + Symbolic links are not followed, which is what keeps the `bazel-*` convenience + links out of the walk. + """ + boundary = Path(within).resolve() if within else None + + def wanted(path): + if boundary is not None and not path.resolve().is_relative_to(boundary): + return False + return any(fnmatch(path.name, p) for p in patterns) and not any( + fnmatch(str(path), e) for e in excludes + ) + + found = set() + for path in map(Path, paths): + if path.is_file(): + if wanted(path): + found.add(path) + continue + for directory, _, names in os.walk(path): + found.update(p for p in map(Path(directory).joinpath, names) if wanted(p)) + return sorted(os.path.abspath(p) if absolute else str(p) for p in found) + + +def batched(files, limit): + """Split file names into groups that each fit on one command line.""" + batch, length = [], 0 + for file in files: + if batch and length + len(file) + 1 > limit: + yield batch + batch, length = [], 0 + batch.append(file) + length += len(file) + 1 + if batch: + yield batch + + +def comma_separated(value): + """Split an option value listing several patterns. + + Patterns tend to come in groups, and a justfile passes them as one variable, so they + are spelled as one argument here rather than repeated. Repeating the option works + too, which is what lets a list be extended rather than restated. + """ + return value.split(",") + + +def parse_args(): + """Work out what to run, on which files, and what to hide of what it says.""" + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + usage="%(prog)s [option...] [,...] " + " [...] -- [...]", + ) + parser.add_argument( + "--exclude", + action="extend", + default=[], + type=comma_separated, + metavar="[,...]", + help="leave out files whose path matches, repeatable", + ) + parser.add_argument( + "--absolute", + action="store_true", + help="pass absolute file names, needed when the command runs elsewhere", + ) + parser.add_argument( + "--chdir", + metavar="", + help="run the command from here, for one that must be run from a project root", + ) + parser.add_argument( + "--within", + metavar="", + help="leave out files outside this directory, for a command answering for one " + "project that may be handed a path reaching beyond it", + ) + parser.add_argument( + "--drop", + action="append", + default=[], + metavar="", + help="hide matching lines of the command's output, repeatable", + ) + parser.add_argument( + "patterns", + metavar="[,...]", + type=comma_separated, + help="what to match file names against", + ) + parser.add_argument( + "rest", + nargs=argparse.REMAINDER, + metavar=" [...] -- [...]", + help="the command, then the paths to search, separated by the last `--` so " + "that the command may contain one of its own", + ) + args = parser.parse_args() + if "--" not in args.rest: + parser.error("the paths must be separated from the command by `--`") + separator = len(args.rest) - 1 - args.rest[::-1].index("--") + args.command, args.paths = args.rest[:separator], args.rest[separator + 1 :] + if not args.command: + parser.error("no command given") + return args + + +def run(command, drops, chdir=None): + """Run the command, hiding the lines of its output that were asked to be hidden. + + Told nothing to hide, the command keeps this process' own output streams, so that + it can do as it likes with them. Otherwise its diagnostics are read a line at a + time and passed on as they arrive, which is what keeps a long run's progress + visible. Only what was named is hidden, so an unforeseen message still gets out. + + Note that these tools report on their progress over standard error rather than + standard output, which is left alone here. + """ + if not drops: + return subprocess.run(command, cwd=chdir).returncode + hidden = re.compile("|".join(drops)) + process = subprocess.Popen( + command, cwd=chdir, stderr=subprocess.PIPE, text=True, bufsize=1 + ) + for line in process.stderr: + if not hidden.search(line): + sys.stderr.write(line) + sys.stderr.flush() + return process.wait() + + +def main(): + args = parse_args() + files = files_under( + args.paths, args.patterns, args.exclude, args.absolute, args.within + ) + limit = batch_limit() - sum(len(argument) + 1 for argument in args.command) + status = 0 + for batch in batched(files, limit): + status = run([*args.command, *batch], args.drop, args.chdir) or status + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/misc/just/semmle-code-stub.just b/misc/just/semmle-code-stub.just new file mode 100644 index 000000000000..14733ffb648e --- /dev/null +++ b/misc/just/semmle-code-stub.just @@ -0,0 +1 @@ +export SEMMLE_CODE := "" diff --git a/python/justfile b/python/justfile new file mode 100644 index 000000000000..33580aa1e05c --- /dev/null +++ b/python/justfile @@ -0,0 +1,26 @@ +import '../lib.just' +import 'ql/justfile' + +[group('build')] +build: (_build_dist "python") + +# Long filename needed for extractor tests (too long for Git on Windows) +[no-cd] +@_ensure_long_filename: + #!/usr/bin/env bash + longfile="$SEMMLE_CODE/ql/python/ql/test/extractor-tests/long_path/really_rather_too_long_for_windows_path_length/with_unecessarily_longwinded_and_verbose_sub_folder/extremely_long_module_name_with_lots_of_digits_at_the_end_000000000000000000000000000000000000000000000000000000000000000000/test0000000000000000000000000000000000000000000000000000000.py" + mkdir -p "$(dirname "$longfile")" + touch "$longfile" + +_tests := source_dir() / 'ql/test' + +_shared_roots := [_tests / 'library-tests', _tests / 'query-tests', _tests / 'extractor-tests', _tests / 'experimental'] + +roots_2 := _shared_roots ++ [_tests / '2'] +roots_3 := _shared_roots ++ [_tests / 'modelling', _tests / '3'] + +[group('test')] +language-tests-2 *EXTRA_ARGS: _ensure_long_filename (_language_tests (roots_2 ++ _v2_env ++ EXTRA_ARGS)) + +[group('test')] +language-tests-3 *EXTRA_ARGS: _ensure_long_filename (_language_tests (roots_3 ++ _v3_env ++ EXTRA_ARGS)) diff --git a/python/ql/integration-tests/justfile b/python/ql/integration-tests/justfile new file mode 100644 index 000000000000..8392c2b3ba40 --- /dev/null +++ b/python/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_integration_test ARGS) diff --git a/python/ql/justfile b/python/ql/justfile new file mode 100644 index 000000000000..45ed8d733cff --- /dev/null +++ b/python/ql/justfile @@ -0,0 +1,12 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" + +python_version := env("python_version", "3") + +_v2_env := ['CODEQL_EXTRACTOR_PYTHON_ANALYSIS_VERSION=2', 'CODEQL_PYTHON_LEGACY_TEST_EXTRACTION_VERSION=2'] +_v3_env := ['CODEQL_PYTHON_LEGACY_TEST_EXTRACTION_VERSION=3'] +_python_env := if python_version == "2" { _v2_env } else { _v3_env } diff --git a/python/ql/test/justfile b/python/ql/test/justfile new file mode 100644 index 000000000000..0f44a489e82f --- /dev/null +++ b/python/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := _python_env + +all_checks := default_db_checks ++ ['--consistency-queries=' + consistency_queries] + +[no-cd] +test *ARGS=".": (_codeql_test "python" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/ruby/justfile b/ruby/justfile new file mode 100644 index 000000000000..b9cc748f169f --- /dev/null +++ b/ruby/justfile @@ -0,0 +1,9 @@ +import '../lib.just' + +[group('build')] +build: (_build_dist "ruby") + +roots := [source_dir() / 'ql/test'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) diff --git a/ruby/ql/integration-tests/justfile b/ruby/ql/integration-tests/justfile new file mode 100644 index 000000000000..8392c2b3ba40 --- /dev/null +++ b/ruby/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_integration_test ARGS) diff --git a/ruby/ql/justfile b/ruby/ql/justfile new file mode 100644 index 000000000000..ff0e4c1090f3 --- /dev/null +++ b/ruby/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" diff --git a/ruby/ql/test/justfile b/ruby/ql/test/justfile new file mode 100644 index 000000000000..9673cebe0370 --- /dev/null +++ b/ruby/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := [] + +all_checks := default_db_checks ++ ['--check-undefined-labels', '--check-unused-labels', '--check-repeated-labels', '--check-redefined-labels', '--check-use-before-definition', '--consistency-queries=' + consistency_queries] + +[no-cd] +test *ARGS=".": (_codeql_test "ruby" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/rust/codegen/codegen.sh b/rust/codegen/codegen.sh index 2d415009aed8..726ff138db78 100755 --- a/rust/codegen/codegen.sh +++ b/rust/codegen/codegen.sh @@ -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")" diff --git a/rust/justfile b/rust/justfile new file mode 100644 index 000000000000..9877da7b9f1f --- /dev/null +++ b/rust/justfile @@ -0,0 +1,17 @@ +import '../lib.just' + +install: (_bazel ['run', '@codeql//rust:install']) + +[group('build')] +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']) + +roots := [source_dir() / 'ql/test'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) diff --git a/rust/lint.py b/rust/lint.py index 600a888649e9..3ace1667a464 100755 --- a/rust/lint.py +++ b/rust/lint.py @@ -4,6 +4,15 @@ 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 +21,33 @@ def tool(name): return ret -this_dir = pathlib.Path(__file__).resolve().parent +def main(): + args = options() + this_dir = pathlib.Path(__file__).resolve().parent + + cargo = tool("cargo") + bazel = tool("bazel") -cargo = tool("cargo") -bazel = tool("bazel") + runs = [] -runs = [] + def run(tool, args, *, cwd=this_dir): + print("+", tool, args) + runs.append(subprocess.run([tool] + args.split(), cwd=cwd)) -def run(tool, args, *, cwd=this_dir): - print("+", tool, args) - runs.append(subprocess.run([tool] + args.split(), cwd=cwd)) + # 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) -# 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") + return max(r.returncode for r in runs) -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)) +if __name__ == "__main__": + sys.exit(main()) diff --git a/rust/ql/integration-tests/justfile b/rust/ql/integration-tests/justfile new file mode 100644 index 000000000000..2ee4b833c128 --- /dev/null +++ b/rust/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_if_not_on_ci_just ['generate']) (_integration_test ARGS) diff --git a/rust/ql/justfile b/rust/ql/justfile new file mode 100644 index 000000000000..ff0e4c1090f3 --- /dev/null +++ b/rust/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" diff --git a/rust/ql/test/justfile b/rust/ql/test/justfile new file mode 100644 index 000000000000..8d5d6c05da2d --- /dev/null +++ b/rust/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := [] + +all_checks := default_db_checks ++ ['--consistency-queries=' + consistency_queries] + +[no-cd] +test *ARGS=".": (_codeql_test "rust" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/swift/justfile b/swift/justfile new file mode 100644 index 000000000000..b565923cae08 --- /dev/null +++ b/swift/justfile @@ -0,0 +1,18 @@ +import '../lib.just' + +install: (_bazel ['run', '@codeql//swift:install']) + +[group('build')] +build: (_build_dist "swift") + +generate: (_bazel ['run', '@codeql//swift/codegen']) + +format *ARGS=".": (_format_cpp ARGS) + +roots := [source_dir() / 'ql/test'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) + +[group('test')] +extra-tests: (_sembuild "target/test/check-queries-swift") (_sembuild "target/test/check-db-upgrades-swift") (_sembuild "target/test/check-db-downgrades-swift") diff --git a/swift/ql/integration-tests/justfile b/swift/ql/integration-tests/justfile new file mode 100644 index 000000000000..097faf5baebf --- /dev/null +++ b/swift/ql/integration-tests/justfile @@ -0,0 +1,8 @@ +import "../../../lib.just" + +# Integration tests are slow and need an internal checkout, so they only run when +# asked for by name. +explicit_verbs := ['test'] + +[no-cd] +test *ARGS=".": (_just "generate") (_integration_test ARGS) diff --git a/swift/ql/justfile b/swift/ql/justfile new file mode 100644 index 000000000000..ff0e4c1090f3 --- /dev/null +++ b/swift/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" diff --git a/swift/ql/test/justfile b/swift/ql/test/justfile new file mode 100644 index 000000000000..6f15ac6d0723 --- /dev/null +++ b/swift/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := [] + +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" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/unified/extractor/justfile b/unified/extractor/justfile new file mode 100644 index 000000000000..f6a6417ed867 --- /dev/null +++ b/unified/extractor/justfile @@ -0,0 +1,4 @@ +import '../../lib.just' + +[group('test')] +test *BAZEL_ARGS: (_bazel (['test', '--build_tests_only', '@codeql//unified/extractor/...'] ++ BAZEL_ARGS)) diff --git a/unified/justfile b/unified/justfile new file mode 100644 index 000000000000..610ec84901c0 --- /dev/null +++ b/unified/justfile @@ -0,0 +1,12 @@ +import '../lib.just' + +[group('build')] +build: (_build_dist "unified") + +roots := [source_dir() / 'ql/test'] + +[group('test')] +language-tests *EXTRA_ARGS: (_language_tests (roots ++ EXTRA_ARGS)) + +[group('test')] +extractor-tests *BAZEL_ARGS: (_bazel (['test', '--build_tests_only', '@codeql//unified/...'] ++ BAZEL_ARGS)) diff --git a/unified/ql/justfile b/unified/ql/justfile new file mode 100644 index 000000000000..ff0e4c1090f3 --- /dev/null +++ b/unified/ql/justfile @@ -0,0 +1,6 @@ +import "../../lib.just" + +[no-cd] +format *ARGS=".": (_format_ql ARGS) + +consistency_queries := source_dir() / "consistency-queries" diff --git a/unified/ql/test/justfile b/unified/ql/test/justfile new file mode 100644 index 000000000000..1ca509465bd8 --- /dev/null +++ b/unified/ql/test/justfile @@ -0,0 +1,11 @@ +import "../justfile" + +# A whole language test suite is slow, so it only runs when asked for by name. +explicit_verbs := ['test'] + +base_flags := [] + +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 "unified" (base_flags ++ prepend('--all-checks=', all_checks) ++ ARGS)) diff --git a/unified/swift-syntax-rs/justfile b/unified/swift-syntax-rs/justfile new file mode 100644 index 000000000000..021bb0e9e75e --- /dev/null +++ b/unified/swift-syntax-rs/justfile @@ -0,0 +1,4 @@ +import '../../lib.just' + +[group('test')] +test *BAZEL_ARGS: (_bazel (['test', '--build_tests_only', '@codeql//unified/swift-syntax-rs/...'] ++ BAZEL_ARGS))