pre-commit-bigfix 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pre_commit_bigfix/__init__.py +3 -0
- pre_commit_bigfix/bes_actionscript_lint_schclass.py +937 -0
- pre_commit_bigfix/bes_actionscript_validate_prefetch.py +1258 -0
- pre_commit_bigfix/bes_actionscript_validate_script.py +2871 -0
- pre_commit_bigfix/bes_common.py +271 -0
- pre_commit_bigfix/bes_conventions_check.py +3484 -0
- pre_commit_bigfix/bes_relevance_convert_group.py +407 -0
- pre_commit_bigfix/bes_relevance_lint.py +402 -0
- pre_commit_bigfix/bes_schema_validate.py +84 -0
- pre_commit_bigfix/bes_siterepo_validate_layout.py +1203 -0
- pre_commit_bigfix/run_all.py +305 -0
- pre_commit_bigfix/schclass.py +284 -0
- pre_commit_bigfix/schclass_data/ExpandedActionScript.schclass +412 -0
- pre_commit_bigfix/schclass_data/bigfix_overrides.schclass +43 -0
- pre_commit_bigfix/schclass_tokenizer.py +321 -0
- pre_commit_bigfix-2.0.1.dist-info/METADATA +821 -0
- pre_commit_bigfix-2.0.1.dist-info/RECORD +21 -0
- pre_commit_bigfix-2.0.1.dist-info/WHEEL +5 -0
- pre_commit_bigfix-2.0.1.dist-info/entry_points.txt +10 -0
- pre_commit_bigfix-2.0.1.dist-info/licenses/LICENSE +21 -0
- pre_commit_bigfix-2.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tokenizer engine driven by a .schclass Schema (see schclass.py).
|
|
3
|
+
|
|
4
|
+
Walks input text exactly the way the console's SyntaxEdit control does:
|
|
5
|
+
|
|
6
|
+
- `start:Tag` literals enter a state (comment, string, {...} relevance);
|
|
7
|
+
inside it, `skip:Tag` escapes are consumed without ending the state
|
|
8
|
+
(this is how `\\"` stays inside a string and a backslash-newline carries
|
|
9
|
+
a state across lines), `end:Tag` literals end it (consumed), `@eol`
|
|
10
|
+
ends it at the line break, and `end:separators` end it WITHOUT
|
|
11
|
+
consuming the separator.
|
|
12
|
+
- `token:tag` keyword literals match longest-first across every class
|
|
13
|
+
visible in the current container (so `add nohash prefetch item` beats
|
|
14
|
+
`add prefetch item` beats `prefetch`), bounded by the class's
|
|
15
|
+
`token:start:separators` / `token:end:separators` (default: whitespace
|
|
16
|
+
and line boundaries), and gated by `previous:tag` context (`@eol` =
|
|
17
|
+
line start, `@specs` = a non-alphanumeric character).
|
|
18
|
+
- `children` classes may begin inside a state (a url inside {...}); the
|
|
19
|
+
containing state's token is emitted in segments around the child.
|
|
20
|
+
- Text no class claims is emitted as `default` tokens, one per contiguous
|
|
21
|
+
non-whitespace run. Default text is NEVER an engine error: a display
|
|
22
|
+
grammar is total, and judging default text is the consumer's job (the
|
|
23
|
+
ActionScript linter treats default text at line start as an unknown
|
|
24
|
+
command verb).
|
|
25
|
+
|
|
26
|
+
Errors are emitted only for states that reach end of input with no legitimate
|
|
27
|
+
exit (no end tag seen and no `@eol` fallback) -- an unterminated state. States
|
|
28
|
+
that may end at `@eol` close with end_kind 'eol'/'eof' instead, and consumers
|
|
29
|
+
decide whether that matters (an eol-terminated {...} substitution is a lint
|
|
30
|
+
error; an eol-terminated // comment is normal).
|
|
31
|
+
|
|
32
|
+
Options (both off by default to stay console-faithful):
|
|
33
|
+
case_insensitive match tags/keywords regardless of case (the BigFix
|
|
34
|
+
agent accepts `RUN`; the console colorizer does not)
|
|
35
|
+
relaxed_bol weaken `@eol` in previous:tag from "column 0" to "only
|
|
36
|
+
whitespace precedes on this line" (real ActionScript
|
|
37
|
+
allows indented commands)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import bisect
|
|
41
|
+
import dataclasses
|
|
42
|
+
|
|
43
|
+
from pre_commit_bigfix.schclass import AT_EOL, AT_SPECS
|
|
44
|
+
|
|
45
|
+
_DEFAULT_TOKEN_SEPARATORS = (" ", "\t")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclasses.dataclass
|
|
49
|
+
class Token:
|
|
50
|
+
"""One lexed span of the input."""
|
|
51
|
+
|
|
52
|
+
class_name: str # lexClass name, or 'default'
|
|
53
|
+
text: str # exact source slice
|
|
54
|
+
line: int # 1-based start position
|
|
55
|
+
col: int
|
|
56
|
+
end_line: int
|
|
57
|
+
end_col: int # 1-based, exclusive
|
|
58
|
+
end_kind: str # 'tag' | 'separator' | 'eol' | 'eof' | 'token' | 'child'
|
|
59
|
+
keyword: str = None # canonical token:tag literal for keyword tokens
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclasses.dataclass
|
|
63
|
+
class TokenizeError:
|
|
64
|
+
"""A state entered but never legitimately exited."""
|
|
65
|
+
|
|
66
|
+
line: int
|
|
67
|
+
col: int
|
|
68
|
+
class_name: str
|
|
69
|
+
kind: str # 'unterminated'
|
|
70
|
+
message: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclasses.dataclass
|
|
74
|
+
class _Candidate:
|
|
75
|
+
"""A literal that can begin a token or state within some container."""
|
|
76
|
+
|
|
77
|
+
literal: str # canonical literal from the schema
|
|
78
|
+
match: str # what to compare against the text (lowered if insensitive)
|
|
79
|
+
kind: str # 'start' (enters a state) | 'token' (keyword)
|
|
80
|
+
cls: object # the owning LexClass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Tokenizer:
|
|
84
|
+
"""Tokenizes text according to a loaded Schema."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, schema, case_insensitive=False, relaxed_bol=False):
|
|
87
|
+
self.schema = schema
|
|
88
|
+
self.case_insensitive = case_insensitive
|
|
89
|
+
self.relaxed_bol = relaxed_bol
|
|
90
|
+
root = schema.root()
|
|
91
|
+
self._root_name = root.name if root is not None else None
|
|
92
|
+
# candidate lists per container: the root sees every class parented to
|
|
93
|
+
# it; a state with `children` sees the named child classes.
|
|
94
|
+
self._candidates = {}
|
|
95
|
+
if root is not None:
|
|
96
|
+
self._candidates[root.name] = self._build_candidates(
|
|
97
|
+
cls for cls in schema.classes.values() if root.name in cls.parents
|
|
98
|
+
)
|
|
99
|
+
for cls in schema.classes.values():
|
|
100
|
+
if cls.children:
|
|
101
|
+
self._candidates[cls.name] = self._build_candidates(
|
|
102
|
+
schema.classes[name]
|
|
103
|
+
for name in cls.children
|
|
104
|
+
if name in schema.classes
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _fold(self, literal):
|
|
108
|
+
return literal.lower() if self.case_insensitive else literal
|
|
109
|
+
|
|
110
|
+
def _build_candidates(self, classes):
|
|
111
|
+
"""Return per-first-char candidate index, longest literal first."""
|
|
112
|
+
flat = []
|
|
113
|
+
for cls in classes:
|
|
114
|
+
for literal in cls.start_tags:
|
|
115
|
+
flat.append(_Candidate(literal, self._fold(literal), "start", cls))
|
|
116
|
+
for literal in cls.token_tags:
|
|
117
|
+
flat.append(_Candidate(literal, self._fold(literal), "token", cls))
|
|
118
|
+
flat.sort(key=lambda cand: -len(cand.literal)) # stable: schema order ties
|
|
119
|
+
index = {}
|
|
120
|
+
for cand in flat:
|
|
121
|
+
if cand.match:
|
|
122
|
+
index.setdefault(cand.match[0], []).append(cand)
|
|
123
|
+
return index
|
|
124
|
+
|
|
125
|
+
def tokenize(self, text):
|
|
126
|
+
"""Tokenize `text`; return (tokens, errors)."""
|
|
127
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
128
|
+
low = text.lower() if self.case_insensitive else text
|
|
129
|
+
length = len(text)
|
|
130
|
+
line_starts = [0]
|
|
131
|
+
for offset, char in enumerate(text):
|
|
132
|
+
if char == "\n":
|
|
133
|
+
line_starts.append(offset + 1)
|
|
134
|
+
|
|
135
|
+
tokens = []
|
|
136
|
+
errors = []
|
|
137
|
+
stack = [] # [{cls, start, seg_start}] innermost state last
|
|
138
|
+
state = {"pos": 0, "default_start": None}
|
|
139
|
+
|
|
140
|
+
def line_col(offset):
|
|
141
|
+
idx = bisect.bisect_right(line_starts, offset) - 1
|
|
142
|
+
return idx + 1, offset - line_starts[idx] + 1
|
|
143
|
+
|
|
144
|
+
def emit(class_name, start, end, end_kind, keyword=None):
|
|
145
|
+
line, col = line_col(start)
|
|
146
|
+
end_line, end_col = line_col(end)
|
|
147
|
+
tokens.append(
|
|
148
|
+
Token(
|
|
149
|
+
class_name,
|
|
150
|
+
text[start:end],
|
|
151
|
+
line,
|
|
152
|
+
col,
|
|
153
|
+
end_line,
|
|
154
|
+
end_col,
|
|
155
|
+
end_kind,
|
|
156
|
+
keyword,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def flush_default(end):
|
|
161
|
+
if state["default_start"] is not None and end > state["default_start"]:
|
|
162
|
+
emit("default", state["default_start"], end, "separator")
|
|
163
|
+
state["default_start"] = None
|
|
164
|
+
|
|
165
|
+
def match_literal(literals, pos):
|
|
166
|
+
"""Longest matching literal from an iterable, or None."""
|
|
167
|
+
best = None
|
|
168
|
+
for literal in literals:
|
|
169
|
+
if low.startswith(self._fold(literal), pos) and (
|
|
170
|
+
best is None or len(literal) > len(best)
|
|
171
|
+
):
|
|
172
|
+
best = literal
|
|
173
|
+
return best
|
|
174
|
+
|
|
175
|
+
def at_bol(pos):
|
|
176
|
+
line_start = line_starts[bisect.bisect_right(line_starts, pos) - 1]
|
|
177
|
+
if self.relaxed_bol:
|
|
178
|
+
return all(char in " \t" for char in text[line_start:pos])
|
|
179
|
+
return pos == line_start
|
|
180
|
+
|
|
181
|
+
def previous_ok(cls, pos):
|
|
182
|
+
if not cls.previous_tags:
|
|
183
|
+
return True
|
|
184
|
+
for tag in cls.previous_tags:
|
|
185
|
+
if tag == AT_EOL:
|
|
186
|
+
if at_bol(pos):
|
|
187
|
+
return True
|
|
188
|
+
elif tag == AT_SPECS:
|
|
189
|
+
prev = text[pos - 1] if pos > 0 else ""
|
|
190
|
+
if prev and not (prev.isalnum() or prev == "_"):
|
|
191
|
+
return True
|
|
192
|
+
elif pos >= len(tag) and text[pos - len(tag) : pos] == tag:
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def token_boundaries(cls):
|
|
197
|
+
start_seps, start_eol = cls.token_start_separators, cls.token_start_eol
|
|
198
|
+
if not start_seps and not start_eol:
|
|
199
|
+
start_seps, start_eol = _DEFAULT_TOKEN_SEPARATORS, True
|
|
200
|
+
end_seps, end_eol = cls.token_end_separators, cls.token_end_eol
|
|
201
|
+
if not end_seps and not end_eol:
|
|
202
|
+
end_seps, end_eol = _DEFAULT_TOKEN_SEPARATORS, True
|
|
203
|
+
return start_seps, start_eol, end_seps, end_eol
|
|
204
|
+
|
|
205
|
+
def candidate_at(container_name, pos):
|
|
206
|
+
index = self._candidates.get(container_name, {})
|
|
207
|
+
for cand in index.get(low[pos], ()):
|
|
208
|
+
if not low.startswith(cand.match, pos):
|
|
209
|
+
continue
|
|
210
|
+
if not previous_ok(cand.cls, pos):
|
|
211
|
+
continue
|
|
212
|
+
if cand.kind == "token":
|
|
213
|
+
start_seps, start_eol, end_seps, end_eol = token_boundaries(
|
|
214
|
+
cand.cls
|
|
215
|
+
)
|
|
216
|
+
if pos > 0:
|
|
217
|
+
prev = text[pos - 1]
|
|
218
|
+
if prev == "\n":
|
|
219
|
+
if not start_eol:
|
|
220
|
+
continue
|
|
221
|
+
elif prev not in start_seps:
|
|
222
|
+
continue
|
|
223
|
+
end = pos + len(cand.match)
|
|
224
|
+
if end < length:
|
|
225
|
+
nxt = text[end]
|
|
226
|
+
if nxt == "\n":
|
|
227
|
+
if not end_eol:
|
|
228
|
+
continue
|
|
229
|
+
elif nxt not in end_seps:
|
|
230
|
+
continue
|
|
231
|
+
return cand
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def take_candidate(cand, pos):
|
|
235
|
+
"""Emit a keyword token or push a new state; return the new pos."""
|
|
236
|
+
if cand.kind == "token":
|
|
237
|
+
end = pos + len(cand.match)
|
|
238
|
+
emit(cand.cls.name, pos, end, "token", keyword=cand.literal)
|
|
239
|
+
return end
|
|
240
|
+
stack.append({"cls": cand.cls, "start": pos, "seg_start": pos})
|
|
241
|
+
return pos + len(cand.match)
|
|
242
|
+
|
|
243
|
+
def pop_state(end, end_kind):
|
|
244
|
+
"""Emit the innermost state's pending segment and pop it."""
|
|
245
|
+
top = stack.pop()
|
|
246
|
+
if end > top["seg_start"]:
|
|
247
|
+
emit(top["cls"].name, top["seg_start"], end, end_kind)
|
|
248
|
+
if stack:
|
|
249
|
+
stack[-1]["seg_start"] = end
|
|
250
|
+
|
|
251
|
+
pos = 0
|
|
252
|
+
while pos < length:
|
|
253
|
+
if stack:
|
|
254
|
+
cls = stack[-1]["cls"]
|
|
255
|
+
skip = match_literal(cls.skip_tags, pos)
|
|
256
|
+
if skip is not None:
|
|
257
|
+
pos += len(skip)
|
|
258
|
+
continue
|
|
259
|
+
end_tag = match_literal(cls.end_tags, pos)
|
|
260
|
+
if end_tag is not None:
|
|
261
|
+
pos += len(end_tag)
|
|
262
|
+
pop_state(pos, "tag")
|
|
263
|
+
continue
|
|
264
|
+
if text[pos] == "\n":
|
|
265
|
+
if cls.end_at_eol or cls.end_separators_eol:
|
|
266
|
+
pop_state(pos, "eol") # newline left to the container
|
|
267
|
+
else:
|
|
268
|
+
pos += 1 # multi-line state: newline is state content
|
|
269
|
+
continue
|
|
270
|
+
separator = match_literal(cls.end_separators, pos)
|
|
271
|
+
if separator is not None:
|
|
272
|
+
pop_state(pos, "separator") # separator NOT consumed
|
|
273
|
+
continue
|
|
274
|
+
if cls.children:
|
|
275
|
+
cand = candidate_at(cls.name, pos)
|
|
276
|
+
if cand is not None:
|
|
277
|
+
if pos > stack[-1]["seg_start"]:
|
|
278
|
+
emit(cls.name, stack[-1]["seg_start"], pos, "child")
|
|
279
|
+
stack[-1]["seg_start"] = pos
|
|
280
|
+
new_pos = take_candidate(cand, pos)
|
|
281
|
+
if stack[-1]["cls"] is cls: # keyword child: resume after
|
|
282
|
+
stack[-1]["seg_start"] = new_pos
|
|
283
|
+
pos = new_pos
|
|
284
|
+
continue
|
|
285
|
+
pos += 1
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
char = text[pos]
|
|
289
|
+
if char == "\n" or char in " \t":
|
|
290
|
+
flush_default(pos)
|
|
291
|
+
pos += 1
|
|
292
|
+
continue
|
|
293
|
+
cand = candidate_at(self._root_name, pos)
|
|
294
|
+
if cand is not None:
|
|
295
|
+
flush_default(pos)
|
|
296
|
+
pos = take_candidate(cand, pos)
|
|
297
|
+
continue
|
|
298
|
+
if state["default_start"] is None:
|
|
299
|
+
state["default_start"] = pos
|
|
300
|
+
pos += 1
|
|
301
|
+
|
|
302
|
+
flush_default(length)
|
|
303
|
+
while stack:
|
|
304
|
+
cls = stack[-1]["cls"]
|
|
305
|
+
start = stack[-1]["start"]
|
|
306
|
+
pop_state(length, "eof")
|
|
307
|
+
if not (cls.end_at_eol or cls.end_separators_eol):
|
|
308
|
+
line, col = line_col(start)
|
|
309
|
+
errors.append(
|
|
310
|
+
TokenizeError(
|
|
311
|
+
line,
|
|
312
|
+
col,
|
|
313
|
+
cls.name,
|
|
314
|
+
"unterminated",
|
|
315
|
+
(
|
|
316
|
+
f"{cls.name} opened at {line}:{col} is never "
|
|
317
|
+
"terminated before end of input"
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
return tokens, errors
|