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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

0.0
/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).
UNCOV
3
from __future__ import annotations
×
4

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

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

UNCOV
69
logger = logging.getLogger(__name__)
×
70

71

UNCOV
72
@dataclass(frozen=True)
×
UNCOV
73
class NodePackageInferenceFieldSet(FieldSet):
×
UNCOV
74
    required_fields = (PackageJsonSourceField, NodePackageDependenciesField)
×
75

UNCOV
76
    source: PackageJsonSourceField
×
UNCOV
77
    dependencies: NodePackageDependenciesField
×
78

79

UNCOV
80
class InferNodePackageDependenciesRequest(InferDependenciesRequest):
×
UNCOV
81
    infer_from = NodePackageInferenceFieldSet
×
82

83

UNCOV
84
@dataclass(frozen=True)
×
UNCOV
85
class JSSourceInferenceFieldSet(FieldSet):
×
UNCOV
86
    required_fields = (JSRuntimeSourceField, JSRuntimeDependenciesField)
×
87

UNCOV
88
    source: JSRuntimeSourceField
×
UNCOV
89
    dependencies: JSRuntimeDependenciesField
×
90

91

UNCOV
92
class InferJSDependenciesRequest(InferDependenciesRequest):
×
UNCOV
93
    infer_from = JSSourceInferenceFieldSet
×
94

95

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

112

UNCOV
113
class NodePackageCandidateMap(FrozenDict[str, Address]):
×
UNCOV
114
    pass
×
115

116

UNCOV
117
@dataclass(frozen=True)
×
UNCOV
118
class RequestNodePackagesCandidateMap:
×
UNCOV
119
    address: Address
×
120

121

UNCOV
122
@rule
×
UNCOV
123
async def map_candidate_node_packages(
×
124
    req: RequestNodePackagesCandidateMap, first_party: FirstPartyNodePackageTargets
125
) -> NodePackageCandidateMap:
126
    owning_pkg = await find_owning_package_get(OwningNodePackageRequest(req.address))
×
127
    candidate_tgts = itertools.chain(
×
128
        first_party, owning_pkg.third_party if owning_pkg != OwningNodePackage.no_owner() else ()
129
    )
130
    return NodePackageCandidateMap(
×
131
        (tgt[NodePackageNameField].value, tgt.address) for tgt in candidate_tgts
132
    )
133

134

UNCOV
135
def _create_inference_metadata(
×
136
    imports: PackageJsonImports, config: TSConfig | None
137
) -> InferenceMetadata:
138
    return InferenceMetadata.javascript(
×
139
        imports.root_dir,
140
        dict(imports.imports),
141
        config.resolution_root_dir if config else None,
142
        dict(config.paths or {}) if config else {},
143
    )
144

145

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

167

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

181

UNCOV
182
async def _determine_import_from_candidates(
×
183
    candidates: ParsedJavascriptDependencyCandidate,
184
    package_candidate_map: NodePackageCandidateMap,
185
    file_extensions: tuple[str, ...],
186
) -> Addresses:
187
    paths = await path_globs_to_paths(
×
188
        _add_extensions(
189
            candidates.file_imports,
190
            file_extensions,
191
        )
192
    )
193
    local_owners = await find_owners(OwnersRequest(paths.files), **implicitly())
×
194
    owning_targets = await resolve_targets(**implicitly(Addresses(local_owners)))
×
195

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

216

UNCOV
217
def _handle_unowned_imports(
×
218
    address: Address,
219
    unowned_dependency_behavior: UnownedDependencyUsage,
220
    unowned_imports: frozenset[str],
221
) -> None:
222
    if not unowned_imports or unowned_dependency_behavior is UnownedDependencyUsage.DoNothing:
×
223
        return
×
224

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

232
        {bullet_list(sorted(unowned_imports))}
233

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

243

UNCOV
244
def _is_node_builtin_module(import_string: str) -> bool:
×
245
    """https://nodejs.org/api/modules.html#built-in-modules."""
246
    return import_string.startswith("node:")
×
247

248

UNCOV
249
@rule
×
UNCOV
250
async def infer_js_source_dependencies(
×
251
    request: InferJSDependenciesRequest,
252
    nodejs_infer: NodeJSInfer,
253
) -> InferredDependencies:
254
    source: JSRuntimeSourceField = request.field_set.source
×
255
    if not nodejs_infer.imports:
×
256
        return InferredDependencies(())
×
257

258
    sources = await hydrate_sources(
×
259
        HydrateSourcesRequest(source, for_sources_types=[JSRuntimeSourceField]), **implicitly()
260
    )
261
    metadata = await _prepare_inference_metadata(request.field_set.address, source.file_path)
×
262

263
    import_strings, candidate_pkgs = await concurrently(
×
264
        parse_javascript_deps(NativeDependenciesRequest(sources.snapshot.digest, metadata)),
265
        map_candidate_node_packages(
266
            RequestNodePackagesCandidateMap(request.field_set.address), **implicitly()
267
        ),
268
    )
269
    imports = dict(
×
270
        zip(
271
            import_strings.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 import_strings.imports.items()
284
            ),
285
        )
286
    )
287
    _handle_unowned_imports(
×
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()))
×
298

299

UNCOV
300
def rules() -> Iterable[Rule | UnionRule]:
×
UNCOV
301
    return [
×
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

© 2025 Coveralls, Inc