pydry-cli 0.0.3__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.
pydry/cli.py ADDED
@@ -0,0 +1,646 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from .engine import abstract_candidates, exact_groups, near_matches, to_jsonable
10
+
11
+ if TYPE_CHECKING:
12
+ from .models import ExactGroup, SimilarityResult
13
+
14
+
15
+ def _diagnostics_payload(
16
+ scan_errors: list[str], plugin_errors: list[str]
17
+ ) -> dict[str, object]:
18
+ unique_plugin_errors = list(dict.fromkeys(plugin_errors))
19
+ return {
20
+ "scan_errors_count": len(scan_errors),
21
+ "scan_error_samples": scan_errors[:5],
22
+ "plugin_errors_count": len(unique_plugin_errors),
23
+ "plugin_error_samples": unique_plugin_errors[:5],
24
+ }
25
+
26
+
27
+ def _json_envelope(
28
+ payload: object,
29
+ *,
30
+ scan_errors: list[str],
31
+ plugin_errors: list[str],
32
+ ) -> dict[str, object]:
33
+ return {
34
+ "results": payload,
35
+ "diagnostics": _diagnostics_payload(scan_errors, plugin_errors),
36
+ }
37
+
38
+
39
+ def _emit_json_output(
40
+ payload: object,
41
+ *,
42
+ scan_errors: list[str],
43
+ plugin_errors: list[str],
44
+ output_path: str | None,
45
+ ) -> None:
46
+ envelope = _json_envelope(
47
+ payload,
48
+ scan_errors=scan_errors,
49
+ plugin_errors=plugin_errors,
50
+ )
51
+ rendered = json.dumps(envelope, indent=2)
52
+ if output_path:
53
+ out = Path(output_path)
54
+ out.parent.mkdir(parents=True, exist_ok=True)
55
+ out.write_text(rendered + "\n", encoding="utf-8")
56
+ print(f"Wrote JSON report to {out}", file=sys.stderr)
57
+ return
58
+ print(rendered)
59
+
60
+
61
+ def _parse_min_count(value: str) -> int:
62
+ parsed = int(value)
63
+ if parsed < 2:
64
+ msg = "min-count must be >= 2"
65
+ raise argparse.ArgumentTypeError(msg)
66
+ return parsed
67
+
68
+
69
+ def _parse_threshold(value: str) -> float:
70
+ parsed = float(value)
71
+ if parsed < 0.0 or parsed > 1.0:
72
+ msg = "threshold must be between 0 and 1"
73
+ raise argparse.ArgumentTypeError(msg)
74
+ return parsed
75
+
76
+
77
+ def _parse_top_k(value: str) -> int:
78
+ parsed = int(value)
79
+ if parsed < 0:
80
+ msg = "top-k must be >= 0"
81
+ raise argparse.ArgumentTypeError(msg)
82
+ return parsed
83
+
84
+
85
+ def _add_json_output_arg(parser: argparse.ArgumentParser) -> None:
86
+ parser.add_argument(
87
+ "--output",
88
+ help=("Write JSON output to a file path. Requires --format json."),
89
+ )
90
+
91
+
92
+ def _validate_output_arg(
93
+ *,
94
+ output_path: str | None,
95
+ output_format: str,
96
+ ) -> bool:
97
+ if output_path and output_format != "json":
98
+ print("Error: --output requires --format json.", file=sys.stderr)
99
+ return False
100
+ return True
101
+
102
+
103
+ def _print_diagnostics(scan_errors: list[str], plugin_errors: list[str]) -> None:
104
+ if scan_errors:
105
+ print(
106
+ (
107
+ f"Warning: skipped {len(scan_errors)} file(s) due to parse/read errors."
108
+ " Use --strict to fail instead."
109
+ ),
110
+ file=sys.stderr,
111
+ )
112
+ for msg in scan_errors[:5]:
113
+ print(f" - {msg}", file=sys.stderr)
114
+ if len(scan_errors) > 5:
115
+ print(f" ... {len(scan_errors) - 5} more", file=sys.stderr)
116
+
117
+ if plugin_errors:
118
+ unique_errors = list(dict.fromkeys(plugin_errors))
119
+ print(
120
+ (
121
+ f"Warning: {len(unique_errors)} plugin error(s) occurred."
122
+ " Plugin failures were isolated."
123
+ ),
124
+ file=sys.stderr,
125
+ )
126
+ for msg in unique_errors[:5]:
127
+ print(f" - {msg}", file=sys.stderr)
128
+ if len(unique_errors) > 5:
129
+ print(f" ... {len(unique_errors) - 5} more", file=sys.stderr)
130
+
131
+
132
+ def _print_exact(groups: list[ExactGroup]) -> None:
133
+ if not groups:
134
+ print("No duplicate functions found.")
135
+ return
136
+ for i, g in enumerate(groups, start=1):
137
+ print(f"Group {i}: {g.count} occurrences hash={g.hash[:12]}")
138
+ for occ in g.occurrences:
139
+ end = f"-{occ.end_lineno}" if occ.end_lineno else ""
140
+ print(f" {occ.path}:{occ.lineno}{end} {occ.kind} {occ.qualname}")
141
+ print()
142
+
143
+
144
+ def _print_near(rows: list[SimilarityResult]) -> None:
145
+ if not rows:
146
+ print("No near matches found.")
147
+ return
148
+ for i, r in enumerate(rows, start=1):
149
+ print(
150
+ f"{i}. sim={r.similarity_score:.4f}"
151
+ f" refactor={r.refactorability_score:.4f}"
152
+ f" {r.a.qualname} <-> {r.b.qualname}"
153
+ )
154
+ print(f" refactor: {r.suggested_refactor_kind}")
155
+ if r.pattern_labels:
156
+ print(" labels: " + ", ".join(r.pattern_labels))
157
+ if r.risk_flags:
158
+ print(" risks: " + ", ".join(r.risk_flags))
159
+ print(f" shared: {r.shared_structure_summary}")
160
+ if r.key_differences:
161
+ print(" diffs: " + "; ".join(r.key_differences))
162
+ print()
163
+
164
+
165
+ def _dedupe_messages(messages: list[str]) -> list[str]:
166
+ return list(dict.fromkeys(messages))
167
+
168
+
169
+ def _showcase_pair_rows(
170
+ rows: list[SimilarityResult], *, top_k: int
171
+ ) -> list[dict[str, object]]:
172
+ out: list[dict[str, object]] = []
173
+ for row in rows[:top_k]:
174
+ out.append(
175
+ {
176
+ "similarity_score": row.similarity_score,
177
+ "refactorability_score": row.refactorability_score,
178
+ "a": row.a.qualname,
179
+ "b": row.b.qualname,
180
+ "suggested_refactor_kind": row.suggested_refactor_kind,
181
+ "pattern_labels": row.pattern_labels,
182
+ "risk_flags": row.risk_flags,
183
+ }
184
+ )
185
+ return out
186
+
187
+
188
+ def _showcase_exact_rows(
189
+ groups: list[ExactGroup], *, top_k: int
190
+ ) -> list[dict[str, object]]:
191
+ out: list[dict[str, object]] = []
192
+ for group in groups[:top_k]:
193
+ out.append(
194
+ {
195
+ "count": group.count,
196
+ "hash_prefix": group.hash[:12],
197
+ "qualnames": [occ.qualname for occ in group.occurrences],
198
+ }
199
+ )
200
+ return out
201
+
202
+
203
+ def _showcase_payload(
204
+ *,
205
+ root: Path,
206
+ threshold: float,
207
+ top_k: int,
208
+ exact_rows: list[ExactGroup],
209
+ near_rows: list[SimilarityResult],
210
+ abstract_rows: list[SimilarityResult],
211
+ ) -> dict[str, Any]:
212
+ return {
213
+ "root": str(root),
214
+ "settings": {
215
+ "threshold": threshold,
216
+ "top_k": top_k,
217
+ "exact_normalization": {
218
+ "normalize_local_names": True,
219
+ "normalize_constants": True,
220
+ },
221
+ },
222
+ "summary": {
223
+ "exact_group_count": len(exact_rows),
224
+ "near_count": len(near_rows),
225
+ "abstract_count": len(abstract_rows),
226
+ },
227
+ "top_examples": {
228
+ "exact": _showcase_exact_rows(exact_rows, top_k=top_k),
229
+ "near": _showcase_pair_rows(near_rows, top_k=top_k),
230
+ "abstract": _showcase_pair_rows(abstract_rows, top_k=top_k),
231
+ },
232
+ }
233
+
234
+
235
+ def _report_payload(
236
+ *,
237
+ root: Path,
238
+ threshold: float,
239
+ top_k: int | None,
240
+ normalize_local_names: bool,
241
+ normalize_constants: bool,
242
+ exact_rows: list[ExactGroup],
243
+ near_rows: list[SimilarityResult],
244
+ abstract_rows: list[SimilarityResult],
245
+ ) -> dict[str, Any]:
246
+ return {
247
+ "root": str(root),
248
+ "settings": {
249
+ "threshold": threshold,
250
+ "top_k": top_k,
251
+ "exact_normalization": {
252
+ "normalize_local_names": normalize_local_names,
253
+ "normalize_constants": normalize_constants,
254
+ },
255
+ },
256
+ "summary": {
257
+ "exact_group_count": len(exact_rows),
258
+ "near_count": len(near_rows),
259
+ "abstract_count": len(abstract_rows),
260
+ },
261
+ "exact": to_jsonable(exact_rows),
262
+ "near": to_jsonable(near_rows),
263
+ "abstract": to_jsonable(abstract_rows),
264
+ }
265
+
266
+
267
+ def _print_showcase(payload: dict[str, Any]) -> None:
268
+ def _score_bar(score: float, *, width: int = 18) -> str:
269
+ clamped = max(0.0, min(1.0, score))
270
+ filled = round(clamped * width)
271
+ return "[" + ("#" * filled) + ("." * (width - filled)) + "]"
272
+
273
+ summary = payload["summary"]
274
+ top_examples = payload["top_examples"]
275
+ settings = payload["settings"]
276
+
277
+ print("=" * 72)
278
+ print("PYDRY SHOWCASE SIMULATION")
279
+ print("=" * 72)
280
+ print(f"Corpus: {payload['root']}")
281
+ print(
282
+ "Summary: "
283
+ f"exact_groups={summary['exact_group_count']} "
284
+ f"near_pairs={summary['near_count']} "
285
+ f"abstract_candidates={summary['abstract_count']}"
286
+ )
287
+ print(f"Config: threshold={settings['threshold']} top_k={settings['top_k']}")
288
+
289
+ print("\n[1/3] Exact duplicate discovery")
290
+ print(
291
+ " command: pydry exact <corpus> --normalize-local-names --normalize-constants"
292
+ )
293
+ if top_examples["exact"]:
294
+ for i, group in enumerate(top_examples["exact"], start=1):
295
+ names = ", ".join(group["qualnames"])
296
+ print(f" {i}. count={group['count']} hash={group['hash_prefix']} {names}")
297
+ else:
298
+ print(" none")
299
+
300
+ print("\n[2/3] Near-match ranking")
301
+ print(f" command: pydry near <corpus> --threshold {settings['threshold']}")
302
+ if top_examples["near"]:
303
+ for i, row in enumerate(top_examples["near"], start=1):
304
+ sim_bar = _score_bar(row["similarity_score"])
305
+ ref_bar = _score_bar(row["refactorability_score"])
306
+ print(
307
+ f" {i}. {row['a']} <-> {row['b']} ({row['suggested_refactor_kind']})"
308
+ )
309
+ print(f" sim {sim_bar} {row['similarity_score']:.4f}")
310
+ print(f" refactor {ref_bar} {row['refactorability_score']:.4f}")
311
+ if row["pattern_labels"]:
312
+ print(" labels " + ", ".join(row["pattern_labels"]))
313
+ if row["risk_flags"]:
314
+ print(" risks " + ", ".join(row["risk_flags"]))
315
+ else:
316
+ print(" none")
317
+
318
+ print("\n[3/3] Abstraction candidates")
319
+ print(f" command: pydry abstract <corpus> --threshold {settings['threshold']}")
320
+ if top_examples["abstract"]:
321
+ for i, row in enumerate(top_examples["abstract"], start=1):
322
+ sim_bar = _score_bar(row["similarity_score"])
323
+ ref_bar = _score_bar(row["refactorability_score"])
324
+ print(
325
+ f" {i}. {row['a']} <-> {row['b']} ({row['suggested_refactor_kind']})"
326
+ )
327
+ print(f" sim {sim_bar} {row['similarity_score']:.4f}")
328
+ print(f" refactor {ref_bar} {row['refactorability_score']:.4f}")
329
+ if row["pattern_labels"]:
330
+ print(" labels " + ", ".join(row["pattern_labels"]))
331
+ else:
332
+ print(" none")
333
+
334
+ print("\nTip: rerun with --format json for machine-readable snapshots.")
335
+
336
+
337
+ def _add_showcase_args(parser: argparse.ArgumentParser) -> None:
338
+ parser.add_argument("root", nargs="?", default=".")
339
+ parser.add_argument("--threshold", type=_parse_threshold, default=0.75)
340
+ parser.add_argument("--top-k", type=_parse_top_k, default=5)
341
+ parser.add_argument("--top-level-only", action="store_true")
342
+ parser.add_argument("--strict", action="store_true")
343
+ parser.add_argument("--format", choices=("text", "json"), default="text")
344
+ _add_json_output_arg(parser)
345
+
346
+
347
+ def main(argv: list[str] | None = None) -> int:
348
+ argv = argv if argv is not None else sys.argv[1:]
349
+ ap = argparse.ArgumentParser(
350
+ prog="pydry",
351
+ description=(
352
+ "AST-based duplicate and structural similarity detector for Python."
353
+ ),
354
+ )
355
+ sub = ap.add_subparsers(dest="cmd", required=True)
356
+
357
+ p_exact = sub.add_parser(
358
+ "exact",
359
+ help="Find exact structural duplicates under configurable normalization.",
360
+ )
361
+ p_exact.add_argument("root")
362
+ p_exact.add_argument("-n", "--min-count", type=_parse_min_count, default=2)
363
+ p_exact.add_argument("--top-level-only", action="store_true")
364
+ p_exact.add_argument("--normalize-local-names", action="store_true")
365
+ p_exact.add_argument("--normalize-constants", action="store_true")
366
+ p_exact.add_argument("--include-canonical", action="store_true")
367
+ p_exact.add_argument("--strict", action="store_true")
368
+ p_exact.add_argument("--format", choices=("text", "json"), default="text")
369
+ _add_json_output_arg(p_exact)
370
+
371
+ p_near = sub.add_parser("near", help="Rank structurally similar functions.")
372
+ p_near.add_argument("root")
373
+ p_near.add_argument("--threshold", type=_parse_threshold, default=0.8)
374
+ p_near.add_argument("--top-k", type=_parse_top_k, default=None)
375
+ p_near.add_argument("--top-level-only", action="store_true")
376
+ p_near.add_argument("--strict", action="store_true")
377
+ p_near.add_argument("--format", choices=("text", "json"), default="text")
378
+ _add_json_output_arg(p_near)
379
+
380
+ p_abs = sub.add_parser(
381
+ "abstract", help="Report likely abstraction/refactor candidates."
382
+ )
383
+ p_abs.add_argument("root")
384
+ p_abs.add_argument("--threshold", type=_parse_threshold, default=0.82)
385
+ p_abs.add_argument("--top-k", type=_parse_top_k, default=None)
386
+ p_abs.add_argument("--top-level-only", action="store_true")
387
+ p_abs.add_argument("--strict", action="store_true")
388
+ p_abs.add_argument("--format", choices=("text", "json"), default="text")
389
+ _add_json_output_arg(p_abs)
390
+
391
+ p_report = sub.add_parser(
392
+ "report",
393
+ help=(
394
+ "Generate a single machine-readable report combining exact, near, "
395
+ "and abstraction-candidate results."
396
+ ),
397
+ )
398
+ p_report.add_argument("root")
399
+ p_report.add_argument("--threshold", type=_parse_threshold, default=0.8)
400
+ p_report.add_argument("--top-k", type=_parse_top_k, default=200)
401
+ p_report.add_argument("--top-level-only", action="store_true")
402
+ p_report.add_argument("--strict", action="store_true")
403
+ p_report.set_defaults(normalize_local_names=True, normalize_constants=True)
404
+ p_report.add_argument(
405
+ "--no-normalize-local-names",
406
+ action="store_false",
407
+ dest="normalize_local_names",
408
+ help="Disable local-name normalization for exact-group analysis.",
409
+ )
410
+ p_report.add_argument(
411
+ "--no-normalize-constants",
412
+ action="store_false",
413
+ dest="normalize_constants",
414
+ help="Disable constant normalization for exact-group analysis.",
415
+ )
416
+ p_report.add_argument("--format", choices=("json",), default="json")
417
+ _add_json_output_arg(p_report)
418
+
419
+ p_show = sub.add_parser(
420
+ "showcase",
421
+ help=(
422
+ "Run a compact summary over a corpus (defaults to the current directory)."
423
+ ),
424
+ )
425
+ _add_showcase_args(p_show)
426
+
427
+ p_sim = sub.add_parser(
428
+ "simulate",
429
+ help=(
430
+ "Run a visual terminal simulation of duplicate detection and "
431
+ "refactor-candidate ranking."
432
+ ),
433
+ )
434
+ _add_showcase_args(p_sim)
435
+
436
+ args = ap.parse_args(argv)
437
+ root = Path(args.root)
438
+ if not root.exists() or not root.is_dir():
439
+ print(f"Invalid directory: {root}", file=sys.stderr)
440
+ return 2
441
+
442
+ scan_errors: list[str] = []
443
+ plugin_errors: list[str] = []
444
+
445
+ if args.cmd == "exact":
446
+ if not _validate_output_arg(output_path=args.output, output_format=args.format):
447
+ return 2
448
+ try:
449
+ exact_rows = exact_groups(
450
+ root,
451
+ min_count=args.min_count,
452
+ top_level_only=args.top_level_only,
453
+ include_canonical=args.include_canonical,
454
+ normalize_local_names=args.normalize_local_names,
455
+ normalize_constants=args.normalize_constants,
456
+ strict=args.strict,
457
+ scan_errors=scan_errors,
458
+ )
459
+ except (RuntimeError, ValueError) as exc:
460
+ print(f"Error: {exc}", file=sys.stderr)
461
+ return 2
462
+ _print_diagnostics(scan_errors, plugin_errors)
463
+ if args.format == "json":
464
+ _emit_json_output(
465
+ to_jsonable(exact_rows),
466
+ scan_errors=scan_errors,
467
+ plugin_errors=plugin_errors,
468
+ output_path=args.output,
469
+ )
470
+ else:
471
+ _print_exact(exact_rows)
472
+ return 0
473
+
474
+ if args.cmd == "near":
475
+ if not _validate_output_arg(output_path=args.output, output_format=args.format):
476
+ return 2
477
+ try:
478
+ near_rows = near_matches(
479
+ root,
480
+ threshold=args.threshold,
481
+ top_k=args.top_k,
482
+ top_level_only=args.top_level_only,
483
+ strict=args.strict,
484
+ scan_errors=scan_errors,
485
+ plugin_errors=plugin_errors,
486
+ )
487
+ except (RuntimeError, ValueError) as exc:
488
+ print(f"Error: {exc}", file=sys.stderr)
489
+ return 2
490
+ _print_diagnostics(scan_errors, plugin_errors)
491
+ if args.format == "json":
492
+ _emit_json_output(
493
+ to_jsonable(near_rows),
494
+ scan_errors=scan_errors,
495
+ plugin_errors=plugin_errors,
496
+ output_path=args.output,
497
+ )
498
+ else:
499
+ _print_near(near_rows)
500
+ return 0
501
+
502
+ if args.cmd == "abstract":
503
+ if not _validate_output_arg(output_path=args.output, output_format=args.format):
504
+ return 2
505
+ try:
506
+ abstract_rows = abstract_candidates(
507
+ root,
508
+ threshold=args.threshold,
509
+ top_k=args.top_k,
510
+ top_level_only=args.top_level_only,
511
+ strict=args.strict,
512
+ scan_errors=scan_errors,
513
+ plugin_errors=plugin_errors,
514
+ )
515
+ except (RuntimeError, ValueError) as exc:
516
+ print(f"Error: {exc}", file=sys.stderr)
517
+ return 2
518
+ _print_diagnostics(scan_errors, plugin_errors)
519
+ if args.format == "json":
520
+ _emit_json_output(
521
+ to_jsonable(abstract_rows),
522
+ scan_errors=scan_errors,
523
+ plugin_errors=plugin_errors,
524
+ output_path=args.output,
525
+ )
526
+ else:
527
+ _print_near(abstract_rows)
528
+ return 0
529
+
530
+ if args.cmd in {"showcase", "simulate"}:
531
+ if not _validate_output_arg(output_path=args.output, output_format=args.format):
532
+ return 2
533
+ exact_scan_errors: list[str] = []
534
+ near_scan_errors: list[str] = []
535
+ near_plugin_errors: list[str] = []
536
+ try:
537
+ exact_rows = exact_groups(
538
+ root,
539
+ min_count=2,
540
+ top_level_only=args.top_level_only,
541
+ include_canonical=False,
542
+ normalize_local_names=True,
543
+ normalize_constants=True,
544
+ strict=args.strict,
545
+ scan_errors=exact_scan_errors,
546
+ )
547
+ near_rows = near_matches(
548
+ root,
549
+ threshold=args.threshold,
550
+ top_k=None,
551
+ top_level_only=args.top_level_only,
552
+ strict=args.strict,
553
+ scan_errors=near_scan_errors,
554
+ plugin_errors=near_plugin_errors,
555
+ )
556
+ except (RuntimeError, ValueError) as exc:
557
+ print(f"Error: {exc}", file=sys.stderr)
558
+ return 2
559
+
560
+ abstract_rows = [
561
+ row for row in near_rows if row.suggested_refactor_kind != "leave_separate"
562
+ ]
563
+ combined_scan_errors = _dedupe_messages([*exact_scan_errors, *near_scan_errors])
564
+ combined_plugin_errors = _dedupe_messages(near_plugin_errors)
565
+ _print_diagnostics(combined_scan_errors, combined_plugin_errors)
566
+
567
+ payload = _showcase_payload(
568
+ root=root,
569
+ threshold=args.threshold,
570
+ top_k=args.top_k,
571
+ exact_rows=exact_rows,
572
+ near_rows=near_rows,
573
+ abstract_rows=abstract_rows,
574
+ )
575
+ if args.format == "json":
576
+ _emit_json_output(
577
+ payload,
578
+ scan_errors=combined_scan_errors,
579
+ plugin_errors=combined_plugin_errors,
580
+ output_path=args.output,
581
+ )
582
+ else:
583
+ _print_showcase(payload)
584
+ return 0
585
+
586
+ if args.cmd == "report":
587
+ if not _validate_output_arg(output_path=args.output, output_format=args.format):
588
+ return 2
589
+ report_exact_scan_errors: list[str] = []
590
+ report_near_scan_errors: list[str] = []
591
+ report_near_plugin_errors: list[str] = []
592
+ try:
593
+ exact_rows = exact_groups(
594
+ root,
595
+ min_count=2,
596
+ top_level_only=args.top_level_only,
597
+ include_canonical=False,
598
+ normalize_local_names=args.normalize_local_names,
599
+ normalize_constants=args.normalize_constants,
600
+ strict=args.strict,
601
+ scan_errors=report_exact_scan_errors,
602
+ )
603
+ near_rows = near_matches(
604
+ root,
605
+ threshold=args.threshold,
606
+ top_k=args.top_k,
607
+ top_level_only=args.top_level_only,
608
+ strict=args.strict,
609
+ scan_errors=report_near_scan_errors,
610
+ plugin_errors=report_near_plugin_errors,
611
+ )
612
+ except (RuntimeError, ValueError) as exc:
613
+ print(f"Error: {exc}", file=sys.stderr)
614
+ return 2
615
+
616
+ abstract_rows = [
617
+ row for row in near_rows if row.suggested_refactor_kind != "leave_separate"
618
+ ]
619
+ report_scan_errors = _dedupe_messages(
620
+ [*report_exact_scan_errors, *report_near_scan_errors]
621
+ )
622
+ report_plugin_errors = _dedupe_messages(report_near_plugin_errors)
623
+ _print_diagnostics(report_scan_errors, report_plugin_errors)
624
+ payload = _report_payload(
625
+ root=root,
626
+ threshold=args.threshold,
627
+ top_k=args.top_k,
628
+ normalize_local_names=args.normalize_local_names,
629
+ normalize_constants=args.normalize_constants,
630
+ exact_rows=exact_rows,
631
+ near_rows=near_rows,
632
+ abstract_rows=abstract_rows,
633
+ )
634
+ _emit_json_output(
635
+ payload,
636
+ scan_errors=report_scan_errors,
637
+ plugin_errors=report_plugin_errors,
638
+ output_path=args.output,
639
+ )
640
+ return 0
641
+
642
+ return 1
643
+
644
+
645
+ if __name__ == "__main__":
646
+ raise SystemExit(main())