-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathreferences.py
More file actions
110 lines (91 loc) · 2.96 KB
/
references.py
File metadata and controls
110 lines (91 loc) · 2.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
from __future__ import annotations
import re
from pathlib import Path
from .ignore import is_ignored
from .models import Asset, Reference
SOURCE_EXTENSIONS = {
".swift",
".m",
".mm",
".h",
".hpp",
".cpp",
".c",
".storyboard",
".xib",
".plist",
".json",
".strings",
".stringsdict",
".md",
".txt",
}
STRING_LITERAL_PATTERNS = (
("quoted-string", re.compile(r'"([^"\\]*(?:\\.[^"\\]*)*)"')),
("single-quoted-string", re.compile(r"'([^'\\]*(?:\\.[^'\\]*)*)'")),
("storyboard-image", re.compile(r'\bimage="([^"]+)"')),
("storyboard-background-image", re.compile(r'\bbackgroundImage="([^"]+)"')),
("plist-string", re.compile(r"<string>([^<]+)</string>")),
)
def discover_references(
root: Path,
assets: list[Asset],
ignore_patterns: list[str],
) -> dict[str, list[Reference]]:
asset_names = {asset.name for asset in assets}
references: dict[str, list[Reference]] = {name: [] for name in asset_names}
for path in root.rglob("*"):
if is_ignored(path, root, ignore_patterns):
continue
if not should_scan_source(path):
continue
scan_file(path, asset_names, references)
return {name: refs for name, refs in references.items() if refs}
def should_scan_source(path: Path) -> bool:
return path.is_file() and path.suffix.lower() in SOURCE_EXTENSIONS
def scan_file(
path: Path,
asset_names: set[str],
references: dict[str, list[Reference]],
) -> None:
try:
lines = path.read_text(encoding="utf-8").splitlines()
except UnicodeDecodeError:
try:
lines = path.read_text(encoding="utf-16").splitlines()
except UnicodeDecodeError:
return
except OSError:
return
for line_number, line in enumerate(lines, start=1):
for pattern_name, pattern in STRING_LITERAL_PATTERNS:
for match in pattern.finditer(line):
value = unescape_string(match.group(1).strip())
register_reference(value, path, line_number, pattern_name, asset_names, references)
def register_reference(
value: str,
path: Path,
line_number: int,
pattern_name: str,
asset_names: set[str],
references: dict[str, list[Reference]],
) -> None:
candidates = reference_candidates(value)
for candidate in candidates:
if candidate in asset_names:
references[candidate].append(Reference(candidate, path, line_number, pattern_name))
def reference_candidates(value: str) -> set[str]:
if not value:
return set()
path = Path(value)
stem = path.stem if path.suffix else value
last_component = Path(value).name
last_stem = Path(last_component).stem if Path(last_component).suffix else last_component
return {value, stem, last_component, last_stem}
def unescape_string(value: str) -> str:
return (
value.replace(r"\"", '"')
.replace(r"\'", "'")
.replace(r"\\", "\\")
.strip()
)