Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "uv run python -m pytest tests/e2e -x -q",
"ran_at": 1788573884,
"exit_code": 0,
"commit": "adc459ef86b67bf5fcf43e1b0f627c9337c13120",
"branch": "claude/1064-pm-tsgate-z8m5"
}
10 changes: 10 additions & 0 deletions internals/checks/src/checks/platforms_mirror/as_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""An object-literal source as JSON: keys quoted, trailing commas dropped."""

from __future__ import annotations

import re


def as_json(text: str) -> str:
keyed = re.sub(r"([{,]\s*)([A-Za-z_$][\w$]*)\s*:", r'\1"\2":', text)
return re.sub(r",(\s*[}\]])", r"\1", keyed)
22 changes: 22 additions & 0 deletions internals/checks/src/checks/platforms_mirror/as_json_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Colocated unit tests for normalizing an object literal to JSON (isolation --
source text in, source text out).
"""

from checks.platforms_mirror.as_json import as_json


def describe_as_json():
def it_quotes_a_bare_key():
assert as_json('{os: "linux"}') == '{"os": "linux"}'

def it_quotes_every_key_of_an_object():
assert as_json('{os: "linux", cpu: "x64"}') == '{"os": "linux", "cpu": "x64"}'

def it_leaves_an_already_quoted_key_alone():
assert as_json('{"os": "linux"}') == '{"os": "linux"}'

def it_drops_a_trailing_comma_before_a_brace():
assert as_json('{"os": "linux",}') == '{"os": "linux"}'

def it_drops_a_trailing_comma_before_a_bracket():
assert as_json('["linux",]') == '["linux"]'
6 changes: 1 addition & 5 deletions internals/checks/src/checks/platforms_mirror/gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,10 @@
from .parse import ParseError
from .problems import problems
from .python_table import python_table
from .read import read
from .typescript_table import typescript_table


def read(path: str) -> str:
with open(path, encoding="utf-8") as handle:
return handle.read()


def run(
python_path: str,
typescript_path: str,
Expand Down
26 changes: 15 additions & 11 deletions internals/checks/src/checks/platforms_mirror/gate_test.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Colocated unit tests for the platforms-mirror gate (#1004).

Isolation: the reader is injected, so nothing here touches the repo's real
platforms.py / platforms.ts. The reader itself is exercised against a scratch
file. The table readers and the verdict in `problems.py` run for real, since
they are pure text-in / messages-out.
platforms.py / platforms.ts; `read.py` carries its own scratch-file test. The
table readers and the verdict in `problems.py` run for real, since they are
pure text-in / messages-out.
"""

from checks.platforms_mirror.gate import read, run
from checks.platforms_mirror.gate import run

PYTHON = '''\
from dataclasses import dataclass
Expand Down Expand Up @@ -55,13 +55,6 @@ def invoke(**kwargs):
return code, "\n".join(lines)


def describe_read():
def it_reads_a_file_as_utf8_text(tmp_path):
path = tmp_path / "platforms.ts"
path.write_text("// caffè\n", encoding="utf-8")
assert read(str(path)) == "// caffè\n"


def describe_run():
def it_reports_the_target_count_when_the_subset_agrees():
code, out = invoke()
Expand Down Expand Up @@ -95,6 +88,17 @@ def it_fails_when_the_python_table_cannot_be_read():
assert "::error::platforms-mirror could not read a platform table:" in out
assert "no `class Platform`" in out

def it_reads_both_paths_through_the_injected_reader():
seen = []
text = sources()

def source(path):
seen.append(path)
return text(path)

run("p.py", "t.ts", source=source, echo=lambda line: None)
assert seen == ["p.py", "t.ts"]

def it_fails_when_the_typescript_table_cannot_be_read():
code, out = invoke(typescript="export const OTHER = [];\n")
assert code == 1
Expand Down
8 changes: 8 additions & 0 deletions internals/checks/src/checks/platforms_mirror/read.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""A platform table's text, off disk."""

from __future__ import annotations


def read(path: str) -> str:
with open(path, encoding="utf-8") as handle:
return handle.read()
17 changes: 17 additions & 0 deletions internals/checks/src/checks/platforms_mirror/read_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Colocated unit test for reading a platform table off disk (isolation -- a
scratch file, never the repo's real platforms.py / platforms.ts).
"""

from checks.platforms_mirror.read import read


def describe_read():
def it_reads_a_file_as_utf8_text(tmp_path):
path = tmp_path / "platforms.ts"
path.write_text("// caffè\n", encoding="utf-8")
assert read(str(path)) == "// caffè\n"

def it_reads_the_whole_file(tmp_path):
path = tmp_path / "platforms.py"
path.write_text("one\ntwo\n", encoding="utf-8")
assert read(str(path)) == "one\ntwo\n"
16 changes: 16 additions & 0 deletions internals/checks/src/checks/platforms_mirror/requoted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""A TypeScript string literal as a JSON one.

Single-quoted and template forms are unquoted by hand and re-encoded, so the
result is something `json` will accept without the escapes changing meaning.
"""

from __future__ import annotations

import json


def requoted(text: str) -> str:
"""A TypeScript string literal as a JSON one."""
if text.startswith('"'):
return text
return json.dumps(text[1:-1].replace('\\"', '"').replace("\\'", "'"))
25 changes: 25 additions & 0 deletions internals/checks/src/checks/platforms_mirror/requoted_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Colocated unit tests for re-quoting a TypeScript string literal (isolation --
one literal in, one literal out).
"""

from checks.platforms_mirror.requoted import requoted


def describe_requoted():
def it_keeps_a_double_quoted_literal_verbatim():
assert requoted('"linux"') == '"linux"'

def it_rewrites_a_single_quoted_literal():
assert requoted("'linux'") == '"linux"'

def it_rewrites_a_template_literal():
assert requoted("`linux`") == '"linux"'

def it_unwraps_only_the_outermost_quotes():
assert requoted("'aba'") == '"aba"'

def it_unescapes_a_quote_the_typescript_form_had_to_escape():
assert requoted(r"'a\'b'") == '"a\'b"'

def it_keeps_an_escaped_double_quote_escaped():
assert requoted(r"'a\"b'") == r'"a\"b"'
34 changes: 4 additions & 30 deletions internals/checks/src/checks/platforms_mirror/typescript_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,21 @@
import json
import re

from .as_json import as_json
from .parse import ParseError, TABLE_NAME

# One alternation of "things a naive scan would cut in half": the three string
# forms first, so a `//` inside a string is matched as string rather than as the
# comment that follows it.
_TOKENS = re.compile(
r'"(?:\\.|[^"\\])*"' r"|'(?:\\.|[^'\\])*'" r"|`(?:\\.|[^`\\])*`" r"|//[^\n]*" r"|/\*.*?\*/",
re.S,
)


def _requoted(text: str) -> str:
"""A TypeScript string literal as a JSON one."""
if text.startswith('"'):
return text
return json.dumps(text[1:-1].replace('\\"', '"').replace("\\'", "'"))


def _without_comments(source: str) -> str:
def keep(match: re.Match) -> str:
text = match.group(0)
return "" if text.startswith(("//", "/*")) else _requoted(text)

return _TOKENS.sub(keep, source)


def _as_json(text: str) -> str:
keyed = re.sub(r"([{,]\s*)([A-Za-z_$][\w$]*)\s*:", r'\1"\2":', text)
return re.sub(r",(\s*[}\]])", r"\1", keyed)
from .without_comments import without_comments


def typescript_table(source: str) -> list[dict]:
"""Rows of the `PLATFORMS` array in a `platforms.ts`-shaped module."""
cleaned = _without_comments(source)
cleaned = without_comments(source)
marker = re.search(rf"\b{TABLE_NAME}\b[^=;]*=\s*(\[)", cleaned)
if marker is None:
raise ParseError(f"no `{TABLE_NAME} = [...]` assignment in the TypeScript source.")
try:
# `raw_decode` stops at the end of the array, so nothing here has to find
# the closing bracket or care what follows it.
rows, _ = json.JSONDecoder().raw_decode(_as_json(cleaned[marker.start(1) :]))
rows, _ = json.JSONDecoder().raw_decode(as_json(cleaned[marker.start(1) :]))
except json.JSONDecodeError as error:
raise ParseError(
f"`{TABLE_NAME}` is not a plain array of object literals ({error}); this check "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,13 @@ def it_reads_the_array_through_comments_and_trailing_commas():
def it_normalizes_single_quoted_strings():
assert typescript_table("const PLATFORMS = [{ os: ['linux'] }];") == [{"os": ["linux"]}]

def it_unwraps_a_single_quoted_string_without_trimming_its_contents():
assert typescript_table("const PLATFORMS = [{ os: ['aba'] }];") == [{"os": ["aba"]}]

def it_unwraps_a_template_literal():
assert typescript_table("const PLATFORMS = [{ os: [`aba`] }];") == [{"os": ["aba"]}]

def it_keeps_a_double_quoted_string_verbatim():
assert typescript_table('const PLATFORMS = [{ os: ["aba"] }];') == [{"os": ["aba"]}]

def it_finds_the_array_when_a_comment_mentions_a_bracket():
source = "// see PLATFORMS = [ elsewhere\nconst PLATFORMS = [{ os: ['a'] }];"
assert typescript_table(source) == [{"os": ["a"]}]

def it_keeps_an_escape_sequence_inside_a_string():
assert typescript_table(r'const PLATFORMS = [{ os: "a\"b" }];') == [{"os": 'a"b'}]
def it_reads_the_first_platforms_binding_it_finds():
source = "const OTHER = [];\nexport const PLATFORMS = [{ os: ['a'] }];"
assert typescript_table(source) == [{"os": ["a"]}]

def it_rejects_a_source_with_no_table():
with pytest.raises(ParseError, match="no `PLATFORMS"):
Expand Down
27 changes: 27 additions & 0 deletions internals/checks/src/checks/platforms_mirror/without_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""A TypeScript source with its comments gone and its strings JSON-quoted.

Both happen in one pass because they cannot be separated: a `//` inside a
string is not a comment, and a quote inside a comment is not a string.
"""

from __future__ import annotations

import re

from .requoted import requoted

# One alternation of "things a naive scan would cut in half": the three string
# forms first, so a `//` inside a string is matched as string rather than as the
# comment that follows it.
_TOKENS = re.compile(
r'"(?:\\.|[^"\\])*"' r"|'(?:\\.|[^'\\])*'" r"|`(?:\\.|[^`\\])*`" r"|//[^\n]*" r"|/\*.*?\*/",
re.S,
)


def without_comments(source: str) -> str:
def keep(match: re.Match) -> str:
text = match.group(0)
return "" if text.startswith(("//", "/*")) else requoted(text)

return _TOKENS.sub(keep, source)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Colocated unit tests for stripping comments and normalizing strings
(isolation -- source text in, source text out).
"""

from checks.platforms_mirror.without_comments import without_comments


def describe_without_comments():
def it_drops_a_line_comment():
assert without_comments("a // gone\nb") == "a \nb"

def it_drops_a_block_comment_across_lines():
assert without_comments("a /* gone\ngone */ b") == "a b"

def it_keeps_a_comment_marker_that_sits_inside_a_string():
assert without_comments("['a // b']") == '["a // b"]'

def it_normalizes_the_strings_it_keeps():
assert without_comments("['linux']") == '["linux"]'

def it_drops_a_line_comment_that_runs_to_the_end_of_the_source():
assert without_comments("a // gone") == "a "

def it_leaves_a_source_with_neither_alone():
assert without_comments("[1, 2]") == "[1, 2]"
Loading