| """Generate the confusables.rs file from the VS Code ambiguous.json file.""" |
| |
| from __future__ import annotations |
| |
| import json |
| import subprocess |
| from pathlib import Path |
| |
| CONFUSABLES_RS_PATH = "crates/ruff_linter/src/rules/ruff/rules/confusables.rs" |
| AMBIGUOUS_JSON_URL = "https://raw.githubusercontent.com/hediet/vscode-unicode-data/main/out/ambiguous.json" |
| |
| prelude = """ |
| //! This file is auto-generated by `scripts/update_ambiguous_characters.py`. |
| |
| /// Via: <https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json> |
| /// See: <https://github.com/microsoft/vscode/blob/095ddabc52b82498ee7f718a34f9dd11d59099a8/src/vs/base/common/strings.ts#L1094> |
| pub(crate) fn confusable(c: u32) -> Option<char> { |
| let result = match c { |
| |
| """.lstrip() |
| |
| postlude = """_ => return None, }; Some(result)}""" |
| |
| |
| def get_mapping_data() -> dict: |
| """ |
| Get the ambiguous character mapping data from the vscode-unicode-data repository. |
| |
| Uses the system's `curl` command to download the data, |
| instead of adding a dependency to a Python-native HTTP client. |
| """ |
| content = subprocess.check_output( |
| ["curl", "-sSL", AMBIGUOUS_JSON_URL], |
| encoding="utf-8", |
| ) |
| # The content is a JSON object literal wrapped in a JSON string, so double decode: |
| return json.loads(json.loads(content)) |
| |
| |
| def format_number(number: int) -> str: |
| """Underscore-separate the digits of a number.""" |
| # For unknown historical reasons, numbers greater than 100,000 were |
| # underscore-delimited in the generated file, so we now preserve that property to |
| # avoid unnecessary churn. |
| if number > 100000: |
| number: str = str(number) |
| number = "_".join(number[i : i + 3] for i in range(0, len(number), 3)) |
| return f"{number}_u32" |
| |
| return f"{number}u32" |
| |
| |
| def format_char(number: int) -> str: |
| """Format a Python integer as a Rust character literal.""" |
| char = chr(number) |
| if char == "\\": |
| return "\\\\" |
| return char |
| |
| |
| def format_confusables_rs(raw_data: dict[str, list[int]]) -> str: |
| """Format the downloaded data into a Rust source file.""" |
| # The input data contains duplicate entries. |
| flattened_items: set[tuple[int, int]] = set() |
| for _category, items in raw_data.items(): |
| assert len(items) % 2 == 0, "Expected pairs of items" |
| for i in range(0, len(items), 2): |
| flattened_items.add((items[i], items[i + 1])) |
| |
| tuples = [ |
| f" {format_number(left)} => '{format_char(right)}',\n" |
| for left, right in sorted(flattened_items) |
| ] |
| |
| # Add some additional confusable pairs that are not included in the VS Code data, |
| # as they're unicode-to-unicode confusables, not unicode-to-ASCII confusables. |
| confusable_units = [ |
| # ANGSTROM SIGN → LATIN CAPITAL LETTER A WITH RING ABOVE |
| ("0x212B", chr(0x00C5)), |
| # OHM SIGN → GREEK CAPITAL LETTER OMEGA |
| ("0x2126", chr(0x03A9)), |
| # MICRO SIGN → GREEK SMALL LETTER MU |
| ("0x00B5", chr(0x03BC)), |
| ] |
| tuples += [f" {left} => '{right}',\n" for left, right in confusable_units] |
| |
| print(f"{len(tuples)} confusable tuples.") |
| |
| return prelude + "".join(tuples) + postlude |
| |
| |
| def main() -> None: |
| print("Retrieving data...") |
| mapping_data = get_mapping_data() |
| formatted_data = format_confusables_rs(mapping_data) |
| confusables_path = Path(__file__).parent.parent / CONFUSABLES_RS_PATH |
| confusables_path.write_text(formatted_data, encoding="utf-8") |
| print("Formatting Rust file with cargo fmt...") |
| subprocess.check_call(["cargo", "fmt", "--", confusables_path]) |
| print("Done.") |
| |
| |
| if __name__ == "__main__": |
| main() |