From c8eb56139ab2bf70994da3429c7286d6db700718 Mon Sep 17 00:00:00 2001 From: ambujsingh Date: Thu, 21 May 2026 11:55:22 +0530 Subject: [PATCH] Added code analysis reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consolidate Python py_binary (tools:generate_code_analysis) - Fix clang-tidy aspect label: //:tools/lint → //tools:lint (subpackage fix) - fix deployment consistency - Inject default_custom.css + version_flyout CSS/JS into old version tarballs - Add shared assets (_shared/) for cross-version CSS/JS consistency --- .github/workflows/deploy_docs.yml | 40 ++ bazel/toolchains/template/conf.template.py | 5 + docs/sphinx/BUILD | 8 +- docs/sphinx/_static/codeanalysis/.gitignore | 2 + docs/sphinx/code_analysis.rst | 15 + docs/sphinx/code_analysis/clang_tidy.rst | 14 + docs/sphinx/code_analysis/codeql.rst | 14 + docs/sphinx/code_analysis/coverage.rst | 14 + docs/sphinx/index.rst | 1 + .../static_analysis/static_analysis.bazelrc | 2 +- tools/BUILD | 19 + .../generate_code_analysis.cpython-312.pyc | Bin 0 -> 23110 bytes tools/generate_code_analysis.py | 493 ++++++++++++++++++ 13 files changed, 624 insertions(+), 3 deletions(-) create mode 100644 docs/sphinx/_static/codeanalysis/.gitignore create mode 100644 docs/sphinx/code_analysis.rst create mode 100644 docs/sphinx/code_analysis/clang_tidy.rst create mode 100644 docs/sphinx/code_analysis/codeql.rst create mode 100644 docs/sphinx/code_analysis/coverage.rst create mode 100644 tools/BUILD create mode 100644 tools/__pycache__/generate_code_analysis.cpython-312.pyc create mode 100644 tools/generate_code_analysis.py diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 6dd11c614..cadad5775 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -22,6 +22,10 @@ on: branches: [main] paths: - 'docs/**' + - 'tools/generate_code_analysis.py' + - 'tools/BUILD' + - 'quality/coverage.bazelrc' + - 'quality/static_analysis/**' - '.github/workflows/deploy_docs.yml' workflow_dispatch: @@ -73,6 +77,23 @@ jobs: echo "should_deploy=true" >> "$GITHUB_OUTPUT" fi + - name: Install lcov + run: | + sudo apt-get update + sudo apt-get install -y lcov + + - name: Generate Coverage Report + run: | + bazel run //tools:generate_code_analysis -- --coverage + + - name: Generate CodeQL Report + run: | + bazel run //tools:generate_code_analysis -- --codeql + + - name: Generate Clang-Tidy Report + run: | + bazel run //tools:generate_code_analysis -- --clang-tidy + - name: Build Sphinx documentation env: DOCS_BASE_URL: "https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" @@ -149,6 +170,24 @@ jobs: fi done + # Inject shared CSS/JS into old versions that lack them + REPO_NAME="${{ github.event.repository.name }}" + CUSTOM_CSS="" + FLYOUT_CSS="" + FLYOUT_JS="" + for dir in publish/v*/; do + [ -d "$dir" ] || continue + while IFS= read -r -d '' f; do + if ! grep -q 'default_custom' "$f"; then + sed -i "s||${CUSTOM_CSS}\n|" "$f" + fi + if ! grep -q 'version_flyout' "$f"; then + sed -i "s||${FLYOUT_CSS}\n|" "$f" + sed -i "s||${FLYOUT_JS}\n|" "$f" + fi + done < <(find "$dir" -name '*.html' -print0) + done + # stable = newest tagged version that has docs if [[ "${IS_TAG}" == "true" ]]; then rm -rf publish/stable @@ -163,6 +202,7 @@ jobs: # Shared assets mkdir -p publish/_shared/css publish/_shared/js + cp docs/sphinx/_static/css/default_custom.css publish/_shared/css/ cp docs/sphinx/_static/css/version_flyout.css publish/_shared/css/ cp docs/sphinx/_static/js/version_flyout.js publish/_shared/js/ diff --git a/bazel/toolchains/template/conf.template.py b/bazel/toolchains/template/conf.template.py index ae8c80deb..fdfaf91ab 100644 --- a/bazel/toolchains/template/conf.template.py +++ b/bazel/toolchains/template/conf.template.py @@ -73,6 +73,11 @@ # -- Options for HTML output -- html_theme = 'pydata_sphinx_theme' +html_static_path = ["_static"] +html_css_files = [ + "css/default_custom.css", +] +html_js_files = [] # Professional theme configuration inspired by modern open-source projects html_theme_options = { diff --git a/docs/sphinx/BUILD b/docs/sphinx/BUILD index b0267a098..314a76d77 100644 --- a/docs/sphinx/BUILD +++ b/docs/sphinx/BUILD @@ -26,7 +26,7 @@ sphinx_docs_library( ) # Static assets for Sphinx documentation -filegroup( +sphinx_docs_library( name = "static_assets", srcs = glob(["_static/**/*"]), ) @@ -52,6 +52,10 @@ sphinx_module( testonly = True, srcs = [ + "code_analysis.rst", + "code_analysis/clang_tidy.rst", + "code_analysis/codeql.rst", + "code_analysis/coverage.rst", "how_to_document.rst", "index.rst", "introduction.rst", @@ -59,9 +63,9 @@ sphinx_module( "safety_reports.rst", ":doxygen_xml", ":generate_api_rst", - ":static_assets", ], docs_library_deps = [ + ":static_assets", "//score/mw/com:readme_md", ], exec_compatible_with = ["@platforms//os:linux"], diff --git a/docs/sphinx/_static/codeanalysis/.gitignore b/docs/sphinx/_static/codeanalysis/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/docs/sphinx/_static/codeanalysis/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docs/sphinx/code_analysis.rst b/docs/sphinx/code_analysis.rst new file mode 100644 index 000000000..08b96b0f6 --- /dev/null +++ b/docs/sphinx/code_analysis.rst @@ -0,0 +1,15 @@ +Code Analysis Reports +===================== + +This section links to locally generated code-quality reports for coverage, +CodeQL, and clang-tidy. + +.. note:: + Run ``tools/generate_code_analysis.sh`` to generate reports locally. + +.. toctree:: + :maxdepth: 1 + + code_analysis/coverage + code_analysis/codeql + code_analysis/clang_tidy diff --git a/docs/sphinx/code_analysis/clang_tidy.rst b/docs/sphinx/code_analysis/clang_tidy.rst new file mode 100644 index 000000000..7a4f13c2c --- /dev/null +++ b/docs/sphinx/code_analysis/clang_tidy.rst @@ -0,0 +1,14 @@ +Clang-Tidy Report +================= + +clang-tidy analysis runs via ``bazel test --config=clang-tidy`` and its lint +outputs are collected into an HTML summary. + +.. note:: + Run ``tools/generate_code_analysis.sh --clang-tidy`` to generate reports locally. + +Generated HTML report: + +`Open clang-tidy report <../_static/codeanalysis/clang_tidy/index.html>`_ + +If the report has not been generated yet, the link points to a placeholder page. diff --git a/docs/sphinx/code_analysis/codeql.rst b/docs/sphinx/code_analysis/codeql.rst new file mode 100644 index 000000000..8e26afdb5 --- /dev/null +++ b/docs/sphinx/code_analysis/codeql.rst @@ -0,0 +1,14 @@ +CodeQL Report +============= + +CodeQL analysis runs via ``//quality/static_analysis:codeql_lint`` and produces +SARIF output that is converted to an HTML summary. + +.. note:: + Run ``tools/generate_code_analysis.sh --codeql`` to generate reports locally. + +Generated HTML report: + +`Open CodeQL report <../_static/codeanalysis/codeql/index.html>`_ + +If the report has not been generated yet, the link points to a placeholder page. diff --git a/docs/sphinx/code_analysis/coverage.rst b/docs/sphinx/code_analysis/coverage.rst new file mode 100644 index 000000000..a578e85ac --- /dev/null +++ b/docs/sphinx/code_analysis/coverage.rst @@ -0,0 +1,14 @@ +Coverage Report +=============== + +Coverage is generated from ``bazel coverage //...`` and rendered with +``genhtml``. + +.. note:: + Run ``tools/generate_code_analysis.sh --coverage`` to generate reports locally. + +Generated HTML report: + +`Open coverage report <../_static/codeanalysis/coverage/index.html>`_ + +If the report has not been generated yet, the link points to a placeholder page. diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index 1d6e7ba66..5dd8ef7ee 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -11,6 +11,7 @@ including the LoLa (Low Latency) implementation and Message Passing library. introduction README message_passing + code_analysis how_to_document .. toctree:: diff --git a/quality/static_analysis/static_analysis.bazelrc b/quality/static_analysis/static_analysis.bazelrc index 6257dab35..03fa9bf72 100644 --- a/quality/static_analysis/static_analysis.bazelrc +++ b/quality/static_analysis/static_analysis.bazelrc @@ -13,7 +13,7 @@ # Clang-tidy configuration # Run clang-tidy on all C++ targets with: bazel test --config=clang-tidy //... -test:clang-tidy --aspects=//:tools/lint/linters.bzl%clang_tidy_aspect +test:clang-tidy --aspects=//tools:lint/linters.bzl%clang_tidy_aspect test:clang-tidy --output_groups=+rules_lint_report # Use LLVM toolchain for clang-tidy so it can find system headers test:clang-tidy --extra_toolchains=@llvm_toolchain//:cc-toolchain-x86_64-linux diff --git a/tools/BUILD b/tools/BUILD new file mode 100644 index 000000000..2d4283f3d --- /dev/null +++ b/tools/BUILD @@ -0,0 +1,19 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +py_binary( + name = "generate_code_analysis", + srcs = ["generate_code_analysis.py"], + main = "generate_code_analysis.py", + visibility = ["//visibility:public"], +) diff --git a/tools/__pycache__/generate_code_analysis.cpython-312.pyc b/tools/__pycache__/generate_code_analysis.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7dddb5812e36068704421850d2bb9eb7f4b6846 GIT binary patch literal 23110 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 + + + + + {title} + + + +
+

{title}

+

{message}

+

Run tools/generate_code_analysis.sh to generate this report.

+
+ + +""", + encoding="utf-8", + ) + + +def safe_filename(path, root_dir, output_path): + """Create a safe filename from a path, truncating with hash if too long.""" + name = path + for prefix in [str(root_dir) + "/", output_path + "/", "/"]: + if name.startswith(prefix): + name = name[len(prefix):] + name = name.replace("/", "__").replace(":", "_") + if len(name) > MAX_FILENAME_LEN: + h = hashlib.sha1(name.encode()).hexdigest()[:12] + name = name[: MAX_FILENAME_LEN - 13] + "_" + h + return name + + +# --- Target Resolution --- + + +def resolve_linux_test_target(label): + """Resolve a test target to its _linux variant if it exists.""" + if label.endswith("_linux") or label.endswith("_qnx"): + return label + if "::" not in label and ":" in label and label.startswith("//"): + linux_label = label + "_linux" + result = run_bazel("query", linux_label) + if result.returncode == 0: + return linux_label + return label + + +def resolve_coverage_targets(patterns): + """Expand and resolve target patterns for coverage.""" + resolved = [] + seen = set() + + for pattern in patterns: + if "..." in pattern or ":all" in pattern or ":*" in pattern: + result = run_bazel("query", f"tests({pattern})") + expanded = result.stdout.strip().split("\n") if result.returncode == 0 else [] + expanded = [t for t in expanded if t] + else: + expanded = [pattern] + + if not expanded: + expanded = [pattern] + + for label in expanded: + resolved_label = resolve_linux_test_target(label) + if resolved_label not in seen: + seen.add(resolved_label) + resolved.append(resolved_label) + + return resolved + + +# --- Coverage Report --- + + +def parse_lcov(coverage_dat): + """Parse LCOV data file.""" + files = [] + current = None + + with open(coverage_dat, "r", encoding="utf-8", errors="replace") as f: + for raw_line in f: + line = raw_line.strip() + if line.startswith("SF:"): + if current: + files.append(current) + current = {"path": line[3:], "covered": 0, "total": 0, "has_da": False} + elif current is None: + continue + elif line.startswith("DA:"): + current["has_da"] = True + current["total"] += 1 + try: + if int(line.split(",", 1)[1]) > 0: + current["covered"] += 1 + except (ValueError, IndexError): + pass + elif line.startswith("LF:") and not current["has_da"]: + try: + current["total"] = int(line[3:]) + except ValueError: + pass + elif line.startswith("LH:") and not current["has_da"]: + try: + current["covered"] = int(line[3:]) + except ValueError: + pass + elif line == "end_of_record": + files.append(current) + current = None + + if current: + files.append(current) + return [e for e in files if e["total"] > 0] + + +def render_coverage_html(files): + """Render coverage HTML from parsed LCOV data.""" + files.sort(key=lambda e: e["path"]) + total_lines = sum(e["total"] for e in files) + covered_lines = sum(e["covered"] for e in files) + coverage_pct = (covered_lines / total_lines * 100.0) if total_lines else 0.0 + + summary_note = ( + "The Bazel-generated LCOV file contains no line coverage counters for the selected targets. " + "The report page is working, but the underlying coverage data is empty." + if total_lines == 0 + else "This is a fallback LCOV summary because genhtml is not available in the current environment." + ) + + rows = [] + for entry in files[:500]: + pct = (entry["covered"] / entry["total"] * 100.0) if entry["total"] else 0.0 + rows.append( + f'{html.escape(entry["path"])}' + f"{entry['covered']}{entry['total']}{pct:.1f}%" + ) + + tbody = "".join(rows) or 'No coverage entries found.' + + return f""" + + + + + Coverage Report + + + +

Coverage Report

+

Covered lines: {covered_lines}/{total_lines} ({coverage_pct:.1f}%)

+

{summary_note}

+ + + {tbody} +
FileCoveredTotalCoverage
+ + +""" + + +def generate_coverage_report(targets): + """Generate the coverage HTML report.""" + out_dir = REPORT_ROOT / "coverage" + coverage_targets = resolve_coverage_targets(targets) + + print(f"[coverage] Running Bazel coverage for targets: {' '.join(coverage_targets)}") + if not run_bazel_checked("coverage", *coverage_targets): + write_placeholder(out_dir, "Coverage Report", f"Bazel coverage failed for targets: {' '.join(targets)}.") + return False + + output_path = get_output_path() + coverage_dat = f"{output_path}/_coverage/_coverage_report.dat" + + if not os.path.isfile(coverage_dat): + write_placeholder(out_dir, "Coverage Report", f"Combined coverage data was not found at {coverage_dat}.") + return True + + shutil.rmtree(out_dir, ignore_errors=True) + os.makedirs(out_dir, exist_ok=True) + + if not shutil.which("genhtml"): + files = parse_lcov(coverage_dat) + (out_dir / "index.html").write_text(render_coverage_html(files), encoding="utf-8") + return True + + result = subprocess.run( + ["genhtml", coverage_dat, "--output-directory", str(out_dir), + "--show-details", "--legend", "--function-coverage", + "--branch-coverage", "--ignore-errors", "category,inconsistent"], + cwd=ROOT_DIR, + ) + if result.returncode != 0: + write_placeholder(out_dir, "Coverage Report", f"genhtml failed while rendering {coverage_dat}.") + return False + + print(f"[coverage] Wrote {out_dir}/index.html") + return True + + +# --- CodeQL Report --- + + +def render_codeql_html(results): + """Render CodeQL SARIF results as HTML.""" + by_level = collections.Counter((r.get("level") or "unknown") for r in results) + + rows = [] + for r in results[:300]: + message = r.get("message", {}).get("text", "") + rule_id = r.get("ruleId", "") + level = r.get("level", "") + file_path, start_line = "", "" + locations = r.get("locations", []) + if locations: + phys = locations[0].get("physicalLocation", {}) + file_path = phys.get("artifactLocation", {}).get("uri", "") + start_line = str(phys.get("region", {}).get("startLine", "")) + rows.append( + f"{html.escape(level)}{html.escape(rule_id)}" + f"{html.escape(file_path)}{html.escape(start_line)}" + f"{html.escape(message)}" + ) + + summary = ", ".join(f"{k}: {v}" for k, v in sorted(by_level.items())) or "no findings" + tbody = "".join(rows) or 'No findings.' + + return f""" + + + + + CodeQL Report + + + +

CodeQL Report

+

Total findings: {len(results)}
By level: {html.escape(summary)}

+

SARIF source: codeql.sarif

+ + + {tbody} +
LevelRuleFileLineMessage
+ + +""" + + +def generate_codeql_report(targets): + """Generate the CodeQL HTML report.""" + out_dir = REPORT_ROOT / "codeql" + codeql_targets = " ".join(targets) + + print(f"[codeql] Running CodeQL lint target for: {codeql_targets}") + if not run_bazel_checked("run", "//quality/static_analysis:codeql_lint", "--", f"--target={codeql_targets}"): + write_placeholder(out_dir, "CodeQL Report", f"CodeQL analysis failed for targets: {codeql_targets}.") + return False + + output_path = get_output_path() + sarif_path = f"{output_path}/codeql.sarif" + + shutil.rmtree(out_dir, ignore_errors=True) + os.makedirs(out_dir, exist_ok=True) + + if not os.path.isfile(sarif_path): + write_placeholder(out_dir, "CodeQL Report", f"CodeQL SARIF output was not found at {sarif_path}.") + return True + + shutil.copy2(sarif_path, out_dir / "codeql.sarif") + + with open(sarif_path, "r", encoding="utf-8") as f: + sarif = json.load(f) + + results = [] + for run in sarif.get("runs", []): + results.extend(run.get("results", [])) + + (out_dir / "index.html").write_text(render_codeql_html(results), encoding="utf-8") + print(f"[codeql] Wrote {out_dir}/index.html") + return True + + +# --- Clang-Tidy Report --- + + +def render_clang_tidy_html(raw_dir): + """Render clang-tidy report files as HTML.""" + reports = sorted(glob.glob(os.path.join(raw_dir, "*"))) + + rows = [] + for report in reports: + name = os.path.basename(report) + try: + with open(report, "r", encoding="utf-8", errors="replace") as f: + excerpt = "".join(f.readlines()[:40]) + except OSError: + excerpt = "Failed to read report file." + rows.append( + f"{html.escape(name)}
{html.escape(excerpt)}
" + ) + + if not reports: + body = "

No clang-tidy report artifacts were found. Run the generator locally and inspect Bazel outputs.

" + else: + body = ( + f"

Collected files: {len(reports)}

" + "" + + "".join(rows) + "
FileExcerpt
" + ) + + return f""" + + + + + Clang-Tidy Report + + + +

Clang-Tidy Report

+ {body} + + +""" + + +def generate_clang_tidy_report(targets): + """Generate the clang-tidy HTML report.""" + out_dir = REPORT_ROOT / "clang_tidy" + + print(f"[clang-tidy] Running clang-tidy via Bazel for targets: {' '.join(targets)}") + if not run_bazel_checked("test", "--config=clang-tidy", *targets, "--output_groups=+rules_lint_report"): + write_placeholder(out_dir, "Clang-Tidy Report", f"clang-tidy failed for targets: {' '.join(targets)}.") + return False + + output_path = get_output_path() + + shutil.rmtree(out_dir, ignore_errors=True) + raw_dir = out_dir / "raw" + os.makedirs(raw_dir, exist_ok=True) + + # Collect clang-tidy report files + search_dirs = [ROOT_DIR / "bazel-bin", ROOT_DIR / "bazel-testlogs", Path(output_path)] + for search_dir in search_dirs: + if not search_dir.exists(): + continue + for report in search_dir.rglob("*"): + if not report.is_file(): + continue + name_lower = report.name.lower() + # Match clang-tidy or lint report files + name_match = ("clang" in name_lower and "tidy" in name_lower) or \ + ("lint" in name_lower and "report" in name_lower) or \ + ".clang-tidy" in name_lower + ext_match = name_lower.endswith((".txt", ".log", ".out", ".report", ".json")) + if name_match and ext_match: + dest_name = safe_filename(str(report), str(ROOT_DIR), output_path) + shutil.copy2(report, raw_dir / dest_name) + + (out_dir / "index.html").write_text(render_clang_tidy_html(str(raw_dir)), encoding="utf-8") + print(f"[clang-tidy] Wrote {out_dir}/index.html") + return True + + +# --- Main --- + + +def main(): + parser = argparse.ArgumentParser(description="Generate code analysis reports.") + parser.add_argument("--coverage", action="store_true", help="Generate coverage report") + parser.add_argument("--codeql", action="store_true", help="Generate CodeQL report") + parser.add_argument("--clang-tidy", action="store_true", help="Generate clang-tidy report") + parser.add_argument("--all-targets", action="store_true", help="Analyze full workspace (//...)") + parser.add_argument("--targets", type=str, default=None, help="Comma-separated Bazel target patterns") + args = parser.parse_args() + + # If nothing specified, run all + if not args.coverage and not args.codeql and not args.clang_tidy: + args.coverage = args.codeql = args.clang_tidy = True + + targets = DEFAULT_TARGETS + if args.targets: + targets = [t.strip() for t in args.targets.split(",")] + if args.all_targets: + targets = ["//..."] + + os.makedirs(REPORT_ROOT, exist_ok=True) + status = 0 + + if args.coverage: + if not generate_coverage_report(targets): + status = 1 + + if args.codeql: + if not generate_codeql_report(targets): + status = 1 + + if args.clang_tidy: + if not generate_clang_tidy_report(targets): + status = 1 + + print(f"Reports available under {REPORT_ROOT}") + sys.exit(status) + + +if __name__ == "__main__": + main()