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

pantsbuild / pants / 26342152999

23 May 2026 07:59PM UTC coverage: 91.165% (-1.6%) from 92.792%
26342152999

push

github

web-flow
Run Linux ARM CI on Depot runners (#23363)

RunsOn is deprecating their v2 stack, and rather than migrate
to v3 we should use the resources kindly donated by Depot.

GitHub also now has Linux ARM runners, should we need them.

87305 of 95766 relevant lines covered (91.16%)

3.87 hits per line

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

96.36
/src/python/pants/backend/javascript/dependency_inference/rules.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
from __future__ import annotations
11✔
4

5
import itertools
11✔
6
import logging
11✔
7
import os.path
11✔
8
from collections.abc import Iterable
11✔
9
from dataclasses import dataclass
11✔
10
from pathlib import PurePath
11✔
11

12
from pants.backend.javascript import package_json
11✔
13
from pants.backend.javascript.package_json import (
11✔
14
    FirstPartyNodePackageTargets,
15
    NodePackageDependenciesField,
16
    NodePackageNameField,
17
    OwningNodePackage,
18
    OwningNodePackageRequest,
19
    PackageJsonImports,
20
    PackageJsonSourceField,
21
    find_owning_package,
22
    script_entrypoints_for_source,
23
    subpath_imports_for_source,
24
)
25
from pants.backend.javascript.subsystems.nodejs_infer import NodeJSInfer
11✔
26
from pants.backend.javascript.target_types import (
11✔
27
    JS_FILE_EXTENSIONS,
28
    JSRuntimeDependenciesField,
29
    JSRuntimeSourceField,
30
)
31
from pants.backend.jsx.target_types import JSX_FILE_EXTENSIONS
11✔
32
from pants.backend.tsx.target_types import TSX_FILE_EXTENSIONS
11✔
33
from pants.backend.typescript import tsconfig
11✔
34
from pants.backend.typescript.target_types import TS_FILE_EXTENSIONS
11✔
35
from pants.backend.typescript.tsconfig import ParentTSConfigRequest, TSConfig, find_parent_ts_config
11✔
36
from pants.build_graph.address import Address
11✔
37
from pants.core.util_rules.unowned_dependency_behavior import (
11✔
38
    UnownedDependencyError,
39
    UnownedDependencyUsage,
40
)
41
from pants.engine.addresses import Addresses
11✔
42
from pants.engine.fs import PathGlobs
11✔
43
from pants.engine.internals.graph import (
11✔
44
    OwnersRequest,
45
    find_owners,
46
    hydrate_sources,
47
    resolve_targets,
48
)
49
from pants.engine.internals.native_dep_inference import JavascriptDependencyCandidate
11✔
50
from pants.engine.internals.native_engine import InferenceMetadata, NativeDependenciesRequest
11✔
51
from pants.engine.internals.selectors import concurrently
11✔
52
from pants.engine.intrinsics import parse_javascript_deps, path_globs_to_paths
11✔
53
from pants.engine.rules import Rule, collect_rules, implicitly, rule
11✔
54
from pants.engine.target import (
11✔
55
    FieldSet,
56
    HydrateSourcesRequest,
57
    InferDependenciesRequest,
58
    InferredDependencies,
59
)
60
from pants.engine.unions import UnionRule
11✔
61
from pants.util.docutil import doc_url
11✔
62
from pants.util.frozendict import FrozenDict
11✔
63
from pants.util.ordered_set import FrozenOrderedSet
11✔
64
from pants.util.strutil import bullet_list, softwrap
11✔
65

66
logger = logging.getLogger(__name__)
11✔
67

68

69
@dataclass(frozen=True)
11✔
70
class NodePackageInferenceFieldSet(FieldSet):
11✔
71
    required_fields = (PackageJsonSourceField, NodePackageDependenciesField)
11✔
72

73
    source: PackageJsonSourceField
11✔
74
    dependencies: NodePackageDependenciesField
11✔
75

76

77
class InferNodePackageDependenciesRequest(InferDependenciesRequest):
11✔
78
    infer_from = NodePackageInferenceFieldSet
11✔
79

80

81
@dataclass(frozen=True)
11✔
82
class JSSourceInferenceFieldSet(FieldSet):
11✔
83
    required_fields = (JSRuntimeSourceField, JSRuntimeDependenciesField)
11✔
84

85
    source: JSRuntimeSourceField
11✔
86
    dependencies: JSRuntimeDependenciesField
11✔
87

88

89
class InferJSDependenciesRequest(InferDependenciesRequest):
11✔
90
    infer_from = JSSourceInferenceFieldSet
11✔
91

92

93
@rule
11✔
94
async def infer_node_package_dependencies(
11✔
95
    request: InferNodePackageDependenciesRequest,
96
    nodejs_infer: NodeJSInfer,
97
) -> InferredDependencies:
98
    if not nodejs_infer.package_json_entry_points:
8✔
99
        return InferredDependencies(())
×
100
    entry_points = await script_entrypoints_for_source(request.field_set.source)
8✔
101
    candidate_js_files = await find_owners(
8✔
102
        OwnersRequest(tuple(entry_points.globs_from_root())), **implicitly()
103
    )
104
    js_targets = await resolve_targets(**implicitly(Addresses(candidate_js_files)))
8✔
105
    return InferredDependencies(
8✔
106
        tgt.address for tgt in js_targets if tgt.has_field(JSRuntimeSourceField)
107
    )
108

109

110
class NodePackageCandidateMap(FrozenDict[str, Address]):
11✔
111
    pass
11✔
112

113

114
@dataclass(frozen=True)
11✔
115
class RequestNodePackagesCandidateMap:
11✔
116
    address: Address
11✔
117

118

119
@rule
11✔
120
async def map_candidate_node_packages(
11✔
121
    req: RequestNodePackagesCandidateMap, first_party: FirstPartyNodePackageTargets
122
) -> NodePackageCandidateMap:
123
    owning_pkg = await find_owning_package(OwningNodePackageRequest(req.address))
4✔
124
    candidate_tgts = itertools.chain(
4✔
125
        first_party, owning_pkg.third_party if owning_pkg != OwningNodePackage.no_owner() else ()
126
    )
127
    return NodePackageCandidateMap(
4✔
128
        (tgt[NodePackageNameField].value, tgt.address) for tgt in candidate_tgts
129
    )
130

131

132
def _create_inference_metadata(
11✔
133
    imports: PackageJsonImports, config: TSConfig | None
134
) -> InferenceMetadata:
135
    return InferenceMetadata.javascript(
4✔
136
        imports.root_dir,
137
        dict(imports.imports),
138
        config.resolution_root_dir if config else None,
139
        dict(config.paths or {}) if config else {},
140
    )
141

142

143
async def _prepare_inference_metadata(address: Address, file_path: str) -> InferenceMetadata:
11✔
144
    owning_pkg, maybe_config = await concurrently(
4✔
145
        find_owning_package(OwningNodePackageRequest(address)),
146
        find_parent_ts_config(ParentTSConfigRequest(file_path), **implicitly()),
147
    )
148
    if not owning_pkg.target:
4✔
149
        return InferenceMetadata.javascript(
×
150
            (
151
                os.path.dirname(maybe_config.ts_config.path)
152
                if maybe_config.ts_config
153
                else address.spec_path
154
            ),
155
            {},
156
            maybe_config.ts_config.resolution_root_dir if maybe_config.ts_config else None,
157
            dict(maybe_config.ts_config.paths or {}) if maybe_config.ts_config else {},
158
        )
159
    return _create_inference_metadata(
4✔
160
        await subpath_imports_for_source(owning_pkg.target[PackageJsonSourceField]),
161
        maybe_config.ts_config,
162
    )
163

164

165
def _add_extensions(file_imports: frozenset[str], file_extensions: tuple[str, ...]) -> PathGlobs:
11✔
166
    extensions = file_extensions + tuple(f"/index{ext}" for ext in file_extensions)
3✔
167
    valid_file_extensions = set(file_extensions)
3✔
168
    return PathGlobs(
3✔
169
        string
170
        for file_import in file_imports
171
        for string in (
172
            [file_import]
173
            if PurePath(file_import).suffix in valid_file_extensions
174
            else [f"{file_import}{ext}" for ext in extensions]
175
        )
176
    )
177

178

179
async def _determine_import_from_candidates(
11✔
180
    candidates: JavascriptDependencyCandidate,
181
    package_candidate_map: NodePackageCandidateMap,
182
    file_extensions: tuple[str, ...],
183
) -> Addresses:
184
    paths = await path_globs_to_paths(
3✔
185
        _add_extensions(
186
            candidates.file_imports,
187
            file_extensions,
188
        )
189
    )
190
    local_owners = await find_owners(OwnersRequest(paths.files), **implicitly())
3✔
191
    owning_targets = await resolve_targets(**implicitly(Addresses(local_owners)))
3✔
192

193
    addresses = Addresses(tgt.address for tgt in owning_targets)
3✔
194
    if not local_owners:
3✔
195
        non_path_string_bases = FrozenOrderedSet(
3✔
196
            (
197
                # Handle scoped packages like "@foo/bar"
198
                # Ref: https://docs.npmjs.com/cli/v11/using-npm/scope
199
                "/".join(non_path_string.split("/")[:2])
200
                if non_path_string.startswith("@")
201
                # Handle regular packages like "foo"
202
                else non_path_string.partition("/")[0]
203
            )
204
            for non_path_string in candidates.package_imports
205
        )
206
        addresses = Addresses(
3✔
207
            package_candidate_map[pkg_name]
208
            for pkg_name in non_path_string_bases
209
            if pkg_name in package_candidate_map
210
        )
211
    return addresses
3✔
212

213

214
def _handle_unowned_imports(
11✔
215
    address: Address,
216
    unowned_dependency_behavior: UnownedDependencyUsage,
217
    unowned_imports: frozenset[str],
218
) -> None:
219
    if not unowned_imports or unowned_dependency_behavior is UnownedDependencyUsage.DoNothing:
4✔
220
        return
4✔
221

222
    url = doc_url(
3✔
223
        "docs/using-pants/troubleshooting-common-issues#import-errors-and-missing-dependencies"
224
    )
225
    msg = softwrap(
3✔
226
        f"""
227
        Pants cannot infer owners for the following imports in the target {address}:
228

229
        {bullet_list(sorted(unowned_imports))}
230

231
        If you do not expect an import to be inferable, add `// pants: no-infer-dep` to the
232
        import line. Otherwise, see {url} for common problems.
233
        """
234
    )
235
    if unowned_dependency_behavior is UnownedDependencyUsage.LogWarning:
3✔
236
        logger.warning(msg)
3✔
237
    else:
238
        raise UnownedDependencyError(msg)
×
239

240

241
def _is_node_builtin_module(import_string: str) -> bool:
11✔
242
    """https://nodejs.org/api/modules.html#built-in-modules."""
243
    return import_string.startswith("node:")
3✔
244

245

246
@rule
11✔
247
async def infer_js_source_dependencies(
11✔
248
    request: InferJSDependenciesRequest,
249
    nodejs_infer: NodeJSInfer,
250
) -> InferredDependencies:
251
    source: JSRuntimeSourceField = request.field_set.source
4✔
252
    if not nodejs_infer.imports:
4✔
253
        return InferredDependencies(())
×
254

255
    sources = await hydrate_sources(
4✔
256
        HydrateSourcesRequest(source, for_sources_types=[JSRuntimeSourceField]), **implicitly()
257
    )
258
    metadata = await _prepare_inference_metadata(request.field_set.address, source.file_path)
4✔
259

260
    native_results, candidate_pkgs = await concurrently(
4✔
261
        parse_javascript_deps(NativeDependenciesRequest(sources.snapshot.digest, metadata)),
262
        map_candidate_node_packages(
263
            RequestNodePackagesCandidateMap(request.field_set.address), **implicitly()
264
        ),
265
    )
266
    assert len(native_results.path_to_deps) == 1
4✔
267
    native_result = next(iter(native_results.path_to_deps.values()))
4✔
268

269
    imports = dict(
4✔
270
        zip(
271
            native_result.imports,
272
            await concurrently(
273
                _determine_import_from_candidates(
274
                    candidates,
275
                    candidate_pkgs,
276
                    file_extensions=(
277
                        JS_FILE_EXTENSIONS
278
                        + JSX_FILE_EXTENSIONS
279
                        + TS_FILE_EXTENSIONS
280
                        + TSX_FILE_EXTENSIONS
281
                    ),
282
                )
283
                for string, candidates in native_result.imports.items()
284
            ),
285
        )
286
    )
287
    _handle_unowned_imports(
4✔
288
        request.field_set.address,
289
        nodejs_infer.unowned_dependency_behavior,
290
        frozenset(
291
            string
292
            for string, addresses in imports.items()
293
            if not addresses and not _is_node_builtin_module(string)
294
        ),
295
    )
296

297
    return InferredDependencies(itertools.chain.from_iterable(imports.values()))
4✔
298

299

300
def rules() -> Iterable[Rule | UnionRule]:
11✔
301
    return [
11✔
302
        *collect_rules(),
303
        *package_json.rules(),
304
        *tsconfig.rules(),
305
        UnionRule(InferDependenciesRequest, InferNodePackageDependenciesRequest),
306
        UnionRule(InferDependenciesRequest, InferJSDependenciesRequest),
307
    ]
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