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

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 hits per line

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

48.62
/src/python/pants/backend/typescript/tsconfig.py
1
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
"""tsconfig.json is primarily used by the typescript compiler in order to resolve types during
4
compilation. The format is also used by IDE:s to provide intellisense. The format is used for
5
projects that use only javascript, and is then named jsconfig.json.
6

7
See https://code.visualstudio.com/docs/languages/jsconfig
8
"""
9

10
from __future__ import annotations
5✔
11

12
import json
5✔
13
import logging
5✔
14
import os
5✔
15
import re
5✔
16
from collections.abc import Iterable
5✔
17
from dataclasses import dataclass
5✔
18
from pathlib import PurePath
5✔
19

20
from pants.engine.collection import Collection
5✔
21
from pants.engine.fs import DigestContents, FileContent, PathGlobs
5✔
22
from pants.engine.internals.selectors import concurrently
5✔
23
from pants.engine.intrinsics import get_digest_contents, path_globs_to_digest
5✔
24
from pants.engine.rules import Rule, collect_rules, rule
5✔
25
from pants.util.frozendict import FrozenDict
5✔
26

27
logger = logging.getLogger(__name__)
5✔
28

29

30
@dataclass(frozen=True)
5✔
31
class TSConfig:
5✔
32
    """Parsed tsconfig.json fields with `extends` substitution applied."""
33

34
    path: str
5✔
35
    module_resolution: str | None = None
5✔
36
    paths: FrozenDict[str, tuple[str, ...]] | None = None
5✔
37
    base_url: str | None = None
5✔
38
    out_dir: str | None = None
5✔
39
    allow_js: bool | None = None
5✔
40
    check_js: bool | None = None
5✔
41

42
    @property
5✔
43
    def resolution_root_dir(self) -> str:
5✔
44
        directory = os.path.dirname(self.path)
×
45
        return os.path.join(directory, self.base_url) if self.base_url else directory
×
46

47
    def validate_outdir(self) -> None:
5✔
UNCOV
48
        if not self.out_dir:
×
UNCOV
49
            raise ValueError(
×
50
                f"TypeScript configuration at '{self.path}' is missing required 'outDir' setting. "
51
                f"TypeScript type-checking requires an explicit outDir in compilerOptions to work properly. "
52
                f'Add \'"outDir": "./dist"\' (or your preferred output directory) to the compilerOptions '
53
                f"in {self.path}."
54
            )
55

UNCOV
56
        if ".." in self.out_dir:
×
UNCOV
57
            raise ValueError(
×
58
                f"TypeScript configuration at '{self.path}' has outDir '{self.out_dir}' "
59
                f"that uses '..' path components. Each package should use its own output directory "
60
                f"within its package boundary (e.g., './dist', './build'). Cross-package output "
61
                f"directories can cause build conflicts where packages overwrite each other's artifacts."
62
            )
63

UNCOV
64
        if os.path.isabs(self.out_dir):
×
UNCOV
65
            raise ValueError(
×
66
                f"TypeScript configuration at '{self.path}' has absolute outDir '{self.out_dir}'. "
67
                f"Use a relative path within the package directory instead (e.g., './dist', './build')."
68
            )
69

70

71
class AllTSConfigs(Collection[TSConfig]):
5✔
72
    pass
5✔
73

74

75
@dataclass(frozen=True)
5✔
76
class ParseTSConfigRequest:
5✔
77
    content: FileContent
5✔
78
    others: DigestContents
5✔
79

80

81
def _get_parent_config_content(
5✔
82
    child_path: str,
83
    extends_path: str,
84
    others: DigestContents,
85
) -> FileContent | None:
86
    if child_path.endswith(".json"):
×
87
        relative = os.path.dirname(child_path)
×
88
    else:
89
        relative = child_path
×
90
    relative = os.path.normpath(os.path.join(relative, extends_path))
×
91
    for target_file in ("tsconfig.json", "jsconfig.json"):
×
92
        if not extends_path.endswith(".json"):
×
93
            relative = os.path.join(relative, target_file)
×
94
        parent = next((other for other in others if other.path == relative), None)
×
95
        if parent:
×
96
            break
×
97
    if not parent:
×
98
        logger.warning(
×
99
            f"pants could not locate {child_path}'s 'extends' at {relative}. Found: {[other.path for other in others]}."
100
        )
101
        return None
×
102
    return parent
×
103

104

105
def _clean_tsconfig_contents(content: str) -> str:
5✔
106
    """The tsconfig.json uses a format similar to JSON ("JSON with comments"), but there are some
107
    important differences:
108

109
    * tsconfig.json allows both single-line (`// comment`) and multi-line comments (`/* comment */`) to be added
110
    anywhere in the file.
111
    * Trailing commas in arrays and objects are permitted.
112

113
    TypeScript uses its own parser to read the file; in standard JSON, trailing commas or comments are not allowed.
114
    """
115
    # This pattern matches:
116
    # 1. Strings: "..." or '...'
117
    # 2. Single-line comments: //...
118
    # 3. Multi-line comments: /*...*/
119
    # 4. Everything else (including potential trailing commas)
UNCOV
120
    pattern = r'("(?:\\.|[^"\\])*")|(\'(?:\\.|[^\'\\])*\')|(//.*?$)|(/\*.*?\*/)|,(\s*[\]}])'
×
121

UNCOV
122
    def replace(match):
×
UNCOV
123
        if match.group(1) or match.group(2):  # It's a string
×
UNCOV
124
            return match.group(0)  # Return the string as is
×
UNCOV
125
        elif match.group(3) or match.group(4):  # It's a comment
×
UNCOV
126
            return ""  # Remove the comment
×
UNCOV
127
        elif match.group(5):  # It's a trailing comma
×
UNCOV
128
            return match.group(5)  # Remove the comma keeping the closing brace/bracket
×
129
        return match.group(0)
×
130

UNCOV
131
    cleaned_content = re.sub(pattern, replace, content, flags=re.DOTALL | re.MULTILINE)
×
UNCOV
132
    return cleaned_content
×
133

134

135
def _parse_config_from_content(content: FileContent) -> tuple[TSConfig, str | None]:
5✔
136
    cleaned_tsconfig_contents = _clean_tsconfig_contents(content.content.decode("utf-8"))
×
137
    parsed_ts_config_json = FrozenDict.deep_freeze(json.loads(cleaned_tsconfig_contents))
×
138

139
    compiler_options = parsed_ts_config_json.get("compilerOptions", FrozenDict())
×
140
    return TSConfig(
×
141
        content.path,
142
        module_resolution=compiler_options.get("moduleResolution"),
143
        paths=compiler_options.get("paths"),
144
        base_url=compiler_options.get("baseUrl"),
145
        out_dir=compiler_options.get("outDir"),
146
        allow_js=compiler_options.get("allowJs"),
147
        check_js=compiler_options.get("checkJs"),
148
    ), parsed_ts_config_json.get("extends")
149

150

151
@rule
5✔
152
async def parse_extended_ts_config(request: ParseTSConfigRequest) -> TSConfig:
5✔
153
    ts_config, extends = _parse_config_from_content(request.content)
×
154
    if not extends:
×
155
        return ts_config
×
156

157
    parent_content = _get_parent_config_content(ts_config.path, extends, request.others)
×
158
    if parent_content:
×
159
        extended_parent = await parse_extended_ts_config(
×
160
            ParseTSConfigRequest(parent_content, request.others)
161
        )
162
    else:
163
        extended_parent = None
×
164

165
    if not extended_parent:
×
166
        return ts_config
×
167
    return TSConfig(
×
168
        ts_config.path,
169
        module_resolution=ts_config.module_resolution or extended_parent.module_resolution,
170
        paths=ts_config.paths or extended_parent.paths,
171
        base_url=ts_config.base_url or extended_parent.base_url,
172
        # Do NOT inherit outDir - paths in extended configs are resolved relative to where they're defined,
173
        # not where they're used, making inherited outDir values incorrect for child projects
174
        out_dir=ts_config.out_dir,
175
        allow_js=ts_config.allow_js if ts_config.allow_js is not None else extended_parent.allow_js,
176
        check_js=ts_config.check_js if ts_config.check_js is not None else extended_parent.check_js,
177
    )
178

179

180
@dataclass(frozen=True)
5✔
181
class TSConfigsRequest:
5✔
182
    target_file: str
5✔
183

184

185
@rule
5✔
186
async def construct_effective_ts_configs() -> AllTSConfigs:
5✔
187
    all_files = await path_globs_to_digest(PathGlobs(["**/tsconfig*.json", "**/jsconfig*.json"]))
×
188
    digest_contents = await get_digest_contents(all_files)
×
189

190
    return AllTSConfigs(
×
191
        await concurrently(
192
            parse_extended_ts_config(ParseTSConfigRequest(digest_content, digest_contents))
193
            for digest_content in digest_contents
194
        )
195
    )
196

197

198
@dataclass(frozen=True)
5✔
199
class ClosestTSConfig:
5✔
200
    ts_config: TSConfig | None
5✔
201

202

203
@dataclass(frozen=True)
5✔
204
class ParentTSConfigRequest:
5✔
205
    file: str
5✔
206

207

208
@rule(desc="Finding parent tsconfig.json")
5✔
209
async def find_parent_ts_config(req: ParentTSConfigRequest) -> ClosestTSConfig:
5✔
210
    all_configs = await construct_effective_ts_configs()
×
211
    configs_by_longest_path = sorted(all_configs, key=lambda config: len(config.path), reverse=True)
×
212
    for config in configs_by_longest_path:
×
213
        if PurePath(req.file).is_relative_to(os.path.dirname(config.path)):
×
214
            return ClosestTSConfig(config)
×
215
    return ClosestTSConfig(None)
×
216

217

218
def rules() -> Iterable[Rule]:
5✔
219
    return collect_rules()
5✔
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