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,271 @@
|
|
|
1
|
+
"""Helpers shared by the BES hooks in this package.
|
|
2
|
+
|
|
3
|
+
Each of these was once copied into several hooks and kept identical by hand.
|
|
4
|
+
They live here so a fix to one is a fix to all: reading a file as LF text while
|
|
5
|
+
remembering its line endings, writing it back the same way, finding the BES
|
|
6
|
+
files under a folder when no paths are given, and printing the
|
|
7
|
+
`path:line: [CODE] message` report the hooks share.
|
|
8
|
+
|
|
9
|
+
SchemaGuard also lives here. It is what keeps an auto-fix from turning a
|
|
10
|
+
schema-valid file into one that fails BES.xsd validation.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import array
|
|
14
|
+
import bisect
|
|
15
|
+
import functools
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
# an unrendered mustache template ({{ placeholder }}) is not real content yet.
|
|
21
|
+
# Only an identifier-like placeholder counts: `{{` is also the ActionScript
|
|
22
|
+
# escape for a literal `{`, so heredoc payloads (YARA, JSON, C#) contain `{{`
|
|
23
|
+
# around arbitrary content and must not be mistaken for a template.
|
|
24
|
+
# A GUID-shaped "placeholder" is not one: `msiexec /x{{{GUID}}` escapes a
|
|
25
|
+
# literal `{` in front of an MSI product code.
|
|
26
|
+
MUSTACHE_RE = re.compile(
|
|
27
|
+
r"\{\{(?!\s*[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}\s*\}\})"
|
|
28
|
+
r"\s*[#/^!&>]?\s*[\w.-]+\s*\}\}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# directories never searched by discover_bes_files (hidden ones are skipped too)
|
|
32
|
+
DISCOVERY_SKIP_DIRS = frozenset({"__pycache__", "node_modules"})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def read_source(path):
|
|
36
|
+
"""Read `path`; return (raw bytes, text with LF line endings, was_crlf).
|
|
37
|
+
|
|
38
|
+
The text is decoded as UTF-8 (undecodable bytes replaced) and every CRLF or
|
|
39
|
+
lone CR is turned into LF, so checks need not care about line endings.
|
|
40
|
+
`was_crlf` records whether the file had any CRLF, for encode() to restore.
|
|
41
|
+
"""
|
|
42
|
+
with open(path, "rb") as handle:
|
|
43
|
+
raw = handle.read()
|
|
44
|
+
src = (
|
|
45
|
+
raw.decode("utf-8", errors="replace").replace("\r\n", "\n").replace("\r", "\n")
|
|
46
|
+
)
|
|
47
|
+
return raw, src, b"\r\n" in raw
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def encode(src, was_crlf):
|
|
51
|
+
"""Turn checked text back into file bytes, restoring CRLF if that is the file."""
|
|
52
|
+
return (src.replace("\n", "\r\n") if was_crlf else src).encode("utf-8")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@functools.lru_cache(maxsize=4)
|
|
56
|
+
def _newline_offsets(src):
|
|
57
|
+
"""The offset of every LF in `src`, in order (cached per text).
|
|
58
|
+
|
|
59
|
+
An array of 8-byte ints rather than a list of int objects, so a text with
|
|
60
|
+
many short lines does not cost several times its own size to index.
|
|
61
|
+
"""
|
|
62
|
+
return array.array("q", (match.start() for match in re.finditer("\n", src)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def lineno(src, pos):
|
|
66
|
+
"""Return the 1-based line number of character offset `pos` in `src`.
|
|
67
|
+
|
|
68
|
+
Hooks call this once per match, so counting LFs from the start each time
|
|
69
|
+
is quadratic on a large file (a 12 MB file spent most of its time here).
|
|
70
|
+
Instead the LF offsets are found once per text and bisected; a text
|
|
71
|
+
changed by an auto-fix is a new string and gets its own offsets. `pos` is
|
|
72
|
+
meant to be a match offset; one below 0 gives line 1 and one past the end
|
|
73
|
+
the last line.
|
|
74
|
+
"""
|
|
75
|
+
return bisect.bisect_left(_newline_offsets(src), pos) + 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def discover_bes_files(root=".", extensions=(".bes",)):
|
|
79
|
+
"""Return the files under `root` ending in `extensions`, sorted.
|
|
80
|
+
|
|
81
|
+
Hidden directories and DISCOVERY_SKIP_DIRS are pruned from the walk.
|
|
82
|
+
"""
|
|
83
|
+
root = os.path.normpath(root)
|
|
84
|
+
found = []
|
|
85
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
86
|
+
dirnames[:] = [
|
|
87
|
+
d
|
|
88
|
+
for d in dirnames
|
|
89
|
+
if not d.startswith(".") and d not in DISCOVERY_SKIP_DIRS
|
|
90
|
+
]
|
|
91
|
+
for name in filenames:
|
|
92
|
+
if name.endswith(extensions):
|
|
93
|
+
found.append(os.path.join(dirpath, name))
|
|
94
|
+
return sorted(found)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def report(results, errors_only=False):
|
|
98
|
+
"""Print every fix and issue in `results`; return (issues, warnings, fixes).
|
|
99
|
+
|
|
100
|
+
`results` is a list of (path, issues, fixed) tuples, each of `issues` and
|
|
101
|
+
`fixed` a list of (lineno, code, message). A code starting with W is a
|
|
102
|
+
warning; any other is an error. With `errors_only`, warnings are neither
|
|
103
|
+
printed nor counted.
|
|
104
|
+
"""
|
|
105
|
+
issue_count = 0
|
|
106
|
+
warning_count = 0
|
|
107
|
+
fix_count = 0
|
|
108
|
+
for path, issues, fixed in results:
|
|
109
|
+
for line, check_id, message in fixed:
|
|
110
|
+
fix_count += 1
|
|
111
|
+
print(f"{path}:{line}: [{check_id}] auto-fixed: {message}")
|
|
112
|
+
for line, check_id, message in issues:
|
|
113
|
+
if check_id.startswith("W"):
|
|
114
|
+
if errors_only:
|
|
115
|
+
continue
|
|
116
|
+
warning_count += 1
|
|
117
|
+
print(f"{path}:{line}: [{check_id}] warning: {message}")
|
|
118
|
+
else:
|
|
119
|
+
issue_count += 1
|
|
120
|
+
print(f"{path}:{line}: [{check_id}] {message}")
|
|
121
|
+
return issue_count, warning_count, fix_count
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _bundled_schemas(validate_bes_xml):
|
|
125
|
+
"""Return the schema files validate_bes_xml ships with, not cwd ones.
|
|
126
|
+
|
|
127
|
+
Upstream's SCHEMA_FILES also holds any *.xsd in the current folder, which
|
|
128
|
+
would let a repo-local Foo.xsd make <Foo> checkable, or a repo-local
|
|
129
|
+
BES.xsd stand in for the real one.
|
|
130
|
+
"""
|
|
131
|
+
bundled = os.path.join(
|
|
132
|
+
os.path.dirname(os.path.realpath(validate_bes_xml.__file__)), "schemas"
|
|
133
|
+
)
|
|
134
|
+
return [
|
|
135
|
+
path
|
|
136
|
+
for path in validate_bes_xml.SCHEMA_FILES
|
|
137
|
+
if os.path.dirname(os.path.realpath(path)) == bundled
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_schema_check_warned = [] # non-empty once the warning below has been printed
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _warn_schema_check_unavailable():
|
|
145
|
+
"""Say, once per run, that auto-fixes are written without a schema check.
|
|
146
|
+
|
|
147
|
+
It goes to stderr so the `path:line: [CODE]` report on stdout stays clean.
|
|
148
|
+
"""
|
|
149
|
+
if _schema_check_warned:
|
|
150
|
+
return
|
|
151
|
+
_schema_check_warned.append(True)
|
|
152
|
+
print(
|
|
153
|
+
"warning: BES.xsd schema check unavailable, so auto-fixes are written "
|
|
154
|
+
'unchecked; install "validate_bes_xml>=2.4.1" to enable it',
|
|
155
|
+
file=sys.stderr,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def schema_errors(raw, path=None):
|
|
160
|
+
"""Validate BES XML bytes against the schema bes-schema-validate would pick.
|
|
161
|
+
|
|
162
|
+
`path`, if given, is the file's name; validate_bes_xml uses it only for
|
|
163
|
+
its extension (.ojo -> BESOJO.xsd, .BESDomain -> BESDomain.xsd). Returns a
|
|
164
|
+
list of "Line N: message" strings, empty when valid. Returns None when no
|
|
165
|
+
bundled schema applies, meaning validity is unknown. validate_bes_xml
|
|
166
|
+
(and so lxml) is imported only here: importing it scans and compiles every
|
|
167
|
+
schema, which only a fix that writes needs. When it is not installed (the
|
|
168
|
+
stdlib-only bes-conventions-check run as a bare script, say), validity is
|
|
169
|
+
unknown too: a warning says so once, and the fix is written unchecked.
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
import validate_bes_xml # pylint: disable=import-outside-toplevel
|
|
173
|
+
except ImportError:
|
|
174
|
+
_warn_schema_check_unavailable()
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
schemas = _bundled_schemas(validate_bes_xml)
|
|
178
|
+
if not schemas: # an empty list would make upstream fall back to the cwd
|
|
179
|
+
return None
|
|
180
|
+
result = validate_bes_xml.validate_bes(
|
|
181
|
+
xml=raw, filename=path, schema_pathnames=schemas
|
|
182
|
+
)
|
|
183
|
+
if result:
|
|
184
|
+
return []
|
|
185
|
+
# compare against the value: every Status member is a non-empty str, so
|
|
186
|
+
# testing `result.status` for truth would always pass.
|
|
187
|
+
if result.status == "no_schema":
|
|
188
|
+
return None
|
|
189
|
+
# an error with no line number (an unreadable file, say) is just its message
|
|
190
|
+
return [
|
|
191
|
+
message if line is None else f"Line {line}: {message}"
|
|
192
|
+
for line, message in result.errors
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def schema_regression(original, new, path=None):
|
|
197
|
+
"""Return the schema errors an edit introduced, or [] if it introduced none.
|
|
198
|
+
|
|
199
|
+
`original` and `new` are a file's bytes before and after an auto-fix. Only
|
|
200
|
+
a valid-to-invalid change counts: a file that already failed validation
|
|
201
|
+
is bes-schema-validate's to report, and blocking every fix to it would
|
|
202
|
+
help nobody. An unknown schema (see schema_errors) also counts as none.
|
|
203
|
+
"""
|
|
204
|
+
if new == original:
|
|
205
|
+
return []
|
|
206
|
+
errors = schema_errors(new, path)
|
|
207
|
+
if not errors:
|
|
208
|
+
return []
|
|
209
|
+
return errors if schema_errors(original, path) == [] else []
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class SchemaGuard:
|
|
213
|
+
"""Accept a file's auto-fix passes one at a time, keeping it BES.xsd-valid.
|
|
214
|
+
|
|
215
|
+
Start it with the file's text, then hand each fix pass's result to
|
|
216
|
+
apply(). A pass that would turn a schema-valid file invalid is held back:
|
|
217
|
+
its fixes are dropped and one issue is recorded under `code` naming them.
|
|
218
|
+
The passes before and after it still apply, so one bad fix does not cost
|
|
219
|
+
the others. A file that already fails validation is never held back,
|
|
220
|
+
since bes-schema-validate reports it regardless -- but once a pass makes
|
|
221
|
+
it valid, the passes after that are checked like any other. Line endings
|
|
222
|
+
do not affect validity, so the text is checked in whatever form it is
|
|
223
|
+
given.
|
|
224
|
+
|
|
225
|
+
Afterwards `src` is the accepted text, `fixed` the accepted fixes, and
|
|
226
|
+
`refused` the issues to report.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(self, src, code, path=None, validate=True):
|
|
230
|
+
self.src = src
|
|
231
|
+
self.fixed = []
|
|
232
|
+
self.refused = []
|
|
233
|
+
self._code = code
|
|
234
|
+
self._path = path
|
|
235
|
+
self._validate = validate
|
|
236
|
+
self._valid = None # is the accepted text schema-valid? (checked lazily)
|
|
237
|
+
|
|
238
|
+
def _errors(self, new_src):
|
|
239
|
+
"""Schema errors `new_src` would introduce over the accepted text."""
|
|
240
|
+
if self._valid is None:
|
|
241
|
+
self._valid = schema_errors(self.src.encode("utf-8"), self._path) == []
|
|
242
|
+
if not self._valid:
|
|
243
|
+
return []
|
|
244
|
+
return schema_errors(new_src.encode("utf-8"), self._path) or []
|
|
245
|
+
|
|
246
|
+
def apply(self, new_src, fixes):
|
|
247
|
+
"""Take one pass's rewritten text and its fixes; return True if kept."""
|
|
248
|
+
if self._validate and new_src != self.src:
|
|
249
|
+
errors = self._errors(new_src)
|
|
250
|
+
if errors:
|
|
251
|
+
codes = ", ".join(sorted({code for _line, code, _msg in fixes}))
|
|
252
|
+
self.refused.append(
|
|
253
|
+
(
|
|
254
|
+
1,
|
|
255
|
+
self._code,
|
|
256
|
+
(
|
|
257
|
+
f"auto-fix ({codes or 'unreported'}) not written: it "
|
|
258
|
+
"would make the file fail BES.xsd validation "
|
|
259
|
+
f"({errors[0]}); fix by hand, or --disable "
|
|
260
|
+
f"{self._code} to write it anyway"
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
return False
|
|
265
|
+
if self._valid is False and new_src != self.src:
|
|
266
|
+
# an invalid (or unknown) file may have just been made valid:
|
|
267
|
+
# re-check before the next pass rather than wave it through
|
|
268
|
+
self._valid = None
|
|
269
|
+
self.src = new_src
|
|
270
|
+
self.fixed += fixes
|
|
271
|
+
return True
|