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

pantsbuild / pants / 19605163397

23 Nov 2025 03:21AM UTC coverage: 80.249% (-0.04%) from 80.284%
19605163397

Pull #22911

github

web-flow
Merge 94c33cc2d into 5244eeb7a
Pull Request #22911: Replaced isort with ruff lint

13 of 16 new or added lines in 13 files covered. (81.25%)

32 existing lines in 1 file now uncovered.

78348 of 97631 relevant lines covered (80.25%)

3.09 hits per line

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

61.11
/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 ParsedJavascriptDependencyCandidate
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:
×
99
        return InferredDependencies(())
×
100
    entry_points = await script_entrypoints_for_source(request.field_set.source)
×
101
    candidate_js_files = await find_owners(
×
102
        OwnersRequest(tuple(entry_points.globs_from_root())), **implicitly()
103
    )
104
    js_targets = await resolve_targets(**implicitly(Addresses(candidate_js_files)))
×
105
    return InferredDependencies(
×
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:
NEW
123
    owning_pkg = await find_owning_package(OwningNodePackageRequest(req.address))
×
124
    candidate_tgts = itertools.chain(
×
125
        first_party, owning_pkg.third_party if owning_pkg != OwningNodePackage.no_owner() else ()
126
    )
127
    return NodePackageCandidateMap(
×
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(
×
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(
×
145
        find_owning_package(OwningNodePackageRequest(address)),
146
        find_parent_ts_config(ParentTSConfigRequest(file_path), **implicitly()),
147
    )
148
    if not owning_pkg.target:
×
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(
×
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)
×
167
    valid_file_extensions = set(file_extensions)
×
168
    return PathGlobs(
×
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: ParsedJavascriptDependencyCandidate,
181
    package_candidate_map: NodePackageCandidateMap,
182
    file_extensions: tuple[str, ...],
183
) -> Addresses:
184
    paths = await path_globs_to_paths(
×
185
        _add_extensions(
186
            candidates.file_imports,
187
            file_extensions,
188
        )
189
    )
190
    local_owners = await find_owners(OwnersRequest(paths.files), **implicitly())
×
191
    owning_targets = await resolve_targets(**implicitly(Addresses(local_owners)))
×
192

193
    addresses = Addresses(tgt.address for tgt in owning_targets)
×
194
    if not local_owners:
×
195
        non_path_string_bases = FrozenOrderedSet(
×
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(
×
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
×
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:
×
220
        return
×
221

222
    url = doc_url(
×
223
        "docs/using-pants/troubleshooting-common-issues#import-errors-and-missing-dependencies"
224
    )
225
    msg = softwrap(
×
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:
×
236
        logger.warning(msg)
×
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:")
×
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
×
252
    if not nodejs_infer.imports:
×
253
        return InferredDependencies(())
×
254

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

260
    import_strings, candidate_pkgs = await concurrently(
×
261
        parse_javascript_deps(NativeDependenciesRequest(sources.snapshot.digest, metadata)),
262
        map_candidate_node_packages(
263
            RequestNodePackagesCandidateMap(request.field_set.address), **implicitly()
264
        ),
265
    )
266
    imports = dict(
×
267
        zip(
268
            import_strings.imports,
269
            await concurrently(
270
                _determine_import_from_candidates(
271
                    candidates,
272
                    candidate_pkgs,
273
                    file_extensions=(
274
                        JS_FILE_EXTENSIONS
275
                        + JSX_FILE_EXTENSIONS
276
                        + TS_FILE_EXTENSIONS
277
                        + TSX_FILE_EXTENSIONS
278
                    ),
279
                )
280
                for string, candidates in import_strings.imports.items()
281
            ),
282
        )
283
    )
284
    _handle_unowned_imports(
×
285
        request.field_set.address,
286
        nodejs_infer.unowned_dependency_behavior,
287
        frozenset(
288
            string
289
            for string, addresses in imports.items()
290
            if not addresses and not _is_node_builtin_module(string)
291
        ),
292
    )
293

294
    return InferredDependencies(itertools.chain.from_iterable(imports.values()))
×
295

296

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