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.
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env python3
2
+ """Pre-commit hook: lint the relevance inside BES files.
3
+
4
+ Every other hook in this repo treats relevance as opaque -- `{...}` is a lexer
5
+ state mechanically identical to a string literal, and the most any check proves
6
+ about a condition is that it starts with a brace. This hook is where that stops:
7
+ it hands each relevance statement to `bigfix-relevance-analyzer`, which parses
8
+ it, binds `it`, resolves every inspector against the dumps, type-checks the
9
+ result, and scores what it costs to evaluate.
10
+
11
+ Every relevance site the analyzer's extractor recognizes is checked -- <Relevance>
12
+ bodies, <SuccessCriteria Option="CustomRelevance"> bodies, Analysis <Property>
13
+ bodies, and the `{...}` substitutions inside <ActionScript> -- so a defect in a
14
+ substitution is reported at the line it lives on, not at the top of the file.
15
+
16
+ Checks:
17
+ E600 the statement could not be parsed
18
+ E601 the statement contains text that could not be lexed
19
+ E602 `it` is used where there is no context to bind it to
20
+ E603 the type checker reported a problem beyond an unbound `it`
21
+ E604 the complexity score is above the ceiling (raise with --max-score)
22
+ E605 the evaluation cost is above the ceiling (raise with
23
+ --max-evaluation-cost)
24
+ E606 a directory tree was deeper than the walk's limit, so it was not
25
+ fully scanned. Only reachable when auto-discovering -- pre-commit
26
+ always passes filenames
27
+ E607 a path given to the linter does not exist, could not be read, or
28
+ is not a file type the analyzer's extractor recognizes (under
29
+ pre-commit the hook's `files:` pattern keeps those away)
30
+ E608 the value does not fit the kind of site it was extracted from -- a
31
+ non-boolean <Relevance>, or an `if`/`elseif`/`continue if`
32
+ condition that is not a boolean or string
33
+ E609 inspectors exclusive to client relevance and to session relevance in
34
+ one statement, which no engine can answer
35
+ E610 an ActionScript command word used as a relevance name -- usually an
36
+ ActionScript line pasted or doubled into relevance
37
+ E611 a plural operand where the engine requires a single value, which it
38
+ refuses before evaluating anything. Split out of E603; where the
39
+ analyzer can work out `unique value of` or the singular spelling of
40
+ an aggregate, it is printed as a suggested fix
41
+ E612 an ActionScript `{` with anything after it on its line and no `}` to
42
+ close it there, which fails the action
43
+ E613 a `<?Relevance` with no closing `?>`, so its relevance was not linted
44
+ E614 a BES file that is not well-formed XML, so nothing in it was linted.
45
+ Off by default -- bes-schema-validate owns file validity -- and
46
+ switched back on with --enable E614
47
+ W600 a name no inspector dump defines. A warning rather than an error
48
+ because a repo running a newer client than the analyzer's snapshot
49
+ legitimately uses names it has never heard of
50
+ W601 a property written singular over an object that may be plural.
51
+ Disabled by default in .pre-commit-hooks.yaml: it fires ~6 times per
52
+ file across real content, which drowns everything else. Enable it
53
+ deliberately with --enable W601
54
+ W602 a singular spelling mid-chain -- under a `whose` filter, or where a
55
+ plural is built from it -- where the plural reads safer
56
+ W603 two version-looking strings compared as strings, not as versions
57
+ W604 a version comparison that truncates to the shorter operand's
58
+ components
59
+ W605 an ordinary ActionScript `{...}` substitution (in a `run`, `wait`,
60
+ `parameter`, ...) whose value is an opaque object with no text form
61
+ -- a `bes *`, `dmi *`, `smbios *`, `xml dom *`, `active directory *`
62
+ or `sqlite *` object, or a bare `registry`, `process`, `wmi
63
+ object`, ... -- so there is nothing to embed in the command line.
64
+ A warning rather than an E608 error because which types cannot
65
+ render is the analyzer's curated inference, not a confirmed engine
66
+ fact
67
+ W606 an ordinary ActionScript `{...}` substitution whose value may be
68
+ more than one value. The client joins a plural with no separator,
69
+ so the action runs but probably not as meant; use
70
+ `concatenation "<sep>" of (...)` or a tighter filter
71
+ W607 a relevance-tagged markdown code fence that is never closed, so its
72
+ relevance was not linted (markdown only; never from a BES file)
73
+
74
+ E-codes fail the hook; warnings fail only under --strict.
75
+
76
+ Unparsable XML is skipped, not reported (E614 is off by default) --
77
+ bes-schema-validate owns file validity, and this hook must not duplicate its
78
+ findings. That does mean a
79
+ truncated file passes here: a clean run says the relevance the extractor could
80
+ see is sound, not that the file parses.
81
+
82
+ A file opts out of every check here with `pre-commit-skip: bes-relevance-lint`
83
+ anywhere in it, the same marker the sibling hooks honor. There is no per-rule
84
+ marker: --disable takes the code repo-wide, and a single file that legitimately
85
+ needs relevance this complex is what --max-score is for.
86
+
87
+ There is no auto-fix: files are never rewritten. Where the analyzer can work
88
+ out a safe rewrite of a whole statement (today, the singular spellings behind
89
+ W602 and the `unique value of` / singular-aggregate rewrites behind E611), the fixed statement is printed once under that statement's first
90
+ finding as `suggested fix: ...`, for a human to copy in. Everything else needs
91
+ a human to decide what the relevance was meant to say.
92
+
93
+ Usage:
94
+ bes-relevance-lint [--strict] [--disable E604] [--enable W601] [FILES...]
95
+
96
+ Exit status:
97
+ 0 no E-code findings (and, without --strict, no warnings either), or the
98
+ interpreter is older than 3.11, where the analyzer cannot be installed
99
+ and the hook skips with a notice rather than failing a build that has
100
+ no way to go green
101
+ 1 an E-code fired, a warning fired under --strict, or the analyzer is
102
+ missing on a 3.11+ interpreter, where it should have been installed
103
+ """
104
+
105
+ import argparse
106
+ import sys
107
+
108
+ try:
109
+ from bigfix_relevance_analyzer import (
110
+ LintConfig,
111
+ Severity,
112
+ lint_directory,
113
+ lint_paths,
114
+ )
115
+
116
+ IMPORT_ERROR = None
117
+ except ImportError as exc: # pragma: no cover - exercised only on an old Python
118
+ LintConfig = lint_directory = lint_paths = Severity = None
119
+ IMPORT_ERROR = exc
120
+
121
+ __all__ = ["main"]
122
+
123
+ # Every rule the analyzer can report, mapped to this repo's code vocabulary.
124
+ # Two spellings exist because a repo configures hooks by E-code (--disable E604,
125
+ # same as every sibling hook) while the analyzer names its rules in prose; the
126
+ # analyzer's own name is printed in the message tail so either one greps to the
127
+ # same line.
128
+ #
129
+ # tests/test_bes_relevance_lint.py fails if a key of the analyzer's RULES is
130
+ # missing here -- that is what turns "the library added a rule" from a silent
131
+ # dropped finding into a red build.
132
+ CODES = {
133
+ "parse-error": "E600",
134
+ "error-token": "E601",
135
+ "unbound-it": "E602",
136
+ "type-error": "E603",
137
+ "complexity": "E604",
138
+ "evaluation-cost": "E605",
139
+ "max-depth-exceeded": "E606",
140
+ "file-error": "E607",
141
+ "site-type-mismatch": "E608",
142
+ "mixed-dialect": "E609",
143
+ "actionscript-keyword": "E610",
144
+ "singular-required": "E611",
145
+ "unterminated-substitution": "E612",
146
+ "unterminated-processing-instruction": "E613",
147
+ "xml-parse-error": "E614",
148
+ "unknown-inspector": "W600",
149
+ "non-unique-risk": "W601",
150
+ "plural-preferred": "W602",
151
+ "version-like-string-compare": "W603",
152
+ "version-truncating-compare": "W604",
153
+ "non-renderable-substitution": "W605",
154
+ "plural-substitution": "W606",
155
+ "unterminated-code-fence": "W607",
156
+ }
157
+
158
+ KNOWN_CODES = frozenset(CODES.values())
159
+
160
+ # The reverse direction, for --disable/--enable: a repo names an E-code, the
161
+ # analyzer's LintConfig wants its own.
162
+ ANALYZER_CODES = {code: name for name, code in CODES.items()}
163
+
164
+ # Off unless a repo names them in --enable. E614 is a BES file that is not
165
+ # well-formed XML: bes-schema-validate already reports that, and this hook must
166
+ # not duplicate its findings, so unparsable XML stays skipped here.
167
+ DEFAULT_DISABLED = frozenset({"E614"})
168
+
169
+
170
+ MIN_ANALYZER_PYTHON = (3, 11)
171
+
172
+
173
+ def _analyzer_unavailable():
174
+ """Explain the missing analyzer and return the exit status to use.
175
+
176
+ The two cases are not the same failure. Below 3.11 the dependency cannot
177
+ be installed at all -- setup.cfg holds it back with an environment marker
178
+ on purpose, because this package still supports 3.8 -- so nothing the repo
179
+ writes in its config makes the hook run, and failing would leave it a
180
+ permanently red build with no fix available. There the hook skips: it
181
+ prints why and exits 0. On 3.11+ the analyzer is a hard dependency, so a
182
+ missing one is a broken install and stays a failure.
183
+ """
184
+ running = f"{sys.version_info[0]}.{sys.version_info[1]}"
185
+ if sys.version_info < MIN_ANALYZER_PYTHON:
186
+ print(
187
+ "bes-relevance-lint: skipped. It requires bigfix-relevance-analyzer, "
188
+ f"which needs Python 3.11 or newer; this interpreter is {running}. "
189
+ "To actually run this hook, point pre-commit at a 3.11+ interpreter "
190
+ "with `default_language_version` in your .pre-commit-config.yaml. "
191
+ f"({IMPORT_ERROR})"
192
+ )
193
+ return 0
194
+ print(
195
+ "bes-relevance-lint requires bigfix-relevance-analyzer, which is not "
196
+ f"importable on this Python {running} interpreter even though it is new "
197
+ "enough for it. Reinstall this package's dependencies (pip install "
198
+ "--upgrade pre-commit-bigfix), or drop this hook from your config. "
199
+ f"({IMPORT_ERROR})"
200
+ )
201
+ return 1
202
+
203
+
204
+ def _split_codes(raw):
205
+ """Parse a comma-separated --disable/--enable value into a set of E-codes."""
206
+ return {code.strip().upper() for code in raw.split(",") if code.strip()}
207
+
208
+
209
+ def _severities(disabled, enabled):
210
+ """Per-code overrides for LintConfig, in the analyzer's own vocabulary."""
211
+ severities = {}
212
+ for code in disabled:
213
+ severities[ANALYZER_CODES[code]] = Severity.IGNORE
214
+ for code in enabled:
215
+ # Restore the analyzer's own default rather than forcing a severity:
216
+ # --enable exists to undo a default --disable from the hook
217
+ # declaration, not to promote a warning into an error. --strict is
218
+ # what does that, for every warning at once.
219
+ severities.pop(ANALYZER_CODES[code], None)
220
+ return severities
221
+
222
+
223
+ SKIP_MARKER = "pre-commit-skip: bes-relevance-lint"
224
+
225
+
226
+ def _opted_out(path):
227
+ """Whether this file carries the marker that skips this hook entirely.
228
+
229
+ Read as bytes and matched against bytes: this runs before the analyzer
230
+ touches the file, so it must not care whether the file decodes cleanly.
231
+ An unreadable file is not opted out -- it simply yields no findings once
232
+ the extractor reaches it.
233
+ """
234
+ try:
235
+ with open(path, "rb") as handle:
236
+ return SKIP_MARKER.encode("utf-8") in handle.read()
237
+ except OSError:
238
+ return False
239
+
240
+
241
+ def _report(findings):
242
+ """Print each finding in this repo's format.
243
+
244
+ Returns (errors, warnings).
245
+ """
246
+ errors = warnings = 0
247
+ # Every fixable finding from one statement carries the same whole-statement
248
+ # rewrite, so print it once per statement rather than once per finding.
249
+ suggested = set()
250
+ for finding in findings:
251
+ code = CODES[finding.code]
252
+ where = f"{finding.path}:{finding.line}"
253
+ label = "warning: " if finding.severity is Severity.WARNING else ""
254
+ print(f"{where}: [{code}] {label}{finding.message} ({finding.code})")
255
+ fix = getattr(finding, "autofix", None)
256
+ if fix is not None:
257
+ site_line = finding.site.line if finding.site is not None else None
258
+ key = (str(finding.path), site_line, fix.original)
259
+ if key not in suggested:
260
+ suggested.add(key)
261
+ print(f" suggested fix: {fix.fixed}")
262
+ if finding.severity is Severity.WARNING:
263
+ warnings += 1
264
+ else:
265
+ errors += 1
266
+ return errors, warnings
267
+
268
+
269
+ def main(argv=None):
270
+ """Execution starts here.
271
+
272
+ argv defaults to None so this works both as a console_scripts entry point
273
+ (pre-commit calls it with no arguments; argparse then reads sys.argv) and
274
+ when called directly as `main(sys.argv[1:])`.
275
+ """
276
+ if IMPORT_ERROR is not None:
277
+ return _analyzer_unavailable()
278
+
279
+ parser = argparse.ArgumentParser(description=__doc__)
280
+ parser.add_argument(
281
+ "--strict",
282
+ action="store_true",
283
+ help="treat warnings as failures (non-zero exit); default: advisory",
284
+ )
285
+ parser.add_argument(
286
+ "--disable",
287
+ default="",
288
+ metavar="CODES",
289
+ help="comma-separated check IDs to skip entirely, e.g. --disable E604",
290
+ )
291
+ parser.add_argument(
292
+ "--enable",
293
+ default="",
294
+ metavar="CODES",
295
+ help=(
296
+ "comma-separated check IDs to switch back on, undoing a --disable "
297
+ "this hook declares by default, e.g. --enable W601"
298
+ ),
299
+ )
300
+ parser.add_argument(
301
+ "--max-score",
302
+ type=float,
303
+ default=None,
304
+ help=(
305
+ "raise E604's complexity ceiling above the analyzer's default; "
306
+ "content that legitimately needs to be this complex should raise "
307
+ "this rather than disable the check"
308
+ ),
309
+ )
310
+ parser.add_argument(
311
+ "--max-evaluation-cost",
312
+ type=float,
313
+ default=None,
314
+ help="raise E605's evaluation-cost ceiling above the analyzer's default",
315
+ )
316
+ parser.add_argument(
317
+ "--dialect",
318
+ choices=["client", "session"],
319
+ default=None,
320
+ help="force the dialect instead of trusting what extraction says",
321
+ )
322
+ parser.add_argument(
323
+ "--platform",
324
+ default=None,
325
+ help="narrow client inspector lookups to one platform, e.g. windows",
326
+ )
327
+ parser.add_argument(
328
+ "--max-depth",
329
+ type=int,
330
+ default=6,
331
+ help=(
332
+ "how many directory levels to walk when auto-discovering "
333
+ "(no effect when files are given, which is always the case under "
334
+ "pre-commit)"
335
+ ),
336
+ )
337
+ parser.add_argument(
338
+ "files",
339
+ nargs="*",
340
+ help=(
341
+ "files to check; a named file the analyzer's extractor does not "
342
+ "recognize is reported as E607. If omitted, the current folder and "
343
+ "below is walked"
344
+ ),
345
+ )
346
+ args = parser.parse_args(argv)
347
+
348
+ disabled = _split_codes(args.disable)
349
+ enabled = _split_codes(args.enable)
350
+ unknown = (disabled | enabled) - KNOWN_CODES
351
+ if unknown:
352
+ print(
353
+ "warning: ignoring unknown --disable/--enable code(s): {}".format(
354
+ ", ".join(sorted(unknown))
355
+ )
356
+ )
357
+ disabled -= unknown
358
+ enabled -= unknown
359
+ disabled |= DEFAULT_DISABLED - enabled
360
+
361
+ config_kwargs = {"severities": _severities(disabled, enabled)}
362
+ # The analyzer's ceilings are on by default and these only raise them, so a
363
+ # None here means "keep the built-in default", not "no limit".
364
+ if args.max_score is not None:
365
+ config_kwargs["max_score"] = args.max_score
366
+ if args.max_evaluation_cost is not None:
367
+ config_kwargs["max_evaluation_cost"] = args.max_evaluation_cost
368
+ if args.platform is not None:
369
+ config_kwargs["platform"] = args.platform
370
+ if args.dialect is not None:
371
+ from bigfix_relevance_analyzer.dialect import Dialect
372
+
373
+ config_kwargs["dialect"] = Dialect(args.dialect)
374
+
375
+ config = LintConfig(**config_kwargs)
376
+
377
+ if args.files:
378
+ findings = lint_paths(
379
+ [path for path in args.files if not _opted_out(path)], config
380
+ )
381
+ else:
382
+ # Filter after the walk rather than before it: the analyzer owns
383
+ # discovery, and re-walking here to drop opted-out files first would
384
+ # mean two definitions of what counts as a file to check.
385
+ findings = [
386
+ finding
387
+ for finding in lint_directory(".", config, max_depth=args.max_depth)
388
+ if not _opted_out(finding.path)
389
+ ]
390
+
391
+ error_count, warning_count = _report(findings)
392
+
393
+ if warning_count:
394
+ print(f"{warning_count} warning(s).")
395
+ if error_count:
396
+ print(f"{error_count} issue(s).")
397
+ # E-codes always fail; warnings fail only under --strict
398
+ return 1 if (error_count or (warning_count and args.strict)) else 0
399
+
400
+
401
+ if __name__ == "__main__":
402
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,84 @@
1
+ """A pre-commit hook to validate BigFix XML files against their schemas.
2
+
3
+ Covers BES and OJO files, Git External Custom Site files (siteConfig.xml,
4
+ site.xml, digest.xml) and BigFix Inventory signatures (Signatures/*.xml).
5
+ Only the schemas bundled with validate_bes_xml are used, never a *.xsd in the
6
+ current folder. Each error is printed as `path:line: message`; files that
7
+ pass print nothing. A .xml file whose root has no bundled schema (a sitemap
8
+ or a Maven site descriptor named site.xml, say) is not a BigFix file and is
9
+ skipped; a .bes or .ojo file with no schema fails.
10
+ """
11
+
12
+ import argparse
13
+ import os
14
+ import sys
15
+
16
+ import validate_bes_xml
17
+
18
+ if __package__ in (None, ""): # run directly as a script, not as a module
19
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
20
+
21
+ from pre_commit_bigfix import bes_common
22
+
23
+
24
+ def build_argument_parser():
25
+ """Build and return the argument parser."""
26
+
27
+ parser = argparse.ArgumentParser(
28
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
29
+ )
30
+ parser.add_argument("filenames", nargs="*", help="Filenames to check.")
31
+
32
+ return parser
33
+
34
+
35
+ def check_file(filename, schemas):
36
+ """Validate one file; print its errors and return True if it failed."""
37
+
38
+ result = validate_bes_xml.validate_bes(path=filename, schema_pathnames=schemas)
39
+ if result:
40
+ return False
41
+ # compare against the value: every Status member is a non-empty str
42
+ if result.status == "no_schema":
43
+ if filename.lower().endswith(".xml"):
44
+ return False # not a BigFix file
45
+ # inferred_schema is left out: it is cut short for namespaced roots
46
+ print(f"{filename}: no schema to validate this file against")
47
+ return True
48
+ for line, message in result.errors:
49
+ if line is None:
50
+ print(f"{filename}: {message}")
51
+ else:
52
+ print(f"{filename}:{line}: {message}")
53
+ if not result.errors: # never expected, but a failure must say something
54
+ print(f"{filename}: {result.status.value}")
55
+ return True
56
+
57
+
58
+ def main(argv=None):
59
+ """Main process."""
60
+
61
+ # Parse command line arguments.
62
+ argparser = build_argument_parser()
63
+ args = argparser.parse_args(argv)
64
+
65
+ if not args.filenames:
66
+ return 0
67
+
68
+ schemas = bes_common._bundled_schemas( # pylint: disable=protected-access
69
+ validate_bes_xml
70
+ )
71
+ if not schemas:
72
+ # an empty list would make validate_bes_xml fall back to the cwd schemas
73
+ print(
74
+ "bes-schema-validate: validate_bes_xml has no bundled schemas; "
75
+ 'reinstall "validate_bes_xml>=2.4.1,<3"',
76
+ file=sys.stderr,
77
+ )
78
+ return len(args.filenames)
79
+
80
+ return sum(check_file(filename, schemas) for filename in args.filenames)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ sys.exit(main())