• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 25443604553

06 May 2026 03:05PM UTC coverage: 92.879% (-0.04%) from 92.915%
25443604553

push

github

web-flow
[pants_ng] Scaffolding for a pants_ng mode. (#23319)

In this mode the command line is parsed as an
NG invocation, and dispatched appropriately.

Of course at the moment there are no
implementations to dispatch to. That will follow.

This does expose a new option, `pants_ng` to users. 
There is a big warning not to set it, but we're not trying
to hide that we're working on a new thing, so I am
comfortable with this.

25 of 76 new or added lines in 9 files covered. (32.89%)

1294 existing lines in 76 files now uncovered.

92234 of 99306 relevant lines covered (92.88%)

4.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.53
/src/python/pants/engine/internals/build_files.py
1
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
12✔
5

6
import ast
12✔
7
import builtins
12✔
8
import itertools
12✔
9
import logging
12✔
10
import os.path
12✔
11
import sys
12✔
12
import typing
12✔
13
from collections import defaultdict
12✔
14
from collections.abc import Coroutine, Sequence
12✔
15
from dataclasses import dataclass
12✔
16
from pathlib import PurePath
12✔
17
from typing import Any, cast
12✔
18

19
import typing_extensions
12✔
20

21
from pants.build_graph.address import (
12✔
22
    Address,
23
    AddressInput,
24
    BuildFileAddress,
25
    BuildFileAddressRequest,
26
    MaybeAddress,
27
    ResolveError,
28
)
29
from pants.core.util_rules.env_vars import environment_vars_subset
12✔
30
from pants.engine.engine_aware import EngineAwareParameter
12✔
31
from pants.engine.env_vars import CompleteEnvironmentVars, EnvironmentVars, EnvironmentVarsRequest
12✔
32
from pants.engine.fs import FileContent, GlobMatchErrorBehavior, PathGlobs
12✔
33
from pants.engine.internals.defaults import BuildFileDefaults, BuildFileDefaultsParserState
12✔
34
from pants.engine.internals.dep_rules import (
12✔
35
    BuildFileDependencyRules,
36
    DependencyRuleApplication,
37
    MaybeBuildFileDependencyRulesImplementation,
38
)
39
from pants.engine.internals.mapper import (
12✔
40
    DELETED_SPEC_PATH,
41
    AddressFamily,
42
    AddressMap,
43
    DuplicateNameError,
44
)
45
from pants.engine.internals.parser import (
12✔
46
    BuildFilePreludeSymbols,
47
    BuildFileSymbolsInfo,
48
    Parser,
49
    error_on_imports,
50
)
51
from pants.engine.internals.selectors import concurrently
12✔
52
from pants.engine.internals.session import SessionValues
12✔
53
from pants.engine.internals.synthetic_targets import (
12✔
54
    SyntheticAddressMapsRequest,
55
    get_synthetic_address_maps,
56
)
57
from pants.engine.internals.target_adaptor import TargetAdaptor, TargetAdaptorRequest
12✔
58
from pants.engine.intrinsics import get_digest_contents, path_globs_to_paths
12✔
59
from pants.engine.rules import QueryRule, collect_rules, implicitly, rule
12✔
60
from pants.engine.target import (
12✔
61
    DependenciesRuleApplication,
62
    DependenciesRuleApplicationRequest,
63
    InvalidTargetException,
64
    RegisteredTargetTypes,
65
)
66
from pants.engine.unions import UnionMembership
12✔
67
from pants.init.bootstrap_scheduler import BootstrapStatus
12✔
68
from pants.option.global_options import GlobalOptions
12✔
69
from pants.util.frozendict import FrozenDict
12✔
70
from pants.util.strutil import softwrap
12✔
71

72
logger = logging.getLogger(__name__)
12✔
73

74

75
# Singleton for use as a reference to the DeletedTarget pseudo-target.
76
# A backend can inject a dependency on DELETED_ADDRESS if it is capable of detecting
77
# (via naming conventions) that a file depends on some other, deleted file.
78
# The generic change detection mechanism will use that dependency to include the
79
# files in the transitive closure of dependents of changed files.
80
# This is somewhat limited - the backend doesn't get the content of the deleted file,
81
# and so cannot use it in the regular inference pipeline.
82
#
83
# This is not the most elegant solution, but the legacy design of dep inference and of
84
# change detection does not admit a simple one.
85
#
86
# A more global solution, that doesn't rely on backend specifics, would be to capture the
87
# state of the repo before and after the changes and apply dep inference to both.
88
# However that is a very substantial change from the status quo, so for now this
89
# incremental improvement will suffice.
90
DELETED_ADDRESS = Address(DELETED_SPEC_PATH)
12✔
91

92

93
class BuildFileSyntaxError(SyntaxError):
12✔
94
    """An error parsing a BUILD file."""
95

96
    def from_syntax_error(error: SyntaxError) -> BuildFileSyntaxError:
12✔
UNCOV
97
        return BuildFileSyntaxError(
1✔
98
            error.msg,
99
            (
100
                error.filename,
101
                error.lineno,
102
                error.offset,
103
                error.text,
104
            ),
105
        )
106

107
    def __str__(self) -> str:
12✔
UNCOV
108
        first_line = f"Error parsing BUILD file {self.filename}:{self.lineno}: {self.msg}"
1✔
109
        # These two fields are optional per the spec, so we can't rely on them being set.
UNCOV
110
        if self.text is not None and self.offset is not None:
1✔
UNCOV
111
            second_line = f"  {self.text.rstrip()}"
1✔
UNCOV
112
            third_line = f"  {' ' * (self.offset - 1)}^"
1✔
UNCOV
113
            return f"{first_line}\n{second_line}\n{third_line}"
1✔
114

115
        return first_line
×
116

117

118
@dataclass(frozen=True)
12✔
119
class BuildFileOptions:
12✔
120
    patterns: tuple[str, ...]
12✔
121
    ignores: tuple[str, ...] = ()
12✔
122
    prelude_globs: tuple[str, ...] = ()
12✔
123

124

125
@rule
12✔
126
async def extract_build_file_options(
12✔
127
    global_options: GlobalOptions,
128
    bootstrap_status: BootstrapStatus,
129
) -> BuildFileOptions:
130
    return BuildFileOptions(
12✔
131
        patterns=global_options.build_patterns,
132
        ignores=global_options.build_ignore,
133
        prelude_globs=(
134
            () if bootstrap_status.in_progress else global_options.build_file_prelude_globs
135
        ),
136
    )
137

138

139
@rule(desc="Expand macros")
12✔
140
async def evaluate_preludes(
12✔
141
    build_file_options: BuildFileOptions,
142
    parser: Parser,
143
) -> BuildFilePreludeSymbols:
144
    prelude_digest_contents = await get_digest_contents(
12✔
145
        **implicitly(
146
            PathGlobs(
147
                build_file_options.prelude_globs,
148
                glob_match_error_behavior=GlobMatchErrorBehavior.ignore,
149
            )
150
        )
151
    )
152
    globals: dict[str, Any] = {
12✔
153
        # Later entries have precendence replacing conflicting keys from previous entries, so we
154
        # start with typing_extensions as the lowest prio source for global values.
155
        **{name: getattr(typing_extensions, name) for name in typing_extensions.__all__},
156
        **{name: getattr(typing, name) for name in typing.__all__},
157
        **{name: getattr(builtins, name) for name in dir(builtins) if name.endswith("Error")},
158
        # Ensure the globals for each prelude includes the builtin symbols (E.g. `python_sources`)
159
        # and any build file aliases (e.g. from plugins)
160
        **parser.symbols,
161
    }
162
    locals: dict[str, Any] = {}
12✔
163
    env_vars: set[str] = set()
12✔
164
    for file_content in prelude_digest_contents:
12✔
UNCOV
165
        try:
1✔
UNCOV
166
            file_content_str = file_content.content.decode()
1✔
UNCOV
167
            content = compile(file_content_str, file_content.path, "exec", dont_inherit=True)
1✔
UNCOV
168
            exec(content, globals, locals)
1✔
UNCOV
169
        except Exception as e:
1✔
UNCOV
170
            raise Exception(f"Error parsing prelude file {file_content.path}: {e}")
1✔
UNCOV
171
        error_on_imports(file_content_str, file_content.path)
1✔
UNCOV
172
        env_vars.update(BUILDFileEnvVarExtractor.get_env_vars(file_content))
1✔
173
    # __builtins__ is a dict, so isn't hashable, and can't be put in a FrozenDict.
174
    # Fortunately, we don't care about it - preludes should not be able to override builtins, so we just pop it out.
175
    # TODO: Give a nice error message if a prelude tries to set and expose a non-hashable value.
176
    locals.pop("__builtins__", None)
12✔
177
    # Ensure preludes can reference each other by populating the shared globals object with references
178
    # to the other symbols
179
    globals.update(locals)
12✔
180
    return BuildFilePreludeSymbols.create(locals, env_vars)
12✔
181

182

183
@rule
12✔
184
async def get_all_build_file_symbols_info(
12✔
185
    parser: Parser, prelude_symbols: BuildFilePreludeSymbols
186
) -> BuildFileSymbolsInfo:
187
    return BuildFileSymbolsInfo.from_info(
×
188
        parser.symbols_info.info.values(), prelude_symbols.info.values()
189
    )
190

191

192
@rule
12✔
193
async def maybe_resolve_address(address_input: AddressInput) -> MaybeAddress:
12✔
194
    if address_input.path_component == DELETED_ADDRESS.spec_path:
12✔
195
        return MaybeAddress(DELETED_ADDRESS)
×
196
    # Determine the type of the path_component of the input.
197
    if address_input.path_component:
12✔
198
        paths = await path_globs_to_paths(PathGlobs(globs=(address_input.path_component,)))
12✔
199
        is_file, is_dir = bool(paths.files), bool(paths.dirs)
12✔
200
    else:
201
        # It is an address in the root directory.
202
        is_file, is_dir = False, True
12✔
203

204
    if is_file:
12✔
205
        return MaybeAddress(address_input.file_to_address())
12✔
206
    if is_dir:
12✔
207
        return MaybeAddress(address_input.dir_to_address())
12✔
208
    spec = address_input.path_component
7✔
209
    if address_input.target_component:
7✔
210
        spec += f":{address_input.target_component}"
6✔
211
    return MaybeAddress(
7✔
212
        ResolveError(
213
            softwrap(
214
                f"""
215
                The file or directory '{address_input.path_component}' does not exist on disk in
216
                the workspace, so the address '{spec}' from {address_input.description_of_origin}
217
                cannot be resolved.
218
                """
219
            )
220
        )
221
    )
222

223

224
@rule
12✔
225
async def resolve_address(maybe_address: MaybeAddress) -> Address:
12✔
226
    if isinstance(maybe_address.val, ResolveError):
12✔
227
        raise maybe_address.val
2✔
228
    return maybe_address.val
12✔
229

230

231
@dataclass(frozen=True)
12✔
232
class AddressFamilyDir(EngineAwareParameter):
12✔
233
    """The directory to find addresses for.
234

235
    This does _not_ recurse into subdirectories.
236
    """
237

238
    path: str
12✔
239

240
    def debug_hint(self) -> str:
12✔
241
        return self.path
12✔
242

243

244
@dataclass(frozen=True)
12✔
245
class OptionalAddressFamily:
12✔
246
    path: str
12✔
247
    address_family: AddressFamily | None = None
12✔
248

249
    def ensure(self) -> AddressFamily:
12✔
250
        if self.address_family is not None:
12✔
251
            return self.address_family
12✔
UNCOV
252
        raise ResolveError(f"Directory '{self.path}' does not contain any BUILD files.")
1✔
253

254

255
_DELETED_OPTIONAL_ADDRESS_FAMILY = OptionalAddressFamily(
12✔
256
    DELETED_ADDRESS.spec_path, AddressFamily.create(DELETED_ADDRESS.spec_path, [])
257
)
258

259

260
@rule
12✔
261
async def ensure_address_family(request: OptionalAddressFamily) -> AddressFamily:
12✔
262
    return request.ensure()
12✔
263

264

265
class BUILDFileEnvVarExtractor(ast.NodeVisitor):
12✔
266
    def __init__(self, filename: str):
12✔
267
        super().__init__()
12✔
268
        self.env_vars: set[str] = set()
12✔
269
        self.filename = filename
12✔
270

271
    @classmethod
12✔
272
    def get_env_vars(cls, file_content: FileContent) -> Sequence[str]:
12✔
273
        obj = cls(file_content.path)
12✔
274
        try:
12✔
275
            obj.visit(ast.parse(file_content.content, file_content.path))
12✔
UNCOV
276
        except SyntaxError as e:
1✔
UNCOV
277
            raise BuildFileSyntaxError.from_syntax_error(e).with_traceback(e.__traceback__)
1✔
278

279
        return tuple(obj.env_vars)
12✔
280

281
    def visit_Call(self, node: ast.Call):
12✔
282
        is_env = isinstance(node.func, ast.Name) and node.func.id == "env"
12✔
283
        for arg in node.args:
12✔
284
            if not is_env:
9✔
285
                self.visit(arg)
9✔
286
                continue
9✔
287

288
            # Only first arg may be checked as env name
289
            is_env = False
2✔
290

291
            if sys.version_info[0:2] < (3, 8):
2✔
292
                value = arg.s if isinstance(arg, ast.Str) else None
×
293
            else:
294
                value = arg.value if isinstance(arg, ast.Constant) else None
2✔
295
            if value:
2✔
296
                # Found env name in this call, we're done here.
297
                self.env_vars.add(value)  # type: ignore[arg-type]
2✔
298
                return
2✔
299
            else:
UNCOV
300
                logger.warning(
1✔
301
                    f"{self.filename}:{arg.lineno}: Only constant string values as variable name to "
302
                    f"`env()` is currently supported. This `env()` call will always result in "
303
                    "the default value only."
304
                )
305

306
        for kwarg in node.keywords:
12✔
307
            self.visit(kwarg)
12✔
308

309

310
@rule(desc="Search for addresses in BUILD files")
12✔
311
async def parse_address_family(
12✔
312
    directory: AddressFamilyDir,
313
    parser: Parser,
314
    bootstrap_status: BootstrapStatus,
315
    build_file_options: BuildFileOptions,
316
    prelude_symbols: BuildFilePreludeSymbols,
317
    registered_target_types: RegisteredTargetTypes,
318
    union_membership: UnionMembership,
319
    maybe_build_file_dependency_rules_implementation: MaybeBuildFileDependencyRulesImplementation,
320
    session_values: SessionValues,
321
) -> OptionalAddressFamily:
322
    """Given an AddressMapper and a directory, return an AddressFamily.
323

324
    The AddressFamily may be empty, but it will not be None.
325
    """
326
    if directory.path == _DELETED_OPTIONAL_ADDRESS_FAMILY.path:
12✔
327
        return _DELETED_OPTIONAL_ADDRESS_FAMILY
×
328

329
    digest_contents, all_synthetic_address_maps = await concurrently(
12✔
330
        get_digest_contents(
331
            **implicitly(
332
                PathGlobs(
333
                    globs=(
334
                        *(os.path.join(directory.path, p) for p in build_file_options.patterns),
335
                        *(f"!{p}" for p in build_file_options.ignores),
336
                    )
337
                )
338
            ),
339
        ),
340
        get_synthetic_address_maps(SyntheticAddressMapsRequest(directory.path), **implicitly()),
341
    )
342
    synthetic_address_maps = tuple(itertools.chain(all_synthetic_address_maps))
12✔
343
    if not digest_contents and not synthetic_address_maps:
12✔
344
        return OptionalAddressFamily(directory.path)
12✔
345

346
    defaults = BuildFileDefaults({})
12✔
347
    dependents_rules: BuildFileDependencyRules | None = None
12✔
348
    dependencies_rules: BuildFileDependencyRules | None = None
12✔
349
    parent_dirs = tuple(PurePath(directory.path).parents)
12✔
350
    if parent_dirs:
12✔
351
        maybe_parents = await concurrently(
12✔
352
            parse_address_family(AddressFamilyDir(str(parent_dir)), **implicitly())
353
            for parent_dir in parent_dirs
354
        )
355
        for maybe_parent in maybe_parents:
12✔
356
            if maybe_parent.address_family is not None:
12✔
357
                family = maybe_parent.address_family
12✔
358
                defaults = family.defaults
12✔
359
                dependents_rules = family.dependents_rules
12✔
360
                dependencies_rules = family.dependencies_rules
12✔
361
                break
12✔
362

363
    defaults_parser_state = BuildFileDefaultsParserState.create(
12✔
364
        directory.path, defaults, registered_target_types, union_membership
365
    )
366
    build_file_dependency_rules_class = (
12✔
367
        maybe_build_file_dependency_rules_implementation.build_file_dependency_rules_class
368
    )
369
    if build_file_dependency_rules_class is not None:
12✔
370
        dependents_rules_parser_state = build_file_dependency_rules_class.create_parser_state(
2✔
371
            directory.path,
372
            dependents_rules,
373
        )
374
        dependencies_rules_parser_state = build_file_dependency_rules_class.create_parser_state(
2✔
375
            directory.path,
376
            dependencies_rules,
377
        )
378
    else:
379
        dependents_rules_parser_state = None
12✔
380
        dependencies_rules_parser_state = None
12✔
381

382
    def _extract_env_vars(
12✔
383
        file_content: FileContent, extra_env: Sequence[str], env: CompleteEnvironmentVars
384
    ) -> Coroutine[Any, Any, EnvironmentVars]:
385
        """For BUILD file env vars, we only ever consult the local systems env."""
386
        env_vars = (*BUILDFileEnvVarExtractor.get_env_vars(file_content), *extra_env)
12✔
387
        return environment_vars_subset(EnvironmentVarsRequest(env_vars), env)
12✔
388

389
    all_env_vars = await concurrently(
12✔
390
        _extract_env_vars(
391
            fc, prelude_symbols.referenced_env_vars, session_values[CompleteEnvironmentVars]
392
        )
393
        for fc in digest_contents
394
    )
395

396
    declared_address_maps = [
12✔
397
        AddressMap.parse(
398
            fc.path,
399
            fc.content.decode(),
400
            parser,
401
            prelude_symbols,
402
            env_vars,
403
            bootstrap_status.in_progress,
404
            defaults_parser_state,
405
            dependents_rules_parser_state,
406
            dependencies_rules_parser_state,
407
        )
408
        for fc, env_vars in zip(digest_contents, all_env_vars)
409
    ]
410
    declared_address_maps.sort(key=lambda x: x.path)
12✔
411

412
    # Freeze defaults and dependency rules
413
    frozen_defaults = defaults_parser_state.get_frozen_defaults()
12✔
414
    frozen_dependents_rules = cast(
12✔
415
        "BuildFileDependencyRules | None",
416
        dependents_rules_parser_state
417
        and dependents_rules_parser_state.get_frozen_dependency_rules(),
418
    )
419
    frozen_dependencies_rules = cast(
12✔
420
        "BuildFileDependencyRules | None",
421
        dependencies_rules_parser_state
422
        and dependencies_rules_parser_state.get_frozen_dependency_rules(),
423
    )
424

425
    # Process synthetic targets.
426

427
    def apply_defaults(tgt: TargetAdaptor) -> TargetAdaptor:
12✔
428
        default_values = frozen_defaults.get(tgt.type_alias)
5✔
429
        if default_values is None:
5✔
430
            return tgt
5✔
UNCOV
431
        return tgt.with_new_kwargs(**{**default_values, **tgt.kwargs})
1✔
432

433
    name_to_path_and_synthetic_target: dict[str, tuple[str, TargetAdaptor]] = {}
12✔
434
    for synthetic_address_map in synthetic_address_maps:
12✔
435
        for name, target in synthetic_address_map.name_to_target_adaptor.items():
5✔
436
            name_to_path_and_synthetic_target[name] = (
5✔
437
                synthetic_address_map.path,
438
                apply_defaults(target),
439
            )
440

441
    name_to_path_and_declared_target: dict[str, tuple[str, TargetAdaptor]] = {}
12✔
442
    for declared_address_map in declared_address_maps:
12✔
443
        for name, target in declared_address_map.name_to_target_adaptor.items():
12✔
444
            if name in name_to_path_and_declared_target:
12✔
445
                # This is a duplicate declared name, raise an exception.
UNCOV
446
                duplicate_path = name_to_path_and_declared_target[name][0]
1✔
UNCOV
447
                raise DuplicateNameError(
1✔
448
                    f"A target already exists at `{duplicate_path}` with name `{name}` and target type "
449
                    f"`{target.type_alias}`. The `{name}` target in `{declared_address_map.path}` "
450
                    "cannot use the same name."
451
                )
452

453
            name_to_path_and_declared_target[name] = (declared_address_map.path, target)
12✔
454

455
    # We copy the dict so we can modify the original in the loop.
456
    for name, (
12✔
457
        declared_target_path,
458
        declared_target,
459
    ) in name_to_path_and_declared_target.copy().items():
460
        # Pop the synthetic target to let the declared target take precedence.
461
        synthetic_target_path, synthetic_target = name_to_path_and_synthetic_target.pop(
12✔
462
            name, (None, None)
463
        )
464
        if "_extend_synthetic" not in declared_target.kwargs:
12✔
465
            # The explicitly declared target should replace the synthetic one.
466
            continue
12✔
467

468
        # The _extend_synthetic kwarg was explicitly provided, so we must strip it.
UNCOV
469
        declared_target_kwargs = dict(declared_target.kwargs)
1✔
UNCOV
470
        extend_synthetic = declared_target_kwargs.pop("_extend_synthetic")
1✔
UNCOV
471
        if extend_synthetic:
1✔
UNCOV
472
            if synthetic_target is None:
1✔
UNCOV
473
                raise InvalidTargetException(
1✔
474
                    softwrap(
475
                        f"""
476
                            The `{declared_target.type_alias}` target {name!r} in {declared_target_path} has
477
                            `_extend_synthetic=True` but there is no synthetic target to extend.
478
                            """
479
                    )
480
                )
481

UNCOV
482
            if synthetic_target.type_alias != declared_target.type_alias:
1✔
UNCOV
483
                raise InvalidTargetException(
1✔
484
                    softwrap(
485
                        f"""
486
                        The `{declared_target.type_alias}` target {name!r} in {declared_target_path} is
487
                        of a different type than the synthetic target
488
                        `{synthetic_target.type_alias}` from {synthetic_target_path}.
489

490
                        When `_extend_synthetic` is true the target types must match, set this to
491
                        false if you want to replace the synthetic target with the target from your
492
                        BUILD file.
493
                        """
494
                    )
495
                )
496

497
            # Preserve synthetic field values not overriden by the declared target from the BUILD.
UNCOV
498
            kwargs = {**synthetic_target.kwargs, **declared_target_kwargs}
1✔
499
        else:
500
            kwargs = declared_target_kwargs
×
UNCOV
501
        name_to_path_and_declared_target[name] = (
1✔
502
            declared_target_path,
503
            declared_target.with_new_kwargs(**kwargs),
504
        )
505

506
    # Now reconstitute into AddressMaps, to pass into AddressFamily.create().
507
    # We no longer need to distinguish between synthetic and declared AddressMaps.
508
    # TODO: We might want to move the validation done by AddressFamily.create() to here, since
509
    #  we're already iterating over the AddressMap data, and simplify AddressFamily.
510
    path_to_targets = defaultdict(list)
12✔
511
    for name_to_path_and_target in [
12✔
512
        name_to_path_and_declared_target,
513
        name_to_path_and_synthetic_target,
514
    ]:
515
        for path_and_target in name_to_path_and_target.values():
12✔
516
            path_to_targets[path_and_target[0]].append(path_and_target[1])
12✔
517
    address_maps = [AddressMap.create(path, targets) for path, targets in path_to_targets.items()]
12✔
518

519
    return OptionalAddressFamily(
12✔
520
        directory.path,
521
        AddressFamily.create(
522
            spec_path=directory.path,
523
            address_maps=address_maps,
524
            defaults=frozen_defaults,
525
            dependents_rules=frozen_dependents_rules,
526
            dependencies_rules=frozen_dependencies_rules,
527
        ),
528
    )
529

530

531
@rule
12✔
532
async def find_build_file(request: BuildFileAddressRequest) -> BuildFileAddress:
12✔
533
    address = request.address
11✔
534
    address_family = await ensure_address_family(**implicitly(AddressFamilyDir(address.spec_path)))
11✔
535
    owning_address = address.maybe_convert_to_target_generator()
11✔
536
    if address_family.get_target_adaptor(owning_address) is None:
11✔
537
        raise ResolveError.did_you_mean(
×
538
            owning_address,
539
            description_of_origin=request.description_of_origin,
540
            known_names=address_family.target_names,
541
            namespace=address_family.namespace,
542
        )
543
    bfa = next(
11✔
544
        build_file_address
545
        for build_file_address in address_family.build_file_addresses
546
        if build_file_address.address == owning_address
547
    )
548
    return BuildFileAddress(address, bfa.rel_path) if address.is_generated_target else bfa
11✔
549

550

551
def _get_target_adaptor(
12✔
552
    address: Address, address_family: AddressFamily, description_of_origin: str
553
) -> TargetAdaptor:
554
    target_adaptor = address_family.get_target_adaptor(address)
12✔
555
    if target_adaptor is None:
12✔
556
        raise ResolveError.did_you_mean(
5✔
557
            address,
558
            description_of_origin=description_of_origin,
559
            known_names=address_family.target_names,
560
            namespace=address_family.namespace,
561
        )
562
    return target_adaptor
12✔
563

564

565
@rule
12✔
566
async def find_target_adaptor(request: TargetAdaptorRequest) -> TargetAdaptor:
12✔
567
    """Hydrate a TargetAdaptor so that it may be converted into the Target API."""
568
    address = request.address
12✔
569
    if address.is_generated_target:
12✔
570
        raise AssertionError(
×
571
            "Generated targets are not defined in BUILD files, and so do not have "
572
            f"TargetAdaptors: {request}"
573
        )
574
    address_family = await ensure_address_family(**implicitly(AddressFamilyDir(address.spec_path)))
12✔
575
    target_adaptor = _get_target_adaptor(address, address_family, request.description_of_origin)
12✔
576
    return target_adaptor
12✔
577

578

579
def _rules_path(address: Address) -> str:
12✔
580
    if address.is_file_target and os.path.sep in address.relative_file_path:  # type: ignore[operator]
2✔
581
        # The file is in a subdirectory of spec_path
582
        return os.path.dirname(address.filename)
1✔
583
    else:
584
        return address.spec_path
2✔
585

586

587
async def _get_target_family_and_adaptor_for_dep_rules(
12✔
588
    *addresses: Address, description_of_origin: str
589
) -> tuple[tuple[AddressFamily, TargetAdaptor], ...]:
590
    # Fetch up to 2 sets of address families per address, as we want the rules from the directory
591
    # the file is in rather than the directory where the target generator was declared, if not the
592
    # same.
593
    rules_paths = set(
2✔
594
        itertools.chain.from_iterable(
595
            {address.spec_path, _rules_path(address)} for address in addresses
596
        )
597
    )
598
    maybe_address_families = await concurrently(
2✔
599
        parse_address_family(AddressFamilyDir(rules_path), **implicitly())
600
        for rules_path in rules_paths
601
    )
602
    maybe_families = {maybe.path: maybe for maybe in maybe_address_families}
2✔
603

604
    return tuple(
2✔
605
        (
606
            (
607
                maybe_families[_rules_path(address)].address_family
608
                or maybe_families[address.spec_path].ensure()
609
            ),
610
            _get_target_adaptor(
611
                address,
612
                maybe_families[address.spec_path].ensure(),
613
                description_of_origin,
614
            ),
615
        )
616
        for address in addresses
617
    )
618

619

620
@rule
12✔
621
async def get_dependencies_rule_application(
12✔
622
    request: DependenciesRuleApplicationRequest,
623
    maybe_build_file_rules_implementation: MaybeBuildFileDependencyRulesImplementation,
624
) -> DependenciesRuleApplication:
625
    build_file_dependency_rules_class = (
2✔
626
        maybe_build_file_rules_implementation.build_file_dependency_rules_class
627
    )
628
    if build_file_dependency_rules_class is None:
2✔
629
        return DependenciesRuleApplication.allow_all()
×
630

631
    (
2✔
632
        (
633
            origin_rules_family,
634
            origin_target,
635
        ),
636
        *dependencies_family_adaptor,
637
    ) = await _get_target_family_and_adaptor_for_dep_rules(
638
        request.address,
639
        *request.dependencies,
640
        description_of_origin=request.description_of_origin,
641
    )
642

643
    dependencies_rule: dict[Address, DependencyRuleApplication] = {}
2✔
644
    for dependency_address, (dependency_rules_family, dependency_target) in zip(
2✔
645
        request.dependencies, dependencies_family_adaptor
646
    ):
647
        dependencies_rule[dependency_address] = (
2✔
648
            build_file_dependency_rules_class.check_dependency_rules(
649
                origin_address=request.address,
650
                origin_adaptor=origin_target,
651
                dependencies_rules=origin_rules_family.dependencies_rules,
652
                dependency_address=dependency_address,
653
                dependency_adaptor=dependency_target,
654
                dependents_rules=dependency_rules_family.dependents_rules,
655
            )
656
        )
657
    return DependenciesRuleApplication(request.address, FrozenDict(dependencies_rule))
2✔
658

659

660
def rules():
12✔
661
    return (
12✔
662
        *collect_rules(),
663
        # The `BuildFileSymbolsInfo` is consumed by the `HelpInfoExtracter` and uses the scheduler
664
        # session `product_request()` directly so we need an explicit QueryRule to provide this type
665
        # as an valid entrypoint into the rule graph.
666
        QueryRule(BuildFileSymbolsInfo, ()),
667
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc