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

pantsbuild / pants / 18517631058

15 Oct 2025 04:18AM UTC coverage: 69.207% (-11.1%) from 80.267%
18517631058

Pull #22745

github

web-flow
Merge 642a76ca1 into 99919310e
Pull Request #22745: [windows] Add windows support in the stdio crate.

53815 of 77759 relevant lines covered (69.21%)

2.42 hits per line

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

49.49
/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
7✔
11

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

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

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

29

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

34
    path: str
7✔
35
    module_resolution: str | None = None
7✔
36
    paths: FrozenDict[str, tuple[str, ...]] | None = None
7✔
37
    base_url: str | None = None
7✔
38

39
    @property
7✔
40
    def resolution_root_dir(self) -> str:
7✔
41
        directory = os.path.dirname(self.path)
×
42
        return os.path.join(directory, self.base_url) if self.base_url else directory
×
43

44

45
class AllTSConfigs(Collection[TSConfig]):
7✔
46
    pass
7✔
47

48

49
@dataclass(frozen=True)
7✔
50
class ParseTSConfigRequest:
7✔
51
    content: FileContent
7✔
52
    others: DigestContents
7✔
53

54

55
def _get_parent_config_content(
7✔
56
    child_path: str,
57
    extends_path: str,
58
    others: DigestContents,
59
) -> FileContent | None:
60
    if child_path.endswith(".json"):
×
61
        relative = os.path.dirname(child_path)
×
62
    else:
63
        relative = child_path
×
64
    relative = os.path.normpath(os.path.join(relative, extends_path))
×
65
    for target_file in ("tsconfig.json", "jsconfig.json"):
×
66
        if not extends_path.endswith(".json"):
×
67
            relative = os.path.join(relative, target_file)
×
68
        parent = next((other for other in others if other.path == relative), None)
×
69
        if parent:
×
70
            break
×
71
    if not parent:
×
72
        logger.warning(
×
73
            f"pants could not locate {child_path}'s 'extends' at {relative}. Found: {[other.path for other in others]}."
74
        )
75
        return None
×
76
    return parent
×
77

78

79
def _clean_tsconfig_contents(content: str) -> str:
7✔
80
    """The tsconfig.json uses a format similar to JSON ("JSON with comments"), but there are some
81
    important differences:
82

83
    * tsconfig.json allows both single-line (`// comment`) and multi-line comments (`/* comment */`) to be added
84
    anywhere in the file.
85
    * Trailing commas in arrays and objects are permitted.
86

87
    TypeScript uses its own parser to read the file; in standard JSON, trailing commas or comments are not allowed.
88
    """
89
    # This pattern matches:
90
    # 1. Strings: "..." or '...'
91
    # 2. Single-line comments: //...
92
    # 3. Multi-line comments: /*...*/
93
    # 4. Everything else (including potential trailing commas)
94
    pattern = r'("(?:\\.|[^"\\])*")|(\'(?:\\.|[^\'\\])*\')|(//.*?$)|(/\*.*?\*/)|,(\s*[\]}])'
×
95

96
    def replace(match):
×
97
        if match.group(1) or match.group(2):  # It's a string
×
98
            return match.group(0)  # Return the string as is
×
99
        elif match.group(3) or match.group(4):  # It's a comment
×
100
            return ""  # Remove the comment
×
101
        elif match.group(5):  # It's a trailing comma
×
102
            return match.group(5)  # Remove the comma keeping the closing brace/bracket
×
103
        return match.group(0)
×
104

105
    cleaned_content = re.sub(pattern, replace, content, flags=re.DOTALL | re.MULTILINE)
×
106
    return cleaned_content
×
107

108

109
def _parse_config_from_content(content: FileContent) -> tuple[TSConfig, str | None]:
7✔
110
    cleaned_tsconfig_contents = _clean_tsconfig_contents(content.content.decode("utf-8"))
×
111
    parsed_ts_config_json = FrozenDict.deep_freeze(json.loads(cleaned_tsconfig_contents))
×
112

113
    compiler_options = parsed_ts_config_json.get("compilerOptions", FrozenDict())
×
114
    return TSConfig(
×
115
        content.path,
116
        module_resolution=compiler_options.get("moduleResolution"),
117
        paths=compiler_options.get("paths"),
118
        base_url=compiler_options.get("baseUrl"),
119
    ), parsed_ts_config_json.get("extends")
120

121

122
@rule
7✔
123
async def parse_extended_ts_config(request: ParseTSConfigRequest) -> TSConfig:
7✔
124
    ts_config, extends = _parse_config_from_content(request.content)
×
125
    if not extends:
×
126
        return ts_config
×
127

128
    parent_content = _get_parent_config_content(ts_config.path, extends, request.others)
×
129
    if parent_content:
×
130
        extended_parent = await parse_extended_ts_config(
×
131
            ParseTSConfigRequest(parent_content, request.others)
132
        )
133
    else:
134
        extended_parent = None
×
135

136
    if not extended_parent:
×
137
        return ts_config
×
138
    return TSConfig(
×
139
        ts_config.path,
140
        module_resolution=ts_config.module_resolution or extended_parent.module_resolution,
141
        paths=ts_config.paths or extended_parent.paths,
142
        base_url=ts_config.base_url or extended_parent.base_url,
143
    )
144

145

146
@dataclass(frozen=True)
7✔
147
class TSConfigsRequest:
7✔
148
    target_file: str
7✔
149

150

151
@rule
7✔
152
async def construct_effective_ts_configs() -> AllTSConfigs:
7✔
153
    all_files = await path_globs_to_digest(PathGlobs(["**/tsconfig*.json", "**/jsconfig*.json"]))
×
154
    digest_contents = await get_digest_contents(all_files)
×
155

156
    return AllTSConfigs(
×
157
        await concurrently(
158
            parse_extended_ts_config(ParseTSConfigRequest(digest_content, digest_contents))
159
            for digest_content in digest_contents
160
        )
161
    )
162

163

164
@dataclass(frozen=True)
7✔
165
class ClosestTSConfig:
7✔
166
    ts_config: TSConfig | None
7✔
167

168

169
@dataclass(frozen=True)
7✔
170
class ParentTSConfigRequest:
7✔
171
    file: str
7✔
172

173

174
@rule(desc="Finding parent tsconfig.json")
7✔
175
async def find_parent_ts_config(req: ParentTSConfigRequest) -> ClosestTSConfig:
7✔
176
    all_configs = await construct_effective_ts_configs()
×
177
    configs_by_longest_path = sorted(all_configs, key=lambda config: len(config.path), reverse=True)
×
178
    for config in configs_by_longest_path:
×
179
        if PurePath(req.file).is_relative_to(os.path.dirname(config.path)):
×
180
            return ClosestTSConfig(config)
×
181
    return ClosestTSConfig(None)
×
182

183

184
def rules() -> Iterable[Rule]:
7✔
185
    return collect_rules()
6✔
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