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,407 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Convert a BES file's console-exported <GroupRelevance> into plain <Relevance>.
|
|
3
|
+
|
|
4
|
+
The BigFix console can save a Fixlet/Task/Baseline/Analysis/FixletStream's
|
|
5
|
+
applicability as a computer-group style <GroupRelevance>: a list of search
|
|
6
|
+
components, each carrying its own already-built <Relevance>, joined by
|
|
7
|
+
intersection (all must hold) or union (any may hold). BES.xsd allows either
|
|
8
|
+
that or a run of <Relevance> elements in the same place, and plain relevance is
|
|
9
|
+
what reads and diffs well in source control, so this hook rewrites
|
|
10
|
+
|
|
11
|
+
<GroupRelevance JoinByIntersection="true"> one <Relevance> per component
|
|
12
|
+
(sibling Relevance must all hold)
|
|
13
|
+
<GroupRelevance JoinByIntersection="false"> one <Relevance>(a) OR (b)</Relevance>,
|
|
14
|
+
or just <Relevance>a</Relevance>
|
|
15
|
+
when there is one component
|
|
16
|
+
|
|
17
|
+
A GroupRelevance is found with the XPath /BES/*/GroupRelevance, and parsed with
|
|
18
|
+
lxml; the rewrite itself is a text splice, so everything outside the replaced
|
|
19
|
+
block -- formatting, comments, CDATA, line endings -- is left byte-identical.
|
|
20
|
+
Each component's <Relevance> is copied verbatim (CDATA stays CDATA, escaped
|
|
21
|
+
stays escaped) except when union-joining several, where the decoded texts are
|
|
22
|
+
joined and written once: as CDATA when any input was CDATA or the text holds
|
|
23
|
+
`<`, `>` or `&`, entity-escaped when it contains `]]>`, and plain otherwise.
|
|
24
|
+
|
|
25
|
+
Checks (E-codes fail the hook; W-codes are advisory unless --strict):
|
|
26
|
+
|
|
27
|
+
E700 a GroupRelevance was converted (or, under --check, needs converting)
|
|
28
|
+
E701 a GroupRelevance holds a SearchComponentGroupReference (or another
|
|
29
|
+
component with no Relevance); converting group membership is not
|
|
30
|
+
supported yet, so that whole GroupRelevance is left unchanged
|
|
31
|
+
E702 JoinByIntersection is not an xs:boolean, or is missing while there
|
|
32
|
+
are several components (with one, AND and OR are the same, so it
|
|
33
|
+
converts); left unchanged
|
|
34
|
+
E703 a GroupRelevance has no components, or one with an empty Relevance;
|
|
35
|
+
left unchanged
|
|
36
|
+
E704 the converted file would no longer pass BES.xsd schema validation;
|
|
37
|
+
nothing is written
|
|
38
|
+
W700 skipped: file not found, not parseable XML, or a GroupRelevance that
|
|
39
|
+
could not be located for the splice
|
|
40
|
+
|
|
41
|
+
The hook converts in place by default and exits 1 when it changed anything,
|
|
42
|
+
so pre-commit shows the rewrite for review. --check is a dry run: it only
|
|
43
|
+
reports. When no files are given, every *.bes under the current folder is
|
|
44
|
+
checked (never rewritten). A file carrying the text
|
|
45
|
+
|
|
46
|
+
pre-commit-skip: bes-relevance-convert-group
|
|
47
|
+
|
|
48
|
+
anywhere is skipped, as is an unrendered mustache template.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
import argparse
|
|
52
|
+
import os
|
|
53
|
+
import re
|
|
54
|
+
import sys
|
|
55
|
+
|
|
56
|
+
from lxml import etree
|
|
57
|
+
|
|
58
|
+
if __package__ in (None, ""): # run directly as a script, not as a module
|
|
59
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
60
|
+
|
|
61
|
+
from pre_commit_bigfix import bes_common
|
|
62
|
+
from pre_commit_bigfix.bes_common import MUSTACHE_RE, encode, lineno, read_source
|
|
63
|
+
|
|
64
|
+
SKIP_MARKER = "pre-commit-skip: bes-relevance-convert-group"
|
|
65
|
+
BES_EXTENSIONS = (".bes",)
|
|
66
|
+
KNOWN_CODES = frozenset({"E700", "E701", "E702", "E703", "E704", "W700"})
|
|
67
|
+
|
|
68
|
+
# where BES.xsd allows a GroupRelevance: directly in a content object
|
|
69
|
+
GROUP_RELEVANCE_XPATH = "/BES/*/GroupRelevance"
|
|
70
|
+
# search components whose <Relevance> child is the component's whole test
|
|
71
|
+
RELEVANCE_COMPONENTS = frozenset(
|
|
72
|
+
{"SearchComponentRelevance", "SearchComponentPropertyReference"}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# comments and CDATA are blanked out (same length) before the regexes below
|
|
76
|
+
# run, so tag-like text inside them cannot be mistaken for markup
|
|
77
|
+
OPAQUE_RE = re.compile(r"<!--.*?-->|<!\[CDATA\[.*?\]\]>", re.DOTALL)
|
|
78
|
+
GROUP_RELEVANCE_RE = re.compile(
|
|
79
|
+
r"<GroupRelevance\b[^>]*?(?:/>|>.*?</GroupRelevance>)", re.DOTALL
|
|
80
|
+
)
|
|
81
|
+
RELEVANCE_RE = re.compile(r"<Relevance\b[^>]*>(.*?)</Relevance>", re.DOTALL)
|
|
82
|
+
CDATA_START = "<![CDATA["
|
|
83
|
+
XS_BOOLEAN = {"true": True, "1": True, "false": False, "0": False}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _mask_opaque(src):
|
|
87
|
+
"""Return `src` with comment and CDATA text blanked, offsets unchanged."""
|
|
88
|
+
return OPAQUE_RE.sub(lambda m: re.sub(r"[^\n]", " ", m.group(0)), src)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _text(element):
|
|
92
|
+
"""The element's own text as the evaluator sees it (CDATA unwrapped)."""
|
|
93
|
+
return "".join(element.xpath("text()"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _escape(text):
|
|
97
|
+
"""Entity-escape `text` for an XML element body."""
|
|
98
|
+
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _component_relevance(component):
|
|
102
|
+
"""Return the relevance a search component stands for, or None.
|
|
103
|
+
|
|
104
|
+
None means the component has no relevance of its own to copy: a
|
|
105
|
+
SearchComponentGroupReference names a computer group, and turning
|
|
106
|
+
membership into relevance needs a template this hook does not have yet.
|
|
107
|
+
"""
|
|
108
|
+
if component.tag not in RELEVANCE_COMPONENTS:
|
|
109
|
+
return None
|
|
110
|
+
relevance = component.find("Relevance")
|
|
111
|
+
return "" if relevance is None else _text(relevance)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _components(group):
|
|
115
|
+
"""The search-component elements of `group` (comments left out)."""
|
|
116
|
+
return [child for child in group if isinstance(child.tag, str)]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _intersection(group):
|
|
120
|
+
"""Return True/False for how `group` joins its components, or None.
|
|
121
|
+
|
|
122
|
+
None means it cannot be told. BES.xsd makes JoinByIntersection optional
|
|
123
|
+
and documents no default, so a missing attribute is only safe with a
|
|
124
|
+
single component, where AND and OR mean the same thing.
|
|
125
|
+
"""
|
|
126
|
+
join = group.get("JoinByIntersection")
|
|
127
|
+
if join is None:
|
|
128
|
+
return True if len(_components(group)) == 1 else None
|
|
129
|
+
return XS_BOOLEAN.get(join.strip())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _refusal(group):
|
|
133
|
+
"""Return (code, message) if `group` cannot be converted, else None."""
|
|
134
|
+
components = _components(group)
|
|
135
|
+
for component in components:
|
|
136
|
+
if _component_relevance(component) is None:
|
|
137
|
+
if component.tag == "SearchComponentGroupReference":
|
|
138
|
+
what = (
|
|
139
|
+
f'a SearchComponentGroupReference (GroupName "'
|
|
140
|
+
f'{component.get("GroupName", "")}")'
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
what = f"a <{component.tag}> component"
|
|
144
|
+
return (
|
|
145
|
+
"E701",
|
|
146
|
+
(
|
|
147
|
+
f"GroupRelevance holds {what}; converting group membership is "
|
|
148
|
+
"not supported yet, so it was left unchanged"
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
# before the join check: with nothing to join, how they join is moot
|
|
152
|
+
if not components:
|
|
153
|
+
return "E703", "GroupRelevance has no search components; left unchanged"
|
|
154
|
+
if _intersection(group) is None:
|
|
155
|
+
found = group.get("JoinByIntersection")
|
|
156
|
+
detail = (
|
|
157
|
+
f"missing and it has {len(components)} components"
|
|
158
|
+
if found is None
|
|
159
|
+
else f'"{found}"'
|
|
160
|
+
)
|
|
161
|
+
return (
|
|
162
|
+
"E702",
|
|
163
|
+
(
|
|
164
|
+
f"GroupRelevance JoinByIntersection is {detail}, so how they join "
|
|
165
|
+
'is unknown; add JoinByIntersection="true" (all must hold) or '
|
|
166
|
+
'JoinByIntersection="false" (any may hold). Left unchanged'
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
for number, component in enumerate(components, start=1):
|
|
170
|
+
if not _component_relevance(component).strip():
|
|
171
|
+
return (
|
|
172
|
+
"E703",
|
|
173
|
+
(
|
|
174
|
+
f"GroupRelevance component {number} (<{component.tag}>) has an "
|
|
175
|
+
"empty Relevance; left unchanged"
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _union_body(statements, any_cdata):
|
|
182
|
+
"""The body of one <Relevance> OR-joining the decoded `statements`."""
|
|
183
|
+
joined = " OR ".join(f"({statement.strip()})" for statement in statements)
|
|
184
|
+
if "]]>" in joined:
|
|
185
|
+
return _escape(joined) # cannot sit inside a single CDATA section
|
|
186
|
+
if any_cdata or any(char in joined for char in "<>&"):
|
|
187
|
+
return f"{CDATA_START}{joined}]]>"
|
|
188
|
+
return joined
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _replacement(src, span, raw_inners, statements, intersection):
|
|
192
|
+
"""Return the text that replaces the GroupRelevance at `span`."""
|
|
193
|
+
if intersection or len(statements) == 1:
|
|
194
|
+
bodies = raw_inners
|
|
195
|
+
else:
|
|
196
|
+
any_cdata = any(raw.strip().startswith(CDATA_START) for raw in raw_inners)
|
|
197
|
+
bodies = [_union_body(statements, any_cdata)]
|
|
198
|
+
line_start = src.rfind("\n", 0, span[0]) + 1
|
|
199
|
+
indent = src[line_start : span[0]]
|
|
200
|
+
separator = "\n" + indent if not indent.strip() else ""
|
|
201
|
+
return separator.join(f"<Relevance>{body}</Relevance>" for body in bodies)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _describe(statements, intersection, join):
|
|
205
|
+
"""E700's account of what a GroupRelevance became; `join` is the attribute."""
|
|
206
|
+
count = len(statements)
|
|
207
|
+
if intersection:
|
|
208
|
+
result = f"{count} <Relevance> element(s)"
|
|
209
|
+
elif count == 1:
|
|
210
|
+
result = "one <Relevance>"
|
|
211
|
+
else:
|
|
212
|
+
result = "one OR-joined <Relevance>"
|
|
213
|
+
join = "missing" if join is None else join.strip()
|
|
214
|
+
return (
|
|
215
|
+
f"GroupRelevance (JoinByIntersection {join}, {count} component(s)) -> {result}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def convert_group_relevance(src):
|
|
220
|
+
"""Convert every convertible GroupRelevance in BES text `src`.
|
|
221
|
+
|
|
222
|
+
Returns (new_src, converted, issues): `converted` lists an E700 per
|
|
223
|
+
GroupRelevance rewritten, `issues` the E701-E703 refusals and any W700.
|
|
224
|
+
Each entry is (lineno, code, message). Pure: no file is touched.
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
root = etree.fromstring(src.encode("utf-8"))
|
|
228
|
+
except etree.XMLSyntaxError as err:
|
|
229
|
+
return src, [], [(1, "W700", f"not parseable BES XML ({err}); skipping")]
|
|
230
|
+
|
|
231
|
+
groups = root.xpath(GROUP_RELEVANCE_XPATH)
|
|
232
|
+
# no tag anywhere: nothing to convert and no misplaced one to warn about
|
|
233
|
+
if "<GroupRelevance" not in src:
|
|
234
|
+
return src, [], []
|
|
235
|
+
|
|
236
|
+
masked = _mask_opaque(src)
|
|
237
|
+
spans = [match.span() for match in GROUP_RELEVANCE_RE.finditer(masked)]
|
|
238
|
+
if len(spans) != len(groups):
|
|
239
|
+
return (
|
|
240
|
+
src,
|
|
241
|
+
[],
|
|
242
|
+
[
|
|
243
|
+
(
|
|
244
|
+
1,
|
|
245
|
+
"W700",
|
|
246
|
+
(
|
|
247
|
+
f"found {len(spans)} <GroupRelevance> tag(s) but "
|
|
248
|
+
f"{len(groups)} at {GROUP_RELEVANCE_XPATH}; skipping"
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
],
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
converted, issues, edits = [], [], []
|
|
255
|
+
for group, span in zip(groups, spans):
|
|
256
|
+
line = lineno(src, span[0])
|
|
257
|
+
refusal = _refusal(group)
|
|
258
|
+
if refusal:
|
|
259
|
+
issues.append((line, *refusal))
|
|
260
|
+
continue
|
|
261
|
+
statements = [_component_relevance(c) for c in _components(group)]
|
|
262
|
+
raw_inners = [
|
|
263
|
+
src[m.start(1) + span[0] : m.end(1) + span[0]]
|
|
264
|
+
for m in RELEVANCE_RE.finditer(masked[span[0] : span[1]])
|
|
265
|
+
]
|
|
266
|
+
if len(raw_inners) != len(statements):
|
|
267
|
+
issues.append(
|
|
268
|
+
(
|
|
269
|
+
line,
|
|
270
|
+
"W700",
|
|
271
|
+
"could not locate each component's <Relevance>; skipping",
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
continue
|
|
275
|
+
intersection = _intersection(group)
|
|
276
|
+
edits.append(
|
|
277
|
+
(span, _replacement(src, span, raw_inners, statements, intersection))
|
|
278
|
+
)
|
|
279
|
+
converted.append(
|
|
280
|
+
(
|
|
281
|
+
line,
|
|
282
|
+
"E700",
|
|
283
|
+
_describe(statements, intersection, group.get("JoinByIntersection")),
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
for (start, end), text in reversed(edits):
|
|
288
|
+
src = src[:start] + text + src[end:]
|
|
289
|
+
return src, converted, issues
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def check_file(path, disabled=frozenset(), check=False):
|
|
293
|
+
"""Check (and unless `check`, convert) one file; return (issues, fixed).
|
|
294
|
+
|
|
295
|
+
Each is a list of (lineno, code, message). Under `check`, a convertible
|
|
296
|
+
GroupRelevance is an E700 issue and nothing is written; otherwise it is
|
|
297
|
+
rewritten in place (line endings preserved) and reported under `fixed`,
|
|
298
|
+
unless the result would break BES.xsd validity (E704). Codes in
|
|
299
|
+
`disabled` are dropped; disabling E700 also stops the conversion.
|
|
300
|
+
"""
|
|
301
|
+
if not os.path.isfile(path):
|
|
302
|
+
return [(1, "W700", "file not found; skipping")], []
|
|
303
|
+
|
|
304
|
+
_raw, src, was_crlf = read_source(path)
|
|
305
|
+
if SKIP_MARKER in src or MUSTACHE_RE.search(src):
|
|
306
|
+
return [], []
|
|
307
|
+
|
|
308
|
+
new_src, converted, issues = convert_group_relevance(src)
|
|
309
|
+
fixed = []
|
|
310
|
+
if "E700" in disabled:
|
|
311
|
+
converted = []
|
|
312
|
+
elif check:
|
|
313
|
+
issues += [
|
|
314
|
+
(line, code, f"{message}; run without --check to convert")
|
|
315
|
+
for line, code, message in converted
|
|
316
|
+
]
|
|
317
|
+
elif converted:
|
|
318
|
+
guard = bes_common.SchemaGuard(
|
|
319
|
+
src, "E704", path, validate="E704" not in disabled
|
|
320
|
+
)
|
|
321
|
+
if guard.apply(new_src, converted):
|
|
322
|
+
with open(path, "wb") as handle:
|
|
323
|
+
handle.write(encode(new_src, was_crlf))
|
|
324
|
+
fixed = converted
|
|
325
|
+
issues += guard.refused
|
|
326
|
+
|
|
327
|
+
issues = [issue for issue in issues if issue[1] not in disabled]
|
|
328
|
+
return sorted(issues), fixed
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def check_files(paths, disabled=frozenset(), check=False):
|
|
332
|
+
"""Check several files; return a list of (path, issues, fixed) tuples.
|
|
333
|
+
|
|
334
|
+
This is the programmatic entry point: it does no printing.
|
|
335
|
+
"""
|
|
336
|
+
return [(path, *check_file(path, disabled=disabled, check=check)) for path in paths]
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def main(argv=None):
|
|
340
|
+
"""Execution starts here.
|
|
341
|
+
|
|
342
|
+
argv defaults to None so this works both as a console_scripts entry point
|
|
343
|
+
(pre-commit calls it with no arguments; argparse then reads sys.argv) and
|
|
344
|
+
when called directly as `main(sys.argv[1:])`.
|
|
345
|
+
"""
|
|
346
|
+
parser = argparse.ArgumentParser(
|
|
347
|
+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
348
|
+
)
|
|
349
|
+
parser.add_argument(
|
|
350
|
+
"--check",
|
|
351
|
+
action="store_true",
|
|
352
|
+
help="dry run: report what would be converted, write nothing",
|
|
353
|
+
)
|
|
354
|
+
parser.add_argument(
|
|
355
|
+
"--strict",
|
|
356
|
+
action="store_true",
|
|
357
|
+
help="treat warnings (W700) as failures; default: advisory",
|
|
358
|
+
)
|
|
359
|
+
parser.add_argument(
|
|
360
|
+
"--disable",
|
|
361
|
+
default="",
|
|
362
|
+
metavar="CODES",
|
|
363
|
+
help="comma-separated check IDs to skip entirely, e.g. --disable E701",
|
|
364
|
+
)
|
|
365
|
+
parser.add_argument(
|
|
366
|
+
"files",
|
|
367
|
+
nargs="*",
|
|
368
|
+
help=(
|
|
369
|
+
"BES files to convert; if omitted, all *.bes files in the current "
|
|
370
|
+
"folder and below are checked (never rewritten)"
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
args = parser.parse_args(argv)
|
|
374
|
+
|
|
375
|
+
disabled = {
|
|
376
|
+
code.strip().upper() for code in args.disable.split(",") if code.strip()
|
|
377
|
+
}
|
|
378
|
+
unknown = disabled - KNOWN_CODES
|
|
379
|
+
if unknown:
|
|
380
|
+
print(
|
|
381
|
+
f"warning: ignoring unknown --disable code(s): {', '.join(sorted(unknown))}"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# like the sibling hooks, auto-discovery never rewrites a whole tree
|
|
385
|
+
check = args.check or not args.files
|
|
386
|
+
paths = (
|
|
387
|
+
args.files if args.files else bes_common.discover_bes_files(".", BES_EXTENSIONS)
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
issue_count, warning_count, fix_count = bes_common.report(
|
|
391
|
+
check_files(paths, disabled=disabled, check=check)
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
if fix_count:
|
|
395
|
+
print(
|
|
396
|
+
f"\nconverted {fix_count} GroupRelevance(s); review and re-stage the changes."
|
|
397
|
+
)
|
|
398
|
+
if warning_count:
|
|
399
|
+
print(f"{warning_count} GroupRelevance warning(s).")
|
|
400
|
+
if issue_count:
|
|
401
|
+
print(f"{issue_count} GroupRelevance issue(s).")
|
|
402
|
+
# E-codes and any conversion always fail; warnings fail only under --strict
|
|
403
|
+
return 1 if (issue_count or fix_count or (warning_count and args.strict)) else 0
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
if __name__ == "__main__":
|
|
407
|
+
sys.exit(main(sys.argv[1:]))
|