|
7 | 7 | import json |
8 | 8 | import subprocess |
9 | 9 | import sys |
| 10 | +import os |
| 11 | +import math |
| 12 | +import concurrent.futures |
10 | 13 | from pathlib import Path |
11 | 14 | from typing import Iterable |
12 | 15 |
|
@@ -380,38 +383,80 @@ def main() -> int: |
380 | 383 | ensure_parent(Path(args.sarif_out)) |
381 | 384 | sarif_out_path = str(Path(args.sarif_out).resolve()) |
382 | 385 |
|
383 | | - print(f"Running analyzer on {len(selected_inputs)} file(s).") |
384 | | - cmd = analyzer_cmd( |
385 | | - analyzer=analyzer, |
386 | | - inputs=selected_inputs, |
387 | | - fmt="json", |
388 | | - compdb_path=compdb_path, |
389 | | - base_dir=args.base_dir, |
390 | | - extra_args=args.analyzer_arg, |
391 | | - sarif_out=sarif_out_path, |
392 | | - ) |
393 | | - run = subprocess.run(cmd, check=False, capture_output=True, text=True) |
394 | | - if run.returncode != 0: |
395 | | - if run.stdout: |
396 | | - sys.stdout.write(run.stdout) |
397 | | - if run.stderr: |
398 | | - sys.stderr.write(run.stderr) |
399 | | - return run.returncode |
400 | | - |
401 | | - try: |
402 | | - payload = json.loads(run.stdout) |
403 | | - except json.JSONDecodeError as exc: |
404 | | - print(f"Analyzer returned invalid JSON: {exc}", file=sys.stderr) |
| 386 | + jobs = int(os.environ.get("ANALYZER_JOBS", os.cpu_count() or 1)) |
| 387 | + chunk_size = max(1, math.ceil(len(selected_inputs) / jobs)) |
| 388 | + chunks = [selected_inputs[i:i + chunk_size] for i in range(0, len(selected_inputs), chunk_size)] |
| 389 | + |
| 390 | + print(f"Running analyzer on {len(selected_inputs)} file(s) across {len(chunks)} job(s).") |
| 391 | + |
| 392 | + def run_chunk(i, chunk): |
| 393 | + chunk_sarif = f"{sarif_out_path}.chunk{i}" if sarif_out_path else None |
| 394 | + |
| 395 | + cmd = analyzer_cmd( |
| 396 | + analyzer=analyzer, |
| 397 | + inputs=chunk, |
| 398 | + fmt="json", |
| 399 | + compdb_path=compdb_path, |
| 400 | + base_dir=args.base_dir, |
| 401 | + extra_args=args.analyzer_arg, |
| 402 | + sarif_out=chunk_sarif, |
| 403 | + ) |
| 404 | + run = subprocess.run(cmd, check=False, capture_output=True, text=True) |
| 405 | + return i, run, chunk_sarif |
| 406 | + |
| 407 | + diags = [] |
| 408 | + has_error = False |
| 409 | + all_sarif_files = [] |
| 410 | + |
| 411 | + with concurrent.futures.ThreadPoolExecutor(max_workers=jobs) as executor: |
| 412 | + futures = [executor.submit(run_chunk, i, c) for i, c in enumerate(chunks)] |
| 413 | + for fut in concurrent.futures.as_completed(futures): |
| 414 | + i, run, chunk_sarif = fut.result() |
| 415 | + |
| 416 | + if chunk_sarif and os.path.exists(chunk_sarif): |
| 417 | + all_sarif_files.append(chunk_sarif) |
| 418 | + |
| 419 | + if run.returncode != 0: |
| 420 | + if run.stdout: |
| 421 | + sys.stdout.write(run.stdout) |
| 422 | + if run.stderr: |
| 423 | + sys.stderr.write(run.stderr) |
| 424 | + has_error = True |
| 425 | + else: |
| 426 | + try: |
| 427 | + payload = json.loads(run.stdout) |
| 428 | + d = payload.get("diagnostics", []) |
| 429 | + if isinstance(d, list): |
| 430 | + diags.extend(d) |
| 431 | + except json.JSONDecodeError as exc: |
| 432 | + print(f"Analyzer returned invalid JSON: {exc}", file=sys.stderr) |
| 433 | + has_error = True |
| 434 | + |
| 435 | + if has_error: |
405 | 436 | return 2 |
406 | 437 |
|
| 438 | + if sarif_out_path and all_sarif_files: |
| 439 | + merged = None |
| 440 | + for p in all_sarif_files: |
| 441 | + with open(p, 'r') as f: |
| 442 | + try: |
| 443 | + data = json.load(f) |
| 444 | + if merged is None: |
| 445 | + merged = data |
| 446 | + else: |
| 447 | + if data.get("runs") and merged.get("runs"): |
| 448 | + merged["runs"][0].setdefault("results", []).extend(data["runs"][0].get("results", [])) |
| 449 | + except json.JSONDecodeError: |
| 450 | + pass |
| 451 | + os.unlink(p) |
| 452 | + if merged: |
| 453 | + with open(sarif_out_path, 'w') as f: |
| 454 | + json.dump(merged, f) |
| 455 | + |
407 | 456 | if args.json_out: |
408 | 457 | json_output_path = Path(args.json_out) |
409 | 458 | ensure_parent(json_output_path) |
410 | | - json_output_path.write_text(run.stdout, encoding="utf-8") |
411 | | - |
412 | | - diags = payload.get("diagnostics", []) |
413 | | - if not isinstance(diags, list): |
414 | | - diags = [] |
| 459 | + json_output_path.write_text(json.dumps({"diagnostics": diags}, indent=2), encoding="utf-8") |
415 | 460 |
|
416 | 461 | errors = sum(1 for d in diags if sev(d) == "ERROR") |
417 | 462 | warnings = sum(1 for d in diags if sev(d) == "WARNING") |
|
0 commit comments