[engine] Add check.with_args(...) method
This allows downstream repositories to override arguments to checks in
shared checks repositories, such as the path to an executable to run.
I also added a `with_name()` method, mainly to make it easier to test
`with_args()` (since it's not allowed to register multiple checks with
the same name) but it may also be useful for downstream users.
Change-Id: I43e50768bbf9c90d3c9aaa28ef447ef330c31d5c
Reviewed-on: https://fuchsia-review.googlesource.com/c/shac-project/shac/+/919559
Reviewed-by: Marc-Antoine Ruel <maruel@google.com>
Commit-Queue: Auto-Submit <auto-submit@fuchsia-infra.iam.gserviceaccount.com>
Fuchsia-Auto-Submit: Oliver Newman <olivernewman@google.com>
diff --git a/checks/check_doc.star b/checks/check_doc.star
index 20d4118..56272be 100644
--- a/checks/check_doc.star
+++ b/checks/check_doc.star
@@ -12,15 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-load("//doc/stdlib.star", doc_ctx = "ctx", doc_shac = "shac")
+load("//doc/stdlib.star", doc_check = "check", doc_ctx = "ctx", doc_shac = "shac")
+
+a_check = shac.check(lambda ctx: None, name = "a_check")
def check_docs(ctx):
- """Validates that the `ctx` and `shac` structs in //doc/stdlib.star are
- up-to-date.
+ """Validates that structs in //doc/stdlib.star are up-to-date.
- Specifically, it should (recursively) have all the same fields with all the
- same types as the `shac` global and the `ctx` object that gets passed to
- checks.
+ Specifically, documented structs should (recursively) have all the same
+ fields with all the same types as the real objects.
Function signatures are not validated.
@@ -28,25 +28,24 @@
Starlark interpreter to get function signatures.
Args:
- ctx: A ctx instance.
+ ctx: A ctx instance.
"""
- want = _struct_signature(ctx)
- got = _struct_signature(doc_ctx)
- if want != got:
- ctx.emit.finding(
- level = "error",
- message = "stdlib.star needs to be updated. Want:\n%s\nGot:\n%s" % (want, got),
- )
- want = _struct_signature(shac)
- got = _struct_signature(doc_shac)
- if want != got:
- ctx.emit.finding(
- level = "error",
- message = "stdlib.star needs to be updated. Want:\n%s\nGot:\n%s" % (want, got),
- )
+ pairs = [
+ (ctx, doc_ctx),
+ (shac, doc_shac),
+ (a_check, doc_check),
+ ]
+ for actual, documented in pairs:
+ want = _struct_signature(actual)
+ got = _struct_signature(documented)
+ if want != got:
+ ctx.emit.finding(
+ level = "error",
+ message = "stdlib.star needs to be updated. Want:\n%s\nGot:\n%s" % (want, got),
+ )
def _struct_signature(s):
- if type(s) != type(struct()):
+ if type(s) != type(struct()) and not type(s).startswith("shac."):
# stdlib.star uses dummy functions instead of actual builtin functions, they
# should be considered equivalent.
if type(s) == "builtin_function_or_method":
diff --git a/doc/stdlib.md b/doc/stdlib.md
index 55643c5..b63737c 100644
--- a/doc/stdlib.md
+++ b/doc/stdlib.md
@@ -20,6 +20,7 @@
## Table of contents
- [shac](#shac)
+- [check](#check)
- [ctx](#ctx)
- [dir](#dir)
- [fail](#fail)
@@ -101,6 +102,66 @@
* **check**: `shac.check()` object or Starlark function that is called back to implement the check.
+## check
+
+check is the object returned by shac.check().
+
+Fields:
+
+- with_args
+- with_name
+
+## check.with_args
+
+Create a copy of the check with keyword arguments overridden.
+
+### Example
+
+```python
+def cb(ctx, level = "warning"):
+ ctx.emit.finding(
+ level = level,
+ message = "Found an issue",
+ )
+
+warning_check = shac.check(cb)
+error_check = warning_check.with_args(level = "error")
+```
+
+### Arguments
+
+* **\*\*kwargs**: Overridden keyword arguments.
+
+## check.with_name
+
+Create a copy of the check with name overridden.
+
+Useful for changing the name of checks provided by shared Starlark
+libraries.
+
+### Example
+
+Shared library code:
+
+```python
+def _check_with_bad_name(ctx):
+ pass
+
+check_with_bad_name = shac.check(_check_with_bad_name)
+```
+
+Downstream code:
+
+```
+load("@library", "check_with_bad_name")
+
+shac.register_check(check_with_bad_name.with_name("better_name"))
+```
+
+### Arguments
+
+* **name**: The new name of the check.
+
## ctx
ctx is the object passed to [shac.register_check(...)](#shac.register-check) callback.
diff --git a/doc/stdlib.star b/doc/stdlib.star
index 91336f3..84473dc 100644
--- a/doc/stdlib.star
+++ b/doc/stdlib.star
@@ -104,6 +104,65 @@
version = (0, 0, 1),
)
+## Methods on the shac.check object.
+
+def _check_with_args(**kwargs):
+ """Create a copy of the check with keyword arguments overridden.
+
+ Example:
+
+ ```python
+ def cb(ctx, level = "warning"):
+ ctx.emit.finding(
+ level = level,
+ message = "Found an issue",
+ )
+
+ warning_check = shac.check(cb)
+ error_check = warning_check.with_args(level = "error")
+ ```
+
+ Args:
+ **kwargs: Overridden keyword arguments.
+ """
+ pass
+
+def _check_with_name(name):
+ """Create a copy of the check with name overridden.
+
+ Useful for changing the name of checks provided by shared Starlark
+ libraries.
+
+ Example:
+
+ Shared library code:
+
+ ```python
+ def _check_with_bad_name(ctx):
+ pass
+
+ check_with_bad_name = shac.check(_check_with_bad_name)
+ ```
+
+ Downstream code:
+
+ ```
+ load("@library", "check_with_bad_name")
+
+ shac.register_check(check_with_bad_name.with_name("better_name"))
+ ```
+
+ Args:
+ name: The new name of the check.
+ """
+ pass
+
+# check is the object returned by shac.check().
+check = struct(
+ with_args = _check_with_args,
+ with_name = _check_with_name,
+)
+
## Methods inside ctx object.
def _ctx_emit_finding(level, message, filepath = None, line = None, col = None, end_line = None, end_col = None, replacements = None):
diff --git a/internal/engine/run.go b/internal/engine/run.go
index 244269f..0851e2d 100644
--- a/internal/engine/run.go
+++ b/internal/engine/run.go
@@ -668,7 +668,7 @@
func (c *registeredCheck) call(ctx context.Context, env *starlarkEnv, args starlark.Tuple, pi printImpl) error {
ctx = context.WithValue(ctx, &checkCtxKey, c)
th := env.thread(ctx, c.name, pi)
- if r, err := starlark.Call(th, c.impl, args, nil); err != nil {
+ if r, err := starlark.Call(th, c.impl, args, c.kwargs); err != nil {
if c.failErr != nil {
// fail() was called, return this error since this is an abnormal failure.
return c.failErr
diff --git a/internal/engine/run_test.go b/internal/engine/run_test.go
index 84f5424..32b1ca1 100644
--- a/internal/engine/run_test.go
+++ b/internal/engine/run_test.go
@@ -179,7 +179,7 @@
i := i
t.Run(data[i].name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, data[i].dir, "", false, data[i].want)
+ testStarlarkPrint(t, data[i].dir, "", false, false, data[i].want)
})
}
}
@@ -266,7 +266,7 @@
t.Fatal(err)
}
}()
- testStarlarkPrint(t, root, data[i].starlarkFile, false, data[i].want, data[i].files...)
+ testStarlarkPrint(t, root, data[i].starlarkFile, false, false, data[i].want, data[i].files...)
})
}
@@ -439,7 +439,7 @@
i := i
t.Run(data[i].name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, root, data[i].name, false, data[i].want)
+ testStarlarkPrint(t, root, data[i].name, false, false, data[i].want)
})
}
@@ -536,13 +536,13 @@
"a.txt: \n" +
scmStarlarkFiles("") +
"\n"
- testStarlarkPrint(t, root, "ctx-scm-affected_files.star", false, want)
+ testStarlarkPrint(t, root, "ctx-scm-affected_files.star", false, false, want)
})
t.Run("affected-new_lines", func(t *testing.T) {
t.Parallel()
want := "[//ctx-scm-affected_files-new_lines.star:33] a.txt\n" +
"1: First file\n"
- testStarlarkPrint(t, root, "ctx-scm-affected_files-new_lines.star", false, want)
+ testStarlarkPrint(t, root, "ctx-scm-affected_files-new_lines.star", false, false, want)
})
t.Run("all", func(t *testing.T) {
t.Parallel()
@@ -550,7 +550,7 @@
"a.txt: \n" +
scmStarlarkFiles("") +
"\n"
- testStarlarkPrint(t, root, "ctx-scm-all_files.star", false, want)
+ testStarlarkPrint(t, root, "ctx-scm-all_files.star", false, false, want)
})
}
@@ -611,7 +611,7 @@
}
t.Run(name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, root, data[i].name, data[i].all, data[i].want)
+ testStarlarkPrint(t, root, data[i].name, data[i].all, false, data[i].want)
})
}
}
@@ -665,7 +665,7 @@
}
t.Run(name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, root, data[i].name, data[i].all, data[i].want)
+ testStarlarkPrint(t, root, data[i].name, data[i].all, false, data[i].want)
})
}
}
@@ -705,7 +705,7 @@
i := i
t.Run(data[i].name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, root, data[i].name, false, data[i].want)
+ testStarlarkPrint(t, root, data[i].name, false, false, data[i].want)
})
}
}
@@ -823,7 +823,7 @@
i := i
t.Run(data[i].name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, root, data[i].name, false, data[i].want)
+ testStarlarkPrint(t, root, data[i].name, false, false, data[i].want)
})
}
}
@@ -887,7 +887,7 @@
i := i
t.Run(data[i].name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, root, data[i].name, false, data[i].want)
+ testStarlarkPrint(t, root, data[i].name, false, false, data[i].want)
})
}
}
@@ -938,7 +938,7 @@
}
t.Run(name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, root, data[i].name, data[i].all, data[i].want)
+ testStarlarkPrint(t, root, data[i].name, data[i].all, false, data[i].want)
})
}
}
@@ -1549,6 +1549,21 @@
" //load-recurse.star:15:1: in <toplevel>\n",
},
{
+ "shac-check-with_args-ctx.star",
+ "with_args: \"ctx\" argument cannot be overridden",
+ " //shac-check-with_args-ctx.star:18:45: in <toplevel>\n",
+ },
+ {
+ "shac-check-with_args-nonexistent_kwarg.star",
+ "with_args: invalid argument \"nonexistent\", must be one of: (foo)",
+ " //shac-check-with_args-nonexistent_kwarg.star:18:45: in <toplevel>\n",
+ },
+ {
+ "shac-check-with_args-positional_args.star",
+ "with_args: only keyword arguments are allowed",
+ " //shac-check-with_args-positional_args.star:18:45: in <toplevel>\n",
+ },
+ {
"shac-immutable.star",
"can't assign to .key field of struct",
" //shac-immutable.star:16:5: in <toplevel>\n",
@@ -1718,7 +1733,7 @@
if err := os.Chmod(filepath.Join(root, "http_get.sh"), 0o700); err != nil {
t.Fatal(err)
}
- testStarlarkPrint(t, root, "shac.star", false, data[i].want)
+ testStarlarkPrint(t, root, "shac.star", false, false, data[i].want)
})
}
}
@@ -1872,20 +1887,21 @@
p, got := enumDir(t, "print")
v := fmt.Sprintf("(%d, %d, %d)", Version[0], Version[1], Version[2])
data := []struct {
- name string
- want string
+ name string
+ want string
+ ignoreOrder bool
}{
{
- "ctx-io-read_file-size.star",
- "[//ctx-io-read_file-size.star:16] {\n \"key\":\n",
+ name: "ctx-io-read_file-size.star",
+ want: "[//ctx-io-read_file-size.star:16] {\n \"key\":\n",
},
{
- "ctx-io-read_file.star",
- "[//ctx-io-read_file.star:17] {\"key\": \"value\"}\n",
+ name: "ctx-io-read_file.star",
+ want: "[//ctx-io-read_file.star:17] {\"key\": \"value\"}\n",
},
{
- "ctx-io-tempdir.star",
- func() string {
+ name: "ctx-io-tempdir.star",
+ want: func() string {
if runtime.GOOS == "windows" {
return "[//ctx-io-tempdir.star:16] \\0\\0\n" +
"[//ctx-io-tempdir.star:17] \\0\\1\n" +
@@ -1897,17 +1913,17 @@
}(),
},
{
- "ctx-io-tempfile.star",
- "[//ctx-io-tempfile.star:18] first\nfile\ncontents\n\n" +
+ name: "ctx-io-tempfile.star",
+ want: "[//ctx-io-tempfile.star:18] first\nfile\ncontents\n\n" +
"[//ctx-io-tempfile.star:19] contents\nof\nsecond\nfile\n\n",
},
{
- "ctx-os-exec-10Mib.star",
- "[//ctx-os-exec-10Mib.star:17] 0\n",
+ name: "ctx-os-exec-10Mib.star",
+ want: "[//ctx-os-exec-10Mib.star:17] 0\n",
},
{
- "ctx-os-exec-abspath.star",
- func() string {
+ name: "ctx-os-exec-abspath.star",
+ want: func() string {
// TODO(maruel): Decide if we want to do CRLF translation automatically.
if runtime.GOOS == "windows" {
return "[//ctx-os-exec-abspath.star:17] Hello, world\r\n\n"
@@ -1916,8 +1932,8 @@
}(),
},
{
- "ctx-os-exec-env.star",
- func() string {
+ name: "ctx-os-exec-env.star",
+ want: func() string {
// TODO(maruel): Decide if we want to do CRLF translation automatically.
if runtime.GOOS == "windows" {
return "[//ctx-os-exec-env.star:24] FOO=foo-value\r\nBAR=bar-value\n"
@@ -1926,12 +1942,12 @@
}(),
},
{
- "ctx-os-exec-parallel.star",
- strings.Repeat("[//ctx-os-exec-parallel.star:27] Hello, world\n", 10),
+ name: "ctx-os-exec-parallel.star",
+ want: strings.Repeat("[//ctx-os-exec-parallel.star:27] Hello, world\n", 10),
},
{
- "ctx-os-exec-relpath.star",
- func() string {
+ name: "ctx-os-exec-relpath.star",
+ want: func() string {
// TODO(maruel): Decide if we want to do CRLF translation automatically.
if runtime.GOOS == "windows" {
return "[//ctx-os-exec-relpath.star:17] Hello from a nested file\r\n\n"
@@ -1940,8 +1956,8 @@
}(),
},
{
- "ctx-os-exec-stdin.star",
- "[//ctx-os-exec-stdin.star:30] stdout given NoneType for stdin:\n" +
+ name: "ctx-os-exec-stdin.star",
+ want: "[//ctx-os-exec-stdin.star:30] stdout given NoneType for stdin:\n" +
"\n" +
"[//ctx-os-exec-stdin.star:30] stdout given string for stdin:\n" +
"hello\nfrom\nstdin\nstring\n" +
@@ -1949,75 +1965,88 @@
"hello\nfrom\nstdin\nbytes\n",
},
{
- "ctx-os-exec-success.star",
- "[//ctx-os-exec-success.star:21] retcode: 0\n" +
+ name: "ctx-os-exec-success.star",
+ want: "[//ctx-os-exec-success.star:21] retcode: 0\n" +
"[//ctx-os-exec-success.star:22] stdout: hello from stdout\n" +
"[//ctx-os-exec-success.star:23] stderr: hello from stderr\n",
},
{
- "ctx-platform.star",
- "[//ctx-platform.star:16] OS: " + runtime.GOOS + "\n" +
+ name: "ctx-platform.star",
+ want: "[//ctx-platform.star:16] OS: " + runtime.GOOS + "\n" +
"[//ctx-platform.star:17] Arch: " + runtime.GOARCH + "\n",
},
{
- "ctx-re-allmatches.star",
- "[//ctx-re-allmatches.star:17] ()\n" +
+ name: "ctx-re-allmatches.star",
+ want: "[//ctx-re-allmatches.star:17] ()\n" +
"[//ctx-re-allmatches.star:20] (match(groups = (\"TODO(foo)\",), offset = 4), match(groups = (\"TODO(bar)\",), offset = 14))\n" +
"[//ctx-re-allmatches.star:23] (match(groups = (\"anc\", \"n\", \"c\"), offset = 0),)\n",
},
{
- "ctx-re-match.star",
- "[//ctx-re-match.star:17] None\n" +
+ name: "ctx-re-match.star",
+ want: "[//ctx-re-match.star:17] None\n" +
"[//ctx-re-match.star:20] match(groups = (\"TODO(foo)\",), offset = 4)\n" +
"[//ctx-re-match.star:23] match(groups = (\"anc\", \"n\", \"c\"), offset = 0)\n" +
"[//ctx-re-match.star:26] match(groups = (\"a\", None), offset = 0)\n",
},
{
- "dir-ctx.star",
- "[//dir-ctx.star:16] [\"emit\", \"io\", \"os\", \"platform\", \"re\", \"scm\", \"vars\"]\n",
+ name: "dir-ctx.star",
+ want: "[//dir-ctx.star:16] [\"emit\", \"io\", \"os\", \"platform\", \"re\", \"scm\", \"vars\"]\n",
},
{
- "dir-shac.star",
- "[//dir-shac.star:15] [\"check\", \"commit_hash\", \"register_check\", \"version\"]\n",
+ name: "dir-shac.star",
+ want: "[//dir-shac.star:15] [\"check\", \"commit_hash\", \"register_check\", \"version\"]\n",
},
{
- "load-diamond_dependency.star",
- "[//load-diamond_dependency.star:18] i am a constant\n" +
+ name: "load-diamond_dependency.star",
+ want: "[//load-diamond_dependency.star:18] i am a constant\n" +
"[//load-diamond_dependency.star:19] i am a constant #2\n",
},
{
- "print-shac-version.star",
- "[//print-shac-version.star:15] " + v + "\n",
+ name: "print-shac-version.star",
+ want: "[//print-shac-version.star:15] " + v + "\n",
},
{
- "shac-check.star",
- "[//shac-check.star:19] str(check): <check hello_world>\n" +
+ name: "shac-check-with_args.star",
+ want: "[//shac-check-with_args.star:33] print_hello_check: <check print_hello>\n" +
+ "[//shac-check-with_args.star:34] print_goodbye_check: <check print_goodbye>\n" +
+ "[//shac-check-with_args.star:35] print_hello_again_check: <check print_hello_again>\n" +
+ "[//shac-check-with_args.star:16] hello again\n" +
+ "[//shac-check-with_args.star:16] goodbye\n" +
+ "[//shac-check-with_args.star:16] hello\n",
+ // Ordering of output lines depends on the order in which checks are
+ // run, which is nondeterministic and doesn't matter for the
+ // purposes of this test.
+ ignoreOrder: true,
+ },
+ {
+ name: "shac-check.star",
+ want: "[//shac-check.star:19] str(check): <check hello_world>\n" +
"[//shac-check.star:20] type(check): shac.check\n" +
"[//shac-check.star:21] bool(check): True\n" +
"[//shac-check.star:26] hashed: set([<check hello_world>])\n",
},
{
- "shac-register_check-object.star",
- "[//shac-register_check-object.star:16] running from a check object\n",
+ name: "shac-register_check-object.star",
+ want: "[//shac-register_check-object.star:16] running from a check object\n",
},
{
- "shac-register_check-optional_param.star",
- "[//shac-register_check-optional_param.star:16] optional_param=\"optional-param-value\"\n",
+ name: "shac-register_check-optional_param.star",
+ want: "[//shac-register_check-optional_param.star:16] optional_param=\"optional-param-value\"\n",
},
{
- "shac-register_check.star",
- "[//shac-register_check.star:16] running\n",
+ name: "shac-register_check.star",
+ want: "[//shac-register_check.star:16] running\n",
},
{
- "subprocess.star",
- "[//subprocess.star:17] str(proc): <subprocess \"echo hello\">\n" +
+ name: "subprocess.star",
+ want: "[//subprocess.star:17] str(proc): <subprocess \"echo hello\">\n" +
"[//subprocess.star:18] type(proc): subprocess\n" +
"[//subprocess.star:19] bool(proc): True\n" +
"[//subprocess.star:20] dir(proc): [\"wait\"]\n",
},
{
- "true.star",
- "[//true.star:15] True\n",
+ name: "true.star",
+ want: "[//true.star:15] True\n",
},
}
want := make([]string, len(data))
@@ -2031,7 +2060,7 @@
i := i
t.Run(data[i].name, func(t *testing.T) {
t.Parallel()
- testStarlarkPrint(t, p, data[i].name, false, data[i].want)
+ testStarlarkPrint(t, p, data[i].name, false, data[i].ignoreOrder, data[i].want)
})
}
}
@@ -2069,32 +2098,44 @@
"\n" +
"[//ctx-os-exec-filesystem_sandbox.star:21] sandbox_write.sh retcode: 1\n" +
"[//ctx-os-exec-filesystem_sandbox.star:22] touch: cannot touch 'file.txt': Read-only file system\n\n"
- testStarlarkPrint(t, root, "ctx-os-exec-filesystem_sandbox.star", false, want)
+ testStarlarkPrint(t, root, "ctx-os-exec-filesystem_sandbox.star", false, false, want)
}
func TestRun_Vendored(t *testing.T) {
t.Parallel()
dir := t.TempDir()
copyTree(t, dir, "testdata/vendored", nil)
- testStarlarkPrint(t, dir, "shac.star", false, "[//shac.star:17] True\n")
+ testStarlarkPrint(t, dir, "shac.star", false, false, "[//shac.star:17] True\n")
}
// Utilities
// testStarlarkPrint test a starlark file that calls print().
-func testStarlarkPrint(t testing.TB, root, name string, all bool, want string, files ...string) {
+func testStarlarkPrint(t testing.TB, root, name string, all bool, ignoreOrder bool, want string, files ...string) {
r := reportPrint{reportNoPrint: reportNoPrint{t: t}}
o := Options{Report: &r, Dir: root, AllFiles: all, EntryPoint: name, Files: files}
if err := Run(context.Background(), &o); err != nil {
t.Helper()
t.Fatal(err)
}
- if diff := cmp.Diff(want, r.b.String()); diff != "" {
+ got := r.b.String()
+ if ignoreOrder {
+ want = sortLines(want)
+ got = sortLines(got)
+ }
+ if diff := cmp.Diff(want, got); diff != "" {
t.Helper()
t.Fatalf("mismatch (-want +got):\n%s", diff)
}
}
+func sortLines(str string) string {
+ str = strings.TrimSuffix(str, "\n")
+ lines := strings.Split(str, "\n")
+ slices.Sort(lines)
+ return strings.Join(lines, "\n") + "\n"
+}
+
func enumDir(t *testing.T, name string) (string, []string) {
p, err := filepath.Abs(filepath.Join("testdata", name))
if err != nil {
diff --git a/internal/engine/runtime.go b/internal/engine/runtime.go
index 041ba62..2073526 100644
--- a/internal/engine/runtime.go
+++ b/internal/engine/runtime.go
@@ -126,19 +126,20 @@
ctx := getContext(th)
s := ctxShacState(ctx)
val, err := f(ctx, s)
- // starlark.UnpackArgs already adds the function name prefix to errors
- // it returns, so make sure not to duplicate the prefix if it's already
- // there.
- if err != nil && !strings.HasPrefix(err.Error(), name+": ") {
- err = fmt.Errorf("%s: %w", name, err)
+ if err != nil {
+ // starlark.UnpackArgs already adds the function name prefix to errors
+ // it returns, so make sure not to duplicate the prefix if it's already
+ // there.
+ if !strings.HasPrefix(err.Error(), name+": ") {
+ err = fmt.Errorf("%s: %w", name, err)
+ }
+ return nil, err
}
- if val != nil {
- // All values returned by builtins are immutable. This is not a hard
- // requirement, and can be relaxed if there's a use case for mutable
- // return values, but it's still a sensible default.
- val.Freeze()
- }
- return val, err
+ // All values returned by builtins are immutable. This is not a hard
+ // requirement, and can be relaxed if there's a use case for mutable
+ // return values, but it's still a sensible default.
+ val.Freeze()
+ return val, nil
}
func newBuiltinNone(name string, f func(ctx context.Context, s *shacState, name string, args starlark.Tuple, kwargs []starlark.Tuple) error) *starlark.Builtin {
diff --git a/internal/engine/runtime_shac_check.go b/internal/engine/runtime_shac_check.go
index 934598a..e6ec24f 100644
--- a/internal/engine/runtime_shac_check.go
+++ b/internal/engine/runtime_shac_check.go
@@ -18,6 +18,8 @@
"context"
"errors"
"fmt"
+ "log"
+ "slices"
"strings"
"go.starlark.net/starlark"
@@ -77,9 +79,10 @@
name string
// Whether the check is an auto-formatter or not.
formatter bool
+ kwargs []starlark.Tuple
}
-var _ starlark.Value = (*check)(nil)
+var _ starlark.HasAttrs = (*check)(nil)
func (c *check) String() string {
return fmt.Sprintf("<check %s>", c.name)
@@ -102,3 +105,90 @@
// hashing just the name of the check seems reasonable.
return starlark.String(c.name).Hash()
}
+
+func (c *check) Attr(name string) (starlark.Value, error) {
+ switch name {
+ case "with_args":
+ return checkWithArgsBuiltin.BindReceiver(c), nil
+ case "with_name":
+ return checkWithNameBuiltin.BindReceiver(c), nil
+ default:
+ return nil, nil
+ }
+}
+
+func (c *check) AttrNames() []string {
+ return []string{"with_args", "with_name"}
+}
+
+func (c *check) withName(name string) (starlark.Value, error) {
+ // Make a copy to modify.
+ res := *c
+ res.name = name
+ return &res, nil
+}
+
+func (c *check) withArgs(kwargs []starlark.Tuple) (starlark.Value, error) {
+ // Make a copy to modify.
+ res := *c
+
+ validParams := make([]string, 0, c.impl.NumParams()-1)
+ for i := 1; i < c.impl.NumParams(); i++ {
+ name, _ := c.impl.Param(i)
+ validParams = append(validParams, name)
+ }
+
+ newKwargs := kwargsMap(res.kwargs)
+ for k, v := range kwargsMap(kwargs) {
+ if k == "ctx" {
+ return nil, errors.New("\"ctx\" argument cannot be overridden")
+ }
+ if !slices.Contains(validParams, k) {
+ return nil, fmt.Errorf("invalid argument %q, must be one of: (%s)", k, strings.Join(validParams, ", "))
+ }
+ newKwargs[k] = v
+ }
+
+ res.kwargs = make([]starlark.Tuple, 0, len(newKwargs))
+ for k, v := range newKwargs {
+ res.kwargs = append(res.kwargs, starlark.Tuple{starlark.String(k), v})
+ }
+ return &res, nil
+}
+
+// checkWithArgsBuiltin implements the native function shac.check().with_args().
+//
+// Make sure to update //doc/stdlib.star whenever this function is modified.
+var checkWithArgsBuiltin = newBoundBuiltin("with_args", func(ctx context.Context, s *shacState, name string, self starlark.Value, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
+ if len(args) != 0 {
+ return nil, fmt.Errorf("only keyword arguments are allowed")
+ }
+ return self.(*check).withArgs(kwargs)
+})
+
+// checkWithNameBuiltin implements the native function shac.check().with_name().
+//
+// Make sure to update //doc/stdlib.star whenever this function is modified.
+var checkWithNameBuiltin = newBoundBuiltin("with_name", func(ctx context.Context, s *shacState, name string, self starlark.Value, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
+ var argname starlark.String
+ if err := starlark.UnpackArgs(name, args, kwargs,
+ "name?", &argname); err != nil {
+ return nil, err
+ }
+ return self.(*check).withName(string(argname))
+})
+
+func kwargsMap(kwargs []starlark.Tuple) map[string]starlark.Value {
+ res := make(map[string]starlark.Value, len(kwargs))
+ for _, item := range kwargs {
+ if len(item) != 2 {
+ log.Panicf("kwargs item does not have length 2: %+v", kwargs)
+ }
+ s, ok := item[0].(starlark.String)
+ if !ok {
+ log.Panicf("kwargs item does not have a string key: %+v", kwargs)
+ }
+ res[string(s)] = item[1]
+ }
+ return res
+}
diff --git a/internal/engine/testdata/fail_or_throw/shac-check-with_args-ctx.star b/internal/engine/testdata/fail_or_throw/shac-check-with_args-ctx.star
new file mode 100644
index 0000000..2ed9f44
--- /dev/null
+++ b/internal/engine/testdata/fail_or_throw/shac-check-with_args-ctx.star
@@ -0,0 +1,18 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def cb(ctx):
+ pass
+
+shac.register_check(shac.check(cb).with_args(ctx = "blah"))
diff --git a/internal/engine/testdata/fail_or_throw/shac-check-with_args-nonexistent_kwarg.star b/internal/engine/testdata/fail_or_throw/shac-check-with_args-nonexistent_kwarg.star
new file mode 100644
index 0000000..a09ab5e
--- /dev/null
+++ b/internal/engine/testdata/fail_or_throw/shac-check-with_args-nonexistent_kwarg.star
@@ -0,0 +1,18 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def cb(ctx, foo = "bar"):
+ pass
+
+shac.register_check(shac.check(cb).with_args(nonexistent = "blah"))
diff --git a/internal/engine/testdata/fail_or_throw/shac-check-with_args-positional_args.star b/internal/engine/testdata/fail_or_throw/shac-check-with_args-positional_args.star
new file mode 100644
index 0000000..fb6374d
--- /dev/null
+++ b/internal/engine/testdata/fail_or_throw/shac-check-with_args-positional_args.star
@@ -0,0 +1,18 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def cb(ctx, an_arg = "bar"):
+ pass
+
+shac.register_check(shac.check(cb).with_args("foo"))
diff --git a/internal/engine/testdata/print/shac-check-with_args.star b/internal/engine/testdata/print/shac-check-with_args.star
new file mode 100644
index 0000000..56ea87d
--- /dev/null
+++ b/internal/engine/testdata/print/shac-check-with_args.star
@@ -0,0 +1,39 @@
+# Copyright 2023 The Shac Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def _print_something(ctx, to_print = "something"):
+ print(to_print)
+
+print_hello_check = (
+ shac.check(_print_something, name = "print_hello")
+ .with_args(to_print = "hello")
+)
+print_goodbye_check = (
+ print_hello_check
+ .with_args(to_print = "goodbye")
+ .with_name("print_goodbye")
+)
+print_hello_again_check = (
+ print_goodbye_check
+ .with_args(to_print = "hello again")
+ .with_name("print_hello_again")
+)
+
+print("print_hello_check:", print_hello_check)
+print("print_goodbye_check:", print_goodbye_check)
+print("print_hello_again_check:", print_hello_again_check)
+
+shac.register_check(print_hello_check)
+shac.register_check(print_goodbye_check)
+shac.register_check(print_hello_again_check)
diff --git a/internal/engine/version.go b/internal/engine/version.go
index b4779ed..24ed15a 100644
--- a/internal/engine/version.go
+++ b/internal/engine/version.go
@@ -18,5 +18,5 @@
// Version is the current tool version.
//
// TODO(maruel): Add proper version, preferably from git tag.
- Version = [...]int{0, 1, 5}
+ Version = [...]int{0, 1, 6}
)
diff --git a/shac.textproto b/shac.textproto
index 8d73af9..bc4b74c 100644
--- a/shac.textproto
+++ b/shac.textproto
@@ -15,7 +15,7 @@
# TODO(olivernewman): Build github.com/protocolbuffers/txtpbfmt into shac and
# enforce formatting of shac.textproto files in all repos that use shac.
-min_shac_version: "0.1.5"
+min_shac_version: "0.1.6"
allow_network: False
ignore: "/vendor/"
# Vendored code for test data only.