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

pantsbuild / pants / 18812500213

26 Oct 2025 03:42AM UTC coverage: 80.284% (+0.005%) from 80.279%
18812500213

Pull #22804

github

web-flow
Merge 2a56fdb46 into 4834308dc
Pull Request #22804: test_shell_command: use correct default cache scope for a test's environment

29 of 31 new or added lines in 2 files covered. (93.55%)

1314 existing lines in 64 files now uncovered.

77900 of 97030 relevant lines covered (80.28%)

3.35 hits per line

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

35.8
/src/python/pants/backend/typescript/goals/check.py
1
# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
from __future__ import annotations
1✔
5

UNCOV
6
import logging
1✔
UNCOV
7
import os
1✔
UNCOV
8
from dataclasses import dataclass, replace
1✔
UNCOV
9
from hashlib import sha256
1✔
UNCOV
10
from pathlib import Path
1✔
UNCOV
11
from typing import TYPE_CHECKING
1✔
12

UNCOV
13
from pants.backend.javascript.nodejs_project import AllNodeJSProjects, find_node_js_projects
1✔
UNCOV
14
from pants.backend.javascript.package_json import (
1✔
15
    AllPackageJson,
16
    OwningNodePackageRequest,
17
    PackageJson,
18
    all_package_json,
19
    find_owning_package,
20
)
UNCOV
21
from pants.backend.javascript.resolve import RequestNodeResolve, resolve_for_package
1✔
UNCOV
22
from pants.backend.javascript.subsystems.nodejs_tool import NodeJSToolRequest, prepare_tool_process
1✔
UNCOV
23
from pants.backend.javascript.target_types import JSRuntimeSourceField
1✔
UNCOV
24
from pants.backend.typescript.subsystem import TypeScriptSubsystem
1✔
UNCOV
25
from pants.backend.typescript.target_types import TypeScriptSourceField, TypeScriptTestSourceField
1✔
UNCOV
26
from pants.backend.typescript.tsconfig import AllTSConfigs, TSConfig, construct_effective_ts_configs
1✔
UNCOV
27
from pants.base.build_root import BuildRoot
1✔
UNCOV
28
from pants.build_graph.address import Address
1✔
UNCOV
29
from pants.core.goals.check import CheckRequest, CheckResult, CheckResults
1✔
UNCOV
30
from pants.core.target_types import FileSourceField
1✔
UNCOV
31
from pants.core.util_rules.system_binaries import CpBinary, FindBinary, MkdirBinary, TouchBinary
1✔
UNCOV
32
from pants.engine.fs import (
1✔
33
    EMPTY_DIGEST,
34
    CreateDigest,
35
    Digest,
36
    FileContent,
37
    GlobMatchErrorBehavior,
38
    PathGlobs,
39
)
UNCOV
40
from pants.engine.internals.graph import find_all_targets, hydrate_sources, transitive_targets
1✔
UNCOV
41
from pants.engine.internals.native_engine import MergeDigests
1✔
UNCOV
42
from pants.engine.internals.selectors import concurrently
1✔
UNCOV
43
from pants.engine.intrinsics import (
1✔
44
    create_digest,
45
    execute_process,
46
    merge_digests,
47
    path_globs_to_digest,
48
)
UNCOV
49
from pants.engine.process import Process
1✔
UNCOV
50
from pants.engine.rules import collect_rules, implicitly, rule
1✔
UNCOV
51
from pants.engine.target import (
1✔
52
    AllTargets,
53
    FieldSet,
54
    HydrateSourcesRequest,
55
    Target,
56
    TransitiveTargetsRequest,
57
)
UNCOV
58
from pants.engine.unions import UnionRule
1✔
UNCOV
59
from pants.option.global_options import GlobalOptions
1✔
UNCOV
60
from pants.util.frozendict import FrozenDict
1✔
UNCOV
61
from pants.util.logging import LogLevel
1✔
UNCOV
62
from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
1✔
63

64
if TYPE_CHECKING:
65
    from pants.backend.javascript.nodejs_project import NodeJSProject
66

UNCOV
67
logger = logging.getLogger(__name__)
1✔
68

69

UNCOV
70
@dataclass(frozen=True)
1✔
UNCOV
71
class CreateTscWrapperScriptRequest:
1✔
UNCOV
72
    project: NodeJSProject
1✔
UNCOV
73
    package_output_dirs: tuple[str, ...]
1✔
UNCOV
74
    build_root: BuildRoot
1✔
UNCOV
75
    named_cache_sandbox_mount_dir: str
1✔
UNCOV
76
    tsc_wrapper_filename: str
1✔
77

78

UNCOV
79
@dataclass(frozen=True)
1✔
UNCOV
80
class TypeScriptCheckFieldSet(FieldSet):
1✔
UNCOV
81
    required_fields = (JSRuntimeSourceField,)
1✔
82

UNCOV
83
    sources: JSRuntimeSourceField
1✔
84

85

UNCOV
86
class TypeScriptCheckRequest(CheckRequest):
1✔
UNCOV
87
    field_set_type = TypeScriptCheckFieldSet
1✔
UNCOV
88
    tool_name = TypeScriptSubsystem.options_scope
1✔
89

90

UNCOV
91
def _build_workspace_tsconfig_map(
1✔
92
    workspaces: FrozenOrderedSet[PackageJson], all_ts_configs: list[TSConfig]
93
) -> dict[str, TSConfig]:
94
    workspace_dir_map = {os.path.normpath(pkg.root_dir): pkg for pkg in workspaces}
×
95
    workspace_dir_to_tsconfig: dict[str, TSConfig] = {}
×
96

97
    # Only consider tsconfig.json files (exclude tsconfig.build.json, tsconfig.test.json, etc.)
98
    # since tsc only reads tsconfig.json by default and we don't support the `--project` flag currently
99
    # Additionally, TSConfig class only supports tsconfig.json.
100
    tsconfig_json_files = [
×
101
        cfg for cfg in all_ts_configs if os.path.basename(cfg.path) == "tsconfig.json"
102
    ]
103

104
    for ts_config in tsconfig_json_files:
×
105
        config_dir = os.path.normpath(os.path.dirname(ts_config.path))
×
106

107
        if config_dir in workspace_dir_map:
×
108
            workspace_dir_to_tsconfig[config_dir] = ts_config
×
109

110
    return workspace_dir_to_tsconfig
×
111

112

UNCOV
113
def _collect_package_output_dirs(
1✔
114
    project: NodeJSProject, project_ts_configs: list[TSConfig], project_root_path: Path
115
) -> tuple[str, ...]:
116
    workspace_dir_to_tsconfig = _build_workspace_tsconfig_map(
×
117
        project.workspaces, project_ts_configs
118
    )
119

120
    package_output_dirs: OrderedSet[str] = OrderedSet()
×
121
    for workspace_pkg in project.workspaces:
×
122
        workspace_pkg_path = Path(workspace_pkg.root_dir)
×
123

124
        if workspace_pkg_path == project_root_path:
×
125
            pkg_prefix = ""
×
126
        else:
127
            relative_path = workspace_pkg_path.relative_to(project_root_path)
×
128
            pkg_prefix = f"{relative_path.as_posix()}/"
×
129

130
        pkg_tsconfig = workspace_dir_to_tsconfig.get(os.path.normpath(workspace_pkg.root_dir))
×
131

132
        if pkg_tsconfig and pkg_tsconfig.out_dir:
×
133
            resolved_out_dir = f"{pkg_prefix}{pkg_tsconfig.out_dir.lstrip('./')}"
×
134
            package_output_dirs.add(resolved_out_dir)
×
135

136
    return tuple(package_output_dirs)
×
137

138

UNCOV
139
def _collect_project_configs(
1✔
140
    project: NodeJSProject, all_package_jsons: AllPackageJson, all_ts_configs: AllTSConfigs
141
) -> tuple[list, list[TSConfig]]:
142
    project_package_jsons = [
×
143
        pkg for pkg in all_package_jsons if pkg.root_dir.startswith(project.root_dir)
144
    ]
145
    project_ts_configs = [
×
146
        config for config in all_ts_configs if config.path.startswith(project.root_dir)
147
    ]
148
    return project_package_jsons, project_ts_configs
×
149

150

UNCOV
151
async def _collect_config_files_for_project(
1✔
152
    project: NodeJSProject,
153
    targets: list[Target],
154
    project_package_jsons: list,
155
    project_ts_configs: list[TSConfig],
156
) -> list[str]:
157
    config_files = []
×
158

159
    for pkg_json in project_package_jsons:
×
160
        config_files.append(pkg_json.file)
×
161

162
    for ts_config in project_ts_configs:
×
163
        config_files.append(ts_config.path)
×
164

165
    # Add file() targets that are dependencies of JS/TS targets
166
    # Note: Package manager config files
167
    # (.npmrc, .pnpmrc, pnpm-workspace.yaml) should be dependencies of package_json targets
168
    # since they affect package installation, not TypeScript compilation directly.
169
    target_addresses = [target.address for target in targets]
×
170
    transitive_targets_result = await transitive_targets(
×
171
        TransitiveTargetsRequest(target_addresses), **implicitly()
172
    )
173

174
    for target in transitive_targets_result.closure:
×
175
        if target.has_field(FileSourceField):
×
176
            file_path = target[FileSourceField].file_path
×
177
            if file_path.startswith(project.root_dir):
×
178
                config_files.append(file_path)
×
179

180
    return config_files
×
181

182

UNCOV
183
@rule
1✔
UNCOV
184
async def create_tsc_wrapper_script(
1✔
185
    request: CreateTscWrapperScriptRequest,
186
    cp_binary: CpBinary,
187
    mkdir_binary: MkdirBinary,
188
    find_binary: FindBinary,
189
    touch_binary: TouchBinary,
190
) -> Digest:
191
    project = request.project
×
192
    package_output_dirs = request.package_output_dirs
×
193
    build_root = request.build_root
×
194
    named_cache_sandbox_mount_dir = request.named_cache_sandbox_mount_dir
×
195
    tsc_wrapper_filename = request.tsc_wrapper_filename
×
196

197
    project_abs_path = os.path.join(build_root.path, project.root_dir)
×
198
    cache_key = sha256(project_abs_path.encode()).hexdigest()
×
199

200
    project_depth = len([p for p in project.root_dir.strip("/").split("/") if p])
×
201
    cache_relative_path = (
×
202
        "../" * project_depth + named_cache_sandbox_mount_dir
203
        if project_depth > 0
204
        else named_cache_sandbox_mount_dir
205
    )
206
    project_cache_subdir = f"{cache_relative_path}/{cache_key}"
×
207

208
    package_dirs = []
×
209
    for workspace_pkg in project.workspaces:
×
210
        if workspace_pkg.root_dir != project.root_dir:
×
211
            package_relative = workspace_pkg.root_dir.removeprefix(f"{project.root_dir}/")
×
212
            package_dirs.append(package_relative)
×
213
        else:
214
            package_dirs.append(".")
×
215
    package_dirs_str = " ".join(f'"{d}"' for d in package_dirs)
×
216
    output_dirs_str = " ".join(f'"{d}"' for d in package_output_dirs)
×
217

218
    script_content = """#!/bin/sh
×
219
# TypeScript incremental compilation cache wrapper
220

221
# All source files in sandbox have mtime as at sandbox creation (e.g. when the process started).
222
# Without any special handling, tsc will do a fast immediate rebuild (without checking tsbuildinfo metadata).
223
# (tsc outputs: "Project 'tsconfig.json' is out of date because output 'tsconfig.tsbuildinfo' is older than input 'tsconfig.json'")
224
# What we want: tsconfig (oldest), tsbuildinfo (newer), source files (newest).
225
# See also https://github.com/microsoft/TypeScript/issues/54563
226

227
set -e
228

229
CP_BIN="{cp_binary_path}"
230
MKDIR_BIN="{mkdir_binary_path}"
231
FIND_BIN="{find_binary_path}"
232
TOUCH_BIN="{touch_binary_path}"
233

234
PROJECT_CACHE_SUBDIR="{project_cache_subdir}"
235

236
# Ensure cache directory exists (it won't on first run)
237
"$MKDIR_BIN" -p "$PROJECT_CACHE_SUBDIR" > /dev/null 2>&1
238

239
# Copy cached files to working directory
240
"$CP_BIN" -a "$PROJECT_CACHE_SUBDIR/." . > /dev/null 2>&1
241

242
# Make tsconfig files oldest
243
# This could be improved by passing a list of tsconfig files, but that might be best done if/when we have a Target for tsconfig files.
244
"$FIND_BIN" . -type f -name "tsconfig*.json" -exec "$TOUCH_BIN" -t 202001010000 {{}} +
245

246
# Run tsc
247
"$@"
248

249
# Update cache
250
# Copy .tsbuildinfo files located at package root to cache
251
for package_dir in {package_dirs_str}; do
252
    for tsbuildinfo_file in "$package_dir"/*.tsbuildinfo; do
253
        if [ -f "$tsbuildinfo_file" ]; then
254
            "$MKDIR_BIN" -p "$PROJECT_CACHE_SUBDIR/$package_dir"
255
            "$CP_BIN" "$tsbuildinfo_file" "$PROJECT_CACHE_SUBDIR/$package_dir/"
256
        fi
257
    done
258
done
259

260
# Copy output directories to cache
261
# (the output files are needed because tsc checks them against tsbuildinfo hashes when determining if a rebuild is required)
262
for output_dir in {output_dirs_str}; do
263
    if [ -d "$output_dir" ]; then
264
        output_parent="$(dirname "$output_dir")"
265
        "$MKDIR_BIN" -p "$PROJECT_CACHE_SUBDIR/$output_parent"
266
        "$CP_BIN" -r "$output_dir" "$PROJECT_CACHE_SUBDIR/$output_parent/"
267
    fi
268
done
269
""".format(
270
        cp_binary_path=cp_binary.path,
271
        mkdir_binary_path=mkdir_binary.path,
272
        find_binary_path=find_binary.path,
273
        touch_binary_path=touch_binary.path,
274
        project_cache_subdir=project_cache_subdir,
275
        package_dirs_str=package_dirs_str,
276
        output_dirs_str=output_dirs_str,
277
    )
278

279
    script_digest = await create_digest(
×
280
        CreateDigest(
281
            [
282
                FileContent(
283
                    f"{project.root_dir}/{tsc_wrapper_filename}",
284
                    script_content.encode(),
285
                    is_executable=True,
286
                )
287
            ]
288
        ),
289
        **implicitly(),
290
    )
291

292
    return script_digest
×
293

294

UNCOV
295
async def _collect_project_targets(
1✔
296
    project: NodeJSProject,
297
    all_targets: AllTargets,
298
    all_projects: AllNodeJSProjects,
299
    project_ts_configs: list[TSConfig],
300
) -> list[Target]:
301
    has_check_js = any(
×
302
        ts_config.allow_js and ts_config.check_js for ts_config in project_ts_configs
303
    )
304

305
    if has_check_js:
×
306
        targets = [target for target in all_targets if target.has_field(JSRuntimeSourceField)]
×
307
    else:
308
        targets = [
×
309
            target
310
            for target in all_targets
311
            if (
312
                target.has_field(TypeScriptSourceField)
313
                or target.has_field(TypeScriptTestSourceField)
314
            )
315
        ]
316

317
    target_owning_packages = await concurrently(
×
318
        find_owning_package(OwningNodePackageRequest(target.address), **implicitly())
319
        for target in targets
320
    )
321

322
    project_targets = []
×
323
    for target, owning_package in zip(targets, target_owning_packages):
×
324
        if owning_package.target:
×
325
            package_directory = owning_package.target.address.spec_path
×
326
            target_project = all_projects.project_for_directory(package_directory)
×
327
            if target_project == project:
×
328
                project_targets.append(target)
×
329

330
    return project_targets
×
331

332

UNCOV
333
async def _hydrate_project_sources(
1✔
334
    project_targets: list[Target],
335
) -> Digest:
336
    workspace_target_sources = await concurrently(
×
337
        hydrate_sources(
338
            HydrateSourcesRequest(target[JSRuntimeSourceField]),
339
            **implicitly(),
340
        )
341
        for target in project_targets
342
    )
343

344
    return await merge_digests(
×
345
        MergeDigests([sources.snapshot.digest for sources in workspace_target_sources])
346
    )
347

348

UNCOV
349
async def _prepare_compilation_input(
1✔
350
    project: NodeJSProject,
351
    project_targets: list[Target],
352
    project_package_jsons: list,
353
    project_ts_configs: list[TSConfig],
354
    all_workspace_sources: Digest,
355
) -> Digest:
356
    config_files = await _collect_config_files_for_project(
×
357
        project, project_targets, project_package_jsons, project_ts_configs
358
    )
359
    for ts_config in project_ts_configs:
×
360
        ts_config.validate_outdir()
×
361

362
    config_digest = (
×
363
        await path_globs_to_digest(
364
            PathGlobs(config_files, glob_match_error_behavior=GlobMatchErrorBehavior.ignore),
365
            **implicitly(),
366
        )
367
        if config_files
368
        else EMPTY_DIGEST
369
    )
370

371
    return await merge_digests(MergeDigests([all_workspace_sources, config_digest]))
×
372

373

UNCOV
374
async def _prepare_tsc_build_process(
1✔
375
    project: NodeJSProject,
376
    subsystem: TypeScriptSubsystem,
377
    input_digest: Digest,
378
    package_output_dirs: tuple[str, ...],
379
    project_targets: list[Target],
380
    build_root: BuildRoot,
381
) -> Process:
382
    tsc_args = ("--build", *subsystem.extra_build_args)
×
383
    tsc_wrapper_filename = "__tsc_wrapper.sh"
×
384
    named_cache_local_dir = "typescript_cache"
×
385
    named_cache_sandbox_mount_dir = "._tsc_cache"
×
386

387
    project_resolve = await resolve_for_package(
×
388
        RequestNodeResolve(Address(project.root_dir)), **implicitly()
389
    )
390

391
    script_digest = await create_tsc_wrapper_script(
×
392
        CreateTscWrapperScriptRequest(
393
            project=project,
394
            package_output_dirs=package_output_dirs,
395
            build_root=build_root,
396
            named_cache_sandbox_mount_dir=named_cache_sandbox_mount_dir,
397
            tsc_wrapper_filename=tsc_wrapper_filename,
398
        ),
399
        **implicitly(),
400
    )
401

402
    tool_input = await merge_digests(MergeDigests([input_digest, script_digest]))
×
403

404
    tool_request = subsystem.request(
×
405
        args=tsc_args,
406
        input_digest=tool_input,
407
        description=f"Type-check TypeScript project {project.root_dir} ({len(project_targets)} targets)",
408
        level=LogLevel.DEBUG,
409
        project_caches=FrozenDict({named_cache_local_dir: named_cache_sandbox_mount_dir}),
410
    )
411

412
    # Uses the project's resolve as TypeScript execution requires project deps.
413
    tool_request_with_resolve: NodeJSToolRequest = replace(
×
414
        tool_request, resolve=project_resolve.resolve_name
415
    )
416

417
    process = await prepare_tool_process(tool_request_with_resolve, **implicitly())
×
418

419
    process_with_wrapper = replace(
×
420
        process,
421
        argv=(f"./{tsc_wrapper_filename}",) + process.argv,
422
        working_directory=project.root_dir,
423
        output_directories=package_output_dirs,
424
    )
425

426
    return process_with_wrapper
×
427

428

UNCOV
429
async def _typecheck_single_project(
1✔
430
    project: NodeJSProject,
431
    subsystem: TypeScriptSubsystem,
432
    global_options: GlobalOptions,
433
    all_targets: AllTargets,
434
    all_projects: AllNodeJSProjects,
435
    all_package_jsons: AllPackageJson,
436
    all_ts_configs: AllTSConfigs,
437
    build_root: BuildRoot,
438
) -> CheckResult:
439
    project_package_jsons, project_ts_configs = _collect_project_configs(
×
440
        project, all_package_jsons, all_ts_configs
441
    )
442

443
    project_targets = await _collect_project_targets(
×
444
        project, all_targets, all_projects, project_ts_configs
445
    )
446

447
    if not project_targets:
×
448
        return CheckResult(
×
449
            exit_code=0,
450
            stdout="",
451
            stderr="",
452
            partition_description=f"TypeScript check on {project.root_dir} (no targets)",
453
        )
454

455
    all_workspace_sources = await _hydrate_project_sources(project_targets)
×
456
    input_digest = await _prepare_compilation_input(
×
457
        project,
458
        project_targets,
459
        project_package_jsons,
460
        project_ts_configs,
461
        all_workspace_sources,
462
    )
463
    package_output_dirs = _collect_package_output_dirs(
×
464
        project, project_ts_configs, Path(project.root_dir)
465
    )
466

467
    process = await _prepare_tsc_build_process(
×
468
        project,
469
        subsystem,
470
        input_digest,
471
        package_output_dirs,
472
        project_targets,
473
        build_root,
474
    )
475
    result = await execute_process(process, **implicitly())
×
476

477
    return CheckResult.from_fallible_process_result(
×
478
        result,
479
        partition_description=f"TypeScript check on {project.root_dir} ({len(project_targets)} targets)",
480
        output_simplifier=global_options.output_simplifier(),
481
    )
482

483

UNCOV
484
@rule(desc="Check TypeScript compilation")
1✔
UNCOV
485
async def typecheck_typescript(
1✔
486
    request: TypeScriptCheckRequest,
487
    subsystem: TypeScriptSubsystem,
488
    global_options: GlobalOptions,
489
    build_root: BuildRoot,
490
) -> CheckResults:
491
    if subsystem.skip:
×
492
        return CheckResults([], checker_name=request.tool_name)
×
493

494
    field_sets = request.field_sets
×
495
    (
×
496
        all_projects,
497
        all_targets,
498
        all_package_jsons,
499
        all_ts_configs,
500
        owning_packages,
501
    ) = await concurrently(
502
        find_node_js_projects(**implicitly()),
503
        find_all_targets(),
504
        all_package_json(),
505
        construct_effective_ts_configs(),
506
        concurrently(
507
            find_owning_package(OwningNodePackageRequest(field_set.address), **implicitly())
508
            for field_set in field_sets
509
        ),
510
    )
511

512
    projects: OrderedSet[NodeJSProject] = OrderedSet()
×
513
    for owning_package in owning_packages:
×
514
        if owning_package.target:
×
515
            package_directory = owning_package.target.address.spec_path
×
516
            owning_project = all_projects.project_for_directory(package_directory)
×
517
            if owning_project:
×
518
                projects.add(owning_project)
×
519

520
    project_results = await concurrently(
×
521
        _typecheck_single_project(
522
            project,
523
            subsystem,
524
            global_options,
525
            all_targets,
526
            all_projects,
527
            all_package_jsons,
528
            all_ts_configs,
529
            build_root,
530
        )
531
        for project in projects
532
    )
533

534
    return CheckResults(project_results, checker_name=request.tool_name)
×
535

536

UNCOV
537
def rules():
1✔
UNCOV
538
    return [
1✔
539
        *collect_rules(),
540
        UnionRule(CheckRequest, TypeScriptCheckRequest),
541
    ]
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