From 9cb72964693ed0b63ded52172c752650375f8405 Mon Sep 17 00:00:00 2001 From: Alex Davis Date: Thu, 5 Feb 2026 10:10:04 -0800 Subject: [PATCH] Adding type testing for Targets, Labels, and Files While these types cannot be created in the loading phase (i.e. `value = type(File())`) for internally consistent type checking, these type rituals allow for a central definition of how/where this type checking can be governed, allowing for future changes, etc to all become generationall compatible as appropriate. --- lib/types.bzl | 44 +++++++++++++++++++++ tests/types_tests.bzl | 91 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/lib/types.bzl b/lib/types.bzl index db1ca185..2356688a 100644 --- a/lib/types.bzl +++ b/lib/types.bzl @@ -23,6 +23,14 @@ _an_int_type = type(1) _a_depset_type = type(depset()) _a_struct_type = type(struct()) +# Unique types that do not have a load-phase type but can still be evaluated +# within the analysis phase. While not perfect, these do centralize the ability +# to express a type and provide a stable test against those types since these +# type strings must be defined at load-time. +_a_file_type = "File" +_a_target_type = "Target" +_a_label_type = "Label" + def _a_function(): pass @@ -138,6 +146,39 @@ def _is_set(v): """ return type(v) == _a_struct_type and hasattr(v, "_values") and _is_dict(v._values) +def _is_file(v): + """Returns True if v is a "File" type. + + Args: + v: The value whose type should be checked. + + Returns: + True if v is a File. + """ + return type(v) == _a_file_type + +def _is_target(v): + """Returns True if v is a "Target" type. + + Args: + v: The value whose type should be checked. + + Returns: + True if v is a Target. + """ + return type(v) == _a_target_type + +def _is_label(v): + """Returns True if v is a "Label" type. + + Args: + v: The value whose type should be checked. + + Returns: + True if v is a Label. + """ + return type(v) == _a_label_type + types = struct( is_list = _is_list, is_string = _is_string, @@ -149,4 +190,7 @@ types = struct( is_function = _is_function, is_depset = _is_depset, is_set = _is_set, + is_file = _is_file, + is_target = _is_target, + is_label = _is_label, ) diff --git a/tests/types_tests.bzl b/tests/types_tests.bzl index a3c654bf..14d3d6c8 100644 --- a/tests/types_tests.bzl +++ b/tests/types_tests.bzl @@ -17,6 +17,9 @@ load("//lib:new_sets.bzl", "sets") load("//lib:types.bzl", "types") load("//lib:unittest.bzl", "asserts", "unittest") +# A placeholder target for unit tests that need a target. +_target_test_hollow_target = "types_test_hollow_target" + def _a_function(): """A dummy function for testing.""" pass @@ -231,7 +234,92 @@ def _is_set_test(ctx): is_set_test = unittest.make(_is_set_test) +def _make_test_file(ctx): + """Makes a file object in the analysis phase. + + Declaring a file for the sake of analysis but we never actually need to + provide the file through an output group or DefaultInfo. + """ + test_file = ctx.actions.declare_file(ctx.label.name + "_test_file") + ctx.actions.write( + output = test_file, + content = "test_file", + ) + + return test_file + +def _is_file_test(ctx): + """Unit test for types.is_file.""" + env = unittest.begin(ctx) + + # Ensuring a file does indeed match. + asserts.true(env, types.is_file(_make_test_file(ctx))) + + asserts.false(env, types.is_file(99)) + asserts.false(env, types.is_file("")) + asserts.false(env, types.is_file(set())) + asserts.false(env, types.is_file(list())) + asserts.false(env, types.is_file(struct())) + + return unittest.end(env) + +is_file_test = unittest.make(_is_file_test) + +def _is_target_test(ctx): + """Unit test for types.is_target.""" + env = unittest.begin(ctx) + + # Ensuring a target does indeed match. + asserts.true(env, types.is_target(ctx.attr._test_target)) + + asserts.false(env, types.is_target(_make_test_file(ctx))) + asserts.false(env, types.is_target(99)) + asserts.false(env, types.is_target("")) + asserts.false(env, types.is_target(set())) + asserts.false(env, types.is_target(list())) + asserts.false(env, types.is_target(struct())) + + return unittest.end(env) + +is_target_test = unittest.make( + _is_target_test, + attrs = { + "_test_target": attr.label( + default = _target_test_hollow_target, + ), + }, +) + +def _is_label_test(ctx): + """Unit test for types.is_label.""" + env = unittest.begin(ctx) + + asserts.true(env, types.is_label(ctx.label)) + + # Ensuring a target does not match. + asserts.false(env, types.is_label(ctx.attr._test_target)) + asserts.false(env, types.is_target(_make_test_file(ctx))) + asserts.false(env, types.is_target(99)) + asserts.false(env, types.is_target("")) + asserts.false(env, types.is_target(set())) + asserts.false(env, types.is_target(list())) + asserts.false(env, types.is_target(struct())) + + return unittest.end(env) + +is_label_test = unittest.make( + _is_label_test, + attrs = { + "_test_target": attr.label( + default = _target_test_hollow_target, + ), + }, +) + def types_test_suite(): + # Used for target type-testing. + native.filegroup(name = _target_test_hollow_target) + """Creates the test targets and test suite for types.bzl tests.""" unittest.suite( "types_tests", @@ -245,4 +333,7 @@ def types_test_suite(): is_function_test, is_depset_test, is_set_test, + is_file_test, + is_target_test, + is_label_test, )